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.
- package/AGENTS.md +36 -32
- package/cli/docs/omp-models.md +88 -53
- 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 +132 -112
- 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 +67 -64
- 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,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 =
|
|
181
|
-
|
|
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(
|
|
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
|
+
}
|