docks-kit 0.1.4 → 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,65 +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)
60
+ case "git":
61
+ return firstLineField(version(), 2)
75
62
  case "node":
76
- return capture("node", ["--version"]).replace(/^v/, "")
77
- case "npm":
78
- return capture("npm", ["--version"])
63
+ return version().replace(/^v/, "")
79
64
  case "jq":
80
- return capture("jq", ["--version"]).replace(/^jq-/, "")
65
+ return version().replace(/^jq-/, "")
81
66
  case "curl":
82
- return firstLineField(capture("curl", ["--version"]), 1)
83
67
  case "tsc":
84
- return firstLineField(capture("tsc", ["--version"]), 1)
68
+ return firstLineField(version(), 1)
69
+ case "bun":
70
+ case "effect-solutions":
71
+ case "npm":
72
+ return version()
85
73
  default:
86
74
  return ""
87
75
  }
88
76
  }
89
77
 
90
- export function latestVersion(tool: string): string {
91
- switch (tool) {
92
- case "rtk": {
93
- const body = capture("curl", ["-fsSL", "--max-time", "5", "https://api.github.com/repos/rtk-ai/rtk/releases/latest"])
94
- const doc = parseJson(body)
95
- const tag = doc !== undefined && isObject(doc) && typeof doc["tag_name"] === "string" ? doc["tag_name"] : ""
96
- return tag.replace(/^v/, "")
97
- }
98
- case "agent-browser":
99
- case "effect-solutions":
100
- return commandExists("npm") ? capture("npm", ["view", tool, "version"]) : ""
101
- default:
102
- return ""
103
- }
78
+ export function latestVersion(ctx: Ctx, tool: ToolId): string {
79
+ return ctx.services.deps.latest(tool)
104
80
  }
105
81
 
106
82
  /** Blocking TTY prompt matching bash `read -r -p` (prompt on stderr). */
107
- function promptLine(prompt: string): string {
108
- 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)
109
89
  const buf = Buffer.alloc(1)
110
90
  let line = ""
111
91
  for (;;) {
112
92
  let n: number
113
93
  try {
114
- n = readSync(0, buf, 0, 1, null)
94
+ n = readByte(buf)
115
95
  } catch {
116
96
  break
117
97
  }
@@ -125,6 +105,7 @@ function promptLine(prompt: string): string {
125
105
 
126
106
  /** toolchain::_gate — { proceed, target } ("" target = latest). */
127
107
  function gate(ctx: Ctx, tool: string, mode: "install" | "upgrade", latest: string): { proceed: boolean; target: string } {
108
+ const { warn } = ctx.services.logger
128
109
  const verified = field(ctx, tool, "verified")
129
110
  const pinnable = field(ctx, tool, "pinnable")
130
111
 
@@ -136,7 +117,7 @@ function gate(ctx: Ctx, tool: string, mode: "install" | "upgrade", latest: strin
136
117
  }
137
118
 
138
119
  if (process.stdin.isTTY === true) {
139
- 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}).`)
140
121
  const answer = promptLine(`Install ${tool} ${latest} anyway? [y/N] `)
141
122
  if (/^[yY]/.test(answer)) return { proceed: true, target: "" }
142
123
  }
@@ -151,11 +132,12 @@ function gate(ctx: Ctx, tool: string, mode: "install" | "upgrade", latest: strin
151
132
  return { proceed: false, target: "" }
152
133
  }
153
134
 
154
- 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
155
137
  const policy = field(ctx, tool, "policy")
156
138
 
157
139
  if (!present(ctx, tool)) {
158
- const latest = latestVersion(tool)
140
+ const latest = latestVersion(ctx, tool)
159
141
  if (ctx.dryRun) {
160
142
  echo(`[dry-run] would install ${tool} (${latest !== "" ? latest : "latest"}, gated by toolchain.json verified pin)`)
161
143
  return 0
@@ -174,7 +156,7 @@ export function ensure(ctx: Ctx, tool: string, installFn: InstallFn): number {
174
156
  if (!g.proceed) return 0
175
157
  target = g.target
176
158
  }
177
- return installFn("install", target !== "" ? target : latest)
159
+ return installFn("install", target !== "" ? target : latest, ctx.services)
178
160
  }
179
161
 
180
162
  const installed = installedVersion(ctx, tool)
@@ -185,17 +167,17 @@ export function ensure(ctx: Ctx, tool: string, installFn: InstallFn): number {
185
167
  echo(`[dry-run] ${tool} present (${installedLabel})`)
186
168
  return 0
187
169
  }
188
- log(`${tool} present (${installedLabel})`)
170
+ verbose(`${tool} present (${installedLabel})`)
189
171
  return 0
190
172
  }
191
173
 
192
- const latest = latestVersion(tool)
174
+ const latest = latestVersion(ctx, tool)
193
175
  if (latest === "") {
194
176
  if (ctx.dryRun) {
195
177
  echo(`[dry-run] ${tool} present (${installedLabel}); latest unknown (offline?) — no action`)
196
178
  return 0
197
179
  }
198
- log(`${tool} present (${installedLabel}; latest unknown — no action)`)
180
+ verbose(`${tool} present (${installedLabel}; latest unknown — no action)`)
199
181
  return 0
200
182
  }
201
183
 
@@ -206,14 +188,14 @@ export function ensure(ctx: Ctx, tool: string, installFn: InstallFn): number {
206
188
  }
207
189
  const g = gate(ctx, tool, "upgrade", latest)
208
190
  if (!g.proceed) return 0
209
- return installFn("upgrade", g.target !== "" ? g.target : latest)
191
+ return installFn("upgrade", g.target !== "" ? g.target : latest, ctx.services)
210
192
  }
211
193
 
212
194
  if (ctx.dryRun) {
213
195
  echo(`[dry-run] ${tool} up to date (${installed})`)
214
196
  return 0
215
197
  }
216
- log(`${tool} up to date (${installed})`)
198
+ verbose(`${tool} up to date (${installed})`)
217
199
  return 0
218
200
  }
219
201
 
@@ -223,9 +205,10 @@ function row(cells: [string, string, string, string, string, string]): string {
223
205
  }
224
206
 
225
207
  export function report(ctx: Ctx): void {
208
+ const { echo } = ctx.services.logger
226
209
  echo(row(["TOOL", "KIND", "INSTALLED", "FLOOR", "VERIFIED", "STATUS"]))
227
- const platformOs =
228
- 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
229
212
  for (const tool of Object.keys(manifest(ctx)).sort(compareCodepoints)) {
230
213
  const os = field(ctx, tool, "os")
231
214
  if (os !== "" && platformOs !== "" && os !== platformOs) continue
@@ -239,8 +222,9 @@ export function report(ctx: Ctx): void {
239
222
  }
240
223
  let installed: string
241
224
  let status: string
242
- if (present(ctx, tool)) {
243
- installed = installedVersion(ctx, tool)
225
+ const toolId = tool as ToolId
226
+ if (present(ctx, toolId)) {
227
+ installed = installedVersion(ctx, toolId)
244
228
  status = "ok"
245
229
  if (floor !== "" && installed !== "" && isNewer(floor, installed)) {
246
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.4",
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
- }