docks-kit 0.17.1 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/AGENTS.md +36 -32
  2. package/cli/docs/omp-models.md +88 -53
  3. package/cli/src/argv.ts +38 -469
  4. package/cli/src/argvSurface.ts +371 -0
  5. package/cli/src/argvValidate.ts +122 -0
  6. package/cli/src/commands/docs.ts +57 -42
  7. package/cli/src/commands/harnesses.ts +38 -45
  8. package/cli/src/commands/model.ts +48 -53
  9. package/cli/src/commands/models.ts +24 -26
  10. package/cli/src/commands/omp.ts +132 -112
  11. package/cli/src/commands/plugins.ts +20 -20
  12. package/cli/src/commands/skills.ts +22 -20
  13. package/cli/src/commands/status.ts +87 -80
  14. package/cli/src/commands/sync.ts +113 -87
  15. package/cli/src/commands/toolchain.ts +21 -21
  16. package/cli/src/commands/update.ts +139 -117
  17. package/cli/src/efforts.ts +35 -34
  18. package/cli/src/engine-native/DESIGN.md +30 -30
  19. package/cli/src/engine-native/bun.ts +77 -55
  20. package/cli/src/engine-native/claudeLsp.ts +154 -0
  21. package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
  22. package/cli/src/engine-native/claudePluginPasses.ts +348 -0
  23. package/cli/src/engine-native/claudeRemovals.ts +195 -0
  24. package/cli/src/engine-native/claudeRetired.ts +4 -4
  25. package/cli/src/engine-native/claudeRuntime.ts +94 -68
  26. package/cli/src/engine-native/claudeSettings.ts +174 -0
  27. package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
  28. package/cli/src/engine-native/claudeSync.ts +176 -456
  29. package/cli/src/engine-native/codexConfig.ts +158 -0
  30. package/cli/src/engine-native/codexHooks.ts +91 -0
  31. package/cli/src/engine-native/codexPlugins.ts +290 -0
  32. package/cli/src/engine-native/codexStatus.ts +24 -0
  33. package/cli/src/engine-native/codexSync.ts +110 -568
  34. package/cli/src/engine-native/codexToml.ts +140 -138
  35. package/cli/src/engine-native/deps.ts +144 -107
  36. package/cli/src/engine-native/engineCtx.ts +126 -0
  37. package/cli/src/engine-native/exec.ts +98 -87
  38. package/cli/src/engine-native/failures.ts +3 -3
  39. package/cli/src/engine-native/harnesses.ts +62 -72
  40. package/cli/src/engine-native/index.ts +34 -315
  41. package/cli/src/engine-native/jq.ts +25 -24
  42. package/cli/src/engine-native/logger.ts +98 -100
  43. package/cli/src/engine-native/models.ts +50 -45
  44. package/cli/src/engine-native/modes.ts +83 -80
  45. package/cli/src/engine-native/ompFileDeploy.ts +111 -0
  46. package/cli/src/engine-native/ompMarketplace.ts +136 -0
  47. package/cli/src/engine-native/ompOverlay.ts +67 -64
  48. package/cli/src/engine-native/ompPaths.ts +49 -38
  49. package/cli/src/engine-native/ompPlugins.ts +170 -0
  50. package/cli/src/engine-native/ompSync.ts +64 -408
  51. package/cli/src/engine-native/ompYaml.ts +43 -41
  52. package/cli/src/engine-native/os/darwin.ts +5 -5
  53. package/cli/src/engine-native/os/index.ts +15 -15
  54. package/cli/src/engine-native/os/linux.ts +5 -5
  55. package/cli/src/engine-native/os/posix.ts +28 -24
  56. package/cli/src/engine-native/os/targets.ts +17 -17
  57. package/cli/src/engine-native/os/types.ts +31 -31
  58. package/cli/src/engine-native/os/windows.ts +61 -61
  59. package/cli/src/engine-native/parseArgs.ts +122 -372
  60. package/cli/src/engine-native/parseHelp.ts +66 -0
  61. package/cli/src/engine-native/parseModifiers.ts +222 -0
  62. package/cli/src/engine-native/services.ts +34 -34
  63. package/cli/src/engine-native/settings.ts +15 -15
  64. package/cli/src/engine-native/sharedTypes.d.ts +46 -0
  65. package/cli/src/engine-native/skillsInstall.ts +105 -0
  66. package/cli/src/engine-native/skillsLinks.ts +203 -0
  67. package/cli/src/engine-native/skillsManifest.ts +16 -0
  68. package/cli/src/engine-native/skillsPrune.ts +108 -0
  69. package/cli/src/engine-native/skillsSync.ts +28 -362
  70. package/cli/src/engine-native/syncConcurrency.ts +55 -0
  71. package/cli/src/engine-native/syncDispatch.ts +141 -0
  72. package/cli/src/engine-native/toolchain.ts +65 -62
  73. package/cli/src/engine.ts +65 -57
  74. package/cli/src/generated/sotPayload.ts +7 -7
  75. package/cli/src/kitHome.ts +44 -40
  76. package/cli/src/main.ts +63 -56
  77. package/cli/src/manifests.ts +53 -54
  78. package/cli/src/md.d.ts +2 -2
  79. package/cli/src/payload.ts +9 -9
  80. package/cli/src/services.ts +21 -13
  81. package/cli/tsconfig.json +4 -1
  82. package/package.json +9 -2
  83. package/cli/src/engine-native/claudePlugins.ts +0 -510
@@ -2,162 +2,165 @@
2
2
  * Direct modes for `model` and `toolchain`. Same public argv vocabulary,
3
3
  * golden-tested messages, same exit codes.
4
4
  */
5
- import { p } from "./exec"
6
- import { readFileSync } from "node:fs"
7
- import { payloadText } from "../payload"
5
+ import { p } from "./exec";
6
+ import { readFileSync } from "node:fs";
7
+ import { payloadText } from "../payload";
8
8
 
9
- import { syncClaudeModel } from "./claudeSettingsModifiers"
10
- import { syncCodexModel } from "./codexToml"
11
- import type { Ctx } from "./index"
12
- import { isObject, parseJson, type Json } from "./jq"
13
- import { printModels, validateClaudeModel, validateCodexModel } from "./models"
14
- import { bunBootstrap } from "./bun"
15
- import { installedVersion, present, report } from "./toolchain"
9
+ import { syncClaudeModel } from "./claudeSettingsModifiers";
10
+ import { syncCodexModel } from "./codexToml";
11
+ import type { Ctx } from "./index";
12
+ import { isObject, parseJson, type Json } from "./jq";
13
+ import { printModels, validateClaudeModel, validateCodexModel } from "./models";
14
+ import { bunBootstrap } from "./bun";
15
+ import { installedVersion, present, report } from "./toolchain";
16
16
 
17
17
  export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
18
- const { echo, err, warn } = ctx.services.logger
19
- let tool = ""
20
- let value = ""
18
+ const { echo, err, warn } = ctx.services.logger;
19
+ let tool = "";
20
+ let value = "";
21
21
  for (const arg of args) {
22
- if (arg === "--dry-run") ctx.dryRun = true
22
+ if (arg === "--dry-run") ctx.dryRun = true;
23
23
  else if (arg === "--verbose") {
24
- ctx.verbose = true
25
- } else if (arg === "claude" || arg === "codex") tool = arg
24
+ ctx.verbose = true;
25
+ } else if (arg === "claude" || arg === "codex") tool = arg;
26
26
  else if (arg.startsWith("-")) {
27
- err(`Unknown flag for model: ${arg}`)
28
- return 2
29
- } else value = arg
27
+ err(`Unknown flag for model: ${arg}`);
28
+ return 2;
29
+ } else value = arg;
30
30
  }
31
31
  if (tool === "") {
32
- err("Usage: model <claude|codex> [value] [--dry-run]")
33
- return 2
32
+ err("Usage: model <claude|codex> [value] [--dry-run]");
33
+ return 2;
34
34
  }
35
35
 
36
36
  if (value === "") {
37
37
  if (tool === "claude") {
38
- const deployed = p(ctx.home, ".claude", "settings.json")
39
- const result = readConfig(deployed)
38
+ const deployed = p(ctx.home, ".claude", "settings.json");
39
+ const result = readConfig(deployed);
40
40
  if (result.kind === "missing") {
41
- warn("~/.claude/settings.json missing")
42
- return 0
41
+ warn("~/.claude/settings.json missing");
42
+ return 0;
43
43
  }
44
44
  if (result.kind === "read-error") {
45
- err(`Failed to read ~/.claude/settings.json: ${String(result.error)}`)
46
- return 1
45
+ err(`Failed to read ~/.claude/settings.json: ${String(result.error)}`);
46
+ return 1;
47
47
  }
48
- echo(`deployed: ${jsonModelText(result.data)}`)
49
- echo(`SoT: ${jsonModelText(payloadText("SoT/.claude/settings.json"))}`)
48
+ echo(`deployed: ${jsonModelText(result.data)}`);
49
+ echo(`SoT: ${jsonModelText(payloadText("SoT/.claude/settings.json"))}`);
50
50
  } else {
51
- const deployed = p(ctx.home, ".codex", "config.toml")
52
- const result = readConfig(deployed)
51
+ const deployed = p(ctx.home, ".codex", "config.toml");
52
+ const result = readConfig(deployed);
53
53
  if (result.kind === "missing") {
54
- warn("~/.codex/config.toml missing")
55
- return 0
54
+ warn("~/.codex/config.toml missing");
55
+ return 0;
56
56
  }
57
57
  if (result.kind === "read-error") {
58
- err(`Failed to read ~/.codex/config.toml: ${String(result.error)}`)
59
- return 1
58
+ err(`Failed to read ~/.codex/config.toml: ${String(result.error)}`);
59
+ return 1;
60
60
  }
61
- echo(`deployed: ${tomlModelText(result.data)}`)
62
- echo(`SoT: ${tomlModelText(payloadText("SoT/.codex/config.toml"))}`)
61
+ echo(`deployed: ${tomlModelText(result.data)}`);
62
+ echo(`SoT: ${tomlModelText(payloadText("SoT/.codex/config.toml"))}`);
63
63
  }
64
- printModels(ctx, tool)
65
- return 0
64
+ printModels(ctx, tool);
65
+ return 0;
66
66
  }
67
67
 
68
68
  if (tool === "claude") {
69
69
  if (!validateClaudeModel(ctx, value)) {
70
- printModels(ctx, "claude")
71
- err(`Invalid Claude model '${value}'`)
72
- return 2
70
+ printModels(ctx, "claude");
71
+ err(`Invalid Claude model '${value}'`);
72
+ return 2;
73
73
  }
74
- syncClaudeModel(ctx, value)
74
+ syncClaudeModel(ctx, value);
75
75
  } else {
76
76
  if (!validateCodexModel(ctx, value)) {
77
- printModels(ctx, "codex")
78
- err(`Invalid Codex model '${value}'`)
79
- return 2
77
+ printModels(ctx, "codex");
78
+ err(`Invalid Codex model '${value}'`);
79
+ return 2;
80
80
  }
81
- syncCodexModel(ctx, value)
81
+ syncCodexModel(ctx, value);
82
82
  }
83
- return 0
83
+ return 0;
84
84
  }
85
85
 
86
86
  type ConfigReadResult =
87
87
  | { readonly kind: "missing" }
88
88
  | { readonly kind: "read-error"; readonly error: unknown }
89
- | { readonly kind: "data"; readonly data: string }
89
+ | { readonly kind: "data"; readonly data: string };
90
90
 
91
91
  function readConfig(file: string): ConfigReadResult {
92
92
  try {
93
- return { kind: "data", data: readFileSync(file, "utf8") }
93
+ return { kind: "data", data: readFileSync(file, "utf8") };
94
94
  } catch (error) {
95
95
  const code =
96
- typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
96
+ typeof error === "object" &&
97
+ error !== null &&
98
+ "code" in error &&
99
+ typeof error.code === "string"
97
100
  ? error.code
98
- : undefined
99
- return code === "ENOENT" ? { kind: "missing" } : { kind: "read-error", error }
101
+ : undefined;
102
+ return code === "ENOENT" ? { kind: "missing" } : { kind: "read-error", error };
100
103
  }
101
104
  }
102
105
 
103
106
  function jsonModelText(text: string): string {
104
- const doc = parseJson(text)
105
- if (doc === undefined) return ""
106
- const v: Json | undefined = isObject(doc) ? doc["model"] : undefined
107
- if (v === undefined || v === null || v === false) return "default (unset)"
108
- return typeof v === "string" ? v : JSON.stringify(v)
107
+ const doc = parseJson(text);
108
+ if (doc === undefined) return "";
109
+ const v: Json | undefined = isObject(doc) ? doc["model"] : undefined;
110
+ if (v === undefined || v === null || v === false) return "default (unset)";
111
+ return typeof v === "string" ? v : JSON.stringify(v);
109
112
  }
110
113
 
111
114
  /** `awk -F'"' '/^model[[:space:]]*=/{print $2; exit}'`. */
112
115
 
113
116
  function tomlModelText(text: string): string {
114
117
  for (const line of text.split("\n")) {
115
- if (/^model[ \t]*=/.test(line)) return line.split('"')[1] ?? ""
118
+ if (/^model[ \t]*=/.test(line)) return line.split('"')[1] ?? "";
116
119
  }
117
- return ""
120
+ return "";
118
121
  }
119
122
 
120
123
  export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
121
- const { echo, err, verbose } = ctx.services.logger
122
- const words = args.filter((arg) => !arg.startsWith("--"))
123
- const op = words[0] ?? "check"
124
- const tool = words[1] ?? ""
124
+ const { echo, err, verbose } = ctx.services.logger;
125
+ const words = args.filter((arg) => !arg.startsWith("--"));
126
+ const op = words[0] ?? "check";
127
+ const tool = words[1] ?? "";
125
128
  for (const arg of args) {
126
- if (arg === "--verbose") ctx.verbose = true
129
+ if (arg === "--verbose") ctx.verbose = true;
127
130
  }
128
131
 
129
132
  if (op === "check") {
130
- await report(ctx)
131
- return 0
133
+ await report(ctx);
134
+ return 0;
132
135
  }
133
136
  if (op !== "ensure") {
134
- err("Usage: toolchain [check|ensure <tool>]")
135
- return 2
137
+ err("Usage: toolchain [check|ensure <tool>]");
138
+ return 2;
136
139
  }
137
140
  if (tool === "") {
138
- err("Usage: toolchain ensure <tool>")
139
- return 2
141
+ err("Usage: toolchain ensure <tool>");
142
+ return 2;
140
143
  }
141
144
  switch (tool) {
142
145
  case "bun": {
143
146
  // `bun` is the one managed tool, and its bootstrap is silent when Bun is
144
147
  // already installed. Probe first so the no-op confirmation the `--verbose`
145
148
  // contract promises is not mistaken for a fresh install.
146
- const alreadyInstalled = present(ctx, "bun")
147
- if ((await bunBootstrap(ctx, ctx.services)).kind !== "ready") return 1
149
+ const alreadyInstalled = present(ctx, "bun");
150
+ if ((await bunBootstrap(ctx, ctx.services)).kind !== "ready") return 1;
148
151
  if (alreadyInstalled) {
149
152
  // A present tool whose --version cannot be read reports `unknown` in the
150
153
  // doctor table; keep the same vocabulary rather than an empty pair of
151
154
  // parentheses.
152
- const probed = await installedVersion(ctx, "bun")
153
- const installed = probed === "" ? "version unknown" : probed
154
- if (ctx.dryRun) echo(`[dry-run] bun up to date (${installed})`)
155
- else verbose(`bun up to date (${installed})`)
155
+ const probed = await installedVersion(ctx, "bun");
156
+ const installed = probed === "" ? "version unknown" : probed;
157
+ if (ctx.dryRun) echo(`[dry-run] bun up to date (${installed})`);
158
+ else verbose(`bun up to date (${installed})`);
156
159
  }
157
- return 0
160
+ return 0;
158
161
  }
159
162
  default:
160
- err("toolchain ensure supports managed tools only (bun)")
161
- return 2
163
+ err("toolchain ensure supports managed tools only (bun)");
164
+ return 2;
162
165
  }
163
166
  }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * EngineNative omp file deploy: private file modes, whole-file sync, and
3
+ * merged-YAML sync for the agent directory. Split from ompSync.ts; the
4
+ * orchestration still lives there.
5
+ */
6
+ import {
7
+ chmodSync,
8
+ copyFileSync,
9
+ existsSync,
10
+ mkdirSync,
11
+ readFileSync,
12
+ renameSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { basename } from "node:path";
16
+
17
+ import { payloadDisplayPath, payloadText, type PayloadPath } from "../payload";
18
+ import type { Ctx } from "./index";
19
+
20
+ export type OmpTextPayloadPath = Extract<PayloadPath, `SoT/.omp/${string}`>;
21
+
22
+ // ------------------------------------------------------------ file modes ----
23
+
24
+ /**
25
+ * `mkdirSync` and `writeFileSync` apply their `mode` only when they create the
26
+ * path, so an existing world-readable directory or file would keep its mode.
27
+ * The explicit chmod reproduces `install -d -m 0700` / `install -m 0600`.
28
+ */
29
+ export function ensureDirectory(directory: string): void {
30
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
31
+ chmodSync(directory, 0o700);
32
+ }
33
+
34
+ function writePrivateFile(target: string, content: string): void {
35
+ writeFileSync(target, content, { mode: 0o600 });
36
+ chmodSync(target, 0o600);
37
+ }
38
+
39
+ function backupPrivateFile(target: string): void {
40
+ copyFileSync(target, `${target}.bak`);
41
+ chmodSync(`${target}.bak`, 0o600);
42
+ }
43
+
44
+ // --------------------------------------------------------------- config ----
45
+
46
+ export function syncWholeFile(
47
+ ctx: Ctx,
48
+ sourcePath: OmpTextPayloadPath,
49
+ target: string,
50
+ alreadyMessage: string,
51
+ syncedMessage: string,
52
+ ): void {
53
+ const { change, echo, verbose } = ctx.services.logger;
54
+ const source = payloadDisplayPath(sourcePath);
55
+ const content = payloadText(sourcePath);
56
+
57
+ if (ctx.dryRun) {
58
+ echo(`[dry-run] cp ${source} -> ${target}`);
59
+ return;
60
+ }
61
+
62
+ if (existsSync(target) && readFileSync(target, "utf8") === content) {
63
+ verbose(alreadyMessage);
64
+ return;
65
+ }
66
+ if (existsSync(target)) backupPrivateFile(target);
67
+ writePrivateFile(target, content);
68
+ change(syncedMessage);
69
+ ctx.nextStepTriggers.ompRestart = true;
70
+ }
71
+
72
+ export function syncMergedYaml(
73
+ ctx: Ctx,
74
+ sourcePath: OmpTextPayloadPath,
75
+ target: string,
76
+ merge: (sotText: string, deployedText: string) => string,
77
+ ): void {
78
+ const { change, echo, verbose } = ctx.services.logger;
79
+ const source = payloadDisplayPath(sourcePath);
80
+ const sotText = payloadText(sourcePath);
81
+ const name = basename(sourcePath);
82
+
83
+ if (!existsSync(target)) {
84
+ if (ctx.dryRun) {
85
+ echo(`[dry-run] cp ${source} -> ${target}`);
86
+ return;
87
+ }
88
+ writePrivateFile(target, sotText);
89
+ change(`omp ${name} installed`);
90
+ ctx.nextStepTriggers.ompRestart = true;
91
+ return;
92
+ }
93
+
94
+ const deployedText = readFileSync(target, "utf8");
95
+ const merged = merge(sotText, deployedText);
96
+ if (merged === deployedText) {
97
+ verbose(`omp ${name} already in sync`);
98
+ return;
99
+ }
100
+ if (ctx.dryRun) {
101
+ echo(`[dry-run] merge ${source} -> ${target} (backup at ${target}.bak)`);
102
+ return;
103
+ }
104
+
105
+ backupPrivateFile(target);
106
+ writePrivateFile(`${target}.tmp`, merged);
107
+ renameSync(`${target}.tmp`, target);
108
+ chmodSync(target, 0o600);
109
+ change(`omp ${name} merged (backup at ${name}.bak)`);
110
+ ctx.nextStepTriggers.ompRestart = true;
111
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * EngineNative omp marketplace pass: registry detection plus register, adopt,
3
+ * and refresh flows. Split from ompSync.ts; the orchestration still lives
4
+ * there. `firstOutputLine` is shared with the plugin pass in ompPlugins.ts.
5
+ */
6
+ import { existsSync, readFileSync } from "node:fs";
7
+
8
+ import { spawnProcess, type AsyncProcessResult } from "./exec";
9
+ import { recordFailure } from "./failures";
10
+ import type { Ctx } from "./index";
11
+ import { isObject, parseJson } from "./jq";
12
+
13
+ const MARKETPLACE_NAME = "docks";
14
+ const MARKETPLACE_SOURCE = "https://github.com/DocksDocks/docks.git";
15
+
16
+ // ---------------------------------------------------------- marketplace ----
17
+
18
+ function registryHasDocks(registryFile: string): boolean {
19
+ if (!existsSync(registryFile)) return false;
20
+
21
+ let registryText: string;
22
+ try {
23
+ registryText = readFileSync(registryFile, "utf8");
24
+ } catch {
25
+ return false;
26
+ }
27
+ const registry = parseJson(registryText);
28
+ if (registry === undefined) return false;
29
+ if (isObject(registry) && Object.hasOwn(registry, MARKETPLACE_NAME)) return true;
30
+
31
+ const entries = Array.isArray(registry)
32
+ ? registry
33
+ : isObject(registry) && Array.isArray(registry["marketplaces"])
34
+ ? registry["marketplaces"]
35
+ : [];
36
+ return entries.some((entry) => isObject(entry) && entry["name"] === MARKETPLACE_NAME);
37
+ }
38
+
39
+ export function firstOutputLine(result: AsyncProcessResult): string {
40
+ const output = `${result.stdout}${result.stderr}`;
41
+ return output.split("\n")[0] || "unknown error";
42
+ }
43
+
44
+ /**
45
+ * Three states, because upstream `getMarketplacesRegistryPath` copies a legacy
46
+ * `configRoot` registry forward the first time it resolves an XDG data root:
47
+ * the active registry lists docks, only the legacy registry lists it, or
48
+ * neither does. In the middle state the kit never copies user data itself; it
49
+ * takes the refresh path, whose own resolution inside omp performs that
50
+ * adoption and leaves the active registry present.
51
+ */
52
+ export async function syncMarketplace(
53
+ ctx: Ctx,
54
+ registryFile: string,
55
+ legacyRegistryFile?: string,
56
+ ): Promise<void> {
57
+ const { change, clearProgress, echo, progress, verbose } = ctx.services.logger;
58
+ const registered = registryHasDocks(registryFile);
59
+ const adoptable =
60
+ !registered && legacyRegistryFile !== undefined && registryHasDocks(legacyRegistryFile);
61
+
62
+ if (ctx.dryRun) {
63
+ if (adoptable) {
64
+ echo(
65
+ ctx.skipPluginRefresh === true
66
+ ? "[dry-run] omp plugin marketplace list"
67
+ : `[dry-run] omp plugin marketplace update ${MARKETPLACE_NAME}`,
68
+ );
69
+ } else if (registered) verbose("omp docks marketplace already registered");
70
+ else echo(`[dry-run] omp plugin marketplace add ${MARKETPLACE_SOURCE}`);
71
+ return;
72
+ }
73
+
74
+ // syncPlugins owns the single skip message for both missing CLIs, and it runs
75
+ // right after this pass. Without omp no marketplace command can run at all,
76
+ // and without git a marketplace clone cannot resolve, so both are deliberate
77
+ // skips: return silently instead of spawning and recording a failure.
78
+ if (ctx.services.deps.probe("omp").state === "missing") return;
79
+ if (ctx.services.deps.probe("git").state === "missing") return;
80
+
81
+ if (!registered && !adoptable) {
82
+ progress("Registering omp docks marketplace...");
83
+ const result = await spawnProcess("omp", ["plugin", "marketplace", "add", MARKETPLACE_SOURCE], {
84
+ stdio: ["ignore", "pipe", "pipe"],
85
+ });
86
+ clearProgress();
87
+ if (result.error === undefined && result.exitCode === 0) {
88
+ change("omp docks marketplace registered");
89
+ } else {
90
+ recordFailure(
91
+ ctx,
92
+ `omp docks marketplace registration failed: ${firstOutputLine(result)}; run manually: omp plugin marketplace add ${MARKETPLACE_SOURCE}`,
93
+ );
94
+ }
95
+ return;
96
+ }
97
+
98
+ if (ctx.skipPluginRefresh === true) {
99
+ if (!adoptable) {
100
+ verbose("omp docks marketplace already registered; refresh-only update skipped");
101
+ return;
102
+ }
103
+
104
+ // Adoption only needs omp to resolve the registry path. `marketplace list`
105
+ // does that and fetches nothing, so it stays inside the flag's contract
106
+ // while leaving the active registry present.
107
+ progress("Adopting omp docks marketplace registry...");
108
+ const listed = await spawnProcess("omp", ["plugin", "marketplace", "list"], {
109
+ stdio: ["ignore", "ignore", "pipe"],
110
+ });
111
+ clearProgress();
112
+ if (listed.error === undefined && listed.exitCode === 0) {
113
+ change("omp docks marketplace registry adopted; refresh-only update skipped");
114
+ } else {
115
+ recordFailure(
116
+ ctx,
117
+ `omp docks marketplace adoption failed: ${firstOutputLine(listed)}; run manually: omp plugin marketplace list`,
118
+ );
119
+ }
120
+ return;
121
+ }
122
+
123
+ progress("Updating omp docks marketplace...");
124
+ const result = await spawnProcess("omp", ["plugin", "marketplace", "update", MARKETPLACE_NAME], {
125
+ stdio: ["ignore", "pipe", "pipe"],
126
+ });
127
+ clearProgress();
128
+ if (result.error === undefined && result.exitCode === 0) {
129
+ verbose("omp docks marketplace refreshed");
130
+ } else {
131
+ recordFailure(
132
+ ctx,
133
+ `omp docks marketplace update failed: ${firstOutputLine(result)}; run manually: omp plugin marketplace update ${MARKETPLACE_NAME}`,
134
+ );
135
+ }
136
+ }