docks-kit 0.14.2 → 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,10 +26,9 @@ 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
33
 
35
34
  export type ClaudeRuntimeState =
@@ -48,7 +47,6 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
48
47
  )
49
48
  }
50
49
 
51
- syncRtk(ctx, claudeDir)
52
50
  const bun = bunBootstrap(ctx, ctx.services)
53
51
  const runtime: ClaudeRuntimeState = bun.kind === "ready"
54
52
  ? { kind: "ready", paths: claudeRuntimePaths(claudeDir, bun.executable) }
@@ -86,86 +84,6 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
86
84
  return runtime
87
85
  }
88
86
 
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
87
  // ----------------------------------------------------------- runtime ----
170
88
 
171
89
  function syncClaudeRuntime(ctx: Ctx, runtime: ClaudeRuntimeState): void {
@@ -448,10 +366,11 @@ const REMOVED_MANIFEST = {
448
366
  "env.CLAUDE_CODE_DISABLE_1M_CONTEXT",
449
367
  "env.CLAUDE_CODE_FORK_SUBAGENT",
450
368
  "env.CLAUDE_CODE_EFFORT_LEVEL",
451
- "enabledPlugins.session-relay@docks"
369
+ "enabledPlugins.session-relay@docks",
370
+ "hooks.PreToolUse"
452
371
  ],
453
372
  permissionRules: {
454
- allow: ["Write(./)"],
373
+ allow: ["Write(./)", "Bash(rtk *)"],
455
374
  deny: ["Write(**/.env)", "Write(**/.env.local)", "Write(**/secrets/**)"]
456
375
  },
457
376
  claudeJsonKeys: [] as Array<string>,
@@ -646,7 +565,7 @@ function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
646
565
  }
647
566
 
648
567
  function syncPlugins(ctx: Ctx, claudeDir: string): void {
649
- const { change, echo, verbose, warn } = ctx.services.logger
568
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
650
569
  const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
651
570
  const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
652
571
 
@@ -687,7 +606,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
687
606
  const known = readJsonFile(knownMarketplaces)
688
607
  if (known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false) continue
689
608
  const repo = isObject(mpValue) && isObject(mpValue["source"]) ? String((mpValue["source"] as { [k: string]: Json })["repo"] ?? "") : ""
690
- 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) {
691
613
  addedMp++
692
614
  } else {
693
615
  warn(`Failed to add marketplace: ${mpName} (${repo})`)
@@ -695,17 +617,25 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
695
617
  }
696
618
  }
697
619
 
698
- // 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.
699
622
  let addedPl = 0
700
623
  let f2 = 0
701
- let refreshed = false
624
+ const refreshedMarketplaces = new Set<string>()
702
625
  for (const pluginId of sortedKeys(sotPlugins)) {
703
626
  if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue
704
- if (!refreshed) {
705
- cli(["plugin", "marketplace", "update"])
706
- 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)
707
634
  }
708
- if (cli(["plugin", "install", pluginId]).ok) {
635
+ progress(`Installing plugin ${pluginId}...`)
636
+ const installResult = cli(["plugin", "install", pluginId])
637
+ clearProgress()
638
+ if (installResult.ok) {
709
639
  addedPl++
710
640
  } else {
711
641
  warn(`Failed to install plugin: ${pluginId}`)
@@ -717,12 +647,32 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
717
647
  const installedDoc = readJsonFile(installedPlugins)
718
648
  const installedKeys = installedDoc !== undefined && isObject(installedDoc) ? sortedKeys(installedDoc["plugins"]) : []
719
649
  const nonUserMarketplaces = nonUserScopeMarketplaces(installedDoc)
720
- // Pass 3 refresh every installed plugin unless the update command
721
- // 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.
722
664
  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++
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++
726
676
  }
727
677
  }
728
678
 
@@ -735,7 +685,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
735
685
  for (const pluginId of installedKeys) {
736
686
  if (isObject(sotPlugins) && Object.prototype.hasOwnProperty.call(sotPlugins, pluginId)) continue
737
687
  if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
738
- 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) {
739
692
  removedPl++
740
693
  } else {
741
694
  warn(`Failed to uninstall plugin: ${pluginId}`)
@@ -748,7 +701,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
748
701
  if (nonUserMarketplaces.has(mpName)) continue
749
702
  const declared = isObject(sotMarketplaces) ? sotMarketplaces[mpName] : undefined
750
703
  if (declared !== undefined && declared !== null && declared !== false) continue
751
- 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) {
752
708
  removedMp++
753
709
  } else {
754
710
  warn(`Failed to remove marketplace: ${mpName}`)
@@ -810,7 +766,7 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
810
766
  // ------------------------------------------------------ optional plugins ----
811
767
 
812
768
  function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): boolean {
813
- const { change, verbose, warn } = ctx.services.logger
769
+ const { change, clearProgress, progress, verbose, warn } = ctx.services.logger
814
770
  const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
815
771
  const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
816
772
  const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1)
@@ -830,7 +786,10 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
830
786
 
831
787
  const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
832
788
  if (!wasInstalled) {
833
- if (!cli(["plugin", "install", pluginId]).ok) {
789
+ progress(`Installing plugin ${pluginId}...`)
790
+ const installResult = cli(["plugin", "install", pluginId])
791
+ clearProgress()
792
+ if (!installResult.ok) {
834
793
  if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
835
794
  warn(`Failed to install optional plugin ${pluginId}`)
836
795
  return marketplaceAdded
@@ -889,7 +848,7 @@ function lspPkg(ctx: Ctx, tool: string, pkg: string): string {
889
848
  }
890
849
 
891
850
  function syncLspServers(ctx: Ctx): void {
892
- const { change, echo, verbose, warn } = ctx.services.logger
851
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
893
852
  const sot = parseJson(payloadText("SoT/.claude/settings.json"))
894
853
  const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
895
854
  if (enabled === undefined) return
@@ -929,7 +888,10 @@ function syncLspServers(ctx: Ctx): void {
929
888
  }
930
889
 
931
890
  verbose(`Installing LSP servers via npm: ${specs}...`)
932
- 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) {
933
895
  change(`LSP servers installed (${specs})`)
934
896
  ctx.nextStepTriggers.claudeRestart = true
935
897
  } else {
@@ -949,12 +911,6 @@ export function claudeSummary(ctx: Ctx, runtime: ClaudeRuntimeState): void {
949
911
  } else {
950
912
  echo("Hooks: migration deferred (Bun unavailable; existing hook/statusline settings preserved)")
951
913
  }
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
914
  if (ctx.services.deps.probe("claude").state === "present") {
959
915
  const installed = readJsonFile(p(claudeDir, "plugins", "installed_plugins.json"))
960
916
  const count = installed !== undefined && isObject(installed) && isObject(installed["plugins"]) ? Object.keys(installed["plugins"]).length : 0
@@ -42,9 +42,9 @@ function ensureBubblewrap(ctx: Ctx): void {
42
42
 
43
43
  if (ctx.services.deps.probe("bwrap").state === "present") return
44
44
 
45
- if (ctx.skipRtk) {
45
+ if (ctx.skipBubblewrap) {
46
46
  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"
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"
48
48
  )
49
49
  return
50
50
  }
@@ -491,7 +491,7 @@ function installedPluginIdsFromCli(): Set<string> | undefined {
491
491
  }
492
492
 
493
493
  function syncPlugins(ctx: Ctx, sotConfigText: string): void {
494
- const { change, echo, verbose, warn } = ctx.services.logger
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
+ progress("Checking installed Codex plugins...")
522
523
  const installedPluginIds = 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,7 +532,9 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
530
532
  let refreshed = 0
531
533
  let failed = 0
532
534
  for (const pluginId of pluginIds) {
535
+ progress(`Updating Codex plugin ${pluginId}...`)
533
536
  const res = spawnSync("codex", ["plugin", "add", pluginId], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
537
+ clearProgress()
534
538
  const addOut = `${res.stdout ?? ""}${res.stderr ?? ""}`
535
539
  if (res.error === undefined && res.status === 0) {
536
540
  refreshed++
@@ -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,12 +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
- | "chrome-for-testing"
33
29
  | "ffplay"
34
30
  | "intelephense"
35
31
  | "typescript-language-server"
@@ -166,35 +162,26 @@ const locateEffectSolutions = (exec: ProbeExecutor): DependencyLocation => {
166
162
  return { path: resolved, binDir: globalBin }
167
163
  }
168
164
 
169
- const resolveChrome = (exec: ProbeExecutor, platform: NodeJS.Platform): ProbeResult => {
170
- const root = p(home(), ".agent-browser", "browsers")
171
- const relative =
172
- platform === "darwin"
173
- ? "Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
174
- : "chrome"
175
- if (existsSync(root)) {
176
- for (const directory of readdirSync(root).filter((name) => name.startsWith("chrome-")).sort().reverse()) {
177
- const path = exec.which(p(root, directory, relative))
178
- 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"]
179
176
  }
180
177
  }
181
- for (const command of ["chrome-for-testing", "google-chrome-for-testing", "google-chrome", "chromium", "chromium-browser", "brave-browser", "brave"]) {
182
- const path = exec.which(command)
183
- if (path !== "") return { state: "present", path }
184
- }
185
- return { state: "missing" }
178
+ npmGlobalCache.set(exec, out)
179
+ return out
186
180
  }
187
181
 
188
- const latestRtk = (exec: ProbeExecutor): string => {
189
- if (!exec.commandExists("curl")) return ""
190
- const doc = parseJson(
191
- exec.capture("curl", ["-fsSL", "--max-time", "5", "https://api.github.com/repos/rtk-ai/rtk/releases/latest"])
192
- )
193
- const tag = doc !== undefined && isObject(doc) && typeof doc["tag_name"] === "string" ? doc["tag_name"] : ""
194
- return tag.replace(/^v/, "")
195
- }
182
+ const versionNpmGlobal = (pkg: string) => (exec: ProbeExecutor): string => npmGlobalVersions(exec)[pkg] ?? ""
196
183
 
197
- const latestNpm = (id: "agent-browser" | "effect-solutions") => (exec: ProbeExecutor): string =>
184
+ const latestNpm = (id: "effect-solutions") => (exec: ProbeExecutor): string =>
198
185
  exec.commandExists("npm") ? exec.capture("npm", ["view", id, "version"]) : ""
199
186
 
200
187
  export const defaultProbeExecutor: ProbeExecutor = { commandExists, capture, which }
@@ -236,10 +223,6 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
236
223
  () => 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"',
237
224
  { version: versionProbe("codex") }
238
225
  ),
239
- rtk: spec("rtk", "optional", () => "see https://github.com/rtk-ai/rtk (kit auto-install is Linux/macOS-only)", {
240
- version: versionProbe("rtk"),
241
- latest: latestRtk
242
- }),
243
226
  bun: spec(
244
227
  "bun",
245
228
  "optional",
@@ -250,10 +233,8 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
250
233
  locate: (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
251
234
  }
252
235
  ),
253
- bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)"),
254
- "agent-browser": spec("agent-browser", "optional", () => "npm install -g agent-browser", {
255
- version: versionProbe("agent-browser"),
256
- latest: latestNpm("agent-browser")
236
+ bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)", {
237
+ version: versionProbe("bwrap")
257
238
  }),
258
239
  "effect-solutions": spec("effect-solutions", "optional", () => "bun add -g effect-solutions", {
259
240
  resolve: resolveEffectSolutions,
@@ -261,27 +242,22 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
261
242
  locate: locateEffectSolutions,
262
243
  latest: latestNpm("effect-solutions")
263
244
  }),
264
- "chrome-for-testing": spec(
265
- "chrome-for-testing",
266
- "optional",
267
- (pf = rawPlatform()) => (pf === "linux" ? "agent-browser install --with-deps" : "agent-browser install"),
268
- { resolve: resolveChrome }
269
- ),
270
245
  ffplay: spec(
271
246
  "ffplay",
272
247
  "optional",
273
248
  (pf = rawPlatform()) =>
274
249
  pf === "darwin" ? "brew install ffmpeg" : "sudo apt install -y ffmpeg",
275
- { versionArgs: ["-version"], resolve: pathProbe("ffplay") }
250
+ { versionArgs: ["-version"], version: versionProbe("ffplay", ["-version"]), resolve: pathProbe("ffplay") }
276
251
  ),
277
252
  intelephense: spec("intelephense", "optional", () => "npm install -g intelephense", {
278
- resolve: pathProbe("intelephense")
253
+ resolve: pathProbe("intelephense"),
254
+ version: versionNpmGlobal("intelephense")
279
255
  }),
280
256
  "typescript-language-server": spec(
281
257
  "typescript-language-server",
282
258
  "optional",
283
259
  () => "npm install -g typescript-language-server typescript",
284
- { resolve: pathProbe("typescript-language-server") }
260
+ { resolve: pathProbe("typescript-language-server"), version: versionProbe("typescript-language-server") }
285
261
  ),
286
262
  tsc: spec("tsc", "optional", () => "npm install -g typescript", {
287
263
  resolve: pathProbe("tsc"),
@@ -11,9 +11,9 @@ import { homedir } from "node:os"
11
11
  import { kitHome } from "../kitHome"
12
12
  import { makeEngineServices, type EngineServices, type Logger } from "./services"
13
13
  import type { BunRuntimeState } from "./bun"
14
- import { claudeNextSteps, claudeSummary, claudeSync } from "./claudeSync"
14
+ import { claudeNextSteps, claudeSummary, claudeSync, type ClaudeRuntimeState } from "./claudeSync"
15
15
  import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
16
- import { skillsNextSteps, skillsSummary, skillsSync } from "./skillsSync"
16
+ import { skillsNextSteps, skillsSummary, skillsSync, type SkillsState } from "./skillsSync"
17
17
  import { modeModel, modeToolchain } from "./modes"
18
18
  import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs"
19
19
 
@@ -30,7 +30,7 @@ export interface Ctx {
30
30
  readonly agentsDir: string
31
31
  dryRun: boolean
32
32
  verbose: boolean
33
- skipRtk: boolean
33
+ skipBubblewrap: boolean
34
34
  skipPluginRefresh?: boolean
35
35
  reconcile: boolean
36
36
  prune: boolean
@@ -71,7 +71,7 @@ function makeCtx(services: EngineServices): Ctx {
71
71
  agentsDir: env["AGENTS_DIR"] !== undefined && env["AGENTS_DIR"] !== "" ? env["AGENTS_DIR"] : p(home, ".agents"),
72
72
  dryRun: env["DRY_RUN"] === "1",
73
73
  verbose: env["DOCKS_KIT_VERBOSE"] === "1",
74
- skipRtk: env["SKIP_RTK"] === "1",
74
+ skipBubblewrap: env["SKIP_BUBBLEWRAP"] === "1",
75
75
  skipPluginRefresh: false,
76
76
  reconcile: env["RECONCILE"] === "1",
77
77
  prune: env["PRUNE"] === "1",
@@ -95,17 +95,40 @@ function makeCtx(services: EngineServices): Ctx {
95
95
  }
96
96
 
97
97
  function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
98
- const { echo } = ctx.services.logger
98
+ const { clearProgress, echo, progress } = ctx.services.logger
99
99
  parseArgs(ctx, args)
100
100
  validateModifierFlags(ctx)
101
101
 
102
102
  const claudeRan = ctx.syncClaude
103
- const claudeRuntime = claudeRan ? claudeSync(ctx) : undefined
103
+ let claudeRuntime: ClaudeRuntimeState | undefined
104
+ if (claudeRan) {
105
+ progress("Syncing Claude...")
106
+ try {
107
+ claudeRuntime = claudeSync(ctx)
108
+ } finally {
109
+ clearProgress()
110
+ }
111
+ }
104
112
 
105
113
  const codexRan = ctx.syncCodex
106
- if (codexRan) codexSync(ctx)
114
+ if (codexRan) {
115
+ progress("Syncing Codex...")
116
+ try {
117
+ codexSync(ctx)
118
+ } finally {
119
+ clearProgress()
120
+ }
121
+ }
107
122
 
108
- const skillsState = ctx.syncAgents ? skillsSync(ctx) : undefined
123
+ let skillsState: SkillsState | undefined
124
+ if (ctx.syncAgents) {
125
+ progress("Syncing skills...")
126
+ try {
127
+ skillsState = skillsSync(ctx)
128
+ } finally {
129
+ clearProgress()
130
+ }
131
+ }
109
132
 
110
133
  echo("")
111
134
  echo("--- Sync complete ---")
@@ -133,6 +156,8 @@ export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineSe
133
156
  const baseLogger = baseServices.logger
134
157
  const logger: Logger = {
135
158
  change: (msg) => baseLogger.change(msg),
159
+ progress: (msg) => baseLogger.progress(msg),
160
+ clearProgress: () => baseLogger.clearProgress(),
136
161
  verbose: (msg) => {
137
162
  if (ctx.verbose) baseLogger.verbose(msg)
138
163
  },
@@ -1,13 +1,19 @@
1
1
  /**
2
- * Leveled stderr logger + stdout data writer — the Output Policy contract in
3
- * DESIGN.md. Filtering is explicit and synchronous: engine code is imperative,
4
- * so fiber-scoped Effect log levels cannot see these writes. The prefixes and
5
- * ANSI codes are stable golden surface; the level controls visibility only.
2
+ * Leveled stderr logger, transient progress writer, and stdout data writer —
3
+ * the Output Policy contract in DESIGN.md. Filtering is explicit and
4
+ * synchronous: engine code is imperative, so fiber-scoped Effect log levels
5
+ * cannot see these writes. Transient progress is active only through an
6
+ * injected progress sink or an interactive stderr. The prefixes and ANSI codes
7
+ * are stable golden surface; the level controls visibility only.
6
8
  */
7
9
 
8
10
  export interface Logger {
9
11
  /** `[ok]` green — an operation actually mutated something. Always visible. */
10
12
  readonly change: (msg: string) => void
13
+ /** Dim, transient single-line status for blocking work. */
14
+ readonly progress: (msg: string) => void
15
+ /** Erase a pending transient status line. */
16
+ readonly clearProgress: () => void
11
17
  /** `[ok]` green — status-quo confirmation; visible only with verbosity on. */
12
18
  readonly verbose: (msg: string) => void
13
19
  readonly warn: (msg: string) => void
@@ -18,18 +24,75 @@ export interface Logger {
18
24
 
19
25
  export interface LoggerSinks {
20
26
  readonly stderr?: (chunk: string) => void
27
+ readonly progress?: (chunk: string) => void
21
28
  readonly stdout?: (chunk: string) => void
22
29
  }
23
30
 
31
+ interface WritableStreamLike {
32
+ write: (chunk: string) => unknown
33
+ on?: (event: "error", listener: (error: unknown) => void) => unknown
34
+ }
35
+
36
+ const epipeGuarded = new WeakSet<WritableStreamLike>()
37
+
38
+ /**
39
+ * A downstream reader may close the pipe early (`docks-kit toolchain check |
40
+ * head`). That is a normal end of consumption, not a CLI failure, so both the
41
+ * synchronous throw and the asynchronous error event are ignored for EPIPE.
42
+ */
43
+ export function writeIgnoringEpipe(stream: WritableStreamLike, chunk: string): void {
44
+ if (!epipeGuarded.has(stream)) {
45
+ epipeGuarded.add(stream)
46
+ stream.on?.("error", (error) => {
47
+ if ((error as NodeJS.ErrnoException | null)?.code !== "EPIPE") throw error
48
+ })
49
+ }
50
+ try {
51
+ stream.write(chunk)
52
+ } catch (error) {
53
+ if ((error as NodeJS.ErrnoException | null)?.code !== "EPIPE") throw error
54
+ }
55
+ }
56
+
24
57
  export function makeLogger(sinks: LoggerSinks): Logger {
25
- const errWrite = sinks.stderr ?? ((chunk: string) => void process.stderr.write(chunk))
26
- const outWrite = sinks.stdout ?? ((chunk: string) => void process.stdout.write(chunk))
27
- const ok = (msg: string): void => errWrite(`\x1b[1;32m[ok]\x1b[0m ${msg}\n`)
58
+ const errWrite = sinks.stderr ?? ((chunk: string) => writeIgnoringEpipe(process.stderr, chunk))
59
+ const outWrite = sinks.stdout ?? ((chunk: string) => writeIgnoringEpipe(process.stdout, chunk))
60
+ const progressWrite =
61
+ sinks.progress ??
62
+ (sinks.stderr === undefined && process.stderr.isTTY === true
63
+ ? (chunk: string) => writeIgnoringEpipe(process.stderr, chunk)
64
+ : undefined)
65
+ let progressPending = false
66
+
67
+ const clearProgress = (): void => {
68
+ if (!progressPending || progressWrite === undefined) return
69
+ progressWrite("\r\x1b[2K")
70
+ progressPending = false
71
+ }
72
+ const ok = (msg: string): void => {
73
+ clearProgress()
74
+ errWrite(`\x1b[1;32m[ok]\x1b[0m ${msg}\n`)
75
+ }
28
76
  return {
29
77
  change: ok,
30
78
  verbose: ok,
31
- warn: (msg) => errWrite(`\x1b[1;33m[warn]\x1b[0m ${msg}\n`),
32
- err: (msg) => errWrite(`\x1b[1;31m[err]\x1b[0m ${msg}\n`),
33
- echo: (line) => outWrite(`${line}\n`)
79
+ progress: (msg) => {
80
+ if (progressWrite === undefined) return
81
+ progressWrite(`\r\x1b[2K\x1b[2m${msg}\x1b[0m`)
82
+ progressPending = true
83
+ },
84
+ clearProgress,
85
+ warn: (msg) => {
86
+ clearProgress()
87
+ errWrite(`\x1b[1;33m[warn]\x1b[0m ${msg}\n`)
88
+ },
89
+ err: (msg) => {
90
+ clearProgress()
91
+ errWrite(`\x1b[1;31m[err]\x1b[0m ${msg}\n`)
92
+ },
93
+ echo: (line) => {
94
+ clearProgress()
95
+ outWrite(`${line}\n`)
96
+ }
34
97
  }
35
98
  }