docks-kit 0.1.4 → 0.2.0
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/README.md +1 -1
- package/SoT/.codex/config.toml +1 -1
- package/SoT/models.json +5 -2
- package/SoT/toolchain.json +1 -0
- package/cli/docs/flags.md +1 -0
- package/cli/docs/overview.md +3 -0
- package/cli/docs/platforms.md +6 -5
- package/cli/src/commands/model.ts +7 -3
- package/cli/src/commands/sync.ts +14 -3
- package/cli/src/commands/toolchain.ts +7 -3
- package/cli/src/engine-native/DESIGN.md +75 -2
- package/cli/src/engine-native/claudeModel.ts +9 -3
- package/cli/src/engine-native/claudeSync.ts +205 -94
- package/cli/src/engine-native/codexSync.ts +97 -44
- package/cli/src/engine-native/codexToml.ts +12 -5
- package/cli/src/engine-native/deps.ts +325 -0
- package/cli/src/engine-native/exec.ts +50 -2
- package/cli/src/engine-native/index.ts +45 -9
- package/cli/src/engine-native/logger.ts +35 -0
- package/cli/src/engine-native/models.ts +13 -12
- package/cli/src/engine-native/modes.ts +17 -10
- package/cli/src/engine-native/os.ts +29 -0
- package/cli/src/engine-native/parseArgs.ts +19 -17
- package/cli/src/engine-native/services.ts +96 -0
- package/cli/src/engine-native/skillsSync.ts +70 -51
- package/cli/src/engine-native/toolchain.ts +46 -62
- package/cli/src/engine.ts +11 -2
- package/cli/src/main.ts +3 -2
- package/cli/src/services.ts +34 -0
- package/package.json +1 -1
- package/cli/src/engine-native/output.ts +0 -20
|
@@ -6,40 +6,34 @@
|
|
|
6
6
|
import { spawnSync } from "node:child_process"
|
|
7
7
|
import {
|
|
8
8
|
appendFileSync,
|
|
9
|
-
chmodSync,
|
|
10
9
|
copyFileSync,
|
|
11
|
-
cpSync,
|
|
12
10
|
existsSync,
|
|
13
11
|
mkdirSync,
|
|
14
12
|
readdirSync,
|
|
15
13
|
readFileSync,
|
|
16
14
|
renameSync,
|
|
17
15
|
rmSync,
|
|
18
|
-
statSync,
|
|
19
16
|
writeFileSync
|
|
20
17
|
} from "node:fs"
|
|
21
18
|
import { tmpdir } from "node:os"
|
|
22
19
|
import { syncClaudeModel } from "./claudeModel"
|
|
23
|
-
import {
|
|
20
|
+
import { copyFileIfChanged, copyTreeIfChanged, ensureExecutable, p, writeFileIfChanged } from "./exec"
|
|
24
21
|
import type { Ctx } from "./index"
|
|
25
22
|
import { compareCodepoints, deepMerge, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
26
|
-
import {
|
|
23
|
+
import type { EngineServices } from "./services"
|
|
27
24
|
import { ExitError } from "./parseArgs"
|
|
28
25
|
import { mergeSettings, reconcileSettings } from "./settings"
|
|
29
26
|
import { ensure, field } from "./toolchain"
|
|
30
27
|
|
|
31
28
|
export function claudeSync(ctx: Ctx): void {
|
|
29
|
+
const { warn } = ctx.services.logger
|
|
32
30
|
const claudeDir = p(ctx.home, ".claude")
|
|
33
31
|
|
|
34
32
|
if (!ctx.dryRun) mkdirSync(claudeDir, { recursive: true })
|
|
35
33
|
|
|
36
|
-
if (
|
|
37
|
-
const hint =
|
|
38
|
-
process.platform === "win32"
|
|
39
|
-
? "winget install Anthropic.ClaudeCode"
|
|
40
|
-
: "curl -fsSL https://claude.ai/install.sh -o /tmp/claude-install.sh && bash /tmp/claude-install.sh"
|
|
34
|
+
if (ctx.services.deps.probe("claude").state === "missing") {
|
|
41
35
|
warn(
|
|
42
|
-
`claude CLI not found - config deploys, but plugin passes are skipped. Install Claude Code: ${
|
|
36
|
+
`claude CLI not found - config deploys, but plugin passes are skipped. Install Claude Code: ${ctx.services.deps.spec("claude").installHint()} | docs: https://code.claude.com/docs/en/setup`
|
|
43
37
|
)
|
|
44
38
|
}
|
|
45
39
|
|
|
@@ -62,11 +56,12 @@ export function claudeSync(ctx: Ctx): void {
|
|
|
62
56
|
// ------------------------------------------------------------------ rtk ----
|
|
63
57
|
|
|
64
58
|
/** RTK toolchain install callback. */
|
|
65
|
-
export function rtkInstall(ctx: Ctx): (mode: "install" | "upgrade", version: string) => number {
|
|
66
|
-
return (mode, version) => {
|
|
59
|
+
export function rtkInstall(ctx: Ctx): (mode: "install" | "upgrade", version: string, services: EngineServices) => number {
|
|
60
|
+
return (mode, version, services) => {
|
|
61
|
+
const { change, err, verbose, warn } = services.logger
|
|
67
62
|
const installerRef = version !== "" ? `refs/tags/v${version}` : "refs/heads/master"
|
|
68
63
|
|
|
69
|
-
if (mode === "upgrade")
|
|
64
|
+
if (mode === "upgrade") verbose(`Upgrading RTK${version !== "" ? ` to ${version}` : ""}...`)
|
|
70
65
|
else warn(`RTK not found. Installing${version !== "" ? ` ${version}` : ""}...`)
|
|
71
66
|
const installer = p(tmpdir(), `rtk-install-${process.pid}.sh`)
|
|
72
67
|
const dl = spawnSync("curl", ["-fsSL", `https://raw.githubusercontent.com/rtk-ai/rtk/${installerRef}/install.sh`, "-o", installer], {
|
|
@@ -80,9 +75,9 @@ export function rtkInstall(ctx: Ctx): (mode: "install" | "upgrade", version: str
|
|
|
80
75
|
}
|
|
81
76
|
rmSync(installer, { force: true })
|
|
82
77
|
process.env["PATH"] = `${ctx.home}/.local/bin:${ctx.home}/.cargo/bin:${process.env["PATH"] ?? ""}`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
const installed = services.deps.version("rtk")
|
|
79
|
+
if (services.deps.probe("rtk").state === "present") {
|
|
80
|
+
change(`RTK ready (${installed !== "" ? installed : "version unknown"})`)
|
|
86
81
|
return 0
|
|
87
82
|
}
|
|
88
83
|
err("RTK install failed. Install manually: https://github.com/rtk-ai/rtk")
|
|
@@ -91,13 +86,14 @@ export function rtkInstall(ctx: Ctx): (mode: "install" | "upgrade", version: str
|
|
|
91
86
|
}
|
|
92
87
|
|
|
93
88
|
function syncRtk(ctx: Ctx, claudeDir: string): void {
|
|
89
|
+
const { change, echo, verbose, warn } = ctx.services.logger
|
|
94
90
|
if (ctx.skipRtk) {
|
|
95
91
|
warn("Skipping RTK (--skip-rtk)")
|
|
96
92
|
return
|
|
97
93
|
}
|
|
98
94
|
|
|
99
|
-
if (
|
|
100
|
-
if (
|
|
95
|
+
if (ctx.services.platform.isWindows()) {
|
|
96
|
+
if (ctx.services.deps.probe("rtk").state === "missing") {
|
|
101
97
|
warn("rtk not installed — the kit's auto-install is Unix-only. Install natively (winget, or the rtk-*-windows-msvc.zip release), then re-run sync")
|
|
102
98
|
return
|
|
103
99
|
}
|
|
@@ -105,7 +101,7 @@ function syncRtk(ctx: Ctx, claudeDir: string): void {
|
|
|
105
101
|
warn("RTK bootstrap failed — continuing sync without it")
|
|
106
102
|
}
|
|
107
103
|
|
|
108
|
-
if (
|
|
104
|
+
if (ctx.services.deps.probe("rtk").state === "missing") return
|
|
109
105
|
if (!existsSync(p(claudeDir, "RTK.md"))) {
|
|
110
106
|
if (ctx.dryRun) {
|
|
111
107
|
echo("[dry-run] rtk init --global (RTK.md missing; runs before the settings merge, which normalizes rtk's settings rewrite)")
|
|
@@ -115,30 +111,35 @@ function syncRtk(ctx: Ctx, claudeDir: string): void {
|
|
|
115
111
|
// sync before the success log and before any settings/plugin mutation.
|
|
116
112
|
const res = spawnSync("rtk", ["init", "--global"], { stdio: "inherit" })
|
|
117
113
|
if (res.error !== undefined || res.status !== 0) throw new ExitError(res.status ?? 1)
|
|
118
|
-
|
|
114
|
+
change("RTK initialized (RTK.md generated; the following settings merge re-asserts the SoT hooks)")
|
|
119
115
|
} else if (!ctx.dryRun) {
|
|
120
|
-
|
|
116
|
+
verbose("RTK already initialized")
|
|
121
117
|
}
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
// ------------------------------------------------------ scripts + hooks ----
|
|
125
121
|
|
|
126
122
|
function syncScripts(ctx: Ctx, claudeDir: string): void {
|
|
123
|
+
const { change, echo, verbose } = ctx.services.logger
|
|
127
124
|
if (ctx.dryRun) {
|
|
128
125
|
echo("[dry-run] cp statusline.sh, fetch-usage.sh, notification.mp3")
|
|
129
126
|
return
|
|
130
127
|
}
|
|
131
128
|
|
|
129
|
+
let changed = false
|
|
132
130
|
for (const script of ["statusline.sh", "fetch-usage.sh"]) {
|
|
133
131
|
const src = p(ctx.repoDir, "SoT", ".claude", script)
|
|
134
132
|
if (existsSync(src)) {
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
if (copyFileIfChanged(src, p(claudeDir, script))) changed = true
|
|
134
|
+
if (ensureExecutable(p(claudeDir, script))) changed = true
|
|
137
135
|
}
|
|
138
136
|
}
|
|
139
137
|
const mp3 = p(ctx.repoDir, "notification.mp3")
|
|
140
|
-
if (existsSync(mp3)
|
|
141
|
-
|
|
138
|
+
if (existsSync(mp3) && copyFileIfChanged(mp3, p(claudeDir, "notification.mp3"))) changed = true
|
|
139
|
+
if (changed) {
|
|
140
|
+
change("Scripts synced (statusline, fetch-usage, notification)")
|
|
141
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
142
|
+
} else verbose("Scripts already in sync (statusline, fetch-usage, notification)")
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
function shellScriptCount(hooksDir: string): number {
|
|
@@ -150,6 +151,7 @@ function shellScriptCount(hooksDir: string): number {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
function syncHooks(ctx: Ctx, claudeDir: string): void {
|
|
154
|
+
const { change, echo, verbose } = ctx.services.logger
|
|
153
155
|
const sotHooks = p(ctx.repoDir, "SoT", ".claude", "hooks")
|
|
154
156
|
if (!existsSync(sotHooks)) return
|
|
155
157
|
|
|
@@ -160,19 +162,31 @@ function syncHooks(ctx: Ctx, claudeDir: string): void {
|
|
|
160
162
|
|
|
161
163
|
const hooksDir = p(claudeDir, "hooks")
|
|
162
164
|
mkdirSync(hooksDir, { recursive: true })
|
|
163
|
-
|
|
165
|
+
let changed = copyTreeIfChanged(sotHooks, hooksDir)
|
|
164
166
|
for (const e of readdirSync(hooksDir, { withFileTypes: true })) {
|
|
165
167
|
if (e.isFile() && e.name.endsWith(".sh")) {
|
|
166
|
-
|
|
168
|
+
if (ensureExecutable(p(hooksDir, e.name))) changed = true
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
|
-
|
|
171
|
+
if (changed) {
|
|
172
|
+
change(`Hooks synced (${shellScriptCount(hooksDir)} scripts)`)
|
|
173
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
174
|
+
} else verbose(`Hooks already in sync (${shellScriptCount(hooksDir)} scripts)`)
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
function syncClaudeMd(ctx: Ctx, claudeDir: string): void {
|
|
178
|
+
const { change, echo, verbose } = ctx.services.logger
|
|
179
|
+
// The @RTK.md import only resolves once `rtk init` has generated
|
|
180
|
+
// ~/.claude/RTK.md (the rtk phase runs before this). Deploying the import
|
|
181
|
+
// without the file leaves a dangling reference in every Claude session
|
|
182
|
+
// (seen on Windows, where rtk never auto-installs) — strip it while the
|
|
183
|
+
// file is absent; a later sync after rtk init restores it.
|
|
184
|
+
const rtkMdAbsent = !existsSync(p(claudeDir, "RTK.md"))
|
|
173
185
|
if (ctx.dryRun) {
|
|
174
186
|
if (ctx.skipRtk) {
|
|
175
187
|
echo("[dry-run] cp SoT/.claude/CLAUDE.md -> ~/.claude/CLAUDE.md (stripping @RTK.md import: --skip-rtk)")
|
|
188
|
+
} else if (rtkMdAbsent) {
|
|
189
|
+
echo("[dry-run] cp SoT/.claude/CLAUDE.md -> ~/.claude/CLAUDE.md (would strip @RTK.md import while ~/.claude/RTK.md is absent)")
|
|
176
190
|
} else {
|
|
177
191
|
echo("[dry-run] cp SoT/.claude/CLAUDE.md -> ~/.claude/CLAUDE.md")
|
|
178
192
|
}
|
|
@@ -180,22 +194,28 @@ function syncClaudeMd(ctx: Ctx, claudeDir: string): void {
|
|
|
180
194
|
}
|
|
181
195
|
|
|
182
196
|
const src = p(ctx.repoDir, "SoT", ".claude", "CLAUDE.md")
|
|
183
|
-
|
|
197
|
+
const stripReason = ctx.skipRtk ? "--skip-rtk" : rtkMdAbsent ? "~/.claude/RTK.md absent (rtk not initialized)" : ""
|
|
198
|
+
if (stripReason !== "") {
|
|
184
199
|
const stripped = readFileSync(src, "utf8")
|
|
185
200
|
.split("\n")
|
|
186
201
|
.filter((l) => l !== "@RTK.md")
|
|
187
202
|
.join("\n")
|
|
188
|
-
|
|
189
|
-
|
|
203
|
+
if (writeFileIfChanged(p(claudeDir, "CLAUDE.md"), stripped)) {
|
|
204
|
+
change(`CLAUDE.md synced (@RTK.md import stripped: ${stripReason})`)
|
|
205
|
+
} else {
|
|
206
|
+
verbose("CLAUDE.md already in sync")
|
|
207
|
+
}
|
|
208
|
+
} else if (copyFileIfChanged(src, p(claudeDir, "CLAUDE.md"))) {
|
|
209
|
+
change("CLAUDE.md synced")
|
|
190
210
|
} else {
|
|
191
|
-
|
|
192
|
-
log("CLAUDE.md synced")
|
|
211
|
+
verbose("CLAUDE.md already in sync")
|
|
193
212
|
}
|
|
194
213
|
}
|
|
195
214
|
|
|
196
215
|
// ------------------------------------------------------------- settings ----
|
|
197
216
|
|
|
198
217
|
function syncSettings(ctx: Ctx, claudeDir: string): void {
|
|
218
|
+
const { change, echo, err, verbose } = ctx.services.logger
|
|
199
219
|
const repoSettings = p(ctx.repoDir, "SoT", ".claude", "settings.json")
|
|
200
220
|
const userSettings = p(claudeDir, "settings.json")
|
|
201
221
|
|
|
@@ -212,7 +232,8 @@ function syncSettings(ctx: Ctx, claudeDir: string): void {
|
|
|
212
232
|
|
|
213
233
|
if (!existsSync(userSettings)) {
|
|
214
234
|
copyFileSync(repoSettings, userSettings)
|
|
215
|
-
|
|
235
|
+
change("Settings installed")
|
|
236
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
216
237
|
return
|
|
217
238
|
}
|
|
218
239
|
|
|
@@ -224,35 +245,47 @@ function syncSettings(ctx: Ctx, claudeDir: string): void {
|
|
|
224
245
|
}
|
|
225
246
|
const repo = parseJson(readFileSync(repoSettings, "utf8"))!
|
|
226
247
|
|
|
227
|
-
copyFileSync(userSettings, `${userSettings}.bak`)
|
|
228
248
|
const merged = ctx.reconcile ? reconcileSettings(repo, user) : mergeSettings(repo, user)
|
|
229
|
-
|
|
249
|
+
const out = jqStringify(merged)
|
|
250
|
+
if (out === readFileSync(userSettings, "utf8")) {
|
|
251
|
+
verbose("Settings already in sync")
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
copyFileSync(userSettings, `${userSettings}.bak`)
|
|
255
|
+
writeFileSync(`${userSettings}.tmp`, out)
|
|
230
256
|
renameSync(`${userSettings}.tmp`, userSettings)
|
|
257
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
231
258
|
if (ctx.reconcile) {
|
|
232
|
-
|
|
259
|
+
change("Settings reconciled (backup at settings.json.bak; user-only keys preserved, permissions arrays replaced by SoT)")
|
|
233
260
|
} else {
|
|
234
|
-
|
|
261
|
+
change("Settings merged (backup at settings.json.bak)")
|
|
235
262
|
}
|
|
236
263
|
}
|
|
237
264
|
|
|
238
265
|
/** Shared shape of the three jq-edit modifiers (compact window, permissive). */
|
|
239
|
-
function jqEditSettings(claudeDir: string, tag: string, edit: (doc: Json) => void):
|
|
266
|
+
function jqEditSettings(ctx: Ctx, claudeDir: string, tag: string, edit: (doc: Json) => void): boolean {
|
|
267
|
+
const { err, warn } = ctx.services.logger
|
|
240
268
|
const userSettings = p(claudeDir, "settings.json")
|
|
241
269
|
if (!existsSync(userSettings)) {
|
|
242
270
|
warn(`(${tag}) ${userSettings} missing — skipped`)
|
|
243
|
-
return
|
|
271
|
+
return false
|
|
244
272
|
}
|
|
245
|
-
const
|
|
273
|
+
const before = readFileSync(userSettings, "utf8")
|
|
274
|
+
const doc = parseJson(before)
|
|
246
275
|
if (doc === undefined) {
|
|
247
276
|
err(`(${tag}) ${userSettings} is not valid JSON — skipped`)
|
|
248
|
-
return
|
|
277
|
+
return false
|
|
249
278
|
}
|
|
250
279
|
edit(doc)
|
|
251
|
-
|
|
280
|
+
const out = jqStringify(doc)
|
|
281
|
+
if (out === before) return false
|
|
282
|
+
writeFileSync(`${userSettings}.tmp`, out)
|
|
252
283
|
renameSync(`${userSettings}.tmp`, userSettings)
|
|
284
|
+
return true
|
|
253
285
|
}
|
|
254
286
|
|
|
255
287
|
function syncCompactWindow(ctx: Ctx, claudeDir: string): void {
|
|
288
|
+
const { change, echo, verbose } = ctx.services.logger
|
|
256
289
|
if (ctx.claudeCompactWindow === "") return
|
|
257
290
|
|
|
258
291
|
if (ctx.dryRun) {
|
|
@@ -260,16 +293,21 @@ function syncCompactWindow(ctx: Ctx, claudeDir: string): void {
|
|
|
260
293
|
return
|
|
261
294
|
}
|
|
262
295
|
|
|
263
|
-
jqEditSettings(claudeDir, "--claude-compact-window", (doc) => {
|
|
296
|
+
const changed = jqEditSettings(ctx, claudeDir, "--claude-compact-window", (doc) => {
|
|
264
297
|
if (!isObject(doc)) return
|
|
265
298
|
const env = isObject(doc["env"]) ? doc["env"] : {}
|
|
266
299
|
env["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] = ctx.claudeCompactWindow
|
|
267
300
|
doc["env"] = env
|
|
268
301
|
})
|
|
269
|
-
|
|
302
|
+
if (changed) {
|
|
303
|
+
change(`Compact window: set to ${ctx.claudeCompactWindow} tokens in deployed settings (SoT and model unchanged; flag-less sync reverts)`)
|
|
304
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
305
|
+
}
|
|
306
|
+
else verbose(`Compact window: already set to ${ctx.claudeCompactWindow} tokens in deployed settings`)
|
|
270
307
|
}
|
|
271
308
|
|
|
272
309
|
function syncPermissive(ctx: Ctx, claudeDir: string): void {
|
|
310
|
+
const { change, echo, verbose } = ctx.services.logger
|
|
273
311
|
if (!ctx.claudePermissive) return
|
|
274
312
|
|
|
275
313
|
if (ctx.dryRun) {
|
|
@@ -277,19 +315,24 @@ function syncPermissive(ctx: Ctx, claudeDir: string): void {
|
|
|
277
315
|
return
|
|
278
316
|
}
|
|
279
317
|
|
|
280
|
-
jqEditSettings(claudeDir, "--claude-permissive", (doc) => {
|
|
318
|
+
const changed = jqEditSettings(ctx, claudeDir, "--claude-permissive", (doc) => {
|
|
281
319
|
if (!isObject(doc)) return
|
|
282
320
|
const permissions = isObject(doc["permissions"]) ? doc["permissions"] : {}
|
|
283
321
|
permissions["ask"] = []
|
|
284
322
|
permissions["deny"] = []
|
|
285
323
|
doc["permissions"] = permissions
|
|
286
324
|
})
|
|
287
|
-
|
|
325
|
+
if (changed) {
|
|
326
|
+
change("Permissive mode: permissions.ask/deny emptied in deployed settings (sandbox use; SoT unchanged)")
|
|
327
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
328
|
+
}
|
|
329
|
+
else verbose("Permissive mode: permissions.ask/deny already empty in deployed settings")
|
|
288
330
|
}
|
|
289
331
|
|
|
290
332
|
// ---------------------------------------------------------- claude.json ----
|
|
291
333
|
|
|
292
334
|
function syncClaudeJson(ctx: Ctx): void {
|
|
335
|
+
const { change, echo, err, verbose } = ctx.services.logger
|
|
293
336
|
const claudeJson = p(ctx.home, ".claude.json")
|
|
294
337
|
const mcpSot = p(ctx.repoDir, "SoT", ".claude", "mcp-servers.json")
|
|
295
338
|
|
|
@@ -311,37 +354,49 @@ function syncClaudeJson(ctx: Ctx): void {
|
|
|
311
354
|
}
|
|
312
355
|
}
|
|
313
356
|
|
|
357
|
+
let changed = true
|
|
314
358
|
if (existsSync(claudeJson)) {
|
|
315
|
-
const
|
|
359
|
+
const before = readFileSync(claudeJson, "utf8")
|
|
360
|
+
const doc = parseJson(before)
|
|
316
361
|
if (doc === undefined) {
|
|
317
362
|
err("Skipping ~/.claude.json edit: not valid JSON. Fix or delete it.")
|
|
318
363
|
return
|
|
319
364
|
}
|
|
320
365
|
const obj = isObject(doc) ? doc : {}
|
|
321
366
|
applyFilter(obj)
|
|
322
|
-
|
|
323
|
-
|
|
367
|
+
const out = jqStringify(obj)
|
|
368
|
+
if (out === before) {
|
|
369
|
+
changed = false
|
|
370
|
+
} else {
|
|
371
|
+
writeFileSync(`${claudeJson}.tmp`, out)
|
|
372
|
+
renameSync(`${claudeJson}.tmp`, claudeJson)
|
|
373
|
+
}
|
|
324
374
|
} else {
|
|
325
375
|
const obj: { [k: string]: Json } = {}
|
|
326
376
|
applyFilter(obj)
|
|
327
377
|
writeFileSync(claudeJson, jqStringify(obj))
|
|
328
378
|
}
|
|
329
|
-
|
|
379
|
+
if (changed) {
|
|
380
|
+
change(`~/.claude.json updated (showTurnDuration${haveMcp ? ", mcpServers" : ""})`)
|
|
381
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
382
|
+
}
|
|
383
|
+
else verbose(`~/.claude.json already in sync (showTurnDuration${haveMcp ? ", mcpServers" : ""})`)
|
|
330
384
|
}
|
|
331
385
|
|
|
332
386
|
// -------------------------------------------------------- connector env ----
|
|
333
387
|
|
|
334
388
|
function syncConnectorEnv(ctx: Ctx): void {
|
|
389
|
+
const { change, echo, verbose, warn } = ctx.services.logger
|
|
335
390
|
// win32: Claude Code launches from PowerShell/GUI, so the flag must be a
|
|
336
391
|
// real user env var (setx), not a Git-Bash-only shell-rc export. Never
|
|
337
392
|
// clobbers an existing value (set =true yourself to keep connectors).
|
|
338
|
-
if (
|
|
393
|
+
if (ctx.services.platform.isWindows()) {
|
|
339
394
|
const existing = spawnSync("reg", ["query", "HKCU\\Environment", "/v", "ENABLE_CLAUDEAI_MCP_SERVERS"], {
|
|
340
395
|
stdio: "ignore"
|
|
341
396
|
})
|
|
342
397
|
if (existing.error === undefined && existing.status === 0) {
|
|
343
398
|
if (ctx.dryRun) echo("[dry-run] ENABLE_CLAUDEAI_MCP_SERVERS already in user environment — would skip")
|
|
344
|
-
else
|
|
399
|
+
else verbose("claude.ai connectors: ENABLE_CLAUDEAI_MCP_SERVERS already set in user environment (left as-is)")
|
|
345
400
|
return
|
|
346
401
|
}
|
|
347
402
|
if (ctx.dryRun) {
|
|
@@ -350,7 +405,8 @@ function syncConnectorEnv(ctx: Ctx): void {
|
|
|
350
405
|
}
|
|
351
406
|
const res = spawnSync("setx", ["ENABLE_CLAUDEAI_MCP_SERVERS", "false"], { stdio: "ignore" })
|
|
352
407
|
if (res.error === undefined && res.status === 0) {
|
|
353
|
-
|
|
408
|
+
change("claude.ai connectors disabled via setx (open a new terminal to apply)")
|
|
409
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
354
410
|
} else {
|
|
355
411
|
warn("setx ENABLE_CLAUDEAI_MCP_SERVERS false failed — set it manually in System Properties > Environment Variables")
|
|
356
412
|
}
|
|
@@ -366,7 +422,7 @@ function syncConnectorEnv(ctx: Ctx): void {
|
|
|
366
422
|
if (ctx.dryRun) {
|
|
367
423
|
echo(`[dry-run] ENABLE_CLAUDEAI_MCP_SERVERS already in ${f} — would skip`)
|
|
368
424
|
} else {
|
|
369
|
-
|
|
425
|
+
verbose(`claude.ai connectors: ENABLE_CLAUDEAI_MCP_SERVERS already set in ${f} (left as-is)`)
|
|
370
426
|
}
|
|
371
427
|
return
|
|
372
428
|
}
|
|
@@ -382,7 +438,8 @@ function syncConnectorEnv(ctx: Ctx): void {
|
|
|
382
438
|
}
|
|
383
439
|
|
|
384
440
|
appendFileSync(target, `\n${marker}\n${line}\n`)
|
|
385
|
-
|
|
441
|
+
change(`claude.ai connectors disabled via ${target} (start a new shell to apply)`)
|
|
442
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
386
443
|
}
|
|
387
444
|
|
|
388
445
|
// ------------------------------------------------------------- removals ----
|
|
@@ -439,6 +496,7 @@ function pruneJsonKeys(ctx: Ctx, file: string, keys: Array<string>): number {
|
|
|
439
496
|
}
|
|
440
497
|
|
|
441
498
|
function syncRemovals(ctx: Ctx, claudeDir: string): void {
|
|
499
|
+
const { change, echo } = ctx.services.logger
|
|
442
500
|
let hooksRemoved = 0
|
|
443
501
|
let filesRemoved = 0
|
|
444
502
|
|
|
@@ -474,7 +532,8 @@ function syncRemovals(ctx: Ctx, claudeDir: string): void {
|
|
|
474
532
|
}
|
|
475
533
|
|
|
476
534
|
if (hooksRemoved + filesRemoved + skeys + cjkeys > 0) {
|
|
477
|
-
|
|
535
|
+
change(`Pruned stale artifacts (hooks: ${hooksRemoved}, files: ${filesRemoved}, settings keys: ${skeys}, claude.json keys: ${cjkeys})`)
|
|
536
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
478
537
|
}
|
|
479
538
|
}
|
|
480
539
|
|
|
@@ -504,6 +563,7 @@ function pluginUserScopeInstalled(installedPlugins: string, pluginId: string): b
|
|
|
504
563
|
}
|
|
505
564
|
|
|
506
565
|
function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
566
|
+
const { change, echo, verbose, warn } = ctx.services.logger
|
|
507
567
|
const repoSettingsFile = p(ctx.repoDir, "SoT", ".claude", "settings.json")
|
|
508
568
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
509
569
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
@@ -516,10 +576,18 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
516
576
|
return
|
|
517
577
|
}
|
|
518
578
|
|
|
519
|
-
if (
|
|
579
|
+
if (ctx.services.deps.probe("claude").state === "missing") {
|
|
520
580
|
warn("claude CLI not in PATH — skipping plugin reconcile (run /plugin marketplace add + /plugin install manually)")
|
|
521
581
|
return
|
|
522
582
|
}
|
|
583
|
+
if (ctx.services.deps.probe("git").state === "missing") {
|
|
584
|
+
ctx.services.deps.warnMissing(
|
|
585
|
+
"git",
|
|
586
|
+
ctx.services.logger,
|
|
587
|
+
"plugin marketplaces are git repos — Claude plugin passes skipped; re-run sync after installing"
|
|
588
|
+
)
|
|
589
|
+
return
|
|
590
|
+
}
|
|
523
591
|
|
|
524
592
|
const repoSettings = readJsonFile(repoSettingsFile)
|
|
525
593
|
const repoObj = repoSettings !== undefined && isObject(repoSettings) ? repoSettings : {}
|
|
@@ -599,29 +667,37 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
599
667
|
}
|
|
600
668
|
|
|
601
669
|
// Pass 6 — re-assert SoT enabled-state in the user settings.
|
|
602
|
-
reassertEnabledState(repoObj, p(claudeDir, "settings.json"))
|
|
670
|
+
if (reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
|
|
671
|
+
change("Plugin enable-state re-asserted from SoT in settings.json")
|
|
672
|
+
ctx.nextStepTriggers.claudePlugins = true
|
|
673
|
+
}
|
|
603
674
|
|
|
604
675
|
const failed = f1 + f2 + f4 + f5
|
|
605
676
|
if (addedMp > 0 || addedPl > 0 || updatedPl > 0 || removedPl > 0 || removedMp > 0) {
|
|
606
|
-
|
|
677
|
+
change(`Plugins synced (marketplaces: +${addedMp} -${removedMp}, plugins: +${addedPl} ~${updatedPl} -${removedPl})`)
|
|
678
|
+
ctx.nextStepTriggers.claudePlugins = true
|
|
607
679
|
} else {
|
|
608
|
-
|
|
680
|
+
verbose("Plugins already in sync")
|
|
609
681
|
}
|
|
610
682
|
if (failed > 0) {
|
|
611
683
|
warn(`${failed} plugin operation(s) failed — re-run sync or install manually`)
|
|
612
684
|
}
|
|
613
685
|
}
|
|
614
686
|
|
|
615
|
-
function reassertEnabledState(repoObj: { [k: string]: Json }, userSettingsFile: string):
|
|
616
|
-
|
|
687
|
+
function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSettingsFile: string): boolean {
|
|
688
|
+
const { warn } = ctx.services.logger
|
|
689
|
+
if (!existsSync(userSettingsFile)) return false
|
|
617
690
|
const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
|
|
618
691
|
|
|
692
|
+
let cliDisabled = false
|
|
619
693
|
for (const [pluginId, value] of Object.entries(sotPlugins)) {
|
|
620
694
|
if (value !== false) continue
|
|
621
695
|
const user = readJsonFile(userSettingsFile)
|
|
622
696
|
const enabled = user !== undefined && isObject(user) && isObject(user["enabledPlugins"]) ? (user["enabledPlugins"] as { [k: string]: Json })[pluginId] : undefined
|
|
623
697
|
if (enabled !== true) continue
|
|
624
|
-
if (
|
|
698
|
+
if (cli(["plugin", "disable", pluginId]).ok) {
|
|
699
|
+
cliDisabled = true
|
|
700
|
+
} else {
|
|
625
701
|
warn(`Failed to disable SoT-false plugin: ${pluginId} (will retry next sync)`)
|
|
626
702
|
}
|
|
627
703
|
}
|
|
@@ -629,44 +705,66 @@ function reassertEnabledState(repoObj: { [k: string]: Json }, userSettingsFile:
|
|
|
629
705
|
const user = readJsonFile(userSettingsFile)
|
|
630
706
|
if (user === undefined || !isObject(user)) {
|
|
631
707
|
warn("enabledPlugins re-assert failed — false-keyed plugins may be left enabled")
|
|
632
|
-
return
|
|
708
|
+
return false
|
|
633
709
|
}
|
|
710
|
+
const beforeCanonical = jqStringify(user)
|
|
634
711
|
user["enabledPlugins"] = deepMerge(isObject(user["enabledPlugins"]) ? user["enabledPlugins"] : {}, sotPlugins)
|
|
635
|
-
|
|
712
|
+
const out = jqStringify(user)
|
|
713
|
+
if (out === beforeCanonical) return cliDisabled
|
|
714
|
+
writeFileSync(`${userSettingsFile}.tmp`, out)
|
|
636
715
|
renameSync(`${userSettingsFile}.tmp`, userSettingsFile)
|
|
716
|
+
return true
|
|
637
717
|
}
|
|
638
718
|
|
|
639
719
|
// ------------------------------------------------------ optional plugins ----
|
|
640
720
|
|
|
641
|
-
function enableOptionalPlugin(claudeDir: string, pluginId: string, marketplaceRepo: string):
|
|
721
|
+
function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): boolean {
|
|
722
|
+
const { change, verbose, warn } = ctx.services.logger
|
|
642
723
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
643
724
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
644
725
|
const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1)
|
|
645
726
|
|
|
727
|
+
let marketplaceAdded = false
|
|
646
728
|
if (marketplaceRepo !== "") {
|
|
647
729
|
const known = readJsonFile(knownMarketplaces)
|
|
648
730
|
const has = known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false
|
|
649
|
-
if (!has
|
|
650
|
-
|
|
651
|
-
|
|
731
|
+
if (!has) {
|
|
732
|
+
if (!cli(["plugin", "marketplace", "add", marketplaceRepo]).ok) {
|
|
733
|
+
warn(`Failed to add marketplace ${marketplaceRepo} for ${pluginId}`)
|
|
734
|
+
return false
|
|
735
|
+
}
|
|
736
|
+
marketplaceAdded = true
|
|
652
737
|
}
|
|
653
738
|
}
|
|
654
739
|
|
|
655
|
-
|
|
740
|
+
const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
|
|
741
|
+
if (!wasInstalled) {
|
|
656
742
|
if (!cli(["plugin", "install", pluginId]).ok) {
|
|
743
|
+
if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
|
|
657
744
|
warn(`Failed to install optional plugin ${pluginId}`)
|
|
658
|
-
return
|
|
745
|
+
return marketplaceAdded
|
|
659
746
|
}
|
|
660
747
|
}
|
|
661
748
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
749
|
+
const settingsDoc = readJsonFile(p(claudeDir, "settings.json"))
|
|
750
|
+
const wasEnabled =
|
|
751
|
+
settingsDoc !== undefined && isObject(settingsDoc) && isObject(settingsDoc["enabledPlugins"])
|
|
752
|
+
? (settingsDoc["enabledPlugins"] as { [k: string]: Json })[pluginId] === true
|
|
753
|
+
: false
|
|
754
|
+
|
|
755
|
+
if (!cli(["plugin", "enable", pluginId]).ok) {
|
|
756
|
+
if (marketplaceAdded || !wasInstalled) change(`Optional plugin ${pluginId}: installed (enable failed — will retry next sync)`)
|
|
665
757
|
warn(`Failed to enable optional plugin ${pluginId}`)
|
|
758
|
+
return marketplaceAdded || !wasInstalled
|
|
666
759
|
}
|
|
760
|
+
const changed = marketplaceAdded || !wasInstalled || !wasEnabled
|
|
761
|
+
if (changed) change(`Optional plugin opted in: ${pluginId}`)
|
|
762
|
+
else verbose(`Optional plugin already opted in: ${pluginId} (enable re-asserted)`)
|
|
763
|
+
return changed
|
|
667
764
|
}
|
|
668
765
|
|
|
669
766
|
function syncOptionalPlugins(ctx: Ctx, claudeDir: string): void {
|
|
767
|
+
const { echo, warn } = ctx.services.logger
|
|
670
768
|
if (ctx.claudePlugins.length === 0) return
|
|
671
769
|
|
|
672
770
|
if (ctx.dryRun) {
|
|
@@ -679,16 +777,16 @@ function syncOptionalPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
679
777
|
return
|
|
680
778
|
}
|
|
681
779
|
|
|
682
|
-
if (
|
|
780
|
+
if (ctx.services.deps.probe("claude").state === "missing") {
|
|
683
781
|
warn("claude CLI not in PATH — cannot opt in optional plugins (--claude-plugin)")
|
|
684
782
|
return
|
|
685
783
|
}
|
|
686
784
|
|
|
687
785
|
if (ctx.claudePlugins.includes("supabase")) {
|
|
688
|
-
enableOptionalPlugin(claudeDir, "supabase@claude-plugins-official", "")
|
|
786
|
+
if (enableOptionalPlugin(ctx, claudeDir, "supabase@claude-plugins-official", "")) ctx.nextStepTriggers.claudePlugins = true
|
|
689
787
|
}
|
|
690
788
|
if (ctx.claudePlugins.includes("n8n")) {
|
|
691
|
-
enableOptionalPlugin(claudeDir, "n8n-mcp-skills@n8n-mcp-skills", "czlonkowski/n8n-skills")
|
|
789
|
+
if (enableOptionalPlugin(ctx, claudeDir, "n8n-mcp-skills@n8n-mcp-skills", "czlonkowski/n8n-skills")) ctx.nextStepTriggers.claudePlugins = true
|
|
692
790
|
}
|
|
693
791
|
}
|
|
694
792
|
|
|
@@ -700,6 +798,7 @@ function lspPkg(ctx: Ctx, tool: string, pkg: string): string {
|
|
|
700
798
|
}
|
|
701
799
|
|
|
702
800
|
function syncLspServers(ctx: Ctx): void {
|
|
801
|
+
const { change, echo, verbose, warn } = ctx.services.logger
|
|
703
802
|
const sot = readJsonFile(p(ctx.repoDir, "SoT", ".claude", "settings.json"))
|
|
704
803
|
const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
|
|
705
804
|
if (enabled === undefined) return
|
|
@@ -708,17 +807,17 @@ function syncLspServers(ctx: Ctx): void {
|
|
|
708
807
|
if (!hasPhp && !hasTs) return
|
|
709
808
|
|
|
710
809
|
const missing: Array<string> = []
|
|
711
|
-
if (hasPhp &&
|
|
810
|
+
if (hasPhp && ctx.services.deps.probe("intelephense").state === "missing") missing.push(lspPkg(ctx, "intelephense", "intelephense"))
|
|
712
811
|
if (hasTs) {
|
|
713
|
-
if (
|
|
714
|
-
if (
|
|
812
|
+
if (ctx.services.deps.probe("typescript-language-server").state === "missing") missing.push(lspPkg(ctx, "typescript-language-server", "typescript-language-server"))
|
|
813
|
+
if (ctx.services.deps.probe("tsc").state === "missing") missing.push(lspPkg(ctx, "tsc", "typescript"))
|
|
715
814
|
}
|
|
716
815
|
|
|
717
816
|
if (missing.length === 0) {
|
|
718
817
|
if (ctx.dryRun) {
|
|
719
818
|
echo("[dry-run] LSP server binaries present")
|
|
720
819
|
} else {
|
|
721
|
-
|
|
820
|
+
verbose("LSP server binaries present")
|
|
722
821
|
}
|
|
723
822
|
return
|
|
724
823
|
}
|
|
@@ -729,14 +828,19 @@ function syncLspServers(ctx: Ctx): void {
|
|
|
729
828
|
return
|
|
730
829
|
}
|
|
731
830
|
|
|
732
|
-
if (
|
|
733
|
-
|
|
831
|
+
if (ctx.services.deps.probe("npm").state === "missing") {
|
|
832
|
+
ctx.services.deps.warnMissing(
|
|
833
|
+
"npm",
|
|
834
|
+
ctx.services.logger,
|
|
835
|
+
`cannot install LSP servers (${specs}); the php-lsp/typescript-lsp plugins stay no-ops`
|
|
836
|
+
)
|
|
734
837
|
return
|
|
735
838
|
}
|
|
736
839
|
|
|
737
|
-
|
|
840
|
+
verbose(`Installing LSP servers via npm: ${specs}...`)
|
|
738
841
|
if (spawnSync("npm", ["install", "-g", ...missing], { stdio: "ignore" }).status === 0) {
|
|
739
|
-
|
|
842
|
+
change(`LSP servers installed (${specs})`)
|
|
843
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
740
844
|
} else {
|
|
741
845
|
warn(`npm install -g ${specs} failed. Try manually: npm install -g ${specs}`)
|
|
742
846
|
}
|
|
@@ -745,17 +849,18 @@ function syncLspServers(ctx: Ctx): void {
|
|
|
745
849
|
// -------------------------------------------------------------- summary ----
|
|
746
850
|
|
|
747
851
|
export function claudeSummary(ctx: Ctx): void {
|
|
852
|
+
const { echo } = ctx.services.logger
|
|
748
853
|
const claudeDir = p(ctx.home, ".claude")
|
|
749
854
|
echo(`Claude: ${claudeDir}`)
|
|
750
855
|
if (!ctx.dryRun) {
|
|
751
856
|
echo(`Hooks: ${shellScriptCount(p(claudeDir, "hooks"))} scripts`)
|
|
752
|
-
if (
|
|
753
|
-
const
|
|
754
|
-
echo(`RTK: ${
|
|
857
|
+
if (ctx.services.deps.probe("rtk").state === "present") {
|
|
858
|
+
const version = ctx.services.deps.version("rtk")
|
|
859
|
+
echo(`RTK: ${version !== "" ? version : "installed"}`)
|
|
755
860
|
} else {
|
|
756
861
|
echo("RTK: not installed")
|
|
757
862
|
}
|
|
758
|
-
if (
|
|
863
|
+
if (ctx.services.deps.probe("claude").state === "present") {
|
|
759
864
|
const installed = readJsonFile(p(claudeDir, "plugins", "installed_plugins.json"))
|
|
760
865
|
const count = installed !== undefined && isObject(installed) && isObject(installed["plugins"]) ? Object.keys(installed["plugins"]).length : 0
|
|
761
866
|
echo(`Plugins: ${count} installed (from SoT enabledPlugins + Anthropic auto-installs)`)
|
|
@@ -765,7 +870,13 @@ export function claudeSummary(ctx: Ctx): void {
|
|
|
765
870
|
}
|
|
766
871
|
}
|
|
767
872
|
|
|
768
|
-
export function claudeNextSteps():
|
|
769
|
-
|
|
770
|
-
|
|
873
|
+
export function claudeNextSteps(ctx: Ctx): Array<string> {
|
|
874
|
+
const lines: Array<string> = []
|
|
875
|
+
if (ctx.verbose || ctx.nextStepTriggers.claudePlugins) {
|
|
876
|
+
lines.push("In a Claude Code session, run /reload-plugins to pick up newly installed plugins.")
|
|
877
|
+
}
|
|
878
|
+
if (ctx.verbose || ctx.nextStepTriggers.claudeRestart) {
|
|
879
|
+
lines.push("Restart Claude Code for hook/env-var changes to take effect.")
|
|
880
|
+
}
|
|
881
|
+
return lines
|
|
771
882
|
}
|