docks-kit 0.15.0 → 0.15.2

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.
@@ -4,7 +4,7 @@
4
4
  * substitution: stdout with trailing newlines stripped, empty on failure.
5
5
  */
6
6
  import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process"
7
- import { accessSync, chmodSync, constants, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
7
+ import { accessSync, constants, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
8
8
  import { delimiter, isAbsolute, join } from "node:path"
9
9
 
10
10
  /** Keep engine paths slash-separated so rendered output is host-stable. */
@@ -101,23 +101,10 @@ export function isExecutable(p: string): boolean {
101
101
  }
102
102
  }
103
103
 
104
- export function fileExists(p: string): boolean {
105
- return existsSync(p)
106
- }
107
-
108
104
  // Change-detection primitives (Output Policy in DESIGN.md): operations report
109
105
  // changed:boolean so unchanged repeat runs log at verbose instead of [ok].
110
106
 
111
107
  /** Write only when the content differs; returns whether a write happened. */
112
- /** Add missing +x bits; returns whether a repair actually happened. */
113
- export function ensureExecutable(path: string): boolean {
114
- const mode = statSync(path).mode
115
- const want = mode | 0o111
116
- if (mode === want) return false
117
- chmodSync(path, want)
118
- return true
119
- }
120
-
121
108
  export function writeTextIfChanged(path: string, content: string): boolean {
122
109
  if (existsSync(path) && readFileSync(path, "utf8") === content) return false
123
110
  writeFileSync(path, content)
@@ -17,12 +17,15 @@ import { claudeNextSteps, claudeSummary, claudeSync, type ClaudeRuntimeState } f
17
17
  import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
18
18
  import { normalizeManifest, skillsNextSteps, skillsSummary, skillsSync, type SkillsState } from "./skillsSync"
19
19
  import { modeModel, modeToolchain } from "./modes"
20
- import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs"
20
+ import { ExitError, parseArgs, parseClaudePlugin, parseCompactWindow, validateModifierFlags } from "./parseArgs"
21
21
 
22
22
  export type ModifierFlag =
23
23
  | "--claude-model"
24
24
  | "--claude-effort"
25
25
  | "--claude-advisor"
26
+ | "--claude-compact-window"
27
+ | "--claude-permissive"
28
+ | "--claude-plugin"
26
29
  | "--codex-model"
27
30
  | "--codex-effort"
28
31
 
@@ -117,6 +120,16 @@ export interface Ctx {
117
120
  function makeCtx(services: EngineServices): Ctx {
118
121
  const env = process.env
119
122
  const home = env["HOME"] !== undefined && env["HOME"] !== "" ? env["HOME"] : homedir()
123
+ const compactWindowSource = env["CLAUDE_COMPACT_WINDOW"] ?? ""
124
+ const claudeCompactWindow = compactWindowSource === "" ? "" : parseCompactWindow(compactWindowSource)
125
+ if (claudeCompactWindow === undefined) {
126
+ services.logger.err("CLAUDE_COMPACT_WINDOW expects a token count (e.g. 680000 or 680k)")
127
+ throw new ExitError(2)
128
+ }
129
+ const claudePlugins = (env["CLAUDE_PLUGINS"] ?? "")
130
+ .split(" ")
131
+ .filter((plugin) => plugin !== "")
132
+ .map((plugin) => parseClaudePlugin(plugin, services.logger.err))
120
133
  return {
121
134
  repoDir: kitHome(),
122
135
  home,
@@ -128,9 +141,9 @@ function makeCtx(services: EngineServices): Ctx {
128
141
  reconcile: env["RECONCILE"] === "1",
129
142
  prune: env["PRUNE"] === "1",
130
143
  assumeYes: env["ASSUME_YES"] === "1",
131
- claudeCompactWindow: env["CLAUDE_COMPACT_WINDOW"] ?? "",
144
+ claudeCompactWindow,
132
145
  claudePermissive: env["CLAUDE_PERMISSIVE"] === "1",
133
- claudePlugins: (env["CLAUDE_PLUGINS"] ?? "").split(" ").filter((s) => s !== ""),
146
+ claudePlugins,
134
147
  claudeModel: env["CLAUDE_MODEL"] ?? "",
135
148
  claudeEffort: "",
136
149
  claudeAdvisor: "",
@@ -278,8 +291,8 @@ export async function runEngineNative(argv: ReadonlyArray<string>, services?: En
278
291
  deps: baseServices.deps,
279
292
  platform: baseServices.platform
280
293
  }
281
- ctx = makeCtx(runServices)
282
294
  try {
295
+ ctx = makeCtx(runServices)
283
296
  switch (argv[0]) {
284
297
  case "model":
285
298
  return modeModel(ctx, argv.slice(1))
@@ -6,16 +6,9 @@ import type { Ctx } from "./index"
6
6
  import { isObject, parseJson, type Json } from "./jq"
7
7
  import { payloadDisplayPath, payloadText } from "../payload"
8
8
 
9
- function catalog(): Json | undefined {
10
- try {
11
- return parseJson(payloadText("SoT/models.json"))
12
- } catch {
13
- return undefined
14
- }
15
- }
16
9
 
17
10
  function toolEntry(tool: string): { [k: string]: Json } | undefined {
18
- const doc = catalog()
11
+ const doc = parseJson(payloadText("SoT/models.json"))
19
12
  if (doc === undefined || !isObject(doc)) return undefined
20
13
  const entry = doc[tool]
21
14
  return entry !== undefined && isObject(entry) ? entry : undefined
@@ -37,7 +30,7 @@ export function printModels(ctx: Ctx, tool: string): void {
37
30
  const { echo, warn } = ctx.services.logger
38
31
  const entry = toolEntry(tool)
39
32
  if (entry === undefined) {
40
- warn(`Model catalog unavailable (${payloadDisplayPath("SoT/models.json", ctx.repoDir)})`)
33
+ warn(`Model catalog unavailable (${payloadDisplayPath("SoT/models.json")})`)
41
34
  return
42
35
  }
43
36
  const verified = typeof entry["verified"] === "string" ? entry["verified"] : "?"
@@ -37,19 +37,29 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
37
37
  if (value === "") {
38
38
  if (tool === "claude") {
39
39
  const deployed = p(ctx.home, ".claude", "settings.json")
40
- if (!fileReadable(deployed)) {
40
+ const result = readConfig(deployed)
41
+ if (result.kind === "missing") {
41
42
  warn("~/.claude/settings.json missing")
42
43
  return 0
43
44
  }
44
- echo(`deployed: ${jsonModelField(deployed)}`)
45
+ if (result.kind === "read-error") {
46
+ err(`Failed to read ~/.claude/settings.json: ${String(result.error)}`)
47
+ return 1
48
+ }
49
+ echo(`deployed: ${jsonModelText(result.data)}`)
45
50
  echo(`SoT: ${jsonModelText(payloadText("SoT/.claude/settings.json"))}`)
46
51
  } else {
47
52
  const deployed = p(ctx.home, ".codex", "config.toml")
48
- if (!fileReadable(deployed)) {
53
+ const result = readConfig(deployed)
54
+ if (result.kind === "missing") {
49
55
  warn("~/.codex/config.toml missing")
50
56
  return 0
51
57
  }
52
- echo(`deployed: ${tomlModelField(deployed)}`)
58
+ if (result.kind === "read-error") {
59
+ err(`Failed to read ~/.codex/config.toml: ${String(result.error)}`)
60
+ return 1
61
+ }
62
+ echo(`deployed: ${tomlModelText(result.data)}`)
53
63
  echo(`SoT: ${tomlModelText(payloadText("SoT/.codex/config.toml"))}`)
54
64
  }
55
65
  printModels(ctx, tool)
@@ -74,20 +84,23 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
74
84
  return 0
75
85
  }
76
86
 
77
- function fileReadable(p: string): boolean {
87
+ type ConfigReadResult =
88
+ | { readonly kind: "missing" }
89
+ | { readonly kind: "read-error"; readonly error: unknown }
90
+ | { readonly kind: "data"; readonly data: string }
91
+
92
+ function readConfig(file: string): ConfigReadResult {
78
93
  try {
79
- readFileSync(p)
80
- return true
81
- } catch {
82
- return false
94
+ return { kind: "data", data: readFileSync(file, "utf8") }
95
+ } catch (error) {
96
+ const code =
97
+ typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
98
+ ? error.code
99
+ : undefined
100
+ return code === "ENOENT" ? { kind: "missing" } : { kind: "read-error", error }
83
101
  }
84
102
  }
85
103
 
86
- /** `jq -r '.model // "default (unset)"'` — empty on unparseable input. */
87
- function jsonModelField(file: string): string {
88
- return jsonModelText(readFileSync(file, "utf8"))
89
- }
90
-
91
104
  function jsonModelText(text: string): string {
92
105
  const doc = parseJson(text)
93
106
  if (doc === undefined) return ""
@@ -97,9 +110,6 @@ function jsonModelText(text: string): string {
97
110
  }
98
111
 
99
112
  /** `awk -F'"' '/^model[[:space:]]*=/{print $2; exit}'`. */
100
- function tomlModelField(file: string): string {
101
- return tomlModelText(readFileSync(file, "utf8"))
102
- }
103
113
 
104
114
  function tomlModelText(text: string): string {
105
115
  for (const line of text.split("\n")) {
@@ -110,9 +120,9 @@ function tomlModelText(text: string): string {
110
120
 
111
121
  export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
112
122
  const { err } = ctx.services.logger
113
- const words = args.filter((a) => !a.startsWith("--"))
114
- const op = words[0] ?? args[0] ?? "check"
115
- const tool = words[1] ?? args[1] ?? ""
123
+ const words = args.filter((arg) => !arg.startsWith("--"))
124
+ const op = words[0] ?? "check"
125
+ const tool = words[1] ?? ""
116
126
  for (const arg of args) {
117
127
  if (arg === "--yes") ctx.assumeYes = true
118
128
  else if (arg === "--verbose") {
@@ -128,7 +138,7 @@ export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Prom
128
138
  err("Usage: toolchain [check|ensure <tool>] [--yes]")
129
139
  return 2
130
140
  }
131
- if (tool === "" || tool === "--yes") {
141
+ if (tool === "") {
132
142
  err("Usage: toolchain ensure <tool> [--yes]")
133
143
  return 2
134
144
  }
@@ -14,11 +14,3 @@ export function platformName(pf: NodeJS.Platform = rawPlatform()): PlatformName
14
14
  return pf === "linux" ? "linux" : pf === "darwin" ? "darwin" : "unknown"
15
15
  }
16
16
 
17
- export function isLinux(): boolean {
18
- return rawPlatform() === "linux"
19
- }
20
-
21
- /** Shell-rc exports apply only on supported hosts. */
22
- export function shellRcApplicable(): boolean {
23
- return platformName() !== "unknown"
24
- }
@@ -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
@@ -86,13 +169,33 @@ export function parseCompactWindow(v: string): string | undefined {
86
169
  return /^[0-9]+$/.test(v) ? v : undefined
87
170
  }
88
171
 
89
- function addClaudePlugin(ctx: Ctx, name: string): void {
90
- const { err } = ctx.services.logger
172
+ export function parseClaudePlugin(name: string, err: (message: string) => void): string {
91
173
  if (!KNOWN_CLAUDE_OPTIN_PLUGINS.includes(name)) {
92
174
  err(`Unknown opt-in plugin '${name}'. Known: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join(", ")}`)
93
175
  throw new ExitError(2)
94
176
  }
95
- ctx.claudePlugins.push(name)
177
+ return name
178
+ }
179
+
180
+ function markModifier(ctx: Ctx, flag: ModifierFlag): void {
181
+ const flags = ctx.modifierFlags ?? new Set<ModifierFlag>()
182
+ flags.add(flag)
183
+ ctx.modifierFlags = flags
184
+ }
185
+
186
+ function addClaudePlugin(ctx: Ctx, name: string): void {
187
+ if (name === "") {
188
+ printCatalog(
189
+ ctx,
190
+ `Available Claude optional plugins:\n${KNOWN_CLAUDE_OPTIN_PLUGINS.map((plugin) => ` ${plugin}`).join("\n")}`
191
+ )
192
+ ctx.services.logger.err(
193
+ `Invalid Claude plugin '' — valid: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join("|")}`
194
+ )
195
+ throw new ExitError(2)
196
+ }
197
+ ctx.claudePlugins.push(parseClaudePlugin(name, ctx.services.logger.err))
198
+ markModifier(ctx, "--claude-plugin")
96
199
  }
97
200
 
98
201
  function selectTarget(ctx: Ctx, target: string): void {
@@ -102,7 +205,7 @@ function selectTarget(ctx: Ctx, target: string): void {
102
205
  ctx.targetFilterSet = true
103
206
  }
104
207
 
105
- function setModifier(ctx: Ctx, flag: ModifierFlag, value: string): void {
208
+ function setModifier(ctx: Ctx, flag: ScalarModifierFlag, value: string): void {
106
209
  switch (flag) {
107
210
  case "--claude-model":
108
211
  ctx.claudeModel = value
@@ -120,20 +223,19 @@ function setModifier(ctx: Ctx, flag: ModifierFlag, value: string): void {
120
223
  ctx.codexEffort = value
121
224
  break
122
225
  }
123
- const flags = ctx.modifierFlags ?? new Set<ModifierFlag>()
124
- flags.add(flag)
125
- ctx.modifierFlags = flags
226
+ markModifier(ctx, flag)
126
227
  }
127
228
 
128
- function isModifierFlag(value: string): value is ModifierFlag {
129
- return MODIFIER_FLAGS.has(value as ModifierFlag)
229
+ function isScalarModifierFlag(value: string): value is ScalarModifierFlag {
230
+ return SCALAR_MODIFIER_FLAGS[value as ScalarModifierFlag] === true
130
231
  }
131
232
 
233
+
132
234
  export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
133
235
  const { err } = ctx.services.logger
134
236
  for (let index = 0; index < args.length; index += 1) {
135
237
  const arg = args[index] ?? ""
136
- if (isModifierFlag(arg) && args[index + 1] === "") {
238
+ if (isScalarModifierFlag(arg) && args[index + 1] === "") {
137
239
  setModifier(ctx, arg, "")
138
240
  index += 1
139
241
  continue
@@ -190,6 +292,7 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
190
292
  throw new ExitError(2)
191
293
  case "--claude-permissive":
192
294
  ctx.claudePermissive = true
295
+ markModifier(ctx, "--claude-permissive")
193
296
  continue
194
297
  case "--claude-plugin":
195
298
  err(`--claude-plugin requires a value: --claude-plugin=<${KNOWN_CLAUDE_OPTIN_PLUGINS.join("|")}>`)
@@ -244,8 +347,12 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
244
347
  throw new ExitError(2)
245
348
  }
246
349
  ctx.claudeCompactWindow = parsed
350
+ markModifier(ctx, "--claude-compact-window")
247
351
  } else if (arg.startsWith("--claude-plugin=")) {
248
352
  addClaudePlugin(ctx, arg.slice("--claude-plugin=".length))
353
+ } else if (arg.startsWith("--claude-permissive=")) {
354
+ err("--claude-permissive does not take a value")
355
+ throw new ExitError(2)
249
356
  } else {
250
357
  err(`Unknown arg: ${arg}`)
251
358
  throw new ExitError(2)
@@ -266,53 +373,49 @@ function printCatalog(ctx: Ctx, catalog: string): void {
266
373
 
267
374
  export function validateModifierFlags(ctx: Ctx): void {
268
375
  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)) {
376
+ const supplied = (flag: ModifierFlag): boolean =>
377
+ MODIFIER_METADATA[flag].hasValue(ctx) || ctx.modifierFlags?.has(flag) === true
378
+
379
+ for (const flag of Object.keys(MODIFIER_METADATA) as Array<ModifierFlag>) {
380
+ const metadata = MODIFIER_METADATA[flag]
381
+ const targetSelected = metadata.target === "claude" ? ctx.syncClaude : ctx.syncCodex
382
+ if (!targetSelected && supplied(flag)) {
383
+ warn(metadata.ignoredWarning)
384
+ metadata.clear(ctx)
385
+ ctx.modifierFlags?.delete(flag)
386
+ }
387
+ }
388
+
389
+ if (supplied("--claude-model")) {
390
+ if (!validateClaudeModel(ctx, ctx.claudeModel)) {
276
391
  printModels(ctx, "claude")
277
392
  err(`Invalid Claude model '${ctx.claudeModel}' — use an alias above or a full claude-* ID`)
278
393
  throw new ExitError(2)
279
394
  }
280
395
  }
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)) {
396
+ if (supplied("--claude-effort")) {
397
+ if (!isEffortModifierValue("claude", ctx.claudeEffort)) {
286
398
  printCatalog(ctx, effortCatalog("claude"))
287
399
  err(`Invalid Claude effort '${ctx.claudeEffort}' — valid: ${effortValueGrammar("claude")}`)
288
400
  throw new ExitError(2)
289
401
  }
290
402
  }
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)) {
403
+ if (supplied("--claude-advisor")) {
404
+ if (!CLAUDE_ADVISOR_STATES.some((state) => state === ctx.claudeAdvisor)) {
296
405
  printCatalog(ctx, advisorCatalog())
297
406
  err(`Invalid Claude advisor state '${ctx.claudeAdvisor}' — valid: ${advisorValueGrammar()}`)
298
407
  throw new ExitError(2)
299
408
  }
300
409
  }
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)) {
410
+ if (supplied("--codex-model")) {
411
+ if (!validateCodexModel(ctx, ctx.codexModel)) {
306
412
  printModels(ctx, "codex")
307
413
  err(`Invalid Codex model '${ctx.codexModel}' — must match ^[A-Za-z0-9._-]+$`)
308
414
  throw new ExitError(2)
309
415
  }
310
416
  }
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)) {
417
+ if (supplied("--codex-effort")) {
418
+ if (!isEffortModifierValue("codex", ctx.codexEffort)) {
316
419
  printCatalog(ctx, effortCatalog("codex"))
317
420
  err(`Invalid Codex effort '${ctx.codexEffort}' — valid: ${effortValueGrammar("codex")}`)
318
421
  throw new ExitError(2)
@@ -36,8 +36,6 @@ export interface DependencyManager {
36
36
  export interface Platform {
37
37
  readonly raw: () => NodeJS.Platform
38
38
  readonly name: () => PlatformName
39
- readonly isLinux: () => boolean
40
- readonly shellRcApplicable: () => boolean
41
39
  }
42
40
 
43
41
  export interface EngineServices {
@@ -53,9 +51,7 @@ export interface EngineServiceOptions {
53
51
  /** Platform view over an injectable platform id. */
54
52
  export const makePlatform = (pf: NodeJS.Platform = rawPlatform()): Platform => ({
55
53
  raw: () => pf,
56
- name: () => platformName(pf),
57
- isLinux: () => pf === "linux",
58
- shellRcApplicable: () => pf === "linux" || pf === "darwin"
54
+ name: () => platformName(pf)
59
55
  })
60
56
 
61
57
  /** DependencyManager whose hints default to the INJECTED platform, not the host. */
@@ -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,