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