docks-kit 0.15.1 → 0.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +57 -17
- package/README.md +33 -31
- package/cli/docs/flags.md +0 -1
- package/cli/docs/install.md +28 -13
- package/cli/docs/overview.md +2 -2
- package/cli/docs/platforms.md +5 -2
- package/cli/docs/sync-layers.md +3 -4
- package/cli/docs/toolchain.md +26 -34
- package/cli/src/commands/docs.ts +3 -3
- package/cli/src/commands/model.ts +3 -0
- package/cli/src/commands/models.ts +5 -3
- package/cli/src/commands/status.ts +145 -32
- package/cli/src/commands/sync.ts +4 -5
- package/cli/src/commands/toolchain.ts +4 -7
- package/cli/src/commands/update.ts +177 -32
- package/cli/src/efforts.ts +5 -5
- package/cli/src/engine-native/DESIGN.md +31 -22
- package/cli/src/engine-native/bun.ts +42 -14
- package/cli/src/engine-native/claudeRuntime.ts +17 -9
- package/cli/src/engine-native/claudeSettingsModifiers.ts +29 -11
- package/cli/src/engine-native/claudeSync.ts +91 -55
- package/cli/src/engine-native/codexSync.ts +173 -49
- package/cli/src/engine-native/codexToml.ts +12 -7
- package/cli/src/engine-native/deps.ts +36 -95
- package/cli/src/engine-native/exec.ts +42 -24
- package/cli/src/engine-native/index.ts +17 -6
- package/cli/src/engine-native/models.ts +2 -9
- package/cli/src/engine-native/modes.ts +54 -35
- package/cli/src/engine-native/os/darwin.ts +62 -0
- package/cli/src/engine-native/os/index.ts +42 -0
- package/cli/src/engine-native/os/linux.ts +62 -0
- package/cli/src/engine-native/os/targets.ts +73 -0
- package/cli/src/engine-native/os/types.ts +75 -0
- package/cli/src/engine-native/os/windows.ts +176 -0
- package/cli/src/engine-native/parseArgs.ts +147 -48
- package/cli/src/engine-native/services.ts +1 -11
- package/cli/src/engine-native/settings.ts +3 -2
- package/cli/src/engine-native/skillsSync.ts +141 -89
- package/cli/src/engine-native/toolchain.ts +5 -147
- package/cli/src/engine.ts +41 -11
- package/cli/src/generated/sotPayload.ts +7 -7
- package/cli/src/kitHome.ts +42 -5
- package/cli/src/main.ts +12 -2
- package/cli/src/manifests.ts +28 -11
- package/cli/src/payload.ts +2 -5
- package/docks-kit +4 -4
- package/docks-kit.ps1 +123 -0
- package/package.json +9 -5
- package/cli/src/engine-native/os.ts +0 -24
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { win32 } from "node:path"
|
|
2
|
+
import { p } from "../exec"
|
|
3
|
+
import type { HostOs } from "./types"
|
|
4
|
+
|
|
5
|
+
function powershellLiteral(value: string): string {
|
|
6
|
+
return `'${value.replaceAll("'", "''")}'`
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function encodedPowerShellCommand(script: string): string {
|
|
10
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64")
|
|
11
|
+
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encoded}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Windows shim encoding handles three hazards: Windows argv parsing, cmd
|
|
16
|
+
* metacharacter parsing, and percent/newline values that cmd cannot escape.
|
|
17
|
+
* The encoders are ported from cross-spawn's escape.js and its parseNonShell
|
|
18
|
+
* assembly, based on:
|
|
19
|
+
* https://github.com/moxystudio/node-cross-spawn
|
|
20
|
+
* https://qntm.org/cmd
|
|
21
|
+
* https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
|
|
22
|
+
* https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd
|
|
23
|
+
*/
|
|
24
|
+
const metaCharsRegExp = /([()\][%!^"`<>&|;, *?])/g
|
|
25
|
+
|
|
26
|
+
function escapeCommand(arg: string): string {
|
|
27
|
+
return arg.replace(metaCharsRegExp, "^$1")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function escapeArgument(arg: string, doubleEscapeMetaChars: boolean): string {
|
|
31
|
+
arg = `${arg}`
|
|
32
|
+
arg = arg.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"")
|
|
33
|
+
arg = arg.replace(/(?=(\\+?)?)\1$/, "$1$1")
|
|
34
|
+
arg = `"${arg}"`
|
|
35
|
+
arg = arg.replace(metaCharsRegExp, "^$1")
|
|
36
|
+
if (doubleEscapeMetaChars) {
|
|
37
|
+
arg = arg.replace(metaCharsRegExp, "^$1")
|
|
38
|
+
}
|
|
39
|
+
return arg
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertCommandLineValue(value: string, kind: "executable path" | "argument"): void {
|
|
43
|
+
const unsupported = value.includes("%")
|
|
44
|
+
? "percent sign (%)"
|
|
45
|
+
: value.includes("\r")
|
|
46
|
+
? "carriage return (CR)"
|
|
47
|
+
: value.includes("\n")
|
|
48
|
+
? "line feed (LF)"
|
|
49
|
+
: undefined
|
|
50
|
+
if (unsupported === undefined) return
|
|
51
|
+
|
|
52
|
+
const executablePercentHint =
|
|
53
|
+
kind === "executable path" && unsupported === "percent sign (%)"
|
|
54
|
+
? " The tool must be reached through its .exe or moved out of a directory whose name contains a percent sign."
|
|
55
|
+
: ""
|
|
56
|
+
throw new Error(
|
|
57
|
+
`${kind} ${JSON.stringify(value)} contains a ${unsupported}, which cannot be escaped on a cmd command line; the caller must pass the value another way.${executablePercentHint}`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Always absolute. A pathless name lets CreateProcess and libuv search the
|
|
63
|
+
* parent's current directory before System32, so an untrusted checkout holding
|
|
64
|
+
* a cmd.exe would win. ComSpec is the documented interpreter, honoured only
|
|
65
|
+
* when it is an absolute path; otherwise it is rebuilt under SystemRoot.
|
|
66
|
+
*/
|
|
67
|
+
function commandInterpreter(environment: NodeJS.ProcessEnv = process.env): string {
|
|
68
|
+
const comSpec = environment["ComSpec"] ?? ""
|
|
69
|
+
if (win32.isAbsolute(comSpec)) return comSpec
|
|
70
|
+
const systemRoot = environment["SystemRoot"] ?? ""
|
|
71
|
+
const root = win32.isAbsolute(systemRoot) ? systemRoot : "C:\\Windows"
|
|
72
|
+
return win32.join(root, "System32", "cmd.exe")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function commandShimInvocation(executablePath: string, args: ReadonlyArray<string>) {
|
|
76
|
+
assertCommandLineValue(executablePath, "executable path")
|
|
77
|
+
for (const arg of args) assertCommandLineValue(arg, "argument")
|
|
78
|
+
|
|
79
|
+
const normalizedPath = win32.normalize(executablePath)
|
|
80
|
+
const doubleEscapeMetaChars = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i.test(normalizedPath)
|
|
81
|
+
const commandLine = [
|
|
82
|
+
escapeCommand(normalizedPath),
|
|
83
|
+
...args.map((arg) => escapeArgument(arg, doubleEscapeMetaChars))
|
|
84
|
+
].join(" ")
|
|
85
|
+
return {
|
|
86
|
+
command: commandInterpreter(),
|
|
87
|
+
args: ["/d", "/v:off", "/s", "/c", `"${commandLine}"`],
|
|
88
|
+
windowsVerbatimArguments: true
|
|
89
|
+
} as const
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Authored now, but unreachable until the supported-host admission gate opens.
|
|
93
|
+
export const windows: HostOs = {
|
|
94
|
+
id: "windows",
|
|
95
|
+
toolchainOs: "windows",
|
|
96
|
+
supportsBubblewrap: false,
|
|
97
|
+
directoryLinkKinds: ["symlink", "junction"],
|
|
98
|
+
executableSuffixes: [".exe", ".cmd", ".bat", ""],
|
|
99
|
+
invoke: (executablePath, args) =>
|
|
100
|
+
/\.(?:cmd|bat)$/i.test(executablePath)
|
|
101
|
+
? commandShimInvocation(executablePath, args)
|
|
102
|
+
: { command: executablePath, args },
|
|
103
|
+
bunExecutableName: "bun.exe",
|
|
104
|
+
bunInstaller: (pin, directory) => {
|
|
105
|
+
const scriptPath = p(directory, "install.ps1")
|
|
106
|
+
return {
|
|
107
|
+
scriptPath,
|
|
108
|
+
download: {
|
|
109
|
+
command: "curl",
|
|
110
|
+
args: ["-fsSL", "https://bun.sh/install.ps1", "-o", scriptPath]
|
|
111
|
+
},
|
|
112
|
+
run: {
|
|
113
|
+
command: "powershell.exe",
|
|
114
|
+
args: [
|
|
115
|
+
"-NoProfile",
|
|
116
|
+
"-NonInteractive",
|
|
117
|
+
"-ExecutionPolicy",
|
|
118
|
+
"Bypass",
|
|
119
|
+
"-File",
|
|
120
|
+
scriptPath,
|
|
121
|
+
"-Version",
|
|
122
|
+
pin
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
environmentSetting: (name, value) => ({
|
|
128
|
+
kind: "command",
|
|
129
|
+
probe: {
|
|
130
|
+
command: "reg",
|
|
131
|
+
args: ["query", "HKCU\\Environment", "/v", name]
|
|
132
|
+
},
|
|
133
|
+
apply: {
|
|
134
|
+
command: "setx",
|
|
135
|
+
args: [name, value]
|
|
136
|
+
},
|
|
137
|
+
location: "user environment",
|
|
138
|
+
manualHint: "set it manually in System Properties > Environment Variables"
|
|
139
|
+
}),
|
|
140
|
+
statusLineCommand: (bun, script) => {
|
|
141
|
+
const bunLiteral = powershellLiteral(bun)
|
|
142
|
+
const scriptLiteral = powershellLiteral(script)
|
|
143
|
+
// Module auto-loading reports "Preparing modules for first use" as a
|
|
144
|
+
// progress record, which a redirected host serializes to stderr as CLIXML.
|
|
145
|
+
// Claude runs this command every turn, and the records recur across runs, so
|
|
146
|
+
// silencing progress inside the stored command keeps its streams clean.
|
|
147
|
+
return encodedPowerShellCommand(
|
|
148
|
+
`$ProgressPreference = 'SilentlyContinue'; if ((Test-Path -LiteralPath ${bunLiteral} -PathType Leaf) -and (Test-Path -LiteralPath ${scriptLiteral} -PathType Leaf)) { & ${bunLiteral} ${scriptLiteral} }`
|
|
149
|
+
)
|
|
150
|
+
},
|
|
151
|
+
failureHookCommand: (command) => {
|
|
152
|
+
const prefix = "echo '"
|
|
153
|
+
if (!command.startsWith(prefix) || !command.endsWith("'")) return command
|
|
154
|
+
const quotedPayload = command.slice(prefix.length, -1)
|
|
155
|
+
const posixApostrophe = `'"'"'`
|
|
156
|
+
if (quotedPayload.replaceAll(posixApostrophe, "").includes("'")) return command
|
|
157
|
+
const payload = quotedPayload.replaceAll(posixApostrophe, "'")
|
|
158
|
+
return encodedPowerShellCommand(`Write-Output ${powershellLiteral(payload)}`)
|
|
159
|
+
},
|
|
160
|
+
installHint: (tool) => {
|
|
161
|
+
switch (tool) {
|
|
162
|
+
case "git":
|
|
163
|
+
return "winget install --id Git.Git -e"
|
|
164
|
+
case "jq":
|
|
165
|
+
return "winget install --id jqlang.jq -e"
|
|
166
|
+
case "curl":
|
|
167
|
+
return "winget install --id cURL.cURL -e"
|
|
168
|
+
case "ffplay":
|
|
169
|
+
return "winget install --id Gyan.FFmpeg -e"
|
|
170
|
+
case "claude":
|
|
171
|
+
return "$tmp = Join-Path $env:TEMP 'claude-install.ps1'; curl.exe -fsSL https://claude.ai/install.ps1 -o $tmp; if ($LASTEXITCODE -eq 0) { powershell.exe -NoProfile -ExecutionPolicy Bypass -File $tmp }"
|
|
172
|
+
case "codex":
|
|
173
|
+
return "$tmp = Join-Path $env:TEMP 'codex-install.ps1'; curl.exe -fsSL https://chatgpt.com/codex/install.ps1 -o $tmp; if ($LASTEXITCODE -eq 0) { $env:CODEX_NON_INTERACTIVE = '1'; powershell.exe -NoProfile -ExecutionPolicy Bypass -File $tmp }"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -24,13 +24,96 @@ export class ExitError extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"]
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
|
|
28
|
+
interface ModifierMetadata {
|
|
29
|
+
readonly target: "claude" | "codex"
|
|
30
|
+
readonly ignoredWarning: string
|
|
31
|
+
readonly hasValue: (ctx: Ctx) => boolean
|
|
32
|
+
readonly clear: (ctx: Ctx) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const MODIFIER_METADATA = {
|
|
36
|
+
"--claude-model": {
|
|
37
|
+
target: "claude",
|
|
38
|
+
ignoredWarning: "--claude-model ignored: claude target not selected",
|
|
39
|
+
hasValue: (ctx) => ctx.claudeModel !== "",
|
|
40
|
+
clear: (ctx) => {
|
|
41
|
+
ctx.claudeModel = ""
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"--claude-effort": {
|
|
45
|
+
target: "claude",
|
|
46
|
+
ignoredWarning: "--claude-effort ignored: claude target not selected",
|
|
47
|
+
hasValue: (ctx) => ctx.claudeEffort !== "",
|
|
48
|
+
clear: (ctx) => {
|
|
49
|
+
ctx.claudeEffort = ""
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"--claude-advisor": {
|
|
53
|
+
target: "claude",
|
|
54
|
+
ignoredWarning: "--claude-advisor ignored: claude target not selected",
|
|
55
|
+
hasValue: (ctx) => ctx.claudeAdvisor !== "",
|
|
56
|
+
clear: (ctx) => {
|
|
57
|
+
ctx.claudeAdvisor = ""
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"--claude-compact-window": {
|
|
61
|
+
target: "claude",
|
|
62
|
+
ignoredWarning: "--claude-compact-window ignored: claude target not selected",
|
|
63
|
+
hasValue: (ctx) => ctx.claudeCompactWindow !== "",
|
|
64
|
+
clear: (ctx) => {
|
|
65
|
+
ctx.claudeCompactWindow = ""
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"--claude-permissive": {
|
|
69
|
+
target: "claude",
|
|
70
|
+
ignoredWarning: "--claude-permissive ignored: claude target not selected",
|
|
71
|
+
hasValue: (ctx) => ctx.claudePermissive,
|
|
72
|
+
clear: (ctx) => {
|
|
73
|
+
ctx.claudePermissive = false
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"--claude-plugin": {
|
|
77
|
+
target: "claude",
|
|
78
|
+
ignoredWarning: "--claude-plugin ignored: claude target not selected",
|
|
79
|
+
hasValue: (ctx) => ctx.claudePlugins.length > 0,
|
|
80
|
+
clear: (ctx) => {
|
|
81
|
+
ctx.claudePlugins = []
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"--codex-model": {
|
|
85
|
+
target: "codex",
|
|
86
|
+
ignoredWarning: "--codex-model ignored: codex target not selected",
|
|
87
|
+
hasValue: (ctx) => ctx.codexModel !== "",
|
|
88
|
+
clear: (ctx) => {
|
|
89
|
+
ctx.codexModel = ""
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"--codex-effort": {
|
|
93
|
+
target: "codex",
|
|
94
|
+
ignoredWarning: "--codex-effort ignored: codex target not selected",
|
|
95
|
+
hasValue: (ctx) => ctx.codexEffort !== "",
|
|
96
|
+
clear: (ctx) => {
|
|
97
|
+
ctx.codexEffort = ""
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} satisfies Record<ModifierFlag, ModifierMetadata>
|
|
101
|
+
|
|
102
|
+
type ScalarModifierFlag =
|
|
103
|
+
| "--claude-model"
|
|
104
|
+
| "--claude-effort"
|
|
105
|
+
| "--claude-advisor"
|
|
106
|
+
| "--codex-model"
|
|
107
|
+
| "--codex-effort"
|
|
108
|
+
|
|
109
|
+
const SCALAR_MODIFIER_FLAGS: Record<ScalarModifierFlag, true> = {
|
|
110
|
+
"--claude-model": true,
|
|
111
|
+
"--claude-effort": true,
|
|
112
|
+
"--claude-advisor": true,
|
|
113
|
+
"--codex-model": true,
|
|
114
|
+
"--codex-effort": true
|
|
115
|
+
}
|
|
116
|
+
|
|
34
117
|
|
|
35
118
|
function usage(ctx: Ctx): void {
|
|
36
119
|
const { echo } = ctx.services.logger
|
|
@@ -52,7 +135,6 @@ function usage(ctx: Ctx): void {
|
|
|
52
135
|
)
|
|
53
136
|
echo(" --skip-bubblewrap skip optional bubblewrap bootstrap (Codex Linux sandbox)")
|
|
54
137
|
echo(" --skip-plugin-refresh install missing plugins but skip refresh-only updates")
|
|
55
|
-
echo(" --yes auto-accept toolchain prompts (containers/CI)")
|
|
56
138
|
echo(" --verbose also print no-op confirmations (already in sync, up to date, left as-is)")
|
|
57
139
|
echo("")
|
|
58
140
|
echo("Deploy-time modifiers (deployed config only; SoT untouched; a later flag-less sync reverts)")
|
|
@@ -86,13 +168,33 @@ export function parseCompactWindow(v: string): string | undefined {
|
|
|
86
168
|
return /^[0-9]+$/.test(v) ? v : undefined
|
|
87
169
|
}
|
|
88
170
|
|
|
89
|
-
function
|
|
90
|
-
const { err } = ctx.services.logger
|
|
171
|
+
export function parseClaudePlugin(name: string, err: (message: string) => void): string {
|
|
91
172
|
if (!KNOWN_CLAUDE_OPTIN_PLUGINS.includes(name)) {
|
|
92
173
|
err(`Unknown opt-in plugin '${name}'. Known: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join(", ")}`)
|
|
93
174
|
throw new ExitError(2)
|
|
94
175
|
}
|
|
95
|
-
|
|
176
|
+
return name
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function markModifier(ctx: Ctx, flag: ModifierFlag): void {
|
|
180
|
+
const flags = ctx.modifierFlags ?? new Set<ModifierFlag>()
|
|
181
|
+
flags.add(flag)
|
|
182
|
+
ctx.modifierFlags = flags
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function addClaudePlugin(ctx: Ctx, name: string): void {
|
|
186
|
+
if (name === "") {
|
|
187
|
+
printCatalog(
|
|
188
|
+
ctx,
|
|
189
|
+
`Available Claude optional plugins:\n${KNOWN_CLAUDE_OPTIN_PLUGINS.map((plugin) => ` ${plugin}`).join("\n")}`
|
|
190
|
+
)
|
|
191
|
+
ctx.services.logger.err(
|
|
192
|
+
`Invalid Claude plugin '' — valid: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join("|")}`
|
|
193
|
+
)
|
|
194
|
+
throw new ExitError(2)
|
|
195
|
+
}
|
|
196
|
+
ctx.claudePlugins.push(parseClaudePlugin(name, ctx.services.logger.err))
|
|
197
|
+
markModifier(ctx, "--claude-plugin")
|
|
96
198
|
}
|
|
97
199
|
|
|
98
200
|
function selectTarget(ctx: Ctx, target: string): void {
|
|
@@ -102,7 +204,7 @@ function selectTarget(ctx: Ctx, target: string): void {
|
|
|
102
204
|
ctx.targetFilterSet = true
|
|
103
205
|
}
|
|
104
206
|
|
|
105
|
-
function setModifier(ctx: Ctx, flag:
|
|
207
|
+
function setModifier(ctx: Ctx, flag: ScalarModifierFlag, value: string): void {
|
|
106
208
|
switch (flag) {
|
|
107
209
|
case "--claude-model":
|
|
108
210
|
ctx.claudeModel = value
|
|
@@ -120,20 +222,19 @@ function setModifier(ctx: Ctx, flag: ModifierFlag, value: string): void {
|
|
|
120
222
|
ctx.codexEffort = value
|
|
121
223
|
break
|
|
122
224
|
}
|
|
123
|
-
|
|
124
|
-
flags.add(flag)
|
|
125
|
-
ctx.modifierFlags = flags
|
|
225
|
+
markModifier(ctx, flag)
|
|
126
226
|
}
|
|
127
227
|
|
|
128
|
-
function
|
|
129
|
-
return
|
|
228
|
+
function isScalarModifierFlag(value: string): value is ScalarModifierFlag {
|
|
229
|
+
return SCALAR_MODIFIER_FLAGS[value as ScalarModifierFlag] === true
|
|
130
230
|
}
|
|
131
231
|
|
|
232
|
+
|
|
132
233
|
export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
133
234
|
const { err } = ctx.services.logger
|
|
134
235
|
for (let index = 0; index < args.length; index += 1) {
|
|
135
236
|
const arg = args[index] ?? ""
|
|
136
|
-
if (
|
|
237
|
+
if (isScalarModifierFlag(arg) && args[index + 1] === "") {
|
|
137
238
|
setModifier(ctx, arg, "")
|
|
138
239
|
index += 1
|
|
139
240
|
continue
|
|
@@ -159,9 +260,6 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
159
260
|
case "--prune":
|
|
160
261
|
ctx.prune = true
|
|
161
262
|
continue
|
|
162
|
-
case "--yes":
|
|
163
|
-
ctx.assumeYes = true
|
|
164
|
-
continue
|
|
165
263
|
case "--verbose":
|
|
166
264
|
ctx.verbose = true
|
|
167
265
|
continue
|
|
@@ -190,6 +288,7 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
190
288
|
throw new ExitError(2)
|
|
191
289
|
case "--claude-permissive":
|
|
192
290
|
ctx.claudePermissive = true
|
|
291
|
+
markModifier(ctx, "--claude-permissive")
|
|
193
292
|
continue
|
|
194
293
|
case "--claude-plugin":
|
|
195
294
|
err(`--claude-plugin requires a value: --claude-plugin=<${KNOWN_CLAUDE_OPTIN_PLUGINS.join("|")}>`)
|
|
@@ -244,8 +343,12 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
244
343
|
throw new ExitError(2)
|
|
245
344
|
}
|
|
246
345
|
ctx.claudeCompactWindow = parsed
|
|
346
|
+
markModifier(ctx, "--claude-compact-window")
|
|
247
347
|
} else if (arg.startsWith("--claude-plugin=")) {
|
|
248
348
|
addClaudePlugin(ctx, arg.slice("--claude-plugin=".length))
|
|
349
|
+
} else if (arg.startsWith("--claude-permissive=")) {
|
|
350
|
+
err("--claude-permissive does not take a value")
|
|
351
|
+
throw new ExitError(2)
|
|
249
352
|
} else {
|
|
250
353
|
err(`Unknown arg: ${arg}`)
|
|
251
354
|
throw new ExitError(2)
|
|
@@ -266,53 +369,49 @@ function printCatalog(ctx: Ctx, catalog: string): void {
|
|
|
266
369
|
|
|
267
370
|
export function validateModifierFlags(ctx: Ctx): void {
|
|
268
371
|
const { err, warn } = ctx.services.logger
|
|
269
|
-
const supplied = (flag: ModifierFlag
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
372
|
+
const supplied = (flag: ModifierFlag): boolean =>
|
|
373
|
+
MODIFIER_METADATA[flag].hasValue(ctx) || ctx.modifierFlags?.has(flag) === true
|
|
374
|
+
|
|
375
|
+
for (const flag of Object.keys(MODIFIER_METADATA) as Array<ModifierFlag>) {
|
|
376
|
+
const metadata = MODIFIER_METADATA[flag]
|
|
377
|
+
const targetSelected = metadata.target === "claude" ? ctx.syncClaude : ctx.syncCodex
|
|
378
|
+
if (!targetSelected && supplied(flag)) {
|
|
379
|
+
warn(metadata.ignoredWarning)
|
|
380
|
+
metadata.clear(ctx)
|
|
381
|
+
ctx.modifierFlags?.delete(flag)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (supplied("--claude-model")) {
|
|
386
|
+
if (!validateClaudeModel(ctx, ctx.claudeModel)) {
|
|
276
387
|
printModels(ctx, "claude")
|
|
277
388
|
err(`Invalid Claude model '${ctx.claudeModel}' — use an alias above or a full claude-* ID`)
|
|
278
389
|
throw new ExitError(2)
|
|
279
390
|
}
|
|
280
391
|
}
|
|
281
|
-
if (supplied("--claude-effort"
|
|
282
|
-
if (!ctx.
|
|
283
|
-
warn("--claude-effort ignored: claude target not selected")
|
|
284
|
-
ctx.claudeEffort = ""
|
|
285
|
-
} else if (!isEffortModifierValue("claude", ctx.claudeEffort)) {
|
|
392
|
+
if (supplied("--claude-effort")) {
|
|
393
|
+
if (!isEffortModifierValue("claude", ctx.claudeEffort)) {
|
|
286
394
|
printCatalog(ctx, effortCatalog("claude"))
|
|
287
395
|
err(`Invalid Claude effort '${ctx.claudeEffort}' — valid: ${effortValueGrammar("claude")}`)
|
|
288
396
|
throw new ExitError(2)
|
|
289
397
|
}
|
|
290
398
|
}
|
|
291
|
-
if (supplied("--claude-advisor"
|
|
292
|
-
if (!ctx.
|
|
293
|
-
warn("--claude-advisor ignored: claude target not selected")
|
|
294
|
-
ctx.claudeAdvisor = ""
|
|
295
|
-
} else if (!CLAUDE_ADVISOR_STATES.some((state) => state === ctx.claudeAdvisor)) {
|
|
399
|
+
if (supplied("--claude-advisor")) {
|
|
400
|
+
if (!CLAUDE_ADVISOR_STATES.some((state) => state === ctx.claudeAdvisor)) {
|
|
296
401
|
printCatalog(ctx, advisorCatalog())
|
|
297
402
|
err(`Invalid Claude advisor state '${ctx.claudeAdvisor}' — valid: ${advisorValueGrammar()}`)
|
|
298
403
|
throw new ExitError(2)
|
|
299
404
|
}
|
|
300
405
|
}
|
|
301
|
-
if (supplied("--codex-model"
|
|
302
|
-
if (!ctx.
|
|
303
|
-
warn("--codex-model ignored: codex target not selected")
|
|
304
|
-
ctx.codexModel = ""
|
|
305
|
-
} else if (!validateCodexModel(ctx, ctx.codexModel)) {
|
|
406
|
+
if (supplied("--codex-model")) {
|
|
407
|
+
if (!validateCodexModel(ctx, ctx.codexModel)) {
|
|
306
408
|
printModels(ctx, "codex")
|
|
307
409
|
err(`Invalid Codex model '${ctx.codexModel}' — must match ^[A-Za-z0-9._-]+$`)
|
|
308
410
|
throw new ExitError(2)
|
|
309
411
|
}
|
|
310
412
|
}
|
|
311
|
-
if (supplied("--codex-effort"
|
|
312
|
-
if (!ctx.
|
|
313
|
-
warn("--codex-effort ignored: codex target not selected")
|
|
314
|
-
ctx.codexEffort = ""
|
|
315
|
-
} else if (!isEffortModifierValue("codex", ctx.codexEffort)) {
|
|
413
|
+
if (supplied("--codex-effort")) {
|
|
414
|
+
if (!isEffortModifierValue("codex", ctx.codexEffort)) {
|
|
316
415
|
printCatalog(ctx, effortCatalog("codex"))
|
|
317
416
|
err(`Invalid Codex effort '${ctx.codexEffort}' — valid: ${effortValueGrammar("codex")}`)
|
|
318
417
|
throw new ExitError(2)
|
|
@@ -9,11 +9,9 @@ import {
|
|
|
9
9
|
DEPENDENCIES,
|
|
10
10
|
defaultProbeExecutor,
|
|
11
11
|
resolveDependency,
|
|
12
|
-
resolveLocation,
|
|
13
12
|
resolvePath,
|
|
14
13
|
resolveVersion,
|
|
15
14
|
type DependencySpec,
|
|
16
|
-
type DependencyLocation,
|
|
17
15
|
type ProbeExecutor,
|
|
18
16
|
type ProbeResult,
|
|
19
17
|
type ToolId
|
|
@@ -28,16 +26,12 @@ export interface DependencyManager {
|
|
|
28
26
|
readonly probe: (id: ToolId) => ProbeResult
|
|
29
27
|
readonly version: (id: ToolId) => Promise<string>
|
|
30
28
|
readonly path: (id: ToolId) => Promise<string>
|
|
31
|
-
readonly location: (id: ToolId) => Promise<DependencyLocation>
|
|
32
|
-
readonly latest: (id: ToolId) => Promise<string>
|
|
33
29
|
readonly warnMissing: (id: ToolId, logger: Logger, context?: string) => void
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
export interface Platform {
|
|
37
33
|
readonly raw: () => NodeJS.Platform
|
|
38
34
|
readonly name: () => PlatformName
|
|
39
|
-
readonly isLinux: () => boolean
|
|
40
|
-
readonly shellRcApplicable: () => boolean
|
|
41
35
|
}
|
|
42
36
|
|
|
43
37
|
export interface EngineServices {
|
|
@@ -53,9 +47,7 @@ export interface EngineServiceOptions {
|
|
|
53
47
|
/** Platform view over an injectable platform id. */
|
|
54
48
|
export const makePlatform = (pf: NodeJS.Platform = rawPlatform()): Platform => ({
|
|
55
49
|
raw: () => pf,
|
|
56
|
-
name: () => platformName(pf)
|
|
57
|
-
isLinux: () => pf === "linux",
|
|
58
|
-
shellRcApplicable: () => pf === "linux" || pf === "darwin"
|
|
50
|
+
name: () => platformName(pf)
|
|
59
51
|
})
|
|
60
52
|
|
|
61
53
|
/** DependencyManager whose hints default to the INJECTED platform, not the host. */
|
|
@@ -72,8 +64,6 @@ export const makeDependencyManager = (
|
|
|
72
64
|
probe: (id) => resolveDependency(DEPENDENCIES[id], exec, platform.raw()),
|
|
73
65
|
version: (id) => resolveVersion(DEPENDENCIES[id], exec),
|
|
74
66
|
path: (id) => resolvePath(DEPENDENCIES[id], exec, platform.raw()),
|
|
75
|
-
location: (id) => resolveLocation(DEPENDENCIES[id], exec, platform.raw()),
|
|
76
|
-
latest: (id) => DEPENDENCIES[id].latest?.(exec) ?? Promise.resolve(""),
|
|
77
67
|
warnMissing: (id, logger, context) => {
|
|
78
68
|
if (warned.has(id)) return
|
|
79
69
|
warned.add(id)
|
|
@@ -16,8 +16,9 @@ export function reconcileSettings(repo: Json, user: Json): Json {
|
|
|
16
16
|
* `unique` — i.e. codepoint-sorted and deduplicated, matching jq).
|
|
17
17
|
*/
|
|
18
18
|
export function mergeSettings(repo: Json, user: Json): Json {
|
|
19
|
-
const
|
|
20
|
-
if (!isObject(
|
|
19
|
+
const candidate = deepMerge(user, repo)
|
|
20
|
+
if (!isObject(candidate)) return candidate
|
|
21
|
+
const merged = { ...candidate }
|
|
21
22
|
const permissions = isObject(merged["permissions"]) ? merged["permissions"] : {}
|
|
22
23
|
merged["permissions"] = {
|
|
23
24
|
...permissions,
|