docks-kit 0.19.0 → 0.19.1

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/README.md CHANGED
@@ -53,7 +53,7 @@ docks-kit toolchain [check|ensure <tool>|outdated] verified-version floors for
53
53
  docks-kit status [--json] deployed-vs-SoT drift + toolchain + counts
54
54
  docks-kit plugins list [--json] enabledPlugins tri-state vs installed
55
55
  docks-kit skills list [--json] universal skills vs manifest
56
- docks-kit docs [topic] self-documentation (10 topics)
56
+ docks-kit docs [topic] self-documentation (11 topics)
57
57
  --help --version --wizard --completions built-in
58
58
  ```
59
59
 
@@ -100,7 +100,8 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
100
100
  the full doctor table. Bun is the one managed install and is pinned to its
101
101
  verified version.
102
102
  - **Model catalog** — enabled harnesses provide live Claude, Codex, or omp
103
- model IDs; `SoT/models.json` supplies aliases, notes, and the offline fallback.
103
+ model IDs; `SoT/models.json` supplies Claude and Codex aliases, notes, and the
104
+ offline fallback. omp has no curated fallback list.
104
105
  - **Claude runtime** — sync materializes three dependency-free Bun `.mjs`
105
106
  programs for statusline, SessionStart, and Notification. Quota display uses
106
107
  Claude's native `rate_limits`; there is no OAuth fetch, shared usage cache,
@@ -13,7 +13,7 @@ AI-assisted dev environment on every machine.
13
13
  | `SoT/.codex/` | Codex config (config.toml, rules, AGENTS.md, marketplace) |
14
14
  | `SoT/.agents/` | Universal agent skills manifest (agentskills.io standard) |
15
15
  | `SoT/.omp/` | omp config deployed to `~/.omp/agent/` (AGENTS.md, config.yml, models.yml, mcp.json) plus `intercom.json` for pi intercom |
16
- | `SoT/models.json` | Kit-verified model catalog (see `docks-kit docs models`) |
16
+ | `SoT/models.json` | Curated Claude and Codex aliases, notes, and offline fallback for the live model lists (see `docks-kit docs models`) |
17
17
  | `SoT/toolchain.json` | Verified-version floors for external tools (see `docks-kit docs toolchain`) |
18
18
  | `cli/src/generated/sotPayload.ts` | Deterministic generated payload embedded in standalone/npm execution |
19
19
  | `cli/src/engine-native/` | EngineNative mutation logic for sync/model/toolchain and the `docks-kit omp` session overlay |
@@ -24,8 +24,14 @@ const TOPICS: Record<string, { summary: string; body: string }> = {
24
24
  summary: "Deploy-time modifiers and the flag-less-sync-reverts contract",
25
25
  body: modifiers,
26
26
  },
27
- models: { summary: "Model catalog, validation rules, model get/set", body: models },
28
- toolchain: { summary: "Verified-version floors and the doctor table", body: toolchain },
27
+ models: {
28
+ summary: "Live model lists, the curated overlay, validation, model get/set",
29
+ body: models,
30
+ },
31
+ toolchain: {
32
+ summary: "Verified-version floors, the doctor table, and the upstream outdated report",
33
+ body: toolchain,
34
+ },
29
35
  plugins: { summary: "enabledPlugins tri-state + optional plugin opt-ins", body: plugins },
30
36
  install: {
31
37
  summary: "Install paths: repo checkout, bun add -g, POSIX/Windows installers",
@@ -29,10 +29,12 @@ export const toolchainCommand = Command.make(
29
29
  Effect.gen(function* () {
30
30
  const operation = Option.getOrElse(config.op, () => "check");
31
31
  const flags = config.verbose ? ["--verbose"] : [];
32
+ const operand = Option.getOrUndefined(config.tool);
33
+ const words = operand === undefined ? [] : [operand];
32
34
 
33
35
  switch (operation) {
34
36
  case "check":
35
- return yield* engine(["toolchain", "check", ...(config.verbose ? ["--verbose"] : [])]);
37
+ return yield* engine(["toolchain", "check", ...words, ...flags]);
36
38
  case "ensure": {
37
39
  const t = Option.getOrUndefined(config.tool);
38
40
  if (t === undefined || !MANAGED.includes(t)) {
@@ -41,7 +43,12 @@ export const toolchainCommand = Command.make(
41
43
  return yield* engine(["toolchain", "ensure", t, ...flags]);
42
44
  }
43
45
  case "outdated":
44
- return yield* engine(["toolchain", "outdated", ...(config.refresh ? ["--refresh"] : [])]);
46
+ return yield* engine([
47
+ "toolchain",
48
+ "outdated",
49
+ ...words,
50
+ ...(config.refresh ? ["--refresh"] : []),
51
+ ]);
45
52
  default:
46
53
  return yield* bail(
47
54
  `Unknown toolchain op '${operation}' (valid: check, ensure, outdated)`,
@@ -60,9 +60,8 @@ const capturePackageRoot: CapturePackageRoot = (command, args) => {
60
60
 
61
61
  /**
62
62
  * A Bun global home is `<root>/.bun/install/global/node_modules/<pkg>`. Windows
63
- * reports that path with backslashes, so containment is tested on a normalized
64
- * copy — but only on Windows, because a backslash is a legal POSIX filename
65
- * character and must never be read as a separator there.
63
+ * reports that path with backslashes and compares it without case sensitivity.
64
+ * On POSIX, a backslash remains a legal filename character, not a separator.
66
65
  */
67
66
  export const packageManagerForHome = (
68
67
  home: string,
@@ -70,13 +69,14 @@ export const packageManagerForHome = (
70
69
  host: HostOs = hostOs(),
71
70
  ): PackageManager => {
72
71
  const normalize = (value: string): string =>
73
- host.id === "windows" ? value.replaceAll("\\", "/") : value;
72
+ host.id === "windows" ? value.replaceAll("\\", "/").toLowerCase() : value;
74
73
  const normalizedHome = normalize(home);
75
74
  const underEnvironmentRoot = (name: "BUN_INSTALL_GLOBAL_DIR" | "BUN_INSTALL"): boolean => {
76
75
  const root = environment[name]?.trim();
77
76
  if (root === undefined || root === "") return false;
78
- const normalizedRoot = normalize(root);
79
- return normalizedHome === normalizedRoot || normalizedHome.startsWith(`${normalizedRoot}/`);
77
+ const normalizedRoot = normalize(root).replace(/\/+$/, "") || "/";
78
+ const prefix = normalizedRoot.endsWith("/") ? normalizedRoot : `${normalizedRoot}/`;
79
+ return normalizedHome === normalizedRoot || normalizedHome.startsWith(prefix);
80
80
  };
81
81
  return normalizedHome.includes("/.bun/") ||
82
82
  underEnvironmentRoot("BUN_INSTALL_GLOBAL_DIR") ||
@@ -19,6 +19,9 @@ import {
19
19
  import { payloadText } from "../payload";
20
20
  import type { JsonObject } from "./sharedTypes";
21
21
 
22
+ /** Claude Code's built-in marketplace: always present, never added or pruned by the kit. */
23
+ const OFFICIAL_MARKETPLACE = "claude-plugins-official";
24
+
22
25
  export async function cli(
23
26
  args: Array<string>,
24
27
  ): Promise<{ ok: boolean; out: string; detail: string }> {
@@ -113,20 +116,31 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
113
116
  ? repoObj["extraKnownMarketplaces"]
114
117
  : {};
115
118
  const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {};
116
-
117
- // Pass 1 — add missing marketplaces (SoT insertion order, like to_entries).
118
- let addedMp = 0;
119
- let f1 = 0;
120
- for (const [mpName, mpValue] of Object.entries(sotMarketplaces)) {
119
+ const knownMarketplace = (mpName: string): boolean => {
121
120
  const known = readJsonFile(knownMarketplaces);
122
- if (
121
+ return (
123
122
  known !== undefined &&
124
123
  isObject(known) &&
125
124
  known[mpName] !== undefined &&
126
125
  known[mpName] !== null &&
127
126
  known[mpName] !== false
128
- )
129
- continue;
127
+ );
128
+ };
129
+ const addedMarketplaces = new Set<string>();
130
+ // The official marketplace is built in; a successful add is usable before its inventory is written.
131
+ // A missing inventory proves absence (fresh home); an unreadable one does not, so refresh as before.
132
+ const marketplaceReady = (mpName: string): boolean => {
133
+ if (mpName === OFFICIAL_MARKETPLACE || addedMarketplaces.has(mpName)) return true;
134
+ if (!existsSync(knownMarketplaces)) return false;
135
+ const known = readJsonFile(knownMarketplaces);
136
+ return known === undefined || !isObject(known) || knownMarketplace(mpName);
137
+ };
138
+
139
+ // Pass 1 — add missing marketplaces (SoT insertion order, like to_entries).
140
+ let addedMp = 0;
141
+ let f1 = 0;
142
+ for (const [mpName, mpValue] of Object.entries(sotMarketplaces)) {
143
+ if (knownMarketplace(mpName)) continue;
130
144
  const repo =
131
145
  isObject(mpValue) && isObject(mpValue["source"])
132
146
  ? String((mpValue["source"] as JsonObject)["repo"] ?? "")
@@ -136,6 +150,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
136
150
  clearProgress();
137
151
  if (marketplaceResult.ok) {
138
152
  addedMp++;
153
+ addedMarketplaces.add(mpName);
139
154
  } else {
140
155
  recordFailure(
141
156
  ctx,
@@ -156,7 +171,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
156
171
  if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue;
157
172
  const separator = pluginId.lastIndexOf("@");
158
173
  const mpName = separator > 0 ? pluginId.slice(separator + 1) : "";
159
- if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
174
+ if (mpName !== "" && marketplaceReady(mpName) && !refreshedMarketplaces.has(mpName)) {
160
175
  progress(`Refreshing marketplace ${mpName}...`);
161
176
  const refreshResult = await cli(["plugin", "marketplace", "update", mpName]);
162
177
  clearProgress();
@@ -207,7 +222,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
207
222
  // would duplicate one failure in the ledger and in the failed-operation count.
208
223
  if (!ctx.skipPluginRefresh) {
209
224
  for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
210
- if (refreshedMarketplaces.has(mpName)) continue;
225
+ if (refreshedMarketplaces.has(mpName) || !marketplaceReady(mpName)) continue;
211
226
  progress(`Refreshing marketplace ${mpName}...`);
212
227
  const refreshResult = await cli(["plugin", "marketplace", "update", mpName]);
213
228
  clearProgress();
@@ -263,7 +278,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
263
278
  // Pass 6 — prune-gated marketplace removal.
264
279
  const known = readJsonFile(knownMarketplaces);
265
280
  for (const mpName of sortedKeys(known)) {
266
- if (mpName === "claude-plugins-official") continue;
281
+ if (mpName === OFFICIAL_MARKETPLACE) continue;
267
282
  if (nonUserMarketplaces.has(mpName)) continue;
268
283
  if (kitMarketplaces.has(mpName)) continue;
269
284
  progress(`Removing marketplace ${mpName}...`);
@@ -7,7 +7,7 @@
7
7
  import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
8
 
9
9
  import { payloadDisplayPath } from "../payload";
10
- import { mergeTableSettings, mergeTopLevelSettings } from "./codexToml";
10
+ import { isTomlHeaderLine, mergeTableSettings, mergeTopLevelSettings } from "./codexToml";
11
11
  import type { Ctx } from "./index";
12
12
 
13
13
  // --------------------------------------------------------------- config ----
@@ -68,14 +68,14 @@ function scrubDeprecatedFeaturesText(content: string): string {
68
68
  let changed = false;
69
69
  for (const line of lines) {
70
70
  if (inFeatures) {
71
- if (line.startsWith("[")) {
71
+ if (isTomlHeaderLine(line)) {
72
72
  inFeatures = false;
73
73
  if (keep) out += `${header}\n${body}`;
74
74
  else changed = true;
75
75
  out += `${line}\n`;
76
76
  continue;
77
77
  }
78
- if (/^use_legacy_landlock[ \t]*=/.test(line)) {
78
+ if (/^[ \t]*use_legacy_landlock[ \t]*=/.test(line)) {
79
79
  changed = true;
80
80
  continue;
81
81
  }
@@ -83,7 +83,7 @@ function scrubDeprecatedFeaturesText(content: string): string {
83
83
  if (/[^ \t\f\v\r]/.test(line)) keep = true;
84
84
  continue;
85
85
  }
86
- if (/^\[features\][ \t]*$/.test(line)) {
86
+ if (/^[ \t]*\[features\][ \t]*$/.test(line)) {
87
87
  inFeatures = true;
88
88
  header = line;
89
89
  body = "";
@@ -111,7 +111,7 @@ function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
111
111
  change("Codex: scrubbed deprecated [features].use_legacy_landlock");
112
112
  }
113
113
 
114
- export const PLUGIN_TABLE_HEADER = /^\[plugins\."([^"]+)"\][ \t]*$/;
114
+ export const PLUGIN_TABLE_HEADER = /^[ \t]*\[plugins\."([^"]+)"\][ \t]*$/;
115
115
  /** Plugin ids the kit retired; their deployed tables are stripped on every sync. */
116
116
  const RETIRED_PLUGIN_IDS: Readonly<Record<string, true>> = {
117
117
  "effect-kit@docks": true,
@@ -130,7 +130,7 @@ export function removeRetiredPluginTablesText(content: string): string {
130
130
  skipping = RETIRED_PLUGIN_IDS[header[1]!] === true;
131
131
  if (skipping) continue;
132
132
  } else if (skipping) {
133
- if (!line.startsWith("[")) continue;
133
+ if (!isTomlHeaderLine(line)) continue;
134
134
  skipping = false;
135
135
  }
136
136
  out += `${line}\n`;
@@ -14,6 +14,7 @@ import {
14
14
 
15
15
  import { payloadDisplayPath } from "../payload";
16
16
  import { PLUGIN_TABLE_HEADER } from "./codexConfig";
17
+ import { isTomlHeaderLine } from "./codexToml";
17
18
  import { p, spawnProcess } from "./exec";
18
19
  import { recordFailure } from "./failures";
19
20
  import type { Ctx } from "./index";
@@ -107,20 +108,20 @@ function mergeMarketplace(repo: Json, user: Json): Json {
107
108
  // -------------------------------------------------------------- plugins ----
108
109
 
109
110
  /** codex::_marketplace_source — first `source =` inside [marketplaces.<name>]. */
110
- function marketplaceSource(marketplace: string, configFile: string): string {
111
+ export function marketplaceSource(marketplace: string, configFile: string): string {
111
112
  if (!existsSync(configFile)) return "";
112
113
  let inMarketplace = false;
113
114
  for (const line of readFileSync(configFile, "utf8").split("\n")) {
114
- if (line === `[marketplaces.${marketplace}]`) {
115
+ if (line.trim() === `[marketplaces.${marketplace}]`) {
115
116
  inMarketplace = true;
116
117
  continue;
117
118
  }
118
- if (line.startsWith("[")) inMarketplace = false;
119
+ if (isTomlHeaderLine(line)) inMarketplace = false;
119
120
  if (inMarketplace && /^[ \t]*source[ \t]*=/.test(line)) {
120
121
  return line
121
122
  .replace(/^[^=]+=[ \t]*/, "")
122
123
  .replace(/[ \t]*#.*/, "")
123
- .replace(/^"|"$/g, "");
124
+ .replace(/^(["'])(.*)\1$/, "$2");
124
125
  }
125
126
  }
126
127
  return "";
@@ -175,7 +176,7 @@ function enabledPluginIdsFromText(configText: string): Array<string> {
175
176
  enabled = false;
176
177
  continue;
177
178
  }
178
- if (line.startsWith("[")) {
179
+ if (isTomlHeaderLine(line)) {
179
180
  flush();
180
181
  plugin = "";
181
182
  enabled = false;
@@ -12,15 +12,19 @@ import { resolveEffort } from "../efforts";
12
12
  import type { Ctx } from "./index";
13
13
  import type { SettingEdit } from "./sharedTypes";
14
14
 
15
+ export function isTomlHeaderLine(line: string): boolean {
16
+ return /^[ \t]*\[/.test(line);
17
+ }
18
+
15
19
  export function replaceTopLevelSetting(content: string, key: string, replacement: string): string {
16
20
  const lines = content.split("\n");
17
21
  if (lines[lines.length - 1] === "") lines.pop(); // awk records exclude a trailing empty split artifact
18
- const keyRe = new RegExp(`^${key}[ \\t]*=`);
22
+ const keyRe = new RegExp(`^[ \\t]*${key}[ \\t]*=`);
19
23
  const out: Array<string> = [];
20
24
  let inTable = false;
21
25
  let replaced = false;
22
26
  for (const line of lines) {
23
- if (line.startsWith("[")) {
27
+ if (isTomlHeaderLine(line)) {
24
28
  if (!replaced) {
25
29
  out.push(replacement);
26
30
  replaced = true;
@@ -113,11 +117,11 @@ export function syncCodexEffort(ctx: Ctx, effort: string): void {
113
117
 
114
118
  export function mergeTopLevelSettings(sotConfigText: string, userConfig: string): void {
115
119
  for (const line of sotConfigText.split("\n")) {
116
- if (line.startsWith("[")) break;
120
+ if (isTomlHeaderLine(line)) break;
117
121
  if (/^[ \t]*($|#)/.test(line)) continue;
118
- if (!/^[A-Za-z0-9_.-]+[ \t]*=/.test(line)) continue;
119
- const key = line.slice(0, line.indexOf("=")).replace(/[ \t]+$/, "");
120
- replaceTopLevelSettingInFile(userConfig, key, line);
122
+ const setting = /^[ \t]*([A-Za-z0-9_.-]+)[ \t]*=/.exec(line);
123
+ if (setting === null) continue;
124
+ replaceTopLevelSettingInFile(userConfig, setting[1]!, line);
121
125
  }
122
126
  }
123
127
 
@@ -14,6 +14,7 @@ import { printModels, validateClaudeModel, validateCodexModel } from "./models";
14
14
  import { defaultLiveInputs, resolveCatalog } from "./liveModels";
15
15
  import { bunBootstrap } from "./bun";
16
16
  import { installedVersion, outdatedReport, present, report } from "./toolchain";
17
+ import { topLevelTomlString } from "../manifests";
17
18
 
18
19
  export async function modeModel(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
19
20
  const { echo, err, warn } = ctx.services.logger;
@@ -59,8 +60,8 @@ export async function modeModel(ctx: Ctx, args: ReadonlyArray<string>): Promise<
59
60
  err(`Failed to read ~/.codex/config.toml: ${String(result.error)}`);
60
61
  return 1;
61
62
  }
62
- echo(`deployed: ${tomlModelText(result.data)}`);
63
- echo(`SoT: ${tomlModelText(payloadText("SoT/.codex/config.toml"))}`);
63
+ echo(`deployed: ${topLevelTomlString(result.data, "model") ?? ""}`);
64
+ echo(`SoT: ${topLevelTomlString(payloadText("SoT/.codex/config.toml"), "model") ?? ""}`);
64
65
  }
65
66
  printModels(ctx, catalog);
66
67
  return 0;
@@ -112,15 +113,6 @@ function jsonModelText(text: string): string {
112
113
  return typeof v === "string" ? v : JSON.stringify(v);
113
114
  }
114
115
 
115
- /** `awk -F'"' '/^model[[:space:]]*=/{print $2; exit}'`. */
116
-
117
- function tomlModelText(text: string): string {
118
- for (const line of text.split("\n")) {
119
- if (/^model[ \t]*=/.test(line)) return line.split('"')[1] ?? "";
120
- }
121
- return "";
122
- }
123
-
124
116
  export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
125
117
  const { echo, err, verbose } = ctx.services.logger;
126
118
  const words = args.filter((arg) => !arg.startsWith("--"));
@@ -129,6 +121,10 @@ export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Prom
129
121
  for (const arg of args) {
130
122
  if (arg === "--verbose") ctx.verbose = true;
131
123
  }
124
+ if (words.length > (op === "ensure" ? 2 : 1)) {
125
+ err("Usage: toolchain [check|ensure <tool>|outdated [--refresh]]");
126
+ return 2;
127
+ }
132
128
 
133
129
  if (op === "check") {
134
130
  await report(ctx);
@@ -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.19.0"
4
+ export const GENERATED_PACKAGE_VERSION = "0.19.1"
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",
package/cli/src/main.ts CHANGED
@@ -32,8 +32,8 @@ const root = Command.make("docks-kit", {}, () =>
32
32
  " docks-kit update [--no-sync] self-update the kit, then sync",
33
33
  );
34
34
  yield* Console.log(" docks-kit model <claude|codex> [value] get/set the deployed model");
35
- yield* Console.log(" docks-kit models [tool] kit-verified model catalog");
36
- yield* Console.log(" docks-kit toolchain [check|ensure <tool>] verified-version floors");
35
+ yield* Console.log(" docks-kit models [tool] [--refresh] live model lists per harness");
36
+ yield* Console.log(" docks-kit toolchain [check|ensure|outdated] verified-version floors");
37
37
  yield* Console.log(" docks-kit status deployed-vs-SoT doctor view");
38
38
  yield* Console.log(" docks-kit plugins list plugin tri-state");
39
39
  yield* Console.log(" docks-kit skills list universal skills");
@@ -23,8 +23,8 @@ export const deployedClaudeSettings = (): any | undefined => {
23
23
  export function topLevelTomlString(text: string, setting: string): string | undefined {
24
24
  for (const line of text.split(/\r?\n/)) {
25
25
  if (/^\s*\[/.test(line)) return undefined;
26
- const assignment = line.match(/^\s*([A-Za-z0-9_-]+)\s*=\s*"([^"]+)"/);
27
- if (assignment?.[1] === setting) return assignment[2];
26
+ const assignment = line.match(/^\s*([A-Za-z0-9_-]+)\s*=\s*(?:"([^"]+)"|'([^']*)')/);
27
+ if (assignment?.[1] === setting) return assignment[2] ?? assignment[3];
28
28
  }
29
29
  return undefined;
30
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docks-kit",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
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",