docks-kit 0.2.0 → 0.4.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 +5 -3
- package/README.md +15 -7
- package/cli/docs/install.md +12 -12
- package/cli/docs/overview.md +7 -1
- package/cli/docs/platforms.md +8 -7
- package/cli/docs/sync-layers.md +15 -7
- package/cli/docs/toolchain.md +8 -2
- package/cli/src/engine-native/DESIGN.md +25 -14
- package/cli/src/engine-native/bun.ts +87 -0
- package/cli/src/engine-native/claudeRuntime.ts +132 -0
- package/cli/src/engine-native/claudeSync.ts +179 -115
- package/cli/src/engine-native/codexSync.ts +42 -36
- package/cli/src/engine-native/deps.ts +27 -10
- package/cli/src/engine-native/exec.ts +8 -23
- package/cli/src/engine-native/index.ts +8 -8
- package/cli/src/engine-native/models.ts +14 -16
- package/cli/src/engine-native/modes.ts +18 -9
- package/cli/src/engine-native/parseArgs.ts +1 -17
- package/cli/src/engine-native/powershell.ts +11 -0
- package/cli/src/engine-native/skillsSync.ts +11 -46
- package/cli/src/engine-native/toolchain.ts +6 -6
- package/cli/src/generated/sotPayload.ts +41 -0
- package/cli/src/kitHome.ts +15 -11
- package/cli/src/manifests.ts +17 -15
- package/cli/src/payload.ts +28 -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 -31
- package/SoT/toolchain.json +0 -27
- package/notification.mp3 +0 -0
|
@@ -4,25 +4,26 @@
|
|
|
4
4
|
* change. Guard order, message strings, and backup behavior are golden-tested.
|
|
5
5
|
*/
|
|
6
6
|
import { spawnSync } from "node:child_process"
|
|
7
|
-
import { copyFileSync, existsSync, mkdirSync,
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
8
8
|
|
|
9
9
|
import { syncCodexModel, replaceTopLevelSettingInFile } from "./codexToml"
|
|
10
10
|
import { p } from "./exec"
|
|
11
11
|
import type { Ctx } from "./index"
|
|
12
12
|
import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
13
|
+
import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
|
|
13
14
|
|
|
14
15
|
export function codexSync(ctx: Ctx): void {
|
|
15
16
|
const codexDir = p(ctx.home, ".codex")
|
|
16
|
-
const sotConfig =
|
|
17
|
+
const sotConfig = payloadText("SoT/.codex/config.toml")
|
|
17
18
|
const userConfig = p(codexDir, "config.toml")
|
|
18
19
|
|
|
19
20
|
ensureBubblewrap(ctx)
|
|
20
21
|
if (!ctx.dryRun) mkdirSync(codexDir, { recursive: true })
|
|
21
22
|
syncConfig(ctx, sotConfig, userConfig)
|
|
22
23
|
syncCodexModel(ctx, ctx.codexModel)
|
|
23
|
-
syncRules(ctx,
|
|
24
|
-
syncAgentsMd(ctx,
|
|
25
|
-
syncMarketplace(ctx,
|
|
24
|
+
syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
|
|
25
|
+
syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
|
|
26
|
+
syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
|
|
26
27
|
removeLegacyDocksMarketplace(ctx, userConfig)
|
|
27
28
|
syncPlugins(ctx, sotConfig)
|
|
28
29
|
}
|
|
@@ -95,9 +96,9 @@ function bwrapDetectPmInstallCmd(ctx: Ctx): string {
|
|
|
95
96
|
|
|
96
97
|
// --------------------------------------------------------------- config ----
|
|
97
98
|
|
|
98
|
-
function syncConfig(ctx: Ctx,
|
|
99
|
+
function syncConfig(ctx: Ctx, sotConfigText: string, userConfig: string): void {
|
|
99
100
|
const { change, echo, verbose } = ctx.services.logger
|
|
100
|
-
|
|
101
|
+
const sotConfig = payloadDisplayPath("SoT/.codex/config.toml", ctx.repoDir)
|
|
101
102
|
|
|
102
103
|
if (ctx.dryRun) {
|
|
103
104
|
echo(`[dry-run] merge ${sotConfig} -> ${userConfig}`)
|
|
@@ -105,7 +106,7 @@ function syncConfig(ctx: Ctx, sotConfig: string, userConfig: string): void {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
if (!existsSync(userConfig)) {
|
|
108
|
-
|
|
109
|
+
writeFileSync(userConfig, sotConfigText)
|
|
109
110
|
change("Codex config installed")
|
|
110
111
|
ctx.nextStepTriggers.codexRestart = true
|
|
111
112
|
return
|
|
@@ -119,8 +120,8 @@ function syncConfig(ctx: Ctx, sotConfig: string, userConfig: string): void {
|
|
|
119
120
|
writeFileSync(staging, before)
|
|
120
121
|
|
|
121
122
|
scrubDeprecatedFeatures(ctx, staging)
|
|
122
|
-
mergeTopLevelSettings(
|
|
123
|
-
mergeTableSettings(
|
|
123
|
+
mergeTopLevelSettings(sotConfigText, staging)
|
|
124
|
+
mergeTableSettings(sotConfigText, staging)
|
|
124
125
|
|
|
125
126
|
if (readFileSync(staging, "utf8") === before) {
|
|
126
127
|
rmSync(staging, { force: true })
|
|
@@ -179,8 +180,8 @@ function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
|
|
|
179
180
|
change("Codex: scrubbed deprecated [features].use_legacy_landlock")
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
function mergeTopLevelSettings(
|
|
183
|
-
for (const line of
|
|
183
|
+
function mergeTopLevelSettings(sotConfigText: string, userConfig: string): void {
|
|
184
|
+
for (const line of sotConfigText.split("\n")) {
|
|
184
185
|
if (line.startsWith("[")) break
|
|
185
186
|
if (/^[ \t]*($|#)/.test(line)) continue
|
|
186
187
|
if (!/^[A-Za-z0-9_.-]+[ \t]*=/.test(line)) continue
|
|
@@ -189,8 +190,8 @@ function mergeTopLevelSettings(sotConfig: string, userConfig: string): void {
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
function mergeTableSettings(
|
|
193
|
-
const sotLines =
|
|
193
|
+
function mergeTableSettings(sotConfigText: string, userConfig: string): void {
|
|
194
|
+
const sotLines = sotConfigText.split("\n")
|
|
194
195
|
for (const tableHeader of sotLines.filter((l) => /^\[[^\]]+\]/.test(l))) {
|
|
195
196
|
// Extract the SoT block: from the exact header line to (excluding) the
|
|
196
197
|
// next table header; `$(...)` strips trailing newlines.
|
|
@@ -225,9 +226,12 @@ function mergeTableSettings(sotConfig: string, userConfig: string): void {
|
|
|
225
226
|
|
|
226
227
|
// ------------------------------------------------------- rules + agents ----
|
|
227
228
|
|
|
228
|
-
function syncRules(ctx: Ctx,
|
|
229
|
+
function syncRules(ctx: Ctx, sotRules: ReadonlyArray<PayloadPath>, userRulesDir: string): void {
|
|
229
230
|
const { change, echo, verbose } = ctx.services.logger
|
|
230
|
-
|
|
231
|
+
const firstRule = sotRules[0]
|
|
232
|
+
if (firstRule === undefined) return
|
|
233
|
+
const firstDisplay = payloadDisplayPath(firstRule, ctx.repoDir)
|
|
234
|
+
const sotRulesDir = firstDisplay.slice(0, firstDisplay.lastIndexOf("/"))
|
|
231
235
|
|
|
232
236
|
if (ctx.dryRun) {
|
|
233
237
|
echo(`[dry-run] cp ${sotRulesDir}/*.rules -> ${userRulesDir}/`)
|
|
@@ -237,17 +241,15 @@ function syncRules(ctx: Ctx, sotRulesDir: string, userRulesDir: string): void {
|
|
|
237
241
|
mkdirSync(userRulesDir, { recursive: true })
|
|
238
242
|
let sawRules = false
|
|
239
243
|
let rulesChanged = false
|
|
240
|
-
const ruleFiles =
|
|
241
|
-
.filter((e) => e.isFile() && e.name.endsWith(".rules"))
|
|
242
|
-
.map((e) => p(sotRulesDir, e.name))
|
|
243
|
-
.sort(compareCodepoints)
|
|
244
|
+
const ruleFiles = sotRules.filter((path) => path.endsWith(".rules")).sort(compareCodepoints)
|
|
244
245
|
for (const ruleFile of ruleFiles) {
|
|
245
246
|
sawRules = true
|
|
246
247
|
const userRuleFile = p(userRulesDir, ruleFile.slice(ruleFile.lastIndexOf("/") + 1))
|
|
247
|
-
const
|
|
248
|
+
const content = payloadBytes(ruleFile)
|
|
249
|
+
const identical = existsSync(userRuleFile) && readFileSync(userRuleFile).equals(content)
|
|
248
250
|
if (identical) continue
|
|
249
251
|
if (existsSync(userRuleFile)) copyFileSync(userRuleFile, `${userRuleFile}.bak`)
|
|
250
|
-
|
|
252
|
+
writeFileSync(userRuleFile, content)
|
|
251
253
|
rulesChanged = true
|
|
252
254
|
}
|
|
253
255
|
if (rulesChanged) {
|
|
@@ -256,30 +258,30 @@ function syncRules(ctx: Ctx, sotRulesDir: string, userRulesDir: string): void {
|
|
|
256
258
|
} else if (sawRules) verbose("Codex rules already in sync")
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
function syncAgentsMd(ctx: Ctx,
|
|
261
|
+
function syncAgentsMd(ctx: Ctx, sotAgentsMdText: string, userAgentsMd: string): void {
|
|
260
262
|
const { change, echo, verbose } = ctx.services.logger
|
|
261
|
-
|
|
263
|
+
const sotAgentsMd = payloadDisplayPath("SoT/.codex/AGENTS.md", ctx.repoDir)
|
|
262
264
|
|
|
263
265
|
if (ctx.dryRun) {
|
|
264
266
|
echo(`[dry-run] cp ${sotAgentsMd} -> ${userAgentsMd}`)
|
|
265
267
|
return
|
|
266
268
|
}
|
|
267
269
|
|
|
268
|
-
if (existsSync(userAgentsMd) && readFileSync(userAgentsMd)
|
|
270
|
+
if (existsSync(userAgentsMd) && readFileSync(userAgentsMd, "utf8") === sotAgentsMdText) {
|
|
269
271
|
verbose("Codex AGENTS.md already in sync")
|
|
270
272
|
return
|
|
271
273
|
}
|
|
272
274
|
if (existsSync(userAgentsMd)) copyFileSync(userAgentsMd, `${userAgentsMd}.bak`)
|
|
273
|
-
|
|
275
|
+
writeFileSync(userAgentsMd, sotAgentsMdText)
|
|
274
276
|
change("Codex AGENTS.md synced")
|
|
275
277
|
ctx.nextStepTriggers.codexRestart = true
|
|
276
278
|
}
|
|
277
279
|
|
|
278
280
|
// ---------------------------------------------------------- marketplace ----
|
|
279
281
|
|
|
280
|
-
function syncMarketplace(ctx: Ctx,
|
|
282
|
+
function syncMarketplace(ctx: Ctx, sotMarketplaceText: string, userMarketplace: string): void {
|
|
281
283
|
const { change, echo, err, verbose } = ctx.services.logger
|
|
282
|
-
|
|
284
|
+
const sotMarketplace = payloadDisplayPath("SoT/.codex/plugins/marketplace.json", ctx.repoDir)
|
|
283
285
|
|
|
284
286
|
if (ctx.dryRun) {
|
|
285
287
|
echo(`[dry-run] cp ${sotMarketplace} -> ${userMarketplace}`)
|
|
@@ -287,7 +289,7 @@ function syncMarketplace(ctx: Ctx, sotMarketplace: string, userMarketplace: stri
|
|
|
287
289
|
}
|
|
288
290
|
|
|
289
291
|
mkdirSync(p(ctx.agentsDir, "plugins"), { recursive: true })
|
|
290
|
-
const repo = parseJson(
|
|
292
|
+
const repo = parseJson(sotMarketplaceText)
|
|
291
293
|
if (repo === undefined) throw new Error(`invalid SoT marketplace JSON: ${sotMarketplace}`)
|
|
292
294
|
|
|
293
295
|
if (existsSync(userMarketplace)) {
|
|
@@ -307,7 +309,7 @@ function syncMarketplace(ctx: Ctx, sotMarketplace: string, userMarketplace: stri
|
|
|
307
309
|
change("Codex marketplace merged (backup at marketplace.json.bak)")
|
|
308
310
|
ctx.nextStepTriggers.codexRestart = true
|
|
309
311
|
} else {
|
|
310
|
-
|
|
312
|
+
writeFileSync(userMarketplace, sotMarketplaceText)
|
|
311
313
|
change("Codex marketplace installed")
|
|
312
314
|
ctx.nextStepTriggers.codexRestart = true
|
|
313
315
|
}
|
|
@@ -390,13 +392,17 @@ const standaloneInstallCommand = (ctx: Ctx): string => ctx.services.deps.spec("c
|
|
|
390
392
|
/** codex::_enabled_plugin_ids — [plugins."<id>"] tables with enabled = true. */
|
|
391
393
|
export function enabledPluginIds(configFile: string): Array<string> {
|
|
392
394
|
if (!existsSync(configFile)) return []
|
|
395
|
+
return enabledPluginIdsFromText(readFileSync(configFile, "utf8"))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function enabledPluginIdsFromText(configText: string): Array<string> {
|
|
393
399
|
const ids: Array<string> = []
|
|
394
400
|
let plugin = ""
|
|
395
401
|
let enabled = false
|
|
396
402
|
const flush = (): void => {
|
|
397
403
|
if (plugin !== "" && enabled) ids.push(plugin)
|
|
398
404
|
}
|
|
399
|
-
for (const line of
|
|
405
|
+
for (const line of configText.split("\n")) {
|
|
400
406
|
const m = /^\[plugins\."([^"]+)"\][ \t]*$/.exec(line)
|
|
401
407
|
if (m !== null) {
|
|
402
408
|
flush()
|
|
@@ -418,12 +424,12 @@ export function enabledPluginIds(configFile: string): Array<string> {
|
|
|
418
424
|
return ids
|
|
419
425
|
}
|
|
420
426
|
|
|
421
|
-
function manualPluginRefreshCommand(
|
|
422
|
-
const first =
|
|
427
|
+
function manualPluginRefreshCommand(sotConfigText: string): string {
|
|
428
|
+
const first = enabledPluginIdsFromText(sotConfigText)[0]
|
|
423
429
|
return first !== undefined ? `codex plugin add ${first}` : "codex plugin add <plugin@marketplace>"
|
|
424
430
|
}
|
|
425
431
|
|
|
426
|
-
function syncPlugins(ctx: Ctx,
|
|
432
|
+
function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
427
433
|
const { change, echo, warn } = ctx.services.logger
|
|
428
434
|
if (ctx.dryRun) {
|
|
429
435
|
echo("[dry-run] add enabled Codex plugins from SoT")
|
|
@@ -432,7 +438,7 @@ function syncPlugins(ctx: Ctx, sotConfig: string): void {
|
|
|
432
438
|
|
|
433
439
|
if (ctx.services.deps.probe("codex").state === "missing") {
|
|
434
440
|
warn(
|
|
435
|
-
`codex CLI not in PATH - deployed config/marketplace only; install Codex with: ${standaloneInstallCommand(ctx)} | docs: https://developers.openai.com/codex/cli; then run: ${manualPluginRefreshCommand(
|
|
441
|
+
`codex CLI not in PATH - deployed config/marketplace only; install Codex with: ${standaloneInstallCommand(ctx)} | docs: https://developers.openai.com/codex/cli; then run: ${manualPluginRefreshCommand(sotConfigText)}`
|
|
436
442
|
)
|
|
437
443
|
return
|
|
438
444
|
}
|
|
@@ -447,7 +453,7 @@ function syncPlugins(ctx: Ctx, sotConfig: string): void {
|
|
|
447
453
|
|
|
448
454
|
let refreshed = 0
|
|
449
455
|
let failed = 0
|
|
450
|
-
for (const pluginId of
|
|
456
|
+
for (const pluginId of enabledPluginIdsFromText(sotConfigText)) {
|
|
451
457
|
const res = spawnSync("codex", ["plugin", "add", pluginId], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
|
|
452
458
|
const addOut = `${res.stdout ?? ""}${res.stderr ?? ""}`
|
|
453
459
|
if (res.error === undefined && res.status === 0) {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* command that installs a missing one.
|
|
9
9
|
*/
|
|
10
10
|
import { homedir } from "node:os"
|
|
11
|
+
import { isAbsolute } from "node:path"
|
|
11
12
|
import { existsSync, readdirSync } from "node:fs"
|
|
12
13
|
|
|
13
14
|
import { capture, commandExists, p, which } from "./exec"
|
|
@@ -110,21 +111,34 @@ const home = (): string => {
|
|
|
110
111
|
return envHome !== undefined && envHome !== "" ? envHome : homedir()
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
const
|
|
114
|
+
const absoluteWindowsExe = (path: string): boolean =>
|
|
115
|
+
/\.exe$/i.test(path) && (/^[A-Za-z]:[\\/]/.test(path) || /^\\\\/.test(path))
|
|
116
|
+
|
|
117
|
+
// The resolved path gets persisted into global direct-exec hooks, so a
|
|
118
|
+
// relative `which` hit (relative PATH entry, relative BUN_INSTALL) would
|
|
119
|
+
// break outside the sync working directory.
|
|
120
|
+
const absoluteExecutable = (path: string, platform: NodeJS.Platform): boolean =>
|
|
121
|
+
platform === "win32" ? absoluteWindowsExe(path) : isAbsolute(path)
|
|
122
|
+
|
|
123
|
+
const findBun = (exec: ProbeExecutor, platform: NodeJS.Platform = rawPlatform()): { command: string; path: string } | undefined => {
|
|
114
124
|
const onPath = exec.which("bun")
|
|
115
|
-
if (onPath !== ""
|
|
125
|
+
if (onPath !== "" && absoluteExecutable(onPath, platform)) {
|
|
126
|
+
return { command: platform === "win32" ? onPath : "bun", path: onPath }
|
|
127
|
+
}
|
|
116
128
|
const root =
|
|
117
129
|
process.env["BUN_INSTALL"] !== undefined && process.env["BUN_INSTALL"] !== ""
|
|
118
130
|
? process.env["BUN_INSTALL"]!
|
|
119
131
|
: p(home(), ".bun")
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
const name = platform === "win32" ? "bun.exe" : "bun"
|
|
133
|
+
for (const candidate of [p(root, "bin", name), p(home(), ".bun", "bin", name)]) {
|
|
134
|
+
const found = exec.which(candidate)
|
|
135
|
+
if (found !== "" && absoluteExecutable(found, platform)) return { command: found, path: found }
|
|
122
136
|
}
|
|
123
137
|
return undefined
|
|
124
138
|
}
|
|
125
139
|
|
|
126
|
-
const resolveBun = (exec: ProbeExecutor): ProbeResult => {
|
|
127
|
-
const bun = findBun(exec)
|
|
140
|
+
const resolveBun = (exec: ProbeExecutor, platform: NodeJS.Platform): ProbeResult => {
|
|
141
|
+
const bun = findBun(exec, platform)
|
|
128
142
|
return bun === undefined
|
|
129
143
|
? { state: "missing" }
|
|
130
144
|
: { state: "present", path: bun.path }
|
|
@@ -147,7 +161,9 @@ const versionEffectSolutions = (exec: ProbeExecutor): string => {
|
|
|
147
161
|
}
|
|
148
162
|
|
|
149
163
|
const locateEffectSolutions = (exec: ProbeExecutor, platform: NodeJS.Platform): DependencyLocation => {
|
|
150
|
-
const
|
|
164
|
+
const strictBun = findBun(exec, platform)
|
|
165
|
+
const pathBun = exec.which("bun")
|
|
166
|
+
const bun = strictBun ?? (pathBun !== "" ? { command: "bun", path: pathBun } : undefined)
|
|
151
167
|
if (bun === undefined) return { path: "", binDir: "" }
|
|
152
168
|
const globalBin = exec.capture(bun.command, ["pm", "-g", "bin"])
|
|
153
169
|
const names =
|
|
@@ -180,6 +196,7 @@ const resolveChrome = (exec: ProbeExecutor, platform: NodeJS.Platform): ProbeRes
|
|
|
180
196
|
}
|
|
181
197
|
|
|
182
198
|
const latestRtk = (exec: ProbeExecutor): string => {
|
|
199
|
+
if (!exec.commandExists("curl")) return ""
|
|
183
200
|
const doc = parseJson(
|
|
184
201
|
exec.capture("curl", ["-fsSL", "--max-time", "5", "https://api.github.com/repos/rtk-ai/rtk/releases/latest"])
|
|
185
202
|
)
|
|
@@ -204,7 +221,7 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
204
221
|
: "sudo apt install -y git (or your distro's package manager)",
|
|
205
222
|
{ version: versionProbe("git") }
|
|
206
223
|
),
|
|
207
|
-
jq: spec("jq", "
|
|
224
|
+
jq: spec("jq", "optional", (pf = rawPlatform()) =>
|
|
208
225
|
pf === "win32"
|
|
209
226
|
? "winget install jqlang.jq (then open a new terminal)"
|
|
210
227
|
: pf === "darwin"
|
|
@@ -212,7 +229,7 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
212
229
|
: "sudo apt install -y jq",
|
|
213
230
|
{ version: versionProbe("jq") }
|
|
214
231
|
),
|
|
215
|
-
curl: spec("curl", "
|
|
232
|
+
curl: spec("curl", "optional", (pf = rawPlatform()) =>
|
|
216
233
|
pf === "win32" ? "winget install cURL.cURL" : pf === "darwin" ? "brew install curl" : "sudo apt install -y curl",
|
|
217
234
|
{ version: versionProbe("curl") }
|
|
218
235
|
),
|
|
@@ -251,7 +268,7 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
251
268
|
{
|
|
252
269
|
resolve: resolveBun,
|
|
253
270
|
version: (exec) => exec.capture(versionBunCommand(exec), ["--version"]),
|
|
254
|
-
locate: (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
|
|
271
|
+
locate: (exec, platform) => ({ path: findBun(exec, platform)?.path ?? "", binDir: "" })
|
|
255
272
|
}
|
|
256
273
|
),
|
|
257
274
|
bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)"),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* substitution: stdout with trailing newlines stripped, empty on failure.
|
|
5
5
|
*/
|
|
6
6
|
import { spawnSync } from "node:child_process"
|
|
7
|
-
import { accessSync, chmodSync, constants,
|
|
7
|
+
import { accessSync, chmodSync, constants, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
8
8
|
import { delimiter, isAbsolute, join } from "node:path"
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -69,34 +69,19 @@ export function ensureExecutable(path: string): boolean {
|
|
|
69
69
|
return true
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export function
|
|
72
|
+
export function writeTextIfChanged(path: string, content: string): boolean {
|
|
73
73
|
if (existsSync(path) && readFileSync(path, "utf8") === content) return false
|
|
74
74
|
writeFileSync(path, content)
|
|
75
75
|
return true
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (existsSync(
|
|
81
|
-
|
|
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
82
|
return true
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let changed = false
|
|
88
|
-
for (const e of readdirSync(srcDir, { withFileTypes: true })) {
|
|
89
|
-
const src = p(srcDir, e.name)
|
|
90
|
-
const dest = p(destDir, e.name)
|
|
91
|
-
if (e.isDirectory()) {
|
|
92
|
-
if (!existsSync(dest)) {
|
|
93
|
-
mkdirSync(dest, { recursive: true })
|
|
94
|
-
changed = true
|
|
95
|
-
}
|
|
96
|
-
if (copyTreeIfChanged(src, dest)) changed = true
|
|
97
|
-
} else if (copyFileIfChanged(src, dest)) {
|
|
98
|
-
changed = true
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return changed
|
|
85
|
+
export function writeFileIfChanged(path: string, content: string): boolean {
|
|
86
|
+
return writeTextIfChanged(path, content)
|
|
102
87
|
}
|
|
@@ -6,16 +6,16 @@
|
|
|
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"
|
|
13
12
|
import { makeEngineServices, type EngineServices, type Logger } from "./services"
|
|
13
|
+
import type { BunRuntimeState } from "./bun"
|
|
14
14
|
import { claudeNextSteps, claudeSummary, claudeSync } from "./claudeSync"
|
|
15
15
|
import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
|
|
16
16
|
import { skillsNextSteps, skillsSummary, skillsSync } from "./skillsSync"
|
|
17
17
|
import { modeModel, modeToolchain } from "./modes"
|
|
18
|
-
import { ExitError, parseArgs,
|
|
18
|
+
import { ExitError, parseArgs, validateModelFlags } from "./parseArgs"
|
|
19
19
|
|
|
20
20
|
export interface Ctx {
|
|
21
21
|
readonly repoDir: string
|
|
@@ -34,6 +34,7 @@ export interface Ctx {
|
|
|
34
34
|
codexModel: string
|
|
35
35
|
/** Injected capability seam (logger/deps/platform) — see services.ts. */
|
|
36
36
|
readonly services: EngineServices
|
|
37
|
+
bunRuntime?: BunRuntimeState
|
|
37
38
|
targetFilterSet: boolean
|
|
38
39
|
syncClaude: boolean
|
|
39
40
|
syncCodex: boolean
|
|
@@ -78,21 +79,20 @@ function makeCtx(services: EngineServices): Ctx {
|
|
|
78
79
|
function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
79
80
|
const { echo } = ctx.services.logger
|
|
80
81
|
parseArgs(ctx, args)
|
|
81
|
-
preflight(ctx)
|
|
82
82
|
validateModelFlags(ctx)
|
|
83
83
|
|
|
84
|
-
const claudeRan = ctx.syncClaude
|
|
85
|
-
|
|
84
|
+
const claudeRan = ctx.syncClaude
|
|
85
|
+
const claudeRuntime = claudeRan ? claudeSync(ctx) : undefined
|
|
86
86
|
|
|
87
|
-
const codexRan = ctx.syncCodex
|
|
87
|
+
const codexRan = ctx.syncCodex
|
|
88
88
|
if (codexRan) codexSync(ctx)
|
|
89
89
|
|
|
90
|
-
const skillsState = ctx.syncAgents
|
|
90
|
+
const skillsState = ctx.syncAgents ? skillsSync(ctx) : undefined
|
|
91
91
|
|
|
92
92
|
echo("")
|
|
93
93
|
echo("--- Sync complete ---")
|
|
94
94
|
echo(`Repo: ${ctx.repoDir}`)
|
|
95
|
-
if (
|
|
95
|
+
if (claudeRuntime !== undefined) claudeSummary(ctx, claudeRuntime)
|
|
96
96
|
if (codexRan) codexSummary(ctx)
|
|
97
97
|
if (skillsState !== undefined) skillsSummary(ctx, skillsState)
|
|
98
98
|
|
|
@@ -2,49 +2,47 @@
|
|
|
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 { p } from "./exec"
|
|
6
|
-
import { readFileSync } from "node:fs"
|
|
7
|
-
|
|
8
5
|
import type { Ctx } from "./index"
|
|
9
6
|
import { isObject, parseJson, type Json } from "./jq"
|
|
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
36
|
export function printModels(ctx: Ctx, tool: string): void {
|
|
39
37
|
const { echo, warn } = ctx.services.logger
|
|
40
|
-
const entry = toolEntry(
|
|
38
|
+
const entry = toolEntry(tool)
|
|
41
39
|
if (entry === undefined) {
|
|
42
|
-
warn(`Model catalog unavailable (${
|
|
40
|
+
warn(`Model catalog unavailable (${payloadDisplayPath("SoT/models.json", ctx.repoDir)})`)
|
|
43
41
|
return
|
|
44
42
|
}
|
|
45
43
|
const verified = typeof entry["verified"] === "string" ? entry["verified"] : "?"
|
|
46
44
|
const lines = [`Available ${tool} models (kit-verified ${verified} — SoT/models.json):`]
|
|
47
|
-
for (const m of modelEntries(
|
|
45
|
+
for (const m of modelEntries(tool)) {
|
|
48
46
|
const note = typeof m["note"] === "string" ? ` — ${m["note"]}` : ""
|
|
49
47
|
lines.push(` ${String(m["id"] ?? "")}${note}`)
|
|
50
48
|
}
|
|
@@ -55,7 +53,7 @@ export function printModels(ctx: Ctx, tool: string): void {
|
|
|
55
53
|
|
|
56
54
|
export function validateClaudeModel(ctx: Ctx, m: string): boolean {
|
|
57
55
|
if (m === "") return false
|
|
58
|
-
if (modelsFromManifest(
|
|
56
|
+
if (modelsFromManifest("claude").includes(m)) return true
|
|
59
57
|
if (m.startsWith("claude-")) {
|
|
60
58
|
ctx.services.logger.warn(`Claude model '${m}' is not in the kit-verified catalog (SoT/models.json) — applying anyway`)
|
|
61
59
|
return true
|
|
@@ -65,7 +63,7 @@ export function validateClaudeModel(ctx: Ctx, m: string): boolean {
|
|
|
65
63
|
|
|
66
64
|
export function validateCodexModel(ctx: Ctx, m: string): boolean {
|
|
67
65
|
if (!/^[A-Za-z0-9._-]+$/.test(m)) return false
|
|
68
|
-
if (!modelsFromManifest(
|
|
66
|
+
if (!modelsFromManifest("codex").includes(m)) {
|
|
69
67
|
ctx.services.logger.warn(
|
|
70
68
|
`Codex model '${m}' is not in the kit-verified catalog (SoT/models.json) — applying anyway (check ~/.codex/config.toml if Codex rejects it)`
|
|
71
69
|
)
|
|
@@ -4,14 +4,16 @@
|
|
|
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 {
|
|
14
|
-
import {
|
|
14
|
+
import { ensureRtk } from "./claudeSync"
|
|
15
|
+
import { bunBootstrap } from "./bun"
|
|
16
|
+
import { agentBrowserInstall, effectSolutionsInstall } from "./skillsSync"
|
|
15
17
|
import { ensure, report } from "./toolchain"
|
|
16
18
|
|
|
17
19
|
export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
@@ -41,7 +43,7 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
41
43
|
return 0
|
|
42
44
|
}
|
|
43
45
|
echo(`deployed: ${jsonModelField(deployed)}`)
|
|
44
|
-
echo(`SoT: ${
|
|
46
|
+
echo(`SoT: ${jsonModelText(payloadText("SoT/.claude/settings.json"))}`)
|
|
45
47
|
} else {
|
|
46
48
|
const deployed = p(ctx.home, ".codex", "config.toml")
|
|
47
49
|
if (!fileReadable(deployed)) {
|
|
@@ -49,7 +51,7 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
49
51
|
return 0
|
|
50
52
|
}
|
|
51
53
|
echo(`deployed: ${tomlModelField(deployed)}`)
|
|
52
|
-
echo(`SoT: ${
|
|
54
|
+
echo(`SoT: ${tomlModelText(payloadText("SoT/.codex/config.toml"))}`)
|
|
53
55
|
}
|
|
54
56
|
printModels(ctx, tool)
|
|
55
57
|
return 0
|
|
@@ -84,7 +86,11 @@ function fileReadable(p: string): boolean {
|
|
|
84
86
|
|
|
85
87
|
/** `jq -r '.model // "default (unset)"'` — empty on unparseable input. */
|
|
86
88
|
function jsonModelField(file: string): string {
|
|
87
|
-
|
|
89
|
+
return jsonModelText(readFileSync(file, "utf8"))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function jsonModelText(text: string): string {
|
|
93
|
+
const doc = parseJson(text)
|
|
88
94
|
if (doc === undefined) return ""
|
|
89
95
|
const v: Json | undefined = isObject(doc) ? doc["model"] : undefined
|
|
90
96
|
if (v === undefined || v === null || v === false) return "default (unset)"
|
|
@@ -93,7 +99,11 @@ function jsonModelField(file: string): string {
|
|
|
93
99
|
|
|
94
100
|
/** `awk -F'"' '/^model[[:space:]]*=/{print $2; exit}'`. */
|
|
95
101
|
function tomlModelField(file: string): string {
|
|
96
|
-
|
|
102
|
+
return tomlModelText(readFileSync(file, "utf8"))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function tomlModelText(text: string): string {
|
|
106
|
+
for (const line of text.split("\n")) {
|
|
97
107
|
if (/^model[ \t]*=/.test(line)) return line.split('"')[1] ?? ""
|
|
98
108
|
}
|
|
99
109
|
return ""
|
|
@@ -125,10 +135,9 @@ export function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
125
135
|
}
|
|
126
136
|
switch (tool) {
|
|
127
137
|
case "rtk":
|
|
128
|
-
return
|
|
138
|
+
return ensureRtk(ctx, "cannot download RTK installer; toolchain ensure rtk aborted", 1)
|
|
129
139
|
case "bun":
|
|
130
|
-
|
|
131
|
-
return bunBootstrap(ctx, ctx.services) !== "" ? 0 : 1
|
|
140
|
+
return bunBootstrap(ctx, ctx.services).kind === "ready" ? 0 : 1
|
|
132
141
|
case "effect-solutions":
|
|
133
142
|
return ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
|
|
134
143
|
case "agent-browser":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EngineNative flag layer: usage / target selection / compact-window parsing /
|
|
3
|
-
* optional-plugin parsing / model validation
|
|
3
|
+
* optional-plugin parsing / model validation. ExitError mirrors an
|
|
4
4
|
* early parser exit and is caught once in runEngineNative.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -184,22 +184,6 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
export function preflight(ctx: Ctx): void {
|
|
188
|
-
const { err } = ctx.services.logger
|
|
189
|
-
if (ctx.syncClaude || ctx.syncCodex) {
|
|
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()}`)
|
|
192
|
-
throw new ExitError(1)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (ctx.syncClaude) {
|
|
196
|
-
if (ctx.services.deps.probe("curl").state === "missing") {
|
|
197
|
-
err(`curl is required. Install: ${ctx.services.deps.spec("curl").installHint()}`)
|
|
198
|
-
throw new ExitError(1)
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
187
|
export function validateModelFlags(ctx: Ctx): void {
|
|
204
188
|
const { err, warn } = ctx.services.logger
|
|
205
189
|
if (ctx.claudeModel !== "") {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function powerShellLiteral(value: string): string {
|
|
2
|
+
return `'${value.replaceAll("'", "''")}'`
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function encodePowerShellCommand(script: string): string {
|
|
6
|
+
return Buffer.from(script, "utf16le").toString("base64")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function decodePowerShellCommand(encoded: string): string {
|
|
10
|
+
return Buffer.from(encoded, "base64").toString("utf16le")
|
|
11
|
+
}
|