docks-kit 0.17.1 → 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.
- package/AGENTS.md +36 -32
- package/cli/docs/omp-models.md +88 -53
- package/cli/src/argv.ts +38 -469
- package/cli/src/argvSurface.ts +371 -0
- package/cli/src/argvValidate.ts +122 -0
- package/cli/src/commands/docs.ts +57 -42
- package/cli/src/commands/harnesses.ts +38 -45
- package/cli/src/commands/model.ts +48 -53
- package/cli/src/commands/models.ts +24 -26
- package/cli/src/commands/omp.ts +132 -112
- package/cli/src/commands/plugins.ts +20 -20
- package/cli/src/commands/skills.ts +22 -20
- package/cli/src/commands/status.ts +87 -80
- package/cli/src/commands/sync.ts +113 -87
- package/cli/src/commands/toolchain.ts +21 -21
- package/cli/src/commands/update.ts +139 -117
- package/cli/src/efforts.ts +35 -34
- package/cli/src/engine-native/DESIGN.md +30 -30
- package/cli/src/engine-native/bun.ts +77 -55
- package/cli/src/engine-native/claudeLsp.ts +154 -0
- package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
- package/cli/src/engine-native/claudePluginPasses.ts +348 -0
- package/cli/src/engine-native/claudeRemovals.ts +195 -0
- package/cli/src/engine-native/claudeRetired.ts +4 -4
- package/cli/src/engine-native/claudeRuntime.ts +94 -68
- package/cli/src/engine-native/claudeSettings.ts +174 -0
- package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
- package/cli/src/engine-native/claudeSync.ts +176 -456
- package/cli/src/engine-native/codexConfig.ts +158 -0
- package/cli/src/engine-native/codexHooks.ts +91 -0
- package/cli/src/engine-native/codexPlugins.ts +290 -0
- package/cli/src/engine-native/codexStatus.ts +24 -0
- package/cli/src/engine-native/codexSync.ts +110 -568
- package/cli/src/engine-native/codexToml.ts +140 -138
- package/cli/src/engine-native/deps.ts +144 -107
- package/cli/src/engine-native/engineCtx.ts +126 -0
- package/cli/src/engine-native/exec.ts +98 -87
- package/cli/src/engine-native/failures.ts +3 -3
- package/cli/src/engine-native/harnesses.ts +62 -72
- package/cli/src/engine-native/index.ts +34 -315
- package/cli/src/engine-native/jq.ts +25 -24
- package/cli/src/engine-native/logger.ts +98 -100
- package/cli/src/engine-native/models.ts +50 -45
- package/cli/src/engine-native/modes.ts +83 -80
- package/cli/src/engine-native/ompFileDeploy.ts +111 -0
- package/cli/src/engine-native/ompMarketplace.ts +136 -0
- package/cli/src/engine-native/ompOverlay.ts +67 -64
- package/cli/src/engine-native/ompPaths.ts +49 -38
- package/cli/src/engine-native/ompPlugins.ts +170 -0
- package/cli/src/engine-native/ompSync.ts +64 -408
- package/cli/src/engine-native/ompYaml.ts +43 -41
- package/cli/src/engine-native/os/darwin.ts +5 -5
- package/cli/src/engine-native/os/index.ts +15 -15
- package/cli/src/engine-native/os/linux.ts +5 -5
- package/cli/src/engine-native/os/posix.ts +28 -24
- package/cli/src/engine-native/os/targets.ts +17 -17
- package/cli/src/engine-native/os/types.ts +31 -31
- package/cli/src/engine-native/os/windows.ts +61 -61
- package/cli/src/engine-native/parseArgs.ts +122 -372
- package/cli/src/engine-native/parseHelp.ts +66 -0
- package/cli/src/engine-native/parseModifiers.ts +222 -0
- package/cli/src/engine-native/services.ts +34 -34
- package/cli/src/engine-native/settings.ts +15 -15
- package/cli/src/engine-native/sharedTypes.d.ts +46 -0
- package/cli/src/engine-native/skillsInstall.ts +105 -0
- package/cli/src/engine-native/skillsLinks.ts +203 -0
- package/cli/src/engine-native/skillsManifest.ts +16 -0
- package/cli/src/engine-native/skillsPrune.ts +108 -0
- package/cli/src/engine-native/skillsSync.ts +28 -362
- package/cli/src/engine-native/syncConcurrency.ts +55 -0
- package/cli/src/engine-native/syncDispatch.ts +141 -0
- package/cli/src/engine-native/toolchain.ts +65 -62
- package/cli/src/engine.ts +65 -57
- package/cli/src/generated/sotPayload.ts +7 -7
- package/cli/src/kitHome.ts +44 -40
- package/cli/src/main.ts +63 -56
- package/cli/src/manifests.ts +53 -54
- package/cli/src/md.d.ts +2 -2
- package/cli/src/payload.ts +9 -9
- package/cli/src/services.ts +21 -13
- package/cli/tsconfig.json +4 -1
- package/package.json +9 -2
- 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))
|
|
35
|
-
|
|
36
|
-
|
|
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"]))
|
|
41
|
-
|
|
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(
|
|
45
|
-
|
|
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)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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)
|
|
73
|
-
|
|
74
|
-
|
|
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)
|
|
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 (
|
|
106
|
-
|
|
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"]))
|
|
124
|
-
|
|
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 [
|
|
128
|
-
|
|
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
|
|
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
|
}
|