@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.
Files changed (105) hide show
  1. package/.github/icon.png +0 -0
  2. package/.github/logo.png +0 -0
  3. package/.github/workflows/publish.yml +1 -1
  4. package/LICENSE +21 -0
  5. package/README.md +243 -138
  6. package/biome.json +1 -1
  7. package/bun.lock +59 -0
  8. package/dist/cli.js +6311 -2900
  9. package/dist/commands/config.d.ts +32 -0
  10. package/dist/commands/config.d.ts.map +1 -0
  11. package/dist/commands/create.d.ts.map +1 -1
  12. package/dist/commands/doctor.d.ts +1 -0
  13. package/dist/commands/doctor.d.ts.map +1 -1
  14. package/dist/commands/enable.d.ts +1 -0
  15. package/dist/commands/enable.d.ts.map +1 -1
  16. package/dist/commands/env.d.ts +14 -0
  17. package/dist/commands/env.d.ts.map +1 -0
  18. package/dist/commands/features.d.ts +25 -0
  19. package/dist/commands/features.d.ts.map +1 -0
  20. package/dist/commands/info.d.ts +1 -0
  21. package/dist/commands/info.d.ts.map +1 -1
  22. package/dist/commands/init.d.ts.map +1 -1
  23. package/dist/commands/install.d.ts +37 -0
  24. package/dist/commands/install.d.ts.map +1 -1
  25. package/dist/commands/link.d.ts +2 -0
  26. package/dist/commands/link.d.ts.map +1 -1
  27. package/dist/commands/list.d.ts +1 -0
  28. package/dist/commands/list.d.ts.map +1 -1
  29. package/dist/commands/outdated.d.ts +1 -0
  30. package/dist/commands/outdated.d.ts.map +1 -1
  31. package/dist/commands/search.d.ts.map +1 -1
  32. package/dist/commands/uninstall.d.ts +1 -0
  33. package/dist/commands/uninstall.d.ts.map +1 -1
  34. package/dist/commands/update.d.ts +1 -0
  35. package/dist/commands/update.d.ts.map +1 -1
  36. package/dist/commands/why.d.ts +1 -0
  37. package/dist/commands/why.d.ts.map +1 -1
  38. package/dist/conflicts.d.ts +9 -1
  39. package/dist/conflicts.d.ts.map +1 -1
  40. package/dist/errors.d.ts +8 -0
  41. package/dist/errors.d.ts.map +1 -0
  42. package/dist/index.d.ts +18 -19
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/lock.d.ts +3 -0
  45. package/dist/lock.d.ts.map +1 -0
  46. package/dist/lockfile.d.ts +52 -0
  47. package/dist/lockfile.d.ts.map +1 -0
  48. package/dist/manifest.d.ts +60 -25
  49. package/dist/manifest.d.ts.map +1 -1
  50. package/dist/npm.d.ts +14 -2
  51. package/dist/npm.d.ts.map +1 -1
  52. package/dist/paths.d.ts +34 -3
  53. package/dist/paths.d.ts.map +1 -1
  54. package/dist/runtime.d.ts +14 -0
  55. package/dist/runtime.d.ts.map +1 -0
  56. package/dist/symlinks.d.ts +43 -7
  57. package/dist/symlinks.d.ts.map +1 -1
  58. package/package.json +11 -5
  59. package/plugins/exa/README.md +153 -0
  60. package/plugins/exa/package.json +56 -0
  61. package/plugins/exa/tools/exa/company.ts +35 -0
  62. package/plugins/exa/tools/exa/index.ts +66 -0
  63. package/plugins/exa/tools/exa/linkedin.ts +35 -0
  64. package/plugins/exa/tools/exa/researcher.ts +40 -0
  65. package/plugins/exa/tools/exa/runtime.json +4 -0
  66. package/plugins/exa/tools/exa/search.ts +46 -0
  67. package/plugins/exa/tools/exa/shared.ts +230 -0
  68. package/plugins/exa/tools/exa/websets.ts +62 -0
  69. package/plugins/metal-theme/package.json +7 -2
  70. package/plugins/subagents/package.json +7 -2
  71. package/plugins/user-prompt/README.md +130 -0
  72. package/plugins/user-prompt/package.json +19 -0
  73. package/plugins/user-prompt/tools/user-prompt/index.ts +235 -0
  74. package/src/cli.ts +133 -58
  75. package/src/commands/config.ts +384 -0
  76. package/src/commands/create.ts +51 -1
  77. package/src/commands/doctor.ts +95 -7
  78. package/src/commands/enable.ts +25 -8
  79. package/src/commands/env.ts +38 -0
  80. package/src/commands/features.ts +295 -0
  81. package/src/commands/info.ts +41 -5
  82. package/src/commands/init.ts +20 -2
  83. package/src/commands/install.ts +453 -80
  84. package/src/commands/link.ts +60 -9
  85. package/src/commands/list.ts +122 -7
  86. package/src/commands/outdated.ts +17 -6
  87. package/src/commands/search.ts +20 -3
  88. package/src/commands/uninstall.ts +57 -6
  89. package/src/commands/update.ts +67 -9
  90. package/src/commands/why.ts +47 -16
  91. package/src/conflicts.ts +33 -1
  92. package/src/errors.ts +22 -0
  93. package/src/index.ts +18 -25
  94. package/src/lock.ts +46 -0
  95. package/src/lockfile.ts +132 -0
  96. package/src/manifest.ts +219 -71
  97. package/src/npm.ts +74 -18
  98. package/src/paths.ts +77 -12
  99. package/src/runtime.ts +116 -0
  100. package/src/symlinks.ts +291 -35
  101. package/tsconfig.json +7 -3
  102. package/CHECK.md +0 -352
  103. package/dist/migrate.d.ts +0 -9
  104. package/dist/migrate.d.ts.map +0 -1
  105. package/src/migrate.ts +0 -181
@@ -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
- import { loadPluginsJson, type PluginPackageJson, savePluginsJson } from "../manifest.js";
6
- import { NODE_MODULES_DIR, PROJECT_NODE_MODULES } from "../paths.js";
7
- import { createPluginSymlinks } from "../symlinks.js";
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.global !== false;
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.yellow(" Warning: No package.json or omp.json found"));
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
- console.log(chalk.yellow(`Plugin "${pluginName}" is already installed.`));
69
- console.log(chalk.dim("Use omp uninstall first, or specify a different name with -n"));
70
- return;
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 });
@@ -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.global !== false;
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(` v${pkgJson.version}`) : "";
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 icon = disabled ? chalk.gray("") : chalk.green("");
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(` path: ${localPath}`));
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
- console.log(chalk.dim(` files: ${files.join(", ")}`));
178
+ const contributeLines = formatContributes(files);
179
+ for (const line of contributeLines) {
180
+ console.log(line);
181
+ }
67
182
  }
68
183
 
69
184
  console.log();
@@ -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.global !== false;
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
- return pluginsJson.plugins[name] !== undefined;
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
  }
@@ -1,5 +1,10 @@
1
+ import { npmSearch } from "@omp/npm";
1
2
  import chalk from "chalk";
2
- import { npmSearch } from "../npm.js";
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
- console.log(chalk.red(`Error searching: ${(err as Error).message}`));
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.global !== false; // Default to global
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));
@@ -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.global !== false;
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, false);
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
- // Create new symlinks
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
  }