docks-kit 0.1.5 → 0.3.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 +1 -1
- package/README.md +7 -5
- package/cli/docs/flags.md +1 -0
- package/cli/docs/install.md +9 -11
- package/cli/docs/overview.md +6 -0
- package/cli/docs/platforms.md +9 -10
- package/cli/src/commands/model.ts +7 -3
- package/cli/src/commands/sync.ts +14 -3
- package/cli/src/commands/toolchain.ts +7 -3
- package/cli/src/engine-native/DESIGN.md +79 -2
- package/cli/src/engine-native/claudeModel.ts +9 -3
- package/cli/src/engine-native/claudeSync.ts +220 -116
- package/cli/src/engine-native/codexSync.ts +131 -77
- package/cli/src/engine-native/codexToml.ts +12 -5
- package/cli/src/engine-native/deps.ts +325 -0
- package/cli/src/engine-native/exec.ts +35 -2
- package/cli/src/engine-native/index.ts +48 -13
- package/cli/src/engine-native/logger.ts +35 -0
- package/cli/src/engine-native/models.ts +22 -23
- package/cli/src/engine-native/modes.ts +30 -14
- package/cli/src/engine-native/os.ts +29 -0
- package/cli/src/engine-native/parseArgs.ts +19 -17
- package/cli/src/engine-native/services.ts +96 -0
- package/cli/src/engine-native/skillsSync.ts +77 -61
- package/cli/src/engine-native/toolchain.ts +50 -68
- package/cli/src/engine.ts +11 -2
- package/cli/src/generated/sotPayload.ts +41 -0
- package/cli/src/kitHome.ts +15 -11
- package/cli/src/main.ts +3 -2
- package/cli/src/manifests.ts +17 -15
- package/cli/src/payload.ts +28 -0
- package/cli/src/services.ts +34 -0
- package/docks-kit +6 -6
- package/package.json +2 -3
- package/SoT/.agents/skills.txt +0 -14
- package/SoT/.claude/CLAUDE.md +0 -146
- package/SoT/.claude/fetch-usage.sh +0 -66
- package/SoT/.claude/hooks/notify.sh +0 -14
- package/SoT/.claude/mcp-servers.json +0 -10
- package/SoT/.claude/settings.json +0 -235
- package/SoT/.claude/statusline.sh +0 -175
- package/SoT/.codex/AGENTS.md +0 -75
- package/SoT/.codex/agents/.gitkeep +0 -1
- package/SoT/.codex/config.toml +0 -45
- package/SoT/.codex/plugins/marketplace.json +0 -50
- package/SoT/.codex/rules/docks.rules +0 -116
- package/SoT/models.json +0 -28
- package/SoT/toolchain.json +0 -27
- package/cli/src/engine-native/output.ts +0 -20
- package/notification.mp3 +0 -0
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
import { spawnSync } from "node:child_process"
|
|
8
8
|
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, rmdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"
|
|
9
9
|
import { tmpdir } from "node:os"
|
|
10
|
-
import {
|
|
10
|
+
import { p, writeFileIfChanged } from "./exec"
|
|
11
11
|
import type { Ctx } from "./index"
|
|
12
12
|
import { compareCodepoints } from "./jq"
|
|
13
|
-
import {
|
|
13
|
+
import type { EngineServices, Platform } from "./services"
|
|
14
14
|
import { ensure, field } from "./toolchain"
|
|
15
|
+
import { payloadText } from "../payload"
|
|
15
16
|
|
|
16
17
|
export interface SkillsState {
|
|
17
18
|
present: number
|
|
@@ -20,11 +21,9 @@ export interface SkillsState {
|
|
|
20
21
|
export function skillsSync(ctx: Ctx): SkillsState {
|
|
21
22
|
const state: SkillsState = { present: 0 }
|
|
22
23
|
const skillsDir = p(ctx.agentsDir, "skills")
|
|
23
|
-
const manifest =
|
|
24
|
+
const manifest = payloadText("SoT/.agents/skills.txt")
|
|
24
25
|
const snapshot = p(ctx.agentsDir, ".kit-managed-skills")
|
|
25
26
|
|
|
26
|
-
if (!existsSync(manifest)) return state
|
|
27
|
-
|
|
28
27
|
if (!ctx.dryRun) mkdirSync(skillsDir, { recursive: true })
|
|
29
28
|
|
|
30
29
|
syncUniversal(ctx, state, skillsDir, manifest)
|
|
@@ -58,8 +57,9 @@ function readSlugs(file: string): Array<string> {
|
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest: string): void {
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
const { change, echo, verbose, warn } = ctx.services.logger
|
|
61
|
+
if (ctx.services.deps.probe("npx").state === "missing") {
|
|
62
|
+
ctx.services.deps.warnMissing("npx", ctx.services.logger, "skipping universal skills bootstrap")
|
|
63
63
|
return
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -68,7 +68,7 @@ function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest
|
|
|
68
68
|
let failed = 0
|
|
69
69
|
let healed = 0
|
|
70
70
|
|
|
71
|
-
for (const slug of
|
|
71
|
+
for (const slug of normalizeManifest(manifest)) {
|
|
72
72
|
const base = slug.slice(slug.lastIndexOf("/") + 1)
|
|
73
73
|
|
|
74
74
|
if (ctx.dryRun) {
|
|
@@ -103,12 +103,14 @@ function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest
|
|
|
103
103
|
state.present = added + already
|
|
104
104
|
|
|
105
105
|
if (added > 0) {
|
|
106
|
-
|
|
106
|
+
change(`Universal skills synced (+${added} new, ${already} already present)`)
|
|
107
|
+
ctx.nextStepTriggers.skillsRestart = true
|
|
107
108
|
} else {
|
|
108
|
-
|
|
109
|
+
verbose(`Universal skills already in sync (${already} present)`)
|
|
109
110
|
}
|
|
110
111
|
if (healed > 0) {
|
|
111
|
-
|
|
112
|
+
change(`Claude per-tool symlinks healed (+${healed}) — canonical present, ~/.claude/skills/<name> was missing or broken`)
|
|
113
|
+
ctx.nextStepTriggers.skillsRestart = true
|
|
112
114
|
}
|
|
113
115
|
if (failed > 0) {
|
|
114
116
|
warn(`${failed} skill install(s) failed — re-run sync or install manually with: npx skills add <slug> -g -y -a claude-code codex`)
|
|
@@ -125,6 +127,7 @@ function isDir(path: string): boolean {
|
|
|
125
127
|
|
|
126
128
|
/** skills::heal_claude_symlink — true when a heal occurred. */
|
|
127
129
|
function healClaudeSymlink(ctx: Ctx, skillsDir: string, base: string): boolean {
|
|
130
|
+
const { echo, warn } = ctx.services.logger
|
|
128
131
|
const canonical = p(skillsDir, base)
|
|
129
132
|
const claudeSkillsDir = p(ctx.home, ".claude", "skills")
|
|
130
133
|
const claudeLink = p(claudeSkillsDir, base)
|
|
@@ -138,7 +141,7 @@ function healClaudeSymlink(ctx: Ctx, skillsDir: string, base: string): boolean {
|
|
|
138
141
|
if (current === relTarget) return false
|
|
139
142
|
// win32: `npx skills add` creates absolute symlinks/junctions — any link
|
|
140
143
|
// that RESOLVES to the canonical dir is healthy, not stale.
|
|
141
|
-
if (
|
|
144
|
+
if (ctx.services.platform.isWindows() && realpathEquals(claudeLink, canonical)) return false
|
|
142
145
|
if (ctx.dryRun) {
|
|
143
146
|
echo(`[dry-run] would replace stale Claude symlink: ~/.claude/skills/${base} -> ${current} (correct: ${relTarget})`)
|
|
144
147
|
return true
|
|
@@ -156,7 +159,7 @@ function healClaudeSymlink(ctx: Ctx, skillsDir: string, base: string): boolean {
|
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
mkdirSync(claudeSkillsDir, { recursive: true })
|
|
159
|
-
return
|
|
162
|
+
return linkOrCopyWithWarnings(relTarget, claudeLink, ctx.services)
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
function lstat(path: string): ReturnType<typeof lstatSync> | undefined {
|
|
@@ -201,10 +204,10 @@ function removeLink(path: string): boolean {
|
|
|
201
204
|
}
|
|
202
205
|
|
|
203
206
|
/** skills::_link_or_copy — real symlink preferred, copy fallback (Windows). */
|
|
204
|
-
export function linkOrCopy(target: string, link: string): boolean {
|
|
207
|
+
export function linkOrCopy(target: string, link: string, platform: Platform): boolean {
|
|
205
208
|
removeLink(link)
|
|
206
209
|
try {
|
|
207
|
-
symlinkSync(target, link,
|
|
210
|
+
symlinkSync(target, link, platform.isWindows() ? "dir" : undefined)
|
|
208
211
|
} catch {
|
|
209
212
|
// fall through to the copy fallback below
|
|
210
213
|
}
|
|
@@ -216,47 +219,56 @@ export function linkOrCopy(target: string, link: string): boolean {
|
|
|
216
219
|
} catch {
|
|
217
220
|
// fall through to the existence check below
|
|
218
221
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
return existsSync(link)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function linkOrCopyWithWarnings(target: string, link: string, services: EngineServices): boolean {
|
|
226
|
+
const linked = linkOrCopy(target, link, services.platform)
|
|
227
|
+
if (!linked) {
|
|
228
|
+
services.logger.warn(`could not create ${link} (symlink and copy both failed)`)
|
|
229
|
+
} else if (lstat(link)?.isSymbolicLink() !== true) {
|
|
230
|
+
services.logger.warn(`symlinks unsupported here — ${link} is a copy (refreshed on sync; enable Windows Developer Mode for real links)`)
|
|
222
231
|
}
|
|
223
|
-
|
|
224
|
-
return false
|
|
232
|
+
return linked
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
// ------------------------------------------------- toolchain callbacks ----
|
|
228
236
|
|
|
229
237
|
/** skills::_agent_browser_install. */
|
|
230
|
-
export function agentBrowserInstall(mode: "install" | "upgrade", version: string): number {
|
|
238
|
+
export function agentBrowserInstall(mode: "install" | "upgrade", version: string, services: EngineServices): number {
|
|
239
|
+
const { change, verbose, warn } = services.logger
|
|
231
240
|
const verb = mode === "upgrade" ? "Upgrading" : "Installing"
|
|
232
241
|
const pkg = version !== "" ? `agent-browser@${version}` : "agent-browser"
|
|
233
|
-
const installFlags =
|
|
242
|
+
const installFlags = services.platform.isLinux() ? ["--with-deps"] : []
|
|
234
243
|
|
|
235
|
-
|
|
244
|
+
verbose(`${verb} agent-browser CLI via npm${version !== "" ? ` (pinned ${version})` : ""}...`)
|
|
236
245
|
if (spawnSync("npm", ["install", "-g", pkg], { stdio: "ignore" }).status !== 0) {
|
|
237
246
|
warn(`npm install -g ${pkg} failed. Try manually: npm install -g ${pkg}`)
|
|
238
247
|
return 1
|
|
239
248
|
}
|
|
240
249
|
|
|
241
250
|
if (mode === "install") {
|
|
242
|
-
|
|
251
|
+
warn("Downloading Chrome for Testing (~175 MB; sudo may be requested for system libs on Linux)...")
|
|
243
252
|
if (spawnSync("agent-browser", ["install", ...installFlags], { stdio: "inherit" }).status !== 0) {
|
|
244
253
|
warn(`agent-browser install failed. Re-run manually: agent-browser install ${installFlags.join(" ")}`)
|
|
245
254
|
return 1
|
|
246
255
|
}
|
|
247
256
|
}
|
|
248
|
-
const out =
|
|
257
|
+
const out = services.deps.version("agent-browser")
|
|
249
258
|
const fields = (out.split("\n")[0] ?? "").trim().split(/[ \t]+/)
|
|
250
259
|
const version2 = out !== "" ? fields[fields.length - 1] ?? "version unknown" : "version unknown"
|
|
251
|
-
|
|
260
|
+
change(`agent-browser CLI ready (${version2})`)
|
|
252
261
|
return 0
|
|
253
262
|
}
|
|
254
263
|
|
|
255
264
|
function syncAgentBrowserCli(ctx: Ctx, manifest: string): void {
|
|
256
|
-
|
|
265
|
+
const { warn } = ctx.services.logger
|
|
266
|
+
if (!manifest.split("\n").includes("vercel-labs/agent-browser")) return
|
|
257
267
|
|
|
258
|
-
if (
|
|
259
|
-
if (!ctx.dryRun)
|
|
268
|
+
if (ctx.services.deps.probe("npm").state === "missing") {
|
|
269
|
+
if (!ctx.dryRun) {
|
|
270
|
+
ctx.services.deps.warnMissing("npm", ctx.services.logger, "cannot auto-install agent-browser CLI; re-run sync after installing")
|
|
271
|
+
}
|
|
260
272
|
return
|
|
261
273
|
}
|
|
262
274
|
|
|
@@ -267,21 +279,16 @@ function syncAgentBrowserCli(ctx: Ctx, manifest: string): void {
|
|
|
267
279
|
|
|
268
280
|
/** skills::_find_bun — resolved bun path or "". */
|
|
269
281
|
function findBun(ctx: Ctx): string {
|
|
270
|
-
|
|
271
|
-
if (onPath !== "") return onPath
|
|
272
|
-
const bunInstall = process.env["BUN_INSTALL"] !== undefined && process.env["BUN_INSTALL"] !== "" ? process.env["BUN_INSTALL"] : p(ctx.home, ".bun")
|
|
273
|
-
for (const cand of [p(bunInstall, "bin", "bun"), p(ctx.home, ".bun", "bin", "bun")]) {
|
|
274
|
-
if (isExecutable(cand)) return cand
|
|
275
|
-
}
|
|
276
|
-
return ""
|
|
282
|
+
return ctx.services.deps.path("bun")
|
|
277
283
|
}
|
|
278
284
|
|
|
279
285
|
/** skills::_bun_bootstrap — bun path or "" after a failed bootstrap. */
|
|
280
|
-
export function bunBootstrap(ctx: Ctx): string {
|
|
286
|
+
export function bunBootstrap(ctx: Ctx, services: EngineServices): string {
|
|
287
|
+
const { change, warn } = services.logger
|
|
281
288
|
let bun = findBun(ctx)
|
|
282
289
|
if (bun !== "") return bun
|
|
283
290
|
|
|
284
|
-
if (
|
|
291
|
+
if (services.deps.probe("curl").state === "missing") {
|
|
285
292
|
warn("Bun and curl both missing — cannot bootstrap Bun. Install Bun manually, then re-run sync.")
|
|
286
293
|
return ""
|
|
287
294
|
}
|
|
@@ -298,41 +305,44 @@ export function bunBootstrap(ctx: Ctx): string {
|
|
|
298
305
|
warn("Bun install failed. Install manually: curl -fsSL https://bun.sh/install -o /tmp/bun.sh && bash /tmp/bun.sh")
|
|
299
306
|
return ""
|
|
300
307
|
}
|
|
301
|
-
const
|
|
302
|
-
|
|
308
|
+
const version = services.deps.version("bun")
|
|
309
|
+
change(`Bun installed (${version !== "" ? version : "version unknown"})`)
|
|
303
310
|
return bun
|
|
304
311
|
}
|
|
305
312
|
|
|
306
313
|
/** skills::_effect_solutions_install. */
|
|
307
|
-
export function effectSolutionsInstall(
|
|
308
|
-
|
|
314
|
+
export function effectSolutionsInstall(
|
|
315
|
+
ctx: Ctx
|
|
316
|
+
): (mode: "install" | "upgrade", version: string, services: EngineServices) => number {
|
|
317
|
+
return (mode, version, services) => {
|
|
318
|
+
const { change, verbose, warn } = services.logger
|
|
309
319
|
const verb = mode === "upgrade" ? "Upgrading" : "Installing"
|
|
310
320
|
const pkg = `effect-solutions@${version !== "" ? version : "latest"}`
|
|
311
321
|
|
|
312
|
-
const bun = bunBootstrap(ctx)
|
|
322
|
+
const bun = bunBootstrap(ctx, services)
|
|
313
323
|
if (bun === "") return 1
|
|
314
324
|
|
|
315
|
-
|
|
325
|
+
verbose(`${verb} effect-solutions CLI via bun${version !== "" ? ` (pinned ${version})` : ""}...`)
|
|
316
326
|
if (spawnSync(bun, ["add", "-g", pkg], { stdio: "ignore" }).status !== 0) {
|
|
317
327
|
warn(`bun add -g ${pkg} failed. Try manually: bun add -g ${pkg}`)
|
|
318
328
|
return 1
|
|
319
329
|
}
|
|
320
330
|
|
|
321
|
-
const
|
|
331
|
+
const location = services.deps.location("effect-solutions")
|
|
332
|
+
const gbin = location.binDir
|
|
322
333
|
// win32: bun writes an .exe shim (not the bare Unix name), and the
|
|
323
334
|
// ~/.local/bin link step below is Unix-only plumbing (non-interactive
|
|
324
335
|
// agent PATH) — bun's global bin is already the Windows PATH entry.
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
if (found) log(`effect-solutions CLI ready (${gbin})`)
|
|
336
|
+
if (services.platform.isWindows()) {
|
|
337
|
+
if (location.path !== "") change(`effect-solutions CLI ready (${gbin})`)
|
|
328
338
|
else warn(`effect-solutions installed but no shim found under '${gbin !== "" ? gbin : "<unknown>"}' — check bun pm -g bin`)
|
|
329
339
|
return 0
|
|
330
340
|
}
|
|
331
|
-
if (
|
|
341
|
+
if (location.path !== "") {
|
|
332
342
|
mkdirSync(p(ctx.home, ".local", "bin"), { recursive: true })
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
343
|
+
linkOrCopyWithWarnings(bun, p(ctx.home, ".local", "bin", "bun"), services)
|
|
344
|
+
linkOrCopyWithWarnings(location.path, p(ctx.home, ".local", "bin", "effect-solutions"), services)
|
|
345
|
+
change("effect-solutions CLI ready (linked bun + effect-solutions into ~/.local/bin)")
|
|
336
346
|
} else {
|
|
337
347
|
warn(`effect-solutions installed but binary not found under '${gbin !== "" ? gbin : "<unknown>"}' — link it onto PATH manually`)
|
|
338
348
|
}
|
|
@@ -341,9 +351,8 @@ export function effectSolutionsInstall(ctx: Ctx): (mode: "install" | "upgrade",
|
|
|
341
351
|
}
|
|
342
352
|
|
|
343
353
|
function syncEffectSolutionsCli(ctx: Ctx): void {
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
346
|
-
if (!/"effect-kit@docks"[ \t]*:[ \t]*true/.test(readFileSync(settings, "utf8"))) return
|
|
354
|
+
const { warn } = ctx.services.logger
|
|
355
|
+
if (!/"effect-kit@docks"[ \t]*:[ \t]*true/.test(payloadText("SoT/.claude/settings.json"))) return
|
|
347
356
|
|
|
348
357
|
if (ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx)) !== 0) {
|
|
349
358
|
warn("effect-solutions bootstrap failed — continuing sync")
|
|
@@ -353,6 +362,7 @@ function syncEffectSolutionsCli(ctx: Ctx): void {
|
|
|
353
362
|
// ----------------------------------------------------- prune + snapshot ----
|
|
354
363
|
|
|
355
364
|
function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
365
|
+
const { change, echo, warn } = ctx.services.logger
|
|
356
366
|
if (!existsSync(snapshot)) {
|
|
357
367
|
if (ctx.dryRun) {
|
|
358
368
|
echo(
|
|
@@ -362,7 +372,7 @@ function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
|
362
372
|
return
|
|
363
373
|
}
|
|
364
374
|
|
|
365
|
-
const current =
|
|
375
|
+
const current = normalizeManifest(manifest)
|
|
366
376
|
let removed = 0
|
|
367
377
|
let failed = 0
|
|
368
378
|
for (const slug of readSlugs(snapshot)) {
|
|
@@ -383,7 +393,10 @@ function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
|
383
393
|
}
|
|
384
394
|
}
|
|
385
395
|
|
|
386
|
-
if (removed > 0)
|
|
396
|
+
if (removed > 0) {
|
|
397
|
+
change(`Kit-managed skills removed (-${removed})`)
|
|
398
|
+
ctx.nextStepTriggers.skillsRestart = true
|
|
399
|
+
}
|
|
387
400
|
if (failed > 0) warn(`${failed} skill remove(s) failed — re-run with --prune or run: npx skills remove -g -y -a '*' -s <name>`)
|
|
388
401
|
}
|
|
389
402
|
|
|
@@ -391,19 +404,22 @@ function updateSnapshot(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
|
391
404
|
if (ctx.dryRun) return
|
|
392
405
|
|
|
393
406
|
mkdirSync(ctx.agentsDir, { recursive: true })
|
|
394
|
-
const sorted = [...new Set(
|
|
395
|
-
|
|
407
|
+
const sorted = [...new Set(normalizeManifest(manifest))].sort(compareCodepoints)
|
|
408
|
+
writeFileIfChanged(snapshot, sorted.length > 0 ? `${sorted.join("\n")}\n` : "")
|
|
396
409
|
}
|
|
397
410
|
|
|
398
411
|
// -------------------------------------------------------------- summary ----
|
|
399
412
|
|
|
400
413
|
export function skillsSummary(ctx: Ctx, state: SkillsState): void {
|
|
414
|
+
const { echo } = ctx.services.logger
|
|
401
415
|
echo(`Skills: ${p(ctx.agentsDir, "skills")}`)
|
|
402
416
|
if (!ctx.dryRun) {
|
|
403
417
|
echo(` ${state.present} universal skill(s) installed`)
|
|
404
418
|
}
|
|
405
419
|
}
|
|
406
420
|
|
|
407
|
-
export function skillsNextSteps():
|
|
408
|
-
|
|
421
|
+
export function skillsNextSteps(ctx: Ctx): Array<string> {
|
|
422
|
+
return ctx.verbose || ctx.nextStepTriggers.skillsRestart
|
|
423
|
+
? ["Restart Claude Code (and Codex) to discover newly installed universal skills."]
|
|
424
|
+
: []
|
|
409
425
|
}
|
|
@@ -2,23 +2,24 @@
|
|
|
2
2
|
* Verified-version-floor layer over SoT/toolchain.json. Probe/install commands
|
|
3
3
|
* spawn deterministic argv arrays and are covered by golden regression cases.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { readSync } from "node:fs"
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import type { ToolId } from "./deps"
|
|
8
8
|
import type { Ctx } from "./index"
|
|
9
9
|
import { compareCodepoints, isObject, parseJson, type Json } from "./jq"
|
|
10
|
-
import {
|
|
10
|
+
import type { EngineServices } from "./services"
|
|
11
|
+
import { payloadText } from "../payload"
|
|
11
12
|
|
|
12
|
-
type InstallFn = (mode: "install" | "upgrade", version: string) => number
|
|
13
|
+
type InstallFn = (mode: "install" | "upgrade", version: string, services: EngineServices) => number
|
|
13
14
|
|
|
14
|
-
function manifest(
|
|
15
|
-
const doc = parseJson(
|
|
15
|
+
function manifest(): { [k: string]: Json } {
|
|
16
|
+
const doc = parseJson(payloadText("SoT/toolchain.json"))
|
|
16
17
|
const tools = doc !== undefined && isObject(doc) ? doc["tools"] : undefined
|
|
17
18
|
return tools !== undefined && isObject(tools) ? tools : {}
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export function field(ctx: Ctx, tool: string, name: string): string {
|
|
21
|
-
const entry = manifest(
|
|
22
|
+
const entry = manifest()[tool]
|
|
22
23
|
if (entry === undefined || !isObject(entry)) return ""
|
|
23
24
|
const v = entry[name]
|
|
24
25
|
return v === undefined || v === null ? "" : String(v)
|
|
@@ -37,15 +38,8 @@ export function isNewer(a: string, b: string): boolean {
|
|
|
37
38
|
return compareCodepoints(a, b) > 0
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export function present(ctx: Ctx, tool:
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
commandExists("bun") ||
|
|
44
|
-
isExecutable(p(process.env["BUN_INSTALL"] ?? p(ctx.home, ".bun"), "bin", "bun")) ||
|
|
45
|
-
isExecutable(p(ctx.home, ".bun", "bin", "bun"))
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
return commandExists(tool)
|
|
41
|
+
export function present(ctx: Ctx, tool: ToolId): boolean {
|
|
42
|
+
return ctx.services.deps.probe(tool).state === "present"
|
|
49
43
|
}
|
|
50
44
|
|
|
51
45
|
function firstLineField(out: string, index: number): string {
|
|
@@ -53,67 +47,51 @@ function firstLineField(out: string, index: number): string {
|
|
|
53
47
|
return fields[index === -1 ? fields.length - 1 : index] ?? ""
|
|
54
48
|
}
|
|
55
49
|
|
|
56
|
-
export function installedVersion(ctx: Ctx, tool:
|
|
57
|
-
|
|
50
|
+
export function installedVersion(ctx: Ctx, tool: ToolId): string {
|
|
51
|
+
const version = (): string => ctx.services.deps.version(tool)
|
|
58
52
|
switch (tool) {
|
|
59
53
|
case "rtk":
|
|
60
|
-
return firstLineField(
|
|
54
|
+
return firstLineField(version(), 1)
|
|
61
55
|
case "claude":
|
|
62
|
-
return firstLineField(
|
|
56
|
+
return firstLineField(version(), 0)
|
|
63
57
|
case "codex":
|
|
64
|
-
return firstLineField(capture("codex", ["--version"]), -1)
|
|
65
|
-
case "bun":
|
|
66
|
-
return commandExists("bun") ? capture("bun", ["--version"]) : capture(p(ctx.home, ".bun", "bin", "bun"), ["--version"])
|
|
67
58
|
case "agent-browser":
|
|
68
|
-
return firstLineField(
|
|
69
|
-
case "effect-solutions": {
|
|
70
|
-
const bunbin = commandExists("bun") ? "bun" : p(ctx.home, ".bun", "bin", "bun")
|
|
71
|
-
if (bunbin !== "bun" && !isExecutable(bunbin)) return ""
|
|
72
|
-
const m = /effect-solutions@([0-9][0-9.]*)/.exec(capture(bunbin, ["pm", "-g", "ls"]))
|
|
73
|
-
return m?.[1] ?? ""
|
|
74
|
-
}
|
|
59
|
+
return firstLineField(version(), -1)
|
|
75
60
|
case "git":
|
|
76
|
-
return firstLineField(
|
|
61
|
+
return firstLineField(version(), 2)
|
|
77
62
|
case "node":
|
|
78
|
-
return
|
|
79
|
-
case "npm":
|
|
80
|
-
return capture("npm", ["--version"])
|
|
63
|
+
return version().replace(/^v/, "")
|
|
81
64
|
case "jq":
|
|
82
|
-
return
|
|
65
|
+
return version().replace(/^jq-/, "")
|
|
83
66
|
case "curl":
|
|
84
|
-
return firstLineField(capture("curl", ["--version"]), 1)
|
|
85
67
|
case "tsc":
|
|
86
|
-
return firstLineField(
|
|
68
|
+
return firstLineField(version(), 1)
|
|
69
|
+
case "bun":
|
|
70
|
+
case "effect-solutions":
|
|
71
|
+
case "npm":
|
|
72
|
+
return version()
|
|
87
73
|
default:
|
|
88
74
|
return ""
|
|
89
75
|
}
|
|
90
76
|
}
|
|
91
77
|
|
|
92
|
-
export function latestVersion(tool:
|
|
93
|
-
|
|
94
|
-
case "rtk": {
|
|
95
|
-
const body = capture("curl", ["-fsSL", "--max-time", "5", "https://api.github.com/repos/rtk-ai/rtk/releases/latest"])
|
|
96
|
-
const doc = parseJson(body)
|
|
97
|
-
const tag = doc !== undefined && isObject(doc) && typeof doc["tag_name"] === "string" ? doc["tag_name"] : ""
|
|
98
|
-
return tag.replace(/^v/, "")
|
|
99
|
-
}
|
|
100
|
-
case "agent-browser":
|
|
101
|
-
case "effect-solutions":
|
|
102
|
-
return commandExists("npm") ? capture("npm", ["view", tool, "version"]) : ""
|
|
103
|
-
default:
|
|
104
|
-
return ""
|
|
105
|
-
}
|
|
78
|
+
export function latestVersion(ctx: Ctx, tool: ToolId): string {
|
|
79
|
+
return ctx.services.deps.latest(tool)
|
|
106
80
|
}
|
|
107
81
|
|
|
108
82
|
/** Blocking TTY prompt matching bash `read -r -p` (prompt on stderr). */
|
|
109
|
-
function promptLine(
|
|
110
|
-
|
|
83
|
+
export function promptLine(
|
|
84
|
+
prompt: string,
|
|
85
|
+
write: (chunk: string) => void = (chunk) => void process.stderr.write(chunk),
|
|
86
|
+
readByte: (buffer: Buffer) => number = (buffer) => readSync(0, buffer, 0, 1, null)
|
|
87
|
+
): string {
|
|
88
|
+
write(prompt)
|
|
111
89
|
const buf = Buffer.alloc(1)
|
|
112
90
|
let line = ""
|
|
113
91
|
for (;;) {
|
|
114
92
|
let n: number
|
|
115
93
|
try {
|
|
116
|
-
n =
|
|
94
|
+
n = readByte(buf)
|
|
117
95
|
} catch {
|
|
118
96
|
break
|
|
119
97
|
}
|
|
@@ -127,6 +105,7 @@ function promptLine(prompt: string): string {
|
|
|
127
105
|
|
|
128
106
|
/** toolchain::_gate — { proceed, target } ("" target = latest). */
|
|
129
107
|
function gate(ctx: Ctx, tool: string, mode: "install" | "upgrade", latest: string): { proceed: boolean; target: string } {
|
|
108
|
+
const { warn } = ctx.services.logger
|
|
130
109
|
const verified = field(ctx, tool, "verified")
|
|
131
110
|
const pinnable = field(ctx, tool, "pinnable")
|
|
132
111
|
|
|
@@ -138,7 +117,7 @@ function gate(ctx: Ctx, tool: string, mode: "install" | "upgrade", latest: strin
|
|
|
138
117
|
}
|
|
139
118
|
|
|
140
119
|
if (process.stdin.isTTY === true) {
|
|
141
|
-
|
|
120
|
+
ctx.services.logger.warn(`${tool} ${latest} is not kit-verified (verified: ${verified}).`)
|
|
142
121
|
const answer = promptLine(`Install ${tool} ${latest} anyway? [y/N] `)
|
|
143
122
|
if (/^[yY]/.test(answer)) return { proceed: true, target: "" }
|
|
144
123
|
}
|
|
@@ -153,11 +132,12 @@ function gate(ctx: Ctx, tool: string, mode: "install" | "upgrade", latest: strin
|
|
|
153
132
|
return { proceed: false, target: "" }
|
|
154
133
|
}
|
|
155
134
|
|
|
156
|
-
export function ensure(ctx: Ctx, tool:
|
|
135
|
+
export function ensure(ctx: Ctx, tool: ToolId, installFn: InstallFn): number {
|
|
136
|
+
const { echo, verbose, warn } = ctx.services.logger
|
|
157
137
|
const policy = field(ctx, tool, "policy")
|
|
158
138
|
|
|
159
139
|
if (!present(ctx, tool)) {
|
|
160
|
-
const latest = latestVersion(tool)
|
|
140
|
+
const latest = latestVersion(ctx, tool)
|
|
161
141
|
if (ctx.dryRun) {
|
|
162
142
|
echo(`[dry-run] would install ${tool} (${latest !== "" ? latest : "latest"}, gated by toolchain.json verified pin)`)
|
|
163
143
|
return 0
|
|
@@ -176,7 +156,7 @@ export function ensure(ctx: Ctx, tool: string, installFn: InstallFn): number {
|
|
|
176
156
|
if (!g.proceed) return 0
|
|
177
157
|
target = g.target
|
|
178
158
|
}
|
|
179
|
-
return installFn("install", target !== "" ? target : latest)
|
|
159
|
+
return installFn("install", target !== "" ? target : latest, ctx.services)
|
|
180
160
|
}
|
|
181
161
|
|
|
182
162
|
const installed = installedVersion(ctx, tool)
|
|
@@ -187,17 +167,17 @@ export function ensure(ctx: Ctx, tool: string, installFn: InstallFn): number {
|
|
|
187
167
|
echo(`[dry-run] ${tool} present (${installedLabel})`)
|
|
188
168
|
return 0
|
|
189
169
|
}
|
|
190
|
-
|
|
170
|
+
verbose(`${tool} present (${installedLabel})`)
|
|
191
171
|
return 0
|
|
192
172
|
}
|
|
193
173
|
|
|
194
|
-
const latest = latestVersion(tool)
|
|
174
|
+
const latest = latestVersion(ctx, tool)
|
|
195
175
|
if (latest === "") {
|
|
196
176
|
if (ctx.dryRun) {
|
|
197
177
|
echo(`[dry-run] ${tool} present (${installedLabel}); latest unknown (offline?) — no action`)
|
|
198
178
|
return 0
|
|
199
179
|
}
|
|
200
|
-
|
|
180
|
+
verbose(`${tool} present (${installedLabel}; latest unknown — no action)`)
|
|
201
181
|
return 0
|
|
202
182
|
}
|
|
203
183
|
|
|
@@ -208,14 +188,14 @@ export function ensure(ctx: Ctx, tool: string, installFn: InstallFn): number {
|
|
|
208
188
|
}
|
|
209
189
|
const g = gate(ctx, tool, "upgrade", latest)
|
|
210
190
|
if (!g.proceed) return 0
|
|
211
|
-
return installFn("upgrade", g.target !== "" ? g.target : latest)
|
|
191
|
+
return installFn("upgrade", g.target !== "" ? g.target : latest, ctx.services)
|
|
212
192
|
}
|
|
213
193
|
|
|
214
194
|
if (ctx.dryRun) {
|
|
215
195
|
echo(`[dry-run] ${tool} up to date (${installed})`)
|
|
216
196
|
return 0
|
|
217
197
|
}
|
|
218
|
-
|
|
198
|
+
verbose(`${tool} up to date (${installed})`)
|
|
219
199
|
return 0
|
|
220
200
|
}
|
|
221
201
|
|
|
@@ -225,10 +205,11 @@ function row(cells: [string, string, string, string, string, string]): string {
|
|
|
225
205
|
}
|
|
226
206
|
|
|
227
207
|
export function report(ctx: Ctx): void {
|
|
208
|
+
const { echo } = ctx.services.logger
|
|
228
209
|
echo(row(["TOOL", "KIND", "INSTALLED", "FLOOR", "VERIFIED", "STATUS"]))
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
for (const tool of Object.keys(manifest(
|
|
210
|
+
const pn = ctx.services.platform.name()
|
|
211
|
+
const platformOs = pn === "unknown" ? "" : pn
|
|
212
|
+
for (const tool of Object.keys(manifest()).sort(compareCodepoints)) {
|
|
232
213
|
const os = field(ctx, tool, "os")
|
|
233
214
|
if (os !== "" && platformOs !== "" && os !== platformOs) continue
|
|
234
215
|
const kind = field(ctx, tool, "kind")
|
|
@@ -241,8 +222,9 @@ export function report(ctx: Ctx): void {
|
|
|
241
222
|
}
|
|
242
223
|
let installed: string
|
|
243
224
|
let status: string
|
|
244
|
-
|
|
245
|
-
|
|
225
|
+
const toolId = tool as ToolId
|
|
226
|
+
if (present(ctx, toolId)) {
|
|
227
|
+
installed = installedVersion(ctx, toolId)
|
|
246
228
|
status = "ok"
|
|
247
229
|
if (floor !== "" && installed !== "" && isNewer(floor, installed)) {
|
|
248
230
|
status = "below-floor"
|
package/cli/src/engine.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Console, Effect } from "effect"
|
|
2
2
|
import { spawnSync } from "node:child_process"
|
|
3
3
|
import { runEngineNative } from "./engine-native"
|
|
4
|
+
import { makeEngineServices } from "./engine-native/services"
|
|
4
5
|
import { kitHome } from "./kitHome"
|
|
6
|
+
import { DependencyManagerService, LoggerService, PlatformService } from "./services"
|
|
7
|
+
|
|
8
|
+
// Same factory as the Effect rim's live layers — this path runs outside the
|
|
9
|
+
// runtime (child-spawn capture), so it takes the services directly.
|
|
10
|
+
const services = makeEngineServices()
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* The single seam between the typed CLI and EngineNative. Engine execution
|
|
@@ -24,7 +30,10 @@ export const engine = (args: ReadonlyArray<string>) =>
|
|
|
24
30
|
if (bashEngineRequested()) {
|
|
25
31
|
yield* bail(bashRemovedMessage, 2)
|
|
26
32
|
}
|
|
27
|
-
const
|
|
33
|
+
const logger = yield* LoggerService
|
|
34
|
+
const deps = yield* DependencyManagerService
|
|
35
|
+
const platform = yield* PlatformService
|
|
36
|
+
const code = yield* Effect.sync(() => runEngineNative(args, { logger, deps, platform }))
|
|
28
37
|
if (code !== 0) {
|
|
29
38
|
yield* Effect.sync(() => process.exit(code))
|
|
30
39
|
}
|
|
@@ -43,7 +52,7 @@ export const engineCapture = (args: ReadonlyArray<string>) =>
|
|
|
43
52
|
stdio: ["ignore", "pipe", "inherit"]
|
|
44
53
|
})
|
|
45
54
|
if (res.error !== undefined || res.status !== 0) {
|
|
46
|
-
|
|
55
|
+
services.logger.warn(`engine capture failed (${args.join(" ")} exited ${res.status ?? "spawn-error"})`)
|
|
47
56
|
}
|
|
48
57
|
return res.stdout ?? ""
|
|
49
58
|
})
|