docks-kit 0.1.5 → 0.2.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.
@@ -4,12 +4,13 @@
4
4
  */
5
5
  import { readFileSync, readSync } from "node:fs"
6
6
 
7
- import { capture, commandExists, isExecutable, p } from "./exec"
7
+ import type { ToolId } from "./deps"
8
+ import { p } from "./exec"
8
9
  import type { Ctx } from "./index"
9
10
  import { compareCodepoints, isObject, parseJson, type Json } from "./jq"
10
- import { echo, log, warn } from "./output"
11
+ import type { EngineServices } from "./services"
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
15
  function manifest(ctx: Ctx): { [k: string]: Json } {
15
16
  const doc = parseJson(readFileSync(p(ctx.repoDir, "SoT", "toolchain.json"), "utf8"))
@@ -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: string): boolean {
41
- if (tool === "bun") {
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: string): string {
57
- if (!present(ctx, tool)) return ""
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(capture("rtk", ["--version"]), 1)
54
+ return firstLineField(version(), 1)
61
55
  case "claude":
62
- return firstLineField(capture("claude", ["--version"]), 0)
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(capture("agent-browser", ["--version"]), -1)
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(capture("git", ["--version"]), 2)
61
+ return firstLineField(version(), 2)
77
62
  case "node":
78
- return capture("node", ["--version"]).replace(/^v/, "")
79
- case "npm":
80
- return capture("npm", ["--version"])
63
+ return version().replace(/^v/, "")
81
64
  case "jq":
82
- return capture("jq", ["--version"]).replace(/^jq-/, "")
65
+ return version().replace(/^jq-/, "")
83
66
  case "curl":
84
- return firstLineField(capture("curl", ["--version"]), 1)
85
67
  case "tsc":
86
- return firstLineField(capture("tsc", ["--version"]), 1)
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: string): string {
93
- switch (tool) {
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(prompt: string): string {
110
- process.stderr.write(prompt)
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 = readSync(0, buf, 0, 1, null)
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
- process.stderr.write(`\x1b[1;33m[warn]\x1b[0m ${tool} ${latest} is not kit-verified (verified: ${verified}).\n`)
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: string, installFn: InstallFn): number {
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
- log(`${tool} present (${installedLabel})`)
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
- log(`${tool} present (${installedLabel}; latest unknown — no action)`)
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
- log(`${tool} up to date (${installed})`)
198
+ verbose(`${tool} up to date (${installed})`)
219
199
  return 0
220
200
  }
221
201
 
@@ -225,9 +205,10 @@ 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 platformOs =
230
- process.platform === "linux" ? "linux" : process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : ""
210
+ const pn = ctx.services.platform.name()
211
+ const platformOs = pn === "unknown" ? "" : pn
231
212
  for (const tool of Object.keys(manifest(ctx)).sort(compareCodepoints)) {
232
213
  const os = field(ctx, tool, "os")
233
214
  if (os !== "" && platformOs !== "" && os !== platformOs) continue
@@ -241,8 +222,9 @@ export function report(ctx: Ctx): void {
241
222
  }
242
223
  let installed: string
243
224
  let status: string
244
- if (present(ctx, tool)) {
245
- installed = installedVersion(ctx, tool)
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 code = yield* Effect.sync(() => runEngineNative(args))
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
- process.stderr.write(`\x1b[1;33m[warn]\x1b[0m engine capture failed (${args.join(" ")} exited ${res.status ?? "spawn-error"})\n`)
55
+ services.logger.warn(`engine capture failed (${args.join(" ")} exited ${res.status ?? "spawn-error"})`)
47
56
  }
48
57
  return res.stdout ?? ""
49
58
  })
package/cli/src/main.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Command } from "@effect/cli"
3
3
  import { BunContext, BunRuntime } from "@effect/platform-bun"
4
- import { Console, Effect } from "effect"
4
+ import { Console, Effect, Layer } from "effect"
5
+ import { EngineServicesLive } from "./services"
5
6
  import { docsCommand } from "./commands/docs"
6
7
  import { modelCommand } from "./commands/model"
7
8
  import { modelsCommand } from "./commands/models"
@@ -70,4 +71,4 @@ const argv = process.argv.flatMap((a) =>
70
71
  : [a]
71
72
  )
72
73
 
73
- cli(argv).pipe(Effect.provide(BunContext.layer), BunRuntime.runMain)
74
+ cli(argv).pipe(Effect.provide(Layer.mergeAll(BunContext.layer, EngineServicesLive)), BunRuntime.runMain)
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Effect rim over the engine's injectable capabilities: Context.Tags with
3
+ * live layers built from the shared factory (engine-native/services.ts) plus
4
+ * named test-layer constructors. Composed ONCE at main.ts — command code
5
+ * accesses services via `yield*`, never by re-providing.
6
+ */
7
+ import { Context, Layer } from "effect"
8
+ import { makeLogger, type Logger, type LoggerSinks } from "./engine-native/logger"
9
+ import { makeEngineServices, makePlatform, type DependencyManager, type Platform } from "./engine-native/services"
10
+
11
+ export class LoggerService extends Context.Tag("docks-kit/Logger")<LoggerService, Logger>() {}
12
+
13
+ export class DependencyManagerService extends Context.Tag("docks-kit/DependencyManager")<
14
+ DependencyManagerService,
15
+ DependencyManager
16
+ >() {}
17
+
18
+ export class PlatformService extends Context.Tag("docks-kit/Platform")<PlatformService, Platform>() {}
19
+
20
+ const live = makeEngineServices()
21
+
22
+ export const LoggerLive = Layer.succeed(LoggerService, live.logger)
23
+ export const DependencyManagerLive = Layer.succeed(DependencyManagerService, live.deps)
24
+ export const PlatformLive = Layer.succeed(PlatformService, live.platform)
25
+ export const EngineServicesLive = Layer.mergeAll(LoggerLive, DependencyManagerLive, PlatformLive)
26
+
27
+ export const LoggerTest = (sinks: LoggerSinks): Layer.Layer<LoggerService> =>
28
+ Layer.succeed(LoggerService, makeLogger(sinks))
29
+
30
+ export const PlatformTest = (pf: NodeJS.Platform): Layer.Layer<PlatformService> =>
31
+ Layer.succeed(PlatformService, makePlatform(pf))
32
+
33
+ export const DependencyManagerTest = (impl: DependencyManager): Layer.Layer<DependencyManagerService> =>
34
+ Layer.succeed(DependencyManagerService, impl)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docks-kit",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
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",
@@ -1,20 +0,0 @@
1
- /**
2
- * Shared log/warn/err and dry-run emitters. Prefixes go to stderr with stable
3
- * ANSI codes; dry-run lines go to stdout.
4
- */
5
-
6
- export function log(msg: string): void {
7
- process.stderr.write(`\x1b[1;32m[ok]\x1b[0m ${msg}\n`)
8
- }
9
-
10
- export function warn(msg: string): void {
11
- process.stderr.write(`\x1b[1;33m[warn]\x1b[0m ${msg}\n`)
12
- }
13
-
14
- export function err(msg: string): void {
15
- process.stderr.write(`\x1b[1;31m[err]\x1b[0m ${msg}\n`)
16
- }
17
-
18
- export function echo(line: string): void {
19
- process.stdout.write(`${line}\n`)
20
- }