docks-kit 0.17.0 → 0.17.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.
Files changed (84) hide show
  1. package/AGENTS.md +37 -33
  2. package/README.md +1 -0
  3. package/cli/docs/omp-models.md +114 -58
  4. package/cli/src/argv.ts +38 -469
  5. package/cli/src/argvSurface.ts +371 -0
  6. package/cli/src/argvValidate.ts +122 -0
  7. package/cli/src/commands/docs.ts +57 -42
  8. package/cli/src/commands/harnesses.ts +38 -45
  9. package/cli/src/commands/model.ts +48 -53
  10. package/cli/src/commands/models.ts +24 -26
  11. package/cli/src/commands/omp.ts +178 -88
  12. package/cli/src/commands/plugins.ts +20 -20
  13. package/cli/src/commands/skills.ts +22 -20
  14. package/cli/src/commands/status.ts +87 -80
  15. package/cli/src/commands/sync.ts +113 -87
  16. package/cli/src/commands/toolchain.ts +21 -21
  17. package/cli/src/commands/update.ts +139 -117
  18. package/cli/src/efforts.ts +35 -34
  19. package/cli/src/engine-native/DESIGN.md +30 -30
  20. package/cli/src/engine-native/bun.ts +77 -55
  21. package/cli/src/engine-native/claudeLsp.ts +154 -0
  22. package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
  23. package/cli/src/engine-native/claudePluginPasses.ts +348 -0
  24. package/cli/src/engine-native/claudeRemovals.ts +195 -0
  25. package/cli/src/engine-native/claudeRetired.ts +4 -4
  26. package/cli/src/engine-native/claudeRuntime.ts +94 -68
  27. package/cli/src/engine-native/claudeSettings.ts +174 -0
  28. package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
  29. package/cli/src/engine-native/claudeSync.ts +176 -456
  30. package/cli/src/engine-native/codexConfig.ts +158 -0
  31. package/cli/src/engine-native/codexHooks.ts +91 -0
  32. package/cli/src/engine-native/codexPlugins.ts +290 -0
  33. package/cli/src/engine-native/codexStatus.ts +24 -0
  34. package/cli/src/engine-native/codexSync.ts +110 -568
  35. package/cli/src/engine-native/codexToml.ts +140 -138
  36. package/cli/src/engine-native/deps.ts +144 -107
  37. package/cli/src/engine-native/engineCtx.ts +126 -0
  38. package/cli/src/engine-native/exec.ts +98 -87
  39. package/cli/src/engine-native/failures.ts +3 -3
  40. package/cli/src/engine-native/harnesses.ts +62 -72
  41. package/cli/src/engine-native/index.ts +34 -315
  42. package/cli/src/engine-native/jq.ts +25 -24
  43. package/cli/src/engine-native/logger.ts +98 -100
  44. package/cli/src/engine-native/models.ts +50 -45
  45. package/cli/src/engine-native/modes.ts +83 -80
  46. package/cli/src/engine-native/ompFileDeploy.ts +111 -0
  47. package/cli/src/engine-native/ompMarketplace.ts +136 -0
  48. package/cli/src/engine-native/ompOverlay.ts +93 -53
  49. package/cli/src/engine-native/ompPaths.ts +49 -38
  50. package/cli/src/engine-native/ompPlugins.ts +170 -0
  51. package/cli/src/engine-native/ompSync.ts +64 -408
  52. package/cli/src/engine-native/ompYaml.ts +43 -41
  53. package/cli/src/engine-native/os/darwin.ts +5 -5
  54. package/cli/src/engine-native/os/index.ts +15 -15
  55. package/cli/src/engine-native/os/linux.ts +5 -5
  56. package/cli/src/engine-native/os/posix.ts +28 -24
  57. package/cli/src/engine-native/os/targets.ts +17 -17
  58. package/cli/src/engine-native/os/types.ts +31 -31
  59. package/cli/src/engine-native/os/windows.ts +61 -61
  60. package/cli/src/engine-native/parseArgs.ts +122 -372
  61. package/cli/src/engine-native/parseHelp.ts +66 -0
  62. package/cli/src/engine-native/parseModifiers.ts +222 -0
  63. package/cli/src/engine-native/services.ts +34 -34
  64. package/cli/src/engine-native/settings.ts +15 -15
  65. package/cli/src/engine-native/sharedTypes.d.ts +46 -0
  66. package/cli/src/engine-native/skillsInstall.ts +105 -0
  67. package/cli/src/engine-native/skillsLinks.ts +203 -0
  68. package/cli/src/engine-native/skillsManifest.ts +16 -0
  69. package/cli/src/engine-native/skillsPrune.ts +108 -0
  70. package/cli/src/engine-native/skillsSync.ts +28 -362
  71. package/cli/src/engine-native/syncConcurrency.ts +55 -0
  72. package/cli/src/engine-native/syncDispatch.ts +141 -0
  73. package/cli/src/engine-native/toolchain.ts +65 -62
  74. package/cli/src/engine.ts +65 -57
  75. package/cli/src/generated/sotPayload.ts +7 -7
  76. package/cli/src/kitHome.ts +44 -40
  77. package/cli/src/main.ts +63 -56
  78. package/cli/src/manifests.ts +53 -54
  79. package/cli/src/md.d.ts +2 -2
  80. package/cli/src/payload.ts +9 -9
  81. package/cli/src/services.ts +21 -13
  82. package/cli/tsconfig.json +4 -1
  83. package/package.json +9 -2
  84. package/cli/src/engine-native/claudePlugins.ts +0 -510
@@ -1,17 +1,17 @@
1
- import { p } from "./exec"
2
- import { isObject, parseJson, type Json } from "./jq"
3
- import { hostOs, type HostOs } from "./os"
1
+ import { p } from "./exec";
2
+ import { isObject, parseJson, type Json } from "./jq";
3
+ import { hostOs, type HostOs } from "./os";
4
4
 
5
- const BUN_SENTINEL = "__DOCKS_KIT_BUN__"
6
- const SESSION_START_SENTINEL = "__DOCKS_KIT_SESSION_START__"
7
- const NOTIFY_SENTINEL = "__DOCKS_KIT_NOTIFY__"
8
- const STATUSLINE_SENTINEL = "__DOCKS_KIT_STATUSLINE__"
5
+ const BUN_SENTINEL = "__DOCKS_KIT_BUN__";
6
+ const SESSION_START_SENTINEL = "__DOCKS_KIT_SESSION_START__";
7
+ const NOTIFY_SENTINEL = "__DOCKS_KIT_NOTIFY__";
8
+ const STATUSLINE_SENTINEL = "__DOCKS_KIT_STATUSLINE__";
9
9
 
10
10
  export interface ClaudeRuntimePaths {
11
- readonly bun: string
12
- readonly statusline: string
13
- readonly sessionStart: string
14
- readonly notify: string
11
+ readonly bun: string;
12
+ readonly statusline: string;
13
+ readonly sessionStart: string;
14
+ readonly notify: string;
15
15
  }
16
16
 
17
17
  export function claudeRuntimePaths(claudeDir: string, bun: string): ClaudeRuntimePaths {
@@ -19,113 +19,139 @@ export function claudeRuntimePaths(claudeDir: string, bun: string): ClaudeRuntim
19
19
  bun,
20
20
  statusline: p(claudeDir, "bin", "statusline.mjs"),
21
21
  sessionStart: p(claudeDir, "bin", "session-start.mjs"),
22
- notify: p(claudeDir, "bin", "notify.mjs")
23
- }
22
+ notify: p(claudeDir, "bin", "notify.mjs"),
23
+ };
24
24
  }
25
25
 
26
26
  function cloneJson(value: Json): Json {
27
- const cloned = parseJson(JSON.stringify(value))
28
- if (cloned === undefined) throw new Error("Claude settings template cannot be serialized")
29
- return cloned
27
+ const cloned = parseJson(JSON.stringify(value));
28
+ if (cloned === undefined) throw new Error("Claude settings template cannot be serialized");
29
+ return cloned;
30
30
  }
31
31
 
32
32
  function countString(value: Json, expected: string): number {
33
- if (value === expected) return 1
34
- if (Array.isArray(value)) return value.reduce<number>((total, item) => total + countString(item, expected), 0)
35
- if (!isObject(value)) return 0
36
- return Object.values(value).reduce<number>((total, item) => total + countString(item, expected), 0)
33
+ if (value === expected) return 1;
34
+ if (Array.isArray(value))
35
+ return value.reduce<number>((total, item) => total + countString(item, expected), 0);
36
+ if (!isObject(value)) return 0;
37
+ return Object.values(value).reduce<number>(
38
+ (total, item) => total + countString(item, expected),
39
+ 0,
40
+ );
37
41
  }
38
42
 
39
43
  function hooksObject(template: Json): { [key: string]: Json } {
40
- if (!isObject(template) || !isObject(template["hooks"])) throw new Error("Claude settings hooks object is missing")
41
- return template["hooks"]
44
+ if (!isObject(template) || !isObject(template["hooks"]))
45
+ throw new Error("Claude settings hooks object is missing");
46
+ return template["hooks"];
42
47
  }
43
48
 
44
- function commandHandler(template: Json, event: "SessionStart" | "Notification"): { [key: string]: Json } {
45
- const entries = hooksObject(template)[event]
49
+ function commandHandler(
50
+ template: Json,
51
+ event: "SessionStart" | "Notification",
52
+ ): { [key: string]: Json } {
53
+ const entries = hooksObject(template)[event];
46
54
  if (!Array.isArray(entries) || entries.length !== 1 || !isObject(entries[0])) {
47
- throw new Error(`${event} sentinel location is invalid`)
55
+ throw new Error(`${event} sentinel location is invalid`);
48
56
  }
49
- const handlers = entries[0]["hooks"]
57
+ const handlers = entries[0]["hooks"];
50
58
  if (!Array.isArray(handlers) || handlers.length !== 1 || !isObject(handlers[0])) {
51
- throw new Error(`${event} sentinel handler is invalid`)
59
+ throw new Error(`${event} sentinel handler is invalid`);
52
60
  }
53
- return handlers[0]
61
+ return handlers[0];
54
62
  }
55
63
 
56
64
  function oneArg(handler: { [key: string]: Json }, sentinel: string, event: string): void {
57
- const args = handler["args"]
65
+ const args = handler["args"];
58
66
  if (!Array.isArray(args) || args.length !== 1 || args[0] !== sentinel) {
59
- throw new Error(`${event} argument sentinel location is invalid`)
67
+ throw new Error(`${event} argument sentinel location is invalid`);
60
68
  }
61
69
  }
62
70
 
63
71
  function validateTemplate(template: Json): void {
64
- const sessionStart = commandHandler(template, "SessionStart")
65
- const notification = commandHandler(template, "Notification")
66
- if (sessionStart["command"] !== BUN_SENTINEL) throw new Error("SessionStart Bun sentinel location is invalid")
67
- if (notification["command"] !== BUN_SENTINEL) throw new Error("Notification Bun sentinel location is invalid")
68
- oneArg(sessionStart, SESSION_START_SENTINEL, "SessionStart")
69
- oneArg(notification, NOTIFY_SENTINEL, "Notification")
72
+ const sessionStart = commandHandler(template, "SessionStart");
73
+ const notification = commandHandler(template, "Notification");
74
+ if (sessionStart["command"] !== BUN_SENTINEL)
75
+ throw new Error("SessionStart Bun sentinel location is invalid");
76
+ if (notification["command"] !== BUN_SENTINEL)
77
+ throw new Error("Notification Bun sentinel location is invalid");
78
+ oneArg(sessionStart, SESSION_START_SENTINEL, "SessionStart");
79
+ oneArg(notification, NOTIFY_SENTINEL, "Notification");
70
80
 
71
- const hooks = hooksObject(template)
72
- if (hooks["Stop"] !== undefined) throw new Error("hooks.Stop must be absent from the Claude settings template")
73
- if (!isObject(template) || !isObject(template["statusLine"]) || template["statusLine"]["command"] !== STATUSLINE_SENTINEL) {
74
- throw new Error("Statusline sentinel location is invalid")
81
+ const hooks = hooksObject(template);
82
+ if (hooks["Stop"] !== undefined)
83
+ throw new Error("hooks.Stop must be absent from the Claude settings template");
84
+ if (
85
+ !isObject(template) ||
86
+ !isObject(template["statusLine"]) ||
87
+ template["statusLine"]["command"] !== STATUSLINE_SENTINEL
88
+ ) {
89
+ throw new Error("Statusline sentinel location is invalid");
75
90
  }
76
91
  const counts: ReadonlyArray<[string, string, number]> = [
77
92
  ["Bun", BUN_SENTINEL, 2],
78
93
  ["SessionStart", SESSION_START_SENTINEL, 1],
79
94
  ["notify", NOTIFY_SENTINEL, 1],
80
- ["statusline", STATUSLINE_SENTINEL, 1]
81
- ]
95
+ ["statusline", STATUSLINE_SENTINEL, 1],
96
+ ];
82
97
  for (const [label, sentinel, expected] of counts) {
83
- if (countString(template, sentinel) !== expected) throw new Error(`${label} sentinel residue/cardinality is invalid`)
98
+ if (countString(template, sentinel) !== expected)
99
+ throw new Error(`${label} sentinel residue/cardinality is invalid`);
84
100
  }
85
101
  }
86
102
 
87
-
88
103
  export function statusLineCommand(runtime: ClaudeRuntimePaths, host: HostOs = hostOs()): string {
89
- return host.statusLineCommand(runtime.bun, runtime.statusline)
104
+ return host.statusLineCommand(runtime.bun, runtime.statusline);
90
105
  }
91
106
 
92
107
  export function materializeClaudeSettings(
93
108
  template: Json,
94
109
  runtime: ClaudeRuntimePaths | undefined,
95
- host: HostOs = hostOs()
110
+ host: HostOs = hostOs(),
96
111
  ): Json {
97
- validateTemplate(template)
98
- const result = cloneJson(template)
99
- const hooks = hooksObject(result)
100
- const failureGroups = hooks["PostToolUseFailure"]
112
+ validateTemplate(template);
113
+ const result = cloneJson(template);
114
+ const hooks = hooksObject(result);
115
+ const failureGroups = hooks["PostToolUseFailure"];
101
116
  if (Array.isArray(failureGroups)) {
102
117
  for (const group of failureGroups) {
103
- if (!isObject(group) || !Array.isArray(group["hooks"])) continue
118
+ if (!isObject(group) || !Array.isArray(group["hooks"])) continue;
104
119
  for (const handler of group["hooks"]) {
105
- if (isObject(handler) && handler["type"] === "command" && typeof handler["command"] === "string") {
106
- handler["command"] = host.failureHookCommand(handler["command"])
120
+ if (
121
+ isObject(handler) &&
122
+ handler["type"] === "command" &&
123
+ typeof handler["command"] === "string"
124
+ ) {
125
+ handler["command"] = host.failureHookCommand(handler["command"]);
107
126
  }
108
127
  }
109
128
  }
110
129
  }
111
130
  if (runtime === undefined) {
112
- delete hooks["SessionStart"]
113
- delete hooks["Notification"]
114
- if (!isObject(result)) throw new Error("Claude settings template must be an object")
115
- delete result["statusLine"]
131
+ delete hooks["SessionStart"];
132
+ delete hooks["Notification"];
133
+ if (!isObject(result)) throw new Error("Claude settings template must be an object");
134
+ delete result["statusLine"];
116
135
  } else {
117
- const sessionStart = commandHandler(result, "SessionStart")
118
- sessionStart["command"] = runtime.bun
119
- sessionStart["args"] = [runtime.sessionStart]
120
- const notification = commandHandler(result, "Notification")
121
- notification["command"] = runtime.bun
122
- notification["args"] = [runtime.notify]
123
- if (!isObject(result) || !isObject(result["statusLine"])) throw new Error("Claude statusLine object is missing")
124
- result["statusLine"]["command"] = statusLineCommand(runtime, host)
136
+ const sessionStart = commandHandler(result, "SessionStart");
137
+ sessionStart["command"] = runtime.bun;
138
+ sessionStart["args"] = [runtime.sessionStart];
139
+ const notification = commandHandler(result, "Notification");
140
+ notification["command"] = runtime.bun;
141
+ notification["args"] = [runtime.notify];
142
+ if (!isObject(result) || !isObject(result["statusLine"]))
143
+ throw new Error("Claude statusLine object is missing");
144
+ result["statusLine"]["command"] = statusLineCommand(runtime, host);
125
145
  }
126
146
 
127
- for (const sentinel of [BUN_SENTINEL, SESSION_START_SENTINEL, NOTIFY_SENTINEL, STATUSLINE_SENTINEL]) {
128
- if (countString(result, sentinel) !== 0) throw new Error(`Claude settings sentinel residue: ${sentinel}`)
147
+ for (const sentinel of [
148
+ BUN_SENTINEL,
149
+ SESSION_START_SENTINEL,
150
+ NOTIFY_SENTINEL,
151
+ STATUSLINE_SENTINEL,
152
+ ]) {
153
+ if (countString(result, sentinel) !== 0)
154
+ throw new Error(`Claude settings sentinel residue: ${sentinel}`);
129
155
  }
130
- return result
156
+ return result;
131
157
  }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * EngineNative `sync claude` settings materialization and merge. Step order
3
+ * with the runtime cutover and deploy-time modifiers is load-bearing and
4
+ * golden-tested; message strings and JSON semantics are part of the contract.
5
+ */
6
+ import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
7
+ import { p } from "./exec";
8
+ import type { Ctx } from "./index";
9
+ import { isObject, jqStringify, parseJson, type Json } from "./jq";
10
+ import { ExitError } from "./parseArgs";
11
+ import { mergeSettings, reconcileSettings } from "./settings";
12
+ import { payloadDisplayPath } from "../payload";
13
+
14
+ export interface PreparedClaudeSettings {
15
+ readonly path: string;
16
+ readonly bytes: string;
17
+ readonly previousBytes: string | undefined;
18
+ readonly changed: boolean;
19
+ }
20
+
21
+ function assertMaterializedSettings(bytes: string): void {
22
+ if (bytes.includes("__DOCKS_KIT_"))
23
+ throw new Error("Claude settings contain unresolved runtime sentinels");
24
+ }
25
+
26
+ /** Build the candidate settings bytes before the readiness-gated runtime cutover mutates disk. */
27
+ export function prepareClaudeSettings(
28
+ ctx: Ctx,
29
+ claudeDir: string,
30
+ repo: Json,
31
+ ): PreparedClaudeSettings {
32
+ const path = p(claudeDir, "settings.json");
33
+ if (!existsSync(path)) {
34
+ const bytes = jqStringify(repo);
35
+ assertMaterializedSettings(bytes);
36
+ return { path, bytes, previousBytes: undefined, changed: true };
37
+ }
38
+
39
+ const previousBytes = readFileSync(path, "utf8");
40
+ const user = parseJson(previousBytes);
41
+ if (user === undefined || !isObject(user)) {
42
+ const reason = user === undefined ? "is not valid JSON" : "must contain a JSON object";
43
+ ctx.services.logger.err(
44
+ `Aborting sync: ${path} ${reason}. Fix it manually or delete it to reinstall.`,
45
+ );
46
+ throw new ExitError(1);
47
+ }
48
+ const merged = ctx.reconcile ? reconcileSettings(repo, user) : mergeSettings(repo, user);
49
+ const bytes = jqStringify(merged);
50
+ assertMaterializedSettings(bytes);
51
+ return { path, bytes, previousBytes, changed: bytes !== previousBytes };
52
+ }
53
+
54
+ /** Commit a fully prepared document; callers must finish runtime preparation first. */
55
+ export function commitClaudeSettings(ctx: Ctx, prepared: PreparedClaudeSettings): void {
56
+ const { change, verbose } = ctx.services.logger;
57
+ if (!prepared.changed) {
58
+ verbose("Settings already in sync");
59
+ return;
60
+ }
61
+
62
+ if (prepared.previousBytes !== undefined) copyFileSync(prepared.path, `${prepared.path}.bak`);
63
+ writeFileSync(`${prepared.path}.tmp`, prepared.bytes);
64
+ renameSync(`${prepared.path}.tmp`, prepared.path);
65
+ ctx.nextStepTriggers.claudeRestart = true;
66
+ if (prepared.previousBytes === undefined) {
67
+ change("Settings installed");
68
+ } else if (ctx.reconcile) {
69
+ change(
70
+ "Settings reconciled (backup at settings.json.bak; user-only keys preserved, permissions arrays replaced by SoT)",
71
+ );
72
+ } else {
73
+ change("Settings merged (backup at settings.json.bak)");
74
+ }
75
+ }
76
+
77
+ export function describeSettingsSync(ctx: Ctx, claudeDir: string): void {
78
+ const { echo } = ctx.services.logger;
79
+ const repoSettings = payloadDisplayPath("SoT/.claude/settings.json");
80
+ const userSettings = p(claudeDir, "settings.json");
81
+
82
+ if (!existsSync(userSettings)) {
83
+ echo(`[dry-run] install ${repoSettings} -> ${userSettings}`);
84
+ } else if (ctx.reconcile) {
85
+ echo(
86
+ `[dry-run] reconcile ${repoSettings} -> ${userSettings} (SoT keys win; permissions arrays replaced; user-only keys preserved)`,
87
+ );
88
+ } else {
89
+ echo(
90
+ `[dry-run] merge ${repoSettings} -> ${userSettings} (SoT keys win; permissions arrays unioned; user-only keys preserved)`,
91
+ );
92
+ }
93
+ }
94
+
95
+ /** Shared shape of the three jq-edit modifiers (compact window, permissive). */
96
+ function jqEditSettings(
97
+ ctx: Ctx,
98
+ claudeDir: string,
99
+ tag: string,
100
+ edit: (doc: Json) => void,
101
+ ): boolean {
102
+ const { err, warn } = ctx.services.logger;
103
+ const userSettings = p(claudeDir, "settings.json");
104
+ if (!existsSync(userSettings)) {
105
+ warn(`(${tag}) ${userSettings} missing — skipped`);
106
+ return false;
107
+ }
108
+ const before = readFileSync(userSettings, "utf8");
109
+ const doc = parseJson(before);
110
+ if (doc === undefined) {
111
+ err(`(${tag}) ${userSettings} is not valid JSON — skipped`);
112
+ return false;
113
+ }
114
+ edit(doc);
115
+ const out = jqStringify(doc);
116
+ if (out === before) return false;
117
+ writeFileSync(`${userSettings}.tmp`, out);
118
+ renameSync(`${userSettings}.tmp`, userSettings);
119
+ return true;
120
+ }
121
+
122
+ export function syncCompactWindow(ctx: Ctx, claudeDir: string): void {
123
+ const { change, echo, verbose } = ctx.services.logger;
124
+ if (ctx.claudeCompactWindow === "") return;
125
+
126
+ if (ctx.dryRun) {
127
+ echo(
128
+ `[dry-run] (--claude-compact-window) set env.CLAUDE_CODE_AUTO_COMPACT_WINDOW=${ctx.claudeCompactWindow} in ${p(claudeDir, "settings.json")}`,
129
+ );
130
+ return;
131
+ }
132
+
133
+ const changed = jqEditSettings(ctx, claudeDir, "--claude-compact-window", (doc) => {
134
+ if (!isObject(doc)) return;
135
+ const env = isObject(doc["env"]) ? doc["env"] : {};
136
+ env["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] = ctx.claudeCompactWindow;
137
+ doc["env"] = env;
138
+ });
139
+ if (changed) {
140
+ change(
141
+ `Compact window: set to ${ctx.claudeCompactWindow} tokens in deployed settings (SoT and model unchanged; flag-less sync reverts)`,
142
+ );
143
+ ctx.nextStepTriggers.claudeRestart = true;
144
+ } else
145
+ verbose(
146
+ `Compact window: already set to ${ctx.claudeCompactWindow} tokens in deployed settings`,
147
+ );
148
+ }
149
+
150
+ export function syncPermissive(ctx: Ctx, claudeDir: string): void {
151
+ const { change, echo, verbose } = ctx.services.logger;
152
+ if (!ctx.claudePermissive) return;
153
+
154
+ if (ctx.dryRun) {
155
+ echo(
156
+ `[dry-run] (--claude-permissive) empty permissions.ask and permissions.deny in ${p(claudeDir, "settings.json")}`,
157
+ );
158
+ return;
159
+ }
160
+
161
+ const changed = jqEditSettings(ctx, claudeDir, "--claude-permissive", (doc) => {
162
+ if (!isObject(doc)) return;
163
+ const permissions = isObject(doc["permissions"]) ? doc["permissions"] : {};
164
+ permissions["ask"] = [];
165
+ permissions["deny"] = [];
166
+ doc["permissions"] = permissions;
167
+ });
168
+ if (changed) {
169
+ change(
170
+ "Permissive mode: permissions.ask/deny emptied in deployed settings (sandbox use; SoT unchanged)",
171
+ );
172
+ ctx.nextStepTriggers.claudeRestart = true;
173
+ } else verbose("Permissive mode: permissions.ask/deny already empty in deployed settings");
174
+ }
@@ -1,28 +1,24 @@
1
1
  /** Atomic top-level Claude settings modifiers, including direct model mode. */
2
- import { p } from "./exec"
3
- import { readFileSync, renameSync, writeFileSync } from "node:fs"
2
+ import { p } from "./exec";
3
+ import { readFileSync, renameSync, writeFileSync } from "node:fs";
4
4
 
5
- import { resolveEffort } from "../efforts"
6
- import type { Ctx } from "./index"
7
- import { ExitError } from "./parseArgs"
8
- import { isObject, jqStringify, parseJson } from "./jq"
5
+ import { resolveEffort } from "../efforts";
6
+ import type { Ctx } from "./index";
7
+ import { ExitError } from "./parseArgs";
8
+ import { isObject, jqStringify, parseJson } from "./jq";
9
+ import type { SettingEdit } from "./sharedTypes";
9
10
 
10
- interface ClaudeSettingEdit {
11
- readonly tag: string
12
- readonly key: "model" | "effortLevel" | "advisorModel"
13
- readonly value: string | undefined
14
- readonly dryRun: string
15
- readonly changed: string
16
- readonly unchanged: string
11
+ interface ClaudeSettingEdit extends SettingEdit<"model" | "effortLevel" | "advisorModel"> {
12
+ readonly dryRun: string;
17
13
  }
18
14
 
19
15
  function syncClaudeSetting(ctx: Ctx, edit: ClaudeSettingEdit): void {
20
- const { change, echo, err, verbose, warn } = ctx.services.logger
21
- const userSettings = p(ctx.home, ".claude", "settings.json")
16
+ const { change, echo, err, verbose, warn } = ctx.services.logger;
17
+ const userSettings = p(ctx.home, ".claude", "settings.json");
22
18
 
23
- let text: string
19
+ let text: string;
24
20
  try {
25
- text = readFileSync(userSettings, "utf8")
21
+ text = readFileSync(userSettings, "utf8");
26
22
  } catch (error) {
27
23
  if (error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT") {
28
24
  // A dry-run sync writes nothing, so an absent file here does not mean the
@@ -31,61 +27,61 @@ function syncClaudeSetting(ctx: Ctx, edit: ClaudeSettingEdit): void {
31
27
  // `docks-kit model claude <m>`, nothing creates the file and the skip
32
28
  // stands.
33
29
  if (ctx.dryRun && ctx.syncClaude) {
34
- echo(`[dry-run] (${edit.tag}) ${edit.dryRun} in ${userSettings}`)
35
- return
30
+ echo(`[dry-run] (${edit.tag}) ${edit.dryRun} in ${userSettings}`);
31
+ return;
36
32
  }
37
- warn(`(${edit.tag}) ${userSettings} missing — skipped`)
38
- return
33
+ warn(`(${edit.tag}) ${userSettings} missing — skipped`);
34
+ return;
39
35
  }
40
- const cause = error instanceof Error ? error.message : String(error)
41
- err(`(${edit.tag}) could not read ${userSettings}: ${cause}`)
42
- throw new ExitError(1)
36
+ const cause = error instanceof Error ? error.message : String(error);
37
+ err(`(${edit.tag}) could not read ${userSettings}: ${cause}`);
38
+ throw new ExitError(1);
43
39
  }
44
- const doc = parseJson(text)
40
+ const doc = parseJson(text);
45
41
  if (doc === undefined) {
46
- err(`(${edit.tag}) ${userSettings} is not valid JSON — skipped`)
47
- return
42
+ err(`(${edit.tag}) ${userSettings} is not valid JSON — skipped`);
43
+ return;
48
44
  }
49
45
  if (!isObject(doc)) {
50
- err(`(${edit.tag}) ${userSettings} must contain a JSON object — aborting`)
51
- throw new ExitError(1)
46
+ err(`(${edit.tag}) ${userSettings} must contain a JSON object — aborting`);
47
+ throw new ExitError(1);
52
48
  }
53
49
 
54
50
  if (ctx.dryRun) {
55
- echo(`[dry-run] (${edit.tag}) ${edit.dryRun} in ${userSettings}`)
56
- return
51
+ echo(`[dry-run] (${edit.tag}) ${edit.dryRun} in ${userSettings}`);
52
+ return;
57
53
  }
58
54
 
59
- if (edit.value === undefined) delete doc[edit.key]
60
- else doc[edit.key] = edit.value
61
- const out = jqStringify(doc)
55
+ if (edit.value === undefined) delete doc[edit.key];
56
+ else doc[edit.key] = edit.value;
57
+ const out = jqStringify(doc);
62
58
  if (out === text) {
63
- verbose(edit.unchanged)
64
- return
59
+ verbose(edit.unchanged);
60
+ return;
65
61
  }
66
- writeFileSync(`${userSettings}.tmp`, out)
67
- renameSync(`${userSettings}.tmp`, userSettings)
68
- change(edit.changed)
69
- ctx.nextStepTriggers.claudeRestart = true
62
+ writeFileSync(`${userSettings}.tmp`, out);
63
+ renameSync(`${userSettings}.tmp`, userSettings);
64
+ change(edit.changed);
65
+ ctx.nextStepTriggers.claudeRestart = true;
70
66
  }
71
67
 
72
68
  export function syncClaudeModel(ctx: Ctx, model: string): void {
73
- if (model === "") return
74
- const unset = model === "default"
69
+ if (model === "") return;
70
+ const unset = model === "default";
75
71
  syncClaudeSetting(ctx, {
76
72
  tag: "--claude-model",
77
73
  key: "model",
78
74
  value: unset ? undefined : model,
79
75
  dryRun: unset ? "delete .model (account default applies)" : `set .model=${model}`,
80
76
  changed: `Model: deployed settings model set to ${model} (SoT unchanged; flag-less sync reverts)`,
81
- unchanged: `Model: deployed settings model already ${unset ? "unset (account default)" : model}`
82
- })
77
+ unchanged: `Model: deployed settings model already ${unset ? "unset (account default)" : model}`,
78
+ });
83
79
  }
84
80
 
85
81
  export function syncClaudeEffort(ctx: Ctx, effort: string): void {
86
- if (effort === "") return
87
- const resolved = resolveEffort("claude", effort)
88
- const useDefault = effort === "default"
82
+ if (effort === "") return;
83
+ const resolved = resolveEffort("claude", effort);
84
+ const useDefault = effort === "default";
89
85
  syncClaudeSetting(ctx, {
90
86
  tag: "--claude-effort",
91
87
  key: "effortLevel",
@@ -94,14 +90,14 @@ export function syncClaudeEffort(ctx: Ctx, effort: string): void {
94
90
  changed: useDefault
95
91
  ? `Effort: deployed settings effortLevel set to ${resolved} (SoT default)`
96
92
  : `Effort: deployed settings effortLevel set to ${resolved} (SoT unchanged; flag-less sync reverts)`,
97
- unchanged: `Effort: deployed settings effortLevel already ${resolved}`
98
- })
93
+ unchanged: `Effort: deployed settings effortLevel already ${resolved}`,
94
+ });
99
95
  }
100
96
 
101
97
  export function syncClaudeAdvisor(ctx: Ctx, state: string): void {
102
- if (state === "") return
103
- const enabled = state === "on"
104
- const useDefault = state === "default"
98
+ if (state === "") return;
99
+ const enabled = state === "on";
100
+ const useDefault = state === "default";
105
101
  syncClaudeSetting(ctx, {
106
102
  tag: "--claude-advisor",
107
103
  key: "advisorModel",
@@ -114,6 +110,6 @@ export function syncClaudeAdvisor(ctx: Ctx, state: string): void {
114
110
  : "Advisor: deployed settings advisorModel unset (--claude-advisor=off; SoT unchanged)",
115
111
  unchanged: enabled
116
112
  ? "Advisor: deployed settings advisorModel already fable"
117
- : `Advisor: deployed settings advisorModel already unset (${useDefault ? "SoT default: off" : "advisor off"})`
118
- })
113
+ : `Advisor: deployed settings advisorModel already unset (${useDefault ? "SoT default: off" : "advisor off"})`,
114
+ });
119
115
  }