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.
Files changed (50) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +7 -5
  3. package/cli/docs/flags.md +1 -0
  4. package/cli/docs/install.md +9 -11
  5. package/cli/docs/overview.md +6 -0
  6. package/cli/docs/platforms.md +9 -10
  7. package/cli/src/commands/model.ts +7 -3
  8. package/cli/src/commands/sync.ts +14 -3
  9. package/cli/src/commands/toolchain.ts +7 -3
  10. package/cli/src/engine-native/DESIGN.md +79 -2
  11. package/cli/src/engine-native/claudeModel.ts +9 -3
  12. package/cli/src/engine-native/claudeSync.ts +220 -116
  13. package/cli/src/engine-native/codexSync.ts +131 -77
  14. package/cli/src/engine-native/codexToml.ts +12 -5
  15. package/cli/src/engine-native/deps.ts +325 -0
  16. package/cli/src/engine-native/exec.ts +35 -2
  17. package/cli/src/engine-native/index.ts +48 -13
  18. package/cli/src/engine-native/logger.ts +35 -0
  19. package/cli/src/engine-native/models.ts +22 -23
  20. package/cli/src/engine-native/modes.ts +30 -14
  21. package/cli/src/engine-native/os.ts +29 -0
  22. package/cli/src/engine-native/parseArgs.ts +19 -17
  23. package/cli/src/engine-native/services.ts +96 -0
  24. package/cli/src/engine-native/skillsSync.ts +77 -61
  25. package/cli/src/engine-native/toolchain.ts +50 -68
  26. package/cli/src/engine.ts +11 -2
  27. package/cli/src/generated/sotPayload.ts +41 -0
  28. package/cli/src/kitHome.ts +15 -11
  29. package/cli/src/main.ts +3 -2
  30. package/cli/src/manifests.ts +17 -15
  31. package/cli/src/payload.ts +28 -0
  32. package/cli/src/services.ts +34 -0
  33. package/docks-kit +6 -6
  34. package/package.json +2 -3
  35. package/SoT/.agents/skills.txt +0 -14
  36. package/SoT/.claude/CLAUDE.md +0 -146
  37. package/SoT/.claude/fetch-usage.sh +0 -66
  38. package/SoT/.claude/hooks/notify.sh +0 -14
  39. package/SoT/.claude/mcp-servers.json +0 -10
  40. package/SoT/.claude/settings.json +0 -235
  41. package/SoT/.claude/statusline.sh +0 -175
  42. package/SoT/.codex/AGENTS.md +0 -75
  43. package/SoT/.codex/agents/.gitkeep +0 -1
  44. package/SoT/.codex/config.toml +0 -45
  45. package/SoT/.codex/plugins/marketplace.json +0 -50
  46. package/SoT/.codex/rules/docks.rules +0 -116
  47. package/SoT/models.json +0 -28
  48. package/SoT/toolchain.json +0 -27
  49. package/cli/src/engine-native/output.ts +0 -20
  50. package/notification.mp3 +0 -0
@@ -4,23 +4,26 @@
4
4
  */
5
5
  import { p } from "./exec"
6
6
  import { readFileSync } from "node:fs"
7
+ import { payloadText } from "../payload"
7
8
 
8
9
  import { syncClaudeModel } from "./claudeModel"
9
10
  import { syncCodexModel } from "./codexToml"
10
11
  import type { Ctx } from "./index"
11
12
  import { isObject, parseJson, type Json } from "./jq"
12
13
  import { printModels, validateClaudeModel, validateCodexModel } from "./models"
13
- import { echo, err, warn } from "./output"
14
14
  import { rtkInstall } from "./claudeSync"
15
15
  import { agentBrowserInstall, bunBootstrap, effectSolutionsInstall } from "./skillsSync"
16
16
  import { ensure, report } from "./toolchain"
17
17
 
18
18
  export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
19
+ const { echo, err, warn } = ctx.services.logger
19
20
  let tool = ""
20
21
  let value = ""
21
22
  for (const arg of args) {
22
23
  if (arg === "--dry-run") ctx.dryRun = true
23
- else if (arg === "claude" || arg === "codex") tool = arg
24
+ else if (arg === "--verbose") {
25
+ ctx.verbose = true
26
+ } else if (arg === "claude" || arg === "codex") tool = arg
24
27
  else if (arg.startsWith("-")) {
25
28
  err(`Unknown flag for model: ${arg}`)
26
29
  return 2
@@ -39,7 +42,7 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
39
42
  return 0
40
43
  }
41
44
  echo(`deployed: ${jsonModelField(deployed)}`)
42
- echo(`SoT: ${jsonModelField(p(ctx.repoDir, "SoT", ".claude", "settings.json"))}`)
45
+ echo(`SoT: ${jsonModelText(payloadText("SoT/.claude/settings.json"))}`)
43
46
  } else {
44
47
  const deployed = p(ctx.home, ".codex", "config.toml")
45
48
  if (!fileReadable(deployed)) {
@@ -47,22 +50,22 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
47
50
  return 0
48
51
  }
49
52
  echo(`deployed: ${tomlModelField(deployed)}`)
50
- echo(`SoT: ${tomlModelField(p(ctx.repoDir, "SoT", ".codex", "config.toml"))}`)
53
+ echo(`SoT: ${tomlModelText(payloadText("SoT/.codex/config.toml"))}`)
51
54
  }
52
- printModels(ctx.repoDir, tool)
55
+ printModels(ctx, tool)
53
56
  return 0
54
57
  }
55
58
 
56
59
  if (tool === "claude") {
57
- if (!validateClaudeModel(ctx.repoDir, value)) {
58
- printModels(ctx.repoDir, "claude")
60
+ if (!validateClaudeModel(ctx, value)) {
61
+ printModels(ctx, "claude")
59
62
  err(`Invalid Claude model '${value}'`)
60
63
  return 2
61
64
  }
62
65
  syncClaudeModel(ctx, value)
63
66
  } else {
64
- if (!validateCodexModel(ctx.repoDir, value)) {
65
- printModels(ctx.repoDir, "codex")
67
+ if (!validateCodexModel(ctx, value)) {
68
+ printModels(ctx, "codex")
66
69
  err(`Invalid Codex model '${value}'`)
67
70
  return 2
68
71
  }
@@ -82,7 +85,11 @@ function fileReadable(p: string): boolean {
82
85
 
83
86
  /** `jq -r '.model // "default (unset)"'` — empty on unparseable input. */
84
87
  function jsonModelField(file: string): string {
85
- const doc = parseJson(readFileSync(file, "utf8"))
88
+ return jsonModelText(readFileSync(file, "utf8"))
89
+ }
90
+
91
+ function jsonModelText(text: string): string {
92
+ const doc = parseJson(text)
86
93
  if (doc === undefined) return ""
87
94
  const v: Json | undefined = isObject(doc) ? doc["model"] : undefined
88
95
  if (v === undefined || v === null || v === false) return "default (unset)"
@@ -91,17 +98,26 @@ function jsonModelField(file: string): string {
91
98
 
92
99
  /** `awk -F'"' '/^model[[:space:]]*=/{print $2; exit}'`. */
93
100
  function tomlModelField(file: string): string {
94
- for (const line of readFileSync(file, "utf8").split("\n")) {
101
+ return tomlModelText(readFileSync(file, "utf8"))
102
+ }
103
+
104
+ function tomlModelText(text: string): string {
105
+ for (const line of text.split("\n")) {
95
106
  if (/^model[ \t]*=/.test(line)) return line.split('"')[1] ?? ""
96
107
  }
97
108
  return ""
98
109
  }
99
110
 
100
111
  export function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): number {
101
- const op = args[0] ?? "check"
102
- const tool = args[1] ?? ""
112
+ const { err } = ctx.services.logger
113
+ const words = args.filter((a) => !a.startsWith("--"))
114
+ const op = words[0] ?? args[0] ?? "check"
115
+ const tool = words[1] ?? args[1] ?? ""
103
116
  for (const arg of args) {
104
117
  if (arg === "--yes") ctx.assumeYes = true
118
+ else if (arg === "--verbose") {
119
+ ctx.verbose = true
120
+ }
105
121
  }
106
122
 
107
123
  if (op === "check") {
@@ -121,7 +137,7 @@ export function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): number {
121
137
  return ensure(ctx, "rtk", rtkInstall(ctx))
122
138
  case "bun":
123
139
  // skills::_bun_bootstrap >/dev/null — the found-bun stdout is discarded.
124
- return bunBootstrap(ctx) !== "" ? 0 : 1
140
+ return bunBootstrap(ctx, ctx.services) !== "" ? 0 : 1
125
141
  case "effect-solutions":
126
142
  return ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
127
143
  case "agent-browser":
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Platform capability seam (Output Policy in DESIGN.md) — the only engine
3
+ * module that reads process.platform besides exec.ts's PATH/executability
4
+ * primitives. Per-tool package identifiers stay in deps.ts; symlink handling
5
+ * stays try-then-fallback at the call site (capability-driven, not predicted).
6
+ */
7
+
8
+ export type PlatformName = "linux" | "darwin" | "windows" | "unknown"
9
+
10
+ export function rawPlatform(): NodeJS.Platform {
11
+ return process.platform
12
+ }
13
+
14
+ export function platformName(pf: NodeJS.Platform = rawPlatform()): PlatformName {
15
+ return pf === "linux" ? "linux" : pf === "darwin" ? "darwin" : pf === "win32" ? "windows" : "unknown"
16
+ }
17
+
18
+ export function isWindows(): boolean {
19
+ return rawPlatform() === "win32"
20
+ }
21
+
22
+ export function isLinux(): boolean {
23
+ return rawPlatform() === "linux"
24
+ }
25
+
26
+ /** Shell-rc exports (bashrc/zshrc) apply only off Windows. */
27
+ export function shellRcApplicable(): boolean {
28
+ return !isWindows()
29
+ }
@@ -4,10 +4,8 @@
4
4
  * early parser exit and is caught once in runEngineNative.
5
5
  */
6
6
 
7
- import { commandExists } from "./exec"
8
7
  import type { Ctx } from "./index"
9
8
  import { printModels, validateClaudeModel, validateCodexModel } from "./models"
10
- import { echo, err, warn } from "./output"
11
9
 
12
10
  export class ExitError extends Error {
13
11
  constructor(readonly code: number) {
@@ -18,6 +16,7 @@ export class ExitError extends Error {
18
16
  const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"]
19
17
 
20
18
  function usage(ctx: Ctx): void {
19
+ const { echo } = ctx.services.logger
21
20
  const argv0 = "docks-kit sync"
22
21
  echo(`Usage: ${argv0} [claude] [codex] [agents] [flags]`)
23
22
  echo("")
@@ -36,6 +35,7 @@ function usage(ctx: Ctx): void {
36
35
  )
37
36
  echo(" --skip-rtk skip optional tool bootstrap (RTK, bubblewrap)")
38
37
  echo(" --yes auto-accept toolchain prompts (containers/CI)")
38
+ echo(" --verbose also print no-op confirmations (already in sync, up to date, left as-is)")
39
39
  echo("")
40
40
  echo("Deploy-time modifiers (deployed config only; SoT untouched; a later flag-less sync reverts)")
41
41
  echo(
@@ -66,6 +66,7 @@ export function parseCompactWindow(v: string): string | undefined {
66
66
  }
67
67
 
68
68
  function addClaudePlugin(ctx: Ctx, name: string): void {
69
+ const { err } = ctx.services.logger
69
70
  if (!KNOWN_CLAUDE_OPTIN_PLUGINS.includes(name)) {
70
71
  err(`Unknown opt-in plugin '${name}'. Known: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join(", ")}`)
71
72
  throw new ExitError(2)
@@ -81,6 +82,7 @@ function selectTarget(ctx: Ctx, target: string): void {
81
82
  }
82
83
 
83
84
  export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
85
+ const { err } = ctx.services.logger
84
86
  for (const arg of args) {
85
87
  switch (arg) {
86
88
  case "claude":
@@ -103,12 +105,15 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
103
105
  case "--yes":
104
106
  ctx.assumeYes = true
105
107
  continue
108
+ case "--verbose":
109
+ ctx.verbose = true
110
+ continue
106
111
  case "--claude-model":
107
- printModels(ctx.repoDir, "claude")
112
+ printModels(ctx, "claude")
108
113
  err("--claude-model requires a value: --claude-model=<model>")
109
114
  throw new ExitError(2)
110
115
  case "--codex-model":
111
- printModels(ctx.repoDir, "codex")
116
+ printModels(ctx, "codex")
112
117
  err("--codex-model requires a value: --codex-model=<model>")
113
118
  throw new ExitError(2)
114
119
  case "--claude-compact-window":
@@ -180,32 +185,29 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
180
185
  }
181
186
 
182
187
  export function preflight(ctx: Ctx): void {
188
+ const { err } = ctx.services.logger
183
189
  if (ctx.syncClaude || ctx.syncCodex) {
184
- if (!commandExists("jq")) {
185
- const hint = process.platform === "win32"
186
- ? "winget install jqlang.jq (then open a new terminal)"
187
- : process.platform === "darwin"
188
- ? "brew install jq"
189
- : "sudo apt install -y jq"
190
- err(`jq is required (deployed statusline/hooks call it). Install: ${hint}`)
190
+ if (ctx.services.deps.probe("jq").state === "missing") {
191
+ err(`jq is required (deployed statusline/hooks call it). Install: ${ctx.services.deps.spec("jq").installHint()}`)
191
192
  throw new ExitError(1)
192
193
  }
193
194
  }
194
195
  if (ctx.syncClaude) {
195
- if (!commandExists("curl")) {
196
- err("curl is required.")
196
+ if (ctx.services.deps.probe("curl").state === "missing") {
197
+ err(`curl is required. Install: ${ctx.services.deps.spec("curl").installHint()}`)
197
198
  throw new ExitError(1)
198
199
  }
199
200
  }
200
201
  }
201
202
 
202
203
  export function validateModelFlags(ctx: Ctx): void {
204
+ const { err, warn } = ctx.services.logger
203
205
  if (ctx.claudeModel !== "") {
204
206
  if (!ctx.syncClaude) {
205
207
  warn("--claude-model ignored: claude target not selected")
206
208
  ctx.claudeModel = ""
207
- } else if (!validateClaudeModel(ctx.repoDir, ctx.claudeModel)) {
208
- printModels(ctx.repoDir, "claude")
209
+ } else if (!validateClaudeModel(ctx, ctx.claudeModel)) {
210
+ printModels(ctx, "claude")
209
211
  err(`Invalid Claude model '${ctx.claudeModel}' — use an alias above or a full claude-* ID`)
210
212
  throw new ExitError(2)
211
213
  }
@@ -214,8 +216,8 @@ export function validateModelFlags(ctx: Ctx): void {
214
216
  if (!ctx.syncCodex) {
215
217
  warn("--codex-model ignored: codex target not selected")
216
218
  ctx.codexModel = ""
217
- } else if (!validateCodexModel(ctx.repoDir, ctx.codexModel)) {
218
- printModels(ctx.repoDir, "codex")
219
+ } else if (!validateCodexModel(ctx, ctx.codexModel)) {
220
+ printModels(ctx, "codex")
219
221
  err(`Invalid Codex model '${ctx.codexModel}' — must match ^[A-Za-z0-9._-]+$`)
220
222
  throw new ExitError(2)
221
223
  }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Shared service factory — the single construction point for the engine's
3
+ * injectable capabilities (Output Policy in DESIGN.md). The Effect rim wraps
4
+ * these in Layers (cli/src/services.ts); the harness-private native-raw entry
5
+ * and engineCapture's parent-side warn call it directly, so every execution
6
+ * path shares one implementation.
7
+ */
8
+ import {
9
+ DEPENDENCIES,
10
+ defaultProbeExecutor,
11
+ resolveDependency,
12
+ resolveLocation,
13
+ resolvePath,
14
+ resolveVersion,
15
+ type DependencySpec,
16
+ type DependencyLocation,
17
+ type ProbeExecutor,
18
+ type ProbeResult,
19
+ type ToolId
20
+ } from "./deps"
21
+ import { makeLogger, type Logger, type LoggerSinks } from "./logger"
22
+ import { platformName, rawPlatform, type PlatformName } from "./os"
23
+
24
+ export type { Logger } from "./logger"
25
+
26
+ export interface DependencyManager {
27
+ readonly spec: (id: ToolId) => DependencySpec
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
33
+ readonly warnMissing: (id: ToolId, logger: Logger, context?: string) => void
34
+ }
35
+
36
+ export interface Platform {
37
+ readonly raw: () => NodeJS.Platform
38
+ readonly name: () => PlatformName
39
+ readonly isWindows: () => boolean
40
+ readonly isLinux: () => boolean
41
+ readonly shellRcApplicable: () => boolean
42
+ }
43
+
44
+ export interface EngineServices {
45
+ readonly logger: Logger
46
+ readonly deps: DependencyManager
47
+ readonly platform: Platform
48
+ }
49
+
50
+ export interface EngineServiceOptions {
51
+ readonly sinks?: LoggerSinks
52
+ }
53
+
54
+ /** Platform view over an injectable platform id (tests pass e.g. "win32"). */
55
+ export const makePlatform = (pf: NodeJS.Platform = rawPlatform()): Platform => ({
56
+ raw: () => pf,
57
+ name: () => platformName(pf),
58
+ isWindows: () => pf === "win32",
59
+ isLinux: () => pf === "linux",
60
+ shellRcApplicable: () => pf !== "win32"
61
+ })
62
+
63
+ /** DependencyManager whose hints default to the INJECTED platform, not the host. */
64
+ export const makeDependencyManager = (
65
+ platform: Platform,
66
+ exec: ProbeExecutor = defaultProbeExecutor
67
+ ): DependencyManager => {
68
+ const warned = new Set<ToolId>()
69
+ return {
70
+ spec: (id) => {
71
+ const s = DEPENDENCIES[id]
72
+ return { ...s, installHint: (pf = platform.raw()) => s.installHint(pf) }
73
+ },
74
+ probe: (id) => resolveDependency(DEPENDENCIES[id], exec, platform.raw()),
75
+ version: (id) => resolveVersion(DEPENDENCIES[id], exec),
76
+ path: (id) => resolvePath(DEPENDENCIES[id], exec, platform.raw()),
77
+ location: (id) => resolveLocation(DEPENDENCIES[id], exec, platform.raw()),
78
+ latest: (id) => DEPENDENCIES[id].latest?.(exec) ?? "",
79
+ warnMissing: (id, logger, context) => {
80
+ if (warned.has(id)) return
81
+ warned.add(id)
82
+ const suffix = context !== undefined && context !== "" ? ` (${context})` : ""
83
+ logger.warn(`${id} not installed — ${DEPENDENCIES[id].installHint(platform.raw())}${suffix}`)
84
+ }
85
+ }
86
+ }
87
+
88
+ export const makeEngineServices = (opts?: EngineServiceOptions): EngineServices => {
89
+ const platform = makePlatform()
90
+ const logger = makeLogger(opts?.sinks ?? {})
91
+ return {
92
+ logger,
93
+ deps: makeDependencyManager(platform),
94
+ platform
95
+ }
96
+ }