docks-kit 0.14.2 → 0.14.4

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,9 +1,9 @@
1
1
  /**
2
- * EngineNative `sync claude` pipeline. Step order is load-bearing: rtk BEFORE
3
- * the settings merge, modifiers after it, removals before plugins. Message
4
- * strings, guard order, JSON semantics, and spawned argv are golden-tested.
2
+ * EngineNative `sync claude` pipeline. Step order is load-bearing: the Bun
3
+ * bootstrap BEFORE the settings merge, modifiers after it, removals before
4
+ * plugins. Message strings, guard order, JSON semantics, and spawned argv are
5
+ * golden-tested.
5
6
  */
6
- import { spawnSync } from "node:child_process"
7
7
  import {
8
8
  appendFileSync,
9
9
  copyFileSync,
@@ -15,7 +15,6 @@ import {
15
15
  rmSync,
16
16
  writeFileSync
17
17
  } from "node:fs"
18
- import { tmpdir } from "node:os"
19
18
  import { bunBootstrap } from "./bun"
20
19
  import {
21
20
  syncClaudeAdvisor,
@@ -23,20 +22,19 @@ 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
- import type { EngineServices } from "./services"
30
28
  import { ExitError } from "./parseArgs"
31
29
  import { mergeSettings, reconcileSettings } from "./settings"
32
- import { ensure, field } from "./toolchain"
30
+ import { field } from "./toolchain"
33
31
  import { payloadBytes, payloadDisplayPath, payloadText } from "../payload"
34
32
 
35
33
  export type ClaudeRuntimeState =
36
34
  | { readonly kind: "ready"; readonly paths: ClaudeRuntimePaths }
37
35
  | { readonly kind: "deferred"; readonly reason: "bun-unavailable" }
38
36
 
39
- export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
37
+ export async function claudeSync(ctx: Ctx): Promise<ClaudeRuntimeState> {
40
38
  const { err, warn } = ctx.services.logger
41
39
  const claudeDir = p(ctx.home, ".claude")
42
40
 
@@ -48,8 +46,7 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
48
46
  )
49
47
  }
50
48
 
51
- syncRtk(ctx, claudeDir)
52
- const bun = bunBootstrap(ctx, ctx.services)
49
+ const bun = await bunBootstrap(ctx, ctx.services)
53
50
  const runtime: ClaudeRuntimeState = bun.kind === "ready"
54
51
  ? { kind: "ready", paths: claudeRuntimePaths(claudeDir, bun.executable) }
55
52
  : { kind: "deferred", reason: "bun-unavailable" }
@@ -80,92 +77,12 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
80
77
  syncClaudeAdvisor(ctx, ctx.claudeAdvisor)
81
78
  syncClaudeJson(ctx)
82
79
  syncConnectorEnv(ctx)
83
- syncPlugins(ctx, claudeDir)
84
- syncOptionalPlugins(ctx, claudeDir)
85
- syncLspServers(ctx)
80
+ await syncPlugins(ctx, claudeDir)
81
+ await syncOptionalPlugins(ctx, claudeDir)
82
+ await syncLspServers(ctx)
86
83
  return runtime
87
84
  }
88
85
 
89
- // ------------------------------------------------------------------ rtk ----
90
-
91
- type RtkInstaller = ((mode: "install" | "upgrade", version: string, services: EngineServices) => number) & {
92
- readonly prerequisite: (services: EngineServices) => number | undefined
93
- }
94
-
95
- /** RTK toolchain install callback with the shared contextual curl boundary. */
96
- export function rtkInstall(ctx: Ctx, missingCurlContext: string, missingCurlExit: number): RtkInstaller {
97
- const prerequisite = (services: EngineServices): number | undefined => {
98
- if (services.deps.probe("curl").state === "present") return undefined
99
- services.deps.warnMissing("curl", services.logger, missingCurlContext)
100
- return missingCurlExit
101
- }
102
- const install = (mode: "install" | "upgrade", version: string, services: EngineServices): number => {
103
- const blocked = prerequisite(services)
104
- if (blocked !== undefined) return blocked
105
- const { change, err, verbose, warn } = services.logger
106
- const installerRef = version !== "" ? `refs/tags/v${version}` : "refs/heads/master"
107
-
108
- if (mode === "upgrade") verbose(`Upgrading RTK${version !== "" ? ` to ${version}` : ""}...`)
109
- else warn(`RTK not found. Installing${version !== "" ? ` ${version}` : ""}...`)
110
- const installer = p(tmpdir(), `rtk-install-${process.pid}.sh`)
111
- const dl = spawnSync("curl", ["-fsSL", `https://raw.githubusercontent.com/rtk-ai/rtk/${installerRef}/install.sh`, "-o", installer], {
112
- stdio: "inherit"
113
- })
114
- if (dl.error === undefined && dl.status === 0) {
115
- spawnSync("bash", [installer], {
116
- stdio: "inherit",
117
- env: { ...process.env, RTK_VERSION: version !== "" ? `v${version}` : "" }
118
- })
119
- }
120
- rmSync(installer, { force: true })
121
- process.env["PATH"] = `${ctx.home}/.local/bin:${ctx.home}/.cargo/bin:${process.env["PATH"] ?? ""}`
122
- const installed = services.deps.version("rtk")
123
- if (services.deps.probe("rtk").state === "present") {
124
- change(`RTK ready (${installed !== "" ? installed : "version unknown"})`)
125
- return 0
126
- }
127
- err("RTK install failed. Install manually: https://github.com/rtk-ai/rtk")
128
- return 1
129
- }
130
- return Object.assign(install, { prerequisite })
131
- }
132
-
133
- export function ensureRtk(ctx: Ctx, missingCurlContext: string, missingCurlExit: number): number {
134
- const installer = rtkInstall(ctx, missingCurlContext, missingCurlExit)
135
- if (ctx.services.deps.probe("rtk").state === "missing") {
136
- const blocked = installer.prerequisite(ctx.services)
137
- if (blocked !== undefined) return blocked
138
- }
139
- return ensure(ctx, "rtk", installer)
140
- }
141
-
142
- function syncRtk(ctx: Ctx, claudeDir: string): void {
143
- const { change, echo, verbose, warn } = ctx.services.logger
144
- if (ctx.skipRtk) {
145
- warn("Skipping RTK (--skip-rtk)")
146
- return
147
- }
148
-
149
- if (ensureRtk(ctx, "cannot download RTK installer; continuing sync without RTK", 0) !== 0) {
150
- warn("RTK bootstrap failed — continuing sync without it")
151
- }
152
-
153
- if (ctx.services.deps.probe("rtk").state === "missing") return
154
- if (!existsSync(p(claudeDir, "RTK.md"))) {
155
- if (ctx.dryRun) {
156
- echo("[dry-run] rtk init --global (RTK.md missing; runs before the settings merge, which normalizes rtk's settings rewrite)")
157
- return
158
- }
159
- // Plain command under bash set -e: a nonzero `rtk init` aborts the whole
160
- // sync before the success log and before any settings/plugin mutation.
161
- const res = spawnSync("rtk", ["init", "--global"], { stdio: "inherit" })
162
- if (res.error !== undefined || res.status !== 0) throw new ExitError(res.status ?? 1)
163
- change("RTK initialized (RTK.md generated; the following settings merge re-asserts the SoT hooks)")
164
- } else if (!ctx.dryRun) {
165
- verbose("RTK already initialized")
166
- }
167
- }
168
-
169
86
  // ----------------------------------------------------------- runtime ----
170
87
 
171
88
  function syncClaudeRuntime(ctx: Ctx, runtime: ClaudeRuntimeState): void {
@@ -448,10 +365,11 @@ const REMOVED_MANIFEST = {
448
365
  "env.CLAUDE_CODE_DISABLE_1M_CONTEXT",
449
366
  "env.CLAUDE_CODE_FORK_SUBAGENT",
450
367
  "env.CLAUDE_CODE_EFFORT_LEVEL",
451
- "enabledPlugins.session-relay@docks"
368
+ "enabledPlugins.session-relay@docks",
369
+ "hooks.PreToolUse"
452
370
  ],
453
371
  permissionRules: {
454
- allow: ["Write(./)"],
372
+ allow: ["Write(./)", "Bash(rtk *)"],
455
373
  deny: ["Write(**/.env)", "Write(**/.env.local)", "Write(**/secrets/**)"]
456
374
  },
457
375
  claudeJsonKeys: [] as Array<string>,
@@ -604,9 +522,9 @@ function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState):
604
522
 
605
523
  // -------------------------------------------------------------- plugins ----
606
524
 
607
- function cli(args: Array<string>): { ok: boolean; out: string } {
608
- const res = spawnSync("claude", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
609
- 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}` }
610
528
  }
611
529
 
612
530
  function readJsonFile(file: string): Json | undefined {
@@ -645,8 +563,8 @@ function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
645
563
  return marketplaces
646
564
  }
647
565
 
648
- function syncPlugins(ctx: Ctx, claudeDir: string): void {
649
- const { change, echo, verbose, warn } = ctx.services.logger
566
+ async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
567
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
650
568
  const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
651
569
  const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
652
570
 
@@ -687,7 +605,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
687
605
  const known = readJsonFile(knownMarketplaces)
688
606
  if (known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false) continue
689
607
  const repo = isObject(mpValue) && isObject(mpValue["source"]) ? String((mpValue["source"] as { [k: string]: Json })["repo"] ?? "") : ""
690
- if (cli(["plugin", "marketplace", "add", repo]).ok) {
608
+ progress(`Adding marketplace ${mpName}...`)
609
+ const marketplaceResult = await cli(["plugin", "marketplace", "add", repo])
610
+ clearProgress()
611
+ if (marketplaceResult.ok) {
691
612
  addedMp++
692
613
  } else {
693
614
  warn(`Failed to add marketplace: ${mpName} (${repo})`)
@@ -695,17 +616,25 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
695
616
  }
696
617
  }
697
618
 
698
- // Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts).
619
+ // Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts),
620
+ // refreshing each source marketplace once so the install resolves a current snapshot.
699
621
  let addedPl = 0
700
622
  let f2 = 0
701
- let refreshed = false
623
+ const refreshedMarketplaces = new Set<string>()
702
624
  for (const pluginId of sortedKeys(sotPlugins)) {
703
625
  if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue
704
- if (!refreshed) {
705
- cli(["plugin", "marketplace", "update"])
706
- refreshed = true
626
+ const separator = pluginId.lastIndexOf("@")
627
+ const mpName = separator > 0 ? pluginId.slice(separator + 1) : ""
628
+ if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
629
+ progress(`Refreshing marketplace ${mpName}...`)
630
+ await cli(["plugin", "marketplace", "update", mpName])
631
+ clearProgress()
632
+ refreshedMarketplaces.add(mpName)
707
633
  }
708
- if (cli(["plugin", "install", pluginId]).ok) {
634
+ progress(`Installing plugin ${pluginId}...`)
635
+ const installResult = await cli(["plugin", "install", pluginId])
636
+ clearProgress()
637
+ if (installResult.ok) {
709
638
  addedPl++
710
639
  } else {
711
640
  warn(`Failed to install plugin: ${pluginId}`)
@@ -717,12 +646,32 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
717
646
  const installedDoc = readJsonFile(installedPlugins)
718
647
  const installedKeys = installedDoc !== undefined && isObject(installedDoc) ? sortedKeys(installedDoc["plugins"]) : []
719
648
  const nonUserMarketplaces = nonUserScopeMarketplaces(installedDoc)
720
- // Pass 3 refresh every installed plugin unless the update command
721
- // selected its install-missing-only fast path.
649
+ // Kit-owned plugin IDs: SoT-declared plus this run's --claude-plugin opt-ins.
650
+ const kitPluginIds = new Set<string>(Object.keys(sotPlugins))
651
+ if (ctx.claudePlugins.includes("supabase")) kitPluginIds.add("supabase@claude-plugins-official")
652
+ if (ctx.claudePlugins.includes("n8n")) kitPluginIds.add("n8n-mcp-skills@n8n-mcp-skills")
653
+
654
+ // Kit-owned marketplaces: SoT-declared plus every marketplace those plugins come from.
655
+ const kitMarketplaces = new Set<string>(Object.keys(sotMarketplaces))
656
+ for (const pluginId of kitPluginIds) {
657
+ const separator = pluginId.lastIndexOf("@")
658
+ if (separator > 0) kitMarketplaces.add(pluginId.slice(separator + 1))
659
+ }
660
+
661
+ // Pass 3 — refresh the kit-owned marketplaces and plugins unless the update
662
+ // command selected its install-missing-only fast path.
722
663
  if (!ctx.skipPluginRefresh) {
723
- cli(["plugin", "marketplace", "update"])
724
- for (const pluginId of installedKeys) {
725
- if (cli(["plugin", "update", pluginId]).out.includes("Successfully updated")) updatedPl++
664
+ for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
665
+ progress(`Refreshing marketplace ${mpName}...`)
666
+ await cli(["plugin", "marketplace", "update", mpName])
667
+ clearProgress()
668
+ }
669
+ for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
670
+ if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
671
+ progress(`Updating plugin ${pluginId}...`)
672
+ const updateResult = await cli(["plugin", "update", pluginId, "--scope", "user"])
673
+ clearProgress()
674
+ if (updateResult.out.includes("Successfully updated")) updatedPl++
726
675
  }
727
676
  }
728
677
 
@@ -735,7 +684,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
735
684
  for (const pluginId of installedKeys) {
736
685
  if (isObject(sotPlugins) && Object.prototype.hasOwnProperty.call(sotPlugins, pluginId)) continue
737
686
  if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
738
- if (cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId]).ok) {
687
+ progress(`Uninstalling plugin ${pluginId}...`)
688
+ const uninstallResult = await cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
689
+ clearProgress()
690
+ if (uninstallResult.ok) {
739
691
  removedPl++
740
692
  } else {
741
693
  warn(`Failed to uninstall plugin: ${pluginId}`)
@@ -748,7 +700,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
748
700
  if (nonUserMarketplaces.has(mpName)) continue
749
701
  const declared = isObject(sotMarketplaces) ? sotMarketplaces[mpName] : undefined
750
702
  if (declared !== undefined && declared !== null && declared !== false) continue
751
- if (cli(["plugin", "marketplace", "remove", mpName]).ok) {
703
+ progress(`Removing marketplace ${mpName}...`)
704
+ const removeResult = await cli(["plugin", "marketplace", "remove", mpName])
705
+ clearProgress()
706
+ if (removeResult.ok) {
752
707
  removedMp++
753
708
  } else {
754
709
  warn(`Failed to remove marketplace: ${mpName}`)
@@ -758,7 +713,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
758
713
  }
759
714
 
760
715
  // Pass 6 — re-assert SoT enabled-state in the user settings.
761
- if (reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
716
+ if (await reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
762
717
  change("Plugin enable-state re-asserted from SoT in settings.json")
763
718
  ctx.nextStepTriggers.claudePlugins = true
764
719
  }
@@ -775,7 +730,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
775
730
  }
776
731
  }
777
732
 
778
- 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> {
779
734
  const { warn } = ctx.services.logger
780
735
  if (!existsSync(userSettingsFile)) return false
781
736
  const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
@@ -786,7 +741,7 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
786
741
  const user = readJsonFile(userSettingsFile)
787
742
  const enabled = user !== undefined && isObject(user) && isObject(user["enabledPlugins"]) ? (user["enabledPlugins"] as { [k: string]: Json })[pluginId] : undefined
788
743
  if (enabled !== true) continue
789
- if (cli(["plugin", "disable", pluginId]).ok) {
744
+ if ((await cli(["plugin", "disable", pluginId])).ok) {
790
745
  cliDisabled = true
791
746
  } else {
792
747
  warn(`Failed to disable SoT-false plugin: ${pluginId} (will retry next sync)`)
@@ -809,8 +764,8 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
809
764
 
810
765
  // ------------------------------------------------------ optional plugins ----
811
766
 
812
- function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): boolean {
813
- const { change, verbose, warn } = ctx.services.logger
767
+ async function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): Promise<boolean> {
768
+ const { change, clearProgress, progress, verbose, warn } = ctx.services.logger
814
769
  const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
815
770
  const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
816
771
  const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1)
@@ -820,7 +775,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
820
775
  const known = readJsonFile(knownMarketplaces)
821
776
  const has = known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false
822
777
  if (!has) {
823
- if (!cli(["plugin", "marketplace", "add", marketplaceRepo]).ok) {
778
+ if (!(await cli(["plugin", "marketplace", "add", marketplaceRepo])).ok) {
824
779
  warn(`Failed to add marketplace ${marketplaceRepo} for ${pluginId}`)
825
780
  return false
826
781
  }
@@ -830,7 +785,10 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
830
785
 
831
786
  const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
832
787
  if (!wasInstalled) {
833
- if (!cli(["plugin", "install", pluginId]).ok) {
788
+ progress(`Installing plugin ${pluginId}...`)
789
+ const installResult = await cli(["plugin", "install", pluginId])
790
+ clearProgress()
791
+ if (!installResult.ok) {
834
792
  if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
835
793
  warn(`Failed to install optional plugin ${pluginId}`)
836
794
  return marketplaceAdded
@@ -843,7 +801,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
843
801
  ? (settingsDoc["enabledPlugins"] as { [k: string]: Json })[pluginId] === true
844
802
  : false
845
803
 
846
- if (!cli(["plugin", "enable", pluginId]).ok) {
804
+ if (!(await cli(["plugin", "enable", pluginId])).ok) {
847
805
  if (marketplaceAdded || !wasInstalled) change(`Optional plugin ${pluginId}: installed (enable failed — will retry next sync)`)
848
806
  warn(`Failed to enable optional plugin ${pluginId}`)
849
807
  return marketplaceAdded || !wasInstalled
@@ -854,7 +812,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
854
812
  return changed
855
813
  }
856
814
 
857
- function syncOptionalPlugins(ctx: Ctx, claudeDir: string): void {
815
+ async function syncOptionalPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
858
816
  const { echo, warn } = ctx.services.logger
859
817
  if (ctx.claudePlugins.length === 0) return
860
818
 
@@ -874,10 +832,10 @@ function syncOptionalPlugins(ctx: Ctx, claudeDir: string): void {
874
832
  }
875
833
 
876
834
  if (ctx.claudePlugins.includes("supabase")) {
877
- 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
878
836
  }
879
837
  if (ctx.claudePlugins.includes("n8n")) {
880
- 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
881
839
  }
882
840
  }
883
841
 
@@ -888,8 +846,8 @@ function lspPkg(ctx: Ctx, tool: string, pkg: string): string {
888
846
  return v !== "" ? `${pkg}@${v}` : pkg
889
847
  }
890
848
 
891
- function syncLspServers(ctx: Ctx): void {
892
- const { change, echo, verbose, warn } = ctx.services.logger
849
+ async function syncLspServers(ctx: Ctx): Promise<void> {
850
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
893
851
  const sot = parseJson(payloadText("SoT/.claude/settings.json"))
894
852
  const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
895
853
  if (enabled === undefined) return
@@ -929,7 +887,10 @@ function syncLspServers(ctx: Ctx): void {
929
887
  }
930
888
 
931
889
  verbose(`Installing LSP servers via npm: ${specs}...`)
932
- if (spawnSync("npm", ["install", "-g", ...missing], { stdio: "ignore" }).status === 0) {
890
+ progress(`Installing LSP servers via npm: ${specs}...`)
891
+ const installResult = await spawnProcess("npm", ["install", "-g", ...missing], { stdio: "ignore" })
892
+ clearProgress()
893
+ if (installResult.exitCode === 0) {
933
894
  change(`LSP servers installed (${specs})`)
934
895
  ctx.nextStepTriggers.claudeRestart = true
935
896
  } else {
@@ -949,12 +910,6 @@ export function claudeSummary(ctx: Ctx, runtime: ClaudeRuntimeState): void {
949
910
  } else {
950
911
  echo("Hooks: migration deferred (Bun unavailable; existing hook/statusline settings preserved)")
951
912
  }
952
- if (ctx.services.deps.probe("rtk").state === "present") {
953
- const version = ctx.services.deps.version("rtk")
954
- echo(`RTK: ${version !== "" ? version : "installed"}`)
955
- } else {
956
- echo("RTK: not installed")
957
- }
958
913
  if (ctx.services.deps.probe("claude").state === "present") {
959
914
  const installed = readJsonFile(p(claudeDir, "plugins", "installed_plugins.json"))
960
915
  const count = installed !== undefined && isObject(installed) && isObject(installed["plugins"]) ? Object.keys(installed["plugins"]).length : 0
@@ -3,21 +3,20 @@
3
3
  * avoid a TOML library because reformatting user configs would be a behavior
4
4
  * change. Guard order, message strings, and backup behavior are golden-tested.
5
5
  */
6
- import { spawnSync } from "node:child_process"
7
6
  import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
8
7
 
9
8
  import { syncCodexEffort, syncCodexModel, replaceTopLevelSettingInFile } from "./codexToml"
10
- import { p } from "./exec"
9
+ import { p, spawnProcess } from "./exec"
11
10
  import type { Ctx } from "./index"
12
11
  import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
13
12
  import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
14
13
 
15
- export function codexSync(ctx: Ctx): void {
14
+ export async function codexSync(ctx: Ctx): Promise<void> {
16
15
  const codexDir = p(ctx.home, ".codex")
17
16
  const sotConfig = payloadText("SoT/.codex/config.toml")
18
17
  const userConfig = p(codexDir, "config.toml")
19
18
 
20
- ensureBubblewrap(ctx)
19
+ await ensureBubblewrap(ctx)
21
20
  if (!ctx.dryRun) mkdirSync(codexDir, { recursive: true })
22
21
  syncConfig(ctx, sotConfig, userConfig)
23
22
  syncCodexModel(ctx, ctx.codexModel)
@@ -25,13 +24,13 @@ export function codexSync(ctx: Ctx): void {
25
24
  syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
26
25
  syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
27
26
  syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
28
- removeLegacyDocksMarketplace(ctx, userConfig)
29
- syncPlugins(ctx, sotConfig)
27
+ await removeLegacyDocksMarketplace(ctx, userConfig)
28
+ await syncPlugins(ctx, sotConfig)
30
29
  }
31
30
 
32
31
  // ---------------------------------------------------------- bubblewrap ----
33
32
 
34
- function ensureBubblewrap(ctx: Ctx): void {
33
+ async function ensureBubblewrap(ctx: Ctx): Promise<void> {
35
34
  const { change, echo, warn } = ctx.services.logger
36
35
  if (!bwrapSupportedOs(ctx)) return
37
36
 
@@ -42,9 +41,9 @@ function ensureBubblewrap(ctx: Ctx): void {
42
41
 
43
42
  if (ctx.services.deps.probe("bwrap").state === "present") return
44
43
 
45
- if (ctx.skipRtk) {
44
+ if (ctx.skipBubblewrap) {
46
45
  warn(
47
- "bubblewrap not installed (--skip-rtk skips auto-install). Codex may use its bundled helper if user namespaces work; recommended install: sudo apt install -y bubblewrap"
46
+ "bubblewrap not installed (--skip-bubblewrap skips auto-install). Codex may use its bundled helper if user namespaces work; recommended install: sudo apt install -y bubblewrap"
48
47
  )
49
48
  return
50
49
  }
@@ -58,8 +57,10 @@ function ensureBubblewrap(ctx: Ctx): void {
58
57
  }
59
58
 
60
59
  warn(`bubblewrap not installed - recommended for Codex Linux sandbox. Running: ${pmInstall} (sudo prompt may appear)`)
61
- const res = spawnSync("bash", ["-c", pmInstall], { stdio: ["inherit", "inherit", "inherit"] })
62
- if (res.status !== 0) {
60
+ const runInstaller = () =>
61
+ spawnProcess("bash", ["-c", pmInstall], { stdio: ["inherit", "inherit", "inherit"] })
62
+ const res = await (ctx.terminalLease?.withExclusive(runInstaller) ?? runInstaller())
63
+ if (res.exitCode !== 0) {
63
64
  warn(`Failed to auto-install bubblewrap. Install manually: ${pmInstall}`)
64
65
  return
65
66
  }
@@ -69,8 +70,8 @@ function ensureBubblewrap(ctx: Ctx): void {
69
70
  return
70
71
  }
71
72
 
72
- if (spawnSync("unshare", ["-Ur", "true"], { stdio: "ignore" }).status === 0) {
73
- change(`bubblewrap installed and functional (${ctx.services.deps.version("bwrap")})`)
73
+ if ((await spawnProcess("unshare", ["-Ur", "true"], { stdio: "ignore" })).exitCode === 0) {
74
+ change(`bubblewrap installed and functional (${await ctx.services.deps.version("bwrap")})`)
74
75
  } else {
75
76
  warn(
76
77
  "bubblewrap installed but unprivileged user namespaces appear blocked. On Ubuntu 24.04+, prefer loading the AppArmor bwrap-userns-restrict profile; fallback: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0"
@@ -411,7 +412,7 @@ export function marketplaceSource(marketplace: string, configFile: string): stri
411
412
  return ""
412
413
  }
413
414
 
414
- function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): void {
415
+ async function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): Promise<void> {
415
416
  const { change, echo, warn } = ctx.services.logger
416
417
  if (ctx.dryRun) {
417
418
  echo("[dry-run] remove legacy configured Codex Docks marketplace when personal marketplace is deployed")
@@ -422,8 +423,8 @@ function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): void {
422
423
 
423
424
  const source = marketplaceSource("docks", userConfig)
424
425
  if (source !== "https://github.com/DocksDocks/docks.git" && source !== "DocksDocks/docks") return
425
- const res = spawnSync("codex", ["plugin", "marketplace", "remove", "docks"], { stdio: "ignore" })
426
- if (res.error === undefined && res.status === 0) {
426
+ const res = await spawnProcess("codex", ["plugin", "marketplace", "remove", "docks"], { stdio: "ignore" })
427
+ if (res.error === undefined && res.exitCode === 0) {
427
428
  change("Removed legacy configured Codex Docks marketplace; using personal marketplace file")
428
429
  ctx.nextStepTriggers.codexRestart = true
429
430
  } else {
@@ -474,13 +475,12 @@ function manualPluginRefreshCommand(sotConfigText: string): string {
474
475
  return first !== undefined ? `codex plugin add ${first}` : "codex plugin add <plugin@marketplace>"
475
476
  }
476
477
 
477
- function installedPluginIdsFromCli(): Set<string> | undefined {
478
- const result = spawnSync("codex", ["plugin", "list", "--json"], {
479
- encoding: "utf8",
478
+ async function installedPluginIdsFromCli(): Promise<Set<string> | undefined> {
479
+ const result = await spawnProcess("codex", ["plugin", "list", "--json"], {
480
480
  stdio: ["ignore", "pipe", "ignore"]
481
481
  })
482
- if (result.error !== undefined || result.status !== 0) return undefined
483
- const value = parseJson(result.stdout ?? "")
482
+ if (result.error !== undefined || result.exitCode !== 0) return undefined
483
+ const value = parseJson(result.stdout)
484
484
  if (value === undefined || !isObject(value) || !Array.isArray(value["installed"])) return undefined
485
485
  const ids = new Set<string>()
486
486
  for (const row of value["installed"]) {
@@ -490,8 +490,8 @@ function installedPluginIdsFromCli(): Set<string> | undefined {
490
490
  return ids
491
491
  }
492
492
 
493
- function syncPlugins(ctx: Ctx, sotConfigText: string): void {
494
- const { change, echo, verbose, warn } = ctx.services.logger
493
+ async function syncPlugins(ctx: Ctx, sotConfigText: string): Promise<void> {
494
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
495
495
  if (ctx.dryRun) {
496
496
  echo(
497
497
  ctx.skipPluginRefresh
@@ -519,7 +519,9 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
519
519
  const desiredPluginIds = enabledPluginIdsFromText(sotConfigText)
520
520
  let pluginIds = desiredPluginIds
521
521
  if (ctx.skipPluginRefresh) {
522
- const installedPluginIds = installedPluginIdsFromCli()
522
+ progress("Checking installed Codex plugins...")
523
+ const installedPluginIds = await installedPluginIdsFromCli()
524
+ clearProgress()
523
525
  if (installedPluginIds === undefined) {
524
526
  warn("Codex plugin inventory unavailable — falling back to the full refresh path")
525
527
  } else {
@@ -530,9 +532,11 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
530
532
  let refreshed = 0
531
533
  let failed = 0
532
534
  for (const pluginId of pluginIds) {
533
- const res = spawnSync("codex", ["plugin", "add", pluginId], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
534
- const addOut = `${res.stdout ?? ""}${res.stderr ?? ""}`
535
- if (res.error === undefined && res.status === 0) {
535
+ progress(`Updating Codex plugin ${pluginId}...`)
536
+ const res = await spawnProcess("codex", ["plugin", "add", pluginId], { stdio: ["ignore", "pipe", "pipe"] })
537
+ clearProgress()
538
+ const addOut = `${res.stdout}${res.stderr}`
539
+ if (res.error === undefined && res.exitCode === 0) {
536
540
  refreshed++
537
541
  } else if (addOut.includes("could not find a Codex CLI binary")) {
538
542
  warn(