@oh-my-pi/cli 0.1.0 → 0.2.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/.github/icon.png +0 -0
- package/.github/logo.png +0 -0
- package/.github/workflows/publish.yml +1 -1
- package/LICENSE +21 -0
- package/README.md +131 -145
- package/biome.json +1 -1
- package/dist/cli.js +2032 -1136
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/enable.d.ts +1 -0
- package/dist/commands/enable.d.ts.map +1 -1
- package/dist/commands/info.d.ts +1 -0
- package/dist/commands/info.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/link.d.ts +2 -0
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/outdated.d.ts +1 -0
- package/dist/commands/outdated.d.ts.map +1 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/uninstall.d.ts +1 -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 +1 -0
- package/dist/commands/why.d.ts.map +1 -1
- package/dist/conflicts.d.ts +9 -1
- package/dist/conflicts.d.ts.map +1 -1
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +19 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/lock.d.ts +3 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lockfile.d.ts +52 -0
- package/dist/lockfile.d.ts.map +1 -0
- package/dist/manifest.d.ts +5 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/npm.d.ts +14 -2
- package/dist/npm.d.ts.map +1 -1
- package/dist/paths.d.ts +34 -2
- package/dist/paths.d.ts.map +1 -1
- package/dist/symlinks.d.ts +10 -4
- package/dist/symlinks.d.ts.map +1 -1
- package/package.json +7 -2
- package/plugins/metal-theme/package.json +6 -1
- package/plugins/subagents/package.json +6 -1
- package/src/cli.ts +69 -43
- package/src/commands/create.ts +51 -1
- package/src/commands/doctor.ts +95 -7
- package/src/commands/enable.ts +25 -8
- package/src/commands/info.ts +41 -5
- package/src/commands/init.ts +20 -2
- package/src/commands/install.ts +266 -52
- package/src/commands/link.ts +60 -9
- package/src/commands/list.ts +10 -5
- package/src/commands/outdated.ts +17 -6
- package/src/commands/search.ts +20 -3
- package/src/commands/uninstall.ts +57 -6
- package/src/commands/update.ts +67 -9
- package/src/commands/why.ts +47 -16
- package/src/conflicts.ts +33 -1
- package/src/errors.ts +22 -0
- package/src/index.ts +19 -25
- package/src/lock.ts +46 -0
- package/src/lockfile.ts +132 -0
- package/src/manifest.ts +143 -35
- package/src/migrate.ts +14 -3
- package/src/npm.ts +74 -18
- package/src/paths.ts +77 -9
- package/src/symlinks.ts +134 -17
- package/tsconfig.json +7 -3
- package/CHECK.md +0 -352
package/src/commands/install.ts
CHANGED
|
@@ -1,24 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { cp, mkdir, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { basename, join, resolve } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import { type Conflict, detectConflicts, detectIntraPluginDuplicates, formatConflicts } from "@omp/conflicts";
|
|
7
|
+
import { updateLockFile } from "@omp/lockfile";
|
|
8
8
|
import {
|
|
9
9
|
getInstalledPlugins,
|
|
10
10
|
initGlobalPlugins,
|
|
11
|
+
initProjectPlugins,
|
|
11
12
|
loadPluginsJson,
|
|
12
13
|
type PluginPackageJson,
|
|
13
14
|
readPluginPackageJson,
|
|
14
15
|
savePluginsJson,
|
|
15
|
-
} from "
|
|
16
|
-
import { npmInfo, npmInstall } from "
|
|
17
|
-
import {
|
|
18
|
-
|
|
16
|
+
} from "@omp/manifest";
|
|
17
|
+
import { npmInfo, npmInstall } from "@omp/npm";
|
|
18
|
+
import {
|
|
19
|
+
NODE_MODULES_DIR,
|
|
20
|
+
PI_CONFIG_DIR,
|
|
21
|
+
PLUGINS_DIR,
|
|
22
|
+
PROJECT_NODE_MODULES,
|
|
23
|
+
PROJECT_PI_DIR,
|
|
24
|
+
resolveScope,
|
|
25
|
+
} from "@omp/paths";
|
|
26
|
+
import { createPluginSymlinks } from "@omp/symlinks";
|
|
27
|
+
import chalk from "chalk";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Process omp dependencies recursively with cycle detection.
|
|
31
|
+
* Creates symlinks for dependencies that have omp.install entries.
|
|
32
|
+
*/
|
|
33
|
+
async function processOmpDependencies(pkgJson: PluginPackageJson, isGlobal: boolean, seen: Set<string>): Promise<void> {
|
|
34
|
+
if (!pkgJson.dependencies) return;
|
|
35
|
+
|
|
36
|
+
for (const depName of Object.keys(pkgJson.dependencies)) {
|
|
37
|
+
if (seen.has(depName)) {
|
|
38
|
+
console.log(chalk.yellow(` Skipping circular dependency: ${depName}`));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
seen.add(depName);
|
|
42
|
+
|
|
43
|
+
const depPkgJson = await readPluginPackageJson(depName, isGlobal);
|
|
44
|
+
if (depPkgJson?.omp?.install) {
|
|
45
|
+
console.log(chalk.dim(` Processing dependency: ${depName}`));
|
|
46
|
+
await createPluginSymlinks(depName, depPkgJson, isGlobal);
|
|
47
|
+
// Recurse into this dependency's dependencies
|
|
48
|
+
await processOmpDependencies(depPkgJson, isGlobal, seen);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
19
52
|
|
|
20
53
|
export interface InstallOptions {
|
|
21
54
|
global?: boolean;
|
|
55
|
+
local?: boolean;
|
|
22
56
|
save?: boolean;
|
|
23
57
|
saveDev?: boolean;
|
|
24
58
|
force?: boolean;
|
|
@@ -93,7 +127,7 @@ function isLocalPath(spec: string): boolean {
|
|
|
93
127
|
* omp install [pkg...]
|
|
94
128
|
*/
|
|
95
129
|
export async function installPlugin(packages?: string[], options: InstallOptions = {}): Promise<void> {
|
|
96
|
-
const isGlobal = options
|
|
130
|
+
const isGlobal = resolveScope(options);
|
|
97
131
|
const prefix = isGlobal ? PLUGINS_DIR : ".pi";
|
|
98
132
|
const _nodeModules = isGlobal ? NODE_MODULES_DIR : PROJECT_NODE_MODULES;
|
|
99
133
|
|
|
@@ -101,24 +135,29 @@ export async function installPlugin(packages?: string[], options: InstallOptions
|
|
|
101
135
|
if (isGlobal) {
|
|
102
136
|
await initGlobalPlugins();
|
|
103
137
|
} else {
|
|
104
|
-
//
|
|
105
|
-
await
|
|
106
|
-
// Initialize plugins.json if it doesn't exist
|
|
107
|
-
if (!existsSync(PROJECT_PLUGINS_JSON)) {
|
|
108
|
-
await savePluginsJson({ plugins: {} }, false);
|
|
109
|
-
}
|
|
138
|
+
// Initialize project .pi directory with both plugins.json and package.json
|
|
139
|
+
await initProjectPlugins();
|
|
110
140
|
}
|
|
111
141
|
|
|
112
142
|
// If no packages specified, install from plugins.json
|
|
113
143
|
if (!packages || packages.length === 0) {
|
|
114
144
|
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
115
|
-
|
|
145
|
+
// Prefer locked versions for reproducible installs
|
|
146
|
+
const lockFile = await import("@omp/lockfile").then((m) => m.loadLockFile(isGlobal));
|
|
147
|
+
packages = await Promise.all(
|
|
148
|
+
Object.entries(pluginsJson.plugins).map(async ([name, version]) => {
|
|
149
|
+
// Use locked version if available for reproducibility
|
|
150
|
+
const lockedVersion = lockFile?.packages[name]?.version;
|
|
151
|
+
return `${name}@${lockedVersion || version}`;
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
116
154
|
|
|
117
155
|
if (packages.length === 0) {
|
|
118
156
|
console.log(chalk.yellow("No plugins to install."));
|
|
119
157
|
console.log(
|
|
120
158
|
chalk.dim(isGlobal ? "Add plugins with: omp install <package>" : "Add plugins to .pi/plugins.json"),
|
|
121
159
|
);
|
|
160
|
+
process.exitCode = 1;
|
|
122
161
|
return;
|
|
123
162
|
}
|
|
124
163
|
|
|
@@ -143,24 +182,90 @@ export async function installPlugin(packages?: string[], options: InstallOptions
|
|
|
143
182
|
const { name, version } = parsePackageSpec(spec);
|
|
144
183
|
const pkgSpec = version === "latest" ? name : `${name}@${version}`;
|
|
145
184
|
|
|
185
|
+
// Track installation state for rollback
|
|
186
|
+
let npmInstallSucceeded = false;
|
|
187
|
+
let createdSymlinks: string[] = [];
|
|
188
|
+
let resolvedVersion = version;
|
|
189
|
+
|
|
146
190
|
try {
|
|
147
191
|
console.log(chalk.blue(`\nInstalling ${pkgSpec}...`));
|
|
148
192
|
|
|
149
|
-
// 1. Resolve version from npm registry
|
|
193
|
+
// 1. Resolve version and fetch package metadata from npm registry
|
|
194
|
+
// npm info includes omp field if present in package.json
|
|
150
195
|
const info = await npmInfo(pkgSpec);
|
|
151
196
|
if (!info) {
|
|
152
197
|
console.log(chalk.red(` ✗ Package not found: ${name}`));
|
|
198
|
+
process.exitCode = 1;
|
|
153
199
|
results.push({ name, version, success: false, error: "Package not found" });
|
|
154
200
|
continue;
|
|
155
201
|
}
|
|
202
|
+
resolvedVersion = info.version;
|
|
203
|
+
|
|
204
|
+
// 2. Check for conflicts BEFORE npm install using registry metadata
|
|
205
|
+
const skipDestinations = new Set<string>();
|
|
206
|
+
const preInstallPkgJson = info.omp?.install ? { name: info.name, version: info.version, omp: info.omp } : null;
|
|
207
|
+
|
|
208
|
+
if (preInstallPkgJson) {
|
|
209
|
+
// Check for intra-plugin duplicates first
|
|
210
|
+
const intraDupes = detectIntraPluginDuplicates(preInstallPkgJson);
|
|
211
|
+
if (intraDupes.length > 0) {
|
|
212
|
+
console.log(chalk.red(` ✗ Plugin has duplicate destinations:`));
|
|
213
|
+
for (const dupe of intraDupes) {
|
|
214
|
+
console.log(chalk.red(` ${dupe.dest} ← ${dupe.sources.join(", ")}`));
|
|
215
|
+
}
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
results.push({ name, version: info.version, success: false, error: "Duplicate destinations in plugin" });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
156
220
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
221
|
+
const preInstallConflicts = detectConflicts(name, preInstallPkgJson, existingPlugins);
|
|
222
|
+
|
|
223
|
+
if (preInstallConflicts.length > 0 && !options.force) {
|
|
224
|
+
// Check for non-interactive terminal (CI environments)
|
|
225
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
226
|
+
console.log(chalk.red("Conflicts detected in non-interactive mode. Use --force to override."));
|
|
227
|
+
for (const conflict of preInstallConflicts) {
|
|
228
|
+
console.log(chalk.yellow(` ⚠ ${formatConflicts([conflict])[0]}`));
|
|
229
|
+
}
|
|
230
|
+
process.exitCode = 1;
|
|
231
|
+
results.push({
|
|
232
|
+
name,
|
|
233
|
+
version: info.version,
|
|
234
|
+
success: false,
|
|
235
|
+
error: "Conflicts in non-interactive mode",
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Handle conflicts BEFORE downloading the package
|
|
241
|
+
let abort = false;
|
|
242
|
+
for (const conflict of preInstallConflicts) {
|
|
243
|
+
const choice = await promptConflictResolution(conflict);
|
|
244
|
+
if (choice === null) {
|
|
245
|
+
abort = true;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
// choice is 0-indexed: 0 = first plugin (existing), last index = new plugin
|
|
249
|
+
const newPluginIndex = conflict.plugins.length - 1;
|
|
250
|
+
if (choice !== newPluginIndex) {
|
|
251
|
+
// User chose an existing plugin, skip this destination
|
|
252
|
+
skipDestinations.add(conflict.dest);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
160
255
|
|
|
161
|
-
|
|
256
|
+
if (abort) {
|
|
257
|
+
console.log(chalk.yellow(` Aborted due to conflicts (before download)`));
|
|
258
|
+
process.exitCode = 1;
|
|
259
|
+
results.push({ name, version: info.version, success: false, error: "Conflicts" });
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 3. npm install - only reached if no conflicts or user resolved them
|
|
162
266
|
console.log(chalk.dim(` Fetching from npm...`));
|
|
163
267
|
await npmInstall([pkgSpec], prefix, { save: options.save || isGlobal });
|
|
268
|
+
npmInstallSucceeded = true;
|
|
164
269
|
|
|
165
270
|
// 4. Read package.json from installed package
|
|
166
271
|
const pkgJson = await readPluginPackageJson(name, isGlobal);
|
|
@@ -170,55 +275,133 @@ export async function installPlugin(packages?: string[], options: InstallOptions
|
|
|
170
275
|
continue;
|
|
171
276
|
}
|
|
172
277
|
|
|
173
|
-
// 5.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
abort = true;
|
|
183
|
-
break;
|
|
278
|
+
// 5. Re-check conflicts with full package.json if we didn't check pre-install
|
|
279
|
+
// This handles edge cases where omp field wasn't in registry metadata
|
|
280
|
+
if (!preInstallPkgJson) {
|
|
281
|
+
// Check for intra-plugin duplicates first
|
|
282
|
+
const intraDupes = detectIntraPluginDuplicates(pkgJson);
|
|
283
|
+
if (intraDupes.length > 0) {
|
|
284
|
+
console.log(chalk.red(` ✗ Plugin has duplicate destinations:`));
|
|
285
|
+
for (const dupe of intraDupes) {
|
|
286
|
+
console.log(chalk.red(` ${dupe.dest} ← ${dupe.sources.join(", ")}`));
|
|
184
287
|
}
|
|
185
|
-
// If user chose the new plugin, we continue
|
|
186
|
-
// If user chose existing plugin, we skip this destination
|
|
187
|
-
// For now, simplify: if not aborted, force overwrite
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (abort) {
|
|
191
|
-
console.log(chalk.yellow(` Aborted due to conflicts`));
|
|
192
288
|
// Rollback: uninstall the package
|
|
193
|
-
|
|
194
|
-
|
|
289
|
+
execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
|
|
290
|
+
process.exitCode = 1;
|
|
291
|
+
results.push({ name, version: info.version, success: false, error: "Duplicate destinations in plugin" });
|
|
195
292
|
continue;
|
|
196
293
|
}
|
|
197
|
-
}
|
|
198
294
|
|
|
199
|
-
|
|
200
|
-
|
|
295
|
+
const conflicts = detectConflicts(name, pkgJson, existingPlugins);
|
|
296
|
+
|
|
297
|
+
if (conflicts.length > 0 && !options.force) {
|
|
298
|
+
// Check for non-interactive terminal (CI environments)
|
|
299
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
300
|
+
console.log(chalk.red("Conflicts detected in non-interactive mode. Use --force to override."));
|
|
301
|
+
for (const conflict of conflicts) {
|
|
302
|
+
console.log(chalk.yellow(` ⚠ ${formatConflicts([conflict])[0]}`));
|
|
303
|
+
}
|
|
304
|
+
// Rollback: uninstall the package
|
|
305
|
+
execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
|
|
306
|
+
process.exitCode = 1;
|
|
307
|
+
results.push({
|
|
308
|
+
name,
|
|
309
|
+
version: info.version,
|
|
310
|
+
success: false,
|
|
311
|
+
error: "Conflicts in non-interactive mode",
|
|
312
|
+
});
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let abort = false;
|
|
317
|
+
for (const conflict of conflicts) {
|
|
318
|
+
const choice = await promptConflictResolution(conflict);
|
|
319
|
+
if (choice === null) {
|
|
320
|
+
abort = true;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
const newPluginIndex = conflict.plugins.length - 1;
|
|
324
|
+
if (choice !== newPluginIndex) {
|
|
325
|
+
skipDestinations.add(conflict.dest);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
201
328
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
329
|
+
if (abort) {
|
|
330
|
+
console.log(chalk.yellow(` Aborted due to conflicts`));
|
|
331
|
+
// Rollback: uninstall the package
|
|
332
|
+
execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
|
|
333
|
+
process.exitCode = 1;
|
|
334
|
+
results.push({ name, version: info.version, success: false, error: "Conflicts" });
|
|
335
|
+
continue;
|
|
209
336
|
}
|
|
210
337
|
}
|
|
211
338
|
}
|
|
212
339
|
|
|
340
|
+
// 6. Create symlinks for omp.install entries (skip destinations user assigned to existing plugins)
|
|
341
|
+
const symlinkResult = await createPluginSymlinks(name, pkgJson, isGlobal, true, skipDestinations);
|
|
342
|
+
createdSymlinks = symlinkResult.created;
|
|
343
|
+
|
|
344
|
+
// 7. Process dependencies with omp field (with cycle detection)
|
|
345
|
+
await processOmpDependencies(pkgJson, isGlobal, new Set([name]));
|
|
346
|
+
|
|
347
|
+
// 8. Update manifest if --save or --save-dev was passed
|
|
348
|
+
// For global mode, npm --save already updates package.json dependencies
|
|
349
|
+
// but we need to handle devDependencies manually
|
|
350
|
+
// For project-local mode, we must manually update plugins.json
|
|
351
|
+
if (options.save || options.saveDev) {
|
|
352
|
+
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
353
|
+
if (options.saveDev) {
|
|
354
|
+
// Save to devDependencies
|
|
355
|
+
if (!pluginsJson.devDependencies) {
|
|
356
|
+
pluginsJson.devDependencies = {};
|
|
357
|
+
}
|
|
358
|
+
pluginsJson.devDependencies[name] = info.version;
|
|
359
|
+
// Remove from plugins if it was there
|
|
360
|
+
delete pluginsJson.plugins[name];
|
|
361
|
+
} else if (!isGlobal) {
|
|
362
|
+
// Save to plugins (project-local mode only - npm handles global)
|
|
363
|
+
pluginsJson.plugins[name] = info.version;
|
|
364
|
+
}
|
|
365
|
+
await savePluginsJson(pluginsJson, isGlobal);
|
|
366
|
+
}
|
|
367
|
+
|
|
213
368
|
// Add to installed plugins map for subsequent conflict detection
|
|
214
369
|
existingPlugins.set(name, pkgJson);
|
|
215
370
|
|
|
371
|
+
// Update lock file with exact version
|
|
372
|
+
await updateLockFile(name, info.version, isGlobal);
|
|
373
|
+
|
|
216
374
|
console.log(chalk.green(`✓ Installed ${name}@${info.version}`));
|
|
217
375
|
results.push({ name, version: info.version, success: true });
|
|
218
376
|
} catch (err) {
|
|
219
377
|
const errorMsg = (err as Error).message;
|
|
220
378
|
console.log(chalk.red(` ✗ Failed to install ${name}: ${errorMsg}`));
|
|
221
|
-
|
|
379
|
+
|
|
380
|
+
// Rollback: remove any symlinks that were created
|
|
381
|
+
if (createdSymlinks.length > 0) {
|
|
382
|
+
console.log(chalk.dim(" Rolling back symlinks..."));
|
|
383
|
+
const baseDir = isGlobal ? PI_CONFIG_DIR : PROJECT_PI_DIR;
|
|
384
|
+
for (const dest of createdSymlinks) {
|
|
385
|
+
try {
|
|
386
|
+
await rm(join(baseDir, dest), { force: true, recursive: true });
|
|
387
|
+
} catch {
|
|
388
|
+
// Ignore cleanup errors
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Rollback: uninstall npm package if it was installed
|
|
394
|
+
if (npmInstallSucceeded) {
|
|
395
|
+
console.log(chalk.dim(" Rolling back npm install..."));
|
|
396
|
+
try {
|
|
397
|
+
execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
|
|
398
|
+
} catch {
|
|
399
|
+
// Ignore cleanup errors
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
process.exitCode = 1;
|
|
404
|
+
results.push({ name, version: resolvedVersion, success: false, error: errorMsg });
|
|
222
405
|
}
|
|
223
406
|
}
|
|
224
407
|
|
|
@@ -232,6 +415,7 @@ export async function installPlugin(packages?: string[], options: InstallOptions
|
|
|
232
415
|
}
|
|
233
416
|
if (failed.length > 0) {
|
|
234
417
|
console.log(chalk.red(`✗ Failed to install ${failed.length} plugin(s)`));
|
|
418
|
+
process.exitCode = 1;
|
|
235
419
|
}
|
|
236
420
|
|
|
237
421
|
if (options.json) {
|
|
@@ -255,6 +439,7 @@ async function installLocalPlugin(
|
|
|
255
439
|
|
|
256
440
|
if (!existsSync(localPath)) {
|
|
257
441
|
console.log(chalk.red(`Error: Path does not exist: ${localPath}`));
|
|
442
|
+
process.exitCode = 1;
|
|
258
443
|
return { name: basename(localPath), version: "local", success: false, error: "Path not found" };
|
|
259
444
|
}
|
|
260
445
|
|
|
@@ -295,6 +480,22 @@ async function installLocalPlugin(
|
|
|
295
480
|
const pluginName = pkgJson.name;
|
|
296
481
|
const pluginDir = join(nodeModules, pluginName);
|
|
297
482
|
|
|
483
|
+
// Check for intra-plugin duplicates
|
|
484
|
+
const intraDupes = detectIntraPluginDuplicates(pkgJson);
|
|
485
|
+
if (intraDupes.length > 0) {
|
|
486
|
+
console.log(chalk.red(`\nError: Plugin has duplicate destinations:`));
|
|
487
|
+
for (const dupe of intraDupes) {
|
|
488
|
+
console.log(chalk.red(` ${dupe.dest} ← ${dupe.sources.join(", ")}`));
|
|
489
|
+
}
|
|
490
|
+
process.exitCode = 1;
|
|
491
|
+
return {
|
|
492
|
+
name: pluginName,
|
|
493
|
+
version: pkgJson.version,
|
|
494
|
+
success: false,
|
|
495
|
+
error: "Duplicate destinations in plugin",
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
298
499
|
console.log(chalk.blue(`\nInstalling ${pluginName} from ${localPath}...`));
|
|
299
500
|
|
|
300
501
|
// Create node_modules directory
|
|
@@ -311,17 +512,30 @@ async function installLocalPlugin(
|
|
|
311
512
|
|
|
312
513
|
// Update plugins.json/package.json
|
|
313
514
|
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
314
|
-
|
|
515
|
+
if (_options.saveDev) {
|
|
516
|
+
if (!pluginsJson.devDependencies) {
|
|
517
|
+
pluginsJson.devDependencies = {};
|
|
518
|
+
}
|
|
519
|
+
pluginsJson.devDependencies[pluginName] = `file:${localPath}`;
|
|
520
|
+
// Remove from plugins if it was there
|
|
521
|
+
delete pluginsJson.plugins[pluginName];
|
|
522
|
+
} else {
|
|
523
|
+
pluginsJson.plugins[pluginName] = `file:${localPath}`;
|
|
524
|
+
}
|
|
315
525
|
await savePluginsJson(pluginsJson, isGlobal);
|
|
316
526
|
|
|
317
527
|
// Create symlinks
|
|
318
528
|
await createPluginSymlinks(pluginName, pkgJson, isGlobal);
|
|
319
529
|
|
|
530
|
+
// Update lock file for local plugin
|
|
531
|
+
await updateLockFile(pluginName, pkgJson.version, isGlobal);
|
|
532
|
+
|
|
320
533
|
console.log(chalk.green(`✓ Installed ${pluginName}@${pkgJson.version}`));
|
|
321
534
|
return { name: pluginName, version: pkgJson.version, success: true };
|
|
322
535
|
} catch (err) {
|
|
323
536
|
const errorMsg = (err as Error).message;
|
|
324
537
|
console.log(chalk.red(` ✗ Failed: ${errorMsg}`));
|
|
538
|
+
process.exitCode = 1;
|
|
325
539
|
return { name: basename(localPath), version: "local", success: false, error: errorMsg };
|
|
326
540
|
}
|
|
327
541
|
}
|
package/src/commands/link.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, rm, symlink } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { loadPluginsJson, type PluginPackageJson, savePluginsJson } from "@omp/manifest";
|
|
6
|
+
import { NODE_MODULES_DIR, PROJECT_NODE_MODULES, resolveScope } from "@omp/paths";
|
|
7
|
+
import { createPluginSymlinks } from "@omp/symlinks";
|
|
4
8
|
import chalk from "chalk";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
|
|
10
|
+
async function confirmCreate(path: string): Promise<boolean> {
|
|
11
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
12
|
+
console.log(chalk.dim(" Non-interactive mode: auto-creating package.json"));
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
rl.question(chalk.yellow(` Create minimal package.json at ${path}? [Y/n] `), (answer) => {
|
|
19
|
+
rl.close();
|
|
20
|
+
resolve(answer.toLowerCase() !== "n");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
8
24
|
|
|
9
25
|
export interface LinkOptions {
|
|
10
26
|
name?: string;
|
|
11
27
|
global?: boolean;
|
|
28
|
+
local?: boolean;
|
|
29
|
+
force?: boolean;
|
|
12
30
|
}
|
|
13
31
|
|
|
14
32
|
/**
|
|
@@ -16,7 +34,7 @@ export interface LinkOptions {
|
|
|
16
34
|
* Creates a symlink in node_modules pointing to the local directory
|
|
17
35
|
*/
|
|
18
36
|
export async function linkPlugin(localPath: string, options: LinkOptions = {}): Promise<void> {
|
|
19
|
-
const isGlobal = options
|
|
37
|
+
const isGlobal = resolveScope(options);
|
|
20
38
|
const nodeModules = isGlobal ? NODE_MODULES_DIR : PROJECT_NODE_MODULES;
|
|
21
39
|
|
|
22
40
|
// Expand ~ to home directory
|
|
@@ -28,6 +46,7 @@ export async function linkPlugin(localPath: string, options: LinkOptions = {}):
|
|
|
28
46
|
// Verify the path exists
|
|
29
47
|
if (!existsSync(localPath)) {
|
|
30
48
|
console.log(chalk.red(`Error: Path does not exist: ${localPath}`));
|
|
49
|
+
process.exitCode = 1;
|
|
31
50
|
return;
|
|
32
51
|
}
|
|
33
52
|
|
|
@@ -50,13 +69,29 @@ export async function linkPlugin(localPath: string, options: LinkOptions = {}):
|
|
|
50
69
|
install: ompJson.install,
|
|
51
70
|
},
|
|
52
71
|
};
|
|
72
|
+
|
|
73
|
+
// Persist the conversion to package.json
|
|
74
|
+
console.log(chalk.dim(" Converting omp.json to package.json..."));
|
|
75
|
+
await writeFile(localPkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
53
76
|
} else {
|
|
77
|
+
// Create minimal package.json so npm operations work correctly
|
|
78
|
+
console.log(chalk.yellow(" No package.json found in target directory."));
|
|
79
|
+
const shouldCreate = await confirmCreate(localPkgJsonPath);
|
|
80
|
+
if (!shouldCreate) {
|
|
81
|
+
console.log(chalk.yellow(" Aborted: package.json required for linking"));
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
54
85
|
pkgJson = {
|
|
55
86
|
name: options.name || basename(localPath),
|
|
56
87
|
version: "0.0.0-dev",
|
|
57
88
|
keywords: ["omp-plugin"],
|
|
89
|
+
omp: {
|
|
90
|
+
install: [],
|
|
91
|
+
},
|
|
58
92
|
};
|
|
59
|
-
console.log(chalk.
|
|
93
|
+
console.log(chalk.dim(" Creating minimal package.json..."));
|
|
94
|
+
await writeFile(localPkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
60
95
|
}
|
|
61
96
|
|
|
62
97
|
const pluginName = options.name || pkgJson.name;
|
|
@@ -65,9 +100,24 @@ export async function linkPlugin(localPath: string, options: LinkOptions = {}):
|
|
|
65
100
|
// Check if already installed
|
|
66
101
|
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
67
102
|
if (pluginsJson.plugins[pluginName]) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
103
|
+
const existingSpec = pluginsJson.plugins[pluginName];
|
|
104
|
+
const isLinked = existingSpec.startsWith("file:");
|
|
105
|
+
|
|
106
|
+
if (isLinked) {
|
|
107
|
+
console.log(chalk.yellow(`Plugin "${pluginName}" is already linked.`));
|
|
108
|
+
console.log(chalk.dim(` Current link: ${existingSpec}`));
|
|
109
|
+
console.log(chalk.dim(" Re-linking..."));
|
|
110
|
+
// Continue with the linking process (will overwrite)
|
|
111
|
+
} else if (options.force) {
|
|
112
|
+
console.log(chalk.yellow(`Plugin "${pluginName}" is installed from npm. Overwriting with link...`));
|
|
113
|
+
// Continue with the linking process (will overwrite)
|
|
114
|
+
} else {
|
|
115
|
+
console.log(chalk.yellow(`Plugin "${pluginName}" is already installed from npm.`));
|
|
116
|
+
console.log(chalk.dim("Use omp uninstall first, or specify a different name with -n"));
|
|
117
|
+
console.log(chalk.dim("Or use --force to overwrite the npm installation"));
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
71
121
|
}
|
|
72
122
|
|
|
73
123
|
try {
|
|
@@ -100,6 +150,7 @@ export async function linkPlugin(localPath: string, options: LinkOptions = {}):
|
|
|
100
150
|
console.log(chalk.dim(" Changes to the source will be reflected immediately"));
|
|
101
151
|
} catch (err) {
|
|
102
152
|
console.log(chalk.red(`Error linking plugin: ${(err as Error).message}`));
|
|
153
|
+
process.exitCode = 1;
|
|
103
154
|
// Cleanup on failure
|
|
104
155
|
try {
|
|
105
156
|
await rm(pluginDir, { force: true });
|
package/src/commands/list.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { loadPluginsJson, readPluginPackageJson } from "@omp/manifest";
|
|
2
|
+
import { resolveScope } from "@omp/paths";
|
|
1
3
|
import chalk from "chalk";
|
|
2
|
-
import { loadPluginsJson, readPluginPackageJson } from "../manifest.js";
|
|
3
4
|
|
|
4
5
|
export interface ListOptions {
|
|
5
6
|
global?: boolean;
|
|
7
|
+
local?: boolean;
|
|
6
8
|
json?: boolean;
|
|
7
9
|
}
|
|
8
10
|
|
|
@@ -10,13 +12,14 @@ export interface ListOptions {
|
|
|
10
12
|
* List all installed plugins
|
|
11
13
|
*/
|
|
12
14
|
export async function listPlugins(options: ListOptions = {}): Promise<void> {
|
|
13
|
-
const isGlobal = options
|
|
15
|
+
const isGlobal = resolveScope(options);
|
|
14
16
|
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
15
17
|
const pluginNames = Object.keys(pluginsJson.plugins);
|
|
16
18
|
|
|
17
19
|
if (pluginNames.length === 0) {
|
|
18
20
|
console.log(chalk.yellow("No plugins installed."));
|
|
19
21
|
console.log(chalk.dim("Install one with: omp install <package>"));
|
|
22
|
+
process.exitCode = 1;
|
|
20
23
|
return;
|
|
21
24
|
}
|
|
22
25
|
|
|
@@ -44,13 +47,15 @@ export async function listPlugins(options: ListOptions = {}): Promise<void> {
|
|
|
44
47
|
const specifier = pluginsJson.plugins[name];
|
|
45
48
|
const isLocal = specifier.startsWith("file:");
|
|
46
49
|
const disabled = pluginsJson.disabled?.includes(name);
|
|
50
|
+
const isMissing = !pkgJson;
|
|
47
51
|
|
|
48
|
-
const version = pkgJson?.version ? chalk.dim(` v${pkgJson.version}`) :
|
|
52
|
+
const version = pkgJson?.version ? chalk.dim(` v${pkgJson.version}`) : chalk.dim(` (${specifier})`);
|
|
49
53
|
const localBadge = isLocal ? chalk.cyan(" (local)") : "";
|
|
50
54
|
const disabledBadge = disabled ? chalk.yellow(" (disabled)") : "";
|
|
51
|
-
const
|
|
55
|
+
const missingBadge = isMissing ? chalk.red(" (missing)") : "";
|
|
56
|
+
const icon = disabled ? chalk.gray("○") : isMissing ? chalk.red("✗") : chalk.green("◆");
|
|
52
57
|
|
|
53
|
-
console.log(`${icon} ${chalk.bold(name)}${version}${localBadge}${disabledBadge}`);
|
|
58
|
+
console.log(`${icon} ${chalk.bold(name)}${version}${localBadge}${disabledBadge}${missingBadge}`);
|
|
54
59
|
|
|
55
60
|
if (pkgJson?.description) {
|
|
56
61
|
console.log(chalk.dim(` ${pkgJson.description}`));
|
package/src/commands/outdated.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { loadPluginsJson } from "@omp/manifest";
|
|
2
|
+
import { npmOutdated } from "@omp/npm";
|
|
3
|
+
import { PLUGINS_DIR, resolveScope } from "@omp/paths";
|
|
1
4
|
import chalk from "chalk";
|
|
2
|
-
import { loadPluginsJson } from "../manifest.js";
|
|
3
|
-
import { npmOutdated } from "../npm.js";
|
|
4
|
-
import { PLUGINS_DIR } from "../paths.js";
|
|
5
5
|
|
|
6
6
|
export interface OutdatedOptions {
|
|
7
7
|
global?: boolean;
|
|
8
|
+
local?: boolean;
|
|
8
9
|
json?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -12,7 +13,7 @@ export interface OutdatedOptions {
|
|
|
12
13
|
* List plugins with newer versions available
|
|
13
14
|
*/
|
|
14
15
|
export async function showOutdated(options: OutdatedOptions = {}): Promise<void> {
|
|
15
|
-
const isGlobal = options
|
|
16
|
+
const isGlobal = resolveScope(options);
|
|
16
17
|
const prefix = isGlobal ? PLUGINS_DIR : ".pi";
|
|
17
18
|
|
|
18
19
|
console.log(chalk.blue("Checking for outdated plugins..."));
|
|
@@ -21,9 +22,12 @@ export async function showOutdated(options: OutdatedOptions = {}): Promise<void>
|
|
|
21
22
|
const outdated = await npmOutdated(prefix);
|
|
22
23
|
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
23
24
|
|
|
24
|
-
// Filter to only show plugins we manage
|
|
25
|
+
// Filter to only show plugins we manage AND are not local
|
|
25
26
|
const managedOutdated = Object.entries(outdated).filter(([name]) => {
|
|
26
|
-
|
|
27
|
+
const specifier = pluginsJson.plugins[name];
|
|
28
|
+
if (!specifier) return false; // Not in our manifest
|
|
29
|
+
if (specifier.startsWith("file:")) return false; // Local plugin, skip
|
|
30
|
+
return true;
|
|
27
31
|
});
|
|
28
32
|
|
|
29
33
|
if (managedOutdated.length === 0) {
|
|
@@ -66,11 +70,18 @@ export async function showOutdated(options: OutdatedOptions = {}): Promise<void>
|
|
|
66
70
|
);
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
// Note about local plugins excluded from check
|
|
74
|
+
const localPlugins = Object.entries(pluginsJson.plugins).filter(([_, spec]) => spec.startsWith("file:"));
|
|
75
|
+
if (localPlugins.length > 0) {
|
|
76
|
+
console.log(chalk.dim(`\nNote: ${localPlugins.length} local plugin(s) excluded from check`));
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
console.log();
|
|
70
80
|
console.log(chalk.dim("Update with: omp update [package]"));
|
|
71
81
|
console.log(chalk.dim(" - 'wanted' = latest within semver range"));
|
|
72
82
|
console.log(chalk.dim(" - 'latest' = latest available version"));
|
|
73
83
|
} catch (err) {
|
|
74
84
|
console.log(chalk.red(`Error checking outdated: ${(err as Error).message}`));
|
|
85
|
+
process.exitCode = 1;
|
|
75
86
|
}
|
|
76
87
|
}
|