docks-kit 0.14.3 → 0.15.0
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 -1
- package/README.md +11 -5
- package/cli/docs/flags.md +6 -0
- package/cli/docs/overview.md +1 -1
- package/cli/src/argv.ts +433 -0
- package/cli/src/commands/docs.ts +6 -6
- package/cli/src/commands/model.ts +11 -11
- package/cli/src/commands/models.ts +6 -6
- package/cli/src/commands/plugins.ts +6 -6
- package/cli/src/commands/skills.ts +6 -6
- package/cli/src/commands/status.ts +3 -3
- package/cli/src/commands/sync.ts +43 -92
- package/cli/src/commands/toolchain.ts +12 -12
- package/cli/src/commands/update.ts +3 -3
- package/cli/src/engine-native/DESIGN.md +15 -4
- package/cli/src/engine-native/bun.ts +22 -21
- package/cli/src/engine-native/claudeSync.ts +30 -31
- package/cli/src/engine-native/codexSync.ts +24 -24
- package/cli/src/engine-native/deps.ts +43 -35
- package/cli/src/engine-native/exec.ts +61 -5
- package/cli/src/engine-native/index.ts +141 -33
- package/cli/src/engine-native/logger.ts +114 -14
- package/cli/src/engine-native/modes.ts +4 -4
- package/cli/src/engine-native/parseArgs.ts +1 -1
- package/cli/src/engine-native/services.ts +5 -5
- package/cli/src/engine-native/skillsSync.ts +19 -20
- package/cli/src/engine-native/toolchain.ts +40 -27
- package/cli/src/engine.ts +2 -2
- package/cli/src/generated/sotPayload.ts +1 -1
- package/cli/src/main.ts +27 -28
- package/cli/src/services.ts +4 -4
- package/docks-kit +1 -1
- package/package.json +5 -7
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
* avoid a TOML library because reformatting user configs would be a behavior
|
|
4
4
|
* change. Guard order, message strings, and backup behavior are golden-tested.
|
|
5
5
|
*/
|
|
6
|
-
import { spawnSync } from "node:child_process"
|
|
7
6
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
8
7
|
|
|
9
8
|
import { syncCodexEffort, syncCodexModel, replaceTopLevelSettingInFile } from "./codexToml"
|
|
10
|
-
import { p } from "./exec"
|
|
9
|
+
import { p, spawnProcess } from "./exec"
|
|
11
10
|
import type { Ctx } from "./index"
|
|
12
11
|
import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
13
12
|
import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
|
|
14
13
|
|
|
15
|
-
export function codexSync(ctx: Ctx): void {
|
|
14
|
+
export async function codexSync(ctx: Ctx): Promise<void> {
|
|
16
15
|
const codexDir = p(ctx.home, ".codex")
|
|
17
16
|
const sotConfig = payloadText("SoT/.codex/config.toml")
|
|
18
17
|
const userConfig = p(codexDir, "config.toml")
|
|
19
18
|
|
|
20
|
-
ensureBubblewrap(ctx)
|
|
19
|
+
await ensureBubblewrap(ctx)
|
|
21
20
|
if (!ctx.dryRun) mkdirSync(codexDir, { recursive: true })
|
|
22
21
|
syncConfig(ctx, sotConfig, userConfig)
|
|
23
22
|
syncCodexModel(ctx, ctx.codexModel)
|
|
@@ -25,13 +24,13 @@ export function codexSync(ctx: Ctx): void {
|
|
|
25
24
|
syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
|
|
26
25
|
syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
|
|
27
26
|
syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
|
|
28
|
-
removeLegacyDocksMarketplace(ctx, userConfig)
|
|
29
|
-
syncPlugins(ctx, sotConfig)
|
|
27
|
+
await removeLegacyDocksMarketplace(ctx, userConfig)
|
|
28
|
+
await syncPlugins(ctx, sotConfig)
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
// ---------------------------------------------------------- bubblewrap ----
|
|
33
32
|
|
|
34
|
-
function ensureBubblewrap(ctx: Ctx): void {
|
|
33
|
+
async function ensureBubblewrap(ctx: Ctx): Promise<void> {
|
|
35
34
|
const { change, echo, warn } = ctx.services.logger
|
|
36
35
|
if (!bwrapSupportedOs(ctx)) return
|
|
37
36
|
|
|
@@ -58,8 +57,10 @@ function ensureBubblewrap(ctx: Ctx): void {
|
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
warn(`bubblewrap not installed - recommended for Codex Linux sandbox. Running: ${pmInstall} (sudo prompt may appear)`)
|
|
61
|
-
const
|
|
62
|
-
|
|
60
|
+
const runInstaller = () =>
|
|
61
|
+
spawnProcess("bash", ["-c", pmInstall], { stdio: ["inherit", "inherit", "inherit"] })
|
|
62
|
+
const res = await (ctx.terminalLease?.withExclusive(runInstaller) ?? runInstaller())
|
|
63
|
+
if (res.exitCode !== 0) {
|
|
63
64
|
warn(`Failed to auto-install bubblewrap. Install manually: ${pmInstall}`)
|
|
64
65
|
return
|
|
65
66
|
}
|
|
@@ -69,8 +70,8 @@ function ensureBubblewrap(ctx: Ctx): void {
|
|
|
69
70
|
return
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
if (
|
|
73
|
-
change(`bubblewrap installed and functional (${ctx.services.deps.version("bwrap")})`)
|
|
73
|
+
if ((await spawnProcess("unshare", ["-Ur", "true"], { stdio: "ignore" })).exitCode === 0) {
|
|
74
|
+
change(`bubblewrap installed and functional (${await ctx.services.deps.version("bwrap")})`)
|
|
74
75
|
} else {
|
|
75
76
|
warn(
|
|
76
77
|
"bubblewrap installed but unprivileged user namespaces appear blocked. On Ubuntu 24.04+, prefer loading the AppArmor bwrap-userns-restrict profile; fallback: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0"
|
|
@@ -411,7 +412,7 @@ export function marketplaceSource(marketplace: string, configFile: string): stri
|
|
|
411
412
|
return ""
|
|
412
413
|
}
|
|
413
414
|
|
|
414
|
-
function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): void {
|
|
415
|
+
async function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): Promise<void> {
|
|
415
416
|
const { change, echo, warn } = ctx.services.logger
|
|
416
417
|
if (ctx.dryRun) {
|
|
417
418
|
echo("[dry-run] remove legacy configured Codex Docks marketplace when personal marketplace is deployed")
|
|
@@ -422,8 +423,8 @@ function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): void {
|
|
|
422
423
|
|
|
423
424
|
const source = marketplaceSource("docks", userConfig)
|
|
424
425
|
if (source !== "https://github.com/DocksDocks/docks.git" && source !== "DocksDocks/docks") return
|
|
425
|
-
const res =
|
|
426
|
-
if (res.error === undefined && res.
|
|
426
|
+
const res = await spawnProcess("codex", ["plugin", "marketplace", "remove", "docks"], { stdio: "ignore" })
|
|
427
|
+
if (res.error === undefined && res.exitCode === 0) {
|
|
427
428
|
change("Removed legacy configured Codex Docks marketplace; using personal marketplace file")
|
|
428
429
|
ctx.nextStepTriggers.codexRestart = true
|
|
429
430
|
} else {
|
|
@@ -474,13 +475,12 @@ function manualPluginRefreshCommand(sotConfigText: string): string {
|
|
|
474
475
|
return first !== undefined ? `codex plugin add ${first}` : "codex plugin add <plugin@marketplace>"
|
|
475
476
|
}
|
|
476
477
|
|
|
477
|
-
function installedPluginIdsFromCli(): Set<string> | undefined {
|
|
478
|
-
const result =
|
|
479
|
-
encoding: "utf8",
|
|
478
|
+
async function installedPluginIdsFromCli(): Promise<Set<string> | undefined> {
|
|
479
|
+
const result = await spawnProcess("codex", ["plugin", "list", "--json"], {
|
|
480
480
|
stdio: ["ignore", "pipe", "ignore"]
|
|
481
481
|
})
|
|
482
|
-
if (result.error !== undefined || result.
|
|
483
|
-
const value = parseJson(result.stdout
|
|
482
|
+
if (result.error !== undefined || result.exitCode !== 0) return undefined
|
|
483
|
+
const value = parseJson(result.stdout)
|
|
484
484
|
if (value === undefined || !isObject(value) || !Array.isArray(value["installed"])) return undefined
|
|
485
485
|
const ids = new Set<string>()
|
|
486
486
|
for (const row of value["installed"]) {
|
|
@@ -490,7 +490,7 @@ function installedPluginIdsFromCli(): Set<string> | undefined {
|
|
|
490
490
|
return ids
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
493
|
+
async function syncPlugins(ctx: Ctx, sotConfigText: string): Promise<void> {
|
|
494
494
|
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
495
495
|
if (ctx.dryRun) {
|
|
496
496
|
echo(
|
|
@@ -520,7 +520,7 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
|
520
520
|
let pluginIds = desiredPluginIds
|
|
521
521
|
if (ctx.skipPluginRefresh) {
|
|
522
522
|
progress("Checking installed Codex plugins...")
|
|
523
|
-
const installedPluginIds = installedPluginIdsFromCli()
|
|
523
|
+
const installedPluginIds = await installedPluginIdsFromCli()
|
|
524
524
|
clearProgress()
|
|
525
525
|
if (installedPluginIds === undefined) {
|
|
526
526
|
warn("Codex plugin inventory unavailable — falling back to the full refresh path")
|
|
@@ -533,10 +533,10 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
|
533
533
|
let failed = 0
|
|
534
534
|
for (const pluginId of pluginIds) {
|
|
535
535
|
progress(`Updating Codex plugin ${pluginId}...`)
|
|
536
|
-
const res =
|
|
536
|
+
const res = await spawnProcess("codex", ["plugin", "add", pluginId], { stdio: ["ignore", "pipe", "pipe"] })
|
|
537
537
|
clearProgress()
|
|
538
|
-
const addOut = `${res.stdout
|
|
539
|
-
if (res.error === undefined && res.
|
|
538
|
+
const addOut = `${res.stdout}${res.stderr}`
|
|
539
|
+
if (res.error === undefined && res.exitCode === 0) {
|
|
540
540
|
refreshed++
|
|
541
541
|
} else if (addOut.includes("could not find a Codex CLI binary")) {
|
|
542
542
|
warn(
|
|
@@ -49,7 +49,7 @@ export interface DependencyLocation {
|
|
|
49
49
|
|
|
50
50
|
export interface ProbeExecutor {
|
|
51
51
|
readonly commandExists: (name: string) => boolean
|
|
52
|
-
readonly capture: (cmd: string, args: ReadonlyArray<string>) => string
|
|
52
|
+
readonly capture: (cmd: string, args: ReadonlyArray<string>) => Promise<string>
|
|
53
53
|
readonly which: (name: string) => string
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -60,17 +60,17 @@ export interface DependencySpec {
|
|
|
60
60
|
/** Platform-correct one-line install command (param injectable for tests). */
|
|
61
61
|
readonly installHint: (platform?: NodeJS.Platform) => string
|
|
62
62
|
readonly resolve?: (exec: ProbeExecutor, platform: NodeJS.Platform) => ProbeResult
|
|
63
|
-
readonly version?: (exec: ProbeExecutor) => string
|
|
64
|
-
readonly locate?: (exec: ProbeExecutor, platform: NodeJS.Platform) => DependencyLocation
|
|
65
|
-
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>
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
interface SpecOptions {
|
|
69
69
|
readonly versionArgs?: ReadonlyArray<string>
|
|
70
70
|
readonly resolve?: (exec: ProbeExecutor, platform: NodeJS.Platform) => ProbeResult
|
|
71
|
-
readonly version?: (exec: ProbeExecutor) => string
|
|
72
|
-
readonly locate?: (exec: ProbeExecutor, platform: NodeJS.Platform) => DependencyLocation
|
|
73
|
-
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>
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
const spec = (
|
|
@@ -99,8 +99,8 @@ const versionProbe = (
|
|
|
99
99
|
id: string,
|
|
100
100
|
versionArgs: ReadonlyArray<string> = ["--version"],
|
|
101
101
|
parse: (out: string) => string = (out) => out
|
|
102
|
-
): ((exec: ProbeExecutor) => string) =>
|
|
103
|
-
(exec) => parse(exec.capture(id, versionArgs))
|
|
102
|
+
): ((exec: ProbeExecutor) => Promise<string>) =>
|
|
103
|
+
async (exec) => parse(await exec.capture(id, versionArgs))
|
|
104
104
|
|
|
105
105
|
const home = (): string => {
|
|
106
106
|
const envHome = process.env["HOME"]
|
|
@@ -144,45 +144,49 @@ const resolveEffectSolutions = (exec: ProbeExecutor): ProbeResult => {
|
|
|
144
144
|
const versionBunCommand = (exec: ProbeExecutor): string =>
|
|
145
145
|
exec.commandExists("bun") ? "bun" : p(home(), ".bun", "bin", "bun")
|
|
146
146
|
|
|
147
|
-
const versionEffectSolutions = (exec: ProbeExecutor): string => {
|
|
147
|
+
const versionEffectSolutions = async (exec: ProbeExecutor): Promise<string> => {
|
|
148
148
|
const bun = versionBunCommand(exec)
|
|
149
149
|
if (bun !== "bun" && exec.which(bun) === "") return ""
|
|
150
|
-
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"]))
|
|
151
151
|
return match?.[1] ?? ""
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
const locateEffectSolutions = (exec: ProbeExecutor): DependencyLocation => {
|
|
154
|
+
const locateEffectSolutions = async (exec: ProbeExecutor): Promise<DependencyLocation> => {
|
|
155
155
|
const strictBun = findBun(exec)
|
|
156
156
|
const pathBun = exec.which("bun")
|
|
157
157
|
const bun = strictBun ?? (pathBun !== "" ? { command: "bun", path: pathBun } : undefined)
|
|
158
158
|
if (bun === undefined) return { path: "", binDir: "" }
|
|
159
|
-
const globalBin = exec.capture(bun.command, ["pm", "-g", "bin"])
|
|
159
|
+
const globalBin = await exec.capture(bun.command, ["pm", "-g", "bin"])
|
|
160
160
|
const path = globalBin !== "" ? p(globalBin, "effect-solutions") : ""
|
|
161
161
|
const resolved = path !== "" && exec.which(path) !== "" ? path : ""
|
|
162
162
|
return { path: resolved, binDir: globalBin }
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
const npmGlobalCache = new WeakMap<ProbeExecutor, { [k: string]: string }
|
|
165
|
+
const npmGlobalCache = new WeakMap<ProbeExecutor, Promise<{ [k: string]: string }>>()
|
|
166
166
|
|
|
167
|
-
const npmGlobalVersions = (exec: ProbeExecutor): { [k: string]: string } => {
|
|
167
|
+
const npmGlobalVersions = (exec: ProbeExecutor): Promise<{ [k: string]: string }> => {
|
|
168
168
|
const hit = npmGlobalCache.get(exec)
|
|
169
169
|
if (hit !== undefined) return hit
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
}
|
|
176
178
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
return out
|
|
180
|
+
})()
|
|
181
|
+
npmGlobalCache.set(exec, pending)
|
|
182
|
+
return pending
|
|
180
183
|
}
|
|
181
184
|
|
|
182
|
-
const versionNpmGlobal = (pkg: string) => (exec: ProbeExecutor): string =>
|
|
185
|
+
const versionNpmGlobal = (pkg: string) => async (exec: ProbeExecutor): Promise<string> =>
|
|
186
|
+
(await npmGlobalVersions(exec))[pkg] ?? ""
|
|
183
187
|
|
|
184
|
-
const latestNpm = (id: "effect-solutions") => (exec: ProbeExecutor): string =>
|
|
185
|
-
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"]) : ""
|
|
186
190
|
|
|
187
191
|
export const defaultProbeExecutor: ProbeExecutor = { commandExists, capture, which }
|
|
188
192
|
|
|
@@ -230,7 +234,7 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
230
234
|
{
|
|
231
235
|
resolve: resolveBun,
|
|
232
236
|
version: (exec) => exec.capture(versionBunCommand(exec), ["--version"]),
|
|
233
|
-
locate: (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
|
|
237
|
+
locate: async (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
|
|
234
238
|
}
|
|
235
239
|
),
|
|
236
240
|
bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)", {
|
|
@@ -277,21 +281,25 @@ export function resolveDependency(
|
|
|
277
281
|
return (specification.resolve ?? pathProbe(specification.id))(exec, platform)
|
|
278
282
|
}
|
|
279
283
|
|
|
280
|
-
export function resolveVersion(specification: DependencySpec, exec: ProbeExecutor): string {
|
|
284
|
+
export async function resolveVersion(specification: DependencySpec, exec: ProbeExecutor): Promise<string> {
|
|
281
285
|
if (resolveDependency(specification, exec).state !== "present") return ""
|
|
282
|
-
return (specification.version ?? versionProbe(specification.id, specification.versionArgs))(exec)
|
|
286
|
+
return await (specification.version ?? versionProbe(specification.id, specification.versionArgs))(exec)
|
|
283
287
|
}
|
|
284
288
|
|
|
285
|
-
export function resolveLocation(
|
|
289
|
+
export async function resolveLocation(
|
|
286
290
|
specification: DependencySpec,
|
|
287
291
|
exec: ProbeExecutor,
|
|
288
292
|
platform: NodeJS.Platform = rawPlatform()
|
|
289
|
-
): DependencyLocation {
|
|
290
|
-
if (specification.locate !== undefined) return specification.locate(exec, platform)
|
|
293
|
+
): Promise<DependencyLocation> {
|
|
294
|
+
if (specification.locate !== undefined) return await specification.locate(exec, platform)
|
|
291
295
|
const result = resolveDependency(specification, exec, platform)
|
|
292
296
|
return { path: result.state === "present" ? (result.path ?? exec.which(specification.id)) : "", binDir: "" }
|
|
293
297
|
}
|
|
294
298
|
|
|
295
|
-
export function resolvePath(
|
|
296
|
-
|
|
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
|
|
297
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
16
|
import { claudeNextSteps, claudeSummary, claudeSync, type ClaudeRuntimeState } from "./claudeSync"
|
|
15
17
|
import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
|
|
16
|
-
import { skillsNextSteps, skillsSummary, skillsSync, type SkillsState } 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,6 +26,54 @@ 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
|
|
@@ -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
|
|
@@ -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,40 +147,94 @@ function makeCtx(services: EngineServices): Ctx {
|
|
|
94
147
|
}
|
|
95
148
|
}
|
|
96
149
|
|
|
97
|
-
function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
98
|
-
const {
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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)
|
|
111
167
|
}
|
|
112
168
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
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>
|
|
121
176
|
}
|
|
122
177
|
|
|
123
|
-
|
|
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
|
+
}
|
|
124
194
|
if (ctx.syncAgents) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
)
|
|
208
|
+
|
|
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
|
|
131
238
|
}
|
|
132
239
|
|
|
133
240
|
echo("")
|
|
@@ -150,7 +257,7 @@ function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
150
257
|
}
|
|
151
258
|
|
|
152
259
|
|
|
153
|
-
export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineServices): number {
|
|
260
|
+
export async function runEngineNative(argv: ReadonlyArray<string>, services?: EngineServices): Promise<number> {
|
|
154
261
|
let ctx!: Ctx
|
|
155
262
|
const baseServices = services ?? makeEngineServices()
|
|
156
263
|
const baseLogger = baseServices.logger
|
|
@@ -163,7 +270,8 @@ export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineSe
|
|
|
163
270
|
},
|
|
164
271
|
warn: (msg) => baseLogger.warn(msg),
|
|
165
272
|
err: (msg) => baseLogger.err(msg),
|
|
166
|
-
echo: (line) => baseLogger.echo(line)
|
|
273
|
+
echo: (line) => baseLogger.echo(line),
|
|
274
|
+
acquireTerminal: (message) => baseLogger.acquireTerminal(message)
|
|
167
275
|
}
|
|
168
276
|
const runServices: EngineServices = {
|
|
169
277
|
logger,
|
|
@@ -176,11 +284,11 @@ export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineSe
|
|
|
176
284
|
case "model":
|
|
177
285
|
return modeModel(ctx, argv.slice(1))
|
|
178
286
|
case "toolchain":
|
|
179
|
-
return modeToolchain(ctx, argv.slice(1))
|
|
287
|
+
return await modeToolchain(ctx, argv.slice(1))
|
|
180
288
|
case "sync":
|
|
181
|
-
return engineSync(ctx, argv.slice(1))
|
|
289
|
+
return await engineSync(ctx, argv.slice(1))
|
|
182
290
|
default:
|
|
183
|
-
return engineSync(ctx, argv)
|
|
291
|
+
return await engineSync(ctx, argv)
|
|
184
292
|
}
|
|
185
293
|
} catch (e) {
|
|
186
294
|
if (e instanceof ExitError) return e.code
|