@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/search.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { npmSearch } from "@omp/npm";
|
|
1
2
|
import chalk from "chalk";
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
function truncate(str: string, maxLen: number): string {
|
|
5
|
+
if (!str || str.length <= maxLen) return str;
|
|
6
|
+
return `${str.slice(0, maxLen - 3)}...`;
|
|
7
|
+
}
|
|
3
8
|
|
|
4
9
|
export interface SearchOptions {
|
|
5
10
|
json?: boolean;
|
|
@@ -19,6 +24,7 @@ export async function searchPlugins(query: string, options: SearchOptions = {}):
|
|
|
19
24
|
console.log(chalk.yellow("\nNo plugins found."));
|
|
20
25
|
console.log(chalk.dim("Try a different search term, or search without keyword:"));
|
|
21
26
|
console.log(chalk.dim(" npm search omp-plugin"));
|
|
27
|
+
process.exitCode = 1;
|
|
22
28
|
return;
|
|
23
29
|
}
|
|
24
30
|
|
|
@@ -36,7 +42,7 @@ export async function searchPlugins(query: string, options: SearchOptions = {}):
|
|
|
36
42
|
console.log(chalk.green("◆ ") + chalk.bold(result.name) + chalk.dim(` v${result.version}`));
|
|
37
43
|
|
|
38
44
|
if (result.description) {
|
|
39
|
-
console.log(chalk.dim(` ${result.description}`));
|
|
45
|
+
console.log(chalk.dim(` ${truncate(result.description, 100)}`));
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
if (result.keywords?.length) {
|
|
@@ -55,6 +61,17 @@ export async function searchPlugins(query: string, options: SearchOptions = {}):
|
|
|
55
61
|
|
|
56
62
|
console.log(chalk.dim("Install with: omp install <package-name>"));
|
|
57
63
|
} catch (err) {
|
|
58
|
-
|
|
64
|
+
const error = err as Error;
|
|
65
|
+
if (
|
|
66
|
+
error.message.includes("ENOTFOUND") ||
|
|
67
|
+
error.message.includes("ETIMEDOUT") ||
|
|
68
|
+
error.message.includes("EAI_AGAIN")
|
|
69
|
+
) {
|
|
70
|
+
console.log(chalk.red("\nNetwork error: Unable to reach npm registry."));
|
|
71
|
+
console.log(chalk.dim(" Check your internet connection and try again."));
|
|
72
|
+
} else {
|
|
73
|
+
console.log(chalk.red(`\nSearch failed: ${error.message}`));
|
|
74
|
+
}
|
|
75
|
+
process.exitCode = 1;
|
|
59
76
|
}
|
|
60
77
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { getInstalledPlugins, loadPluginsJson, readPluginPackageJson, savePluginsJson } from "@omp/manifest";
|
|
6
|
+
import { npmUninstall } from "@omp/npm";
|
|
7
|
+
import { NODE_MODULES_DIR, PLUGINS_DIR, PROJECT_NODE_MODULES, resolveScope } from "@omp/paths";
|
|
8
|
+
import { removePluginSymlinks } from "@omp/symlinks";
|
|
4
9
|
import chalk from "chalk";
|
|
5
|
-
import { loadPluginsJson, readPluginPackageJson, savePluginsJson } from "../manifest.js";
|
|
6
|
-
import { npmUninstall } from "../npm.js";
|
|
7
|
-
import { NODE_MODULES_DIR, PLUGINS_DIR, PROJECT_NODE_MODULES } from "../paths.js";
|
|
8
|
-
import { removePluginSymlinks } from "../symlinks.js";
|
|
9
10
|
|
|
10
11
|
export interface UninstallOptions {
|
|
11
12
|
global?: boolean;
|
|
13
|
+
local?: boolean;
|
|
12
14
|
json?: boolean;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -16,7 +18,7 @@ export interface UninstallOptions {
|
|
|
16
18
|
* Uninstall a plugin
|
|
17
19
|
*/
|
|
18
20
|
export async function uninstallPlugin(name: string, options: UninstallOptions = {}): Promise<void> {
|
|
19
|
-
const isGlobal = options
|
|
21
|
+
const isGlobal = resolveScope(options);
|
|
20
22
|
const prefix = isGlobal ? PLUGINS_DIR : ".pi";
|
|
21
23
|
const nodeModules = isGlobal ? NODE_MODULES_DIR : PROJECT_NODE_MODULES;
|
|
22
24
|
|
|
@@ -24,6 +26,7 @@ export async function uninstallPlugin(name: string, options: UninstallOptions =
|
|
|
24
26
|
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
25
27
|
if (!pluginsJson.plugins[name]) {
|
|
26
28
|
console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
|
|
29
|
+
process.exitCode = 1;
|
|
27
30
|
return;
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -33,9 +36,56 @@ export async function uninstallPlugin(name: string, options: UninstallOptions =
|
|
|
33
36
|
// 1. Read package.json for omp.install entries before uninstalling
|
|
34
37
|
const pkgJson = await readPluginPackageJson(name, isGlobal);
|
|
35
38
|
|
|
39
|
+
// Check for shared dependencies
|
|
40
|
+
if (pkgJson?.dependencies) {
|
|
41
|
+
const allPlugins = await getInstalledPlugins(isGlobal);
|
|
42
|
+
const sharedDeps: string[] = [];
|
|
43
|
+
|
|
44
|
+
for (const depName of Object.keys(pkgJson.dependencies)) {
|
|
45
|
+
for (const [otherName, otherPkgJson] of allPlugins) {
|
|
46
|
+
if (otherName !== name && otherPkgJson.dependencies?.[depName]) {
|
|
47
|
+
sharedDeps.push(`${depName} (also used by ${otherName})`);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (sharedDeps.length > 0) {
|
|
54
|
+
console.log(chalk.yellow("\n⚠ Warning: This plugin shares dependencies with other plugins:"));
|
|
55
|
+
for (const dep of sharedDeps) {
|
|
56
|
+
console.log(chalk.dim(` - ${dep}`));
|
|
57
|
+
}
|
|
58
|
+
console.log(chalk.dim(" These dependencies will remain installed."));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
36
62
|
// 2. Remove symlinks
|
|
37
63
|
if (pkgJson) {
|
|
38
|
-
await removePluginSymlinks(name, pkgJson);
|
|
64
|
+
const result = await removePluginSymlinks(name, pkgJson, isGlobal);
|
|
65
|
+
|
|
66
|
+
if (result.skippedNonSymlinks.length > 0) {
|
|
67
|
+
console.log(chalk.yellow("\nThe following files are not symlinks and were not removed:"));
|
|
68
|
+
for (const file of result.skippedNonSymlinks) {
|
|
69
|
+
console.log(chalk.dim(` - ${file}`));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
73
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
74
|
+
const answer = await new Promise<string>((resolve) => {
|
|
75
|
+
rl.question(chalk.yellow("Delete these files anyway? [y/N] "), (ans) => {
|
|
76
|
+
rl.close();
|
|
77
|
+
resolve(ans);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (answer.toLowerCase() === "y") {
|
|
82
|
+
for (const file of result.skippedNonSymlinks) {
|
|
83
|
+
await rm(file, { force: true, recursive: true });
|
|
84
|
+
console.log(chalk.dim(` Deleted: ${file}`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
39
89
|
}
|
|
40
90
|
|
|
41
91
|
// 3. npm uninstall
|
|
@@ -65,6 +115,7 @@ export async function uninstallPlugin(name: string, options: UninstallOptions =
|
|
|
65
115
|
}
|
|
66
116
|
} catch (err) {
|
|
67
117
|
console.log(chalk.red(`Error uninstalling plugin: ${(err as Error).message}`));
|
|
118
|
+
process.exitCode = 1;
|
|
68
119
|
|
|
69
120
|
if (options.json) {
|
|
70
121
|
console.log(JSON.stringify({ name, success: false, error: (err as Error).message }, null, 2));
|
package/src/commands/update.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { loadPluginsJson, type OmpInstallEntry, type PluginPackageJson, readPluginPackageJson } from "@omp/manifest";
|
|
4
|
+
import { npmUpdate } from "@omp/npm";
|
|
5
|
+
import {
|
|
6
|
+
NODE_MODULES_DIR,
|
|
7
|
+
PI_CONFIG_DIR,
|
|
8
|
+
PLUGINS_DIR,
|
|
9
|
+
PROJECT_NODE_MODULES,
|
|
10
|
+
PROJECT_PI_DIR,
|
|
11
|
+
resolveScope,
|
|
12
|
+
} from "@omp/paths";
|
|
13
|
+
import { createPluginSymlinks, removePluginSymlinks } from "@omp/symlinks";
|
|
1
14
|
import chalk from "chalk";
|
|
2
|
-
import { loadPluginsJson, readPluginPackageJson } from "../manifest.js";
|
|
3
|
-
import { npmUpdate } from "../npm.js";
|
|
4
|
-
import { NODE_MODULES_DIR, PLUGINS_DIR, PROJECT_NODE_MODULES } from "../paths.js";
|
|
5
|
-
import { createPluginSymlinks, removePluginSymlinks } from "../symlinks.js";
|
|
6
15
|
|
|
7
16
|
export interface UpdateOptions {
|
|
8
17
|
global?: boolean;
|
|
18
|
+
local?: boolean;
|
|
9
19
|
json?: boolean;
|
|
10
20
|
}
|
|
11
21
|
|
|
@@ -13,7 +23,7 @@ export interface UpdateOptions {
|
|
|
13
23
|
* Update plugin(s) to latest within semver range
|
|
14
24
|
*/
|
|
15
25
|
export async function updatePlugin(name?: string, options: UpdateOptions = {}): Promise<void> {
|
|
16
|
-
const isGlobal = options
|
|
26
|
+
const isGlobal = resolveScope(options);
|
|
17
27
|
const prefix = isGlobal ? PLUGINS_DIR : ".pi";
|
|
18
28
|
const _nodeModules = isGlobal ? NODE_MODULES_DIR : PROJECT_NODE_MODULES;
|
|
19
29
|
|
|
@@ -22,12 +32,14 @@ export async function updatePlugin(name?: string, options: UpdateOptions = {}):
|
|
|
22
32
|
|
|
23
33
|
if (pluginNames.length === 0) {
|
|
24
34
|
console.log(chalk.yellow("No plugins installed."));
|
|
35
|
+
process.exitCode = 1;
|
|
25
36
|
return;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
// If specific plugin name provided, verify it's installed
|
|
29
40
|
if (name && !pluginsJson.plugins[name]) {
|
|
30
41
|
console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
|
|
42
|
+
process.exitCode = 1;
|
|
31
43
|
return;
|
|
32
44
|
}
|
|
33
45
|
|
|
@@ -50,6 +62,7 @@ export async function updatePlugin(name?: string, options: UpdateOptions = {}):
|
|
|
50
62
|
|
|
51
63
|
if (npmPlugins.length === 0) {
|
|
52
64
|
console.log(chalk.yellow("No npm plugins to update."));
|
|
65
|
+
process.exitCode = 1;
|
|
53
66
|
return;
|
|
54
67
|
}
|
|
55
68
|
|
|
@@ -57,22 +70,35 @@ export async function updatePlugin(name?: string, options: UpdateOptions = {}):
|
|
|
57
70
|
|
|
58
71
|
const results: Array<{ name: string; from: string; to: string; success: boolean }> = [];
|
|
59
72
|
|
|
73
|
+
// Save old package info before removing symlinks (for recovery on failure)
|
|
74
|
+
const oldPkgJsons = new Map<string, PluginPackageJson>();
|
|
75
|
+
const beforeVersions: Record<string, string> = {};
|
|
76
|
+
const oldInstallEntries = new Map<string, OmpInstallEntry[]>();
|
|
77
|
+
|
|
60
78
|
try {
|
|
61
|
-
// Get current versions before update
|
|
62
|
-
const beforeVersions: Record<string, string> = {};
|
|
79
|
+
// Get current versions and install entries before update
|
|
63
80
|
for (const pluginName of npmPlugins) {
|
|
64
81
|
const pkgJson = await readPluginPackageJson(pluginName, isGlobal);
|
|
65
82
|
if (pkgJson) {
|
|
83
|
+
oldPkgJsons.set(pluginName, pkgJson);
|
|
66
84
|
beforeVersions[pluginName] = pkgJson.version;
|
|
67
85
|
|
|
86
|
+
// Save old install entries for later comparison
|
|
87
|
+
if (pkgJson.omp?.install) {
|
|
88
|
+
oldInstallEntries.set(pluginName, [...pkgJson.omp.install]);
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
// Remove old symlinks before update
|
|
69
|
-
await removePluginSymlinks(pluginName, pkgJson,
|
|
92
|
+
await removePluginSymlinks(pluginName, pkgJson, isGlobal);
|
|
70
93
|
}
|
|
71
94
|
}
|
|
72
95
|
|
|
73
96
|
// npm update
|
|
74
97
|
await npmUpdate(npmPlugins, prefix);
|
|
75
98
|
|
|
99
|
+
// Base directory for symlink destinations
|
|
100
|
+
const baseDir = isGlobal ? PI_CONFIG_DIR : PROJECT_PI_DIR;
|
|
101
|
+
|
|
76
102
|
// Re-process symlinks for each updated plugin
|
|
77
103
|
for (const pluginName of npmPlugins) {
|
|
78
104
|
const pkgJson = await readPluginPackageJson(pluginName, isGlobal);
|
|
@@ -80,7 +106,25 @@ export async function updatePlugin(name?: string, options: UpdateOptions = {}):
|
|
|
80
106
|
const beforeVersion = beforeVersions[pluginName] || "unknown";
|
|
81
107
|
const afterVersion = pkgJson.version;
|
|
82
108
|
|
|
83
|
-
//
|
|
109
|
+
// Handle changed omp.install entries: remove orphaned symlinks
|
|
110
|
+
const oldEntries = oldInstallEntries.get(pluginName) || [];
|
|
111
|
+
const newEntries = pkgJson.omp?.install || [];
|
|
112
|
+
const newDests = new Set(newEntries.map((e) => e.dest));
|
|
113
|
+
|
|
114
|
+
for (const oldEntry of oldEntries) {
|
|
115
|
+
if (!newDests.has(oldEntry.dest)) {
|
|
116
|
+
// This destination was in the old version but not in the new one
|
|
117
|
+
const dest = join(baseDir, oldEntry.dest);
|
|
118
|
+
try {
|
|
119
|
+
await rm(dest, { force: true });
|
|
120
|
+
console.log(chalk.dim(` Removed orphaned: ${oldEntry.dest}`));
|
|
121
|
+
} catch {
|
|
122
|
+
// Ignore removal errors for orphaned symlinks
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Create new symlinks (handles overwrites for existing destinations)
|
|
84
128
|
await createPluginSymlinks(pluginName, pkgJson, isGlobal);
|
|
85
129
|
|
|
86
130
|
const changed = beforeVersion !== afterVersion;
|
|
@@ -107,6 +151,20 @@ export async function updatePlugin(name?: string, options: UpdateOptions = {}):
|
|
|
107
151
|
console.log(JSON.stringify({ results }, null, 2));
|
|
108
152
|
}
|
|
109
153
|
} catch (err) {
|
|
154
|
+
// Restore old symlinks on failure
|
|
155
|
+
if (oldPkgJsons.size > 0) {
|
|
156
|
+
console.log(chalk.yellow(" Update failed, restoring symlinks..."));
|
|
157
|
+
for (const [pluginName, pkgJson] of oldPkgJsons) {
|
|
158
|
+
try {
|
|
159
|
+
await createPluginSymlinks(pluginName, pkgJson, isGlobal);
|
|
160
|
+
} catch (restoreErr) {
|
|
161
|
+
console.log(
|
|
162
|
+
chalk.red(` Failed to restore symlinks for ${pluginName}: ${(restoreErr as Error).message}`),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
110
167
|
console.log(chalk.red(`Error updating plugins: ${(err as Error).message}`));
|
|
168
|
+
process.exitCode = 1;
|
|
111
169
|
}
|
|
112
170
|
}
|
package/src/commands/why.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { existsSync, lstatSync } from "node:fs";
|
|
2
2
|
import { readlink } from "node:fs/promises";
|
|
3
|
-
import { join, relative } from "node:path";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
import { getInstalledPlugins, getPluginSourceDir, readPluginPackageJson } from "@omp/manifest";
|
|
5
|
+
import { PI_CONFIG_DIR, PROJECT_PI_DIR, resolveScope } from "@omp/paths";
|
|
6
|
+
import { traceInstalledFile } from "@omp/symlinks";
|
|
4
7
|
import chalk from "chalk";
|
|
5
|
-
import { getInstalledPlugins, readPluginPackageJson } from "../manifest.js";
|
|
6
|
-
import { PI_CONFIG_DIR } from "../paths.js";
|
|
7
|
-
import { traceInstalledFile } from "../symlinks.js";
|
|
8
8
|
|
|
9
9
|
export interface WhyOptions {
|
|
10
10
|
global?: boolean;
|
|
11
|
+
local?: boolean;
|
|
11
12
|
json?: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -15,31 +16,45 @@ export interface WhyOptions {
|
|
|
15
16
|
* Show which plugin installed a file
|
|
16
17
|
*/
|
|
17
18
|
export async function whyFile(filePath: string, options: WhyOptions = {}): Promise<void> {
|
|
18
|
-
const isGlobal = options
|
|
19
|
+
const isGlobal = resolveScope(options);
|
|
19
20
|
|
|
20
|
-
//
|
|
21
|
+
// Determine the base directory based on scope
|
|
22
|
+
const baseDir = isGlobal ? PI_CONFIG_DIR : resolve(PROJECT_PI_DIR);
|
|
23
|
+
|
|
24
|
+
// Normalize path - make it relative to the appropriate base directory
|
|
21
25
|
let relativePath = filePath;
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
if (isGlobal) {
|
|
27
|
+
if (filePath.startsWith(PI_CONFIG_DIR)) {
|
|
28
|
+
relativePath = relative(PI_CONFIG_DIR, filePath);
|
|
29
|
+
} else if (filePath.startsWith("~/.pi/")) {
|
|
30
|
+
relativePath = filePath.slice(6); // Remove ~/.pi/
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
// Project-local mode
|
|
34
|
+
const resolvedProjectDir = resolve(PROJECT_PI_DIR);
|
|
35
|
+
if (filePath.startsWith(resolvedProjectDir)) {
|
|
36
|
+
relativePath = relative(resolvedProjectDir, filePath);
|
|
37
|
+
} else if (filePath.startsWith(".pi/")) {
|
|
38
|
+
relativePath = filePath.slice(4); // Remove .pi/
|
|
39
|
+
}
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
// Check if it's a path in agent/ directory
|
|
29
43
|
if (!relativePath.startsWith("agent/")) {
|
|
30
44
|
// Try prepending agent/
|
|
31
45
|
const withAgent = `agent/${relativePath}`;
|
|
32
|
-
const fullWithAgent = join(
|
|
46
|
+
const fullWithAgent = join(baseDir, withAgent);
|
|
33
47
|
if (existsSync(fullWithAgent)) {
|
|
34
48
|
relativePath = withAgent;
|
|
35
49
|
}
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
const fullPath = join(
|
|
52
|
+
const fullPath = join(baseDir, relativePath);
|
|
39
53
|
|
|
40
54
|
// Check if file exists
|
|
41
55
|
if (!existsSync(fullPath)) {
|
|
42
56
|
console.log(chalk.yellow(`File not found: ${fullPath}`));
|
|
57
|
+
process.exitCode = 1;
|
|
43
58
|
return;
|
|
44
59
|
}
|
|
45
60
|
|
|
@@ -54,7 +69,7 @@ export async function whyFile(filePath: string, options: WhyOptions = {}): Promi
|
|
|
54
69
|
|
|
55
70
|
// Search through installed plugins
|
|
56
71
|
const installedPlugins = await getInstalledPlugins(isGlobal);
|
|
57
|
-
const result = await traceInstalledFile(relativePath, installedPlugins);
|
|
72
|
+
const result = await traceInstalledFile(relativePath, installedPlugins, isGlobal);
|
|
58
73
|
|
|
59
74
|
if (options.json) {
|
|
60
75
|
console.log(
|
|
@@ -85,9 +100,25 @@ export async function whyFile(filePath: string, options: WhyOptions = {}): Promi
|
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
if (result) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
// Verify it's actually a symlink pointing to the right place
|
|
104
|
+
if (!isSymlink) {
|
|
105
|
+
console.log(chalk.yellow("⚠ This file exists but is not a symlink"));
|
|
106
|
+
console.log(chalk.dim(" It may have been manually created or the symlink was replaced."));
|
|
107
|
+
console.log(chalk.dim(` Expected to be installed by: ${result.plugin}`));
|
|
108
|
+
} else {
|
|
109
|
+
// Verify symlink points to correct source
|
|
110
|
+
const expectedSrc = join(getPluginSourceDir(result.plugin, isGlobal), result.entry.src);
|
|
111
|
+
if (target !== expectedSrc) {
|
|
112
|
+
console.log(chalk.yellow("⚠ Symlink target does not match expected source"));
|
|
113
|
+
console.log(chalk.dim(` Expected: ${expectedSrc}`));
|
|
114
|
+
console.log(chalk.dim(` Actual: ${target}`));
|
|
115
|
+
console.log(chalk.dim(` Expected to be installed by: ${result.plugin}`));
|
|
116
|
+
} else {
|
|
117
|
+
console.log(chalk.green(`✓ Installed by: ${result.plugin}`));
|
|
118
|
+
console.log(chalk.dim(` Source: ${result.entry.src}`));
|
|
119
|
+
console.log(chalk.dim(` Dest: ${result.entry.dest}`));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
91
122
|
|
|
92
123
|
// Get plugin info
|
|
93
124
|
const pkgJson = await readPluginPackageJson(result.plugin, isGlobal);
|
package/src/conflicts.ts
CHANGED
|
@@ -1,10 +1,42 @@
|
|
|
1
|
-
import type { PluginPackageJson } from "
|
|
1
|
+
import type { PluginPackageJson } from "@omp/manifest";
|
|
2
2
|
|
|
3
3
|
export interface Conflict {
|
|
4
4
|
dest: string;
|
|
5
5
|
plugins: Array<{ name: string; src: string }>;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface IntraPluginDuplicate {
|
|
9
|
+
dest: string;
|
|
10
|
+
sources: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Detect duplicate destinations within a single plugin's omp.install array
|
|
15
|
+
*/
|
|
16
|
+
export function detectIntraPluginDuplicates(pkgJson: PluginPackageJson): IntraPluginDuplicate[] {
|
|
17
|
+
const duplicates: IntraPluginDuplicate[] = [];
|
|
18
|
+
|
|
19
|
+
if (!pkgJson.omp?.install?.length) {
|
|
20
|
+
return duplicates;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const destMap = new Map<string, string[]>();
|
|
24
|
+
|
|
25
|
+
for (const entry of pkgJson.omp.install) {
|
|
26
|
+
const sources = destMap.get(entry.dest) || [];
|
|
27
|
+
sources.push(entry.src);
|
|
28
|
+
destMap.set(entry.dest, sources);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const [dest, sources] of destMap) {
|
|
32
|
+
if (sources.length > 1) {
|
|
33
|
+
duplicates.push({ dest, sources });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return duplicates;
|
|
38
|
+
}
|
|
39
|
+
|
|
8
40
|
/**
|
|
9
41
|
* Detect conflicts between a new plugin and existing plugins
|
|
10
42
|
*/
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wraps a command function with consistent error handling.
|
|
5
|
+
* - Catches errors and logs user-friendly messages
|
|
6
|
+
* - Shows stack trace only when DEBUG env var is set
|
|
7
|
+
* - Sets non-zero exit code on error
|
|
8
|
+
*/
|
|
9
|
+
export function withErrorHandling<T extends (...args: any[]) => Promise<void>>(fn: T): T {
|
|
10
|
+
return (async (...args: any[]) => {
|
|
11
|
+
try {
|
|
12
|
+
await fn(...args);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
const error = err as Error;
|
|
15
|
+
console.log(chalk.red(`Error: ${error.message}`));
|
|
16
|
+
if (process.env.DEBUG) {
|
|
17
|
+
console.log(chalk.dim(error.stack));
|
|
18
|
+
}
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
}
|
|
21
|
+
}) as T;
|
|
22
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,43 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export {
|
|
13
|
-
export {
|
|
14
|
-
export { uninstallPlugin } from "./commands/uninstall.js";
|
|
15
|
-
export { updatePlugin } from "./commands/update.js";
|
|
16
|
-
export { whyFile } from "./commands/why.js";
|
|
1
|
+
export { createPlugin } from "@omp/commands/create";
|
|
2
|
+
export { runDoctor } from "@omp/commands/doctor";
|
|
3
|
+
export { disablePlugin, enablePlugin } from "@omp/commands/enable";
|
|
4
|
+
export { showInfo } from "@omp/commands/info";
|
|
5
|
+
export { initProject } from "@omp/commands/init";
|
|
6
|
+
export { installPlugin } from "@omp/commands/install";
|
|
7
|
+
export { linkPlugin } from "@omp/commands/link";
|
|
8
|
+
export { listPlugins } from "@omp/commands/list";
|
|
9
|
+
export { showOutdated } from "@omp/commands/outdated";
|
|
10
|
+
export { searchPlugins } from "@omp/commands/search";
|
|
11
|
+
export { uninstallPlugin } from "@omp/commands/uninstall";
|
|
12
|
+
export { updatePlugin } from "@omp/commands/update";
|
|
13
|
+
export { whyFile } from "@omp/commands/why";
|
|
17
14
|
export {
|
|
18
15
|
detectAllConflicts,
|
|
19
16
|
detectConflicts,
|
|
20
17
|
formatConflicts,
|
|
21
|
-
} from "
|
|
18
|
+
} from "@omp/conflicts";
|
|
22
19
|
|
|
23
|
-
// Types
|
|
24
20
|
export type {
|
|
25
21
|
OmpField,
|
|
26
22
|
OmpInstallEntry,
|
|
27
23
|
PluginPackageJson,
|
|
28
24
|
PluginsJson,
|
|
29
|
-
} from "
|
|
25
|
+
} from "@omp/manifest";
|
|
30
26
|
|
|
31
|
-
// Utilities
|
|
32
27
|
export {
|
|
33
28
|
getInstalledPlugins,
|
|
34
29
|
initGlobalPlugins,
|
|
35
30
|
loadPluginsJson,
|
|
36
31
|
readPluginPackageJson,
|
|
37
32
|
savePluginsJson,
|
|
38
|
-
} from "
|
|
39
|
-
|
|
40
|
-
export { checkMigration, migrateToNpm } from "./migrate.js";
|
|
33
|
+
} from "@omp/manifest";
|
|
34
|
+
export { checkMigration, migrateToNpm } from "@omp/migrate";
|
|
41
35
|
export {
|
|
42
36
|
npmInfo,
|
|
43
37
|
npmInstall,
|
|
@@ -45,9 +39,9 @@ export {
|
|
|
45
39
|
npmSearch,
|
|
46
40
|
npmUninstall,
|
|
47
41
|
npmUpdate,
|
|
48
|
-
} from "
|
|
42
|
+
} from "@omp/npm";
|
|
49
43
|
export {
|
|
50
44
|
checkPluginSymlinks,
|
|
51
45
|
createPluginSymlinks,
|
|
52
46
|
removePluginSymlinks,
|
|
53
|
-
} from "
|
|
47
|
+
} from "@omp/symlinks";
|
package/src/lock.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { PI_CONFIG_DIR, PROJECT_PI_DIR } from "@omp/paths";
|
|
5
|
+
|
|
6
|
+
const LOCK_TIMEOUT_MS = 60000; // 1 minute
|
|
7
|
+
|
|
8
|
+
export async function acquireLock(global = true): Promise<boolean> {
|
|
9
|
+
const lockPath = global ? join(PI_CONFIG_DIR, ".lock") : join(PROJECT_PI_DIR, ".lock");
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
13
|
+
|
|
14
|
+
// Check for existing lock
|
|
15
|
+
if (existsSync(lockPath)) {
|
|
16
|
+
const content = await readFile(lockPath, "utf-8");
|
|
17
|
+
const { pid, timestamp } = JSON.parse(content);
|
|
18
|
+
|
|
19
|
+
// Check if stale (older than timeout)
|
|
20
|
+
if (Date.now() - timestamp > LOCK_TIMEOUT_MS) {
|
|
21
|
+
// Stale lock, remove it
|
|
22
|
+
await rm(lockPath, { force: true });
|
|
23
|
+
} else {
|
|
24
|
+
// Check if process is still alive
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0); // Signal 0 = check existence
|
|
27
|
+
return false; // Process alive, can't acquire
|
|
28
|
+
} catch {
|
|
29
|
+
// Process dead, remove stale lock
|
|
30
|
+
await rm(lockPath, { force: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Create lock
|
|
36
|
+
await writeFile(lockPath, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function releaseLock(global = true): Promise<void> {
|
|
44
|
+
const lockPath = global ? join(PI_CONFIG_DIR, ".lock") : join(PROJECT_PI_DIR, ".lock");
|
|
45
|
+
await rm(lockPath, { force: true });
|
|
46
|
+
}
|