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.
@@ -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"
@@ -53,7 +49,7 @@ export interface DependencyLocation {
53
49
 
54
50
  export interface ProbeExecutor {
55
51
  readonly commandExists: (name: string) => boolean
56
- readonly capture: (cmd: string, args: ReadonlyArray<string>) => string
52
+ readonly capture: (cmd: string, args: ReadonlyArray<string>) => Promise<string>
57
53
  readonly which: (name: string) => string
58
54
  }
59
55
 
@@ -64,17 +60,17 @@ export interface DependencySpec {
64
60
  /** Platform-correct one-line install command (param injectable for tests). */
65
61
  readonly installHint: (platform?: NodeJS.Platform) => string
66
62
  readonly resolve?: (exec: ProbeExecutor, platform: NodeJS.Platform) => ProbeResult
67
- readonly version?: (exec: ProbeExecutor) => string
68
- readonly locate?: (exec: ProbeExecutor, platform: NodeJS.Platform) => DependencyLocation
69
- readonly latest?: (exec: ProbeExecutor) => string
63
+ readonly version?: (exec: ProbeExecutor) => Promise<string>
64
+ readonly locate?: (exec: ProbeExecutor, platform: NodeJS.Platform) => Promise<DependencyLocation>
65
+ readonly latest?: (exec: ProbeExecutor) => Promise<string>
70
66
  }
71
67
 
72
68
  interface SpecOptions {
73
69
  readonly versionArgs?: ReadonlyArray<string>
74
70
  readonly resolve?: (exec: ProbeExecutor, platform: NodeJS.Platform) => ProbeResult
75
- readonly version?: (exec: ProbeExecutor) => string
76
- readonly locate?: (exec: ProbeExecutor, platform: NodeJS.Platform) => DependencyLocation
77
- readonly latest?: (exec: ProbeExecutor) => string
71
+ readonly version?: (exec: ProbeExecutor) => Promise<string>
72
+ readonly locate?: (exec: ProbeExecutor, platform: NodeJS.Platform) => Promise<DependencyLocation>
73
+ readonly latest?: (exec: ProbeExecutor) => Promise<string>
78
74
  }
79
75
 
80
76
  const spec = (
@@ -103,8 +99,8 @@ const versionProbe = (
103
99
  id: string,
104
100
  versionArgs: ReadonlyArray<string> = ["--version"],
105
101
  parse: (out: string) => string = (out) => out
106
- ): ((exec: ProbeExecutor) => string) =>
107
- (exec) => parse(exec.capture(id, versionArgs))
102
+ ): ((exec: ProbeExecutor) => Promise<string>) =>
103
+ async (exec) => parse(await exec.capture(id, versionArgs))
108
104
 
109
105
  const home = (): string => {
110
106
  const envHome = process.env["HOME"]
@@ -148,54 +144,49 @@ const resolveEffectSolutions = (exec: ProbeExecutor): ProbeResult => {
148
144
  const versionBunCommand = (exec: ProbeExecutor): string =>
149
145
  exec.commandExists("bun") ? "bun" : p(home(), ".bun", "bin", "bun")
150
146
 
151
- const versionEffectSolutions = (exec: ProbeExecutor): string => {
147
+ const versionEffectSolutions = async (exec: ProbeExecutor): Promise<string> => {
152
148
  const bun = versionBunCommand(exec)
153
149
  if (bun !== "bun" && exec.which(bun) === "") return ""
154
- const match = /effect-solutions@([0-9][0-9.]*)/.exec(exec.capture(bun, ["pm", "-g", "ls"]))
150
+ const match = /effect-solutions@([0-9][0-9.]*)/.exec(await exec.capture(bun, ["pm", "-g", "ls"]))
155
151
  return match?.[1] ?? ""
156
152
  }
157
153
 
158
- const locateEffectSolutions = (exec: ProbeExecutor): DependencyLocation => {
154
+ const locateEffectSolutions = async (exec: ProbeExecutor): Promise<DependencyLocation> => {
159
155
  const strictBun = findBun(exec)
160
156
  const pathBun = exec.which("bun")
161
157
  const bun = strictBun ?? (pathBun !== "" ? { command: "bun", path: pathBun } : undefined)
162
158
  if (bun === undefined) return { path: "", binDir: "" }
163
- const globalBin = exec.capture(bun.command, ["pm", "-g", "bin"])
159
+ const globalBin = await exec.capture(bun.command, ["pm", "-g", "bin"])
164
160
  const path = globalBin !== "" ? p(globalBin, "effect-solutions") : ""
165
161
  const resolved = path !== "" && exec.which(path) !== "" ? path : ""
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, Promise<{ [k: string]: string }>>()
166
+
167
+ const npmGlobalVersions = (exec: ProbeExecutor): Promise<{ [k: string]: string }> => {
168
+ const hit = npmGlobalCache.get(exec)
169
+ if (hit !== undefined) return hit
170
+ const pending = (async (): Promise<{ [k: string]: string }> => {
171
+ const out: { [k: string]: string } = {}
172
+ if (exec.commandExists("npm")) {
173
+ const doc = parseJson(await exec.capture("npm", ["ls", "-g", "--depth=0", "--json"]))
174
+ const deps = doc !== undefined && isObject(doc) && isObject(doc["dependencies"]) ? doc["dependencies"] : {}
175
+ for (const [name, value] of Object.entries(deps)) {
176
+ if (isObject(value) && typeof value["version"] === "string") out[name] = value["version"]
177
+ }
179
178
  }
180
- }
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" }
179
+ return out
180
+ })()
181
+ npmGlobalCache.set(exec, pending)
182
+ return pending
186
183
  }
187
184
 
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
- }
185
+ const versionNpmGlobal = (pkg: string) => async (exec: ProbeExecutor): Promise<string> =>
186
+ (await npmGlobalVersions(exec))[pkg] ?? ""
196
187
 
197
- const latestNpm = (id: "agent-browser" | "effect-solutions") => (exec: ProbeExecutor): string =>
198
- exec.commandExists("npm") ? exec.capture("npm", ["view", id, "version"]) : ""
188
+ const latestNpm = (id: "effect-solutions") => async (exec: ProbeExecutor): Promise<string> =>
189
+ exec.commandExists("npm") ? await exec.capture("npm", ["view", id, "version"]) : ""
199
190
 
200
191
  export const defaultProbeExecutor: ProbeExecutor = { commandExists, capture, which }
201
192
 
@@ -236,10 +227,6 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
236
227
  () => 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"',
237
228
  { version: versionProbe("codex") }
238
229
  ),
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
230
  bun: spec(
244
231
  "bun",
245
232
  "optional",
@@ -247,13 +234,11 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
247
234
  {
248
235
  resolve: resolveBun,
249
236
  version: (exec) => exec.capture(versionBunCommand(exec), ["--version"]),
250
- locate: (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
237
+ locate: async (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
251
238
  }
252
239
  ),
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")
240
+ bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)", {
241
+ version: versionProbe("bwrap")
257
242
  }),
258
243
  "effect-solutions": spec("effect-solutions", "optional", () => "bun add -g effect-solutions", {
259
244
  resolve: resolveEffectSolutions,
@@ -261,27 +246,22 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
261
246
  locate: locateEffectSolutions,
262
247
  latest: latestNpm("effect-solutions")
263
248
  }),
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
249
  ffplay: spec(
271
250
  "ffplay",
272
251
  "optional",
273
252
  (pf = rawPlatform()) =>
274
253
  pf === "darwin" ? "brew install ffmpeg" : "sudo apt install -y ffmpeg",
275
- { versionArgs: ["-version"], resolve: pathProbe("ffplay") }
254
+ { versionArgs: ["-version"], version: versionProbe("ffplay", ["-version"]), resolve: pathProbe("ffplay") }
276
255
  ),
277
256
  intelephense: spec("intelephense", "optional", () => "npm install -g intelephense", {
278
- resolve: pathProbe("intelephense")
257
+ resolve: pathProbe("intelephense"),
258
+ version: versionNpmGlobal("intelephense")
279
259
  }),
280
260
  "typescript-language-server": spec(
281
261
  "typescript-language-server",
282
262
  "optional",
283
263
  () => "npm install -g typescript-language-server typescript",
284
- { resolve: pathProbe("typescript-language-server") }
264
+ { resolve: pathProbe("typescript-language-server"), version: versionProbe("typescript-language-server") }
285
265
  ),
286
266
  tsc: spec("tsc", "optional", () => "npm install -g typescript", {
287
267
  resolve: pathProbe("tsc"),
@@ -301,21 +281,25 @@ export function resolveDependency(
301
281
  return (specification.resolve ?? pathProbe(specification.id))(exec, platform)
302
282
  }
303
283
 
304
- export function resolveVersion(specification: DependencySpec, exec: ProbeExecutor): string {
284
+ export async function resolveVersion(specification: DependencySpec, exec: ProbeExecutor): Promise<string> {
305
285
  if (resolveDependency(specification, exec).state !== "present") return ""
306
- return (specification.version ?? versionProbe(specification.id, specification.versionArgs))(exec)
286
+ return await (specification.version ?? versionProbe(specification.id, specification.versionArgs))(exec)
307
287
  }
308
288
 
309
- export function resolveLocation(
289
+ export async function resolveLocation(
310
290
  specification: DependencySpec,
311
291
  exec: ProbeExecutor,
312
292
  platform: NodeJS.Platform = rawPlatform()
313
- ): DependencyLocation {
314
- if (specification.locate !== undefined) return specification.locate(exec, platform)
293
+ ): Promise<DependencyLocation> {
294
+ if (specification.locate !== undefined) return await specification.locate(exec, platform)
315
295
  const result = resolveDependency(specification, exec, platform)
316
296
  return { path: result.state === "present" ? (result.path ?? exec.which(specification.id)) : "", binDir: "" }
317
297
  }
318
298
 
319
- export function resolvePath(specification: DependencySpec, exec: ProbeExecutor, platform?: NodeJS.Platform): string {
320
- return resolveLocation(specification, exec, platform).path
299
+ export async function resolvePath(
300
+ specification: DependencySpec,
301
+ exec: ProbeExecutor,
302
+ platform?: NodeJS.Platform
303
+ ): Promise<string> {
304
+ return (await resolveLocation(specification, exec, platform)).path
321
305
  }
@@ -3,7 +3,7 @@
3
3
  * the intended binary with deterministic argv, and capture() mirrors command
4
4
  * substitution: stdout with trailing newlines stripped, empty on failure.
5
5
  */
6
- import { spawnSync } from "node:child_process"
6
+ import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process"
7
7
  import { accessSync, chmodSync, constants, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
8
8
  import { delimiter, isAbsolute, join } from "node:path"
9
9
 
@@ -12,10 +12,66 @@ export function p(...parts: Array<string>): string {
12
12
  return parts.join("/")
13
13
  }
14
14
 
15
- export function capture(cmd: string, args: ReadonlyArray<string>): string {
16
- const res = spawnSync(cmd, args as Array<string>, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
17
- if (res.error !== undefined || res.status !== 0) return ""
18
- return (res.stdout ?? "").replace(/[\r\n]+$/, "")
15
+ export interface AsyncProcessResult {
16
+ readonly exitCode: number | null
17
+ readonly stdout: string
18
+ readonly stderr: string
19
+ readonly error?: Error
20
+ }
21
+
22
+ export interface AsyncProcessOptions {
23
+ readonly stdio?: SpawnOptions["stdio"]
24
+ }
25
+
26
+ export function spawnProcess(
27
+ cmd: string,
28
+ args: ReadonlyArray<string>,
29
+ options: AsyncProcessOptions = {}
30
+ ): Promise<AsyncProcessResult> {
31
+ const { promise, resolve } = Promise.withResolvers<AsyncProcessResult>()
32
+ let child: ChildProcess
33
+ try {
34
+ child = spawn(cmd, [...args], { stdio: options.stdio ?? ["ignore", "pipe", "ignore"] })
35
+ } catch (cause) {
36
+ const error = cause instanceof Error ? cause : new Error(String(cause))
37
+ resolve({ exitCode: null, stdout: "", stderr: "", error })
38
+ return promise
39
+ }
40
+ let stdout = ""
41
+ let stderr = ""
42
+ let error: Error | undefined
43
+
44
+ if (child.stdout !== null) {
45
+ child.stdout.setEncoding("utf8")
46
+ child.stdout.on("data", (chunk: string) => {
47
+ stdout += chunk
48
+ })
49
+ child.stdout.on("error", (cause) => {
50
+ error ??= cause
51
+ })
52
+ }
53
+ if (child.stderr !== null) {
54
+ child.stderr.setEncoding("utf8")
55
+ child.stderr.on("data", (chunk: string) => {
56
+ stderr += chunk
57
+ })
58
+ child.stderr.on("error", (cause) => {
59
+ error ??= cause
60
+ })
61
+ }
62
+ child.once("error", (cause) => {
63
+ error ??= cause
64
+ })
65
+ child.once("close", (exitCode) => {
66
+ resolve({ exitCode, stdout, stderr, ...(error !== undefined ? { error } : {}) })
67
+ })
68
+ return promise
69
+ }
70
+
71
+ export async function capture(cmd: string, args: ReadonlyArray<string>): Promise<string> {
72
+ const res = await spawnProcess(cmd, args, { stdio: ["ignore", "pipe", "ignore"] })
73
+ if (res.error !== undefined || res.exitCode !== 0) return ""
74
+ return res.stdout.replace(/[\r\n]+$/, "")
19
75
  }
20
76
 
21
77
  /** `command -v` — resolve an executable name on PATH. */
@@ -9,11 +9,13 @@ import { p } from "./exec"
9
9
  import { homedir } from "node:os"
10
10
 
11
11
  import { kitHome } from "../kitHome"
12
+ import { payloadText } from "../payload"
12
13
  import { makeEngineServices, type EngineServices, type Logger } from "./services"
14
+ import type { TerminalLease } from "./logger"
13
15
  import type { BunRuntimeState } from "./bun"
14
- import { claudeNextSteps, claudeSummary, claudeSync } from "./claudeSync"
16
+ import { claudeNextSteps, claudeSummary, claudeSync, type ClaudeRuntimeState } from "./claudeSync"
15
17
  import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
16
- import { skillsNextSteps, skillsSummary, skillsSync } from "./skillsSync"
18
+ import { normalizeManifest, skillsNextSteps, skillsSummary, skillsSync, type SkillsState } from "./skillsSync"
17
19
  import { modeModel, modeToolchain } from "./modes"
18
20
  import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs"
19
21
 
@@ -24,13 +26,61 @@ export type ModifierFlag =
24
26
  | "--codex-model"
25
27
  | "--codex-effort"
26
28
 
29
+ export type SyncConcurrency = 1 | 2 | 3
30
+ export type SyncTask<T> = () => Promise<T>
31
+
32
+ /**
33
+ * Run input-ordered tasks with bounded overlap. Once one task rejects, queued
34
+ * tasks stay queued while already-started tasks drain; the earliest rejection
35
+ * in input order is then propagated.
36
+ */
37
+ export async function runBounded<T>(
38
+ tasks: ReadonlyArray<SyncTask<T>>,
39
+ concurrency: SyncConcurrency
40
+ ): Promise<Array<T>> {
41
+ const results = new Array<T>(tasks.length)
42
+ const failures = new Map<number, unknown>()
43
+ let nextIndex = 0
44
+ let stopped = false
45
+
46
+ const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, async () => {
47
+ for (;;) {
48
+ if (stopped || nextIndex >= tasks.length) return
49
+ const index = nextIndex
50
+ nextIndex += 1
51
+ try {
52
+ results[index] = await tasks[index]!()
53
+ } catch (error) {
54
+ failures.set(index, error)
55
+ stopped = true
56
+ }
57
+ }
58
+ })
59
+ await Promise.all(workers)
60
+
61
+ for (let index = 0; index < tasks.length; index += 1) {
62
+ if (failures.has(index)) throw failures.get(index)
63
+ }
64
+ return results
65
+ }
66
+
67
+ export function syncConcurrencyForManifest(
68
+ configured: SyncConcurrency,
69
+ manifest: string,
70
+ claudeSelected: boolean,
71
+ skillsSelected: boolean
72
+ ): SyncConcurrency {
73
+ if (!claudeSelected || !skillsSelected || normalizeManifest(manifest).length === 0) return configured
74
+ return 1
75
+ }
76
+
27
77
  export interface Ctx {
28
78
  readonly repoDir: string
29
79
  readonly home: string
30
80
  readonly agentsDir: string
31
81
  dryRun: boolean
32
82
  verbose: boolean
33
- skipRtk: boolean
83
+ skipBubblewrap: boolean
34
84
  skipPluginRefresh?: boolean
35
85
  reconcile: boolean
36
86
  prune: boolean
@@ -47,7 +97,9 @@ export interface Ctx {
47
97
  modifierFlags?: Set<ModifierFlag>
48
98
  /** Injected capability seam (logger/deps/platform) — see services.ts. */
49
99
  readonly services: EngineServices
50
- bunRuntime?: BunRuntimeState
100
+ syncConcurrency: SyncConcurrency
101
+ terminalLease?: TerminalLease
102
+ bunRuntime?: Promise<BunRuntimeState>
51
103
  targetFilterSet: boolean
52
104
  syncClaude: boolean
53
105
  syncCodex: boolean
@@ -71,7 +123,7 @@ function makeCtx(services: EngineServices): Ctx {
71
123
  agentsDir: env["AGENTS_DIR"] !== undefined && env["AGENTS_DIR"] !== "" ? env["AGENTS_DIR"] : p(home, ".agents"),
72
124
  dryRun: env["DRY_RUN"] === "1",
73
125
  verbose: env["DOCKS_KIT_VERBOSE"] === "1",
74
- skipRtk: env["SKIP_RTK"] === "1",
126
+ skipBubblewrap: env["SKIP_BUBBLEWRAP"] === "1",
75
127
  skipPluginRefresh: false,
76
128
  reconcile: env["RECONCILE"] === "1",
77
129
  prune: env["PRUNE"] === "1",
@@ -85,6 +137,7 @@ function makeCtx(services: EngineServices): Ctx {
85
137
  codexModel: env["CODEX_MODEL"] ?? "",
86
138
  codexEffort: "",
87
139
  modifierFlags: new Set(),
140
+ syncConcurrency: 3,
88
141
  services,
89
142
  targetFilterSet: false,
90
143
  syncClaude: false,
@@ -94,18 +147,95 @@ function makeCtx(services: EngineServices): Ctx {
94
147
  }
95
148
  }
96
149
 
97
- function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
98
- const { echo } = ctx.services.logger
150
+ async function engineSync(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
151
+ const { acquireTerminal, echo, err } = ctx.services.logger
99
152
  parseArgs(ctx, args)
100
153
  validateModifierFlags(ctx)
101
154
 
102
- const claudeRan = ctx.syncClaude
103
- const claudeRuntime = claudeRan ? claudeSync(ctx) : undefined
155
+ const configuredConcurrency = process.env["DOCKS_KIT_SYNC_CONCURRENCY"]
156
+ if (configuredConcurrency === undefined || configuredConcurrency === "") {
157
+ ctx.syncConcurrency = 3
158
+ } else if (
159
+ configuredConcurrency === "1" ||
160
+ configuredConcurrency === "2" ||
161
+ configuredConcurrency === "3"
162
+ ) {
163
+ ctx.syncConcurrency = Number(configuredConcurrency) as SyncConcurrency
164
+ } else {
165
+ err("DOCKS_KIT_SYNC_CONCURRENCY must be 1, 2, or 3")
166
+ throw new ExitError(2)
167
+ }
104
168
 
105
- const codexRan = ctx.syncCodex
106
- if (codexRan) codexSync(ctx)
169
+ type PipelineResult =
170
+ | { readonly kind: "claude"; readonly runtime: ClaudeRuntimeState }
171
+ | { readonly kind: "codex" }
172
+ | { readonly kind: "skills"; readonly state: SkillsState }
173
+ interface SelectedPipeline {
174
+ readonly name: string
175
+ readonly run: SyncTask<PipelineResult>
176
+ }
177
+
178
+ const selected: Array<SelectedPipeline> = []
179
+ if (ctx.syncClaude) {
180
+ selected.push({
181
+ name: "Claude",
182
+ run: async () => ({ kind: "claude", runtime: await claudeSync(ctx) })
183
+ })
184
+ }
185
+ if (ctx.syncCodex) {
186
+ selected.push({
187
+ name: "Codex",
188
+ run: async () => {
189
+ await codexSync(ctx)
190
+ return { kind: "codex" }
191
+ }
192
+ })
193
+ }
194
+ if (ctx.syncAgents) {
195
+ selected.push({
196
+ name: "skills",
197
+ run: async () => ({ kind: "skills", state: await skillsSync(ctx) })
198
+ })
199
+ }
200
+
201
+ // A populated skills manifest deploys with `-a claude-code codex`, and symlink healing also writes into Claude's tree.
202
+ ctx.syncConcurrency = syncConcurrencyForManifest(
203
+ ctx.syncConcurrency,
204
+ payloadText("SoT/.agents/skills.txt"),
205
+ ctx.syncClaude,
206
+ ctx.syncAgents
207
+ )
107
208
 
108
- const skillsState = ctx.syncAgents ? skillsSync(ctx) : undefined
209
+ const remaining = new Set(selected.map(({ name }) => name))
210
+ const lease = acquireTerminal(`Syncing ${[...remaining].join(", ")}...`)
211
+ ctx.terminalLease = lease
212
+ let results: Array<PipelineResult>
213
+ try {
214
+ const tasks = selected.map(
215
+ ({ name, run }): SyncTask<PipelineResult> =>
216
+ async () => {
217
+ try {
218
+ return await run()
219
+ } finally {
220
+ remaining.delete(name)
221
+ if (remaining.size > 0) lease.update(`Syncing ${[...remaining].join(", ")}...`)
222
+ }
223
+ }
224
+ )
225
+ results = await runBounded(tasks, ctx.syncConcurrency)
226
+ } finally {
227
+ lease.release()
228
+ ctx.terminalLease = undefined
229
+ }
230
+
231
+ const claudeRan = ctx.syncClaude
232
+ const codexRan = ctx.syncCodex
233
+ let claudeRuntime: ClaudeRuntimeState | undefined
234
+ let skillsState: SkillsState | undefined
235
+ for (const result of results) {
236
+ if (result.kind === "claude") claudeRuntime = result.runtime
237
+ else if (result.kind === "skills") skillsState = result.state
238
+ }
109
239
 
110
240
  echo("")
111
241
  echo("--- Sync complete ---")
@@ -127,18 +257,21 @@ function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
127
257
  }
128
258
 
129
259
 
130
- export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineServices): number {
260
+ export async function runEngineNative(argv: ReadonlyArray<string>, services?: EngineServices): Promise<number> {
131
261
  let ctx!: Ctx
132
262
  const baseServices = services ?? makeEngineServices()
133
263
  const baseLogger = baseServices.logger
134
264
  const logger: Logger = {
135
265
  change: (msg) => baseLogger.change(msg),
266
+ progress: (msg) => baseLogger.progress(msg),
267
+ clearProgress: () => baseLogger.clearProgress(),
136
268
  verbose: (msg) => {
137
269
  if (ctx.verbose) baseLogger.verbose(msg)
138
270
  },
139
271
  warn: (msg) => baseLogger.warn(msg),
140
272
  err: (msg) => baseLogger.err(msg),
141
- echo: (line) => baseLogger.echo(line)
273
+ echo: (line) => baseLogger.echo(line),
274
+ acquireTerminal: (message) => baseLogger.acquireTerminal(message)
142
275
  }
143
276
  const runServices: EngineServices = {
144
277
  logger,
@@ -151,11 +284,11 @@ export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineSe
151
284
  case "model":
152
285
  return modeModel(ctx, argv.slice(1))
153
286
  case "toolchain":
154
- return modeToolchain(ctx, argv.slice(1))
287
+ return await modeToolchain(ctx, argv.slice(1))
155
288
  case "sync":
156
- return engineSync(ctx, argv.slice(1))
289
+ return await engineSync(ctx, argv.slice(1))
157
290
  default:
158
- return engineSync(ctx, argv)
291
+ return await engineSync(ctx, argv)
159
292
  }
160
293
  } catch (e) {
161
294
  if (e instanceof ExitError) return e.code