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.
- package/AGENTS.md +3 -4
- package/README.md +5 -5
- package/cli/docs/flags.md +7 -2
- package/cli/docs/install.md +2 -2
- package/cli/docs/sync-layers.md +9 -12
- package/cli/docs/toolchain.md +10 -13
- package/cli/src/commands/sync.ts +5 -5
- package/cli/src/commands/toolchain.ts +1 -1
- package/cli/src/commands/update.ts +28 -2
- package/cli/src/engine-native/DESIGN.md +35 -15
- package/cli/src/engine-native/bun.ts +22 -21
- package/cli/src/engine-native/claudeSync.ts +90 -135
- package/cli/src/engine-native/codexSync.ts +31 -27
- package/cli/src/engine-native/deps.ts +52 -68
- package/cli/src/engine-native/exec.ts +61 -5
- package/cli/src/engine-native/index.ts +150 -17
- package/cli/src/engine-native/logger.ts +173 -10
- package/cli/src/engine-native/modes.ts +6 -11
- package/cli/src/engine-native/parseArgs.ts +5 -5
- package/cli/src/engine-native/services.ts +5 -5
- package/cli/src/engine-native/skillsSync.ts +34 -69
- package/cli/src/engine-native/toolchain.ts +44 -27
- package/cli/src/engine.ts +1 -1
- package/cli/src/generated/sotPayload.ts +4 -4
- package/cli/src/main.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
return { state: "missing" }
|
|
179
|
+
return out
|
|
180
|
+
})()
|
|
181
|
+
npmGlobalCache.set(exec, pending)
|
|
182
|
+
return pending
|
|
186
183
|
}
|
|
187
184
|
|
|
188
|
-
const
|
|
189
|
-
|
|
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: "
|
|
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
|
-
|
|
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(
|
|
320
|
-
|
|
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 {
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
|
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
|