docks-kit 0.17.0 → 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.
- package/AGENTS.md +37 -33
- package/README.md +1 -0
- package/cli/docs/omp-models.md +114 -58
- package/cli/src/argv.ts +38 -469
- package/cli/src/argvSurface.ts +371 -0
- package/cli/src/argvValidate.ts +122 -0
- package/cli/src/commands/docs.ts +57 -42
- package/cli/src/commands/harnesses.ts +38 -45
- package/cli/src/commands/model.ts +48 -53
- package/cli/src/commands/models.ts +24 -26
- package/cli/src/commands/omp.ts +178 -88
- package/cli/src/commands/plugins.ts +20 -20
- package/cli/src/commands/skills.ts +22 -20
- package/cli/src/commands/status.ts +87 -80
- package/cli/src/commands/sync.ts +113 -87
- package/cli/src/commands/toolchain.ts +21 -21
- package/cli/src/commands/update.ts +139 -117
- package/cli/src/efforts.ts +35 -34
- package/cli/src/engine-native/DESIGN.md +30 -30
- package/cli/src/engine-native/bun.ts +77 -55
- package/cli/src/engine-native/claudeLsp.ts +154 -0
- package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
- package/cli/src/engine-native/claudePluginPasses.ts +348 -0
- package/cli/src/engine-native/claudeRemovals.ts +195 -0
- package/cli/src/engine-native/claudeRetired.ts +4 -4
- package/cli/src/engine-native/claudeRuntime.ts +94 -68
- package/cli/src/engine-native/claudeSettings.ts +174 -0
- package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
- package/cli/src/engine-native/claudeSync.ts +176 -456
- package/cli/src/engine-native/codexConfig.ts +158 -0
- package/cli/src/engine-native/codexHooks.ts +91 -0
- package/cli/src/engine-native/codexPlugins.ts +290 -0
- package/cli/src/engine-native/codexStatus.ts +24 -0
- package/cli/src/engine-native/codexSync.ts +110 -568
- package/cli/src/engine-native/codexToml.ts +140 -138
- package/cli/src/engine-native/deps.ts +144 -107
- package/cli/src/engine-native/engineCtx.ts +126 -0
- package/cli/src/engine-native/exec.ts +98 -87
- package/cli/src/engine-native/failures.ts +3 -3
- package/cli/src/engine-native/harnesses.ts +62 -72
- package/cli/src/engine-native/index.ts +34 -315
- package/cli/src/engine-native/jq.ts +25 -24
- package/cli/src/engine-native/logger.ts +98 -100
- package/cli/src/engine-native/models.ts +50 -45
- package/cli/src/engine-native/modes.ts +83 -80
- package/cli/src/engine-native/ompFileDeploy.ts +111 -0
- package/cli/src/engine-native/ompMarketplace.ts +136 -0
- package/cli/src/engine-native/ompOverlay.ts +93 -53
- package/cli/src/engine-native/ompPaths.ts +49 -38
- package/cli/src/engine-native/ompPlugins.ts +170 -0
- package/cli/src/engine-native/ompSync.ts +64 -408
- package/cli/src/engine-native/ompYaml.ts +43 -41
- package/cli/src/engine-native/os/darwin.ts +5 -5
- package/cli/src/engine-native/os/index.ts +15 -15
- package/cli/src/engine-native/os/linux.ts +5 -5
- package/cli/src/engine-native/os/posix.ts +28 -24
- package/cli/src/engine-native/os/targets.ts +17 -17
- package/cli/src/engine-native/os/types.ts +31 -31
- package/cli/src/engine-native/os/windows.ts +61 -61
- package/cli/src/engine-native/parseArgs.ts +122 -372
- package/cli/src/engine-native/parseHelp.ts +66 -0
- package/cli/src/engine-native/parseModifiers.ts +222 -0
- package/cli/src/engine-native/services.ts +34 -34
- package/cli/src/engine-native/settings.ts +15 -15
- package/cli/src/engine-native/sharedTypes.d.ts +46 -0
- package/cli/src/engine-native/skillsInstall.ts +105 -0
- package/cli/src/engine-native/skillsLinks.ts +203 -0
- package/cli/src/engine-native/skillsManifest.ts +16 -0
- package/cli/src/engine-native/skillsPrune.ts +108 -0
- package/cli/src/engine-native/skillsSync.ts +28 -362
- package/cli/src/engine-native/syncConcurrency.ts +55 -0
- package/cli/src/engine-native/syncDispatch.ts +141 -0
- package/cli/src/engine-native/toolchain.ts +65 -62
- package/cli/src/engine.ts +65 -57
- package/cli/src/generated/sotPayload.ts +7 -7
- package/cli/src/kitHome.ts +44 -40
- package/cli/src/main.ts +63 -56
- package/cli/src/manifests.ts +53 -54
- package/cli/src/md.d.ts +2 -2
- package/cli/src/payload.ts +9 -9
- package/cli/src/services.ts +21 -13
- package/cli/tsconfig.json +4 -1
- package/package.json +9 -2
- 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,51 @@ 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);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* What the picker may ask about thinking levels for one model. omp accepts a
|
|
141
|
+
* `:level` suffix on a role only for a level the model publishes, so the
|
|
142
|
+
* question set comes from the model own ladder: no ladder means no question,
|
|
143
|
+
* a single level means no choice to make, and two or more levels mean the
|
|
144
|
+
* user can split the advisor from the other roles.
|
|
145
|
+
*/
|
|
146
|
+
export type EffortPlan =
|
|
147
|
+
| { readonly kind: "none" }
|
|
148
|
+
| { readonly kind: "fixed"; readonly level: string }
|
|
149
|
+
| { readonly kind: "choose"; readonly levels: ReadonlyArray<string>; readonly highest: string };
|
|
150
|
+
|
|
151
|
+
export function planEffortChoice(levels: ReadonlyArray<string>): EffortPlan {
|
|
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 };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Recommend an advisor level for a chosen main level. The advisor runs quick
|
|
161
|
+
* review passes, so it sits two steps down the model own ladder, and it
|
|
162
|
+
* clamps to the lowest level the model publishes. Steps count positions in
|
|
163
|
+
* that model ladder, not the global ladder, so a sparse ladder such as
|
|
164
|
+
* `high, max` still recommends a real level.
|
|
165
|
+
*/
|
|
166
|
+
export function advisorRecommendation(
|
|
167
|
+
levels: ReadonlyArray<string>,
|
|
168
|
+
main: string,
|
|
169
|
+
): string | undefined {
|
|
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)];
|
|
135
174
|
}
|
|
136
175
|
|
|
137
176
|
/**
|
|
@@ -140,29 +179,30 @@ export function advisorLevelFor(levels: ReadonlyArray<string>): string | undefin
|
|
|
140
179
|
* selectors with no thinking keys so the deployed values apply.
|
|
141
180
|
*/
|
|
142
181
|
export function renderFreeOverlay(model: OmpSessionModel): string {
|
|
143
|
-
const thinking =
|
|
144
|
-
|
|
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."];
|
|
145
185
|
if (thinking !== undefined) {
|
|
146
|
-
lines.push(`defaultThinkingLevel: ${thinking}`, "modelRoles:")
|
|
186
|
+
lines.push(`defaultThinkingLevel: ${thinking}`, "modelRoles:");
|
|
147
187
|
const advisor =
|
|
148
188
|
typeof model.advisorThinking === "string" && model.advisorThinking.trim() !== ""
|
|
149
189
|
? model.advisorThinking
|
|
150
|
-
: thinking
|
|
190
|
+
: thinking;
|
|
151
191
|
for (const role of ROLE_ORDER) {
|
|
152
|
-
lines.push(` ${role}: ${model.selector}:${role === "advisor" ? advisor : thinking}`)
|
|
192
|
+
lines.push(` ${role}: ${model.selector}:${role === "advisor" ? advisor : thinking}`);
|
|
153
193
|
}
|
|
154
|
-
lines.push("task:", ` maxEffort: ${thinking}`)
|
|
194
|
+
lines.push("task:", ` maxEffort: ${thinking}`);
|
|
155
195
|
} else {
|
|
156
|
-
lines.push("modelRoles:")
|
|
196
|
+
lines.push("modelRoles:");
|
|
157
197
|
for (const role of ROLE_ORDER) {
|
|
158
|
-
lines.push(` ${role}: ${model.selector}`)
|
|
198
|
+
lines.push(` ${role}: ${model.selector}`);
|
|
159
199
|
}
|
|
160
200
|
}
|
|
161
|
-
lines.push("retry:", " fallbackChains:")
|
|
201
|
+
lines.push("retry:", " fallbackChains:");
|
|
162
202
|
for (const chain of FALLBACK_ORDER) {
|
|
163
|
-
lines.push(` ${chain}: []`)
|
|
203
|
+
lines.push(` ${chain}: []`);
|
|
164
204
|
}
|
|
165
|
-
return `${lines.join("\n")}\n
|
|
205
|
+
return `${lines.join("\n")}\n`;
|
|
166
206
|
}
|
|
167
207
|
|
|
168
208
|
/** Build the omp argv with the overlay flag ahead of the forwarded args. */
|
|
@@ -170,5 +210,5 @@ export function buildOmpArgs(
|
|
|
170
210
|
overlayPath: string,
|
|
171
211
|
passthrough: ReadonlyArray<string>,
|
|
172
212
|
): ReadonlyArray<string> {
|
|
173
|
-
return ["--config", overlayPath, ...passthrough]
|
|
213
|
+
return ["--config", overlayPath, ...passthrough];
|
|
174
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(
|
|
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(
|
|
67
|
-
|
|
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 =
|
|
73
|
-
|
|
74
|
-
|
|
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 {
|
|
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
|
+
}
|