docks-kit 0.14.3 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -1
- package/README.md +11 -5
- package/cli/docs/flags.md +6 -0
- package/cli/docs/overview.md +1 -1
- package/cli/src/argv.ts +433 -0
- package/cli/src/commands/docs.ts +6 -6
- package/cli/src/commands/model.ts +11 -11
- package/cli/src/commands/models.ts +6 -6
- package/cli/src/commands/plugins.ts +6 -6
- package/cli/src/commands/skills.ts +6 -6
- package/cli/src/commands/status.ts +3 -3
- package/cli/src/commands/sync.ts +43 -92
- package/cli/src/commands/toolchain.ts +12 -12
- package/cli/src/commands/update.ts +3 -3
- package/cli/src/engine-native/DESIGN.md +15 -4
- package/cli/src/engine-native/bun.ts +22 -21
- package/cli/src/engine-native/claudeSync.ts +30 -31
- package/cli/src/engine-native/codexSync.ts +24 -24
- package/cli/src/engine-native/deps.ts +43 -35
- package/cli/src/engine-native/exec.ts +61 -5
- package/cli/src/engine-native/index.ts +141 -33
- package/cli/src/engine-native/logger.ts +114 -14
- package/cli/src/engine-native/modes.ts +4 -4
- package/cli/src/engine-native/parseArgs.ts +1 -1
- package/cli/src/engine-native/services.ts +5 -5
- package/cli/src/engine-native/skillsSync.ts +19 -20
- package/cli/src/engine-native/toolchain.ts +40 -27
- package/cli/src/engine.ts +2 -2
- package/cli/src/generated/sotPayload.ts +1 -1
- package/cli/src/main.ts +27 -28
- package/cli/src/services.ts +4 -4
- package/docks-kit +1 -1
- package/package.json +5 -7
|
@@ -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
|
|
@@ -23,7 +23,7 @@ export class ExitError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"]
|
|
26
|
+
export const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"]
|
|
27
27
|
const MODIFIER_FLAGS = new Set<ModifierFlag>([
|
|
28
28
|
"--claude-model",
|
|
29
29
|
"--claude-effort",
|
|
@@ -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)
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* the kit-managed snapshot, the effect-solutions toolchain callback, and the
|
|
5
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,9 +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
|
-
syncEffectSolutionsCli(ctx)
|
|
28
|
+
await syncUniversal(ctx, state, skillsDir, manifest)
|
|
29
|
+
if (ctx.prune) await reconcileRemovals(ctx, manifest, snapshot)
|
|
30
|
+
await syncEffectSolutionsCli(ctx)
|
|
32
31
|
updateSnapshot(ctx, manifest, snapshot)
|
|
33
32
|
return state
|
|
34
33
|
}
|
|
@@ -55,7 +54,7 @@ function readSlugs(file: string): Array<string> {
|
|
|
55
54
|
return existsSync(file) ? normalizeManifest(readFileSync(file, "utf8")) : []
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest: string): void {
|
|
57
|
+
async function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest: string): Promise<void> {
|
|
59
58
|
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
60
59
|
if (ctx.services.deps.probe("npx").state === "missing") {
|
|
61
60
|
ctx.services.deps.warnMissing("npx", ctx.services.logger, "skipping universal skills bootstrap")
|
|
@@ -87,11 +86,11 @@ function syncUniversal(ctx: Ctx, state: SkillsState, skillsDir: string, manifest
|
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
progress(`Installing universal skill ${slug}...`)
|
|
90
|
-
const res =
|
|
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()
|
|
94
|
-
if (res.error === undefined && res.
|
|
93
|
+
if (res.error === undefined && res.exitCode === 0) {
|
|
95
94
|
added++
|
|
96
95
|
} else {
|
|
97
96
|
warn(`Failed to install universal skill: ${slug}`)
|
|
@@ -221,26 +220,26 @@ function linkOrCopyWithWarnings(target: string, link: string, services: EngineSe
|
|
|
221
220
|
/** skills::_effect_solutions_install. */
|
|
222
221
|
export function effectSolutionsInstall(
|
|
223
222
|
ctx: Ctx
|
|
224
|
-
): (mode: "install" | "upgrade", version: string, services: EngineServices) => number {
|
|
225
|
-
return (mode, version, services) => {
|
|
223
|
+
): (mode: "install" | "upgrade", version: string, services: EngineServices) => Promise<number> {
|
|
224
|
+
return async (mode, version, services) => {
|
|
226
225
|
const { change, clearProgress, progress, verbose, warn } = services.logger
|
|
227
226
|
const verb = mode === "upgrade" ? "Upgrading" : "Installing"
|
|
228
227
|
const pkg = `effect-solutions@${version !== "" ? version : "latest"}`
|
|
229
228
|
|
|
230
|
-
const bunState = bunBootstrap(ctx, services)
|
|
229
|
+
const bunState = await bunBootstrap(ctx, services)
|
|
231
230
|
if (bunState.kind === "deferred") return 1
|
|
232
231
|
const bun = bunState.executable
|
|
233
232
|
|
|
234
233
|
verbose(`${verb} effect-solutions CLI via bun${version !== "" ? ` (pinned ${version})` : ""}...`)
|
|
235
234
|
progress(`${verb} effect-solutions CLI...`)
|
|
236
|
-
const installResult =
|
|
235
|
+
const installResult = await spawnProcess(bun, ["add", "-g", pkg], { stdio: "ignore" })
|
|
237
236
|
clearProgress()
|
|
238
|
-
if (installResult.
|
|
237
|
+
if (installResult.exitCode !== 0) {
|
|
239
238
|
warn(`bun add -g ${pkg} failed. Try manually: bun add -g ${pkg}`)
|
|
240
239
|
return 1
|
|
241
240
|
}
|
|
242
241
|
|
|
243
|
-
const location = services.deps.location("effect-solutions")
|
|
242
|
+
const location = await services.deps.location("effect-solutions")
|
|
244
243
|
const gbin = location.binDir
|
|
245
244
|
if (location.path !== "") {
|
|
246
245
|
mkdirSync(p(ctx.home, ".local", "bin"), { recursive: true })
|
|
@@ -254,12 +253,12 @@ export function effectSolutionsInstall(
|
|
|
254
253
|
}
|
|
255
254
|
}
|
|
256
255
|
|
|
257
|
-
function syncEffectSolutionsCli(ctx: Ctx): void {
|
|
256
|
+
async function syncEffectSolutionsCli(ctx: Ctx): Promise<void> {
|
|
258
257
|
const { clearProgress, progress, warn } = ctx.services.logger
|
|
259
258
|
if (!/"effect-kit@docks"[ \t]*:[ \t]*true/.test(payloadText("SoT/.claude/settings.json"))) return
|
|
260
259
|
|
|
261
260
|
progress("Checking effect-solutions CLI...")
|
|
262
|
-
const result = ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
|
|
261
|
+
const result = await ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
|
|
263
262
|
clearProgress()
|
|
264
263
|
if (result !== 0) {
|
|
265
264
|
warn("effect-solutions bootstrap failed — continuing sync")
|
|
@@ -268,7 +267,7 @@ function syncEffectSolutionsCli(ctx: Ctx): void {
|
|
|
268
267
|
|
|
269
268
|
// ----------------------------------------------------- prune + snapshot ----
|
|
270
269
|
|
|
271
|
-
function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
270
|
+
async function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): Promise<void> {
|
|
272
271
|
const { change, clearProgress, echo, progress, warn } = ctx.services.logger
|
|
273
272
|
if (!existsSync(snapshot)) {
|
|
274
273
|
if (ctx.dryRun) {
|
|
@@ -290,11 +289,11 @@ function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
|
290
289
|
continue
|
|
291
290
|
}
|
|
292
291
|
progress(`Removing universal skill ${base}...`)
|
|
293
|
-
const res =
|
|
292
|
+
const res = await spawnProcess("npx", ["--yes", skillsCli(ctx), "remove", "--global", base, "-y"], {
|
|
294
293
|
stdio: "ignore"
|
|
295
294
|
})
|
|
296
295
|
clearProgress()
|
|
297
|
-
if (res.error === undefined && res.
|
|
296
|
+
if (res.error === undefined && res.exitCode === 0) {
|
|
298
297
|
removed++
|
|
299
298
|
} else {
|
|
300
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,40 +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
57
|
case "claude":
|
|
54
|
-
return firstLineField(version(), 0)
|
|
58
|
+
return firstLineField(await version(), 0)
|
|
55
59
|
case "codex":
|
|
56
|
-
return firstLineField(version(), -1)
|
|
60
|
+
return firstLineField(await version(), -1)
|
|
57
61
|
case "git":
|
|
58
|
-
return firstLineField(version(), 2)
|
|
62
|
+
return firstLineField(await version(), 2)
|
|
59
63
|
case "node":
|
|
60
|
-
return version().replace(/^v/, "")
|
|
64
|
+
return (await version()).replace(/^v/, "")
|
|
61
65
|
case "jq":
|
|
62
|
-
return version().replace(/^jq-/, "")
|
|
66
|
+
return (await version()).replace(/^jq-/, "")
|
|
63
67
|
case "curl":
|
|
64
68
|
case "tsc":
|
|
65
|
-
return firstLineField(version(), 1)
|
|
69
|
+
return firstLineField(await version(), 1)
|
|
66
70
|
case "bun":
|
|
67
71
|
case "effect-solutions":
|
|
68
72
|
case "npm":
|
|
69
|
-
return version()
|
|
73
|
+
return await version()
|
|
70
74
|
case "bwrap":
|
|
71
|
-
return firstLineField(version(), 1)
|
|
75
|
+
return firstLineField(await version(), 1)
|
|
72
76
|
case "ffplay":
|
|
73
|
-
return firstLineField(version(), 2).replace(/-.*$/, "")
|
|
77
|
+
return firstLineField(await version(), 2).replace(/-.*$/, "")
|
|
74
78
|
case "intelephense":
|
|
75
79
|
case "typescript-language-server":
|
|
76
|
-
return version().trim()
|
|
80
|
+
return (await version()).trim()
|
|
77
81
|
default:
|
|
78
82
|
return ""
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
export function latestVersion(ctx: Ctx, tool: ToolId): string {
|
|
83
|
-
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)
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
/** Blocking TTY prompt matching bash `read -r -p` (prompt on stderr). */
|
|
@@ -108,7 +112,12 @@ export function promptLine(
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
/** toolchain::_gate — { proceed, target } ("" target = latest). */
|
|
111
|
-
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 }> {
|
|
112
121
|
const { warn } = ctx.services.logger
|
|
113
122
|
const verified = field(ctx, tool, "verified")
|
|
114
123
|
const pinnable = field(ctx, tool, "pinnable")
|
|
@@ -122,7 +131,11 @@ function gate(ctx: Ctx, tool: string, mode: "install" | "upgrade", latest: strin
|
|
|
122
131
|
|
|
123
132
|
if (process.stdin.isTTY === true) {
|
|
124
133
|
ctx.services.logger.warn(`${tool} ${latest} is not kit-verified (verified: ${verified}).`)
|
|
125
|
-
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))
|
|
126
139
|
if (/^[yY]/.test(answer)) return { proceed: true, target: "" }
|
|
127
140
|
}
|
|
128
141
|
|
|
@@ -136,12 +149,12 @@ function gate(ctx: Ctx, tool: string, mode: "install" | "upgrade", latest: strin
|
|
|
136
149
|
return { proceed: false, target: "" }
|
|
137
150
|
}
|
|
138
151
|
|
|
139
|
-
export function ensure(ctx: Ctx, tool: ToolId, installFn: InstallFn): number {
|
|
152
|
+
export async function ensure(ctx: Ctx, tool: ToolId, installFn: InstallFn): Promise<number> {
|
|
140
153
|
const { echo, verbose, warn } = ctx.services.logger
|
|
141
154
|
const policy = field(ctx, tool, "policy")
|
|
142
155
|
|
|
143
156
|
if (!present(ctx, tool)) {
|
|
144
|
-
const latest = latestVersion(ctx, tool)
|
|
157
|
+
const latest = await latestVersion(ctx, tool)
|
|
145
158
|
if (ctx.dryRun) {
|
|
146
159
|
echo(`[dry-run] would install ${tool} (${latest !== "" ? latest : "latest"}, gated by toolchain.json verified pin)`)
|
|
147
160
|
return 0
|
|
@@ -156,14 +169,14 @@ export function ensure(ctx: Ctx, tool: ToolId, installFn: InstallFn): number {
|
|
|
156
169
|
warn(`${tool} latest version unknown (offline?) and not pinnable — installing latest unverified`)
|
|
157
170
|
}
|
|
158
171
|
} else {
|
|
159
|
-
const g = gate(ctx, tool, "install", latest)
|
|
172
|
+
const g = await gate(ctx, tool, "install", latest)
|
|
160
173
|
if (!g.proceed) return 0
|
|
161
174
|
target = g.target
|
|
162
175
|
}
|
|
163
|
-
return installFn("install", target !== "" ? target : latest, ctx.services)
|
|
176
|
+
return await installFn("install", target !== "" ? target : latest, ctx.services)
|
|
164
177
|
}
|
|
165
178
|
|
|
166
|
-
const installed = installedVersion(ctx, tool)
|
|
179
|
+
const installed = await installedVersion(ctx, tool)
|
|
167
180
|
const installedLabel = installed !== "" ? installed : "version unknown"
|
|
168
181
|
|
|
169
182
|
if (policy !== "track") {
|
|
@@ -175,7 +188,7 @@ export function ensure(ctx: Ctx, tool: ToolId, installFn: InstallFn): number {
|
|
|
175
188
|
return 0
|
|
176
189
|
}
|
|
177
190
|
|
|
178
|
-
const latest = latestVersion(ctx, tool)
|
|
191
|
+
const latest = await latestVersion(ctx, tool)
|
|
179
192
|
if (latest === "") {
|
|
180
193
|
if (ctx.dryRun) {
|
|
181
194
|
echo(`[dry-run] ${tool} present (${installedLabel}); latest unknown (offline?) — no action`)
|
|
@@ -190,9 +203,9 @@ export function ensure(ctx: Ctx, tool: ToolId, installFn: InstallFn): number {
|
|
|
190
203
|
echo(`[dry-run] would upgrade ${tool} (${installed !== "" ? installed : "unknown"} -> ${latest}, gated by toolchain.json verified pin)`)
|
|
191
204
|
return 0
|
|
192
205
|
}
|
|
193
|
-
const g = gate(ctx, tool, "upgrade", latest)
|
|
206
|
+
const g = await gate(ctx, tool, "upgrade", latest)
|
|
194
207
|
if (!g.proceed) return 0
|
|
195
|
-
return installFn("upgrade", g.target !== "" ? g.target : latest, ctx.services)
|
|
208
|
+
return await installFn("upgrade", g.target !== "" ? g.target : latest, ctx.services)
|
|
196
209
|
}
|
|
197
210
|
|
|
198
211
|
if (ctx.dryRun) {
|
|
@@ -208,7 +221,7 @@ function row(cells: [string, string, string, string, string, string]): string {
|
|
|
208
221
|
return cells.map((c, i) => (i < widths.length ? c.padEnd(widths[i]!) : c)).join(" ")
|
|
209
222
|
}
|
|
210
223
|
|
|
211
|
-
export function report(ctx: Ctx): void {
|
|
224
|
+
export async function report(ctx: Ctx): Promise<void> {
|
|
212
225
|
const { echo } = ctx.services.logger
|
|
213
226
|
echo(row(["TOOL", "KIND", "INSTALLED", "FLOOR", "VERIFIED", "STATUS"]))
|
|
214
227
|
const pn = ctx.services.platform.name()
|
|
@@ -228,7 +241,7 @@ export function report(ctx: Ctx): void {
|
|
|
228
241
|
let status: string
|
|
229
242
|
const toolId = tool as ToolId
|
|
230
243
|
if (present(ctx, toolId)) {
|
|
231
|
-
installed = installedVersion(ctx, toolId)
|
|
244
|
+
installed = await installedVersion(ctx, toolId)
|
|
232
245
|
status = "ok"
|
|
233
246
|
if (floor !== "" && installed !== "" && isNewer(floor, installed)) {
|
|
234
247
|
status = "below-floor"
|
package/cli/src/engine.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { DependencyManagerService, LoggerService, PlatformService } from "./serv
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* The single seam between the typed CLI and EngineNative. Engine execution
|
|
10
|
-
* stays in-process after
|
|
10
|
+
* stays in-process after effect/unstable/cli has parsed pickers and flag spellings.
|
|
11
11
|
*/
|
|
12
12
|
const bashRemovedMessage = "bash engine removed — recover at tag bash-engine-final"
|
|
13
13
|
const bashEngineRequested = (): boolean => process.env["DOCKS_KIT_ENGINE"] === "bash"
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Generated by cli/scripts/generate-sot-payload.ts. DO NOT EDIT.
|
|
2
2
|
// Edit SoT/, notification.mp3, or package.json, then run: bun cli/scripts/generate-sot-payload.ts
|
|
3
3
|
|
|
4
|
-
export const GENERATED_PACKAGE_VERSION = "0.
|
|
4
|
+
export const GENERATED_PACKAGE_VERSION = "0.15.0"
|
|
5
5
|
|
|
6
6
|
export const GENERATED_PAYLOAD_TEXT = {
|
|
7
7
|
"SoT/.agents/skills.txt": "# Universal AI-agent skill manifest intentionally empty.\n# Global skill discovery is opt-in: add one <owner>/<repo> slug per line.\n# EngineNative ignores comments and blank lines.\n",
|
package/cli/src/main.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { Command } from "
|
|
3
|
-
import {
|
|
2
|
+
import { Command, CliOutput } from "effect/unstable/cli"
|
|
3
|
+
import { BunRuntime, BunServices } from "@effect/platform-bun"
|
|
4
4
|
import { Console, Effect, Layer } from "effect"
|
|
5
|
-
import { engine } from "./engine"
|
|
6
5
|
import { EngineServicesLive } from "./services"
|
|
7
6
|
import { docsCommand } from "./commands/docs"
|
|
8
7
|
import { modelCommand } from "./commands/model"
|
|
@@ -14,6 +13,7 @@ import { syncCommand } from "./commands/sync"
|
|
|
14
13
|
import { toolchainCommand } from "./commands/toolchain"
|
|
15
14
|
import { updateCommand } from "./commands/update"
|
|
16
15
|
import { GENERATED_PACKAGE_VERSION } from "./generated/sotPayload"
|
|
16
|
+
import { prepareArgv } from "./argv"
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
const root = Command.make("docks-kit", {}, () =>
|
|
@@ -50,37 +50,36 @@ const root = Command.make("docks-kit", {}, () =>
|
|
|
50
50
|
])
|
|
51
51
|
)
|
|
52
52
|
|
|
53
|
+
// v4's default formatter renders `name vversion`, but the `docks-kit` launcher
|
|
54
|
+
// compares `--version` against the bare `package.json` version. The trailing
|
|
55
|
+
// newline reproduces the characterized byte-for-byte output of the v3 CLI.
|
|
56
|
+
const bareVersionFormatter: CliOutput.Formatter = {
|
|
57
|
+
...CliOutput.defaultFormatter(),
|
|
58
|
+
formatVersion: (_name, version) => `${version}\n`
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
// Harness-private raw channel:
|
|
54
|
-
// `DOCKS_KIT_ENGINE=native-raw` bypasses
|
|
62
|
+
// `DOCKS_KIT_ENGINE=native-raw` bypasses effect/unstable/cli and hands the raw engine
|
|
55
63
|
// argv to EngineNative so golden tests drive the internal vocabulary directly.
|
|
56
64
|
// PUBLIC engine execution lives at the engine.ts seam after the CLI has
|
|
57
65
|
// parsed/normalized pickers, --flag value forms, and non-engine commands.
|
|
58
66
|
if (process.env["DOCKS_KIT_ENGINE"] === "native-raw") {
|
|
59
67
|
const { runEngineNative } = await import("./engine-native")
|
|
60
|
-
process.exit(runEngineNative(process.argv.slice(2)))
|
|
68
|
+
process.exit(await runEngineNative(process.argv.slice(2)))
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const emptyTextOptions = new Set([
|
|
72
|
-
"--claude-model=",
|
|
73
|
-
"--claude-effort=",
|
|
74
|
-
"--claude-advisor=",
|
|
75
|
-
"--codex-model=",
|
|
76
|
-
"--codex-effort=",
|
|
77
|
-
])
|
|
78
|
-
const argv = process.argv.flatMap((a, index, all) => {
|
|
79
|
-
if (a.startsWith("--claude-plugin=")) {
|
|
80
|
-
return ["--claude-plugin", a.slice("--claude-plugin=".length)]
|
|
81
|
-
}
|
|
82
|
-
if (emptyTextOptions.has(a)) return [a.slice(0, -1), ""]
|
|
83
|
-
return [a]
|
|
84
|
-
})
|
|
71
|
+
// Validate and normalize before parsing because the kit refuses to guess at unrecognized
|
|
72
|
+
// or duplicated flags, and Effect 4 would otherwise negate `--no-<flag>` into a real
|
|
73
|
+
// mutating run. This seam owns argument normalization.
|
|
74
|
+
const prepared = prepareArgv(process.argv.slice(2))
|
|
75
|
+
if (prepared.kind === "reject") {
|
|
76
|
+
process.stderr.write(`${prepared.message}\n`)
|
|
77
|
+
process.exit(prepared.exitCode)
|
|
78
|
+
}
|
|
85
79
|
|
|
86
|
-
|
|
80
|
+
Command.runWith(root, { version: GENERATED_PACKAGE_VERSION })(prepared.args).pipe(
|
|
81
|
+
Effect.provide(
|
|
82
|
+
Layer.mergeAll(BunServices.layer, EngineServicesLive, CliOutput.layer(bareVersionFormatter))
|
|
83
|
+
),
|
|
84
|
+
BunRuntime.runMain
|
|
85
|
+
)
|
package/cli/src/services.ts
CHANGED
|
@@ -8,14 +8,14 @@ import { Context, Layer } from "effect"
|
|
|
8
8
|
import { makeLogger, type Logger, type LoggerSinks } from "./engine-native/logger"
|
|
9
9
|
import { makeEngineServices, makePlatform, type DependencyManager, type Platform } from "./engine-native/services"
|
|
10
10
|
|
|
11
|
-
export class LoggerService extends Context.
|
|
11
|
+
export class LoggerService extends Context.Service<LoggerService, Logger>()("docks-kit/Logger") {}
|
|
12
12
|
|
|
13
|
-
export class DependencyManagerService extends Context.
|
|
13
|
+
export class DependencyManagerService extends Context.Service<
|
|
14
14
|
DependencyManagerService,
|
|
15
15
|
DependencyManager
|
|
16
|
-
>() {}
|
|
16
|
+
>()("docks-kit/DependencyManager") {}
|
|
17
17
|
|
|
18
|
-
export class PlatformService extends Context.
|
|
18
|
+
export class PlatformService extends Context.Service<PlatformService, Platform>()("docks-kit/Platform") {}
|
|
19
19
|
|
|
20
20
|
const live = makeEngineServices()
|
|
21
21
|
|
package/docks-kit
CHANGED
|
@@ -69,7 +69,7 @@ fi
|
|
|
69
69
|
|
|
70
70
|
# Sentinel is a real dependency dir, not bare node_modules/ — a failed or
|
|
71
71
|
# partial install leaves node_modules/ present and would suppress the repair.
|
|
72
|
-
if [[ ! -d "$REPO_DIR/node_modules
|
|
72
|
+
if [[ ! -d "$REPO_DIR/node_modules/effect" ]]; then
|
|
73
73
|
echo "[docks-kit] Installing CLI dependencies (bun install --frozen-lockfile)..." >&2
|
|
74
74
|
(cd "$REPO_DIR" && "$BUN" install --frozen-lockfile >/dev/null)
|
|
75
75
|
fi
|