@oh-my-pi/cli 0.1.0 → 0.3.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 +243 -138
- package/biome.json +1 -1
- package/bun.lock +59 -0
- package/dist/cli.js +6311 -2900
- package/dist/commands/config.d.ts +32 -0
- package/dist/commands/config.d.ts.map +1 -0
- 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/env.d.ts +14 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/features.d.ts +25 -0
- package/dist/commands/features.d.ts.map +1 -0
- 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 +37 -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 +18 -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 +60 -25
- package/dist/manifest.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 -3
- package/dist/paths.d.ts.map +1 -1
- package/dist/runtime.d.ts +14 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/symlinks.d.ts +43 -7
- package/dist/symlinks.d.ts.map +1 -1
- package/package.json +11 -5
- package/plugins/exa/README.md +153 -0
- package/plugins/exa/package.json +56 -0
- package/plugins/exa/tools/exa/company.ts +35 -0
- package/plugins/exa/tools/exa/index.ts +66 -0
- package/plugins/exa/tools/exa/linkedin.ts +35 -0
- package/plugins/exa/tools/exa/researcher.ts +40 -0
- package/plugins/exa/tools/exa/runtime.json +4 -0
- package/plugins/exa/tools/exa/search.ts +46 -0
- package/plugins/exa/tools/exa/shared.ts +230 -0
- package/plugins/exa/tools/exa/websets.ts +62 -0
- package/plugins/metal-theme/package.json +7 -2
- package/plugins/subagents/package.json +7 -2
- package/plugins/user-prompt/README.md +130 -0
- package/plugins/user-prompt/package.json +19 -0
- package/plugins/user-prompt/tools/user-prompt/index.ts +235 -0
- package/src/cli.ts +133 -58
- package/src/commands/config.ts +384 -0
- package/src/commands/create.ts +51 -1
- package/src/commands/doctor.ts +95 -7
- package/src/commands/enable.ts +25 -8
- package/src/commands/env.ts +38 -0
- package/src/commands/features.ts +295 -0
- package/src/commands/info.ts +41 -5
- package/src/commands/init.ts +20 -2
- package/src/commands/install.ts +453 -80
- package/src/commands/link.ts +60 -9
- package/src/commands/list.ts +122 -7
- 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 +18 -25
- package/src/lock.ts +46 -0
- package/src/lockfile.ts +132 -0
- package/src/manifest.ts +219 -71
- package/src/npm.ts +74 -18
- package/src/paths.ts +77 -12
- package/src/runtime.ts +116 -0
- package/src/symlinks.ts +291 -35
- package/tsconfig.json +7 -3
- package/CHECK.md +0 -352
- package/dist/migrate.d.ts +0 -9
- package/dist/migrate.d.ts.map +0 -1
- package/src/migrate.ts +0 -181
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,22 +1,132 @@
|
|
|
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
|
|
|
11
|
+
/**
|
|
12
|
+
* Known file categories with their patterns and display info
|
|
13
|
+
*/
|
|
14
|
+
interface FileCategory {
|
|
15
|
+
pattern: RegExp;
|
|
16
|
+
label: string;
|
|
17
|
+
color: (s: string) => string;
|
|
18
|
+
extractName: (dest: string) => string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const FILE_CATEGORIES: FileCategory[] = [
|
|
22
|
+
{
|
|
23
|
+
pattern: /^agent\/tools\/([^/]+)\//,
|
|
24
|
+
label: "Tools",
|
|
25
|
+
color: chalk.cyan,
|
|
26
|
+
extractName: (dest) => dest.match(/^agent\/tools\/([^/]+)\//)?.[1] || dest,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: /^agent\/agents\/(.+)\.md$/,
|
|
30
|
+
label: "Agents",
|
|
31
|
+
color: chalk.magenta,
|
|
32
|
+
extractName: (dest) => dest.match(/^agent\/agents\/(.+)\.md$/)?.[1] || dest,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
pattern: /^agent\/commands\/(.+)\.md$/,
|
|
36
|
+
label: "Commands",
|
|
37
|
+
color: chalk.yellow,
|
|
38
|
+
extractName: (dest) => dest.match(/^agent\/commands\/(.+)\.md$/)?.[1] || dest,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
pattern: /^agent\/themes\/(.+)\.json$/,
|
|
42
|
+
label: "Themes",
|
|
43
|
+
color: chalk.green,
|
|
44
|
+
extractName: (dest) => dest.match(/^agent\/themes\/(.+)\.json$/)?.[1] || dest,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
pattern: /^agent\/prompts?\//,
|
|
48
|
+
label: "Prompts",
|
|
49
|
+
color: chalk.blue,
|
|
50
|
+
extractName: (dest) => dest.split("/").pop()?.replace(/\.[^.]+$/, "") || dest,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
pattern: /^agent\/hooks?\//,
|
|
54
|
+
label: "Hooks",
|
|
55
|
+
color: chalk.red,
|
|
56
|
+
extractName: (dest) => dest.split("/").pop()?.replace(/\.[^.]+$/, "") || dest,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Categorize installed files into known categories
|
|
62
|
+
*/
|
|
63
|
+
function categorizeFiles(files: string[]): { categorized: Map<string, string[]>; uncategorized: string[] } {
|
|
64
|
+
const categorized = new Map<string, string[]>();
|
|
65
|
+
const uncategorized: string[] = [];
|
|
66
|
+
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
let matched = false;
|
|
69
|
+
for (const category of FILE_CATEGORIES) {
|
|
70
|
+
if (category.pattern.test(file)) {
|
|
71
|
+
const name = category.extractName(file);
|
|
72
|
+
const existing = categorized.get(category.label) || [];
|
|
73
|
+
if (!existing.includes(name)) {
|
|
74
|
+
existing.push(name);
|
|
75
|
+
categorized.set(category.label, existing);
|
|
76
|
+
}
|
|
77
|
+
matched = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!matched) {
|
|
82
|
+
uncategorized.push(file);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { categorized, uncategorized };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Format categorized files for display
|
|
91
|
+
*/
|
|
92
|
+
function formatContributes(files: string[]): string[] {
|
|
93
|
+
const { categorized, uncategorized } = categorizeFiles(files);
|
|
94
|
+
const lines: string[] = [];
|
|
95
|
+
|
|
96
|
+
if (categorized.size > 0 || uncategorized.length > 0) {
|
|
97
|
+
lines.push(` ${chalk.white("Contributes:")}`);
|
|
98
|
+
|
|
99
|
+
for (const category of FILE_CATEGORIES) {
|
|
100
|
+
const items = categorized.get(category.label);
|
|
101
|
+
if (items && items.length > 0) {
|
|
102
|
+
const count = category.color(`${items.length}`);
|
|
103
|
+
const names = items.map((n) => chalk.dim(n)).join(chalk.dim(", "));
|
|
104
|
+
lines.push(` ${chalk.green("+")} ${category.label} (${count}): ${names}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (uncategorized.length > 0) {
|
|
109
|
+
const count = chalk.gray(`${uncategorized.length}`);
|
|
110
|
+
const names = uncategorized.map((n) => chalk.dim(n)).join(chalk.dim(", "));
|
|
111
|
+
lines.push(` ${chalk.green("+")} Files (${count}): ${names}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
|
|
9
118
|
/**
|
|
10
119
|
* List all installed plugins
|
|
11
120
|
*/
|
|
12
121
|
export async function listPlugins(options: ListOptions = {}): Promise<void> {
|
|
13
|
-
const isGlobal = options
|
|
122
|
+
const isGlobal = resolveScope(options);
|
|
14
123
|
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
15
124
|
const pluginNames = Object.keys(pluginsJson.plugins);
|
|
16
125
|
|
|
17
126
|
if (pluginNames.length === 0) {
|
|
18
127
|
console.log(chalk.yellow("No plugins installed."));
|
|
19
128
|
console.log(chalk.dim("Install one with: omp install <package>"));
|
|
129
|
+
process.exitCode = 1;
|
|
20
130
|
return;
|
|
21
131
|
}
|
|
22
132
|
|
|
@@ -44,13 +154,15 @@ export async function listPlugins(options: ListOptions = {}): Promise<void> {
|
|
|
44
154
|
const specifier = pluginsJson.plugins[name];
|
|
45
155
|
const isLocal = specifier.startsWith("file:");
|
|
46
156
|
const disabled = pluginsJson.disabled?.includes(name);
|
|
157
|
+
const isMissing = !pkgJson;
|
|
47
158
|
|
|
48
|
-
const version = pkgJson?.version ? chalk.dim(`
|
|
159
|
+
const version = pkgJson?.version ? chalk.dim(`v${pkgJson.version}`) : chalk.dim(`(${specifier})`);
|
|
49
160
|
const localBadge = isLocal ? chalk.cyan(" (local)") : "";
|
|
50
161
|
const disabledBadge = disabled ? chalk.yellow(" (disabled)") : "";
|
|
51
|
-
const
|
|
162
|
+
const missingBadge = isMissing ? chalk.red(" (missing)") : "";
|
|
163
|
+
const icon = disabled ? chalk.gray("○") : isMissing ? chalk.red("✗") : chalk.green("◆");
|
|
52
164
|
|
|
53
|
-
console.log(`${icon} ${chalk.bold(name)}${version}${localBadge}${disabledBadge}`);
|
|
165
|
+
console.log(`${icon} ${chalk.bold(name)} ${version}${localBadge}${disabledBadge}${missingBadge}`);
|
|
54
166
|
|
|
55
167
|
if (pkgJson?.description) {
|
|
56
168
|
console.log(chalk.dim(` ${pkgJson.description}`));
|
|
@@ -58,12 +170,15 @@ export async function listPlugins(options: ListOptions = {}): Promise<void> {
|
|
|
58
170
|
|
|
59
171
|
if (isLocal) {
|
|
60
172
|
const localPath = specifier.replace("file:", "");
|
|
61
|
-
console.log(chalk.dim(`
|
|
173
|
+
console.log(chalk.dim(` Path: ${localPath}`));
|
|
62
174
|
}
|
|
63
175
|
|
|
64
176
|
if (pkgJson?.omp?.install?.length) {
|
|
65
177
|
const files = pkgJson.omp.install.map((e) => e.dest);
|
|
66
|
-
|
|
178
|
+
const contributeLines = formatContributes(files);
|
|
179
|
+
for (const line of contributeLines) {
|
|
180
|
+
console.log(line);
|
|
181
|
+
}
|
|
67
182
|
}
|
|
68
183
|
|
|
69
184
|
console.log();
|
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
|
}
|
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
|
}
|