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.
- package/AGENTS.md +1 -1
- package/README.md +7 -5
- package/cli/docs/flags.md +1 -0
- package/cli/docs/install.md +9 -11
- package/cli/docs/overview.md +6 -0
- package/cli/docs/platforms.md +9 -10
- package/cli/src/commands/model.ts +7 -3
- package/cli/src/commands/sync.ts +14 -3
- package/cli/src/commands/toolchain.ts +7 -3
- package/cli/src/engine-native/DESIGN.md +79 -2
- package/cli/src/engine-native/claudeModel.ts +9 -3
- package/cli/src/engine-native/claudeSync.ts +220 -116
- package/cli/src/engine-native/codexSync.ts +131 -77
- package/cli/src/engine-native/codexToml.ts +12 -5
- package/cli/src/engine-native/deps.ts +325 -0
- package/cli/src/engine-native/exec.ts +35 -2
- package/cli/src/engine-native/index.ts +48 -13
- package/cli/src/engine-native/logger.ts +35 -0
- package/cli/src/engine-native/models.ts +22 -23
- package/cli/src/engine-native/modes.ts +30 -14
- package/cli/src/engine-native/os.ts +29 -0
- package/cli/src/engine-native/parseArgs.ts +19 -17
- package/cli/src/engine-native/services.ts +96 -0
- package/cli/src/engine-native/skillsSync.ts +77 -61
- package/cli/src/engine-native/toolchain.ts +50 -68
- package/cli/src/engine.ts +11 -2
- package/cli/src/generated/sotPayload.ts +41 -0
- package/cli/src/kitHome.ts +15 -11
- package/cli/src/main.ts +3 -2
- package/cli/src/manifests.ts +17 -15
- package/cli/src/payload.ts +28 -0
- package/cli/src/services.ts +34 -0
- package/docks-kit +6 -6
- package/package.json +2 -3
- package/SoT/.agents/skills.txt +0 -14
- package/SoT/.claude/CLAUDE.md +0 -146
- package/SoT/.claude/fetch-usage.sh +0 -66
- package/SoT/.claude/hooks/notify.sh +0 -14
- package/SoT/.claude/mcp-servers.json +0 -10
- package/SoT/.claude/settings.json +0 -235
- package/SoT/.claude/statusline.sh +0 -175
- package/SoT/.codex/AGENTS.md +0 -75
- package/SoT/.codex/agents/.gitkeep +0 -1
- package/SoT/.codex/config.toml +0 -45
- package/SoT/.codex/plugins/marketplace.json +0 -50
- package/SoT/.codex/rules/docks.rules +0 -116
- package/SoT/models.json +0 -28
- package/SoT/toolchain.json +0 -27
- package/cli/src/engine-native/output.ts +0 -20
- package/notification.mp3 +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DependencyManager — one home for external-tool identity, presence probing,
|
|
3
|
+
* and platform-correct install hints (Output Policy in DESIGN.md).
|
|
4
|
+
*
|
|
5
|
+
* Ownership split: SoT/toolchain.json + toolchain.ts keep version floors,
|
|
6
|
+
* pin policy, and managed install/upgrade orchestration; this registry owns
|
|
7
|
+
* WHICH external tools exist, whether they are required, and the one-line
|
|
8
|
+
* command that installs a missing one.
|
|
9
|
+
*/
|
|
10
|
+
import { homedir } from "node:os"
|
|
11
|
+
import { existsSync, readdirSync } from "node:fs"
|
|
12
|
+
|
|
13
|
+
import { capture, commandExists, p, which } from "./exec"
|
|
14
|
+
import { isObject, parseJson } from "./jq"
|
|
15
|
+
import { rawPlatform } from "./os"
|
|
16
|
+
|
|
17
|
+
export type ToolId =
|
|
18
|
+
| "git"
|
|
19
|
+
| "jq"
|
|
20
|
+
| "curl"
|
|
21
|
+
| "node"
|
|
22
|
+
| "npm"
|
|
23
|
+
| "npx"
|
|
24
|
+
| "claude"
|
|
25
|
+
| "codex"
|
|
26
|
+
| "rtk"
|
|
27
|
+
| "bun"
|
|
28
|
+
| "bwrap"
|
|
29
|
+
| "agent-browser"
|
|
30
|
+
| "effect-solutions"
|
|
31
|
+
| "chrome-for-testing"
|
|
32
|
+
| "ffplay"
|
|
33
|
+
| "intelephense"
|
|
34
|
+
| "typescript-language-server"
|
|
35
|
+
| "tsc"
|
|
36
|
+
| "apt-get"
|
|
37
|
+
| "dnf"
|
|
38
|
+
| "pacman"
|
|
39
|
+
| "zypper"
|
|
40
|
+
|
|
41
|
+
/** required = the engine aborts when missing; optional = warn + degrade. */
|
|
42
|
+
export type Requirement = "required" | "optional"
|
|
43
|
+
|
|
44
|
+
export type ProbeResult =
|
|
45
|
+
| { readonly state: "present"; readonly path?: string }
|
|
46
|
+
| { readonly state: "missing" }
|
|
47
|
+
|
|
48
|
+
export interface DependencyLocation {
|
|
49
|
+
readonly path: string
|
|
50
|
+
readonly binDir: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ProbeExecutor {
|
|
54
|
+
readonly commandExists: (name: string) => boolean
|
|
55
|
+
readonly capture: (cmd: string, args: ReadonlyArray<string>) => string
|
|
56
|
+
readonly which: (name: string) => string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DependencySpec {
|
|
60
|
+
readonly id: ToolId
|
|
61
|
+
readonly requirement: Requirement
|
|
62
|
+
readonly versionArgs: ReadonlyArray<string>
|
|
63
|
+
/** Platform-correct one-line install command (param injectable for tests). */
|
|
64
|
+
readonly installHint: (platform?: NodeJS.Platform) => string
|
|
65
|
+
readonly resolve?: (exec: ProbeExecutor, platform: NodeJS.Platform) => ProbeResult
|
|
66
|
+
readonly version?: (exec: ProbeExecutor) => string
|
|
67
|
+
readonly locate?: (exec: ProbeExecutor, platform: NodeJS.Platform) => DependencyLocation
|
|
68
|
+
readonly latest?: (exec: ProbeExecutor) => string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface SpecOptions {
|
|
72
|
+
readonly versionArgs?: ReadonlyArray<string>
|
|
73
|
+
readonly resolve?: (exec: ProbeExecutor, platform: NodeJS.Platform) => ProbeResult
|
|
74
|
+
readonly version?: (exec: ProbeExecutor) => string
|
|
75
|
+
readonly locate?: (exec: ProbeExecutor, platform: NodeJS.Platform) => DependencyLocation
|
|
76
|
+
readonly latest?: (exec: ProbeExecutor) => string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const spec = (
|
|
80
|
+
id: ToolId,
|
|
81
|
+
requirement: Requirement,
|
|
82
|
+
installHint: (platform?: NodeJS.Platform) => string,
|
|
83
|
+
options: SpecOptions = {}
|
|
84
|
+
): DependencySpec => ({
|
|
85
|
+
id,
|
|
86
|
+
requirement,
|
|
87
|
+
versionArgs: options.versionArgs ?? ["--version"],
|
|
88
|
+
installHint,
|
|
89
|
+
resolve: options.resolve,
|
|
90
|
+
version: options.version,
|
|
91
|
+
locate: options.locate,
|
|
92
|
+
latest: options.latest
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const pathProbe = (id: string): ((exec: ProbeExecutor) => ProbeResult) =>
|
|
96
|
+
(exec) =>
|
|
97
|
+
exec.commandExists(id)
|
|
98
|
+
? { state: "present", path: exec.which(id) }
|
|
99
|
+
: { state: "missing" }
|
|
100
|
+
|
|
101
|
+
const versionProbe = (
|
|
102
|
+
id: string,
|
|
103
|
+
versionArgs: ReadonlyArray<string> = ["--version"],
|
|
104
|
+
parse: (out: string) => string = (out) => out
|
|
105
|
+
): ((exec: ProbeExecutor) => string) =>
|
|
106
|
+
(exec) => parse(exec.capture(id, versionArgs))
|
|
107
|
+
|
|
108
|
+
const home = (): string => {
|
|
109
|
+
const envHome = process.env["HOME"]
|
|
110
|
+
return envHome !== undefined && envHome !== "" ? envHome : homedir()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const findBun = (exec: ProbeExecutor): { command: string; path: string } | undefined => {
|
|
114
|
+
const onPath = exec.which("bun")
|
|
115
|
+
if (onPath !== "") return { command: "bun", path: onPath }
|
|
116
|
+
const root =
|
|
117
|
+
process.env["BUN_INSTALL"] !== undefined && process.env["BUN_INSTALL"] !== ""
|
|
118
|
+
? process.env["BUN_INSTALL"]!
|
|
119
|
+
: p(home(), ".bun")
|
|
120
|
+
for (const candidate of [p(root, "bin", "bun"), p(home(), ".bun", "bin", "bun")]) {
|
|
121
|
+
if (exec.which(candidate) !== "") return { command: candidate, path: candidate }
|
|
122
|
+
}
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const resolveBun = (exec: ProbeExecutor): ProbeResult => {
|
|
127
|
+
const bun = findBun(exec)
|
|
128
|
+
return bun === undefined
|
|
129
|
+
? { state: "missing" }
|
|
130
|
+
: { state: "present", path: bun.path }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const resolveEffectSolutions = (exec: ProbeExecutor): ProbeResult => {
|
|
134
|
+
return exec.commandExists("effect-solutions")
|
|
135
|
+
? { state: "present", path: exec.which("effect-solutions") }
|
|
136
|
+
: { state: "missing" }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const versionBunCommand = (exec: ProbeExecutor): string =>
|
|
140
|
+
exec.commandExists("bun") ? "bun" : p(home(), ".bun", "bin", "bun")
|
|
141
|
+
|
|
142
|
+
const versionEffectSolutions = (exec: ProbeExecutor): string => {
|
|
143
|
+
const bun = versionBunCommand(exec)
|
|
144
|
+
if (bun !== "bun" && exec.which(bun) === "") return ""
|
|
145
|
+
const match = /effect-solutions@([0-9][0-9.]*)/.exec(exec.capture(bun, ["pm", "-g", "ls"]))
|
|
146
|
+
return match?.[1] ?? ""
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const locateEffectSolutions = (exec: ProbeExecutor, platform: NodeJS.Platform): DependencyLocation => {
|
|
150
|
+
const bun = findBun(exec)
|
|
151
|
+
if (bun === undefined) return { path: "", binDir: "" }
|
|
152
|
+
const globalBin = exec.capture(bun.command, ["pm", "-g", "bin"])
|
|
153
|
+
const names =
|
|
154
|
+
platform === "win32"
|
|
155
|
+
? ["effect-solutions.exe", "effect-solutions.cmd", "effect-solutions.bunx"]
|
|
156
|
+
: ["effect-solutions"]
|
|
157
|
+
const path = names.map((name) => p(globalBin, name)).find((candidate) => globalBin !== "" && exec.which(candidate) !== "")
|
|
158
|
+
return { path: path ?? "", binDir: globalBin }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const resolveChrome = (exec: ProbeExecutor, platform: NodeJS.Platform): ProbeResult => {
|
|
162
|
+
const root = p(home(), ".agent-browser", "browsers")
|
|
163
|
+
const relative =
|
|
164
|
+
platform === "win32"
|
|
165
|
+
? "chrome.exe"
|
|
166
|
+
: platform === "darwin"
|
|
167
|
+
? "Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
|
|
168
|
+
: "chrome"
|
|
169
|
+
if (existsSync(root)) {
|
|
170
|
+
for (const directory of readdirSync(root).filter((name) => name.startsWith("chrome-")).sort().reverse()) {
|
|
171
|
+
const path = exec.which(p(root, directory, relative))
|
|
172
|
+
if (path !== "") return { state: "present", path }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const command of ["chrome-for-testing", "google-chrome-for-testing", "google-chrome", "chromium", "chromium-browser", "brave-browser", "brave"]) {
|
|
176
|
+
const path = exec.which(command)
|
|
177
|
+
if (path !== "") return { state: "present", path }
|
|
178
|
+
}
|
|
179
|
+
return { state: "missing" }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const latestRtk = (exec: ProbeExecutor): string => {
|
|
183
|
+
const doc = parseJson(
|
|
184
|
+
exec.capture("curl", ["-fsSL", "--max-time", "5", "https://api.github.com/repos/rtk-ai/rtk/releases/latest"])
|
|
185
|
+
)
|
|
186
|
+
const tag = doc !== undefined && isObject(doc) && typeof doc["tag_name"] === "string" ? doc["tag_name"] : ""
|
|
187
|
+
return tag.replace(/^v/, "")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const latestNpm = (id: "agent-browser" | "effect-solutions") => (exec: ProbeExecutor): string =>
|
|
191
|
+
exec.commandExists("npm") ? exec.capture("npm", ["view", id, "version"]) : ""
|
|
192
|
+
|
|
193
|
+
export const defaultProbeExecutor: ProbeExecutor = { commandExists, capture, which }
|
|
194
|
+
|
|
195
|
+
export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
196
|
+
git: spec(
|
|
197
|
+
"git",
|
|
198
|
+
"optional",
|
|
199
|
+
(pf = rawPlatform()) =>
|
|
200
|
+
pf === "win32"
|
|
201
|
+
? "winget install Git.Git (then open a new terminal)"
|
|
202
|
+
: pf === "darwin"
|
|
203
|
+
? "brew install git"
|
|
204
|
+
: "sudo apt install -y git (or your distro's package manager)",
|
|
205
|
+
{ version: versionProbe("git") }
|
|
206
|
+
),
|
|
207
|
+
jq: spec("jq", "required", (pf = rawPlatform()) =>
|
|
208
|
+
pf === "win32"
|
|
209
|
+
? "winget install jqlang.jq (then open a new terminal)"
|
|
210
|
+
: pf === "darwin"
|
|
211
|
+
? "brew install jq"
|
|
212
|
+
: "sudo apt install -y jq",
|
|
213
|
+
{ version: versionProbe("jq") }
|
|
214
|
+
),
|
|
215
|
+
curl: spec("curl", "required", (pf = rawPlatform()) =>
|
|
216
|
+
pf === "win32" ? "winget install cURL.cURL" : pf === "darwin" ? "brew install curl" : "sudo apt install -y curl",
|
|
217
|
+
{ version: versionProbe("curl") }
|
|
218
|
+
),
|
|
219
|
+
node: spec("node", "optional", () => "install Node.js via https://nodejs.org (or your package manager)", {
|
|
220
|
+
version: versionProbe("node")
|
|
221
|
+
}),
|
|
222
|
+
npm: spec("npm", "optional", () => "ships with Node.js — install via https://nodejs.org (or your package manager)"),
|
|
223
|
+
npx: spec("npx", "optional", () => "ships with Node.js — install via https://nodejs.org (or your package manager)"),
|
|
224
|
+
claude: spec(
|
|
225
|
+
"claude",
|
|
226
|
+
"optional",
|
|
227
|
+
(pf = rawPlatform()) =>
|
|
228
|
+
pf === "win32"
|
|
229
|
+
? "winget install Anthropic.ClaudeCode"
|
|
230
|
+
: "curl -fsSL https://claude.ai/install.sh -o /tmp/claude-install.sh && bash /tmp/claude-install.sh",
|
|
231
|
+
{ version: versionProbe("claude") }
|
|
232
|
+
),
|
|
233
|
+
codex: spec(
|
|
234
|
+
"codex",
|
|
235
|
+
"optional",
|
|
236
|
+
(pf = rawPlatform()) =>
|
|
237
|
+
pf === "win32"
|
|
238
|
+
? `powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"`
|
|
239
|
+
: 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"',
|
|
240
|
+
{ version: versionProbe("codex") }
|
|
241
|
+
),
|
|
242
|
+
rtk: spec("rtk", "optional", () => "see https://github.com/rtk-ai/rtk (kit auto-install is Unix-only)", {
|
|
243
|
+
version: versionProbe("rtk"),
|
|
244
|
+
latest: latestRtk
|
|
245
|
+
}),
|
|
246
|
+
bun: spec(
|
|
247
|
+
"bun",
|
|
248
|
+
"optional",
|
|
249
|
+
(pf = rawPlatform()) =>
|
|
250
|
+
pf === "win32" ? `powershell -c "irm bun.sh/install.ps1 | iex"` : "curl -fsSL https://bun.sh/install | bash",
|
|
251
|
+
{
|
|
252
|
+
resolve: resolveBun,
|
|
253
|
+
version: (exec) => exec.capture(versionBunCommand(exec), ["--version"]),
|
|
254
|
+
locate: (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
|
|
255
|
+
}
|
|
256
|
+
),
|
|
257
|
+
bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)"),
|
|
258
|
+
"agent-browser": spec("agent-browser", "optional", () => "npm install -g agent-browser", {
|
|
259
|
+
version: versionProbe("agent-browser"),
|
|
260
|
+
latest: latestNpm("agent-browser")
|
|
261
|
+
}),
|
|
262
|
+
"effect-solutions": spec("effect-solutions", "optional", () => "bun add -g effect-solutions", {
|
|
263
|
+
resolve: resolveEffectSolutions,
|
|
264
|
+
version: versionEffectSolutions,
|
|
265
|
+
locate: locateEffectSolutions,
|
|
266
|
+
latest: latestNpm("effect-solutions")
|
|
267
|
+
}),
|
|
268
|
+
"chrome-for-testing": spec(
|
|
269
|
+
"chrome-for-testing",
|
|
270
|
+
"optional",
|
|
271
|
+
(pf = rawPlatform()) => (pf === "linux" ? "agent-browser install --with-deps" : "agent-browser install"),
|
|
272
|
+
{ resolve: resolveChrome }
|
|
273
|
+
),
|
|
274
|
+
ffplay: spec(
|
|
275
|
+
"ffplay",
|
|
276
|
+
"optional",
|
|
277
|
+
(pf = rawPlatform()) =>
|
|
278
|
+
pf === "win32" ? "winget install Gyan.FFmpeg" : pf === "darwin" ? "brew install ffmpeg" : "sudo apt install -y ffmpeg",
|
|
279
|
+
{ versionArgs: ["-version"], resolve: pathProbe("ffplay") }
|
|
280
|
+
),
|
|
281
|
+
intelephense: spec("intelephense", "optional", () => "npm install -g intelephense", {
|
|
282
|
+
resolve: pathProbe("intelephense")
|
|
283
|
+
}),
|
|
284
|
+
"typescript-language-server": spec(
|
|
285
|
+
"typescript-language-server",
|
|
286
|
+
"optional",
|
|
287
|
+
() => "npm install -g typescript-language-server typescript",
|
|
288
|
+
{ resolve: pathProbe("typescript-language-server") }
|
|
289
|
+
),
|
|
290
|
+
tsc: spec("tsc", "optional", () => "npm install -g typescript", {
|
|
291
|
+
resolve: pathProbe("tsc"),
|
|
292
|
+
version: versionProbe("tsc")
|
|
293
|
+
}),
|
|
294
|
+
"apt-get": spec("apt-get", "optional", () => "install apt via your Linux distribution"),
|
|
295
|
+
dnf: spec("dnf", "optional", () => "install dnf via your Linux distribution"),
|
|
296
|
+
pacman: spec("pacman", "optional", () => "install pacman via your Linux distribution"),
|
|
297
|
+
zypper: spec("zypper", "optional", () => "install zypper via your Linux distribution")
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function resolveDependency(
|
|
301
|
+
specification: DependencySpec,
|
|
302
|
+
exec: ProbeExecutor,
|
|
303
|
+
platform: NodeJS.Platform = rawPlatform()
|
|
304
|
+
): ProbeResult {
|
|
305
|
+
return (specification.resolve ?? pathProbe(specification.id))(exec, platform)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function resolveVersion(specification: DependencySpec, exec: ProbeExecutor): string {
|
|
309
|
+
if (resolveDependency(specification, exec).state !== "present") return ""
|
|
310
|
+
return (specification.version ?? versionProbe(specification.id, specification.versionArgs))(exec)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function resolveLocation(
|
|
314
|
+
specification: DependencySpec,
|
|
315
|
+
exec: ProbeExecutor,
|
|
316
|
+
platform: NodeJS.Platform = rawPlatform()
|
|
317
|
+
): DependencyLocation {
|
|
318
|
+
if (specification.locate !== undefined) return specification.locate(exec, platform)
|
|
319
|
+
const result = resolveDependency(specification, exec, platform)
|
|
320
|
+
return { path: result.state === "present" ? (result.path ?? exec.which(specification.id)) : "", binDir: "" }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function resolvePath(specification: DependencySpec, exec: ProbeExecutor, platform?: NodeJS.Platform): string {
|
|
324
|
+
return resolveLocation(specification, exec, platform).path
|
|
325
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* substitution: stdout with trailing newlines stripped, empty on failure.
|
|
5
5
|
*/
|
|
6
6
|
import { spawnSync } from "node:child_process"
|
|
7
|
-
import { accessSync, constants, existsSync, statSync } from "node:fs"
|
|
8
|
-
import { delimiter, join } from "node:path"
|
|
7
|
+
import { accessSync, chmodSync, constants, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
8
|
+
import { delimiter, isAbsolute, join } from "node:path"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Engine paths are built with "/" because they appear verbatim in output
|
|
@@ -24,6 +24,9 @@ export function capture(cmd: string, args: ReadonlyArray<string>): string {
|
|
|
24
24
|
|
|
25
25
|
/** `command -v` — resolve a name on PATH (PATHEXT-aware on Windows). */
|
|
26
26
|
export function which(name: string): string {
|
|
27
|
+
if (isAbsolute(name) || name.includes("/") || name.includes("\\")) {
|
|
28
|
+
return isExecutable(name) ? name : ""
|
|
29
|
+
}
|
|
27
30
|
const exts = process.platform === "win32" ? (process.env["PATHEXT"] ?? ".EXE;.CMD;.BAT;.COM").split(";").concat("") : [""]
|
|
28
31
|
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
29
32
|
if (dir === "") continue
|
|
@@ -52,3 +55,33 @@ export function isExecutable(p: string): boolean {
|
|
|
52
55
|
export function fileExists(p: string): boolean {
|
|
53
56
|
return existsSync(p)
|
|
54
57
|
}
|
|
58
|
+
|
|
59
|
+
// Change-detection primitives (Output Policy in DESIGN.md): operations report
|
|
60
|
+
// changed:boolean so unchanged repeat runs log at verbose instead of [ok].
|
|
61
|
+
|
|
62
|
+
/** Write only when the content differs; returns whether a write happened. */
|
|
63
|
+
/** Add missing +x bits; returns whether a repair actually happened. */
|
|
64
|
+
export function ensureExecutable(path: string): boolean {
|
|
65
|
+
const mode = statSync(path).mode
|
|
66
|
+
const want = mode | 0o111
|
|
67
|
+
if (mode === want) return false
|
|
68
|
+
chmodSync(path, want)
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function writeTextIfChanged(path: string, content: string): boolean {
|
|
73
|
+
if (existsSync(path) && readFileSync(path, "utf8") === content) return false
|
|
74
|
+
writeFileSync(path, content)
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function writeBytesIfChanged(path: string, content: Uint8Array): boolean {
|
|
79
|
+
const bytes = Buffer.from(content)
|
|
80
|
+
if (existsSync(path) && readFileSync(path).equals(bytes)) return false
|
|
81
|
+
writeFileSync(path, bytes)
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function writeFileIfChanged(path: string, content: string): boolean {
|
|
86
|
+
return writeTextIfChanged(path, content)
|
|
87
|
+
}
|
|
@@ -6,15 +6,14 @@
|
|
|
6
6
|
* vocabulary directly.
|
|
7
7
|
*/
|
|
8
8
|
import { p } from "./exec"
|
|
9
|
-
import { existsSync } from "node:fs"
|
|
10
9
|
import { homedir } from "node:os"
|
|
11
10
|
|
|
12
11
|
import { kitHome } from "../kitHome"
|
|
12
|
+
import { makeEngineServices, type EngineServices, type Logger } from "./services"
|
|
13
13
|
import { claudeNextSteps, claudeSummary, claudeSync } from "./claudeSync"
|
|
14
14
|
import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
|
|
15
15
|
import { skillsNextSteps, skillsSummary, skillsSync } from "./skillsSync"
|
|
16
16
|
import { modeModel, modeToolchain } from "./modes"
|
|
17
|
-
import { echo } from "./output"
|
|
18
17
|
import { ExitError, parseArgs, preflight, validateModelFlags } from "./parseArgs"
|
|
19
18
|
|
|
20
19
|
export interface Ctx {
|
|
@@ -22,6 +21,7 @@ export interface Ctx {
|
|
|
22
21
|
readonly home: string
|
|
23
22
|
readonly agentsDir: string
|
|
24
23
|
dryRun: boolean
|
|
24
|
+
verbose: boolean
|
|
25
25
|
skipRtk: boolean
|
|
26
26
|
reconcile: boolean
|
|
27
27
|
prune: boolean
|
|
@@ -31,14 +31,23 @@ export interface Ctx {
|
|
|
31
31
|
claudePlugins: Array<string>
|
|
32
32
|
claudeModel: string
|
|
33
33
|
codexModel: string
|
|
34
|
+
/** Injected capability seam (logger/deps/platform) — see services.ts. */
|
|
35
|
+
readonly services: EngineServices
|
|
34
36
|
targetFilterSet: boolean
|
|
35
37
|
syncClaude: boolean
|
|
36
38
|
syncCodex: boolean
|
|
37
39
|
syncAgents: boolean
|
|
40
|
+
/** Per-run next-step triggers (Output Policy): advice prints only when its trigger changed or --verbose. */
|
|
41
|
+
readonly nextStepTriggers: {
|
|
42
|
+
claudePlugins: boolean
|
|
43
|
+
claudeRestart: boolean
|
|
44
|
+
codexRestart: boolean
|
|
45
|
+
skillsRestart: boolean
|
|
46
|
+
}
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
/** Globals default from env using the historical ${VAR:-default} contract. */
|
|
41
|
-
function makeCtx(): Ctx {
|
|
50
|
+
function makeCtx(services: EngineServices): Ctx {
|
|
42
51
|
const env = process.env
|
|
43
52
|
const home = env["HOME"] !== undefined && env["HOME"] !== "" ? env["HOME"] : homedir()
|
|
44
53
|
return {
|
|
@@ -46,6 +55,7 @@ function makeCtx(): Ctx {
|
|
|
46
55
|
home,
|
|
47
56
|
agentsDir: env["AGENTS_DIR"] !== undefined && env["AGENTS_DIR"] !== "" ? env["AGENTS_DIR"] : p(home, ".agents"),
|
|
48
57
|
dryRun: env["DRY_RUN"] === "1",
|
|
58
|
+
verbose: env["DOCKS_KIT_VERBOSE"] === "1",
|
|
49
59
|
skipRtk: env["SKIP_RTK"] === "1",
|
|
50
60
|
reconcile: env["RECONCILE"] === "1",
|
|
51
61
|
prune: env["PRUNE"] === "1",
|
|
@@ -55,25 +65,28 @@ function makeCtx(): Ctx {
|
|
|
55
65
|
claudePlugins: (env["CLAUDE_PLUGINS"] ?? "").split(" ").filter((s) => s !== ""),
|
|
56
66
|
claudeModel: env["CLAUDE_MODEL"] ?? "",
|
|
57
67
|
codexModel: env["CODEX_MODEL"] ?? "",
|
|
68
|
+
services,
|
|
58
69
|
targetFilterSet: false,
|
|
59
70
|
syncClaude: false,
|
|
60
71
|
syncCodex: false,
|
|
61
|
-
syncAgents: false
|
|
72
|
+
syncAgents: false,
|
|
73
|
+
nextStepTriggers: { claudePlugins: false, claudeRestart: false, codexRestart: false, skillsRestart: false }
|
|
62
74
|
}
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
78
|
+
const { echo } = ctx.services.logger
|
|
66
79
|
parseArgs(ctx, args)
|
|
67
80
|
preflight(ctx)
|
|
68
81
|
validateModelFlags(ctx)
|
|
69
82
|
|
|
70
|
-
const claudeRan = ctx.syncClaude
|
|
83
|
+
const claudeRan = ctx.syncClaude
|
|
71
84
|
if (claudeRan) claudeSync(ctx)
|
|
72
85
|
|
|
73
|
-
const codexRan = ctx.syncCodex
|
|
86
|
+
const codexRan = ctx.syncCodex
|
|
74
87
|
if (codexRan) codexSync(ctx)
|
|
75
88
|
|
|
76
|
-
const skillsState = ctx.syncAgents
|
|
89
|
+
const skillsState = ctx.syncAgents ? skillsSync(ctx) : undefined
|
|
77
90
|
|
|
78
91
|
echo("")
|
|
79
92
|
echo("--- Sync complete ---")
|
|
@@ -82,15 +95,37 @@ function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
82
95
|
if (codexRan) codexSummary(ctx)
|
|
83
96
|
if (skillsState !== undefined) skillsSummary(ctx, skillsState)
|
|
84
97
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
const advice = [
|
|
99
|
+
...(claudeRan ? claudeNextSteps(ctx) : []),
|
|
100
|
+
...(codexRan ? codexNextSteps(ctx) : []),
|
|
101
|
+
...(skillsState !== undefined ? skillsNextSteps(ctx) : [])
|
|
102
|
+
]
|
|
103
|
+
if (advice.length > 0) {
|
|
104
|
+
echo("")
|
|
105
|
+
for (const line of advice) echo(line)
|
|
106
|
+
}
|
|
89
107
|
return 0
|
|
90
108
|
}
|
|
91
109
|
|
|
92
|
-
export function runEngineNative(argv: ReadonlyArray<string
|
|
93
|
-
|
|
110
|
+
export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineServices): number {
|
|
111
|
+
let ctx!: Ctx
|
|
112
|
+
const baseServices = services ?? makeEngineServices()
|
|
113
|
+
const baseLogger = baseServices.logger
|
|
114
|
+
const logger: Logger = {
|
|
115
|
+
change: (msg) => baseLogger.change(msg),
|
|
116
|
+
verbose: (msg) => {
|
|
117
|
+
if (ctx.verbose) baseLogger.verbose(msg)
|
|
118
|
+
},
|
|
119
|
+
warn: (msg) => baseLogger.warn(msg),
|
|
120
|
+
err: (msg) => baseLogger.err(msg),
|
|
121
|
+
echo: (line) => baseLogger.echo(line)
|
|
122
|
+
}
|
|
123
|
+
const runServices: EngineServices = {
|
|
124
|
+
logger,
|
|
125
|
+
deps: baseServices.deps,
|
|
126
|
+
platform: baseServices.platform
|
|
127
|
+
}
|
|
128
|
+
ctx = makeCtx(runServices)
|
|
94
129
|
try {
|
|
95
130
|
switch (argv[0]) {
|
|
96
131
|
case "model":
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leveled stderr logger + stdout data writer — the Output Policy contract in
|
|
3
|
+
* DESIGN.md. Filtering is explicit and synchronous: engine code is imperative,
|
|
4
|
+
* so fiber-scoped Effect log levels cannot see these writes. The prefixes and
|
|
5
|
+
* ANSI codes are stable golden surface; the level controls visibility only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface Logger {
|
|
9
|
+
/** `[ok]` green — an operation actually mutated something. Always visible. */
|
|
10
|
+
readonly change: (msg: string) => void
|
|
11
|
+
/** `[ok]` green — status-quo confirmation; visible only with verbosity on. */
|
|
12
|
+
readonly verbose: (msg: string) => void
|
|
13
|
+
readonly warn: (msg: string) => void
|
|
14
|
+
readonly err: (msg: string) => void
|
|
15
|
+
/** stdout data line (dry-run report, summary, usage) — never filtered. */
|
|
16
|
+
readonly echo: (line: string) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LoggerSinks {
|
|
20
|
+
readonly stderr?: (chunk: string) => void
|
|
21
|
+
readonly stdout?: (chunk: string) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function makeLogger(sinks: LoggerSinks): Logger {
|
|
25
|
+
const errWrite = sinks.stderr ?? ((chunk: string) => void process.stderr.write(chunk))
|
|
26
|
+
const outWrite = sinks.stdout ?? ((chunk: string) => void process.stdout.write(chunk))
|
|
27
|
+
const ok = (msg: string): void => errWrite(`\x1b[1;32m[ok]\x1b[0m ${msg}\n`)
|
|
28
|
+
return {
|
|
29
|
+
change: ok,
|
|
30
|
+
verbose: ok,
|
|
31
|
+
warn: (msg) => errWrite(`\x1b[1;33m[warn]\x1b[0m ${msg}\n`),
|
|
32
|
+
err: (msg) => errWrite(`\x1b[1;31m[err]\x1b[0m ${msg}\n`),
|
|
33
|
+
echo: (line) => outWrite(`${line}\n`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -2,70 +2,69 @@
|
|
|
2
2
|
* Model-catalog helpers: manifest listing plus Claude/Codex model validation.
|
|
3
3
|
* Message strings are covered by the golden suites.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { readFileSync } from "node:fs"
|
|
7
|
-
|
|
5
|
+
import type { Ctx } from "./index"
|
|
8
6
|
import { isObject, parseJson, type Json } from "./jq"
|
|
9
|
-
import {
|
|
7
|
+
import { payloadDisplayPath, payloadText } from "../payload"
|
|
10
8
|
|
|
11
|
-
function catalog(
|
|
9
|
+
function catalog(): Json | undefined {
|
|
12
10
|
try {
|
|
13
|
-
return parseJson(
|
|
11
|
+
return parseJson(payloadText("SoT/models.json"))
|
|
14
12
|
} catch {
|
|
15
13
|
return undefined
|
|
16
14
|
}
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
function toolEntry(
|
|
20
|
-
const doc = catalog(
|
|
17
|
+
function toolEntry(tool: string): { [k: string]: Json } | undefined {
|
|
18
|
+
const doc = catalog()
|
|
21
19
|
if (doc === undefined || !isObject(doc)) return undefined
|
|
22
20
|
const entry = doc[tool]
|
|
23
21
|
return entry !== undefined && isObject(entry) ? entry : undefined
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
function modelEntries(
|
|
27
|
-
const entry = toolEntry(
|
|
24
|
+
function modelEntries(tool: string): Array<{ [k: string]: Json }> {
|
|
25
|
+
const entry = toolEntry(tool)
|
|
28
26
|
const models = entry?.["models"]
|
|
29
27
|
return Array.isArray(models) ? models.filter(isObject) : []
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
export function modelsFromManifest(
|
|
33
|
-
return modelEntries(
|
|
30
|
+
export function modelsFromManifest(tool: string): Array<string> {
|
|
31
|
+
return modelEntries(tool)
|
|
34
32
|
.map((m) => m["id"])
|
|
35
33
|
.filter((id): id is string => typeof id === "string")
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
export function printModels(
|
|
39
|
-
const
|
|
36
|
+
export function printModels(ctx: Ctx, tool: string): void {
|
|
37
|
+
const { echo, warn } = ctx.services.logger
|
|
38
|
+
const entry = toolEntry(tool)
|
|
40
39
|
if (entry === undefined) {
|
|
41
|
-
warn(`Model catalog unavailable (${
|
|
40
|
+
warn(`Model catalog unavailable (${payloadDisplayPath("SoT/models.json", ctx.repoDir)})`)
|
|
42
41
|
return
|
|
43
42
|
}
|
|
44
43
|
const verified = typeof entry["verified"] === "string" ? entry["verified"] : "?"
|
|
45
44
|
const lines = [`Available ${tool} models (kit-verified ${verified} — SoT/models.json):`]
|
|
46
|
-
for (const m of modelEntries(
|
|
45
|
+
for (const m of modelEntries(tool)) {
|
|
47
46
|
const note = typeof m["note"] === "string" ? ` — ${m["note"]}` : ""
|
|
48
47
|
lines.push(` ${String(m["id"] ?? "")}${note}`)
|
|
49
48
|
}
|
|
50
49
|
if (tool === "claude") lines.push(" (full claude-* model IDs outside the catalog are accepted with a warning)")
|
|
51
50
|
if (tool === "codex") lines.push(" (well-formed IDs outside the catalog are accepted with a warning)")
|
|
52
|
-
|
|
51
|
+
for (const line of lines) echo(line)
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
export function validateClaudeModel(
|
|
54
|
+
export function validateClaudeModel(ctx: Ctx, m: string): boolean {
|
|
56
55
|
if (m === "") return false
|
|
57
|
-
if (modelsFromManifest(
|
|
56
|
+
if (modelsFromManifest("claude").includes(m)) return true
|
|
58
57
|
if (m.startsWith("claude-")) {
|
|
59
|
-
warn(`Claude model '${m}' is not in the kit-verified catalog (SoT/models.json) — applying anyway`)
|
|
58
|
+
ctx.services.logger.warn(`Claude model '${m}' is not in the kit-verified catalog (SoT/models.json) — applying anyway`)
|
|
60
59
|
return true
|
|
61
60
|
}
|
|
62
61
|
return false
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
export function validateCodexModel(
|
|
64
|
+
export function validateCodexModel(ctx: Ctx, m: string): boolean {
|
|
66
65
|
if (!/^[A-Za-z0-9._-]+$/.test(m)) return false
|
|
67
|
-
if (!modelsFromManifest(
|
|
68
|
-
warn(
|
|
66
|
+
if (!modelsFromManifest("codex").includes(m)) {
|
|
67
|
+
ctx.services.logger.warn(
|
|
69
68
|
`Codex model '${m}' is not in the kit-verified catalog (SoT/models.json) — applying anyway (check ~/.codex/config.toml if Codex rejects it)`
|
|
70
69
|
)
|
|
71
70
|
}
|