docks-kit 0.17.1 → 0.17.3

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 (87) hide show
  1. package/AGENTS.md +41 -33
  2. package/cli/docs/omp-context.md +104 -0
  3. package/cli/docs/omp-models.md +106 -56
  4. package/cli/src/argv.ts +38 -469
  5. package/cli/src/argvSurface.ts +371 -0
  6. package/cli/src/argvValidate.ts +122 -0
  7. package/cli/src/commands/docs.ts +62 -42
  8. package/cli/src/commands/harnesses.ts +38 -45
  9. package/cli/src/commands/model.ts +48 -53
  10. package/cli/src/commands/models.ts +24 -26
  11. package/cli/src/commands/omp.ts +132 -112
  12. package/cli/src/commands/plugins.ts +20 -20
  13. package/cli/src/commands/skills.ts +22 -20
  14. package/cli/src/commands/status.ts +87 -80
  15. package/cli/src/commands/sync.ts +113 -87
  16. package/cli/src/commands/toolchain.ts +21 -21
  17. package/cli/src/commands/update.ts +139 -117
  18. package/cli/src/efforts.ts +35 -34
  19. package/cli/src/engine-native/DESIGN.md +30 -30
  20. package/cli/src/engine-native/bun.ts +77 -55
  21. package/cli/src/engine-native/claudeLsp.ts +154 -0
  22. package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
  23. package/cli/src/engine-native/claudePluginPasses.ts +348 -0
  24. package/cli/src/engine-native/claudeRemovals.ts +195 -0
  25. package/cli/src/engine-native/claudeRetired.ts +4 -4
  26. package/cli/src/engine-native/claudeRuntime.ts +94 -68
  27. package/cli/src/engine-native/claudeSettings.ts +174 -0
  28. package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
  29. package/cli/src/engine-native/claudeSync.ts +176 -456
  30. package/cli/src/engine-native/codexConfig.ts +158 -0
  31. package/cli/src/engine-native/codexHooks.ts +91 -0
  32. package/cli/src/engine-native/codexPlugins.ts +290 -0
  33. package/cli/src/engine-native/codexStatus.ts +24 -0
  34. package/cli/src/engine-native/codexSync.ts +110 -568
  35. package/cli/src/engine-native/codexToml.ts +140 -138
  36. package/cli/src/engine-native/deps.ts +144 -107
  37. package/cli/src/engine-native/engineCtx.ts +126 -0
  38. package/cli/src/engine-native/exec.ts +98 -87
  39. package/cli/src/engine-native/failures.ts +3 -3
  40. package/cli/src/engine-native/harnesses.ts +62 -72
  41. package/cli/src/engine-native/index.ts +34 -315
  42. package/cli/src/engine-native/jq.ts +25 -24
  43. package/cli/src/engine-native/logger.ts +98 -100
  44. package/cli/src/engine-native/models.ts +50 -45
  45. package/cli/src/engine-native/modes.ts +83 -80
  46. package/cli/src/engine-native/ompFileDeploy.ts +111 -0
  47. package/cli/src/engine-native/ompMarketplace.ts +136 -0
  48. package/cli/src/engine-native/ompOverlay.ts +67 -64
  49. package/cli/src/engine-native/ompPaths.ts +49 -38
  50. package/cli/src/engine-native/ompPlugins.ts +170 -0
  51. package/cli/src/engine-native/ompRemovals.ts +159 -0
  52. package/cli/src/engine-native/ompSync.ts +69 -409
  53. package/cli/src/engine-native/ompYaml.ts +43 -41
  54. package/cli/src/engine-native/os/darwin.ts +5 -5
  55. package/cli/src/engine-native/os/index.ts +15 -15
  56. package/cli/src/engine-native/os/linux.ts +5 -5
  57. package/cli/src/engine-native/os/posix.ts +28 -24
  58. package/cli/src/engine-native/os/targets.ts +17 -17
  59. package/cli/src/engine-native/os/types.ts +31 -31
  60. package/cli/src/engine-native/os/windows.ts +61 -61
  61. package/cli/src/engine-native/parseArgs.ts +122 -372
  62. package/cli/src/engine-native/parseHelp.ts +66 -0
  63. package/cli/src/engine-native/parseModifiers.ts +222 -0
  64. package/cli/src/engine-native/services.ts +34 -34
  65. package/cli/src/engine-native/settings.ts +15 -15
  66. package/cli/src/engine-native/sharedTypes.d.ts +46 -0
  67. package/cli/src/engine-native/skillsInstall.ts +105 -0
  68. package/cli/src/engine-native/skillsLinks.ts +203 -0
  69. package/cli/src/engine-native/skillsManifest.ts +16 -0
  70. package/cli/src/engine-native/skillsPrune.ts +108 -0
  71. package/cli/src/engine-native/skillsSync.ts +28 -362
  72. package/cli/src/engine-native/syncConcurrency.ts +55 -0
  73. package/cli/src/engine-native/syncDispatch.ts +141 -0
  74. package/cli/src/engine-native/toolchain.ts +65 -62
  75. package/cli/src/engine.ts +65 -57
  76. package/cli/src/generated/sotPayload.ts +7 -7
  77. package/cli/src/kitHome.ts +44 -40
  78. package/cli/src/main.ts +63 -56
  79. package/cli/src/manifests.ts +53 -54
  80. package/cli/src/md.d.ts +2 -2
  81. package/cli/src/payload.ts +9 -9
  82. package/cli/src/services.ts +21 -13
  83. package/cli/tsconfig.json +4 -1
  84. package/docks-kit +13 -2
  85. package/docks-kit.ps1 +21 -6
  86. package/package.json +10 -2
  87. package/cli/src/engine-native/claudePlugins.ts +0 -510
@@ -3,14 +3,14 @@
3
3
  * config for one run only, so every key that can cause paid spend is set here
4
4
  * and every other key falls through to the deployed paid config.
5
5
  */
6
- import { createHash } from "node:crypto"
7
- import type { OmpSessionModel } from "./harnesses"
6
+ import { createHash } from "node:crypto";
7
+ import type { OmpSessionModel } from "./harnesses";
8
8
 
9
9
  export interface CatalogModel {
10
- readonly selector: string
11
- readonly name: string
12
- readonly thinking: ReadonlyArray<string>
13
- readonly contextWindow: number
10
+ readonly selector: string;
11
+ readonly name: string;
12
+ readonly thinking: ReadonlyArray<string>;
13
+ readonly contextWindow: number;
14
14
  }
15
15
 
16
16
  /** Thinking levels from lowest to highest effort. */
@@ -21,7 +21,7 @@ export const THINKING_LADDER: ReadonlyArray<string> = [
21
21
  "high",
22
22
  "xhigh",
23
23
  "max",
24
- ]
24
+ ];
25
25
 
26
26
  /**
27
27
  * One overlay file per model, so a second launcher on another model cannot
@@ -32,9 +32,9 @@ export const THINKING_LADDER: ReadonlyArray<string> = [
32
32
  * two distinct models on two distinct files.
33
33
  */
34
34
  export function overlayFileName(selector: string): string {
35
- const safe = selector.replace(/[^A-Za-z0-9._-]/g, "-")
36
- const digest = createHash("sha256").update(selector).digest("hex").slice(0, 8)
37
- return `omp-free-${safe}-${digest}.yml`
35
+ const safe = selector.replace(/[^A-Za-z0-9._-]/g, "-");
36
+ const digest = createHash("sha256").update(selector).digest("hex").slice(0, 8);
37
+ return `omp-free-${safe}-${digest}.yml`;
38
38
  }
39
39
 
40
40
  /** Role keys in the order used by SoT/.omp/config.yml for readable diffs. */
@@ -51,7 +51,8 @@ const ROLE_ORDER: ReadonlyArray<string> = [
51
51
  "slow",
52
52
  "fable",
53
53
  "switch_fable",
54
- ]
54
+ "astra",
55
+ ];
55
56
 
56
57
  /** Retry chain keys in the order used by SoT/.omp/config.yml. */
57
58
  const FALLBACK_ORDER: ReadonlyArray<string> = [
@@ -64,7 +65,8 @@ const FALLBACK_ORDER: ReadonlyArray<string> = [
64
65
  "commit",
65
66
  "switch_fable",
66
67
  "fable",
67
- ]
68
+ "astra",
69
+ ];
68
70
 
69
71
  /**
70
72
  * Collect free models from parsed `omp models --json` output. The function is
@@ -73,36 +75,36 @@ const FALLBACK_ORDER: ReadonlyArray<string> = [
73
75
  */
74
76
  export function parseFreeModels(catalogJson: unknown): ReadonlyArray<CatalogModel> {
75
77
  if (typeof catalogJson !== "object" || catalogJson === null || Array.isArray(catalogJson)) {
76
- return []
78
+ return [];
77
79
  }
78
- const models = (catalogJson as Record<string, unknown>)["models"]
79
- if (!Array.isArray(models)) return []
80
- const found: Array<CatalogModel> = []
80
+ const models = (catalogJson as Record<string, unknown>)["models"];
81
+ if (!Array.isArray(models)) return [];
82
+ const found: Array<CatalogModel> = [];
81
83
  for (const row of models) {
82
- if (typeof row !== "object" || row === null || Array.isArray(row)) continue
83
- const record = row as Record<string, unknown>
84
- const selector = record["selector"]
85
- if (typeof selector !== "string" || selector.length === 0) continue
86
- const cost = record["cost"]
87
- if (typeof cost !== "object" || cost === null || Array.isArray(cost)) continue
88
- const charges = cost as Record<string, unknown>
89
- if (charges["input"] !== 0 || charges["output"] !== 0) continue
90
- const rawName = record["name"]
91
- const name = typeof rawName === "string" && rawName.length > 0 ? rawName : selector
92
- const rawLevels = record["thinking"]
84
+ if (typeof row !== "object" || row === null || Array.isArray(row)) continue;
85
+ const record = row as Record<string, unknown>;
86
+ const selector = record["selector"];
87
+ if (typeof selector !== "string" || selector.length === 0) continue;
88
+ const cost = record["cost"];
89
+ if (typeof cost !== "object" || cost === null || Array.isArray(cost)) continue;
90
+ const charges = cost as Record<string, unknown>;
91
+ if (charges["input"] !== 0 || charges["output"] !== 0) continue;
92
+ const rawName = record["name"];
93
+ const name = typeof rawName === "string" && rawName.length > 0 ? rawName : selector;
94
+ const rawLevels = record["thinking"];
93
95
  // Keep only known ladder levels in ladder order so an unknown future
94
96
  // level never reaches a role value that omp would reject.
95
97
  const thinking = THINKING_LADDER.filter(
96
98
  (level) => Array.isArray(rawLevels) && (rawLevels as Array<unknown>).includes(level),
97
- )
98
- const rawWindow = record["contextWindow"]
99
+ );
100
+ const rawWindow = record["contextWindow"];
99
101
  const contextWindow =
100
- typeof rawWindow === "number" && Number.isFinite(rawWindow) ? rawWindow : 0
101
- found.push({ selector, name, thinking, contextWindow })
102
+ typeof rawWindow === "number" && Number.isFinite(rawWindow) ? rawWindow : 0;
103
+ found.push({ selector, name, thinking, contextWindow });
102
104
  }
103
105
  // Sort by selector so the picker list is stable across catalog orderings.
104
- found.sort((a, b) => (a.selector < b.selector ? -1 : a.selector > b.selector ? 1 : 0))
105
- return found
106
+ found.sort((a, b) => (a.selector < b.selector ? -1 : a.selector > b.selector ? 1 : 0));
107
+ return found;
106
108
  }
107
109
 
108
110
  /**
@@ -112,9 +114,9 @@ export function parseFreeModels(catalogJson: unknown): ReadonlyArray<CatalogMode
112
114
  */
113
115
  export function ladderCeiling(levels: ReadonlyArray<string>): string | undefined {
114
116
  for (let index = THINKING_LADDER.length - 1; index >= 0; index -= 1) {
115
- if (levels.includes(THINKING_LADDER[index] as string)) return THINKING_LADDER[index] as string
117
+ if (levels.includes(THINKING_LADDER[index] as string)) return THINKING_LADDER[index] as string;
116
118
  }
117
- return undefined
119
+ return undefined;
118
120
  }
119
121
 
120
122
  /**
@@ -124,14 +126,14 @@ export function ladderCeiling(levels: ReadonlyArray<string>): string | undefined
124
126
  * never exceeds the ladder ceiling by construction.
125
127
  */
126
128
  export function advisorLevelFor(levels: ReadonlyArray<string>): string | undefined {
127
- const known = THINKING_LADDER.filter((level) => levels.includes(level))
128
- if (known.length === 0) return undefined
129
- const mediumIndex = THINKING_LADDER.indexOf("medium")
130
- let advisor: string | undefined
129
+ const known = THINKING_LADDER.filter((level) => levels.includes(level));
130
+ if (known.length === 0) return undefined;
131
+ const mediumIndex = THINKING_LADDER.indexOf("medium");
132
+ let advisor: string | undefined;
131
133
  for (const level of known) {
132
- if ((THINKING_LADDER.indexOf(level) as number) <= (mediumIndex as number)) advisor = level
134
+ if ((THINKING_LADDER.indexOf(level) as number) <= (mediumIndex as number)) advisor = level;
133
135
  }
134
- return advisor ?? (known[0] as string)
136
+ return advisor ?? (known[0] as string);
135
137
  }
136
138
 
137
139
  /**
@@ -144,14 +146,14 @@ export function advisorLevelFor(levels: ReadonlyArray<string>): string | undefin
144
146
  export type EffortPlan =
145
147
  | { readonly kind: "none" }
146
148
  | { readonly kind: "fixed"; readonly level: string }
147
- | { readonly kind: "choose"; readonly levels: ReadonlyArray<string>; readonly highest: string }
149
+ | { readonly kind: "choose"; readonly levels: ReadonlyArray<string>; readonly highest: string };
148
150
 
149
151
  export function planEffortChoice(levels: ReadonlyArray<string>): EffortPlan {
150
- const known = THINKING_LADDER.filter((level) => levels.includes(level))
151
- const only = known[0]
152
- if (only === undefined) return { kind: "none" }
153
- if (known.length === 1) return { kind: "fixed", level: only }
154
- return { kind: "choose", levels: known, highest: known[known.length - 1] as string }
152
+ const known = THINKING_LADDER.filter((level) => levels.includes(level));
153
+ const only = known[0];
154
+ if (only === undefined) return { kind: "none" };
155
+ if (known.length === 1) return { kind: "fixed", level: only };
156
+ return { kind: "choose", levels: known, highest: known[known.length - 1] as string };
155
157
  }
156
158
 
157
159
  /**
@@ -163,12 +165,12 @@ export function planEffortChoice(levels: ReadonlyArray<string>): EffortPlan {
163
165
  */
164
166
  export function advisorRecommendation(
165
167
  levels: ReadonlyArray<string>,
166
- main: string
168
+ main: string,
167
169
  ): string | undefined {
168
- const known = THINKING_LADDER.filter((level) => levels.includes(level))
169
- const index = known.indexOf(main)
170
- if (index < 0) return known[0]
171
- return known[Math.max(0, index - 2)]
170
+ const known = THINKING_LADDER.filter((level) => levels.includes(level));
171
+ const index = known.indexOf(main);
172
+ if (index < 0) return known[0];
173
+ return known[Math.max(0, index - 2)];
172
174
  }
173
175
 
174
176
  /**
@@ -177,29 +179,30 @@ export function advisorRecommendation(
177
179
  * selectors with no thinking keys so the deployed values apply.
178
180
  */
179
181
  export function renderFreeOverlay(model: OmpSessionModel): string {
180
- const thinking = typeof model.thinking === "string" && model.thinking.trim() !== "" ? model.thinking : undefined
181
- const lines: Array<string> = ["# Generated per run by docks-kit omp. Edits are overwritten."]
182
+ const thinking =
183
+ typeof model.thinking === "string" && model.thinking.trim() !== "" ? model.thinking : undefined;
184
+ const lines: Array<string> = ["# Generated per run by docks-kit omp. Edits are overwritten."];
182
185
  if (thinking !== undefined) {
183
- lines.push(`defaultThinkingLevel: ${thinking}`, "modelRoles:")
186
+ lines.push(`defaultThinkingLevel: ${thinking}`, "modelRoles:");
184
187
  const advisor =
185
188
  typeof model.advisorThinking === "string" && model.advisorThinking.trim() !== ""
186
189
  ? model.advisorThinking
187
- : thinking
190
+ : thinking;
188
191
  for (const role of ROLE_ORDER) {
189
- lines.push(` ${role}: ${model.selector}:${role === "advisor" ? advisor : thinking}`)
192
+ lines.push(` ${role}: ${model.selector}:${role === "advisor" ? advisor : thinking}`);
190
193
  }
191
- lines.push("task:", ` maxEffort: ${thinking}`)
194
+ lines.push("task:", ` maxEffort: ${thinking}`);
192
195
  } else {
193
- lines.push("modelRoles:")
196
+ lines.push("modelRoles:");
194
197
  for (const role of ROLE_ORDER) {
195
- lines.push(` ${role}: ${model.selector}`)
198
+ lines.push(` ${role}: ${model.selector}`);
196
199
  }
197
200
  }
198
- lines.push("retry:", " fallbackChains:")
201
+ lines.push("retry:", " fallbackChains:");
199
202
  for (const chain of FALLBACK_ORDER) {
200
- lines.push(` ${chain}: []`)
203
+ lines.push(` ${chain}: []`);
201
204
  }
202
- return `${lines.join("\n")}\n`
205
+ return `${lines.join("\n")}\n`;
203
206
  }
204
207
 
205
208
  /** Build the omp argv with the overlay flag ahead of the forwarded args. */
@@ -207,5 +210,5 @@ export function buildOmpArgs(
207
210
  overlayPath: string,
208
211
  passthrough: ReadonlyArray<string>,
209
212
  ): ReadonlyArray<string> {
210
- return ["--config", overlayPath, ...passthrough]
213
+ return ["--config", overlayPath, ...passthrough];
211
214
  }
@@ -7,35 +7,35 @@
7
7
  * Resolution reads the environment and probes directory existence only, so a
8
8
  * dry run stays free of omp subcommands.
9
9
  */
10
- import { existsSync } from "node:fs"
11
- import { isAbsolute, resolve } from "node:path"
10
+ import { existsSync } from "node:fs";
11
+ import { isAbsolute, resolve } from "node:path";
12
12
 
13
- import { p } from "./exec"
13
+ import { p } from "./exec";
14
14
 
15
15
  /** Upstream CONFIG_DIR_NAME; PI_CONFIG_DIR renames this directory under home. */
16
- const DEFAULT_CONFIG_DIR_NAME = ".omp"
16
+ const DEFAULT_CONFIG_DIR_NAME = ".omp";
17
17
  /** Upstream APP_NAME, the fixed segment under an XDG category root. */
18
- const APP_NAME = "omp"
18
+ const APP_NAME = "omp";
19
19
  /** Upstream PROFILE_NAME_RE. An invalid name degrades to the default profile. */
20
- const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/
20
+ const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
21
21
  /** Windows device aliases upstream rejects, bare or with any extension. */
22
- const WINDOWS_RESERVED_BASENAME_RE = /^(?:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?$/i
22
+ const WINDOWS_RESERVED_BASENAME_RE = /^(?:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?$/i;
23
23
 
24
24
  export interface OmpPaths {
25
25
  /** undefined for the default profile, else the normalized profile name */
26
- readonly profile: string | undefined
26
+ readonly profile: string | undefined;
27
27
  /** config root for the active profile */
28
- readonly configRoot: string
28
+ readonly configRoot: string;
29
29
  /** directory holding AGENTS.md, config.yml, mcp.json - never XDG-redirected */
30
- readonly agentDir: string
30
+ readonly agentDir: string;
31
31
  /** root holding marketplaces.json and plugins/ - XDG-redirected when adopted */
32
- readonly dataRoot: string
32
+ readonly dataRoot: string;
33
33
  }
34
34
 
35
35
  export interface OmpPathInputs {
36
- readonly home: string
37
- readonly env: Record<string, string | undefined>
38
- readonly platform: NodeJS.Platform
36
+ readonly home: string;
37
+ readonly env: Record<string, string | undefined>;
38
+ readonly platform: NodeJS.Platform;
39
39
  }
40
40
 
41
41
  /**
@@ -44,36 +44,47 @@ export interface OmpPathInputs {
44
44
  * var cannot crash a bare import.
45
45
  */
46
46
  function normalizeProfile(value: string | undefined): string | undefined {
47
- const name = value?.trim()
48
- if (name === undefined || name === "" || name === "default") return undefined
49
- if (name === "." || name === ".." || name.endsWith(".")) return undefined
50
- if (!PROFILE_NAME_RE.test(name) || WINDOWS_RESERVED_BASENAME_RE.test(name)) return undefined
51
- return name
47
+ const name = value?.trim();
48
+ if (name === undefined || name === "" || name === "default") return undefined;
49
+ if (name === "." || name === ".." || name.endsWith(".")) return undefined;
50
+ if (!PROFILE_NAME_RE.test(name) || WINDOWS_RESERVED_BASENAME_RE.test(name)) return undefined;
51
+ return name;
52
52
  }
53
53
 
54
54
  /** Upstream applies `path.resolve` to PI_CODING_AGENT_DIR, so cwd anchors a relative value. */
55
55
  function resolveAgentOverride(value: string): string {
56
- return isAbsolute(value) ? value : resolve(process.cwd(), value)
56
+ return isAbsolute(value) ? value : resolve(process.cwd(), value);
57
57
  }
58
58
 
59
59
  export function ompPaths({ home, env, platform }: OmpPathInputs): OmpPaths {
60
60
  // OMP_PROFILE wins whenever it is defined, including when explicitly empty;
61
61
  // PI_PROFILE is only the legacy fallback.
62
- const profile = normalizeProfile(env["OMP_PROFILE"] !== undefined ? env["OMP_PROFILE"] : env["PI_PROFILE"])
62
+ const profile = normalizeProfile(
63
+ env["OMP_PROFILE"] !== undefined ? env["OMP_PROFILE"] : env["PI_PROFILE"],
64
+ );
63
65
 
64
66
  // PI_CONFIG_DIR is a config root dirname joined under home, not a path.
65
- const configDirName = env["PI_CONFIG_DIR"]
66
- const baseRoot = p(home, configDirName !== undefined && configDirName !== "" ? configDirName : DEFAULT_CONFIG_DIR_NAME)
67
- const configRoot = profile === undefined ? baseRoot : p(baseRoot, "profiles", profile)
67
+ const configDirName = env["PI_CONFIG_DIR"];
68
+ const baseRoot = p(
69
+ home,
70
+ configDirName !== undefined && configDirName !== "" ? configDirName : DEFAULT_CONFIG_DIR_NAME,
71
+ );
72
+ const configRoot = profile === undefined ? baseRoot : p(baseRoot, "profiles", profile);
68
73
 
69
74
  // A named profile derives its own agent directory and ignores the override.
70
- const agentOverride = env["PI_CODING_AGENT_DIR"]
71
- const defaultAgentDir = p(configRoot, "agent")
72
- const agentDir = profile === undefined && agentOverride !== undefined && agentOverride !== ""
73
- ? resolveAgentOverride(agentOverride)
74
- : defaultAgentDir
75
+ const agentOverride = env["PI_CODING_AGENT_DIR"];
76
+ const defaultAgentDir = p(configRoot, "agent");
77
+ const agentDir =
78
+ profile === undefined && agentOverride !== undefined && agentOverride !== ""
79
+ ? resolveAgentOverride(agentOverride)
80
+ : defaultAgentDir;
75
81
 
76
- return { profile, configRoot, agentDir, dataRoot: xdgDataRoot(env, platform, profile, agentDir === defaultAgentDir) ?? configRoot }
82
+ return {
83
+ profile,
84
+ configRoot,
85
+ agentDir,
86
+ dataRoot: xdgDataRoot(env, platform, profile, agentDir === defaultAgentDir) ?? configRoot,
87
+ };
77
88
  }
78
89
 
79
90
  /**
@@ -87,15 +98,15 @@ function xdgDataRoot(
87
98
  env: Record<string, string | undefined>,
88
99
  platform: NodeJS.Platform,
89
100
  profile: string | undefined,
90
- agentDirIsDefault: boolean
101
+ agentDirIsDefault: boolean,
91
102
  ): string | undefined {
92
- if (!agentDirIsDefault) return undefined
93
- if (platform !== "linux" && platform !== "darwin") return undefined
103
+ if (!agentDirIsDefault) return undefined;
104
+ if (platform !== "linux" && platform !== "darwin") return undefined;
94
105
 
95
- const dataHome = env["XDG_DATA_HOME"]
96
- if (dataHome === undefined || dataHome === "") return undefined
106
+ const dataHome = env["XDG_DATA_HOME"];
107
+ if (dataHome === undefined || dataHome === "") return undefined;
97
108
 
98
- const appRoot = p(dataHome, APP_NAME)
99
- const candidate = profile === undefined ? appRoot : p(appRoot, "profiles", profile)
100
- return existsSync(candidate) ? candidate : undefined
109
+ const appRoot = p(dataHome, APP_NAME);
110
+ const candidate = profile === undefined ? appRoot : p(appRoot, "profiles", profile);
111
+ return existsSync(candidate) ? candidate : undefined;
101
112
  }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * EngineNative omp plugin pass: marketplace-plugin install/upgrade plus the
3
+ * pinned pi-intercom npm plugin. Split from ompSync.ts; the orchestration
4
+ * still lives there.
5
+ */
6
+ import { spawnProcess } from "./exec";
7
+ import { recordFailure } from "./failures";
8
+ import type { Ctx } from "./index";
9
+ import { isObject, parseJson } from "./jq";
10
+ import { firstOutputLine } from "./ompMarketplace";
11
+ import { field } from "./toolchain";
12
+
13
+ const MARKETPLACE_PLUGIN_IDS = ["docks@docks", "plan-lifecycle@docks"] as const;
14
+
15
+ // -------------------------------------------------------------- plugins ----
16
+
17
+ interface InstalledPlugins {
18
+ readonly marketplace: Set<string>;
19
+ readonly npm: Map<string, string>;
20
+ }
21
+
22
+ async function installedPluginIdsFromCli(): Promise<InstalledPlugins | undefined> {
23
+ const result = await spawnProcess("omp", ["plugin", "list", "--json"], {
24
+ stdio: ["ignore", "pipe", "ignore"],
25
+ });
26
+ if (result.error !== undefined || result.exitCode !== 0) return undefined;
27
+
28
+ const value = parseJson(result.stdout);
29
+ if (
30
+ value === undefined ||
31
+ !isObject(value) ||
32
+ !Array.isArray(value["marketplace"]) ||
33
+ !Array.isArray(value["npm"])
34
+ ) {
35
+ return undefined;
36
+ }
37
+
38
+ // `omp plugin list --json` reports marketplace rows as
39
+ // `{ id: "<plugin>@<marketplace>", scope, entries: [...] }` - the composite id
40
+ // is already the token `omp plugin install/upgrade` takes. omp emits one row
41
+ // per scope holding the plugin, so a match found only in a `project` row
42
+ // means the user scope is empty and `upgrade --scope user` would fail; only a
43
+ // `user` row counts as installed for this pipeline.
44
+ const marketplace = new Set<string>();
45
+ for (const row of value["marketplace"]) {
46
+ if (!isObject(row) || typeof row["id"] !== "string" || row["scope"] !== "user") continue;
47
+ marketplace.add(row["id"]);
48
+ }
49
+
50
+ const npm = new Map<string, string>();
51
+ for (const row of value["npm"]) {
52
+ if (!isObject(row) || typeof row["name"] !== "string" || typeof row["version"] !== "string")
53
+ continue;
54
+ npm.set(row["name"], row["version"]);
55
+ }
56
+ return { marketplace, npm };
57
+ }
58
+
59
+ async function runPluginCommand(
60
+ ctx: Ctx,
61
+ plugin: string,
62
+ args: ReadonlyArray<string>,
63
+ ): Promise<boolean> {
64
+ const { clearProgress, progress } = ctx.services.logger;
65
+ progress(`Updating omp plugin ${plugin}...`);
66
+ const result = await spawnProcess("omp", args, { stdio: ["ignore", "pipe", "pipe"] });
67
+ clearProgress();
68
+ if (result.error === undefined && result.exitCode === 0) return true;
69
+
70
+ recordFailure(
71
+ ctx,
72
+ `omp plugin operation failed for ${plugin}: ${firstOutputLine(result)}; run manually: omp ${args.join(" ")}`,
73
+ );
74
+ return false;
75
+ }
76
+
77
+ export async function syncPlugins(ctx: Ctx): Promise<number> {
78
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger;
79
+
80
+ if (ctx.dryRun) {
81
+ const piIntercomPin = field("pi-intercom", "verified");
82
+ for (const pluginId of MARKETPLACE_PLUGIN_IDS) {
83
+ echo(`[dry-run] omp plugin install --scope user ${pluginId}`);
84
+ }
85
+ if (piIntercomPin === "") {
86
+ warn(
87
+ "pi-intercom install skipped because SoT/toolchain.json has no verified pi-intercom pin",
88
+ );
89
+ } else {
90
+ echo(`[dry-run] omp install pi-intercom@${piIntercomPin}`);
91
+ }
92
+ return 0;
93
+ }
94
+
95
+ if (ctx.services.deps.probe("omp").state === "missing") {
96
+ ctx.services.deps.warnMissing(
97
+ "omp",
98
+ ctx.services.logger,
99
+ "deployed omp config only — marketplace and plugin passes skipped; re-run sync after installing",
100
+ );
101
+ return 0;
102
+ }
103
+ if (ctx.services.deps.probe("git").state === "missing") {
104
+ ctx.services.deps.warnMissing(
105
+ "git",
106
+ ctx.services.logger,
107
+ "plugin marketplaces are git repos — omp plugin refresh skipped; re-run sync after installing",
108
+ );
109
+ return 0;
110
+ }
111
+ const piIntercomPin = field("pi-intercom", "verified");
112
+
113
+ progress("Checking installed omp plugins...");
114
+ const installed = await installedPluginIdsFromCli();
115
+ clearProgress();
116
+ if (installed === undefined) {
117
+ warn("omp plugin inventory unavailable — falling back to the full refresh path");
118
+ }
119
+
120
+ let pluginsInstalled =
121
+ installed === undefined
122
+ ? 0
123
+ : MARKETPLACE_PLUGIN_IDS.filter((pluginId) => installed.marketplace.has(pluginId)).length +
124
+ (piIntercomPin !== "" && installed.npm.has("pi-intercom") ? 1 : 0);
125
+ let operationsSucceeded = 0;
126
+
127
+ for (const pluginId of MARKETPLACE_PLUGIN_IDS) {
128
+ const present = installed?.marketplace.has(pluginId) === true;
129
+ if (present && ctx.skipPluginRefresh === true) {
130
+ verbose(`omp plugin ${pluginId} already installed; refresh-only update skipped`);
131
+ continue;
132
+ }
133
+
134
+ const args = present
135
+ ? ["plugin", "upgrade", "--scope", "user", pluginId]
136
+ : ["plugin", "install", "--scope", "user", pluginId];
137
+ if (await runPluginCommand(ctx, pluginId, args)) {
138
+ operationsSucceeded++;
139
+ if (!present) pluginsInstalled++;
140
+ }
141
+ }
142
+
143
+ if (piIntercomPin === "") {
144
+ warn("pi-intercom install skipped because SoT/toolchain.json has no verified pi-intercom pin");
145
+ } else {
146
+ const installedVersion = installed?.npm.get("pi-intercom");
147
+ const present = installedVersion !== undefined;
148
+ if (installedVersion === piIntercomPin) {
149
+ verbose(`omp npm plugin pi-intercom already installed at ${piIntercomPin}`);
150
+ } else if (present && ctx.skipPluginRefresh === true) {
151
+ verbose("omp npm plugin pi-intercom already installed; refresh-only update skipped");
152
+ } else {
153
+ const args = present
154
+ ? ["install", "--force", `pi-intercom@${piIntercomPin}`]
155
+ : ["install", `pi-intercom@${piIntercomPin}`];
156
+ if (await runPluginCommand(ctx, "pi-intercom", args)) {
157
+ operationsSucceeded++;
158
+ if (!present) pluginsInstalled++;
159
+ }
160
+ }
161
+ }
162
+
163
+ if (operationsSucceeded > 0) {
164
+ change(`omp plugins synced (plugins: ~${operationsSucceeded})`);
165
+ ctx.nextStepTriggers.ompRestart = true;
166
+ } else {
167
+ verbose("omp plugins already in sync");
168
+ }
169
+ return pluginsInstalled;
170
+ }