docks-kit 0.14.1 → 0.14.3

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,7 +1,8 @@
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
7
  import { spawnSync } from "node:child_process"
7
8
  import {
@@ -15,7 +16,6 @@ import {
15
16
  rmSync,
16
17
  writeFileSync
17
18
  } from "node:fs"
18
- import { tmpdir } from "node:os"
19
19
  import { bunBootstrap } from "./bun"
20
20
  import {
21
21
  syncClaudeAdvisor,
@@ -26,12 +26,10 @@ import { claudeRuntimePaths, materializeClaudeSettings, type ClaudeRuntimePaths
26
26
  import { p, writeBytesIfChanged, writeTextIfChanged } from "./exec"
27
27
  import type { Ctx } from "./index"
28
28
  import { compareCodepoints, deepMerge, isObject, jqStringify, parseJson, type Json } from "./jq"
29
- import type { EngineServices } from "./services"
30
29
  import { ExitError } from "./parseArgs"
31
30
  import { mergeSettings, reconcileSettings } from "./settings"
32
- import { ensure, field } from "./toolchain"
31
+ import { field } from "./toolchain"
33
32
  import { payloadBytes, payloadDisplayPath, payloadText } from "../payload"
34
- import { ensureSessionRelayCli } from "./sessionRelayCli"
35
33
 
36
34
  export type ClaudeRuntimeState =
37
35
  | { readonly kind: "ready"; readonly paths: ClaudeRuntimePaths }
@@ -49,7 +47,6 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
49
47
  )
50
48
  }
51
49
 
52
- syncRtk(ctx, claudeDir)
53
50
  const bun = bunBootstrap(ctx, ctx.services)
54
51
  const runtime: ClaudeRuntimeState = bun.kind === "ready"
55
52
  ? { kind: "ready", paths: claudeRuntimePaths(claudeDir, bun.executable) }
@@ -81,93 +78,12 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
81
78
  syncClaudeAdvisor(ctx, ctx.claudeAdvisor)
82
79
  syncClaudeJson(ctx)
83
80
  syncConnectorEnv(ctx)
84
- ensureSessionRelayCli(ctx)
85
81
  syncPlugins(ctx, claudeDir)
86
82
  syncOptionalPlugins(ctx, claudeDir)
87
83
  syncLspServers(ctx)
88
84
  return runtime
89
85
  }
90
86
 
91
- // ------------------------------------------------------------------ rtk ----
92
-
93
- type RtkInstaller = ((mode: "install" | "upgrade", version: string, services: EngineServices) => number) & {
94
- readonly prerequisite: (services: EngineServices) => number | undefined
95
- }
96
-
97
- /** RTK toolchain install callback with the shared contextual curl boundary. */
98
- export function rtkInstall(ctx: Ctx, missingCurlContext: string, missingCurlExit: number): RtkInstaller {
99
- const prerequisite = (services: EngineServices): number | undefined => {
100
- if (services.deps.probe("curl").state === "present") return undefined
101
- services.deps.warnMissing("curl", services.logger, missingCurlContext)
102
- return missingCurlExit
103
- }
104
- const install = (mode: "install" | "upgrade", version: string, services: EngineServices): number => {
105
- const blocked = prerequisite(services)
106
- if (blocked !== undefined) return blocked
107
- const { change, err, verbose, warn } = services.logger
108
- const installerRef = version !== "" ? `refs/tags/v${version}` : "refs/heads/master"
109
-
110
- if (mode === "upgrade") verbose(`Upgrading RTK${version !== "" ? ` to ${version}` : ""}...`)
111
- else warn(`RTK not found. Installing${version !== "" ? ` ${version}` : ""}...`)
112
- const installer = p(tmpdir(), `rtk-install-${process.pid}.sh`)
113
- const dl = spawnSync("curl", ["-fsSL", `https://raw.githubusercontent.com/rtk-ai/rtk/${installerRef}/install.sh`, "-o", installer], {
114
- stdio: "inherit"
115
- })
116
- if (dl.error === undefined && dl.status === 0) {
117
- spawnSync("bash", [installer], {
118
- stdio: "inherit",
119
- env: { ...process.env, RTK_VERSION: version !== "" ? `v${version}` : "" }
120
- })
121
- }
122
- rmSync(installer, { force: true })
123
- process.env["PATH"] = `${ctx.home}/.local/bin:${ctx.home}/.cargo/bin:${process.env["PATH"] ?? ""}`
124
- const installed = services.deps.version("rtk")
125
- if (services.deps.probe("rtk").state === "present") {
126
- change(`RTK ready (${installed !== "" ? installed : "version unknown"})`)
127
- return 0
128
- }
129
- err("RTK install failed. Install manually: https://github.com/rtk-ai/rtk")
130
- return 1
131
- }
132
- return Object.assign(install, { prerequisite })
133
- }
134
-
135
- export function ensureRtk(ctx: Ctx, missingCurlContext: string, missingCurlExit: number): number {
136
- const installer = rtkInstall(ctx, missingCurlContext, missingCurlExit)
137
- if (ctx.services.deps.probe("rtk").state === "missing") {
138
- const blocked = installer.prerequisite(ctx.services)
139
- if (blocked !== undefined) return blocked
140
- }
141
- return ensure(ctx, "rtk", installer)
142
- }
143
-
144
- function syncRtk(ctx: Ctx, claudeDir: string): void {
145
- const { change, echo, verbose, warn } = ctx.services.logger
146
- if (ctx.skipRtk) {
147
- warn("Skipping RTK (--skip-rtk)")
148
- return
149
- }
150
-
151
- if (ensureRtk(ctx, "cannot download RTK installer; continuing sync without RTK", 0) !== 0) {
152
- warn("RTK bootstrap failed — continuing sync without it")
153
- }
154
-
155
- if (ctx.services.deps.probe("rtk").state === "missing") return
156
- if (!existsSync(p(claudeDir, "RTK.md"))) {
157
- if (ctx.dryRun) {
158
- echo("[dry-run] rtk init --global (RTK.md missing; runs before the settings merge, which normalizes rtk's settings rewrite)")
159
- return
160
- }
161
- // Plain command under bash set -e: a nonzero `rtk init` aborts the whole
162
- // sync before the success log and before any settings/plugin mutation.
163
- const res = spawnSync("rtk", ["init", "--global"], { stdio: "inherit" })
164
- if (res.error !== undefined || res.status !== 0) throw new ExitError(res.status ?? 1)
165
- change("RTK initialized (RTK.md generated; the following settings merge re-asserts the SoT hooks)")
166
- } else if (!ctx.dryRun) {
167
- verbose("RTK already initialized")
168
- }
169
- }
170
-
171
87
  // ----------------------------------------------------------- runtime ----
172
88
 
173
89
  function syncClaudeRuntime(ctx: Ctx, runtime: ClaudeRuntimeState): void {
@@ -449,13 +365,17 @@ const REMOVED_MANIFEST = {
449
365
  "env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE",
450
366
  "env.CLAUDE_CODE_DISABLE_1M_CONTEXT",
451
367
  "env.CLAUDE_CODE_FORK_SUBAGENT",
452
- "env.CLAUDE_CODE_EFFORT_LEVEL"
368
+ "env.CLAUDE_CODE_EFFORT_LEVEL",
369
+ "enabledPlugins.session-relay@docks",
370
+ "hooks.PreToolUse"
453
371
  ],
454
372
  permissionRules: {
455
- allow: ["Write(./)"],
373
+ allow: ["Write(./)", "Bash(rtk *)"],
456
374
  deny: ["Write(**/.env)", "Write(**/.env.local)", "Write(**/secrets/**)"]
457
375
  },
458
376
  claudeJsonKeys: [] as Array<string>,
377
+ /** Home-relative artifacts the kit installed outside ~/.claude. */
378
+ homeFiles: [".local/bin/session-relay"],
459
379
  runtimeReady: {
460
380
  hooks: ["notify.sh"],
461
381
  files: ["statusline.sh", "fetch-usage.sh"],
@@ -566,6 +486,17 @@ function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState):
566
486
  }
567
487
  }
568
488
 
489
+ for (const rel of REMOVED_MANIFEST.homeFiles) {
490
+ const path = p(ctx.home, rel)
491
+ if (!existsSync(path)) continue
492
+ if (ctx.dryRun) {
493
+ echo(`[dry-run] rm ${path}`)
494
+ } else {
495
+ rmSync(path, { force: true })
496
+ filesRemoved++
497
+ }
498
+ }
499
+
569
500
  const skeys = pruneJsonKeys(ctx, p(claudeDir, "settings.json"), settingsKeys)
570
501
  const permissionRules = prunePermissionRules(
571
502
  ctx,
@@ -634,7 +565,7 @@ function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
634
565
  }
635
566
 
636
567
  function syncPlugins(ctx: Ctx, claudeDir: string): void {
637
- const { change, echo, verbose, warn } = ctx.services.logger
568
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
638
569
  const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
639
570
  const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
640
571
 
@@ -675,7 +606,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
675
606
  const known = readJsonFile(knownMarketplaces)
676
607
  if (known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false) continue
677
608
  const repo = isObject(mpValue) && isObject(mpValue["source"]) ? String((mpValue["source"] as { [k: string]: Json })["repo"] ?? "") : ""
678
- if (cli(["plugin", "marketplace", "add", repo]).ok) {
609
+ progress(`Adding marketplace ${mpName}...`)
610
+ const marketplaceResult = cli(["plugin", "marketplace", "add", repo])
611
+ clearProgress()
612
+ if (marketplaceResult.ok) {
679
613
  addedMp++
680
614
  } else {
681
615
  warn(`Failed to add marketplace: ${mpName} (${repo})`)
@@ -683,17 +617,25 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
683
617
  }
684
618
  }
685
619
 
686
- // Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts).
620
+ // Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts),
621
+ // refreshing each source marketplace once so the install resolves a current snapshot.
687
622
  let addedPl = 0
688
623
  let f2 = 0
689
- let refreshed = false
624
+ const refreshedMarketplaces = new Set<string>()
690
625
  for (const pluginId of sortedKeys(sotPlugins)) {
691
626
  if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue
692
- if (!refreshed) {
693
- cli(["plugin", "marketplace", "update"])
694
- refreshed = true
627
+ const separator = pluginId.lastIndexOf("@")
628
+ const mpName = separator > 0 ? pluginId.slice(separator + 1) : ""
629
+ if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
630
+ progress(`Refreshing marketplace ${mpName}...`)
631
+ cli(["plugin", "marketplace", "update", mpName])
632
+ clearProgress()
633
+ refreshedMarketplaces.add(mpName)
695
634
  }
696
- if (cli(["plugin", "install", pluginId]).ok) {
635
+ progress(`Installing plugin ${pluginId}...`)
636
+ const installResult = cli(["plugin", "install", pluginId])
637
+ clearProgress()
638
+ if (installResult.ok) {
697
639
  addedPl++
698
640
  } else {
699
641
  warn(`Failed to install plugin: ${pluginId}`)
@@ -705,12 +647,32 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
705
647
  const installedDoc = readJsonFile(installedPlugins)
706
648
  const installedKeys = installedDoc !== undefined && isObject(installedDoc) ? sortedKeys(installedDoc["plugins"]) : []
707
649
  const nonUserMarketplaces = nonUserScopeMarketplaces(installedDoc)
708
- // Pass 3 refresh every installed plugin unless the update command
709
- // selected its install-missing-only fast path.
650
+ // Kit-owned plugin IDs: SoT-declared plus this run's --claude-plugin opt-ins.
651
+ const kitPluginIds = new Set<string>(Object.keys(sotPlugins))
652
+ if (ctx.claudePlugins.includes("supabase")) kitPluginIds.add("supabase@claude-plugins-official")
653
+ if (ctx.claudePlugins.includes("n8n")) kitPluginIds.add("n8n-mcp-skills@n8n-mcp-skills")
654
+
655
+ // Kit-owned marketplaces: SoT-declared plus every marketplace those plugins come from.
656
+ const kitMarketplaces = new Set<string>(Object.keys(sotMarketplaces))
657
+ for (const pluginId of kitPluginIds) {
658
+ const separator = pluginId.lastIndexOf("@")
659
+ if (separator > 0) kitMarketplaces.add(pluginId.slice(separator + 1))
660
+ }
661
+
662
+ // Pass 3 — refresh the kit-owned marketplaces and plugins unless the update
663
+ // command selected its install-missing-only fast path.
710
664
  if (!ctx.skipPluginRefresh) {
711
- cli(["plugin", "marketplace", "update"])
712
- for (const pluginId of installedKeys) {
713
- if (cli(["plugin", "update", pluginId]).out.includes("Successfully updated")) updatedPl++
665
+ for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
666
+ progress(`Refreshing marketplace ${mpName}...`)
667
+ cli(["plugin", "marketplace", "update", mpName])
668
+ clearProgress()
669
+ }
670
+ for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
671
+ if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
672
+ progress(`Updating plugin ${pluginId}...`)
673
+ const updateResult = cli(["plugin", "update", pluginId, "--scope", "user"])
674
+ clearProgress()
675
+ if (updateResult.out.includes("Successfully updated")) updatedPl++
714
676
  }
715
677
  }
716
678
 
@@ -723,7 +685,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
723
685
  for (const pluginId of installedKeys) {
724
686
  if (isObject(sotPlugins) && Object.prototype.hasOwnProperty.call(sotPlugins, pluginId)) continue
725
687
  if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
726
- if (cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId]).ok) {
688
+ progress(`Uninstalling plugin ${pluginId}...`)
689
+ const uninstallResult = cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
690
+ clearProgress()
691
+ if (uninstallResult.ok) {
727
692
  removedPl++
728
693
  } else {
729
694
  warn(`Failed to uninstall plugin: ${pluginId}`)
@@ -736,7 +701,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
736
701
  if (nonUserMarketplaces.has(mpName)) continue
737
702
  const declared = isObject(sotMarketplaces) ? sotMarketplaces[mpName] : undefined
738
703
  if (declared !== undefined && declared !== null && declared !== false) continue
739
- if (cli(["plugin", "marketplace", "remove", mpName]).ok) {
704
+ progress(`Removing marketplace ${mpName}...`)
705
+ const removeResult = cli(["plugin", "marketplace", "remove", mpName])
706
+ clearProgress()
707
+ if (removeResult.ok) {
740
708
  removedMp++
741
709
  } else {
742
710
  warn(`Failed to remove marketplace: ${mpName}`)
@@ -798,7 +766,7 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
798
766
  // ------------------------------------------------------ optional plugins ----
799
767
 
800
768
  function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): boolean {
801
- const { change, verbose, warn } = ctx.services.logger
769
+ const { change, clearProgress, progress, verbose, warn } = ctx.services.logger
802
770
  const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
803
771
  const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
804
772
  const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1)
@@ -818,7 +786,10 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
818
786
 
819
787
  const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
820
788
  if (!wasInstalled) {
821
- if (!cli(["plugin", "install", pluginId]).ok) {
789
+ progress(`Installing plugin ${pluginId}...`)
790
+ const installResult = cli(["plugin", "install", pluginId])
791
+ clearProgress()
792
+ if (!installResult.ok) {
822
793
  if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
823
794
  warn(`Failed to install optional plugin ${pluginId}`)
824
795
  return marketplaceAdded
@@ -877,7 +848,7 @@ function lspPkg(ctx: Ctx, tool: string, pkg: string): string {
877
848
  }
878
849
 
879
850
  function syncLspServers(ctx: Ctx): void {
880
- const { change, echo, verbose, warn } = ctx.services.logger
851
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
881
852
  const sot = parseJson(payloadText("SoT/.claude/settings.json"))
882
853
  const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
883
854
  if (enabled === undefined) return
@@ -917,7 +888,10 @@ function syncLspServers(ctx: Ctx): void {
917
888
  }
918
889
 
919
890
  verbose(`Installing LSP servers via npm: ${specs}...`)
920
- if (spawnSync("npm", ["install", "-g", ...missing], { stdio: "ignore" }).status === 0) {
891
+ progress(`Installing LSP servers via npm: ${specs}...`)
892
+ const installResult = spawnSync("npm", ["install", "-g", ...missing], { stdio: "ignore" })
893
+ clearProgress()
894
+ if (installResult.status === 0) {
921
895
  change(`LSP servers installed (${specs})`)
922
896
  ctx.nextStepTriggers.claudeRestart = true
923
897
  } else {
@@ -937,12 +911,6 @@ export function claudeSummary(ctx: Ctx, runtime: ClaudeRuntimeState): void {
937
911
  } else {
938
912
  echo("Hooks: migration deferred (Bun unavailable; existing hook/statusline settings preserved)")
939
913
  }
940
- if (ctx.services.deps.probe("rtk").state === "present") {
941
- const version = ctx.services.deps.version("rtk")
942
- echo(`RTK: ${version !== "" ? version : "installed"}`)
943
- } else {
944
- echo("RTK: not installed")
945
- }
946
914
  if (ctx.services.deps.probe("claude").state === "present") {
947
915
  const installed = readJsonFile(p(claudeDir, "plugins", "installed_plugins.json"))
948
916
  const count = installed !== undefined && isObject(installed) && isObject(installed["plugins"]) ? Object.keys(installed["plugins"]).length : 0
@@ -10,9 +10,7 @@ import { syncCodexEffort, syncCodexModel, replaceTopLevelSettingInFile } from ".
10
10
  import { p } from "./exec"
11
11
  import type { Ctx } from "./index"
12
12
  import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
13
- import { sessionRelayReadiness } from "./sessionRelayReadiness"
14
13
  import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
15
- import { ensureSessionRelayCli } from "./sessionRelayCli"
16
14
 
17
15
  export function codexSync(ctx: Ctx): void {
18
16
  const codexDir = p(ctx.home, ".codex")
@@ -28,7 +26,6 @@ export function codexSync(ctx: Ctx): void {
28
26
  syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
29
27
  syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
30
28
  removeLegacyDocksMarketplace(ctx, userConfig)
31
- ensureSessionRelayCli(ctx)
32
29
  syncPlugins(ctx, sotConfig)
33
30
  }
34
31
 
@@ -45,9 +42,9 @@ function ensureBubblewrap(ctx: Ctx): void {
45
42
 
46
43
  if (ctx.services.deps.probe("bwrap").state === "present") return
47
44
 
48
- if (ctx.skipRtk) {
45
+ if (ctx.skipBubblewrap) {
49
46
  warn(
50
- "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"
47
+ "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"
51
48
  )
52
49
  return
53
50
  }
@@ -124,6 +121,7 @@ function syncConfig(ctx: Ctx, sotConfigText: string, userConfig: string): void {
124
121
  writeFileSync(staging, before)
125
122
 
126
123
  scrubDeprecatedFeatures(ctx, staging)
124
+ removeRetiredPluginTables(ctx, staging)
127
125
  mergeTopLevelSettings(sotConfigText, staging)
128
126
  mergeTableSettings(sotConfigText, staging)
129
127
 
@@ -184,6 +182,49 @@ function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
184
182
  change("Codex: scrubbed deprecated [features].use_legacy_landlock")
185
183
  }
186
184
 
185
+ const PLUGIN_TABLE_HEADER = /^\[plugins\."([^"]+)"\][ \t]*$/
186
+ /** Plugin ids the kit retired; their deployed tables are stripped on every sync. */
187
+ const RETIRED_PLUGIN_IDS: Readonly<Record<string, true>> = { "session-relay@docks": true }
188
+
189
+ /** codex::remove_retired_plugin_tables — drop [plugins."<id>"] blocks for retired ids. */
190
+ export function removeRetiredPluginTablesText(content: string): string {
191
+ const lines = content.split("\n")
192
+ if (lines[lines.length - 1] === "") lines.pop()
193
+ let out = ""
194
+ let skipping = false
195
+ for (const line of lines) {
196
+ const header = PLUGIN_TABLE_HEADER.exec(line)
197
+ if (header !== null) {
198
+ skipping = RETIRED_PLUGIN_IDS[header[1]!] === true
199
+ if (skipping) continue
200
+ } else if (skipping) {
201
+ if (!line.startsWith("[")) continue
202
+ skipping = false
203
+ }
204
+ out += `${line}\n`
205
+ }
206
+ return out
207
+ }
208
+
209
+ function retiredPluginTables(content: string): Array<string> {
210
+ return content
211
+ .split("\n")
212
+ .map((line) => PLUGIN_TABLE_HEADER.exec(line)?.[1])
213
+ .filter((id): id is string => id !== undefined && RETIRED_PLUGIN_IDS[id] === true)
214
+ }
215
+
216
+ function removeRetiredPluginTables(ctx: Ctx, userConfig: string): void {
217
+ const { change } = ctx.services.logger
218
+ if (!existsSync(userConfig)) return
219
+ const content = readFileSync(userConfig, "utf8")
220
+ const present = retiredPluginTables(content)
221
+ if (present.length === 0) return
222
+
223
+ writeFileSync(`${userConfig}.tmp`, removeRetiredPluginTablesText(content))
224
+ renameSync(`${userConfig}.tmp`, userConfig)
225
+ for (const id of present) change(`Codex: removed retired plugin table [plugins."${id}"]`)
226
+ }
227
+
187
228
  function mergeTopLevelSettings(sotConfigText: string, userConfig: string): void {
188
229
  for (const line of sotConfigText.split("\n")) {
189
230
  if (line.startsWith("[")) break
@@ -407,7 +448,7 @@ export function enabledPluginIdsFromText(configText: string): Array<string> {
407
448
  if (plugin !== "" && enabled) ids.push(plugin)
408
449
  }
409
450
  for (const line of configText.split("\n")) {
410
- const m = /^\[plugins\."([^"]+)"\][ \t]*$/.exec(line)
451
+ const m = PLUGIN_TABLE_HEADER.exec(line)
411
452
  if (m !== null) {
412
453
  flush()
413
454
  plugin = m[1]!
@@ -450,7 +491,7 @@ function installedPluginIdsFromCli(): Set<string> | undefined {
450
491
  }
451
492
 
452
493
  function syncPlugins(ctx: Ctx, sotConfigText: string): void {
453
- const { change, echo, verbose, warn } = ctx.services.logger
494
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
454
495
  if (ctx.dryRun) {
455
496
  echo(
456
497
  ctx.skipPluginRefresh
@@ -478,7 +519,9 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
478
519
  const desiredPluginIds = enabledPluginIdsFromText(sotConfigText)
479
520
  let pluginIds = desiredPluginIds
480
521
  if (ctx.skipPluginRefresh) {
522
+ progress("Checking installed Codex plugins...")
481
523
  const installedPluginIds = installedPluginIdsFromCli()
524
+ clearProgress()
482
525
  if (installedPluginIds === undefined) {
483
526
  warn("Codex plugin inventory unavailable — falling back to the full refresh path")
484
527
  } else {
@@ -489,7 +532,9 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
489
532
  let refreshed = 0
490
533
  let failed = 0
491
534
  for (const pluginId of pluginIds) {
535
+ progress(`Updating Codex plugin ${pluginId}...`)
492
536
  const res = spawnSync("codex", ["plugin", "add", pluginId], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
537
+ clearProgress()
493
538
  const addOut = `${res.stdout ?? ""}${res.stderr ?? ""}`
494
539
  if (res.error === undefined && res.status === 0) {
495
540
  refreshed++
@@ -510,10 +555,6 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
510
555
  if (refreshed > 0) {
511
556
  change(`Codex plugins synced (plugins: ~${refreshed})`)
512
557
  ctx.nextStepTriggers.codexRestart = true
513
- const readiness = sessionRelayReadiness()
514
- if (readiness.state !== "ready") {
515
- warn(`Session Relay readiness unavailable after refresh: ${readiness.reason}`)
516
- }
517
558
  }
518
559
  if (ctx.skipPluginRefresh && pluginIds.length === 0) verbose("Codex plugins already installed; refresh-only updates skipped")
519
560
  if (failed > 0) warn(`${failed} Codex plugin operation(s) failed — re-run sync or install manually`)
@@ -9,7 +9,6 @@
9
9
  */
10
10
  import { homedir } from "node:os"
11
11
  import { isAbsolute } from "node:path"
12
- import { existsSync, readdirSync } from "node:fs"
13
12
 
14
13
  import { capture, commandExists, p, which } from "./exec"
15
14
  import { isObject, parseJson } from "./jq"
@@ -24,13 +23,9 @@ export type ToolId =
24
23
  | "npx"
25
24
  | "claude"
26
25
  | "codex"
27
- | "rtk"
28
26
  | "bun"
29
27
  | "bwrap"
30
- | "agent-browser"
31
28
  | "effect-solutions"
32
- | "session-relay"
33
- | "chrome-for-testing"
34
29
  | "ffplay"
35
30
  | "intelephense"
36
31
  | "typescript-language-server"
@@ -167,35 +162,26 @@ const locateEffectSolutions = (exec: ProbeExecutor): DependencyLocation => {
167
162
  return { path: resolved, binDir: globalBin }
168
163
  }
169
164
 
170
- const resolveChrome = (exec: ProbeExecutor, platform: NodeJS.Platform): ProbeResult => {
171
- const root = p(home(), ".agent-browser", "browsers")
172
- const relative =
173
- platform === "darwin"
174
- ? "Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
175
- : "chrome"
176
- if (existsSync(root)) {
177
- for (const directory of readdirSync(root).filter((name) => name.startsWith("chrome-")).sort().reverse()) {
178
- const path = exec.which(p(root, directory, relative))
179
- if (path !== "") return { state: "present", path }
165
+ const npmGlobalCache = new WeakMap<ProbeExecutor, { [k: string]: string }>()
166
+
167
+ const npmGlobalVersions = (exec: ProbeExecutor): { [k: string]: string } => {
168
+ const hit = npmGlobalCache.get(exec)
169
+ if (hit !== undefined) return hit
170
+ const out: { [k: string]: string } = {}
171
+ if (exec.commandExists("npm")) {
172
+ const doc = parseJson(exec.capture("npm", ["ls", "-g", "--depth=0", "--json"]))
173
+ const deps = doc !== undefined && isObject(doc) && isObject(doc["dependencies"]) ? doc["dependencies"] : {}
174
+ for (const [name, value] of Object.entries(deps)) {
175
+ if (isObject(value) && typeof value["version"] === "string") out[name] = value["version"]
180
176
  }
181
177
  }
182
- for (const command of ["chrome-for-testing", "google-chrome-for-testing", "google-chrome", "chromium", "chromium-browser", "brave-browser", "brave"]) {
183
- const path = exec.which(command)
184
- if (path !== "") return { state: "present", path }
185
- }
186
- return { state: "missing" }
178
+ npmGlobalCache.set(exec, out)
179
+ return out
187
180
  }
188
181
 
189
- const latestRtk = (exec: ProbeExecutor): string => {
190
- if (!exec.commandExists("curl")) return ""
191
- const doc = parseJson(
192
- exec.capture("curl", ["-fsSL", "--max-time", "5", "https://api.github.com/repos/rtk-ai/rtk/releases/latest"])
193
- )
194
- const tag = doc !== undefined && isObject(doc) && typeof doc["tag_name"] === "string" ? doc["tag_name"] : ""
195
- return tag.replace(/^v/, "")
196
- }
182
+ const versionNpmGlobal = (pkg: string) => (exec: ProbeExecutor): string => npmGlobalVersions(exec)[pkg] ?? ""
197
183
 
198
- const latestNpm = (id: "agent-browser" | "effect-solutions") => (exec: ProbeExecutor): string =>
184
+ const latestNpm = (id: "effect-solutions") => (exec: ProbeExecutor): string =>
199
185
  exec.commandExists("npm") ? exec.capture("npm", ["view", id, "version"]) : ""
200
186
 
201
187
  export const defaultProbeExecutor: ProbeExecutor = { commandExists, capture, which }
@@ -237,15 +223,6 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
237
223
  () => 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"',
238
224
  { version: versionProbe("codex") }
239
225
  ),
240
- rtk: spec("rtk", "optional", () => "see https://github.com/rtk-ai/rtk (kit auto-install is Linux/macOS-only)", {
241
- version: versionProbe("rtk"),
242
- latest: latestRtk
243
- }),
244
- "session-relay": spec("session-relay", "optional", () => "docks-kit toolchain ensure session-relay", {
245
- resolve: (exec) => pathProbe(p(home(), ".local", "bin", "session-relay"))(exec),
246
- version: (exec) => exec.capture(p(home(), ".local", "bin", "session-relay"), ["--version"]),
247
- locate: () => ({ path: p(home(), ".local", "bin", "session-relay"), binDir: p(home(), ".local", "bin") })
248
- }),
249
226
  bun: spec(
250
227
  "bun",
251
228
  "optional",
@@ -256,10 +233,8 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
256
233
  locate: (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
257
234
  }
258
235
  ),
259
- bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)"),
260
- "agent-browser": spec("agent-browser", "optional", () => "npm install -g agent-browser", {
261
- version: versionProbe("agent-browser"),
262
- latest: latestNpm("agent-browser")
236
+ bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)", {
237
+ version: versionProbe("bwrap")
263
238
  }),
264
239
  "effect-solutions": spec("effect-solutions", "optional", () => "bun add -g effect-solutions", {
265
240
  resolve: resolveEffectSolutions,
@@ -267,27 +242,22 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
267
242
  locate: locateEffectSolutions,
268
243
  latest: latestNpm("effect-solutions")
269
244
  }),
270
- "chrome-for-testing": spec(
271
- "chrome-for-testing",
272
- "optional",
273
- (pf = rawPlatform()) => (pf === "linux" ? "agent-browser install --with-deps" : "agent-browser install"),
274
- { resolve: resolveChrome }
275
- ),
276
245
  ffplay: spec(
277
246
  "ffplay",
278
247
  "optional",
279
248
  (pf = rawPlatform()) =>
280
249
  pf === "darwin" ? "brew install ffmpeg" : "sudo apt install -y ffmpeg",
281
- { versionArgs: ["-version"], resolve: pathProbe("ffplay") }
250
+ { versionArgs: ["-version"], version: versionProbe("ffplay", ["-version"]), resolve: pathProbe("ffplay") }
282
251
  ),
283
252
  intelephense: spec("intelephense", "optional", () => "npm install -g intelephense", {
284
- resolve: pathProbe("intelephense")
253
+ resolve: pathProbe("intelephense"),
254
+ version: versionNpmGlobal("intelephense")
285
255
  }),
286
256
  "typescript-language-server": spec(
287
257
  "typescript-language-server",
288
258
  "optional",
289
259
  () => "npm install -g typescript-language-server typescript",
290
- { resolve: pathProbe("typescript-language-server") }
260
+ { resolve: pathProbe("typescript-language-server"), version: versionProbe("typescript-language-server") }
291
261
  ),
292
262
  tsc: spec("tsc", "optional", () => "npm install -g typescript", {
293
263
  resolve: pathProbe("tsc"),