@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.
Files changed (78) 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 +131 -145
  6. package/biome.json +1 -1
  7. package/dist/cli.js +2032 -1136
  8. package/dist/commands/create.d.ts.map +1 -1
  9. package/dist/commands/doctor.d.ts +1 -0
  10. package/dist/commands/doctor.d.ts.map +1 -1
  11. package/dist/commands/enable.d.ts +1 -0
  12. package/dist/commands/enable.d.ts.map +1 -1
  13. package/dist/commands/info.d.ts +1 -0
  14. package/dist/commands/info.d.ts.map +1 -1
  15. package/dist/commands/init.d.ts.map +1 -1
  16. package/dist/commands/install.d.ts +1 -0
  17. package/dist/commands/install.d.ts.map +1 -1
  18. package/dist/commands/link.d.ts +2 -0
  19. package/dist/commands/link.d.ts.map +1 -1
  20. package/dist/commands/list.d.ts +1 -0
  21. package/dist/commands/list.d.ts.map +1 -1
  22. package/dist/commands/outdated.d.ts +1 -0
  23. package/dist/commands/outdated.d.ts.map +1 -1
  24. package/dist/commands/search.d.ts.map +1 -1
  25. package/dist/commands/uninstall.d.ts +1 -0
  26. package/dist/commands/uninstall.d.ts.map +1 -1
  27. package/dist/commands/update.d.ts +1 -0
  28. package/dist/commands/update.d.ts.map +1 -1
  29. package/dist/commands/why.d.ts +1 -0
  30. package/dist/commands/why.d.ts.map +1 -1
  31. package/dist/conflicts.d.ts +9 -1
  32. package/dist/conflicts.d.ts.map +1 -1
  33. package/dist/errors.d.ts +8 -0
  34. package/dist/errors.d.ts.map +1 -0
  35. package/dist/index.d.ts +19 -19
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/lock.d.ts +3 -0
  38. package/dist/lock.d.ts.map +1 -0
  39. package/dist/lockfile.d.ts +52 -0
  40. package/dist/lockfile.d.ts.map +1 -0
  41. package/dist/manifest.d.ts +5 -0
  42. package/dist/manifest.d.ts.map +1 -1
  43. package/dist/migrate.d.ts.map +1 -1
  44. package/dist/npm.d.ts +14 -2
  45. package/dist/npm.d.ts.map +1 -1
  46. package/dist/paths.d.ts +34 -2
  47. package/dist/paths.d.ts.map +1 -1
  48. package/dist/symlinks.d.ts +10 -4
  49. package/dist/symlinks.d.ts.map +1 -1
  50. package/package.json +7 -2
  51. package/plugins/metal-theme/package.json +6 -1
  52. package/plugins/subagents/package.json +6 -1
  53. package/src/cli.ts +69 -43
  54. package/src/commands/create.ts +51 -1
  55. package/src/commands/doctor.ts +95 -7
  56. package/src/commands/enable.ts +25 -8
  57. package/src/commands/info.ts +41 -5
  58. package/src/commands/init.ts +20 -2
  59. package/src/commands/install.ts +266 -52
  60. package/src/commands/link.ts +60 -9
  61. package/src/commands/list.ts +10 -5
  62. package/src/commands/outdated.ts +17 -6
  63. package/src/commands/search.ts +20 -3
  64. package/src/commands/uninstall.ts +57 -6
  65. package/src/commands/update.ts +67 -9
  66. package/src/commands/why.ts +47 -16
  67. package/src/conflicts.ts +33 -1
  68. package/src/errors.ts +22 -0
  69. package/src/index.ts +19 -25
  70. package/src/lock.ts +46 -0
  71. package/src/lockfile.ts +132 -0
  72. package/src/manifest.ts +143 -35
  73. package/src/migrate.ts +14 -3
  74. package/src/npm.ts +74 -18
  75. package/src/paths.ts +77 -9
  76. package/src/symlinks.ts +134 -17
  77. package/tsconfig.json +7 -3
  78. package/CHECK.md +0 -352
package/src/cli.ts CHANGED
@@ -1,22 +1,32 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ import { createPlugin } from "@omp/commands/create";
4
+ import { runDoctor } from "@omp/commands/doctor";
5
+ import { disablePlugin, enablePlugin } from "@omp/commands/enable";
6
+ import { showInfo } from "@omp/commands/info";
7
+ import { initProject } from "@omp/commands/init";
8
+ import { installPlugin } from "@omp/commands/install";
9
+ import { linkPlugin } from "@omp/commands/link";
10
+ import { listPlugins } from "@omp/commands/list";
11
+ import { showOutdated } from "@omp/commands/outdated";
12
+ import { searchPlugins } from "@omp/commands/search";
13
+ import { uninstallPlugin } from "@omp/commands/uninstall";
14
+ import { updatePlugin } from "@omp/commands/update";
15
+ import { whyFile } from "@omp/commands/why";
16
+ import { withErrorHandling } from "@omp/errors";
17
+ import { checkMigration, migrateToNpm } from "@omp/migrate";
18
+ import { checkNpmAvailable } from "@omp/npm";
19
+ import chalk from "chalk";
3
20
  import { program } from "commander";
4
- import { createPlugin } from "./commands/create.js";
5
- import { runDoctor } from "./commands/doctor.js";
6
- import { disablePlugin, enablePlugin } from "./commands/enable.js";
7
- import { showInfo } from "./commands/info.js";
8
- import { initProject } from "./commands/init.js";
9
- import { installPlugin } from "./commands/install.js";
10
- import { linkPlugin } from "./commands/link.js";
11
- import { listPlugins } from "./commands/list.js";
12
- import { showOutdated } from "./commands/outdated.js";
13
- import { searchPlugins } from "./commands/search.js";
14
- import { uninstallPlugin } from "./commands/uninstall.js";
15
- import { updatePlugin } from "./commands/update.js";
16
- import { whyFile } from "./commands/why.js";
17
- import { checkMigration, migrateToNpm } from "./migrate.js";
18
-
19
- program.name("omp").description("Oh My Pi - Plugin manager for pi configuration").version("0.1.0");
21
+
22
+ // Check npm availability at startup
23
+ const npmCheck = checkNpmAvailable();
24
+ if (!npmCheck.available) {
25
+ console.log(chalk.red(npmCheck.error));
26
+ process.exit(1);
27
+ }
28
+
29
+ program.name("omp").description("Oh My Pi - Plugin manager for pi configuration").version("0.2.0");
20
30
 
21
31
  // Check for migration on startup (only for commands that need it)
22
32
  program.hook("preAction", async (thisCommand) => {
@@ -45,36 +55,40 @@ Examples:
45
55
  $ omp install # Install all from plugins.json
46
56
  `,
47
57
  )
48
- .option("-g, --global", "Install globally to ~/.pi (default)")
58
+ .option("-g, --global", "Install globally to ~/.pi")
59
+ .option("-l, --local", "Install to project-local .pi/")
49
60
  .option("-S, --save", "Add to plugins.json")
50
61
  .option("-D, --save-dev", "Add as dev dependency")
51
62
  .option("--force", "Overwrite conflicts without prompting")
52
63
  .option("--json", "Output as JSON")
53
- .action(installPlugin);
64
+ .action(withErrorHandling(installPlugin));
54
65
 
55
66
  program
56
67
  .command("uninstall <name>")
57
68
  .alias("rm")
58
69
  .description("Remove plugin and its symlinks")
59
- .option("-g, --global", "Uninstall from ~/.pi (default)")
70
+ .option("-g, --global", "Uninstall from ~/.pi")
71
+ .option("-l, --local", "Uninstall from project-local .pi/")
60
72
  .option("--json", "Output as JSON")
61
- .action(uninstallPlugin);
73
+ .action(withErrorHandling(uninstallPlugin));
62
74
 
63
75
  program
64
76
  .command("update [name]")
65
77
  .alias("up")
66
78
  .description("Update to latest within semver range")
67
- .option("-g, --global", "Update global plugins (default)")
79
+ .option("-g, --global", "Update global plugins")
80
+ .option("-l, --local", "Update project-local plugins")
68
81
  .option("--json", "Output as JSON")
69
- .action(updatePlugin);
82
+ .action(withErrorHandling(updatePlugin));
70
83
 
71
84
  program
72
85
  .command("list")
73
86
  .alias("ls")
74
87
  .description("Show installed plugins")
75
- .option("-g, --global", "List global plugins (default)")
88
+ .option("-g, --global", "List global plugins")
89
+ .option("-l, --local", "List project-local plugins")
76
90
  .option("--json", "Output as JSON")
77
- .action(listPlugins);
91
+ .action(withErrorHandling(listPlugins));
78
92
 
79
93
  program
80
94
  .command("link <path>")
@@ -87,8 +101,10 @@ so changes are reflected immediately without reinstalling.
87
101
  `,
88
102
  )
89
103
  .option("-n, --name <name>", "Custom name for the plugin")
90
- .option("-g, --global", "Link globally (default)")
91
- .action(linkPlugin);
104
+ .option("-g, --global", "Link globally")
105
+ .option("-l, --local", "Link to project-local .pi/")
106
+ .option("--force", "Overwrite existing npm-installed plugin")
107
+ .action(withErrorHandling(linkPlugin));
92
108
 
93
109
  // ============================================================================
94
110
  // New Commands
@@ -98,70 +114,80 @@ program
98
114
  .command("init")
99
115
  .description("Create .pi/plugins.json in current project")
100
116
  .option("--force", "Overwrite existing plugins.json")
101
- .action(initProject);
117
+ .action(withErrorHandling(initProject));
102
118
 
103
119
  program
104
120
  .command("search <query>")
105
121
  .description("Search npm for omp-plugin keyword")
106
122
  .option("--json", "Output as JSON")
107
123
  .option("--limit <n>", "Maximum results to show", "20")
108
- .action((query, options) => searchPlugins(query, { ...options, limit: parseInt(options.limit, 10) }));
124
+ .action(
125
+ withErrorHandling((query, options) => searchPlugins(query, { ...options, limit: parseInt(options.limit, 10) })),
126
+ );
109
127
 
110
128
  program
111
129
  .command("info <package>")
112
130
  .description("Show plugin details before install")
113
131
  .option("--json", "Output as JSON")
114
132
  .option("--versions", "Show available versions")
115
- .action(showInfo);
133
+ .option("--all-versions", "Show all published versions")
134
+ .action(withErrorHandling(showInfo));
116
135
 
117
136
  program
118
137
  .command("outdated")
119
138
  .description("List plugins with newer versions")
120
- .option("-g, --global", "Check global plugins (default)")
139
+ .option("-g, --global", "Check global plugins")
140
+ .option("-l, --local", "Check project-local plugins")
121
141
  .option("--json", "Output as JSON")
122
- .action(showOutdated);
142
+ .action(withErrorHandling(showOutdated));
123
143
 
124
144
  program
125
145
  .command("doctor")
126
146
  .description("Check for broken symlinks, conflicts")
127
- .option("-g, --global", "Check global plugins (default)")
147
+ .option("-g, --global", "Check global plugins")
148
+ .option("-l, --local", "Check project-local plugins")
128
149
  .option("--fix", "Attempt to fix issues")
129
150
  .option("--json", "Output as JSON")
130
- .action(runDoctor);
151
+ .action(withErrorHandling(runDoctor));
131
152
 
132
153
  program
133
154
  .command("create <name>")
134
155
  .description("Scaffold new plugin from template")
135
156
  .option("-d, --description <desc>", "Plugin description")
136
157
  .option("-a, --author <author>", "Plugin author")
137
- .action(createPlugin);
158
+ .action(withErrorHandling(createPlugin));
138
159
 
139
160
  program
140
161
  .command("why <file>")
141
162
  .description("Show which plugin installed a file")
142
- .option("-g, --global", "Check global plugins (default)")
163
+ .option("-g, --global", "Check global plugins")
164
+ .option("-l, --local", "Check project-local plugins")
143
165
  .option("--json", "Output as JSON")
144
- .action(whyFile);
166
+ .action(withErrorHandling(whyFile));
145
167
 
146
168
  program
147
169
  .command("enable <name>")
148
170
  .description("Enable a disabled plugin")
149
- .option("-g, --global", "Target global plugins (default)")
171
+ .option("-g, --global", "Target global plugins")
172
+ .option("-l, --local", "Target project-local plugins")
150
173
  .option("--json", "Output as JSON")
151
- .action(enablePlugin);
174
+ .action(withErrorHandling(enablePlugin));
152
175
 
153
176
  program
154
177
  .command("disable <name>")
155
178
  .description("Disable plugin without uninstalling")
156
- .option("-g, --global", "Target global plugins (default)")
179
+ .option("-g, --global", "Target global plugins")
180
+ .option("-l, --local", "Target project-local plugins")
157
181
  .option("--json", "Output as JSON")
158
- .action(disablePlugin);
182
+ .action(withErrorHandling(disablePlugin));
159
183
 
160
184
  program
161
185
  .command("migrate")
162
186
  .description("Migrate from legacy manifest.json to npm-native format")
163
- .action(async () => {
164
- await migrateToNpm();
165
- });
187
+ .action(
188
+ withErrorHandling(async () => {
189
+ await migrateToNpm();
190
+ }),
191
+ );
166
192
 
167
193
  program.parse();
@@ -8,16 +8,65 @@ export interface CreateOptions {
8
8
  author?: string;
9
9
  }
10
10
 
11
+ const VALID_NPM_CHARS = new Set("abcdefghijklmnopqrstuvwxyz0123456789-_.");
12
+
13
+ /**
14
+ * Validate that a name conforms to npm naming rules
15
+ */
16
+ function isValidNpmName(name: string): boolean {
17
+ if (!name || name.length === 0) return false;
18
+ if (name.startsWith(".") || name.startsWith("_")) return false;
19
+ if (name.includes(" ")) return false;
20
+ for (const char of name) {
21
+ if (!VALID_NPM_CHARS.has(char)) return false;
22
+ }
23
+ return true;
24
+ }
25
+
26
+ /**
27
+ * Normalize a string to be a valid npm package name
28
+ */
29
+ function normalizePluginName(name: string): string {
30
+ let normalized = name.toLowerCase().split(" ").join("-");
31
+
32
+ // Remove invalid characters (keep alphanumeric, -, _, .)
33
+ normalized = Array.from(normalized)
34
+ .filter((char) => VALID_NPM_CHARS.has(char))
35
+ .join("");
36
+
37
+ // Can't start with . or _ or -
38
+ while (normalized.startsWith(".") || normalized.startsWith("_") || normalized.startsWith("-")) {
39
+ normalized = normalized.slice(1);
40
+ }
41
+
42
+ return normalized;
43
+ }
44
+
11
45
  /**
12
46
  * Scaffold a new plugin from template
13
47
  */
14
48
  export async function createPlugin(name: string, options: CreateOptions = {}): Promise<void> {
15
49
  // Ensure name follows conventions
16
- const pluginName = name.startsWith("omp-") ? name : `omp-${name}`;
50
+ let pluginName = name.startsWith("omp-") ? name : `omp-${name}`;
51
+
52
+ // Validate and normalize the plugin name
53
+ if (!isValidNpmName(pluginName)) {
54
+ const normalized = normalizePluginName(pluginName);
55
+ if (!normalized || normalized === "omp-" || normalized === "omp") {
56
+ console.log(chalk.red(`Error: Invalid plugin name "${name}" cannot be normalized to a valid npm name`));
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+ // Ensure omp- prefix after normalization
61
+ const finalName = normalized.startsWith("omp-") ? normalized : `omp-${normalized}`;
62
+ console.log(chalk.yellow(`Invalid plugin name. Normalized to: ${finalName}`));
63
+ pluginName = finalName;
64
+ }
17
65
  const pluginDir = pluginName;
18
66
 
19
67
  if (existsSync(pluginDir)) {
20
68
  console.log(chalk.red(`Error: Directory ${pluginDir} already exists`));
69
+ process.exitCode = 1;
21
70
  return;
22
71
  }
23
72
 
@@ -149,5 +198,6 @@ Provide instructions for the agent here.
149
198
  console.log(chalk.dim(" 5. Publish: npm publish"));
150
199
  } catch (err) {
151
200
  console.log(chalk.red(`Error creating plugin: ${(err as Error).message}`));
201
+ process.exitCode = 1;
152
202
  }
153
203
  }
@@ -1,12 +1,20 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { detectAllConflicts, formatConflicts } from "@omp/conflicts";
3
+ import { getInstalledPlugins, loadPluginsJson, readPluginPackageJson, savePluginsJson } from "@omp/manifest";
4
+ import {
5
+ GLOBAL_PACKAGE_JSON,
6
+ NODE_MODULES_DIR,
7
+ PLUGINS_DIR,
8
+ PROJECT_NODE_MODULES,
9
+ PROJECT_PLUGINS_JSON,
10
+ resolveScope,
11
+ } from "@omp/paths";
12
+ import { checkPluginSymlinks, createPluginSymlinks } from "@omp/symlinks";
2
13
  import chalk from "chalk";
3
- import { detectAllConflicts, formatConflicts } from "../conflicts.js";
4
- import { getInstalledPlugins, loadPluginsJson, readPluginPackageJson } from "../manifest.js";
5
- import { GLOBAL_PACKAGE_JSON, NODE_MODULES_DIR, PLUGINS_DIR } from "../paths.js";
6
- import { checkPluginSymlinks } from "../symlinks.js";
7
14
 
8
15
  export interface DoctorOptions {
9
16
  global?: boolean;
17
+ local?: boolean;
10
18
  fix?: boolean;
11
19
  json?: boolean;
12
20
  }
@@ -22,7 +30,7 @@ interface DiagnosticResult {
22
30
  * Run health checks on the plugin system
23
31
  */
24
32
  export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
25
- const isGlobal = options.global !== false;
33
+ const isGlobal = resolveScope(options);
26
34
  const results: DiagnosticResult[] = [];
27
35
 
28
36
  console.log(chalk.blue("Running health checks...\n"));
@@ -45,7 +53,7 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
45
53
  }
46
54
 
47
55
  // 2. Check package.json exists
48
- const packageJsonPath = isGlobal ? GLOBAL_PACKAGE_JSON : ".pi/plugins.json";
56
+ const packageJsonPath = isGlobal ? GLOBAL_PACKAGE_JSON : PROJECT_PLUGINS_JSON;
49
57
  if (!existsSync(packageJsonPath)) {
50
58
  results.push({
51
59
  check: "Package manifest",
@@ -62,7 +70,7 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
62
70
  }
63
71
 
64
72
  // 3. Check node_modules exists
65
- const nodeModules = isGlobal ? NODE_MODULES_DIR : ".pi/node_modules";
73
+ const nodeModules = isGlobal ? NODE_MODULES_DIR : PROJECT_NODE_MODULES;
66
74
  if (!existsSync(nodeModules)) {
67
75
  results.push({
68
76
  check: "Node modules",
@@ -152,6 +160,38 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
152
160
  });
153
161
  }
154
162
 
163
+ // 7. Check for missing omp dependencies
164
+ const missingDeps: string[] = [];
165
+ for (const [name, pkgJson] of installedPlugins) {
166
+ if (pkgJson.dependencies) {
167
+ for (const depName of Object.keys(pkgJson.dependencies)) {
168
+ const depPkgJson = await readPluginPackageJson(depName, isGlobal);
169
+ if (!depPkgJson) {
170
+ // Dependency not found in node_modules
171
+ // Check if it's supposed to be an omp plugin by looking in the plugins manifest
172
+ if (pluginsJson.plugins[depName]) {
173
+ missingDeps.push(`${name} requires ${depName} (not in node_modules)`);
174
+ }
175
+ } else if (depPkgJson.omp?.install && depPkgJson.omp.install.length > 0) {
176
+ // Dependency is an omp plugin (has install entries) and is present - that's fine
177
+ // But check if it's registered in the plugins manifest
178
+ if (!pluginsJson.plugins[depName]) {
179
+ missingDeps.push(`${name} requires omp plugin ${depName} (installed but not in manifest)`);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ if (missingDeps.length > 0) {
187
+ results.push({
188
+ check: "Missing omp dependencies",
189
+ status: "warning",
190
+ message: missingDeps.join("; "),
191
+ fix: isGlobal ? "Run: npm install in ~/.pi/plugins" : "Run: npm install in .pi",
192
+ });
193
+ }
194
+
155
195
  // Output results
156
196
  if (options.json) {
157
197
  console.log(JSON.stringify({ results }, null, 2));
@@ -194,6 +234,7 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
194
234
  } else {
195
235
  if (errors.length > 0) {
196
236
  console.log(chalk.red(`${errors.length} error(s) found`));
237
+ process.exitCode = 1;
197
238
  }
198
239
  if (warnings.length > 0) {
199
240
  console.log(chalk.yellow(`${warnings.length} warning(s) found`));
@@ -214,4 +255,51 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
214
255
  console.log(chalk.dim(` - ${s}`));
215
256
  }
216
257
  }
258
+
259
+ // Apply fixes if --fix flag was passed
260
+ if (options.fix) {
261
+ let fixedAnything = false;
262
+
263
+ // Fix broken/missing symlinks by re-creating them
264
+ if (brokenSymlinks.length > 0 || missingSymlinks.length > 0) {
265
+ console.log(chalk.blue("\nAttempting to fix broken/missing symlinks..."));
266
+ for (const [name, pkgJson] of installedPlugins) {
267
+ const symlinkResult = await createPluginSymlinks(name, pkgJson, isGlobal, false);
268
+ if (symlinkResult.created.length > 0) {
269
+ fixedAnything = true;
270
+ console.log(chalk.green(` ✓ Re-created symlinks for ${name}`));
271
+ }
272
+ if (symlinkResult.errors.length > 0) {
273
+ for (const err of symlinkResult.errors) {
274
+ console.log(chalk.red(` ✗ ${name}: ${err}`));
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ // Remove orphaned manifest entries
281
+ if (orphaned.length > 0) {
282
+ console.log(chalk.blue("\nRemoving orphaned entries from manifest..."));
283
+ for (const name of orphaned) {
284
+ delete pluginsJson.plugins[name];
285
+ console.log(chalk.green(` ✓ Removed ${name}`));
286
+ }
287
+ await savePluginsJson(pluginsJson, isGlobal);
288
+ fixedAnything = true;
289
+ }
290
+
291
+ // Conflicts cannot be auto-fixed
292
+ if (conflicts.length > 0) {
293
+ console.log(chalk.yellow("\nConflicts cannot be auto-fixed. Please resolve manually:"));
294
+ for (const conflict of formatConflicts(conflicts)) {
295
+ console.log(chalk.dim(` - ${conflict}`));
296
+ }
297
+ }
298
+
299
+ if (fixedAnything) {
300
+ console.log(chalk.green("\n✓ Fixes applied. Run 'omp doctor' again to verify."));
301
+ } else if (conflicts.length === 0) {
302
+ console.log(chalk.dim("\nNo fixable issues found."));
303
+ }
304
+ }
217
305
  }
@@ -1,9 +1,11 @@
1
+ import { loadPluginsJson, readPluginPackageJson, savePluginsJson } from "@omp/manifest";
2
+ import { resolveScope } from "@omp/paths";
3
+ import { checkPluginSymlinks, createPluginSymlinks, removePluginSymlinks } from "@omp/symlinks";
1
4
  import chalk from "chalk";
2
- import { loadPluginsJson, readPluginPackageJson, savePluginsJson } from "../manifest.js";
3
- import { createPluginSymlinks, removePluginSymlinks } from "../symlinks.js";
4
5
 
5
6
  export interface EnableDisableOptions {
6
7
  global?: boolean;
8
+ local?: boolean;
7
9
  json?: boolean;
8
10
  }
9
11
 
@@ -11,19 +13,21 @@ export interface EnableDisableOptions {
11
13
  * Enable a disabled plugin (re-create symlinks)
12
14
  */
13
15
  export async function enablePlugin(name: string, options: EnableDisableOptions = {}): Promise<void> {
14
- const isGlobal = options.global !== false;
16
+ const isGlobal = resolveScope(options);
15
17
 
16
18
  const pluginsJson = await loadPluginsJson(isGlobal);
17
19
 
18
20
  // Check if plugin exists
19
21
  if (!pluginsJson.plugins[name]) {
20
22
  console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
23
+ process.exitCode = 1;
21
24
  return;
22
25
  }
23
26
 
24
27
  // Check if already enabled
25
28
  if (!pluginsJson.disabled?.includes(name)) {
26
29
  console.log(chalk.yellow(`Plugin "${name}" is already enabled.`));
30
+ process.exitCode = 1;
27
31
  return;
28
32
  }
29
33
 
@@ -32,12 +36,20 @@ export async function enablePlugin(name: string, options: EnableDisableOptions =
32
36
  const pkgJson = await readPluginPackageJson(name, isGlobal);
33
37
  if (!pkgJson) {
34
38
  console.log(chalk.red(`Could not read package.json for ${name}`));
39
+ process.exitCode = 1;
35
40
  return;
36
41
  }
37
42
 
38
- // Re-create symlinks
39
- console.log(chalk.blue(`Enabling ${name}...`));
40
- await createPluginSymlinks(name, pkgJson, isGlobal);
43
+ // Check if symlinks are already in place
44
+ const symlinkStatus = await checkPluginSymlinks(name, pkgJson, isGlobal);
45
+
46
+ if (symlinkStatus.valid.length > 0 && symlinkStatus.broken.length === 0 && symlinkStatus.missing.length === 0) {
47
+ console.log(chalk.yellow(`Plugin "${name}" symlinks are already in place.`));
48
+ } else {
49
+ // Re-create symlinks
50
+ console.log(chalk.blue(`Enabling ${name}...`));
51
+ await createPluginSymlinks(name, pkgJson, isGlobal);
52
+ }
41
53
 
42
54
  // Remove from disabled list
43
55
  pluginsJson.disabled = pluginsJson.disabled.filter((n) => n !== name);
@@ -50,6 +62,7 @@ export async function enablePlugin(name: string, options: EnableDisableOptions =
50
62
  }
51
63
  } catch (err) {
52
64
  console.log(chalk.red(`Error enabling plugin: ${(err as Error).message}`));
65
+ process.exitCode = 1;
53
66
  }
54
67
  }
55
68
 
@@ -57,19 +70,21 @@ export async function enablePlugin(name: string, options: EnableDisableOptions =
57
70
  * Disable a plugin (remove symlinks but keep installed)
58
71
  */
59
72
  export async function disablePlugin(name: string, options: EnableDisableOptions = {}): Promise<void> {
60
- const isGlobal = options.global !== false;
73
+ const isGlobal = resolveScope(options);
61
74
 
62
75
  const pluginsJson = await loadPluginsJson(isGlobal);
63
76
 
64
77
  // Check if plugin exists
65
78
  if (!pluginsJson.plugins[name]) {
66
79
  console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
80
+ process.exitCode = 1;
67
81
  return;
68
82
  }
69
83
 
70
84
  // Check if already disabled
71
85
  if (pluginsJson.disabled?.includes(name)) {
72
86
  console.log(chalk.yellow(`Plugin "${name}" is already disabled.`));
87
+ process.exitCode = 1;
73
88
  return;
74
89
  }
75
90
 
@@ -78,12 +93,13 @@ export async function disablePlugin(name: string, options: EnableDisableOptions
78
93
  const pkgJson = await readPluginPackageJson(name, isGlobal);
79
94
  if (!pkgJson) {
80
95
  console.log(chalk.red(`Could not read package.json for ${name}`));
96
+ process.exitCode = 1;
81
97
  return;
82
98
  }
83
99
 
84
100
  // Remove symlinks
85
101
  console.log(chalk.blue(`Disabling ${name}...`));
86
- await removePluginSymlinks(name, pkgJson);
102
+ await removePluginSymlinks(name, pkgJson, isGlobal);
87
103
 
88
104
  // Add to disabled list
89
105
  if (!pluginsJson.disabled) {
@@ -101,5 +117,6 @@ export async function disablePlugin(name: string, options: EnableDisableOptions
101
117
  }
102
118
  } catch (err) {
103
119
  console.log(chalk.red(`Error disabling plugin: ${(err as Error).message}`));
120
+ process.exitCode = 1;
104
121
  }
105
122
  }
@@ -1,9 +1,10 @@
1
+ import { npmInfo } from "@omp/npm";
1
2
  import chalk from "chalk";
2
- import { npmInfo } from "../npm.js";
3
3
 
4
4
  export interface InfoOptions {
5
5
  json?: boolean;
6
6
  versions?: boolean;
7
+ allVersions?: boolean;
7
8
  }
8
9
 
9
10
  /**
@@ -17,6 +18,7 @@ export async function showInfo(packageName: string, options: InfoOptions = {}):
17
18
 
18
19
  if (!info) {
19
20
  console.log(chalk.red(`Package not found: ${packageName}`));
21
+ process.exitCode = 1;
20
22
  return;
21
23
  }
22
24
 
@@ -59,6 +61,14 @@ export async function showInfo(packageName: string, options: InfoOptions = {}):
59
61
  console.log(chalk.dim("keywords: ") + info.keywords.join(", "));
60
62
  }
61
63
 
64
+ // Dependencies
65
+ if (info.dependencies && Object.keys(info.dependencies).length > 0) {
66
+ console.log(chalk.dim("\ndependencies:"));
67
+ for (const [depName, depVersion] of Object.entries(info.dependencies)) {
68
+ console.log(chalk.dim(` ${depName}: ${depVersion}`));
69
+ }
70
+ }
71
+
62
72
  // Is it an omp plugin?
63
73
  const isOmpPlugin = info.keywords?.includes("omp-plugin");
64
74
  if (isOmpPlugin) {
@@ -68,11 +78,36 @@ export async function showInfo(packageName: string, options: InfoOptions = {}):
68
78
  console.log(chalk.dim(" It may work, but might not have omp.install configuration"));
69
79
  }
70
80
 
81
+ // Show what files will be installed
82
+ if (info.omp?.install?.length) {
83
+ console.log(chalk.dim("\nFiles to install:"));
84
+ for (const entry of info.omp.install) {
85
+ console.log(chalk.dim(` ${entry.src} → ${entry.dest}`));
86
+ }
87
+ }
88
+
71
89
  // Versions
72
- if (options.versions && info["dist-tags"]) {
73
- console.log(chalk.dim("\ndist-tags:"));
74
- for (const [tag, version] of Object.entries(info["dist-tags"])) {
75
- console.log(chalk.dim(` ${tag}: `) + version);
90
+ if (options.versions || options.allVersions) {
91
+ if (info["dist-tags"]) {
92
+ console.log(chalk.dim("\ndist-tags:"));
93
+ for (const [tag, version] of Object.entries(info["dist-tags"])) {
94
+ console.log(chalk.dim(` ${tag}: `) + version);
95
+ }
96
+ }
97
+
98
+ if (info.versions?.length) {
99
+ console.log(chalk.dim("\nall versions:"));
100
+ if (options.allVersions) {
101
+ // Show all versions
102
+ console.log(chalk.dim(` ${info.versions.join(", ")}`));
103
+ } else {
104
+ // Show last 10
105
+ const versionsToShow = info.versions.slice(-10);
106
+ console.log(chalk.dim(` ${versionsToShow.join(", ")}`));
107
+ if (info.versions.length > 10) {
108
+ console.log(chalk.dim(` ... and ${info.versions.length - 10} more (use --all-versions to see all)`));
109
+ }
110
+ }
76
111
  }
77
112
  }
78
113
 
@@ -80,5 +115,6 @@ export async function showInfo(packageName: string, options: InfoOptions = {}):
80
115
  console.log(chalk.dim(`Install with: omp install ${packageName}`));
81
116
  } catch (err) {
82
117
  console.log(chalk.red(`Error fetching info: ${(err as Error).message}`));
118
+ process.exitCode = 1;
83
119
  }
84
120
  }
@@ -1,7 +1,17 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
+ import { PROJECT_PI_DIR, PROJECT_PLUGINS_JSON } from "@omp/paths";
3
4
  import chalk from "chalk";
4
- import { PROJECT_PI_DIR, PROJECT_PLUGINS_JSON } from "../paths.js";
5
+
6
+ /**
7
+ * Format permission-related errors with actionable guidance
8
+ */
9
+ function formatPermissionError(err: NodeJS.ErrnoException, path: string): string {
10
+ if (err.code === "EACCES" || err.code === "EPERM") {
11
+ return `Permission denied: Cannot write to ${path}. Check directory permissions or run with appropriate privileges.`;
12
+ }
13
+ return err.message;
14
+ }
5
15
 
6
16
  export interface InitOptions {
7
17
  force?: boolean;
@@ -15,6 +25,7 @@ export async function initProject(options: InitOptions = {}): Promise<void> {
15
25
  if (existsSync(PROJECT_PLUGINS_JSON) && !options.force) {
16
26
  console.log(chalk.yellow(`${PROJECT_PLUGINS_JSON} already exists.`));
17
27
  console.log(chalk.dim("Use --force to overwrite"));
28
+ process.exitCode = 1;
18
29
  return;
19
30
  }
20
31
 
@@ -37,6 +48,13 @@ export async function initProject(options: InitOptions = {}): Promise<void> {
37
48
  console.log(chalk.dim(" 2. Or edit plugins.json directly"));
38
49
  console.log(chalk.dim(" 3. Run: omp install (to install all)"));
39
50
  } catch (err) {
40
- console.log(chalk.red(`Error initializing project: ${(err as Error).message}`));
51
+ const error = err as NodeJS.ErrnoException;
52
+ if (error.code === "EACCES" || error.code === "EPERM") {
53
+ console.log(chalk.red(formatPermissionError(error, PROJECT_PI_DIR)));
54
+ console.log(chalk.dim(" Check directory permissions or run with appropriate privileges."));
55
+ } else {
56
+ console.log(chalk.red(`Error initializing project: ${error.message}`));
57
+ }
58
+ process.exitCode = 1;
41
59
  }
42
60
  }