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.
@@ -1,18 +1,11 @@
1
- import { Args, Command, Options } from "@effect/cli"
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
- // Renamed pre-CLI flags: @effect/cli routes unknown flags into the excess
42
- // positional args, so the rename hints are mirrored here before EngineNative
43
- // sees the normalized argv.
44
- const LEGACY_HINTS: Record<string, string> = {
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 = Options.boolean("dry-run").pipe(
72
- Options.withDescription("Preview without applying")
40
+ const dryRun = Flag.boolean("dry-run").pipe(
41
+ Flag.withDescription("Preview without applying")
73
42
  )
74
- const reconcile = Options.boolean("reconcile").pipe(
75
- Options.withDescription("Reconcile kit-owned settings with SoT (SoT keys win; user-only keys preserved; permissions arrays replaced)")
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 = Options.boolean("prune").pipe(
78
- Options.withDescription("Uninstall kit-managed installs not in SoT (plugins, marketplaces, universal skills)")
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 = Options.boolean("skip-bubblewrap").pipe(
81
- Options.withDescription("Skip optional bubblewrap bootstrap (Codex Linux sandbox)")
49
+ const skipBubblewrap = Flag.boolean("skip-bubblewrap").pipe(
50
+ Flag.withDescription("Skip optional bubblewrap bootstrap (Codex Linux sandbox)")
82
51
  )
83
- const skipPluginRefresh = Options.boolean("skip-plugin-refresh").pipe(
84
- Options.withDescription("Install missing plugins but skip refresh-only updates for existing plugins")
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 = Options.boolean("yes").pipe(
87
- Options.withDescription("Auto-accept toolchain prompts (containers/CI)")
55
+ const yes = Flag.boolean("yes").pipe(
56
+ Flag.withDescription("Auto-accept toolchain prompts (containers/CI)")
88
57
  )
89
- const verbose = Options.boolean("verbose").pipe(
90
- Options.withAlias("v"),
91
- Options.withDescription("Also print no-op confirmations (already in sync, up to date, left as-is)")
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 = Options.text("claude-model").pipe(
94
- Options.withDescription("Deploy-time modifier: set deployed Claude model (see `docks-kit models claude`)"),
95
- Options.optional
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 = Options.text("claude-effort").pipe(
98
- Options.withDescription("Deploy-time modifier: set Claude effortLevel (bare flag shows valid levels)"),
99
- Options.optional
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 = Options.text("claude-advisor").pipe(
102
- Options.withDescription("Deploy-time modifier: set Claude advisor on/off/default"),
103
- Options.optional
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 = Options.text("claude-compact-window").pipe(
106
- Options.withDescription("Deploy-time modifier: set deployed autocompact window in tokens (e.g. 680000 or 680k)"),
107
- Options.optional
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 = Options.boolean("claude-permissive").pipe(
110
- Options.withDescription("Deploy-time modifier: empty permissions.ask/deny in deployed settings (sandboxes)")
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 = Options.text("claude-plugin").pipe(
113
- Options.withDescription(
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
- Options.repeated
85
+ Flag.atLeast(0)
117
86
  )
118
- const codexModel = Options.text("codex-model").pipe(
119
- Options.withDescription("Deploy-time modifier: set deployed Codex model (see `docks-kit models codex`)"),
120
- Options.optional
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 = Options.text("codex-effort").pipe(
123
- Options.withDescription("Deploy-time modifier: set Codex model_reasoning_effort (bare flag shows valid levels)"),
124
- Options.optional
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 { Args, Command, Options } from "@effect/cli"
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 = Args.text({ name: "op" }).pipe(
8
- Args.withDescription("check (default) | ensure <tool>"),
9
- Args.optional
7
+ const op = Argument.string("op").pipe(
8
+ Argument.withDescription("check (default) | ensure <tool>"),
9
+ Argument.optional
10
10
  )
11
- const tool = Args.text({ name: "tool" }).pipe(
12
- Args.withDescription(`Managed tool for ensure: ${MANAGED.join(", ")}`),
13
- Args.optional
11
+ const tool = Argument.string("tool").pipe(
12
+ Argument.withDescription(`Managed tool for ensure: ${MANAGED.join(", ")}`),
13
+ Argument.optional
14
14
  )
15
- const yes = Options.boolean("yes").pipe(
16
- Options.withDescription("Auto-accept above-verified installs")
15
+ const yes = Flag.boolean("yes").pipe(
16
+ Flag.withDescription("Auto-accept above-verified installs")
17
17
  )
18
- const verbose = Options.boolean("verbose").pipe(
19
- Options.withAlias("v"),
20
- Options.withDescription("Also print no-op confirmations (present, up to date)")
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, Options } from "@effect/cli"
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 = Options.boolean("no-sync").pipe(
10
- Options.withDescription("Update the kit only; skip the chained flag-less sync")
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. `clearProgress()` erases a
73
- pending line and does nothing when no line is pending. Every durable
74
- `change`, `verbose`, `warn`, `err`, or `echo` write erases the pending line
75
- before it writes durable output.
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 = spawnSync("curl", ["-fsSL", "https://bun.sh/install", "-o", installer], { stdio: "ignore" })
28
- if (download.error === undefined && download.status === 0) {
29
- spawnSync("bash", [installer, `bun-v${pin}`], { stdio: "ignore" })
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 remember(ctx, { kind: "ready", executable: existing })
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 remember(ctx, { kind: "deferred", reason: "install-failed" })
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 remember(ctx, { kind: "deferred", reason: "missing-curl" })
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 remember(ctx, { kind: "ready", executable })
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 remember(ctx, { kind: "deferred", reason: "install-failed" })
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 remember(ctx, { kind: "ready", executable: installed })
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 = spawnSync("claude", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
528
- return { ok: res.error === undefined && res.status === 0, out: `${res.stdout ?? ""}${res.stderr ?? ""}` }
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 = spawnSync("npm", ["install", "-g", ...missing], { stdio: "ignore" })
891
+ const installResult = await spawnProcess("npm", ["install", "-g", ...missing], { stdio: "ignore" })
893
892
  clearProgress()
894
- if (installResult.status === 0) {
893
+ if (installResult.exitCode === 0) {
895
894
  change(`LSP servers installed (${specs})`)
896
895
  ctx.nextStepTriggers.claudeRestart = true
897
896
  } else {