docks-kit 0.14.3 → 0.15.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 +3 -1
- package/README.md +11 -5
- package/cli/docs/flags.md +6 -0
- package/cli/docs/overview.md +1 -1
- package/cli/src/argv.ts +433 -0
- package/cli/src/commands/docs.ts +6 -6
- package/cli/src/commands/model.ts +11 -11
- package/cli/src/commands/models.ts +6 -6
- package/cli/src/commands/plugins.ts +6 -6
- package/cli/src/commands/skills.ts +6 -6
- package/cli/src/commands/status.ts +3 -3
- package/cli/src/commands/sync.ts +43 -92
- package/cli/src/commands/toolchain.ts +12 -12
- package/cli/src/commands/update.ts +3 -3
- package/cli/src/engine-native/DESIGN.md +15 -4
- package/cli/src/engine-native/bun.ts +22 -21
- package/cli/src/engine-native/claudeSync.ts +30 -31
- package/cli/src/engine-native/codexSync.ts +24 -24
- package/cli/src/engine-native/deps.ts +43 -35
- package/cli/src/engine-native/exec.ts +61 -5
- package/cli/src/engine-native/index.ts +141 -33
- package/cli/src/engine-native/logger.ts +114 -14
- package/cli/src/engine-native/modes.ts +4 -4
- package/cli/src/engine-native/parseArgs.ts +1 -1
- package/cli/src/engine-native/services.ts +5 -5
- package/cli/src/engine-native/skillsSync.ts +19 -20
- package/cli/src/engine-native/toolchain.ts +40 -27
- package/cli/src/engine.ts +2 -2
- package/cli/src/generated/sotPayload.ts +1 -1
- package/cli/src/main.ts +27 -28
- package/cli/src/services.ts +4 -4
- package/docks-kit +1 -1
- package/package.json +5 -7
package/cli/src/commands/sync.ts
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Argument, Command, Flag } from "effect/unstable/cli"
|
|
2
2
|
import { Effect, Option } from "effect"
|
|
3
3
|
import { spawnSync } from "node:child_process"
|
|
4
4
|
import { existsSync } from "node:fs"
|
|
5
5
|
import { join } from "node:path"
|
|
6
6
|
import { bail, engine } from "../engine"
|
|
7
7
|
import type { Logger } from "../engine-native/logger"
|
|
8
|
-
import {
|
|
9
|
-
advisorCatalog,
|
|
10
|
-
advisorFlagGrammar,
|
|
11
|
-
effortCatalog,
|
|
12
|
-
effortFlagGrammar
|
|
13
|
-
} from "../efforts"
|
|
14
8
|
import { kitHome } from "../kitHome"
|
|
15
|
-
import { modelCatalog, type Tool } from "../manifests"
|
|
16
9
|
import { LoggerService } from "../services"
|
|
17
10
|
|
|
18
11
|
/** Best-effort update autodetection: nudge (never block, never fail) when
|
|
@@ -38,90 +31,66 @@ const updateNudge = (logger: Logger): void => {
|
|
|
38
31
|
|
|
39
32
|
const VALID_TARGETS = ["claude", "codex", "agents"]
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"--force": "--force was renamed to --reconcile",
|
|
46
|
-
"--remove-plugins":
|
|
47
|
-
"--remove-plugins was renamed to --prune (it also removes marketplaces + kit-managed skills)",
|
|
48
|
-
"--680k": "--680k was renamed to --claude-compact-window=680k",
|
|
49
|
-
"--permissive": "--permissive was renamed to --claude-permissive",
|
|
50
|
-
"--supabase": "--supabase was renamed to --claude-plugin=supabase",
|
|
51
|
-
"--n8n": "--n8n was renamed to --claude-plugin=n8n",
|
|
52
|
-
"--skip-rtk": "--skip-rtk was renamed to --skip-bubblewrap",
|
|
53
|
-
"--claude": "--claude was renamed: pass the target as a word, e.g. 'sync claude'",
|
|
54
|
-
"--codex": "--codex was renamed: pass the target as a word, e.g. 'sync codex'",
|
|
55
|
-
"--agents": "--agents was renamed: pass the target as a word, e.g. 'sync agents'"
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const modelCatalogHint = (t: Tool): string => {
|
|
59
|
-
const c = modelCatalog(t)
|
|
60
|
-
const list = c.models
|
|
61
|
-
.map((m) => ` ${m.id}${m.note !== undefined ? ` — ${m.note}` : ""}`)
|
|
62
|
-
.join("\n")
|
|
63
|
-
return `Available ${t} models (kit-verified ${c.verified} — SoT/models.json):\n${list}`
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const targets = Args.text({ name: "target" }).pipe(
|
|
67
|
-
Args.withDescription("Sync targets: claude, codex, agents (default: all three)"),
|
|
68
|
-
Args.repeated
|
|
34
|
+
const targets = Argument.variadic(
|
|
35
|
+
Argument.string("target").pipe(
|
|
36
|
+
Argument.withDescription("Sync targets: claude, codex, agents (default: all three)")
|
|
37
|
+
)
|
|
69
38
|
)
|
|
70
39
|
|
|
71
|
-
const dryRun =
|
|
72
|
-
|
|
40
|
+
const dryRun = Flag.boolean("dry-run").pipe(
|
|
41
|
+
Flag.withDescription("Preview without applying")
|
|
73
42
|
)
|
|
74
|
-
const reconcile =
|
|
75
|
-
|
|
43
|
+
const reconcile = Flag.boolean("reconcile").pipe(
|
|
44
|
+
Flag.withDescription("Reconcile kit-owned settings with SoT (SoT keys win; user-only keys preserved; permissions arrays replaced)")
|
|
76
45
|
)
|
|
77
|
-
const prune =
|
|
78
|
-
|
|
46
|
+
const prune = Flag.boolean("prune").pipe(
|
|
47
|
+
Flag.withDescription("Uninstall kit-managed installs not in SoT (plugins, marketplaces, universal skills)")
|
|
79
48
|
)
|
|
80
|
-
const skipBubblewrap =
|
|
81
|
-
|
|
49
|
+
const skipBubblewrap = Flag.boolean("skip-bubblewrap").pipe(
|
|
50
|
+
Flag.withDescription("Skip optional bubblewrap bootstrap (Codex Linux sandbox)")
|
|
82
51
|
)
|
|
83
|
-
const skipPluginRefresh =
|
|
84
|
-
|
|
52
|
+
const skipPluginRefresh = Flag.boolean("skip-plugin-refresh").pipe(
|
|
53
|
+
Flag.withDescription("Install missing plugins but skip refresh-only updates for existing plugins")
|
|
85
54
|
)
|
|
86
|
-
const yes =
|
|
87
|
-
|
|
55
|
+
const yes = Flag.boolean("yes").pipe(
|
|
56
|
+
Flag.withDescription("Auto-accept toolchain prompts (containers/CI)")
|
|
88
57
|
)
|
|
89
|
-
const verbose =
|
|
90
|
-
|
|
91
|
-
|
|
58
|
+
const verbose = Flag.boolean("verbose").pipe(
|
|
59
|
+
Flag.withAlias("v"),
|
|
60
|
+
Flag.withDescription("Also print no-op confirmations (already in sync, up to date, left as-is)")
|
|
92
61
|
)
|
|
93
|
-
const claudeModel =
|
|
94
|
-
|
|
95
|
-
|
|
62
|
+
const claudeModel = Flag.string("claude-model").pipe(
|
|
63
|
+
Flag.withDescription("Deploy-time modifier: set deployed Claude model (see `docks-kit models claude`)"),
|
|
64
|
+
Flag.optional
|
|
96
65
|
)
|
|
97
|
-
const claudeEffort =
|
|
98
|
-
|
|
99
|
-
|
|
66
|
+
const claudeEffort = Flag.string("claude-effort").pipe(
|
|
67
|
+
Flag.withDescription("Deploy-time modifier: set Claude effortLevel (bare flag shows valid levels)"),
|
|
68
|
+
Flag.optional
|
|
100
69
|
)
|
|
101
|
-
const claudeAdvisor =
|
|
102
|
-
|
|
103
|
-
|
|
70
|
+
const claudeAdvisor = Flag.string("claude-advisor").pipe(
|
|
71
|
+
Flag.withDescription("Deploy-time modifier: set Claude advisor on/off/default"),
|
|
72
|
+
Flag.optional
|
|
104
73
|
)
|
|
105
|
-
const claudeCompactWindow =
|
|
106
|
-
|
|
107
|
-
|
|
74
|
+
const claudeCompactWindow = Flag.string("claude-compact-window").pipe(
|
|
75
|
+
Flag.withDescription("Deploy-time modifier: set deployed autocompact window in tokens (e.g. 680000 or 680k)"),
|
|
76
|
+
Flag.optional
|
|
108
77
|
)
|
|
109
|
-
const claudePermissive =
|
|
110
|
-
|
|
78
|
+
const claudePermissive = Flag.boolean("claude-permissive").pipe(
|
|
79
|
+
Flag.withDescription("Deploy-time modifier: empty permissions.ask/deny in deployed settings (sandboxes)")
|
|
111
80
|
)
|
|
112
|
-
const claudePlugin =
|
|
113
|
-
|
|
81
|
+
const claudePlugin = Flag.string("claude-plugin").pipe(
|
|
82
|
+
Flag.withDescription(
|
|
114
83
|
"Sticky opt-in plugin(s); repeatable and/or comma-separated (known: supabase, n8n)"
|
|
115
84
|
),
|
|
116
|
-
|
|
85
|
+
Flag.atLeast(0)
|
|
117
86
|
)
|
|
118
|
-
const codexModel =
|
|
119
|
-
|
|
120
|
-
|
|
87
|
+
const codexModel = Flag.string("codex-model").pipe(
|
|
88
|
+
Flag.withDescription("Deploy-time modifier: set deployed Codex model (see `docks-kit models codex`)"),
|
|
89
|
+
Flag.optional
|
|
121
90
|
)
|
|
122
|
-
const codexEffort =
|
|
123
|
-
|
|
124
|
-
|
|
91
|
+
const codexEffort = Flag.string("codex-effort").pipe(
|
|
92
|
+
Flag.withDescription("Deploy-time modifier: set Codex model_reasoning_effort (bare flag shows valid levels)"),
|
|
93
|
+
Flag.optional
|
|
125
94
|
)
|
|
126
95
|
|
|
127
96
|
export const syncCommand = Command.make(
|
|
@@ -146,24 +115,6 @@ export const syncCommand = Command.make(
|
|
|
146
115
|
},
|
|
147
116
|
(config) =>
|
|
148
117
|
Effect.gen(function* () {
|
|
149
|
-
for (const t of config.targets) {
|
|
150
|
-
if (VALID_TARGETS.includes(t)) continue
|
|
151
|
-
if (t === "--claude-model" || t === "--codex-model") {
|
|
152
|
-
const tool: Tool = t === "--claude-model" ? "claude" : "codex"
|
|
153
|
-
return yield* bail(`${modelCatalogHint(tool)}\n${t} requires a value: ${t}=<model>`)
|
|
154
|
-
}
|
|
155
|
-
if (t === "--claude-effort" || t === "--codex-effort") {
|
|
156
|
-
const tool: Tool = t === "--claude-effort" ? "claude" : "codex"
|
|
157
|
-
return yield* bail(`${effortCatalog(tool)}\n${t} requires a value: ${effortFlagGrammar(tool)}`)
|
|
158
|
-
}
|
|
159
|
-
if (t === "--claude-advisor") {
|
|
160
|
-
return yield* bail(`${advisorCatalog()}\n${t} requires a value: ${advisorFlagGrammar()}`)
|
|
161
|
-
}
|
|
162
|
-
const hint = LEGACY_HINTS[t]
|
|
163
|
-
if (hint !== undefined) {
|
|
164
|
-
return yield* bail(hint)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
118
|
const bad = config.targets.filter((t) => !VALID_TARGETS.includes(t))
|
|
168
119
|
if (bad.length > 0) {
|
|
169
120
|
return yield* bail(
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Argument, Command, Flag } from "effect/unstable/cli"
|
|
2
2
|
import { Effect, Option } from "effect"
|
|
3
3
|
import { bail, engine } from "../engine"
|
|
4
4
|
|
|
5
5
|
const MANAGED = ["bun", "effect-solutions"]
|
|
6
6
|
|
|
7
|
-
const op =
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const op = Argument.string("op").pipe(
|
|
8
|
+
Argument.withDescription("check (default) | ensure <tool>"),
|
|
9
|
+
Argument.optional
|
|
10
10
|
)
|
|
11
|
-
const tool =
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const tool = Argument.string("tool").pipe(
|
|
12
|
+
Argument.withDescription(`Managed tool for ensure: ${MANAGED.join(", ")}`),
|
|
13
|
+
Argument.optional
|
|
14
14
|
)
|
|
15
|
-
const yes =
|
|
16
|
-
|
|
15
|
+
const yes = Flag.boolean("yes").pipe(
|
|
16
|
+
Flag.withDescription("Auto-accept above-verified installs")
|
|
17
17
|
)
|
|
18
|
-
const verbose =
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const verbose = Flag.boolean("verbose").pipe(
|
|
19
|
+
Flag.withAlias("v"),
|
|
20
|
+
Flag.withDescription("Also print no-op confirmations (present, up to date)")
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
export const toolchainCommand = Command.make("toolchain", { op, tool, yes, verbose }, (config) =>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command,
|
|
1
|
+
import { Command, Flag } from "effect/unstable/cli"
|
|
2
2
|
import { Console, Effect } from "effect"
|
|
3
3
|
import { spawnSync } from "node:child_process"
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs"
|
|
@@ -6,8 +6,8 @@ import { join } from "node:path"
|
|
|
6
6
|
import { bail, compiled } from "../engine"
|
|
7
7
|
import { kitHome } from "../kitHome"
|
|
8
8
|
|
|
9
|
-
const noSync =
|
|
10
|
-
|
|
9
|
+
const noSync = Flag.boolean("no-sync").pipe(
|
|
10
|
+
Flag.withDescription("Update the kit only; skip the chained flag-less sync")
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
const git = (home: string, args: Array<string>): { ok: boolean; out: string } => {
|
|
@@ -31,6 +31,11 @@ explicit removed-engine diagnostic and exits 2 with the recovery tag message.
|
|
|
31
31
|
materialized settings, writes runtime assets, commits settings, then performs
|
|
32
32
|
readiness-gated legacy cleanup. Modifiers run after the base commit, removals
|
|
33
33
|
before plugins, and LSP checks after plugin state.
|
|
34
|
+
- **Pipeline concurrency is bounded.** Selected Claude, Codex, and skills
|
|
35
|
+
pipelines overlap through one input-ordered pool. The production default is
|
|
36
|
+
3; `DOCKS_KIT_SYNC_CONCURRENCY=1` restores serial execution for golden tests
|
|
37
|
+
and debugging (`2` allows two-way overlap). Each individual pipeline remains
|
|
38
|
+
serial, and summaries retain canonical Claude, Codex, skills order.
|
|
34
39
|
- **External CLIs stay external.** `claude`, `codex`, `npx`, `npm`, `bun`,
|
|
35
40
|
`curl`, and platform package managers are spawned with argv arrays,
|
|
36
41
|
not shell command strings except where the external installer contract is a
|
|
@@ -69,10 +74,16 @@ warnings, and the summary. Status-quo confirmations exist but are opt-in.
|
|
|
69
74
|
stderr sink exists and `process.stderr.isTTY` is `true`. It disables progress
|
|
70
75
|
in every other case. Injected golden sinks therefore keep their existing
|
|
71
76
|
bytes unless the harness supplies a progress sink.
|
|
72
|
-
- Each progress write replaces one terminal line.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
- Each progress write replaces one terminal line. Every durable `change`,
|
|
78
|
+
`verbose`, `warn`, `err`, or `echo` write erases a visible transient before
|
|
79
|
+
its own write.
|
|
80
|
+
- During sync, the coordinator holds one run-scoped terminal lease and owns
|
|
81
|
+
the transient line. Terminal-exclusive sections serialize input ownership
|
|
82
|
+
by acquisition order and suspend progress redraw while an inherited-stdio
|
|
83
|
+
installer or blocking prompt holds the lease.
|
|
84
|
+
- Automated tests cannot exercise real input contention: golden children use
|
|
85
|
+
ignored stdin, so `process.stdin.isTTY` is false. The simultaneous-prompt
|
|
86
|
+
path requires hand verification in a real TTY.
|
|
76
87
|
|
|
77
88
|
### Change detection
|
|
78
89
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process"
|
|
2
1
|
import { rmSync } from "node:fs"
|
|
3
2
|
import { tmpdir } from "node:os"
|
|
4
3
|
|
|
5
|
-
import { p } from "./exec"
|
|
4
|
+
import { p, spawnProcess } from "./exec"
|
|
6
5
|
import type { Ctx } from "./index"
|
|
7
6
|
import type { EngineServices } from "./services"
|
|
8
7
|
import { field } from "./toolchain"
|
|
@@ -11,10 +10,6 @@ export type BunRuntimeState =
|
|
|
11
10
|
| { readonly kind: "ready"; readonly executable: string }
|
|
12
11
|
| { readonly kind: "deferred"; readonly reason: "missing-curl" | "install-failed" }
|
|
13
12
|
|
|
14
|
-
function remember(ctx: Ctx, state: BunRuntimeState): BunRuntimeState {
|
|
15
|
-
ctx.bunRuntime = state
|
|
16
|
-
return state
|
|
17
|
-
}
|
|
18
13
|
|
|
19
14
|
function predictedExecutable(ctx: Ctx): string {
|
|
20
15
|
const root = process.env["BUN_INSTALL"] !== undefined && process.env["BUN_INSTALL"] !== ""
|
|
@@ -23,47 +18,53 @@ function predictedExecutable(ctx: Ctx): string {
|
|
|
23
18
|
return p(root, "bin", "bun")
|
|
24
19
|
}
|
|
25
20
|
|
|
26
|
-
function installBun(pin: string, installer: string): void {
|
|
27
|
-
const download =
|
|
28
|
-
if (download.error === undefined && download.
|
|
29
|
-
|
|
21
|
+
async function installBun(pin: string, installer: string): Promise<void> {
|
|
22
|
+
const download = await spawnProcess("curl", ["-fsSL", "https://bun.sh/install", "-o", installer], { stdio: "ignore" })
|
|
23
|
+
if (download.error === undefined && download.exitCode === 0) {
|
|
24
|
+
await spawnProcess("bash", [installer, `bun-v${pin}`], { stdio: "ignore" })
|
|
30
25
|
}
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
export function bunBootstrap(ctx: Ctx, services: EngineServices): BunRuntimeState {
|
|
28
|
+
export function bunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunRuntimeState> {
|
|
34
29
|
if (ctx.bunRuntime !== undefined) return ctx.bunRuntime
|
|
30
|
+
const pending = runBunBootstrap(ctx, services)
|
|
31
|
+
ctx.bunRuntime = pending
|
|
32
|
+
return pending
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runBunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunRuntimeState> {
|
|
35
36
|
|
|
36
|
-
const existing = services.deps.path("bun")
|
|
37
|
-
if (existing !== "") return
|
|
37
|
+
const existing = await services.deps.path("bun")
|
|
38
|
+
if (existing !== "") return { kind: "ready", executable: existing }
|
|
38
39
|
|
|
39
40
|
const pin = field(ctx, "bun", "verified")
|
|
40
41
|
if (pin === "") {
|
|
41
42
|
services.logger.warn("Bun bootstrap aborted — SoT/toolchain.json has no verified Bun pin")
|
|
42
|
-
return
|
|
43
|
+
return { kind: "deferred", reason: "install-failed" }
|
|
43
44
|
}
|
|
44
45
|
if (services.deps.probe("curl").state === "missing") {
|
|
45
46
|
services.deps.warnMissing("curl", services.logger, "cannot bootstrap Bun; install Bun manually, then re-run sync")
|
|
46
|
-
return
|
|
47
|
+
return { kind: "deferred", reason: "missing-curl" }
|
|
47
48
|
}
|
|
48
49
|
if (ctx.dryRun) {
|
|
49
50
|
const executable = predictedExecutable(ctx)
|
|
50
51
|
services.logger.echo(`[dry-run] install Bun ${pin} (kit-verified) -> ${executable}`)
|
|
51
|
-
return
|
|
52
|
+
return { kind: "ready", executable }
|
|
52
53
|
}
|
|
53
54
|
services.logger.warn(`Bun not found — installing Bun ${pin} (kit-verified)...`)
|
|
54
55
|
const installer = p(tmpdir(), `bun-install-${process.pid}.sh`)
|
|
55
56
|
try {
|
|
56
|
-
installBun(pin, installer)
|
|
57
|
+
await installBun(pin, installer)
|
|
57
58
|
} finally {
|
|
58
59
|
rmSync(installer, { force: true })
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
const installed = services.deps.path("bun")
|
|
62
|
+
const installed = await services.deps.path("bun")
|
|
62
63
|
if (installed === "") {
|
|
63
64
|
services.logger.warn("Bun install failed. Install manually from https://bun.sh/docs/installation, then re-run sync.")
|
|
64
|
-
return
|
|
65
|
+
return { kind: "deferred", reason: "install-failed" }
|
|
65
66
|
}
|
|
66
|
-
const version = services.deps.version("bun")
|
|
67
|
+
const version = await services.deps.version("bun")
|
|
67
68
|
services.logger.change(`Bun installed (${version !== "" ? version : "version unknown"})`)
|
|
68
|
-
return
|
|
69
|
+
return { kind: "ready", executable: installed }
|
|
69
70
|
}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* plugins. Message strings, guard order, JSON semantics, and spawned argv are
|
|
5
5
|
* golden-tested.
|
|
6
6
|
*/
|
|
7
|
-
import { spawnSync } from "node:child_process"
|
|
8
7
|
import {
|
|
9
8
|
appendFileSync,
|
|
10
9
|
copyFileSync,
|
|
@@ -23,7 +22,7 @@ import {
|
|
|
23
22
|
syncClaudeModel
|
|
24
23
|
} from "./claudeSettingsModifiers"
|
|
25
24
|
import { claudeRuntimePaths, materializeClaudeSettings, type ClaudeRuntimePaths } from "./claudeRuntime"
|
|
26
|
-
import { p, writeBytesIfChanged, writeTextIfChanged } from "./exec"
|
|
25
|
+
import { p, spawnProcess, writeBytesIfChanged, writeTextIfChanged } from "./exec"
|
|
27
26
|
import type { Ctx } from "./index"
|
|
28
27
|
import { compareCodepoints, deepMerge, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
29
28
|
import { ExitError } from "./parseArgs"
|
|
@@ -35,7 +34,7 @@ export type ClaudeRuntimeState =
|
|
|
35
34
|
| { readonly kind: "ready"; readonly paths: ClaudeRuntimePaths }
|
|
36
35
|
| { readonly kind: "deferred"; readonly reason: "bun-unavailable" }
|
|
37
36
|
|
|
38
|
-
export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
37
|
+
export async function claudeSync(ctx: Ctx): Promise<ClaudeRuntimeState> {
|
|
39
38
|
const { err, warn } = ctx.services.logger
|
|
40
39
|
const claudeDir = p(ctx.home, ".claude")
|
|
41
40
|
|
|
@@ -47,7 +46,7 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
|
47
46
|
)
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
const bun = bunBootstrap(ctx, ctx.services)
|
|
49
|
+
const bun = await bunBootstrap(ctx, ctx.services)
|
|
51
50
|
const runtime: ClaudeRuntimeState = bun.kind === "ready"
|
|
52
51
|
? { kind: "ready", paths: claudeRuntimePaths(claudeDir, bun.executable) }
|
|
53
52
|
: { kind: "deferred", reason: "bun-unavailable" }
|
|
@@ -78,9 +77,9 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
|
78
77
|
syncClaudeAdvisor(ctx, ctx.claudeAdvisor)
|
|
79
78
|
syncClaudeJson(ctx)
|
|
80
79
|
syncConnectorEnv(ctx)
|
|
81
|
-
syncPlugins(ctx, claudeDir)
|
|
82
|
-
syncOptionalPlugins(ctx, claudeDir)
|
|
83
|
-
syncLspServers(ctx)
|
|
80
|
+
await syncPlugins(ctx, claudeDir)
|
|
81
|
+
await syncOptionalPlugins(ctx, claudeDir)
|
|
82
|
+
await syncLspServers(ctx)
|
|
84
83
|
return runtime
|
|
85
84
|
}
|
|
86
85
|
|
|
@@ -523,9 +522,9 @@ function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState):
|
|
|
523
522
|
|
|
524
523
|
// -------------------------------------------------------------- plugins ----
|
|
525
524
|
|
|
526
|
-
function cli(args: Array<string>): { ok: boolean; out: string } {
|
|
527
|
-
const res =
|
|
528
|
-
return { ok: res.error === undefined && res.
|
|
525
|
+
async function cli(args: Array<string>): Promise<{ ok: boolean; out: string }> {
|
|
526
|
+
const res = await spawnProcess("claude", args, { stdio: ["ignore", "pipe", "pipe"] })
|
|
527
|
+
return { ok: res.error === undefined && res.exitCode === 0, out: `${res.stdout}${res.stderr}` }
|
|
529
528
|
}
|
|
530
529
|
|
|
531
530
|
function readJsonFile(file: string): Json | undefined {
|
|
@@ -564,7 +563,7 @@ function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
|
|
|
564
563
|
return marketplaces
|
|
565
564
|
}
|
|
566
565
|
|
|
567
|
-
function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
566
|
+
async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
568
567
|
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
569
568
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
570
569
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
@@ -607,7 +606,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
607
606
|
if (known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false) continue
|
|
608
607
|
const repo = isObject(mpValue) && isObject(mpValue["source"]) ? String((mpValue["source"] as { [k: string]: Json })["repo"] ?? "") : ""
|
|
609
608
|
progress(`Adding marketplace ${mpName}...`)
|
|
610
|
-
const marketplaceResult = cli(["plugin", "marketplace", "add", repo])
|
|
609
|
+
const marketplaceResult = await cli(["plugin", "marketplace", "add", repo])
|
|
611
610
|
clearProgress()
|
|
612
611
|
if (marketplaceResult.ok) {
|
|
613
612
|
addedMp++
|
|
@@ -628,12 +627,12 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
628
627
|
const mpName = separator > 0 ? pluginId.slice(separator + 1) : ""
|
|
629
628
|
if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
|
|
630
629
|
progress(`Refreshing marketplace ${mpName}...`)
|
|
631
|
-
cli(["plugin", "marketplace", "update", mpName])
|
|
630
|
+
await cli(["plugin", "marketplace", "update", mpName])
|
|
632
631
|
clearProgress()
|
|
633
632
|
refreshedMarketplaces.add(mpName)
|
|
634
633
|
}
|
|
635
634
|
progress(`Installing plugin ${pluginId}...`)
|
|
636
|
-
const installResult = cli(["plugin", "install", pluginId])
|
|
635
|
+
const installResult = await cli(["plugin", "install", pluginId])
|
|
637
636
|
clearProgress()
|
|
638
637
|
if (installResult.ok) {
|
|
639
638
|
addedPl++
|
|
@@ -664,13 +663,13 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
664
663
|
if (!ctx.skipPluginRefresh) {
|
|
665
664
|
for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
|
|
666
665
|
progress(`Refreshing marketplace ${mpName}...`)
|
|
667
|
-
cli(["plugin", "marketplace", "update", mpName])
|
|
666
|
+
await cli(["plugin", "marketplace", "update", mpName])
|
|
668
667
|
clearProgress()
|
|
669
668
|
}
|
|
670
669
|
for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
|
|
671
670
|
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
672
671
|
progress(`Updating plugin ${pluginId}...`)
|
|
673
|
-
const updateResult = cli(["plugin", "update", pluginId, "--scope", "user"])
|
|
672
|
+
const updateResult = await cli(["plugin", "update", pluginId, "--scope", "user"])
|
|
674
673
|
clearProgress()
|
|
675
674
|
if (updateResult.out.includes("Successfully updated")) updatedPl++
|
|
676
675
|
}
|
|
@@ -686,7 +685,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
686
685
|
if (isObject(sotPlugins) && Object.prototype.hasOwnProperty.call(sotPlugins, pluginId)) continue
|
|
687
686
|
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
688
687
|
progress(`Uninstalling plugin ${pluginId}...`)
|
|
689
|
-
const uninstallResult = cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
|
|
688
|
+
const uninstallResult = await cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
|
|
690
689
|
clearProgress()
|
|
691
690
|
if (uninstallResult.ok) {
|
|
692
691
|
removedPl++
|
|
@@ -702,7 +701,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
702
701
|
const declared = isObject(sotMarketplaces) ? sotMarketplaces[mpName] : undefined
|
|
703
702
|
if (declared !== undefined && declared !== null && declared !== false) continue
|
|
704
703
|
progress(`Removing marketplace ${mpName}...`)
|
|
705
|
-
const removeResult = cli(["plugin", "marketplace", "remove", mpName])
|
|
704
|
+
const removeResult = await cli(["plugin", "marketplace", "remove", mpName])
|
|
706
705
|
clearProgress()
|
|
707
706
|
if (removeResult.ok) {
|
|
708
707
|
removedMp++
|
|
@@ -714,7 +713,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
714
713
|
}
|
|
715
714
|
|
|
716
715
|
// Pass 6 — re-assert SoT enabled-state in the user settings.
|
|
717
|
-
if (reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
|
|
716
|
+
if (await reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
|
|
718
717
|
change("Plugin enable-state re-asserted from SoT in settings.json")
|
|
719
718
|
ctx.nextStepTriggers.claudePlugins = true
|
|
720
719
|
}
|
|
@@ -731,7 +730,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
731
730
|
}
|
|
732
731
|
}
|
|
733
732
|
|
|
734
|
-
function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSettingsFile: string): boolean {
|
|
733
|
+
async function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSettingsFile: string): Promise<boolean> {
|
|
735
734
|
const { warn } = ctx.services.logger
|
|
736
735
|
if (!existsSync(userSettingsFile)) return false
|
|
737
736
|
const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
|
|
@@ -742,7 +741,7 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
|
|
|
742
741
|
const user = readJsonFile(userSettingsFile)
|
|
743
742
|
const enabled = user !== undefined && isObject(user) && isObject(user["enabledPlugins"]) ? (user["enabledPlugins"] as { [k: string]: Json })[pluginId] : undefined
|
|
744
743
|
if (enabled !== true) continue
|
|
745
|
-
if (cli(["plugin", "disable", pluginId]).ok) {
|
|
744
|
+
if ((await cli(["plugin", "disable", pluginId])).ok) {
|
|
746
745
|
cliDisabled = true
|
|
747
746
|
} else {
|
|
748
747
|
warn(`Failed to disable SoT-false plugin: ${pluginId} (will retry next sync)`)
|
|
@@ -765,7 +764,7 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
|
|
|
765
764
|
|
|
766
765
|
// ------------------------------------------------------ optional plugins ----
|
|
767
766
|
|
|
768
|
-
function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): boolean {
|
|
767
|
+
async function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): Promise<boolean> {
|
|
769
768
|
const { change, clearProgress, progress, verbose, warn } = ctx.services.logger
|
|
770
769
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
771
770
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
@@ -776,7 +775,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
776
775
|
const known = readJsonFile(knownMarketplaces)
|
|
777
776
|
const has = known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false
|
|
778
777
|
if (!has) {
|
|
779
|
-
if (!cli(["plugin", "marketplace", "add", marketplaceRepo]).ok) {
|
|
778
|
+
if (!(await cli(["plugin", "marketplace", "add", marketplaceRepo])).ok) {
|
|
780
779
|
warn(`Failed to add marketplace ${marketplaceRepo} for ${pluginId}`)
|
|
781
780
|
return false
|
|
782
781
|
}
|
|
@@ -787,7 +786,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
787
786
|
const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
|
|
788
787
|
if (!wasInstalled) {
|
|
789
788
|
progress(`Installing plugin ${pluginId}...`)
|
|
790
|
-
const installResult = cli(["plugin", "install", pluginId])
|
|
789
|
+
const installResult = await cli(["plugin", "install", pluginId])
|
|
791
790
|
clearProgress()
|
|
792
791
|
if (!installResult.ok) {
|
|
793
792
|
if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
|
|
@@ -802,7 +801,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
802
801
|
? (settingsDoc["enabledPlugins"] as { [k: string]: Json })[pluginId] === true
|
|
803
802
|
: false
|
|
804
803
|
|
|
805
|
-
if (!cli(["plugin", "enable", pluginId]).ok) {
|
|
804
|
+
if (!(await cli(["plugin", "enable", pluginId])).ok) {
|
|
806
805
|
if (marketplaceAdded || !wasInstalled) change(`Optional plugin ${pluginId}: installed (enable failed — will retry next sync)`)
|
|
807
806
|
warn(`Failed to enable optional plugin ${pluginId}`)
|
|
808
807
|
return marketplaceAdded || !wasInstalled
|
|
@@ -813,7 +812,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
813
812
|
return changed
|
|
814
813
|
}
|
|
815
814
|
|
|
816
|
-
function syncOptionalPlugins(ctx: Ctx, claudeDir: string): void {
|
|
815
|
+
async function syncOptionalPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
817
816
|
const { echo, warn } = ctx.services.logger
|
|
818
817
|
if (ctx.claudePlugins.length === 0) return
|
|
819
818
|
|
|
@@ -833,10 +832,10 @@ function syncOptionalPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
833
832
|
}
|
|
834
833
|
|
|
835
834
|
if (ctx.claudePlugins.includes("supabase")) {
|
|
836
|
-
if (enableOptionalPlugin(ctx, claudeDir, "supabase@claude-plugins-official", "")) ctx.nextStepTriggers.claudePlugins = true
|
|
835
|
+
if (await enableOptionalPlugin(ctx, claudeDir, "supabase@claude-plugins-official", "")) ctx.nextStepTriggers.claudePlugins = true
|
|
837
836
|
}
|
|
838
837
|
if (ctx.claudePlugins.includes("n8n")) {
|
|
839
|
-
if (enableOptionalPlugin(ctx, claudeDir, "n8n-mcp-skills@n8n-mcp-skills", "czlonkowski/n8n-skills")) ctx.nextStepTriggers.claudePlugins = true
|
|
838
|
+
if (await enableOptionalPlugin(ctx, claudeDir, "n8n-mcp-skills@n8n-mcp-skills", "czlonkowski/n8n-skills")) ctx.nextStepTriggers.claudePlugins = true
|
|
840
839
|
}
|
|
841
840
|
}
|
|
842
841
|
|
|
@@ -847,7 +846,7 @@ function lspPkg(ctx: Ctx, tool: string, pkg: string): string {
|
|
|
847
846
|
return v !== "" ? `${pkg}@${v}` : pkg
|
|
848
847
|
}
|
|
849
848
|
|
|
850
|
-
function syncLspServers(ctx: Ctx): void {
|
|
849
|
+
async function syncLspServers(ctx: Ctx): Promise<void> {
|
|
851
850
|
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
852
851
|
const sot = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
853
852
|
const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
|
|
@@ -889,9 +888,9 @@ function syncLspServers(ctx: Ctx): void {
|
|
|
889
888
|
|
|
890
889
|
verbose(`Installing LSP servers via npm: ${specs}...`)
|
|
891
890
|
progress(`Installing LSP servers via npm: ${specs}...`)
|
|
892
|
-
const installResult =
|
|
891
|
+
const installResult = await spawnProcess("npm", ["install", "-g", ...missing], { stdio: "ignore" })
|
|
893
892
|
clearProgress()
|
|
894
|
-
if (installResult.
|
|
893
|
+
if (installResult.exitCode === 0) {
|
|
895
894
|
change(`LSP servers installed (${specs})`)
|
|
896
895
|
ctx.nextStepTriggers.claudeRestart = true
|
|
897
896
|
} else {
|