docks-kit 0.16.4 → 0.16.5

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.
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Top-level Codex TOML setting replacement plus `--codex-model`. The pass is
3
- * line-based on purpose: a TOML library would reformat user configs and break
4
- * the golden contract. The key is used as a raw regex fragment, preserving the
2
+ * Line-based Codex TOML passes: top-level setting replacement, the SoT
3
+ * top-level and table merges used by `sync codex`, and `--codex-model` /
4
+ * `--codex-effort`. A TOML library would reformat user configs and break the
5
+ * golden contract. The key is used as a raw regex fragment, preserving the
5
6
  * historical record-oriented behavior.
6
7
  */
7
8
  import { p } from "./exec"
@@ -110,3 +111,143 @@ export function syncCodexEffort(ctx: Ctx, effort: string): void {
110
111
  unchanged: `Effort: deployed Codex model_reasoning_effort already ${resolved}`
111
112
  })
112
113
  }
114
+
115
+ export function mergeTopLevelSettings(sotConfigText: string, userConfig: string): void {
116
+ for (const line of sotConfigText.split("\n")) {
117
+ if (line.startsWith("[")) break
118
+ if (/^[ \t]*($|#)/.test(line)) continue
119
+ if (!/^[A-Za-z0-9_.-]+[ \t]*=/.test(line)) continue
120
+ const key = line.slice(0, line.indexOf("=")).replace(/[ \t]+$/, "")
121
+ replaceTopLevelSettingInFile(userConfig, key, line)
122
+ }
123
+ }
124
+
125
+ interface TomlTableHeader {
126
+ readonly path: string
127
+ }
128
+
129
+ const TOML_BASIC_ESCAPES: Readonly<Record<string, string>> = {
130
+ b: "\b",
131
+ t: "\t",
132
+ n: "\n",
133
+ f: "\f",
134
+ r: "\r",
135
+ '"': '"',
136
+ "\\": "\\"
137
+ }
138
+
139
+ function tomlBasicEscape(line: string, offset: number): { readonly next: number; readonly value: string } | undefined {
140
+ const escaped = line[offset]
141
+ const simple = escaped === undefined ? undefined : TOML_BASIC_ESCAPES[escaped]
142
+ if (simple !== undefined) return { next: offset + 1, value: simple }
143
+ const digits = escaped === "u" ? 4 : escaped === "U" ? 8 : 0
144
+ if (digits === 0) return undefined
145
+ const hex = line.slice(offset + 1, offset + 1 + digits)
146
+ if (hex.length !== digits || !/^[0-9A-Fa-f]+$/.test(hex)) return undefined
147
+ const codePoint = Number.parseInt(hex, 16)
148
+ if (codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) return undefined
149
+ return { next: offset + 1 + digits, value: String.fromCodePoint(codePoint) }
150
+ }
151
+
152
+ /** Decode a table header to the TOML path that determines managed ownership. */
153
+ function tomlTableHeader(line: string): TomlTableHeader | undefined {
154
+ let offset = 0
155
+ const skipWhitespace = (): void => {
156
+ while (line[offset] === " " || line[offset] === "\t") offset++
157
+ }
158
+
159
+ skipWhitespace()
160
+ if (line[offset] !== "[") return undefined
161
+ offset++
162
+ const array = line[offset] === "["
163
+ if (array) offset++
164
+
165
+ const keys: Array<string> = []
166
+ while (true) {
167
+ skipWhitespace()
168
+ const quote = line[offset]
169
+ let key = ""
170
+ if (quote === '"' || quote === "'") {
171
+ offset++
172
+ let closed = false
173
+ while (offset < line.length) {
174
+ const char = line[offset]!
175
+ if (char === quote) {
176
+ offset++
177
+ closed = true
178
+ break
179
+ }
180
+ if (quote === '"' && char === "\\") {
181
+ const escape = tomlBasicEscape(line, offset + 1)
182
+ if (escape === undefined) return undefined
183
+ key += escape.value
184
+ offset = escape.next
185
+ continue
186
+ }
187
+ if (char === "\n" || char === "\r") return undefined
188
+ key += char
189
+ offset++
190
+ }
191
+ if (!closed) return undefined
192
+ } else {
193
+ const start = offset
194
+ while (offset < line.length && /[A-Za-z0-9_-]/.test(line[offset]!)) offset++
195
+ if (offset === start) return undefined
196
+ key = line.slice(start, offset)
197
+ }
198
+ keys.push(key)
199
+
200
+ skipWhitespace()
201
+ if (line[offset] === ".") {
202
+ offset++
203
+ continue
204
+ }
205
+ if (line[offset] !== "]") return undefined
206
+ offset++
207
+ if (array) {
208
+ if (line[offset] !== "]") return undefined
209
+ offset++
210
+ }
211
+ skipWhitespace()
212
+ if (offset < line.length && line[offset] !== "#") return undefined
213
+ return { path: JSON.stringify(keys) }
214
+ }
215
+ }
216
+
217
+ function mergeTableSettingsText(sotConfigText: string, userConfigText: string): string {
218
+ const sotLines = sotConfigText.split("\n")
219
+ let merged = userConfigText
220
+ for (let tableOffset = 0; tableOffset < sotLines.length; tableOffset++) {
221
+ const managedHeader = tomlTableHeader(sotLines[tableOffset]!)
222
+ if (managedHeader === undefined) continue
223
+
224
+ const block: Array<string> = []
225
+ for (let blockOffset = tableOffset; blockOffset < sotLines.length; blockOffset++) {
226
+ const line = sotLines[blockOffset]!
227
+ if (blockOffset !== tableOffset && tomlTableHeader(line) !== undefined) break
228
+ block.push(line)
229
+ }
230
+ const tableBlock = block.join("\n").replace(/\n+$/, "")
231
+
232
+ const userLines = merged.split("\n")
233
+ if (userLines[userLines.length - 1] === "") userLines.pop()
234
+ let skip = false
235
+ const kept: Array<string> = []
236
+ for (const line of userLines) {
237
+ const header = tomlTableHeader(line)
238
+ if (header !== undefined) {
239
+ skip = header.path === managedHeader.path
240
+ if (skip) continue
241
+ }
242
+ if (!skip) kept.push(line)
243
+ }
244
+ merged = `${kept.join("\n")}\n\n${tableBlock}\n`
245
+ }
246
+ return merged
247
+ }
248
+
249
+ export function mergeTableSettings(sotConfigText: string, userConfig: string): void {
250
+ const next = mergeTableSettingsText(sotConfigText, readFileSync(userConfig, "utf8"))
251
+ writeFileSync(`${userConfig}.tmp`, next)
252
+ renameSync(`${userConfig}.tmp`, userConfig)
253
+ }
@@ -116,7 +116,7 @@ export function commandExists(name: string): boolean {
116
116
  return which(name) !== ""
117
117
  }
118
118
 
119
- export function isExecutable(path: string, suffixes: ReadonlyArray<string> = hostOs().executableSuffixes): boolean {
119
+ function isExecutable(path: string, suffixes: ReadonlyArray<string> = hostOs().executableSuffixes): boolean {
120
120
  try {
121
121
  if (!statSync(path).isFile()) return false
122
122
  if (suffixes.some((suffix) => suffix !== "")) {
@@ -148,7 +148,3 @@ export function writeBytesIfChanged(path: string, content: Uint8Array): boolean
148
148
  writeFileSync(path, bytes)
149
149
  return true
150
150
  }
151
-
152
- export function writeFileIfChanged(path: string, content: string): boolean {
153
- return writeTextIfChanged(path, content)
154
- }
@@ -11,6 +11,8 @@
11
11
  * - output formatting: 2-space indent, UTF-8 raw, trailing newline.
12
12
  */
13
13
 
14
+ import { existsSync, readFileSync } from "node:fs"
15
+
14
16
  export type Json = null | boolean | number | string | Array<Json> | { [key: string]: Json }
15
17
 
16
18
  export function isObject(v: Json): v is { [key: string]: Json } {
@@ -61,3 +63,8 @@ export function parseJson(text: string): Json | undefined {
61
63
  return undefined
62
64
  }
63
65
  }
66
+
67
+ /** Parse a JSON file with `parseJson` semantics; a missing file is `undefined`. */
68
+ export function readJsonFile(file: string): Json | undefined {
69
+ return existsSync(file) ? parseJson(readFileSync(file, "utf8")) : undefined
70
+ }
@@ -6,6 +6,16 @@ import type { Ctx } from "./index"
6
6
  import { isObject, parseJson, type Json } from "./jq"
7
7
  import { payloadDisplayPath, payloadText } from "../payload"
8
8
 
9
+ export interface ModelEntry {
10
+ readonly id: string
11
+ readonly kind: "alias" | "id"
12
+ readonly note?: string
13
+ }
14
+
15
+ export interface ModelCatalog {
16
+ readonly verified: string
17
+ readonly models: ReadonlyArray<ModelEntry>
18
+ }
9
19
 
10
20
  function toolEntry(tool: string): { [k: string]: Json } | undefined {
11
21
  const doc = parseJson(payloadText("SoT/models.json"))
@@ -14,16 +24,28 @@ function toolEntry(tool: string): { [k: string]: Json } | undefined {
14
24
  return entry !== undefined && isObject(entry) ? entry : undefined
15
25
  }
16
26
 
17
- function modelEntries(tool: string): Array<{ [k: string]: Json }> {
18
- const entry = toolEntry(tool)
27
+ function modelEntries(entry: { [k: string]: Json } | undefined): Array<{ [k: string]: Json }> {
19
28
  const models = entry?.["models"]
20
29
  return Array.isArray(models) ? models.filter(isObject) : []
21
30
  }
22
31
 
23
- export function modelsFromManifest(tool: string): Array<string> {
24
- return modelEntries(tool)
25
- .map((m) => m["id"])
26
- .filter((id): id is string => typeof id === "string")
32
+ /**
33
+ * Typed view of one tool's `SoT/models.json` section. An entry whose `id` is not a
34
+ * string or whose `kind` is neither `alias` nor `id` is skipped; a missing section
35
+ * yields `verified: "?"` and no models.
36
+ */
37
+ export function modelCatalog(tool: string): ModelCatalog {
38
+ const entry = toolEntry(tool)
39
+ const verified = entry?.["verified"]
40
+ const models: Array<ModelEntry> = []
41
+ for (const m of modelEntries(entry)) {
42
+ const id = m["id"]
43
+ const kind = m["kind"]
44
+ if (typeof id !== "string" || (kind !== "alias" && kind !== "id")) continue
45
+ const note = m["note"]
46
+ models.push(typeof note === "string" ? { id, kind, note } : { id, kind })
47
+ }
48
+ return { verified: typeof verified === "string" ? verified : "?", models }
27
49
  }
28
50
 
29
51
  export function printModels(ctx: Ctx, tool: string): void {
@@ -35,7 +57,7 @@ export function printModels(ctx: Ctx, tool: string): void {
35
57
  }
36
58
  const verified = typeof entry["verified"] === "string" ? entry["verified"] : "?"
37
59
  const lines = [`Available ${tool} models (kit-verified ${verified} — SoT/models.json):`]
38
- for (const m of modelEntries(tool)) {
60
+ for (const m of modelEntries(entry)) {
39
61
  const note = typeof m["note"] === "string" ? ` — ${m["note"]}` : ""
40
62
  lines.push(` ${String(m["id"] ?? "")}${note}`)
41
63
  }
@@ -46,7 +68,7 @@ export function printModels(ctx: Ctx, tool: string): void {
46
68
 
47
69
  export function validateClaudeModel(ctx: Ctx, m: string): boolean {
48
70
  if (m === "") return false
49
- if (modelsFromManifest("claude").includes(m)) return true
71
+ if (modelCatalog("claude").models.some((entry) => entry.id === m)) return true
50
72
  if (m.startsWith("claude-")) {
51
73
  ctx.services.logger.warn(`Claude model '${m}' is not in the kit-verified catalog (SoT/models.json) — applying anyway`)
52
74
  return true
@@ -56,7 +78,7 @@ export function validateClaudeModel(ctx: Ctx, m: string): boolean {
56
78
 
57
79
  export function validateCodexModel(ctx: Ctx, m: string): boolean {
58
80
  if (!/^[A-Za-z0-9._-]+$/.test(m)) return false
59
- if (!modelsFromManifest("codex").includes(m)) {
81
+ if (!modelCatalog("codex").models.some((entry) => entry.id === m)) {
60
82
  ctx.services.logger.warn(
61
83
  `Codex model '${m}' is not in the kit-verified catalog (SoT/models.json) — applying anyway (check ~/.codex/config.toml if Codex rejects it)`
62
84
  )
@@ -10,7 +10,7 @@ import { isAbsolute, resolve } from "node:path"
10
10
 
11
11
  import { payloadDisplayPath, payloadText, type PayloadPath } from "../payload"
12
12
  import { bunBootstrap } from "./bun"
13
- import { commandExists, p, spawnProcess, type AsyncProcessResult } from "./exec"
13
+ import { p, spawnProcess, type AsyncProcessResult } from "./exec"
14
14
  import type { Ctx } from "./index"
15
15
  import { isObject, parseJson } from "./jq"
16
16
  import { ompPaths } from "./ompPaths"
@@ -317,7 +317,7 @@ async function syncPlugins(ctx: Ctx): Promise<number> {
317
317
  const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
318
318
 
319
319
  if (ctx.dryRun) {
320
- const piIntercomPin = field(ctx, "pi-intercom", "verified")
320
+ const piIntercomPin = field("pi-intercom", "verified")
321
321
  for (const pluginId of MARKETPLACE_PLUGIN_IDS) {
322
322
  echo(`[dry-run] omp plugin install --scope user ${pluginId}`)
323
323
  }
@@ -345,7 +345,7 @@ async function syncPlugins(ctx: Ctx): Promise<number> {
345
345
  )
346
346
  return 0
347
347
  }
348
- const piIntercomPin = field(ctx, "pi-intercom", "verified")
348
+ const piIntercomPin = field("pi-intercom", "verified")
349
349
 
350
350
  progress("Checking installed omp plugins...")
351
351
  const installed = await installedPluginIdsFromCli()
@@ -1,62 +1,14 @@
1
- import { p } from "../exec"
1
+ import { posixHost } from "./posix"
2
2
  import type { HostOs } from "./types"
3
3
 
4
- function posixLiteral(value: string): string {
5
- return `'${value.replaceAll("'", `'"'"'`)}'`
6
- }
7
-
8
- export const darwin: HostOs = {
4
+ export const darwin: HostOs = posixHost({
9
5
  id: "darwin",
10
6
  toolchainOs: "darwin",
11
7
  supportsBubblewrap: false,
12
- directoryLinkKinds: ["symlink"],
13
- executableSuffixes: [""],
14
- invoke: (executablePath, args) => ({ command: executablePath, args }),
15
- bunExecutableName: "bun",
16
- bunInstaller: (pin, directory) => {
17
- const scriptPath = p(directory, "install.sh")
18
- return {
19
- scriptPath,
20
- download: {
21
- command: "curl",
22
- args: ["-fsSL", "https://bun.sh/install", "-o", scriptPath]
23
- },
24
- run: {
25
- command: "bash",
26
- args: [scriptPath, `bun-v${pin}`]
27
- }
28
- }
29
- },
30
- environmentSetting: (name, value) => ({
31
- kind: "profile",
32
- candidates: [".zshrc", ".bashrc", ".bash_profile", ".profile", ".zshenv"],
33
- target: (shell) => {
34
- const shellPath = shell ?? "bash"
35
- const shellName = shellPath.slice(shellPath.lastIndexOf("/") + 1)
36
- return shellName === "zsh" ? ".zshrc" : shellName === "bash" ? ".bashrc" : ".profile"
37
- },
38
- line: `export ${name}=${value}`
39
- }),
40
- statusLineCommand: (bun, script) => {
41
- const bunLiteral = posixLiteral(bun)
42
- const scriptLiteral = posixLiteral(script)
43
- return `test -x ${bunLiteral} && test -f ${scriptLiteral} && exec ${bunLiteral} ${scriptLiteral} || true`
44
- },
45
- failureHookCommand: (command) => command,
46
- installHint: (tool) => {
47
- switch (tool) {
48
- case "git":
49
- return "brew install git"
50
- case "jq":
51
- return "brew install jq"
52
- case "curl":
53
- return "brew install curl"
54
- case "ffplay":
55
- return "brew install ffmpeg"
56
- case "claude":
57
- return "curl -fsSL https://claude.ai/install.sh -o /tmp/claude-install.sh && bash /tmp/claude-install.sh"
58
- case "codex":
59
- return 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"'
60
- }
8
+ packageManagerHints: {
9
+ git: "brew install git",
10
+ jq: "brew install jq",
11
+ curl: "brew install curl",
12
+ ffplay: "brew install ffmpeg"
61
13
  }
62
- }
14
+ })
@@ -1,62 +1,14 @@
1
- import { p } from "../exec"
1
+ import { posixHost } from "./posix"
2
2
  import type { HostOs } from "./types"
3
3
 
4
- function posixLiteral(value: string): string {
5
- return `'${value.replaceAll("'", `'"'"'`)}'`
6
- }
7
-
8
- export const linux: HostOs = {
4
+ export const linux: HostOs = posixHost({
9
5
  id: "linux",
10
6
  toolchainOs: "linux",
11
7
  supportsBubblewrap: true,
12
- directoryLinkKinds: ["symlink"],
13
- executableSuffixes: [""],
14
- invoke: (executablePath, args) => ({ command: executablePath, args }),
15
- bunExecutableName: "bun",
16
- bunInstaller: (pin, directory) => {
17
- const scriptPath = p(directory, "install.sh")
18
- return {
19
- scriptPath,
20
- download: {
21
- command: "curl",
22
- args: ["-fsSL", "https://bun.sh/install", "-o", scriptPath]
23
- },
24
- run: {
25
- command: "bash",
26
- args: [scriptPath, `bun-v${pin}`]
27
- }
28
- }
29
- },
30
- environmentSetting: (name, value) => ({
31
- kind: "profile",
32
- candidates: [".zshrc", ".bashrc", ".bash_profile", ".profile", ".zshenv"],
33
- target: (shell) => {
34
- const shellPath = shell ?? "bash"
35
- const shellName = shellPath.slice(shellPath.lastIndexOf("/") + 1)
36
- return shellName === "zsh" ? ".zshrc" : shellName === "bash" ? ".bashrc" : ".profile"
37
- },
38
- line: `export ${name}=${value}`
39
- }),
40
- statusLineCommand: (bun, script) => {
41
- const bunLiteral = posixLiteral(bun)
42
- const scriptLiteral = posixLiteral(script)
43
- return `test -x ${bunLiteral} && test -f ${scriptLiteral} && exec ${bunLiteral} ${scriptLiteral} || true`
44
- },
45
- failureHookCommand: (command) => command,
46
- installHint: (tool) => {
47
- switch (tool) {
48
- case "git":
49
- return "sudo apt install -y git (or your distro's package manager)"
50
- case "jq":
51
- return "sudo apt install -y jq"
52
- case "curl":
53
- return "sudo apt install -y curl"
54
- case "ffplay":
55
- return "sudo apt install -y ffmpeg"
56
- case "claude":
57
- return "curl -fsSL https://claude.ai/install.sh -o /tmp/claude-install.sh && bash /tmp/claude-install.sh"
58
- case "codex":
59
- return 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"'
60
- }
8
+ packageManagerHints: {
9
+ git: "sudo apt install -y git (or your distro's package manager)",
10
+ jq: "sudo apt install -y jq",
11
+ curl: "sudo apt install -y curl",
12
+ ffplay: "sudo apt install -y ffmpeg"
61
13
  }
62
- }
14
+ })
@@ -0,0 +1,67 @@
1
+ // No import from "../exec": exec.ts imports "./os", which evaluates linux and darwin
2
+ // through this module. A slash join keeps the same host-stable path p() would produce.
3
+ import type { HintedTool, HostOs } from "./types"
4
+
5
+ function posixLiteral(value: string): string {
6
+ return `'${value.replaceAll("'", `'"'"'`)}'`
7
+ }
8
+
9
+ /** Package-manager hints for the tools a POSIX host installs through its own package manager. */
10
+ export type PackageManagerHints = Readonly<Record<Exclude<HintedTool, "claude" | "codex">, string>>
11
+
12
+ const UPSTREAM_INSTALLER_HINTS: Readonly<Record<"claude" | "codex", string>> = {
13
+ claude: "curl -fsSL https://claude.ai/install.sh -o /tmp/claude-install.sh && bash /tmp/claude-install.sh",
14
+ codex: 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"'
15
+ }
16
+
17
+ interface PosixHostInputs {
18
+ readonly id: HostOs["id"]
19
+ readonly toolchainOs: HostOs["toolchainOs"]
20
+ readonly supportsBubblewrap: boolean
21
+ readonly packageManagerHints: PackageManagerHints
22
+ }
23
+
24
+ /** Members shared by every POSIX host; linux and darwin differ only in the inputs. */
25
+ export function posixHost(inputs: PosixHostInputs): HostOs {
26
+ return {
27
+ id: inputs.id,
28
+ toolchainOs: inputs.toolchainOs,
29
+ supportsBubblewrap: inputs.supportsBubblewrap,
30
+ directoryLinkKinds: ["symlink"],
31
+ executableSuffixes: [""],
32
+ invoke: (executablePath, args) => ({ command: executablePath, args }),
33
+ bunExecutableName: "bun",
34
+ bunInstaller: (pin, directory) => {
35
+ const scriptPath = `${directory}/install.sh`
36
+ return {
37
+ scriptPath,
38
+ download: {
39
+ command: "curl",
40
+ args: ["-fsSL", "https://bun.sh/install", "-o", scriptPath]
41
+ },
42
+ run: {
43
+ command: "bash",
44
+ args: [scriptPath, `bun-v${pin}`]
45
+ }
46
+ }
47
+ },
48
+ environmentSetting: (name, value) => ({
49
+ kind: "profile",
50
+ candidates: [".zshrc", ".bashrc", ".bash_profile", ".profile", ".zshenv"],
51
+ target: (shell) => {
52
+ const shellPath = shell ?? "bash"
53
+ const shellName = shellPath.slice(shellPath.lastIndexOf("/") + 1)
54
+ return shellName === "zsh" ? ".zshrc" : shellName === "bash" ? ".bashrc" : ".profile"
55
+ },
56
+ line: `export ${name}=${value}`
57
+ }),
58
+ statusLineCommand: (bun, script) => {
59
+ const bunLiteral = posixLiteral(bun)
60
+ const scriptLiteral = posixLiteral(script)
61
+ return `test -x ${bunLiteral} && test -f ${scriptLiteral} && exec ${bunLiteral} ${scriptLiteral} || true`
62
+ },
63
+ failureHookCommand: (command) => command,
64
+ installHint: (tool) =>
65
+ tool === "claude" || tool === "codex" ? UPSTREAM_INSTALLER_HINTS[tool] : inputs.packageManagerHints[tool]
66
+ }
67
+ }
@@ -63,7 +63,7 @@ export const HOST_TARGETS: ReadonlyArray<HostTarget> = [
63
63
  target("windows-arm64", "windows", "arm64", [], ["ARM64"])
64
64
  ]
65
65
 
66
- export function targetFor(platform: PlatformName, arch: string): HostTarget | undefined {
66
+ function targetFor(platform: PlatformName, arch: string): HostTarget | undefined {
67
67
  return HOST_TARGETS.find((t) => t.platform === platform && t.arch === arch)
68
68
  }
69
69
 
@@ -17,7 +17,7 @@ import {
17
17
  } from "node:fs"
18
18
  import { dirname, relative, resolve } from "node:path"
19
19
  import { payloadText } from "../payload"
20
- import { p, spawnProcess, writeFileIfChanged } from "./exec"
20
+ import { p, spawnProcess, writeTextIfChanged } from "./exec"
21
21
  import type { Ctx } from "./index"
22
22
  import { compareCodepoints } from "./jq"
23
23
  import { hostOs, type DirectoryLinkKind } from "./os"
@@ -50,7 +50,7 @@ export async function skillsSync(ctx: Ctx): Promise<SkillsState> {
50
50
 
51
51
  /** skills::_skills_cli — the pinned npx package spec. */
52
52
  function skillsCli(ctx: Ctx): string {
53
- const version = field(ctx, "skills-cli", "verified")
53
+ const version = field("skills-cli", "verified")
54
54
  if (version !== "") return `skills@${version}`
55
55
  ctx.services.logger.err("Universal skills sync aborted because SoT/toolchain.json has no verified skills-cli pin")
56
56
  throw new ExitError(1)
@@ -365,7 +365,7 @@ function updateSnapshot(ctx: Ctx, manifest: string, snapshot: string, failedRemo
365
365
 
366
366
  mkdirSync(ctx.agentsDir, { recursive: true })
367
367
  const sorted = [...new Set([...normalizeManifest(manifest), ...failedRemovals])].sort(compareCodepoints)
368
- writeFileIfChanged(snapshot, sorted.length > 0 ? `${sorted.join("\n")}\n` : "")
368
+ writeTextIfChanged(snapshot, sorted.length > 0 ? `${sorted.join("\n")}\n` : "")
369
369
  }
370
370
 
371
371
  // -------------------------------------------------------------- summary ----
@@ -14,7 +14,7 @@ function manifest(): { [k: string]: Json } {
14
14
  return tools !== undefined && isObject(tools) ? tools : {}
15
15
  }
16
16
 
17
- export function field(ctx: Ctx, tool: string, name: string): string {
17
+ export function field(tool: string, name: string): string {
18
18
  const entry = manifest()[tool]
19
19
  if (entry === undefined || !isObject(entry)) return ""
20
20
  const v = entry[name]
@@ -22,7 +22,7 @@ export function field(ctx: Ctx, tool: string, name: string): string {
22
22
  }
23
23
 
24
24
  /** toolchain::_is_newer — numeric per dotted field, GNU-sort last-resort tie-break. */
25
- export function isNewer(a: string, b: string): boolean {
25
+ function isNewer(a: string, b: string): boolean {
26
26
  if (a === "" || b === "" || a === b) return false
27
27
  const fa = a.split(".")
28
28
  const fb = b.split(".")
@@ -86,14 +86,14 @@ export async function report(ctx: Ctx): Promise<void> {
86
86
  const pn = ctx.services.platform.name()
87
87
  const platformOs = hostOs(pn).toolchainOs
88
88
  for (const tool of Object.keys(manifest()).sort(compareCodepoints)) {
89
- const os = field(ctx, tool, "os")
89
+ const os = field(tool, "os")
90
90
  if (os !== "" && platformOs !== "" && os !== platformOs) continue
91
- const kind = field(ctx, tool, "kind")
92
- const floor = field(ctx, tool, "floor")
93
- const verified = field(ctx, tool, "verified")
91
+ const kind = field(tool, "kind")
92
+ const floor = field(tool, "floor")
93
+ const verified = field(tool, "verified")
94
94
  const dash = (v: string): string => (v !== "" ? v : "-")
95
95
  if (kind === "pin") {
96
- const via = field(ctx, tool, "via")
96
+ const via = field(tool, "via")
97
97
  echo(row([tool, kind, `(${via !== "" ? via : "npx"})`, dash(floor), dash(verified), "pinned"]))
98
98
  continue
99
99
  }
@@ -1,7 +1,7 @@
1
1
  // Generated by cli/scripts/generate-sot-payload.ts. DO NOT EDIT.
2
2
  // Edit SoT/, notification.mp3, or package.json, then run: bun cli/scripts/generate-sot-payload.ts
3
3
 
4
- export const GENERATED_PACKAGE_VERSION = "0.16.4"
4
+ export const GENERATED_PACKAGE_VERSION = "0.16.5"
5
5
 
6
6
  export const GENERATED_PAYLOAD_TEXT = {
7
7
  "SoT/.agents/skills.txt": "# Universal AI-agent skill manifest intentionally empty.\n# Global skill discovery is opt-in: add one <owner>/<repo> slug per line.\n# EngineNative ignores comments and blank lines.\n",
@@ -1,34 +1,17 @@
1
1
  import { existsSync, lstatSync, readFileSync, readdirSync, readlinkSync } from "node:fs"
2
2
  import { homedir } from "node:os"
3
3
  import { dirname, join, resolve } from "node:path"
4
- import { pluginUserScopeInstalled } from "./engine-native/claudeSync"
4
+ import { pluginUserScopeInstalled } from "./engine-native/claudePlugins"
5
5
  import { COPY_MARKER } from "./engine-native/skillsSync"
6
6
  import { payloadText } from "./payload"
7
7
 
8
8
  export { homedir }
9
9
 
10
- export interface ModelEntry {
11
- readonly id: string
12
- readonly kind: "alias" | "id"
13
- readonly note?: string
14
- }
15
-
16
- export interface ModelCatalog {
17
- readonly verified: string
18
- readonly models: ReadonlyArray<ModelEntry>
19
- }
20
-
21
10
  export type Tool = "claude" | "codex"
22
11
 
23
12
  const readJson = (path: string): any => JSON.parse(readFileSync(path, "utf8"))
24
13
  const readJsonText = (text: string): any => JSON.parse(text)
25
14
 
26
- export const modelCatalog = (tool: Tool): ModelCatalog =>
27
- readJsonText(payloadText("SoT/models.json"))[tool]
28
-
29
- export const toolchainManifest = (): Record<string, any> =>
30
- readJsonText(payloadText("SoT/toolchain.json")).tools
31
-
32
15
  /** SoT settings (claude) — model/effort/env for drift display. */
33
16
  export const sotClaudeSettings = (): any =>
34
17
  readJsonText(payloadText("SoT/.claude/settings.json"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docks-kit",
3
- "version": "0.16.4",
3
+ "version": "0.16.5",
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",