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
|
@@ -1,35 +1,198 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks"
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Leveled stderr logger
|
|
3
|
-
* DESIGN.md. Filtering is explicit and
|
|
4
|
-
* so fiber-scoped Effect log levels
|
|
5
|
-
*
|
|
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
|
|
6
|
+
* synchronous: engine code is imperative, so fiber-scoped Effect log levels
|
|
7
|
+
* cannot see these writes. Transient progress is active only through an
|
|
8
|
+
* injected progress sink or an interactive stderr. The prefixes and ANSI codes
|
|
9
|
+
* are stable golden surface; the level controls visibility only.
|
|
6
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
|
+
|
|
7
20
|
|
|
8
21
|
export interface Logger {
|
|
9
22
|
/** `[ok]` green — an operation actually mutated something. Always visible. */
|
|
10
23
|
readonly change: (msg: string) => void
|
|
24
|
+
/** Dim, transient single-line status for blocking work. */
|
|
25
|
+
readonly progress: (msg: string) => void
|
|
26
|
+
/** Erase a pending transient status line. */
|
|
27
|
+
readonly clearProgress: () => void
|
|
11
28
|
/** `[ok]` green — status-quo confirmation; visible only with verbosity on. */
|
|
12
29
|
readonly verbose: (msg: string) => void
|
|
13
30
|
readonly warn: (msg: string) => void
|
|
14
31
|
readonly err: (msg: string) => void
|
|
15
32
|
/** stdout data line (dry-run report, summary, usage) — never filtered. */
|
|
16
33
|
readonly echo: (line: string) => void
|
|
34
|
+
/** Acquire the run-scoped coordinator lease for terminal progress and input. */
|
|
35
|
+
readonly acquireTerminal: (message: string) => TerminalLease
|
|
17
36
|
}
|
|
18
37
|
|
|
19
38
|
export interface LoggerSinks {
|
|
20
39
|
readonly stderr?: (chunk: string) => void
|
|
40
|
+
readonly progress?: (chunk: string) => void
|
|
21
41
|
readonly stdout?: (chunk: string) => void
|
|
22
42
|
}
|
|
23
43
|
|
|
44
|
+
interface WritableStreamLike {
|
|
45
|
+
write: (chunk: string) => unknown
|
|
46
|
+
on?: (event: "error", listener: (error: unknown) => void) => unknown
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const epipeGuarded = new WeakSet<WritableStreamLike>()
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A downstream reader may close the pipe early (`docks-kit toolchain check |
|
|
53
|
+
* head`). That is a normal end of consumption, not a CLI failure, so both the
|
|
54
|
+
* synchronous throw and the asynchronous error event are ignored for EPIPE.
|
|
55
|
+
*/
|
|
56
|
+
export function writeIgnoringEpipe(stream: WritableStreamLike, chunk: string): void {
|
|
57
|
+
if (!epipeGuarded.has(stream)) {
|
|
58
|
+
epipeGuarded.add(stream)
|
|
59
|
+
stream.on?.("error", (error) => {
|
|
60
|
+
if ((error as NodeJS.ErrnoException | null)?.code !== "EPIPE") throw error
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
stream.write(chunk)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if ((error as NodeJS.ErrnoException | null)?.code !== "EPIPE") throw error
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
24
70
|
export function makeLogger(sinks: LoggerSinks): Logger {
|
|
25
|
-
const errWrite = sinks.stderr ?? ((chunk: string) =>
|
|
26
|
-
const outWrite = sinks.stdout ?? ((chunk: string) =>
|
|
27
|
-
const
|
|
71
|
+
const errWrite = sinks.stderr ?? ((chunk: string) => writeIgnoringEpipe(process.stderr, chunk))
|
|
72
|
+
const outWrite = sinks.stdout ?? ((chunk: string) => writeIgnoringEpipe(process.stdout, chunk))
|
|
73
|
+
const progressWrite =
|
|
74
|
+
sinks.progress ??
|
|
75
|
+
(sinks.stderr === undefined && process.stderr.isTTY === true
|
|
76
|
+
? (chunk: string) => writeIgnoringEpipe(process.stderr, chunk)
|
|
77
|
+
: undefined)
|
|
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> = []
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
const eraseProgress = (): void => {
|
|
86
|
+
if (!progressPending || progressWrite === undefined) return
|
|
87
|
+
progressWrite("\r\x1b[2K")
|
|
88
|
+
progressPending = false
|
|
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
|
+
}
|
|
120
|
+
const ok = (msg: string): void => {
|
|
121
|
+
writeDurable(() => errWrite(`\x1b[1;32m[ok]\x1b[0m ${msg}\n`))
|
|
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
|
+
|
|
28
178
|
return {
|
|
29
179
|
change: ok,
|
|
30
180
|
verbose: ok,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
181
|
+
progress: (msg) => {
|
|
182
|
+
if (activeLease !== undefined) return
|
|
183
|
+
drawProgress(msg)
|
|
184
|
+
},
|
|
185
|
+
clearProgress,
|
|
186
|
+
warn: (msg) => {
|
|
187
|
+
writeDurable(() => errWrite(`\x1b[1;33m[warn]\x1b[0m ${msg}\n`))
|
|
188
|
+
},
|
|
189
|
+
err: (msg) => {
|
|
190
|
+
eraseProgress()
|
|
191
|
+
errWrite(`\x1b[1;31m[err]\x1b[0m ${msg}\n`)
|
|
192
|
+
},
|
|
193
|
+
echo: (line) => {
|
|
194
|
+
writeDurable(() => outWrite(`${line}\n`))
|
|
195
|
+
},
|
|
196
|
+
acquireTerminal
|
|
34
197
|
}
|
|
35
198
|
}
|
|
@@ -11,9 +11,8 @@ import { syncCodexModel } from "./codexToml"
|
|
|
11
11
|
import type { Ctx } from "./index"
|
|
12
12
|
import { isObject, parseJson, type Json } from "./jq"
|
|
13
13
|
import { printModels, validateClaudeModel, validateCodexModel } from "./models"
|
|
14
|
-
import { ensureRtk } from "./claudeSync"
|
|
15
14
|
import { bunBootstrap } from "./bun"
|
|
16
|
-
import {
|
|
15
|
+
import { effectSolutionsInstall } from "./skillsSync"
|
|
17
16
|
import { ensure, report } from "./toolchain"
|
|
18
17
|
|
|
19
18
|
export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
@@ -109,7 +108,7 @@ function tomlModelText(text: string): string {
|
|
|
109
108
|
return ""
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
export function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
111
|
+
export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
|
|
113
112
|
const { err } = ctx.services.logger
|
|
114
113
|
const words = args.filter((a) => !a.startsWith("--"))
|
|
115
114
|
const op = words[0] ?? args[0] ?? "check"
|
|
@@ -122,7 +121,7 @@ export function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
122
121
|
}
|
|
123
122
|
|
|
124
123
|
if (op === "check") {
|
|
125
|
-
report(ctx)
|
|
124
|
+
await report(ctx)
|
|
126
125
|
return 0
|
|
127
126
|
}
|
|
128
127
|
if (op !== "ensure") {
|
|
@@ -134,16 +133,12 @@ export function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
134
133
|
return 2
|
|
135
134
|
}
|
|
136
135
|
switch (tool) {
|
|
137
|
-
case "rtk":
|
|
138
|
-
return ensureRtk(ctx, "cannot download RTK installer; toolchain ensure rtk aborted", 1)
|
|
139
136
|
case "bun":
|
|
140
|
-
return bunBootstrap(ctx, ctx.services).kind === "ready" ? 0 : 1
|
|
137
|
+
return (await bunBootstrap(ctx, ctx.services)).kind === "ready" ? 0 : 1
|
|
141
138
|
case "effect-solutions":
|
|
142
|
-
return ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
|
|
143
|
-
case "agent-browser":
|
|
144
|
-
return ensure(ctx, "agent-browser", agentBrowserInstall)
|
|
139
|
+
return await ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
|
|
145
140
|
default:
|
|
146
|
-
err("toolchain ensure supports managed tools only (
|
|
141
|
+
err("toolchain ensure supports managed tools only (bun, effect-solutions)")
|
|
147
142
|
return 2
|
|
148
143
|
}
|
|
149
144
|
}
|
|
@@ -50,7 +50,7 @@ function usage(ctx: Ctx): void {
|
|
|
50
50
|
echo(
|
|
51
51
|
" --prune uninstall kit-managed installs not in SoT (plugins, marketplaces, skills in SoT/.agents/skills.txt)"
|
|
52
52
|
)
|
|
53
|
-
echo(" --skip-
|
|
53
|
+
echo(" --skip-bubblewrap skip optional bubblewrap bootstrap (Codex Linux sandbox)")
|
|
54
54
|
echo(" --skip-plugin-refresh install missing plugins but skip refresh-only updates")
|
|
55
55
|
echo(" --yes auto-accept toolchain prompts (containers/CI)")
|
|
56
56
|
echo(" --verbose also print no-op confirmations (already in sync, up to date, left as-is)")
|
|
@@ -147,8 +147,8 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
147
147
|
case "--dry-run":
|
|
148
148
|
ctx.dryRun = true
|
|
149
149
|
continue
|
|
150
|
-
case "--skip-
|
|
151
|
-
ctx.
|
|
150
|
+
case "--skip-bubblewrap":
|
|
151
|
+
ctx.skipBubblewrap = true
|
|
152
152
|
continue
|
|
153
153
|
case "--skip-plugin-refresh":
|
|
154
154
|
ctx.skipPluginRefresh = true
|
|
@@ -217,8 +217,8 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
217
217
|
case "--n8n":
|
|
218
218
|
err("--n8n was renamed to --claude-plugin=n8n")
|
|
219
219
|
throw new ExitError(2)
|
|
220
|
-
case "--
|
|
221
|
-
err("--
|
|
220
|
+
case "--skip-rtk":
|
|
221
|
+
err("--skip-rtk was renamed to --skip-bubblewrap")
|
|
222
222
|
throw new ExitError(2)
|
|
223
223
|
case "-h":
|
|
224
224
|
case "--help":
|
|
@@ -26,10 +26,10 @@ export type { Logger } from "./logger"
|
|
|
26
26
|
export interface DependencyManager {
|
|
27
27
|
readonly spec: (id: ToolId) => DependencySpec
|
|
28
28
|
readonly probe: (id: ToolId) => ProbeResult
|
|
29
|
-
readonly version: (id: ToolId) => string
|
|
30
|
-
readonly path: (id: ToolId) => string
|
|
31
|
-
readonly location: (id: ToolId) => DependencyLocation
|
|
32
|
-
readonly latest: (id: ToolId) => string
|
|
29
|
+
readonly version: (id: ToolId) => Promise<string>
|
|
30
|
+
readonly path: (id: ToolId) => Promise<string>
|
|
31
|
+
readonly location: (id: ToolId) => Promise<DependencyLocation>
|
|
32
|
+
readonly latest: (id: ToolId) => Promise<string>
|
|
33
33
|
readonly warnMissing: (id: ToolId, logger: Logger, context?: string) => void
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -73,7 +73,7 @@ export const makeDependencyManager = (
|
|
|
73
73
|
version: (id) => resolveVersion(DEPENDENCIES[id], exec),
|
|
74
74
|
path: (id) => resolvePath(DEPENDENCIES[id], exec, platform.raw()),
|
|
75
75
|
location: (id) => resolveLocation(DEPENDENCIES[id], exec, platform.raw()),
|
|
76
|
-
latest: (id) => DEPENDENCIES[id].latest?.(exec) ?? "",
|
|
76
|
+
latest: (id) => DEPENDENCIES[id].latest?.(exec) ?? Promise.resolve(""),
|
|
77
77
|
warnMissing: (id, logger, context) => {
|
|
78
78
|
if (warned.has(id)) return
|
|
79
79
|
warned.add(id)
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EngineNative `sync agents` pipeline: universal-skill bootstrap
|
|
3
3
|
* (`npx skills@<pin> add`), Claude symlink healing, --prune reconcile against
|
|
4
|
-
* the kit-managed snapshot,
|
|
5
|
-
*
|
|
4
|
+
* the kit-managed snapshot, the effect-solutions toolchain callback, and the
|
|
5
|
+
* snapshot write.
|
|
6
6
|
*/
|
|
7
|
-
import { spawnSync } from "node:child_process"
|
|
8
7
|
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync } from "node:fs"
|
|
9
|
-
import { p, writeFileIfChanged } from "./exec"
|
|
8
|
+
import { p, spawnProcess, writeFileIfChanged } from "./exec"
|
|
10
9
|
import { bunBootstrap } from "./bun"
|
|
11
10
|
import type { Ctx } from "./index"
|
|
12
11
|
import { compareCodepoints } from "./jq"
|
|
@@ -18,7 +17,7 @@ export interface SkillsState {
|
|
|
18
17
|
present: number
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
export function skillsSync(ctx: Ctx): SkillsState {
|
|
20
|
+
export async function skillsSync(ctx: Ctx): Promise<SkillsState> {
|
|
22
21
|
const state: SkillsState = { present: 0 }
|
|
23
22
|
const skillsDir = p(ctx.agentsDir, "skills")
|
|
24
23
|
const manifest = payloadText("SoT/.agents/skills.txt")
|
|
@@ -26,10 +25,9 @@ export function skillsSync(ctx: Ctx): SkillsState {
|
|
|
26
25
|
|
|
27
26
|
if (!ctx.dryRun) mkdirSync(skillsDir, { recursive: true })
|
|
28
27
|
|
|
29
|
-
syncUniversal(ctx, state, skillsDir, manifest)
|
|
30
|
-
if (ctx.prune) reconcileRemovals(ctx, manifest, snapshot)
|
|
31
|
-
|
|
32
|
-
syncEffectSolutionsCli(ctx)
|
|
28
|
+
await syncUniversal(ctx, state, skillsDir, manifest)
|
|
29
|
+
if (ctx.prune) await reconcileRemovals(ctx, manifest, snapshot)
|
|
30
|
+
await syncEffectSolutionsCli(ctx)
|
|
33
31
|
updateSnapshot(ctx, manifest, snapshot)
|
|
34
32
|
return state
|
|
35
33
|
}
|
|
@@ -56,8 +54,8 @@ function readSlugs(file: string): Array<string> {
|
|
|
56
54
|
return existsSync(file) ? normalizeManifest(readFileSync(file, "utf8")) : []
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest: string): void {
|
|
60
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
57
|
+
async function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest: string): Promise<void> {
|
|
58
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
61
59
|
if (ctx.services.deps.probe("npx").state === "missing") {
|
|
62
60
|
ctx.services.deps.warnMissing("npx", ctx.services.logger, "skipping universal skills bootstrap")
|
|
63
61
|
return
|
|
@@ -87,10 +85,12 @@ function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest
|
|
|
87
85
|
continue
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
|
|
88
|
+
progress(`Installing universal skill ${slug}...`)
|
|
89
|
+
const res = await spawnProcess("npx", ["--yes", skillsCli(ctx), "add", slug, "-g", "-y", "-a", "claude-code", "codex"], {
|
|
91
90
|
stdio: "ignore"
|
|
92
91
|
})
|
|
93
|
-
|
|
92
|
+
clearProgress()
|
|
93
|
+
if (res.error === undefined && res.exitCode === 0) {
|
|
94
94
|
added++
|
|
95
95
|
} else {
|
|
96
96
|
warn(`Failed to install universal skill: ${slug}`)
|
|
@@ -217,69 +217,29 @@ function linkOrCopyWithWarnings(target: string, link: string, services: EngineSe
|
|
|
217
217
|
|
|
218
218
|
// ------------------------------------------------- toolchain callbacks ----
|
|
219
219
|
|
|
220
|
-
/** skills::_agent_browser_install. */
|
|
221
|
-
export function agentBrowserInstall(mode: "install" | "upgrade", version: string, services: EngineServices): number {
|
|
222
|
-
const { change, verbose, warn } = services.logger
|
|
223
|
-
const verb = mode === "upgrade" ? "Upgrading" : "Installing"
|
|
224
|
-
const pkg = version !== "" ? `agent-browser@${version}` : "agent-browser"
|
|
225
|
-
const installFlags = services.platform.isLinux() ? ["--with-deps"] : []
|
|
226
|
-
|
|
227
|
-
verbose(`${verb} agent-browser CLI via npm${version !== "" ? ` (pinned ${version})` : ""}...`)
|
|
228
|
-
if (spawnSync("npm", ["install", "-g", pkg], { stdio: "ignore" }).status !== 0) {
|
|
229
|
-
warn(`npm install -g ${pkg} failed. Try manually: npm install -g ${pkg}`)
|
|
230
|
-
return 1
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (mode === "install") {
|
|
234
|
-
warn("Downloading Chrome for Testing (~175 MB; sudo may be requested for system libs on Linux)...")
|
|
235
|
-
if (spawnSync("agent-browser", ["install", ...installFlags], { stdio: "inherit" }).status !== 0) {
|
|
236
|
-
warn(`agent-browser install failed. Re-run manually: agent-browser install ${installFlags.join(" ")}`)
|
|
237
|
-
return 1
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const out = services.deps.version("agent-browser")
|
|
241
|
-
const fields = (out.split("\n")[0] ?? "").trim().split(/[ \t]+/)
|
|
242
|
-
const version2 = out !== "" ? fields[fields.length - 1] ?? "version unknown" : "version unknown"
|
|
243
|
-
change(`agent-browser CLI ready (${version2})`)
|
|
244
|
-
return 0
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function syncAgentBrowserCli(ctx: Ctx, manifest: string): void {
|
|
248
|
-
const { warn } = ctx.services.logger
|
|
249
|
-
if (!manifest.split("\n").includes("vercel-labs/agent-browser")) return
|
|
250
|
-
|
|
251
|
-
if (ctx.services.deps.probe("npm").state === "missing") {
|
|
252
|
-
if (!ctx.dryRun) {
|
|
253
|
-
ctx.services.deps.warnMissing("npm", ctx.services.logger, "cannot auto-install agent-browser CLI; re-run sync after installing")
|
|
254
|
-
}
|
|
255
|
-
return
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (ensure(ctx, "agent-browser", agentBrowserInstall) !== 0) {
|
|
259
|
-
warn("agent-browser bootstrap failed — continuing sync")
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
220
|
/** skills::_effect_solutions_install. */
|
|
264
221
|
export function effectSolutionsInstall(
|
|
265
222
|
ctx: Ctx
|
|
266
|
-
): (mode: "install" | "upgrade", version: string, services: EngineServices) => number {
|
|
267
|
-
return (mode, version, services) => {
|
|
268
|
-
const { change, verbose, warn } = services.logger
|
|
223
|
+
): (mode: "install" | "upgrade", version: string, services: EngineServices) => Promise<number> {
|
|
224
|
+
return async (mode, version, services) => {
|
|
225
|
+
const { change, clearProgress, progress, verbose, warn } = services.logger
|
|
269
226
|
const verb = mode === "upgrade" ? "Upgrading" : "Installing"
|
|
270
227
|
const pkg = `effect-solutions@${version !== "" ? version : "latest"}`
|
|
271
228
|
|
|
272
|
-
const bunState = bunBootstrap(ctx, services)
|
|
229
|
+
const bunState = await bunBootstrap(ctx, services)
|
|
273
230
|
if (bunState.kind === "deferred") return 1
|
|
274
231
|
const bun = bunState.executable
|
|
275
232
|
|
|
276
233
|
verbose(`${verb} effect-solutions CLI via bun${version !== "" ? ` (pinned ${version})` : ""}...`)
|
|
277
|
-
|
|
234
|
+
progress(`${verb} effect-solutions CLI...`)
|
|
235
|
+
const installResult = await spawnProcess(bun, ["add", "-g", pkg], { stdio: "ignore" })
|
|
236
|
+
clearProgress()
|
|
237
|
+
if (installResult.exitCode !== 0) {
|
|
278
238
|
warn(`bun add -g ${pkg} failed. Try manually: bun add -g ${pkg}`)
|
|
279
239
|
return 1
|
|
280
240
|
}
|
|
281
241
|
|
|
282
|
-
const location = services.deps.location("effect-solutions")
|
|
242
|
+
const location = await services.deps.location("effect-solutions")
|
|
283
243
|
const gbin = location.binDir
|
|
284
244
|
if (location.path !== "") {
|
|
285
245
|
mkdirSync(p(ctx.home, ".local", "bin"), { recursive: true })
|
|
@@ -293,19 +253,22 @@ export function effectSolutionsInstall(
|
|
|
293
253
|
}
|
|
294
254
|
}
|
|
295
255
|
|
|
296
|
-
function syncEffectSolutionsCli(ctx: Ctx): void {
|
|
297
|
-
const { warn } = ctx.services.logger
|
|
256
|
+
async function syncEffectSolutionsCli(ctx: Ctx): Promise<void> {
|
|
257
|
+
const { clearProgress, progress, warn } = ctx.services.logger
|
|
298
258
|
if (!/"effect-kit@docks"[ \t]*:[ \t]*true/.test(payloadText("SoT/.claude/settings.json"))) return
|
|
299
259
|
|
|
300
|
-
|
|
260
|
+
progress("Checking effect-solutions CLI...")
|
|
261
|
+
const result = await ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
|
|
262
|
+
clearProgress()
|
|
263
|
+
if (result !== 0) {
|
|
301
264
|
warn("effect-solutions bootstrap failed — continuing sync")
|
|
302
265
|
}
|
|
303
266
|
}
|
|
304
267
|
|
|
305
268
|
// ----------------------------------------------------- prune + snapshot ----
|
|
306
269
|
|
|
307
|
-
function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
308
|
-
const { change, echo, warn } = ctx.services.logger
|
|
270
|
+
async function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): Promise<void> {
|
|
271
|
+
const { change, clearProgress, echo, progress, warn } = ctx.services.logger
|
|
309
272
|
if (!existsSync(snapshot)) {
|
|
310
273
|
if (ctx.dryRun) {
|
|
311
274
|
echo(
|
|
@@ -325,10 +288,12 @@ function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
|
325
288
|
echo(`[dry-run] kit-managed skill no longer in SoT — would remove: ${base}`)
|
|
326
289
|
continue
|
|
327
290
|
}
|
|
328
|
-
|
|
291
|
+
progress(`Removing universal skill ${base}...`)
|
|
292
|
+
const res = await spawnProcess("npx", ["--yes", skillsCli(ctx), "remove", "--global", base, "-y"], {
|
|
329
293
|
stdio: "ignore"
|
|
330
294
|
})
|
|
331
|
-
|
|
295
|
+
clearProgress()
|
|
296
|
+
if (res.error === undefined && res.exitCode === 0) {
|
|
332
297
|
removed++
|
|
333
298
|
} else {
|
|
334
299
|
warn(`Failed to remove kit-managed skill: ${base}`)
|
|
@@ -10,7 +10,11 @@ import { compareCodepoints, isObject, parseJson, type Json } from "./jq"
|
|
|
10
10
|
import type { EngineServices } from "./services"
|
|
11
11
|
import { payloadText } from "../payload"
|
|
12
12
|
|
|
13
|
-
type InstallFn = (
|
|
13
|
+
type InstallFn = (
|
|
14
|
+
mode: "install" | "upgrade",
|
|
15
|
+
version: string,
|
|
16
|
+
services: EngineServices
|
|
17
|
+
) => number | Promise<number>
|
|
14
18
|
|
|
15
19
|
function manifest(): { [k: string]: Json } {
|
|
16
20
|
const doc = parseJson(payloadText("SoT/toolchain.json"))
|
|
@@ -47,36 +51,40 @@ function firstLineField(out: string, index: number): string {
|
|
|
47
51
|
return fields[index === -1 ? fields.length - 1 : index] ?? ""
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
export function installedVersion(ctx: Ctx, tool: ToolId): string {
|
|
51
|
-
const version = ()
|
|
54
|
+
export async function installedVersion(ctx: Ctx, tool: ToolId): Promise<string> {
|
|
55
|
+
const version = () => ctx.services.deps.version(tool)
|
|
52
56
|
switch (tool) {
|
|
53
|
-
case "rtk":
|
|
54
|
-
return firstLineField(version(), 1)
|
|
55
57
|
case "claude":
|
|
56
|
-
return firstLineField(version(), 0)
|
|
58
|
+
return firstLineField(await version(), 0)
|
|
57
59
|
case "codex":
|
|
58
|
-
|
|
59
|
-
return firstLineField(version(), -1)
|
|
60
|
+
return firstLineField(await version(), -1)
|
|
60
61
|
case "git":
|
|
61
|
-
return firstLineField(version(), 2)
|
|
62
|
+
return firstLineField(await version(), 2)
|
|
62
63
|
case "node":
|
|
63
|
-
return version().replace(/^v/, "")
|
|
64
|
+
return (await version()).replace(/^v/, "")
|
|
64
65
|
case "jq":
|
|
65
|
-
return version().replace(/^jq-/, "")
|
|
66
|
+
return (await version()).replace(/^jq-/, "")
|
|
66
67
|
case "curl":
|
|
67
68
|
case "tsc":
|
|
68
|
-
return firstLineField(version(), 1)
|
|
69
|
+
return firstLineField(await version(), 1)
|
|
69
70
|
case "bun":
|
|
70
71
|
case "effect-solutions":
|
|
71
72
|
case "npm":
|
|
72
|
-
return version()
|
|
73
|
+
return await version()
|
|
74
|
+
case "bwrap":
|
|
75
|
+
return firstLineField(await version(), 1)
|
|
76
|
+
case "ffplay":
|
|
77
|
+
return firstLineField(await version(), 2).replace(/-.*$/, "")
|
|
78
|
+
case "intelephense":
|
|
79
|
+
case "typescript-language-server":
|
|
80
|
+
return (await version()).trim()
|
|
73
81
|
default:
|
|
74
82
|
return ""
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
|
|
78
|
-
export function latestVersion(ctx: Ctx, tool: ToolId): string {
|
|
79
|
-
return ctx.services.deps.latest(tool)
|
|
86
|
+
export async function latestVersion(ctx: Ctx, tool: ToolId): Promise<string> {
|
|
87
|
+
return await ctx.services.deps.latest(tool)
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
/** Blocking TTY prompt matching bash `read -r -p` (prompt on stderr). */
|
|
@@ -104,7 +112,12 @@ export function promptLine(
|
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
/** toolchain::_gate — { proceed, target } ("" target = latest). */
|
|
107
|
-
function gate(
|
|
115
|
+
async function gate(
|
|
116
|
+
ctx: Ctx,
|
|
117
|
+
tool: string,
|
|
118
|
+
mode: "install" | "upgrade",
|
|
119
|
+
latest: string
|
|
120
|
+
): Promise<{ proceed: boolean; target: string }> {
|
|
108
121
|
const { warn } = ctx.services.logger
|
|
109
122
|
const verified = field(ctx, tool, "verified")
|
|
110
123
|
const pinnable = field(ctx, tool, "pinnable")
|
|
@@ -118,7 +131,11 @@ function gate(ctx: Ctx, tool: string, mode: "install" | "upgrade", latest: strin
|
|
|
118
131
|
|
|
119
132
|
if (process.stdin.isTTY === true) {
|
|
120
133
|
ctx.services.logger.warn(`${tool} ${latest} is not kit-verified (verified: ${verified}).`)
|
|
121
|
-
const
|
|
134
|
+
const question = `Install ${tool} ${latest} anyway? [y/N] `
|
|
135
|
+
const answer =
|
|
136
|
+
ctx.terminalLease === undefined
|
|
137
|
+
? promptLine(question)
|
|
138
|
+
: await ctx.terminalLease.withExclusive(() => promptLine(question))
|
|
122
139
|
if (/^[yY]/.test(answer)) return { proceed: true, target: "" }
|
|
123
140
|
}
|
|
124
141
|
|
|
@@ -132,12 +149,12 @@ function gate(ctx: Ctx, tool: string, mode: "install" | "upgrade", latest: strin
|
|
|
132
149
|
return { proceed: false, target: "" }
|
|
133
150
|
}
|
|
134
151
|
|
|
135
|
-
export function ensure(ctx: Ctx, tool: ToolId, installFn: InstallFn): number {
|
|
152
|
+
export async function ensure(ctx: Ctx, tool: ToolId, installFn: InstallFn): Promise<number> {
|
|
136
153
|
const { echo, verbose, warn } = ctx.services.logger
|
|
137
154
|
const policy = field(ctx, tool, "policy")
|
|
138
155
|
|
|
139
156
|
if (!present(ctx, tool)) {
|
|
140
|
-
const latest = latestVersion(ctx, tool)
|
|
157
|
+
const latest = await latestVersion(ctx, tool)
|
|
141
158
|
if (ctx.dryRun) {
|
|
142
159
|
echo(`[dry-run] would install ${tool} (${latest !== "" ? latest : "latest"}, gated by toolchain.json verified pin)`)
|
|
143
160
|
return 0
|
|
@@ -152,14 +169,14 @@ export function ensure(ctx: Ctx, tool: ToolId, installFn: InstallFn): number {
|
|
|
152
169
|
warn(`${tool} latest version unknown (offline?) and not pinnable — installing latest unverified`)
|
|
153
170
|
}
|
|
154
171
|
} else {
|
|
155
|
-
const g = gate(ctx, tool, "install", latest)
|
|
172
|
+
const g = await gate(ctx, tool, "install", latest)
|
|
156
173
|
if (!g.proceed) return 0
|
|
157
174
|
target = g.target
|
|
158
175
|
}
|
|
159
|
-
return installFn("install", target !== "" ? target : latest, ctx.services)
|
|
176
|
+
return await installFn("install", target !== "" ? target : latest, ctx.services)
|
|
160
177
|
}
|
|
161
178
|
|
|
162
|
-
const installed = installedVersion(ctx, tool)
|
|
179
|
+
const installed = await installedVersion(ctx, tool)
|
|
163
180
|
const installedLabel = installed !== "" ? installed : "version unknown"
|
|
164
181
|
|
|
165
182
|
if (policy !== "track") {
|
|
@@ -171,7 +188,7 @@ export function ensure(ctx: Ctx, tool: ToolId, installFn: InstallFn): number {
|
|
|
171
188
|
return 0
|
|
172
189
|
}
|
|
173
190
|
|
|
174
|
-
const latest = latestVersion(ctx, tool)
|
|
191
|
+
const latest = await latestVersion(ctx, tool)
|
|
175
192
|
if (latest === "") {
|
|
176
193
|
if (ctx.dryRun) {
|
|
177
194
|
echo(`[dry-run] ${tool} present (${installedLabel}); latest unknown (offline?) — no action`)
|
|
@@ -186,9 +203,9 @@ export function ensure(ctx: Ctx, tool: ToolId, installFn: InstallFn): number {
|
|
|
186
203
|
echo(`[dry-run] would upgrade ${tool} (${installed !== "" ? installed : "unknown"} -> ${latest}, gated by toolchain.json verified pin)`)
|
|
187
204
|
return 0
|
|
188
205
|
}
|
|
189
|
-
const g = gate(ctx, tool, "upgrade", latest)
|
|
206
|
+
const g = await gate(ctx, tool, "upgrade", latest)
|
|
190
207
|
if (!g.proceed) return 0
|
|
191
|
-
return installFn("upgrade", g.target !== "" ? g.target : latest, ctx.services)
|
|
208
|
+
return await installFn("upgrade", g.target !== "" ? g.target : latest, ctx.services)
|
|
192
209
|
}
|
|
193
210
|
|
|
194
211
|
if (ctx.dryRun) {
|
|
@@ -204,7 +221,7 @@ function row(cells: [string, string, string, string, string, string]): string {
|
|
|
204
221
|
return cells.map((c, i) => (i < widths.length ? c.padEnd(widths[i]!) : c)).join(" ")
|
|
205
222
|
}
|
|
206
223
|
|
|
207
|
-
export function report(ctx: Ctx): void {
|
|
224
|
+
export async function report(ctx: Ctx): Promise<void> {
|
|
208
225
|
const { echo } = ctx.services.logger
|
|
209
226
|
echo(row(["TOOL", "KIND", "INSTALLED", "FLOOR", "VERIFIED", "STATUS"]))
|
|
210
227
|
const pn = ctx.services.platform.name()
|
|
@@ -224,7 +241,7 @@ export function report(ctx: Ctx): void {
|
|
|
224
241
|
let status: string
|
|
225
242
|
const toolId = tool as ToolId
|
|
226
243
|
if (present(ctx, toolId)) {
|
|
227
|
-
installed = installedVersion(ctx, toolId)
|
|
244
|
+
installed = await installedVersion(ctx, toolId)
|
|
228
245
|
status = "ok"
|
|
229
246
|
if (floor !== "" && installed !== "" && isNewer(floor, installed)) {
|
|
230
247
|
status = "below-floor"
|
package/cli/src/engine.ts
CHANGED
|
@@ -37,7 +37,7 @@ export const engine = (args: ReadonlyArray<string>) =>
|
|
|
37
37
|
const logger = yield* LoggerService
|
|
38
38
|
const deps = yield* DependencyManagerService
|
|
39
39
|
const platform = yield* PlatformService
|
|
40
|
-
const code = yield* Effect.
|
|
40
|
+
const code = yield* Effect.promise(() => runEngineNative(args, { logger, deps, platform }))
|
|
41
41
|
if (code !== 0) {
|
|
42
42
|
yield* Effect.sync(() => process.exit(code))
|
|
43
43
|
}
|