Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480import { Param, Primitive } from "effect/unstable/cli"import { Spec } from "../framework/spec.ts"
type FlagInfo = { readonly takesValue: boolean}
type WordResult = | { readonly _tag: "Success"; readonly words: ReadonlyArray<string> } | { readonly _tag: "Failure"; readonly reason: string }
export interface PreparedArguments { readonly args: ReadonlyArray<string> readonly warnings: ReadonlyArray<string>}
type Suppression = { readonly canonical: ReadonlyMap<string, string> readonly explicit: ReadonlySet<string> readonly partners: ReadonlyMap<string, ReadonlySet<string>>}
export function prepareArguments( commands: Spec.Any, args: ReadonlyArray<string>, environment: Readonly<Record<string, string | undefined>>, globalParams: ReadonlyArray<Param.Any>, conflicts: ReadonlyArray<readonly [string, string]> = [],): PreparedArguments { if (!environment.OPENCODE_ARGS && !environment.OPENCODE_TUI_ARGS) { return { args, warnings: [] } }
const warnings: string[] = [] const route = commandRoute(commands, args, globalParams) const sources: Array<ReadonlyArray<string>> = [] const knownFlags = flagsInTree(commands, globalParams) const supportedFlags = flagsFor(route, globalParams) const suppression: Suppression = { canonical: canonicalFlagsFor(route, globalParams), explicit: explicitFlagsIn(args, commands, globalParams), partners: conflictPartners(conflicts), }
// TUI-scoped defaults are inactive outside the root TUI and `mini` routes. if (environment.OPENCODE_TUI_ARGS && (route === commands || route.name === "mini")) { addSource( "OPENCODE_TUI_ARGS", environment.OPENCODE_TUI_ARGS, supportedFlags, knownFlags, suppression, sources, warnings, ) }
if (environment.OPENCODE_ARGS) { addSource("OPENCODE_ARGS", environment.OPENCODE_ARGS, supportedFlags, knownFlags, suppression, sources, warnings) }
const defaults = sources.flatMap((source) => source) const separator = args.indexOf("--") // Explicit arguments also suppress their environment counterparts below, so // appended defaults never defeat a caller's choice even if a parser ever // resolves repeated flags by last value instead of first. const effective = separator === -1 ? [...args, ...defaults] : [...args.slice(0, separator), ...defaults, ...args.slice(separator)]
return { args: effective, warnings }}
function addSource( name: string, value: string, supported: ReadonlyMap<string, FlagInfo>, known: ReadonlyMap<string, FlagInfo>, suppression: Suppression, sources: Array<ReadonlyArray<string>>, warnings: string[],) { const parsed = shellWords(value) if (parsed._tag === "Failure") { warnings.push(`${name} ignored: invalid shell-word syntax (${parsed.reason}).`) return }
const filtered = filterEnvironmentWords(parsed.words, supported, known, suppression) if (filtered.ignored.length) { warnings.push(`${name} dropped unsupported arguments: ${filtered.ignored.join(", ")}.`) } if (filtered.suppressed.length) { warnings.push(`${name} dropped defaults overruled by explicit arguments: ${filtered.suppressed.join(", ")}.`) } if (filtered.words.length) sources.push(filtered.words)}
function commandRoute(root: Spec.Any, args: ReadonlyArray<string>, globals: ReadonlyArray<Param.Any>) { let current = root let index = 0
while (index < args.length) { const token = args[index] if (token === "--") break
if (token.startsWith("-") && token !== "-") { const flags = flagsFor(current, globals) if (takesNextToken(token, flags) && !token.includes("=")) index++ index++ continue }
const child = [...Object.values(current.commands), ...current.aliases].find((item) => item.name === token) if (!child) break current = child index++ }
return current}
function flagsFor(command: Spec.Any, globals: ReadonlyArray<Param.Any>) { return flagsFromParams([...paramsIn(command.params), ...globals])}
function flagsInTree(root: Spec.Any, globals: ReadonlyArray<Param.Any>) { const output = flagsFromParams(globals) const seen = new Set<Spec.Any>()
function visit(command: Spec.Any) { if (seen.has(command)) return seen.add(command) for (const [name, info] of flagsFromParams(paramsIn(command.params))) { const previous = output.get(name) output.set(name, { takesValue: info.takesValue || previous?.takesValue === true, }) } Object.values(command.commands).forEach(visit) command.aliases.forEach(visit) }
visit(root) return output}
// Resolve every accepted option spelling (long form, `--no-` negation, short// alias) to the primary `--name` flag so explicit-set detection and environment// suppression compare the same identity regardless of spelling.function canonicalFlagsFor(command: Spec.Any, globals: ReadonlyArray<Param.Any>) { const output = new Map<string, string>()
for (const param of [...paramsIn(command.params), ...globals]) { for (const single of singlesIn(param)) { if (single.kind !== Param.flagKind) continue const primary = `--${single.name}` const takesValue = single.primitiveType !== Primitive.boolean output.set(primary, primary) if (!takesValue) output.set(`--no-${single.name}`, primary) for (const alias of single.aliases) { output.set(alias.length === 1 ? `-${alias}` : `--${alias}`, primary) } } }
return output}
// Collect flags the caller set explicitly, resolving aliases and negations to// primary names. Only tokens before `--` count; trailing operands are data.function explicitFlagsIn( args: ReadonlyArray<string>, root: Spec.Any, globals: ReadonlyArray<Param.Any>,): ReadonlySet<string> { const explicit = new Set<string>() let current = root let canonical: ReadonlyMap<string, string> | undefined let index = 0
while (index < args.length) { const token = args[index] if (token === "--") break
if (token.startsWith("-") && token !== "-") { canonical ??= canonicalFlagsFor(current, globals) const flags = flagsFor(current, globals) const equal = token.indexOf("=") if (token.startsWith("--")) { const option = equal === -1 ? token : token.slice(0, equal) const info = flags.get(option) if (info) { const primary = canonical.get(option) if (primary) explicit.add(primary) if (equal === -1 && info.takesValue) index++ } index++ continue }
const cluster = token.slice(1, equal === -1 ? undefined : equal) const characters = [...cluster] for (let position = 0; position < characters.length; position++) { const option = `-${characters[position]}` const info = flags.get(option) if (!info) continue const primary = canonical.get(option) if (primary) explicit.add(primary) if (info.takesValue) { if (position === characters.length - 1) index++ break } } index++ continue }
const child = [...Object.values(current.commands), ...current.aliases].find((item) => item.name === token) if (!child) break current = child canonical = undefined index++ }
return explicit}
function conflictPartners(conflicts: ReadonlyArray<readonly [string, string]>) { const partners = new Map<string, Set<string>>()
function link(left: string, right: string) { let set = partners.get(left) if (!set) { set = new Set() partners.set(left, set) } set.add(right) }
for (const [left, right] of conflicts) { link(`--${left}`, `--${right}`) link(`--${right}`, `--${left}`) }
return partners}
// Returns "explicit" when the flag itself was set, a partner flag name when a// declared conflict was set, or undefined when the default may stand.function suppressionReason( primary: string, explicit: ReadonlySet<string>, partners: ReadonlyMap<string, ReadonlySet<string>>,) { if (explicit.has(primary)) return "explicit" for (const partner of partners.get(primary) ?? []) { if (explicit.has(partner)) return partner } return undefined}
function paramsIn(config: unknown): Array<Param.Any> { if (Param.isParam(config)) return [config] if (Array.isArray(config)) return config.flatMap(paramsIn) if (config && typeof config === "object") return Object.values(config).flatMap(paramsIn) return []}
function flagsFromParams(params: ReadonlyArray<Param.Any>) { const output = new Map<string, FlagInfo>()
for (const param of params) { for (const single of singlesIn(param)) { if (single.kind !== Param.flagKind) continue const info = { takesValue: single.primitiveType !== Primitive.boolean } output.set(`--${single.name}`, info) if (!info.takesValue) output.set(`--no-${single.name}`, info) for (const alias of single.aliases) { output.set(alias.length === 1 ? `-${alias}` : `--${alias}`, info) } } }
return output}
function singlesIn(param: Param.Any): Array<Param.Single<Param.ParamKind, unknown>> { if (Param.isSingle(param)) return [param]
if (param._tag === "Map") return singlesIn((param as Param.Map<Param.ParamKind, unknown, unknown>).param) if (param._tag === "Transform") { const transform = param as Param.Transform<Param.ParamKind, unknown, unknown> return [...singlesIn(transform.param), ...transform.alternatives.flatMap((alternative) => singlesIn(alternative()))] } if (param._tag === "Optional") return singlesIn((param as Param.Optional<Param.ParamKind, unknown>).param) if (param._tag === "Variadic") return singlesIn((param as Param.Variadic<Param.ParamKind, unknown>).param) return []}
function takesNextToken(token: string, flags: ReadonlyMap<string, FlagInfo>) { if (token.startsWith("--")) return flags.get(token.split("=", 1)[0])?.takesValue === true const cluster = token.slice(1).split("=", 1)[0] return [...cluster].some((flag) => flags.get(`-${flag}`)?.takesValue === true)}
function filterEnvironmentWords( words: ReadonlyArray<string>, supported: ReadonlyMap<string, FlagInfo>, known: ReadonlyMap<string, FlagInfo>, suppression: Suppression,) { const kept: string[] = [] const ignored = new Set<string>() const suppressedSet = new Set<string>()
for (let index = 0; index < words.length; index++) { const word = words[index] if (word === "--") { ignored.add("--") break }
if (word.startsWith("--")) { const equal = word.indexOf("=") const option = equal === -1 ? word : word.slice(0, equal) const accepted = supported.get(option) if (!accepted) { ignored.add(option) if (equal === -1 && known.get(option)?.takesValue && words[index + 1] && !words[index + 1].startsWith("-")) { index++ } continue }
const primary = suppression.canonical.get(option) ?? option const reason = suppressionReason(primary, suppression.explicit, suppression.partners) if (reason !== undefined) { suppressedSet.add(reason === "explicit" ? option : `${option} (explicit ${reason})`) if (equal === -1 && accepted.takesValue && words[index + 1] !== undefined) index++ continue }
if (equal === -1 && accepted.takesValue && words[index + 1] === undefined) { ignored.add(`${option} (missing value)`) continue } kept.push(word) if (equal === -1 && accepted.takesValue && words[index + 1] !== undefined) kept.push(words[++index]) continue }
if (word.startsWith("-") && word !== "-") { const equal = word.indexOf("=") const cluster = word.slice(1, equal === -1 ? undefined : equal) if (cluster.length === 1) { const option = `-${cluster}` const accepted = supported.get(option) if (!accepted) { ignored.add(option) if (equal === -1 && known.get(option)?.takesValue && words[index + 1] && !words[index + 1].startsWith("-")) { index++ } continue }
const primary = suppression.canonical.get(option) ?? option const reason = suppressionReason(primary, suppression.explicit, suppression.partners) if (reason !== undefined) { suppressedSet.add(reason === "explicit" ? option : `${option} (explicit ${reason})`) if (equal === -1 && accepted.takesValue && words[index + 1] !== undefined) index++ continue }
if (equal === -1 && accepted.takesValue && words[index + 1] === undefined) { ignored.add(`${option} (missing value)`) continue } kept.push(word) if (equal === -1 && accepted.takesValue && words[index + 1] !== undefined) kept.push(words[++index]) continue }
const optionNames = [...cluster].map((flag) => `-${flag}`) const infos = optionNames.map((option) => supported.get(option)) if (infos.every((info) => info && !info.takesValue)) { let reason: string | undefined for (const option of optionNames) { const primary = suppression.canonical.get(option) ?? option reason = suppressionReason(primary, suppression.explicit, suppression.partners) if (reason !== undefined) break } if (reason !== undefined) { const clusterWord = word.slice(0, equal === -1 ? undefined : equal) suppressedSet.add(reason === "explicit" ? clusterWord : `${clusterWord} (explicit ${reason})`) continue } kept.push(word) continue }
ignored.add(word.slice(0, equal === -1 ? undefined : equal)) if ( equal === -1 && optionNames.some((option) => known.get(option)?.takesValue) && words[index + 1] && !words[index + 1].startsWith("-") ) { index++ } continue }
ignored.add("<positional argument>") }
return { words: kept, ignored: [...ignored], suppressed: [...suppressedSet] }}
function shellWords(input: string): WordResult { const words: string[] = [] let word = "" let quote: "'" | '"' | undefined let escaped = false let started = false
for (const character of input) { if (escaped) { word += character escaped = false started = true continue }
if (quote === "'") { if (character === "'") quote = undefined else word += character started = true continue }
if (character === "\\") { escaped = true started = true continue }
if (quote === '"') { if (character === '"') quote = undefined else word += character started = true continue }
if (character === "'" || character === '"') { quote = character started = true continue }
if (/\s/.test(character)) { if (started) words.push(word) word = "" started = false continue }
word += character started = true }
if (escaped) return { _tag: "Failure", reason: "trailing backslash" } if (quote) return { _tag: "Failure", reason: `unterminated ${quote === "'" ? "single" : "double"} quote`, } if (started) words.push(word) return { _tag: "Success", words }}