@oh-my-pi/cli 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -84
- package/dist/cli.js +5025 -1016
- package/dist/commands/config.d.ts +27 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/features.d.ts.map +1 -1
- package/dist/commands/info.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/install.d.ts +6 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/link.d.ts +1 -0
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/outdated.d.ts.map +1 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/uninstall.d.ts +3 -0
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/why.d.ts.map +1 -1
- package/dist/conflicts.d.ts +7 -2
- package/dist/conflicts.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lock.d.ts.map +1 -1
- package/dist/lockfile.d.ts +24 -3
- package/dist/lockfile.d.ts.map +1 -1
- package/dist/manifest.d.ts +12 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/npm.d.ts +11 -0
- package/dist/npm.d.ts.map +1 -1
- package/dist/output.d.ts +51 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/paths.d.ts +5 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/progress.d.ts +78 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/symlinks.d.ts +1 -0
- package/dist/symlinks.d.ts.map +1 -1
- package/package.json +24 -10
- package/.github/icon.png +0 -0
- package/.github/logo.png +0 -0
- package/.github/workflows/ci.yml +0 -32
- package/.github/workflows/publish.yml +0 -42
- package/biome.json +0 -29
- package/bun.lock +0 -109
- package/plugins/exa/README.md +0 -153
- package/plugins/exa/package.json +0 -56
- package/plugins/exa/tools/exa/company.ts +0 -35
- package/plugins/exa/tools/exa/index.ts +0 -66
- package/plugins/exa/tools/exa/linkedin.ts +0 -35
- package/plugins/exa/tools/exa/researcher.ts +0 -40
- package/plugins/exa/tools/exa/runtime.json +0 -4
- package/plugins/exa/tools/exa/search.ts +0 -46
- package/plugins/exa/tools/exa/shared.ts +0 -230
- package/plugins/exa/tools/exa/websets.ts +0 -62
- package/plugins/metal-theme/README.md +0 -13
- package/plugins/metal-theme/omp.json +0 -8
- package/plugins/metal-theme/package.json +0 -19
- package/plugins/metal-theme/themes/metal.json +0 -79
- package/plugins/subagents/README.md +0 -25
- package/plugins/subagents/agents/explore.md +0 -71
- package/plugins/subagents/agents/planner.md +0 -51
- package/plugins/subagents/agents/reviewer.md +0 -53
- package/plugins/subagents/agents/task.md +0 -46
- package/plugins/subagents/commands/architect-plan.md +0 -9
- package/plugins/subagents/commands/implement-with-critic.md +0 -10
- package/plugins/subagents/commands/implement.md +0 -10
- package/plugins/subagents/omp.json +0 -15
- package/plugins/subagents/package.json +0 -26
- package/plugins/subagents/tools/task/index.ts +0 -1019
- package/plugins/user-prompt/README.md +0 -130
- package/plugins/user-prompt/package.json +0 -19
- package/plugins/user-prompt/tools/user-prompt/index.ts +0 -235
- package/scripts/bump-version.sh +0 -52
- package/scripts/publish.sh +0 -35
- package/src/cli.ts +0 -242
- package/src/commands/config.ts +0 -384
- package/src/commands/create.ts +0 -203
- package/src/commands/doctor.ts +0 -305
- package/src/commands/enable.ts +0 -122
- package/src/commands/env.ts +0 -38
- package/src/commands/features.ts +0 -295
- package/src/commands/info.ts +0 -120
- package/src/commands/init.ts +0 -60
- package/src/commands/install.ts +0 -700
- package/src/commands/link.ts +0 -159
- package/src/commands/list.ts +0 -186
- package/src/commands/outdated.ts +0 -87
- package/src/commands/search.ts +0 -77
- package/src/commands/uninstall.ts +0 -124
- package/src/commands/update.ts +0 -170
- package/src/commands/why.ts +0 -136
- package/src/conflicts.ts +0 -116
- package/src/errors.ts +0 -22
- package/src/index.ts +0 -46
- package/src/lock.ts +0 -46
- package/src/lockfile.ts +0 -132
- package/src/manifest.ts +0 -360
- package/src/npm.ts +0 -206
- package/src/paths.ts +0 -137
- package/src/runtime.ts +0 -116
- package/src/symlinks.ts +0 -455
- package/tsconfig.json +0 -28
package/src/commands/install.ts
DELETED
|
@@ -1,700 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { cp, mkdir, readFile, rm } from "node:fs/promises";
|
|
4
|
-
import { basename, join, resolve } from "node:path";
|
|
5
|
-
import { createInterface } from "node:readline";
|
|
6
|
-
import { type Conflict, detectConflicts, detectIntraPluginDuplicates, formatConflicts } from "@omp/conflicts";
|
|
7
|
-
import { updateLockFile } from "@omp/lockfile";
|
|
8
|
-
import {
|
|
9
|
-
getInstalledPlugins,
|
|
10
|
-
initGlobalPlugins,
|
|
11
|
-
initProjectPlugins,
|
|
12
|
-
loadPluginsJson,
|
|
13
|
-
type PluginConfig,
|
|
14
|
-
type PluginPackageJson,
|
|
15
|
-
type PluginsJson,
|
|
16
|
-
readPluginPackageJson,
|
|
17
|
-
savePluginsJson,
|
|
18
|
-
} from "@omp/manifest";
|
|
19
|
-
import { npmInfo, npmInstall } from "@omp/npm";
|
|
20
|
-
import {
|
|
21
|
-
NODE_MODULES_DIR,
|
|
22
|
-
PI_CONFIG_DIR,
|
|
23
|
-
PLUGINS_DIR,
|
|
24
|
-
PROJECT_NODE_MODULES,
|
|
25
|
-
PROJECT_PI_DIR,
|
|
26
|
-
resolveScope,
|
|
27
|
-
} from "@omp/paths";
|
|
28
|
-
import { createPluginSymlinks, getAllFeatureNames, getDefaultFeatures } from "@omp/symlinks";
|
|
29
|
-
import chalk from "chalk";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Parsed package specifier with optional features
|
|
33
|
-
*/
|
|
34
|
-
export interface ParsedPackageSpec {
|
|
35
|
-
name: string;
|
|
36
|
-
version: string;
|
|
37
|
-
/** null = not specified, [] = explicit empty, string[] = specific features */
|
|
38
|
-
features: string[] | null;
|
|
39
|
-
/** true if [*] was used */
|
|
40
|
-
allFeatures: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Parse package specifier with optional features bracket syntax.
|
|
45
|
-
* Examples:
|
|
46
|
-
* "exa" -> { name: "exa", version: "latest", features: null }
|
|
47
|
-
* "exa@^1.0" -> { name: "exa", version: "^1.0", features: null }
|
|
48
|
-
* "exa[search]" -> { name: "exa", version: "latest", features: ["search"] }
|
|
49
|
-
* "exa[search,websets]@^1.0" -> { name: "exa", version: "^1.0", features: ["search", "websets"] }
|
|
50
|
-
* "@scope/exa[*]" -> { name: "@scope/exa", version: "latest", allFeatures: true }
|
|
51
|
-
* "exa[]" -> { name: "exa", version: "latest", features: [] } (no optional features)
|
|
52
|
-
*/
|
|
53
|
-
export function parsePackageSpecWithFeatures(spec: string): ParsedPackageSpec {
|
|
54
|
-
// Regex breakdown:
|
|
55
|
-
// ^(@?[^@[\]]+) - Capture name (optionally scoped with @, no @ [ or ] in name)
|
|
56
|
-
// (?:\[([^\]]*)\])? - Optionally capture features inside []
|
|
57
|
-
// (?:@(.+))?$ - Optionally capture version after @
|
|
58
|
-
const match = spec.match(/^(@?[^@[\]]+)(?:\[([^\]]*)\])?(?:@(.+))?$/);
|
|
59
|
-
|
|
60
|
-
if (!match) {
|
|
61
|
-
// Fallback: treat as plain name
|
|
62
|
-
return { name: spec, version: "latest", features: null, allFeatures: false };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const [, name, featuresStr, version = "latest"] = match;
|
|
66
|
-
|
|
67
|
-
// No bracket at all
|
|
68
|
-
if (featuresStr === undefined) {
|
|
69
|
-
return { name, version, features: null, allFeatures: false };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// [*] = all features
|
|
73
|
-
if (featuresStr === "*") {
|
|
74
|
-
return { name, version, features: null, allFeatures: true };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// [] = explicit empty (no optional features, core only)
|
|
78
|
-
if (featuresStr === "") {
|
|
79
|
-
return { name, version, features: [], allFeatures: false };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// [f1,f2,...] = specific features
|
|
83
|
-
const features = featuresStr.split(",").map((f) => f.trim()).filter(Boolean);
|
|
84
|
-
return { name, version, features, allFeatures: false };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Resolve which features to enable based on user request, existing config, and plugin defaults.
|
|
89
|
-
*
|
|
90
|
-
* Resolution order:
|
|
91
|
-
* 1. User explicitly requested [*] -> all features
|
|
92
|
-
* 2. User explicitly specified [f1,f2] -> exactly those features
|
|
93
|
-
* 3. Reinstall with no bracket -> preserve existing selection
|
|
94
|
-
* 4. First install with no bracket -> ALL features
|
|
95
|
-
*/
|
|
96
|
-
export function resolveFeatures(
|
|
97
|
-
pkgJson: PluginPackageJson,
|
|
98
|
-
requested: ParsedPackageSpec,
|
|
99
|
-
existingConfig: PluginConfig | undefined,
|
|
100
|
-
isReinstall: boolean,
|
|
101
|
-
): { enabledFeatures: string[]; configToStore: PluginConfig | undefined } {
|
|
102
|
-
const pluginFeatures = pkgJson.omp?.features || {};
|
|
103
|
-
const allFeatureNames = Object.keys(pluginFeatures);
|
|
104
|
-
|
|
105
|
-
// No features defined in plugin -> nothing to configure
|
|
106
|
-
if (allFeatureNames.length === 0) {
|
|
107
|
-
return { enabledFeatures: [], configToStore: undefined };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Case 1: User explicitly requested [*] -> all features
|
|
111
|
-
if (requested.allFeatures) {
|
|
112
|
-
return {
|
|
113
|
-
enabledFeatures: allFeatureNames,
|
|
114
|
-
configToStore: { features: ["*"] },
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Case 2: User explicitly specified features -> use exactly those
|
|
119
|
-
if (requested.features !== null) {
|
|
120
|
-
// Validate requested features exist
|
|
121
|
-
for (const f of requested.features) {
|
|
122
|
-
if (!pluginFeatures[f]) {
|
|
123
|
-
throw new Error(`Unknown feature "${f}". Available: ${allFeatureNames.join(", ")}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
enabledFeatures: requested.features,
|
|
128
|
-
configToStore: { features: requested.features },
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Case 3: Reinstall with no bracket -> preserve existing config
|
|
133
|
-
if (isReinstall && existingConfig?.features !== undefined) {
|
|
134
|
-
const storedFeatures = existingConfig.features;
|
|
135
|
-
|
|
136
|
-
// null means "first install, used defaults" - recompute defaults in case plugin updated
|
|
137
|
-
if (storedFeatures === null) {
|
|
138
|
-
return {
|
|
139
|
-
enabledFeatures: getDefaultFeatures(pluginFeatures),
|
|
140
|
-
configToStore: undefined, // Keep existing
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ["*"] means explicitly all
|
|
145
|
-
if (Array.isArray(storedFeatures) && storedFeatures.includes("*")) {
|
|
146
|
-
return {
|
|
147
|
-
enabledFeatures: allFeatureNames,
|
|
148
|
-
configToStore: undefined,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Specific features
|
|
153
|
-
return {
|
|
154
|
-
enabledFeatures: storedFeatures as string[],
|
|
155
|
-
configToStore: undefined,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Case 4: First install with no bracket -> use DEFAULT features
|
|
160
|
-
if (!isReinstall) {
|
|
161
|
-
const defaultFeatures = getDefaultFeatures(pluginFeatures);
|
|
162
|
-
return {
|
|
163
|
-
enabledFeatures: defaultFeatures,
|
|
164
|
-
configToStore: { features: null }, // null = "first install, used defaults"
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Case 5: Reinstall, no existing config -> use defaults
|
|
169
|
-
return {
|
|
170
|
-
enabledFeatures: getDefaultFeatures(pluginFeatures),
|
|
171
|
-
configToStore: undefined,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Process omp dependencies recursively with cycle detection.
|
|
177
|
-
* Creates symlinks for dependencies that have omp.install entries.
|
|
178
|
-
*/
|
|
179
|
-
async function processOmpDependencies(pkgJson: PluginPackageJson, isGlobal: boolean, seen: Set<string>): Promise<void> {
|
|
180
|
-
if (!pkgJson.dependencies) return;
|
|
181
|
-
|
|
182
|
-
for (const depName of Object.keys(pkgJson.dependencies)) {
|
|
183
|
-
if (seen.has(depName)) {
|
|
184
|
-
console.log(chalk.yellow(` Skipping circular dependency: ${depName}`));
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
seen.add(depName);
|
|
188
|
-
|
|
189
|
-
const depPkgJson = await readPluginPackageJson(depName, isGlobal);
|
|
190
|
-
if (depPkgJson?.omp?.install) {
|
|
191
|
-
console.log(chalk.dim(` Processing dependency: ${depName}`));
|
|
192
|
-
await createPluginSymlinks(depName, depPkgJson, isGlobal);
|
|
193
|
-
// Recurse into this dependency's dependencies
|
|
194
|
-
await processOmpDependencies(depPkgJson, isGlobal, seen);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export interface InstallOptions {
|
|
200
|
-
global?: boolean;
|
|
201
|
-
local?: boolean;
|
|
202
|
-
save?: boolean;
|
|
203
|
-
saveDev?: boolean;
|
|
204
|
-
force?: boolean;
|
|
205
|
-
json?: boolean;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Prompt user to choose when there's a conflict
|
|
210
|
-
*/
|
|
211
|
-
async function promptConflictResolution(conflict: Conflict): Promise<number | null> {
|
|
212
|
-
const rl = createInterface({
|
|
213
|
-
input: process.stdin,
|
|
214
|
-
output: process.stdout,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
return new Promise((resolve) => {
|
|
218
|
-
console.log(chalk.yellow(`\n⚠ Conflict: ${formatConflicts([conflict])[0]}`));
|
|
219
|
-
conflict.plugins.forEach((p, i) => {
|
|
220
|
-
console.log(` [${i + 1}] ${p.name}`);
|
|
221
|
-
});
|
|
222
|
-
console.log(` [${conflict.plugins.length + 1}] abort`);
|
|
223
|
-
|
|
224
|
-
rl.question(" Choose: ", (answer) => {
|
|
225
|
-
rl.close();
|
|
226
|
-
const choice = parseInt(answer, 10);
|
|
227
|
-
if (choice > 0 && choice <= conflict.plugins.length) {
|
|
228
|
-
resolve(choice - 1);
|
|
229
|
-
} else {
|
|
230
|
-
resolve(null);
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Check if a path looks like a local path
|
|
238
|
-
*/
|
|
239
|
-
function isLocalPath(spec: string): boolean {
|
|
240
|
-
return spec.startsWith("/") || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("~");
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Install plugins from package specifiers
|
|
245
|
-
* omp install [pkg...]
|
|
246
|
-
*/
|
|
247
|
-
export async function installPlugin(packages?: string[], options: InstallOptions = {}): Promise<void> {
|
|
248
|
-
const isGlobal = resolveScope(options);
|
|
249
|
-
const prefix = isGlobal ? PLUGINS_DIR : ".pi";
|
|
250
|
-
const _nodeModules = isGlobal ? NODE_MODULES_DIR : PROJECT_NODE_MODULES;
|
|
251
|
-
|
|
252
|
-
// Initialize plugins directory if needed
|
|
253
|
-
if (isGlobal) {
|
|
254
|
-
await initGlobalPlugins();
|
|
255
|
-
} else {
|
|
256
|
-
// Initialize project .pi directory with both plugins.json and package.json
|
|
257
|
-
await initProjectPlugins();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// If no packages specified, install from plugins.json
|
|
261
|
-
if (!packages || packages.length === 0) {
|
|
262
|
-
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
263
|
-
// Prefer locked versions for reproducible installs
|
|
264
|
-
const lockFile = await import("@omp/lockfile").then((m) => m.loadLockFile(isGlobal));
|
|
265
|
-
packages = await Promise.all(
|
|
266
|
-
Object.entries(pluginsJson.plugins).map(async ([name, version]) => {
|
|
267
|
-
// Use locked version if available for reproducibility
|
|
268
|
-
const lockedVersion = lockFile?.packages[name]?.version;
|
|
269
|
-
return `${name}@${lockedVersion || version}`;
|
|
270
|
-
}),
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
if (packages.length === 0) {
|
|
274
|
-
console.log(chalk.yellow("No plugins to install."));
|
|
275
|
-
console.log(
|
|
276
|
-
chalk.dim(isGlobal ? "Add plugins with: omp install <package>" : "Add plugins to .pi/plugins.json"),
|
|
277
|
-
);
|
|
278
|
-
process.exitCode = 1;
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
console.log(
|
|
283
|
-
chalk.blue(`Installing ${packages.length} plugin(s) from ${isGlobal ? "package.json" : "plugins.json"}...`),
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Get existing plugins for conflict detection
|
|
288
|
-
const existingPlugins = await getInstalledPlugins(isGlobal);
|
|
289
|
-
|
|
290
|
-
const results: Array<{ name: string; version: string; success: boolean; error?: string }> = [];
|
|
291
|
-
|
|
292
|
-
// Load plugins.json once for reinstall detection and config storage
|
|
293
|
-
let pluginsJson = await loadPluginsJson(isGlobal);
|
|
294
|
-
|
|
295
|
-
for (const spec of packages) {
|
|
296
|
-
// Check if it's a local path
|
|
297
|
-
if (isLocalPath(spec)) {
|
|
298
|
-
const result = await installLocalPlugin(spec, isGlobal, options);
|
|
299
|
-
results.push(result);
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const parsed = parsePackageSpecWithFeatures(spec);
|
|
304
|
-
const { name, version } = parsed;
|
|
305
|
-
const pkgSpec = version === "latest" ? name : `${name}@${version}`;
|
|
306
|
-
|
|
307
|
-
// Track installation state for rollback
|
|
308
|
-
let npmInstallSucceeded = false;
|
|
309
|
-
let createdSymlinks: string[] = [];
|
|
310
|
-
let resolvedVersion = version;
|
|
311
|
-
|
|
312
|
-
// Check if this is a reinstall (plugin already exists)
|
|
313
|
-
const isReinstall = existingPlugins.has(name) || !!pluginsJson.plugins[name];
|
|
314
|
-
const existingConfig = pluginsJson.config?.[name];
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
console.log(chalk.blue(`\nInstalling ${pkgSpec}...`));
|
|
318
|
-
|
|
319
|
-
// 1. Resolve version and fetch package metadata from npm registry
|
|
320
|
-
// npm info includes omp field if present in package.json
|
|
321
|
-
const info = await npmInfo(pkgSpec);
|
|
322
|
-
if (!info) {
|
|
323
|
-
console.log(chalk.red(` ✗ Package not found: ${name}`));
|
|
324
|
-
process.exitCode = 1;
|
|
325
|
-
results.push({ name, version, success: false, error: "Package not found" });
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
resolvedVersion = info.version;
|
|
329
|
-
|
|
330
|
-
// 2. Check for conflicts BEFORE npm install using registry metadata
|
|
331
|
-
const skipDestinations = new Set<string>();
|
|
332
|
-
const preInstallPkgJson = info.omp?.install ? { name: info.name, version: info.version, omp: info.omp } : null;
|
|
333
|
-
|
|
334
|
-
if (preInstallPkgJson) {
|
|
335
|
-
// Check for intra-plugin duplicates first
|
|
336
|
-
const intraDupes = detectIntraPluginDuplicates(preInstallPkgJson);
|
|
337
|
-
if (intraDupes.length > 0) {
|
|
338
|
-
console.log(chalk.red(` ✗ Plugin has duplicate destinations:`));
|
|
339
|
-
for (const dupe of intraDupes) {
|
|
340
|
-
console.log(chalk.red(` ${dupe.dest} ← ${dupe.sources.join(", ")}`));
|
|
341
|
-
}
|
|
342
|
-
process.exitCode = 1;
|
|
343
|
-
results.push({ name, version: info.version, success: false, error: "Duplicate destinations in plugin" });
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const preInstallConflicts = detectConflicts(name, preInstallPkgJson, existingPlugins);
|
|
348
|
-
|
|
349
|
-
if (preInstallConflicts.length > 0 && !options.force) {
|
|
350
|
-
// Check for non-interactive terminal (CI environments)
|
|
351
|
-
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
352
|
-
console.log(chalk.red("Conflicts detected in non-interactive mode. Use --force to override."));
|
|
353
|
-
for (const conflict of preInstallConflicts) {
|
|
354
|
-
console.log(chalk.yellow(` ⚠ ${formatConflicts([conflict])[0]}`));
|
|
355
|
-
}
|
|
356
|
-
process.exitCode = 1;
|
|
357
|
-
results.push({
|
|
358
|
-
name,
|
|
359
|
-
version: info.version,
|
|
360
|
-
success: false,
|
|
361
|
-
error: "Conflicts in non-interactive mode",
|
|
362
|
-
});
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Handle conflicts BEFORE downloading the package
|
|
367
|
-
let abort = false;
|
|
368
|
-
for (const conflict of preInstallConflicts) {
|
|
369
|
-
const choice = await promptConflictResolution(conflict);
|
|
370
|
-
if (choice === null) {
|
|
371
|
-
abort = true;
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
// choice is 0-indexed: 0 = first plugin (existing), last index = new plugin
|
|
375
|
-
const newPluginIndex = conflict.plugins.length - 1;
|
|
376
|
-
if (choice !== newPluginIndex) {
|
|
377
|
-
// User chose an existing plugin, skip this destination
|
|
378
|
-
skipDestinations.add(conflict.dest);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (abort) {
|
|
383
|
-
console.log(chalk.yellow(` Aborted due to conflicts (before download)`));
|
|
384
|
-
process.exitCode = 1;
|
|
385
|
-
results.push({ name, version: info.version, success: false, error: "Conflicts" });
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// 3. npm install - only reached if no conflicts or user resolved them
|
|
392
|
-
console.log(chalk.dim(` Fetching from npm...`));
|
|
393
|
-
await npmInstall([pkgSpec], prefix, { save: options.save || isGlobal });
|
|
394
|
-
npmInstallSucceeded = true;
|
|
395
|
-
|
|
396
|
-
// 4. Read package.json from installed package
|
|
397
|
-
const pkgJson = await readPluginPackageJson(name, isGlobal);
|
|
398
|
-
if (!pkgJson) {
|
|
399
|
-
console.log(chalk.yellow(` ⚠ Installed but no package.json found`));
|
|
400
|
-
results.push({ name, version: info.version, success: true });
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// 5. Re-check conflicts with full package.json if we didn't check pre-install
|
|
405
|
-
// This handles edge cases where omp field wasn't in registry metadata
|
|
406
|
-
if (!preInstallPkgJson) {
|
|
407
|
-
// Check for intra-plugin duplicates first
|
|
408
|
-
const intraDupes = detectIntraPluginDuplicates(pkgJson);
|
|
409
|
-
if (intraDupes.length > 0) {
|
|
410
|
-
console.log(chalk.red(` ✗ Plugin has duplicate destinations:`));
|
|
411
|
-
for (const dupe of intraDupes) {
|
|
412
|
-
console.log(chalk.red(` ${dupe.dest} ← ${dupe.sources.join(", ")}`));
|
|
413
|
-
}
|
|
414
|
-
// Rollback: uninstall the package
|
|
415
|
-
execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
|
|
416
|
-
process.exitCode = 1;
|
|
417
|
-
results.push({ name, version: info.version, success: false, error: "Duplicate destinations in plugin" });
|
|
418
|
-
continue;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const conflicts = detectConflicts(name, pkgJson, existingPlugins);
|
|
422
|
-
|
|
423
|
-
if (conflicts.length > 0 && !options.force) {
|
|
424
|
-
// Check for non-interactive terminal (CI environments)
|
|
425
|
-
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
426
|
-
console.log(chalk.red("Conflicts detected in non-interactive mode. Use --force to override."));
|
|
427
|
-
for (const conflict of conflicts) {
|
|
428
|
-
console.log(chalk.yellow(` ⚠ ${formatConflicts([conflict])[0]}`));
|
|
429
|
-
}
|
|
430
|
-
// Rollback: uninstall the package
|
|
431
|
-
execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
|
|
432
|
-
process.exitCode = 1;
|
|
433
|
-
results.push({
|
|
434
|
-
name,
|
|
435
|
-
version: info.version,
|
|
436
|
-
success: false,
|
|
437
|
-
error: "Conflicts in non-interactive mode",
|
|
438
|
-
});
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
let abort = false;
|
|
443
|
-
for (const conflict of conflicts) {
|
|
444
|
-
const choice = await promptConflictResolution(conflict);
|
|
445
|
-
if (choice === null) {
|
|
446
|
-
abort = true;
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
449
|
-
const newPluginIndex = conflict.plugins.length - 1;
|
|
450
|
-
if (choice !== newPluginIndex) {
|
|
451
|
-
skipDestinations.add(conflict.dest);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (abort) {
|
|
456
|
-
console.log(chalk.yellow(` Aborted due to conflicts`));
|
|
457
|
-
// Rollback: uninstall the package
|
|
458
|
-
execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
|
|
459
|
-
process.exitCode = 1;
|
|
460
|
-
results.push({ name, version: info.version, success: false, error: "Conflicts" });
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// 6. Resolve features and create symlinks
|
|
467
|
-
const { enabledFeatures, configToStore } = resolveFeatures(
|
|
468
|
-
pkgJson,
|
|
469
|
-
parsed,
|
|
470
|
-
existingConfig,
|
|
471
|
-
isReinstall,
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
// Log feature selection if plugin has features
|
|
475
|
-
const allFeatureNames = getAllFeatureNames(pkgJson);
|
|
476
|
-
if (allFeatureNames.length > 0) {
|
|
477
|
-
if (enabledFeatures.length === allFeatureNames.length) {
|
|
478
|
-
console.log(chalk.dim(` Features: all (${enabledFeatures.join(", ")})`));
|
|
479
|
-
} else if (enabledFeatures.length === 0) {
|
|
480
|
-
console.log(chalk.dim(` Features: none (core only)`));
|
|
481
|
-
} else {
|
|
482
|
-
console.log(chalk.dim(` Features: ${enabledFeatures.join(", ")}`));
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Create symlinks for omp.install entries (skip destinations user assigned to existing plugins)
|
|
487
|
-
const symlinkResult = await createPluginSymlinks(name, pkgJson, isGlobal, true, skipDestinations, enabledFeatures);
|
|
488
|
-
createdSymlinks = symlinkResult.created;
|
|
489
|
-
|
|
490
|
-
// 7. Process dependencies with omp field (with cycle detection)
|
|
491
|
-
await processOmpDependencies(pkgJson, isGlobal, new Set([name]));
|
|
492
|
-
|
|
493
|
-
// 8. Update manifest and config
|
|
494
|
-
// For global mode, npm --save already updates package.json dependencies
|
|
495
|
-
// but we need to handle devDependencies and config manually
|
|
496
|
-
// For project-local mode, we must manually update plugins.json
|
|
497
|
-
if (options.save || options.saveDev || configToStore) {
|
|
498
|
-
// Reload to avoid stale data if multiple packages are being installed
|
|
499
|
-
pluginsJson = await loadPluginsJson(isGlobal);
|
|
500
|
-
if (options.saveDev) {
|
|
501
|
-
// Save to devDependencies
|
|
502
|
-
if (!pluginsJson.devDependencies) {
|
|
503
|
-
pluginsJson.devDependencies = {};
|
|
504
|
-
}
|
|
505
|
-
pluginsJson.devDependencies[name] = info.version;
|
|
506
|
-
// Remove from plugins if it was there
|
|
507
|
-
delete pluginsJson.plugins[name];
|
|
508
|
-
} else if (!isGlobal) {
|
|
509
|
-
// Save to plugins (project-local mode only - npm handles global)
|
|
510
|
-
pluginsJson.plugins[name] = info.version;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Store feature config if changed
|
|
514
|
-
if (configToStore) {
|
|
515
|
-
if (!pluginsJson.config) {
|
|
516
|
-
pluginsJson.config = {};
|
|
517
|
-
}
|
|
518
|
-
pluginsJson.config[name] = {
|
|
519
|
-
...pluginsJson.config[name],
|
|
520
|
-
...configToStore,
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
await savePluginsJson(pluginsJson, isGlobal);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Add to installed plugins map for subsequent conflict detection
|
|
528
|
-
existingPlugins.set(name, pkgJson);
|
|
529
|
-
|
|
530
|
-
// Update lock file with exact version
|
|
531
|
-
await updateLockFile(name, info.version, isGlobal);
|
|
532
|
-
|
|
533
|
-
console.log(chalk.green(`✓ Installed ${name}@${info.version}`));
|
|
534
|
-
results.push({ name, version: info.version, success: true });
|
|
535
|
-
} catch (err) {
|
|
536
|
-
const errorMsg = (err as Error).message;
|
|
537
|
-
console.log(chalk.red(` ✗ Failed to install ${name}: ${errorMsg}`));
|
|
538
|
-
|
|
539
|
-
// Rollback: remove any symlinks that were created
|
|
540
|
-
if (createdSymlinks.length > 0) {
|
|
541
|
-
console.log(chalk.dim(" Rolling back symlinks..."));
|
|
542
|
-
const baseDir = isGlobal ? PI_CONFIG_DIR : PROJECT_PI_DIR;
|
|
543
|
-
for (const dest of createdSymlinks) {
|
|
544
|
-
try {
|
|
545
|
-
await rm(join(baseDir, dest), { force: true, recursive: true });
|
|
546
|
-
} catch {
|
|
547
|
-
// Ignore cleanup errors
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Rollback: uninstall npm package if it was installed
|
|
553
|
-
if (npmInstallSucceeded) {
|
|
554
|
-
console.log(chalk.dim(" Rolling back npm install..."));
|
|
555
|
-
try {
|
|
556
|
-
execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
|
|
557
|
-
} catch {
|
|
558
|
-
// Ignore cleanup errors
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
process.exitCode = 1;
|
|
563
|
-
results.push({ name, version: resolvedVersion, success: false, error: errorMsg });
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Summary
|
|
568
|
-
const successful = results.filter((r) => r.success);
|
|
569
|
-
const failed = results.filter((r) => !r.success);
|
|
570
|
-
|
|
571
|
-
console.log();
|
|
572
|
-
if (successful.length > 0) {
|
|
573
|
-
console.log(chalk.green(`✓ Installed ${successful.length} plugin(s)`));
|
|
574
|
-
}
|
|
575
|
-
if (failed.length > 0) {
|
|
576
|
-
console.log(chalk.red(`✗ Failed to install ${failed.length} plugin(s)`));
|
|
577
|
-
process.exitCode = 1;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (options.json) {
|
|
581
|
-
console.log(JSON.stringify({ results }, null, 2));
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* Install a local plugin (copy or link based on path type)
|
|
587
|
-
*/
|
|
588
|
-
async function installLocalPlugin(
|
|
589
|
-
localPath: string,
|
|
590
|
-
isGlobal: boolean,
|
|
591
|
-
_options: InstallOptions,
|
|
592
|
-
): Promise<{ name: string; version: string; success: boolean; error?: string }> {
|
|
593
|
-
// Expand ~ to home directory
|
|
594
|
-
if (localPath.startsWith("~")) {
|
|
595
|
-
localPath = join(process.env.HOME || "", localPath.slice(1));
|
|
596
|
-
}
|
|
597
|
-
localPath = resolve(localPath);
|
|
598
|
-
|
|
599
|
-
if (!existsSync(localPath)) {
|
|
600
|
-
console.log(chalk.red(`Error: Path does not exist: ${localPath}`));
|
|
601
|
-
process.exitCode = 1;
|
|
602
|
-
return { name: basename(localPath), version: "local", success: false, error: "Path not found" };
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const _prefix = isGlobal ? PLUGINS_DIR : ".pi";
|
|
606
|
-
const nodeModules = isGlobal ? NODE_MODULES_DIR : PROJECT_NODE_MODULES;
|
|
607
|
-
|
|
608
|
-
try {
|
|
609
|
-
// Read package.json from local path
|
|
610
|
-
const localPkgJsonPath = join(localPath, "package.json");
|
|
611
|
-
let pkgJson: PluginPackageJson;
|
|
612
|
-
|
|
613
|
-
if (existsSync(localPkgJsonPath)) {
|
|
614
|
-
pkgJson = JSON.parse(await readFile(localPkgJsonPath, "utf-8"));
|
|
615
|
-
} else {
|
|
616
|
-
// Check for omp.json (legacy format)
|
|
617
|
-
const ompJsonPath = join(localPath, "omp.json");
|
|
618
|
-
if (existsSync(ompJsonPath)) {
|
|
619
|
-
const ompJson = JSON.parse(await readFile(ompJsonPath, "utf-8"));
|
|
620
|
-
// Convert omp.json to package.json format
|
|
621
|
-
pkgJson = {
|
|
622
|
-
name: ompJson.name || basename(localPath),
|
|
623
|
-
version: ompJson.version || "0.0.0",
|
|
624
|
-
description: ompJson.description,
|
|
625
|
-
keywords: ["omp-plugin"],
|
|
626
|
-
omp: {
|
|
627
|
-
install: ompJson.install,
|
|
628
|
-
},
|
|
629
|
-
};
|
|
630
|
-
} else {
|
|
631
|
-
pkgJson = {
|
|
632
|
-
name: basename(localPath),
|
|
633
|
-
version: "0.0.0",
|
|
634
|
-
keywords: ["omp-plugin"],
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const pluginName = pkgJson.name;
|
|
640
|
-
const pluginDir = join(nodeModules, pluginName);
|
|
641
|
-
|
|
642
|
-
// Check for intra-plugin duplicates
|
|
643
|
-
const intraDupes = detectIntraPluginDuplicates(pkgJson);
|
|
644
|
-
if (intraDupes.length > 0) {
|
|
645
|
-
console.log(chalk.red(`\nError: Plugin has duplicate destinations:`));
|
|
646
|
-
for (const dupe of intraDupes) {
|
|
647
|
-
console.log(chalk.red(` ${dupe.dest} ← ${dupe.sources.join(", ")}`));
|
|
648
|
-
}
|
|
649
|
-
process.exitCode = 1;
|
|
650
|
-
return {
|
|
651
|
-
name: pluginName,
|
|
652
|
-
version: pkgJson.version,
|
|
653
|
-
success: false,
|
|
654
|
-
error: "Duplicate destinations in plugin",
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
console.log(chalk.blue(`\nInstalling ${pluginName} from ${localPath}...`));
|
|
659
|
-
|
|
660
|
-
// Create node_modules directory
|
|
661
|
-
await mkdir(nodeModules, { recursive: true });
|
|
662
|
-
|
|
663
|
-
// Remove existing if present
|
|
664
|
-
if (existsSync(pluginDir)) {
|
|
665
|
-
await rm(pluginDir, { recursive: true, force: true });
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Copy the plugin
|
|
669
|
-
await cp(localPath, pluginDir, { recursive: true });
|
|
670
|
-
console.log(chalk.dim(` Copied to ${pluginDir}`));
|
|
671
|
-
|
|
672
|
-
// Update plugins.json/package.json
|
|
673
|
-
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
674
|
-
if (_options.saveDev) {
|
|
675
|
-
if (!pluginsJson.devDependencies) {
|
|
676
|
-
pluginsJson.devDependencies = {};
|
|
677
|
-
}
|
|
678
|
-
pluginsJson.devDependencies[pluginName] = `file:${localPath}`;
|
|
679
|
-
// Remove from plugins if it was there
|
|
680
|
-
delete pluginsJson.plugins[pluginName];
|
|
681
|
-
} else {
|
|
682
|
-
pluginsJson.plugins[pluginName] = `file:${localPath}`;
|
|
683
|
-
}
|
|
684
|
-
await savePluginsJson(pluginsJson, isGlobal);
|
|
685
|
-
|
|
686
|
-
// Create symlinks
|
|
687
|
-
await createPluginSymlinks(pluginName, pkgJson, isGlobal);
|
|
688
|
-
|
|
689
|
-
// Update lock file for local plugin
|
|
690
|
-
await updateLockFile(pluginName, pkgJson.version, isGlobal);
|
|
691
|
-
|
|
692
|
-
console.log(chalk.green(`✓ Installed ${pluginName}@${pkgJson.version}`));
|
|
693
|
-
return { name: pluginName, version: pkgJson.version, success: true };
|
|
694
|
-
} catch (err) {
|
|
695
|
-
const errorMsg = (err as Error).message;
|
|
696
|
-
console.log(chalk.red(` ✗ Failed: ${errorMsg}`));
|
|
697
|
-
process.exitCode = 1;
|
|
698
|
-
return { name: basename(localPath), version: "local", success: false, error: errorMsg };
|
|
699
|
-
}
|
|
700
|
-
}
|