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.
Files changed (49) hide show
  1. package/AGENTS.md +57 -17
  2. package/README.md +33 -31
  3. package/cli/docs/flags.md +0 -1
  4. package/cli/docs/install.md +28 -13
  5. package/cli/docs/overview.md +2 -2
  6. package/cli/docs/platforms.md +5 -2
  7. package/cli/docs/sync-layers.md +3 -4
  8. package/cli/docs/toolchain.md +26 -34
  9. package/cli/src/commands/docs.ts +3 -3
  10. package/cli/src/commands/model.ts +3 -0
  11. package/cli/src/commands/models.ts +5 -3
  12. package/cli/src/commands/status.ts +145 -32
  13. package/cli/src/commands/sync.ts +4 -5
  14. package/cli/src/commands/toolchain.ts +4 -7
  15. package/cli/src/commands/update.ts +177 -32
  16. package/cli/src/efforts.ts +5 -5
  17. package/cli/src/engine-native/DESIGN.md +31 -22
  18. package/cli/src/engine-native/bun.ts +42 -14
  19. package/cli/src/engine-native/claudeRuntime.ts +17 -9
  20. package/cli/src/engine-native/claudeSettingsModifiers.ts +29 -11
  21. package/cli/src/engine-native/claudeSync.ts +91 -55
  22. package/cli/src/engine-native/codexSync.ts +173 -49
  23. package/cli/src/engine-native/codexToml.ts +12 -7
  24. package/cli/src/engine-native/deps.ts +36 -95
  25. package/cli/src/engine-native/exec.ts +42 -24
  26. package/cli/src/engine-native/index.ts +17 -6
  27. package/cli/src/engine-native/models.ts +2 -9
  28. package/cli/src/engine-native/modes.ts +54 -35
  29. package/cli/src/engine-native/os/darwin.ts +62 -0
  30. package/cli/src/engine-native/os/index.ts +42 -0
  31. package/cli/src/engine-native/os/linux.ts +62 -0
  32. package/cli/src/engine-native/os/targets.ts +73 -0
  33. package/cli/src/engine-native/os/types.ts +75 -0
  34. package/cli/src/engine-native/os/windows.ts +176 -0
  35. package/cli/src/engine-native/parseArgs.ts +147 -48
  36. package/cli/src/engine-native/services.ts +1 -11
  37. package/cli/src/engine-native/settings.ts +3 -2
  38. package/cli/src/engine-native/skillsSync.ts +141 -89
  39. package/cli/src/engine-native/toolchain.ts +5 -147
  40. package/cli/src/engine.ts +41 -11
  41. package/cli/src/generated/sotPayload.ts +7 -7
  42. package/cli/src/kitHome.ts +42 -5
  43. package/cli/src/main.ts +12 -2
  44. package/cli/src/manifests.ts +28 -11
  45. package/cli/src/payload.ts +2 -5
  46. package/docks-kit +4 -4
  47. package/docks-kit.ps1 +123 -0
  48. package/package.json +9 -5
  49. 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
- const MODIFIER_FLAGS = new Set<ModifierFlag>([
28
- "--claude-model",
29
- "--claude-effort",
30
- "--claude-advisor",
31
- "--codex-model",
32
- "--codex-effort"
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 addClaudePlugin(ctx: Ctx, name: string): void {
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
- ctx.claudePlugins.push(name)
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: ModifierFlag, value: string): void {
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
- const flags = ctx.modifierFlags ?? new Set<ModifierFlag>()
124
- flags.add(flag)
125
- ctx.modifierFlags = flags
225
+ markModifier(ctx, flag)
126
226
  }
127
227
 
128
- function isModifierFlag(value: string): value is ModifierFlag {
129
- return MODIFIER_FLAGS.has(value as ModifierFlag)
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 (isModifierFlag(arg) && args[index + 1] === "") {
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, value: string): boolean =>
270
- value !== "" || ctx.modifierFlags?.has(flag) === true
271
- if (supplied("--claude-model", ctx.claudeModel)) {
272
- if (!ctx.syncClaude) {
273
- warn("--claude-model ignored: claude target not selected")
274
- ctx.claudeModel = ""
275
- } else if (!validateClaudeModel(ctx, ctx.claudeModel)) {
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", ctx.claudeEffort)) {
282
- if (!ctx.syncClaude) {
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", ctx.claudeAdvisor)) {
292
- if (!ctx.syncClaude) {
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", ctx.codexModel)) {
302
- if (!ctx.syncCodex) {
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", ctx.codexEffort)) {
312
- if (!ctx.syncCodex) {
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 merged = deepMerge(user, repo)
20
- if (!isObject(merged)) return merged
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,