docks-kit 0.14.3 → 0.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 = spawnSync("npx", ["--yes", skillsCli(ctx), "add", slug, "-g", "-y", "-a", "claude-code", "codex"], {
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.status === 0) {
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 = spawnSync(bun, ["add", "-g", pkg], { stdio: "ignore" })
235
+ const installResult = await spawnProcess(bun, ["add", "-g", pkg], { stdio: "ignore" })
237
236
  clearProgress()
238
- if (installResult.status !== 0) {
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 = spawnSync("npx", ["--yes", skillsCli(ctx), "remove", "--global", base, "-y"], {
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.status === 0) {
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 = (mode: "install" | "upgrade", version: string, services: EngineServices) => number
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 = (): string => ctx.services.deps.version(tool)
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(ctx: Ctx, tool: string, mode: "install" | "upgrade", latest: string): { proceed: boolean; target: string } {
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 answer = promptLine(`Install ${tool} ${latest} anyway? [y/N] `)
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
@@ -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.sync(() => runEngineNative(args, { logger, deps, platform }))
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.14.3"
4
+ export const GENERATED_PACKAGE_VERSION = "0.14.4"
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
@@ -57,7 +57,7 @@ const root = Command.make("docks-kit", {}, () =>
57
57
  // parsed/normalized pickers, --flag value forms, and non-engine commands.
58
58
  if (process.env["DOCKS_KIT_ENGINE"] === "native-raw") {
59
59
  const { runEngineNative } = await import("./engine-native")
60
- process.exit(runEngineNative(process.argv.slice(2)))
60
+ process.exit(await runEngineNative(process.argv.slice(2)))
61
61
  }
62
62
 
63
63
  const cli = Command.run(root, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docks-kit",
3
- "version": "0.14.3",
3
+ "version": "0.14.4",
4
4
  "description": "Portable AI coding agent config kit — SoT sync engine + typed CLI for Claude Code, Codex, and universal agent skills",
5
5
  "type": "module",
6
6
  "license": "MIT",