docks-kit 0.14.3 → 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/README.md +1 -1
- package/cli/docs/flags.md +6 -0
- 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/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 +1 -1
- package/cli/src/generated/sotPayload.ts +1 -1
- package/cli/src/main.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
@@ -1,11 +1,22 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks"
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Leveled stderr logger,
|
|
3
|
-
* the Output Policy contract in DESIGN.md. Filtering is explicit and
|
|
4
|
+
* Leveled stderr logger, one run-scoped terminal lease, and stdout data
|
|
5
|
+
* writer — the Output Policy contract in DESIGN.md. Filtering is explicit and
|
|
4
6
|
* synchronous: engine code is imperative, so fiber-scoped Effect log levels
|
|
5
7
|
* cannot see these writes. Transient progress is active only through an
|
|
6
8
|
* injected progress sink or an interactive stderr. The prefixes and ANSI codes
|
|
7
9
|
* are stable golden surface; the level controls visibility only.
|
|
8
10
|
*/
|
|
11
|
+
export interface TerminalLease {
|
|
12
|
+
/** Replace the coordinator-owned transient line. */
|
|
13
|
+
readonly update: (message: string) => void
|
|
14
|
+
/** Serialize terminal input owners and suspend transient redraw while held. */
|
|
15
|
+
readonly withExclusive: <T>(action: () => T | Promise<T>) => Promise<T>
|
|
16
|
+
/** Clear coordinator progress and return ownership to ordinary progress calls. */
|
|
17
|
+
readonly release: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
|
|
10
21
|
export interface Logger {
|
|
11
22
|
/** `[ok]` green — an operation actually mutated something. Always visible. */
|
|
@@ -20,6 +31,8 @@ export interface Logger {
|
|
|
20
31
|
readonly err: (msg: string) => void
|
|
21
32
|
/** stdout data line (dry-run report, summary, usage) — never filtered. */
|
|
22
33
|
readonly echo: (line: string) => void
|
|
34
|
+
/** Acquire the run-scoped coordinator lease for terminal progress and input. */
|
|
35
|
+
readonly acquireTerminal: (message: string) => TerminalLease
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
export interface LoggerSinks {
|
|
@@ -63,36 +76,123 @@ export function makeLogger(sinks: LoggerSinks): Logger {
|
|
|
63
76
|
? (chunk: string) => writeIgnoringEpipe(process.stderr, chunk)
|
|
64
77
|
: undefined)
|
|
65
78
|
let progressPending = false
|
|
79
|
+
let activeLease: TerminalLease | undefined
|
|
80
|
+
let exclusiveActive = false
|
|
81
|
+
const exclusiveContext = new AsyncLocalStorage<{ active: boolean }>()
|
|
82
|
+
const durableBuffer: Array<() => void> = []
|
|
66
83
|
|
|
67
|
-
|
|
84
|
+
|
|
85
|
+
const eraseProgress = (): void => {
|
|
68
86
|
if (!progressPending || progressWrite === undefined) return
|
|
69
87
|
progressWrite("\r\x1b[2K")
|
|
70
88
|
progressPending = false
|
|
71
89
|
}
|
|
90
|
+
const drawProgress = (message: string): void => {
|
|
91
|
+
if (progressWrite === undefined) return
|
|
92
|
+
progressWrite(`\r\x1b[2K\x1b[2m${message}\x1b[0m`)
|
|
93
|
+
progressPending = true
|
|
94
|
+
}
|
|
95
|
+
const clearProgress = (): void => {
|
|
96
|
+
if (activeLease !== undefined) return
|
|
97
|
+
eraseProgress()
|
|
98
|
+
}
|
|
99
|
+
const writeDurable = (write: () => void): void => {
|
|
100
|
+
if (exclusiveActive) {
|
|
101
|
+
durableBuffer.push(write)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
eraseProgress()
|
|
105
|
+
write()
|
|
106
|
+
}
|
|
107
|
+
const flushDurable = (): void => {
|
|
108
|
+
let firstError: unknown
|
|
109
|
+
while (durableBuffer.length > 0) {
|
|
110
|
+
for (const write of durableBuffer.splice(0)) {
|
|
111
|
+
try {
|
|
112
|
+
write()
|
|
113
|
+
} catch (error) {
|
|
114
|
+
firstError ??= error
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (firstError !== undefined) throw firstError
|
|
119
|
+
}
|
|
72
120
|
const ok = (msg: string): void => {
|
|
73
|
-
|
|
74
|
-
errWrite(`\x1b[1;32m[ok]\x1b[0m ${msg}\n`)
|
|
121
|
+
writeDurable(() => errWrite(`\x1b[1;32m[ok]\x1b[0m ${msg}\n`))
|
|
75
122
|
}
|
|
123
|
+
|
|
124
|
+
const acquireTerminal = (message: string): TerminalLease => {
|
|
125
|
+
if (activeLease !== undefined) throw new Error("terminal lease already acquired")
|
|
126
|
+
|
|
127
|
+
let released = false
|
|
128
|
+
let coordinatorMessage = message
|
|
129
|
+
let exclusiveTail = Promise.resolve()
|
|
130
|
+
let exclusivePending = 0
|
|
131
|
+
const lease: TerminalLease = {
|
|
132
|
+
update: (nextMessage) => {
|
|
133
|
+
if (released) return
|
|
134
|
+
coordinatorMessage = nextMessage
|
|
135
|
+
if (exclusivePending === 0) drawProgress(coordinatorMessage)
|
|
136
|
+
},
|
|
137
|
+
withExclusive: async <T>(action: () => T | Promise<T>): Promise<T> => {
|
|
138
|
+
if (exclusiveContext.getStore()?.active === true) {
|
|
139
|
+
throw new Error("terminal lease withExclusive cannot be re-entered")
|
|
140
|
+
}
|
|
141
|
+
if (released) return await action()
|
|
142
|
+
exclusivePending += 1
|
|
143
|
+
const previous = exclusiveTail
|
|
144
|
+
let unlock!: () => void
|
|
145
|
+
exclusiveTail = new Promise<void>((resolve) => {
|
|
146
|
+
unlock = resolve
|
|
147
|
+
})
|
|
148
|
+
await previous
|
|
149
|
+
eraseProgress()
|
|
150
|
+
exclusiveActive = true
|
|
151
|
+
const owner = { active: true }
|
|
152
|
+
try {
|
|
153
|
+
return await exclusiveContext.run(owner, action)
|
|
154
|
+
} finally {
|
|
155
|
+
owner.active = false
|
|
156
|
+
try {
|
|
157
|
+
flushDurable()
|
|
158
|
+
} finally {
|
|
159
|
+
exclusiveActive = false
|
|
160
|
+
exclusivePending -= 1
|
|
161
|
+
unlock()
|
|
162
|
+
if (!released && exclusivePending === 0) drawProgress(coordinatorMessage)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
release: () => {
|
|
167
|
+
if (released) return
|
|
168
|
+
released = true
|
|
169
|
+
if (activeLease === lease) activeLease = undefined
|
|
170
|
+
eraseProgress()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
activeLease = lease
|
|
174
|
+
drawProgress(coordinatorMessage)
|
|
175
|
+
return lease
|
|
176
|
+
}
|
|
177
|
+
|
|
76
178
|
return {
|
|
77
179
|
change: ok,
|
|
78
180
|
verbose: ok,
|
|
79
181
|
progress: (msg) => {
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
progressPending = true
|
|
182
|
+
if (activeLease !== undefined) return
|
|
183
|
+
drawProgress(msg)
|
|
83
184
|
},
|
|
84
185
|
clearProgress,
|
|
85
186
|
warn: (msg) => {
|
|
86
|
-
|
|
87
|
-
errWrite(`\x1b[1;33m[warn]\x1b[0m ${msg}\n`)
|
|
187
|
+
writeDurable(() => errWrite(`\x1b[1;33m[warn]\x1b[0m ${msg}\n`))
|
|
88
188
|
},
|
|
89
189
|
err: (msg) => {
|
|
90
|
-
|
|
190
|
+
eraseProgress()
|
|
91
191
|
errWrite(`\x1b[1;31m[err]\x1b[0m ${msg}\n`)
|
|
92
192
|
},
|
|
93
193
|
echo: (line) => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
194
|
+
writeDurable(() => outWrite(`${line}\n`))
|
|
195
|
+
},
|
|
196
|
+
acquireTerminal
|
|
97
197
|
}
|
|
98
198
|
}
|
|
@@ -108,7 +108,7 @@ function tomlModelText(text: string): string {
|
|
|
108
108
|
return ""
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
export function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
111
|
+
export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
|
|
112
112
|
const { err } = ctx.services.logger
|
|
113
113
|
const words = args.filter((a) => !a.startsWith("--"))
|
|
114
114
|
const op = words[0] ?? args[0] ?? "check"
|
|
@@ -121,7 +121,7 @@ export function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
if (op === "check") {
|
|
124
|
-
report(ctx)
|
|
124
|
+
await report(ctx)
|
|
125
125
|
return 0
|
|
126
126
|
}
|
|
127
127
|
if (op !== "ensure") {
|
|
@@ -134,9 +134,9 @@ export function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
134
134
|
}
|
|
135
135
|
switch (tool) {
|
|
136
136
|
case "bun":
|
|
137
|
-
return bunBootstrap(ctx, ctx.services).kind === "ready" ? 0 : 1
|
|
137
|
+
return (await bunBootstrap(ctx, ctx.services)).kind === "ready" ? 0 : 1
|
|
138
138
|
case "effect-solutions":
|
|
139
|
-
return ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
|
|
139
|
+
return await ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
|
|
140
140
|
default:
|
|
141
141
|
err("toolchain ensure supports managed tools only (bun, effect-solutions)")
|
|
142
142
|
return 2
|