@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
@@ -0,0 +1,384 @@
1
+ import type { OmpVariable } from "@omp/manifest";
2
+ import { loadPluginsJson, readPluginPackageJson, savePluginsJson } from "@omp/manifest";
3
+ import { resolveScope } from "@omp/paths";
4
+ import chalk from "chalk";
5
+
6
+ export interface ConfigOptions {
7
+ global?: boolean;
8
+ local?: boolean;
9
+ json?: boolean;
10
+ delete?: boolean;
11
+ }
12
+
13
+ /**
14
+ * Collect all variables from a plugin (top-level + enabled features)
15
+ */
16
+ function collectVariables(
17
+ pkgJson: { omp?: { variables?: Record<string, OmpVariable>; features?: Record<string, { variables?: Record<string, OmpVariable> }> } },
18
+ enabledFeatures: string[],
19
+ ): Record<string, OmpVariable> {
20
+ const vars: Record<string, OmpVariable> = {};
21
+
22
+ // Top-level variables
23
+ if (pkgJson.omp?.variables) {
24
+ Object.assign(vars, pkgJson.omp.variables);
25
+ }
26
+
27
+ // Variables from enabled features
28
+ if (pkgJson.omp?.features) {
29
+ for (const fname of enabledFeatures) {
30
+ const feature = pkgJson.omp.features[fname];
31
+ if (feature?.variables) {
32
+ Object.assign(vars, feature.variables);
33
+ }
34
+ }
35
+ }
36
+
37
+ return vars;
38
+ }
39
+
40
+ /**
41
+ * Parse a string value to the appropriate type based on variable definition
42
+ */
43
+ function parseValue(value: string, varDef: OmpVariable): string | number | boolean | string[] {
44
+ switch (varDef.type) {
45
+ case "number":
46
+ const num = Number(value);
47
+ if (isNaN(num)) {
48
+ throw new Error(`Invalid number: ${value}`);
49
+ }
50
+ return num;
51
+ case "boolean":
52
+ if (value === "true" || value === "1" || value === "yes") return true;
53
+ if (value === "false" || value === "0" || value === "no") return false;
54
+ throw new Error(`Invalid boolean: ${value}. Use true/false, 1/0, or yes/no`);
55
+ case "string[]":
56
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
57
+ default:
58
+ return value;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Format a value for display
64
+ */
65
+ function formatValue(value: unknown, varDef: OmpVariable): string {
66
+ if (value === undefined) {
67
+ return chalk.dim("(not set)");
68
+ }
69
+ if (varDef.type === "string[]" && Array.isArray(value)) {
70
+ return value.join(", ");
71
+ }
72
+ if (typeof value === "string" && varDef.env) {
73
+ // Mask sensitive values (likely API keys)
74
+ if (value.length > 8) {
75
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
76
+ }
77
+ }
78
+ return String(value);
79
+ }
80
+
81
+ /**
82
+ * Resolve which features are currently enabled
83
+ *
84
+ * - null/undefined: use plugin defaults (features with default !== false)
85
+ * - ["*"]: explicitly all features
86
+ * - []: no optional features
87
+ * - ["f1", "f2"]: specific features
88
+ */
89
+ function resolveEnabledFeatures(
90
+ allFeatureNames: string[],
91
+ storedFeatures: string[] | null | undefined,
92
+ pluginFeatures: Record<string, { default?: boolean }>,
93
+ ): string[] {
94
+ // Explicit "all features" request
95
+ if (Array.isArray(storedFeatures) && storedFeatures.includes("*")) return allFeatureNames;
96
+ // Explicit feature list (including empty array = no features)
97
+ if (Array.isArray(storedFeatures)) return storedFeatures;
98
+ // null/undefined = use defaults
99
+ return Object.entries(pluginFeatures)
100
+ .filter(([_, f]) => f.default !== false)
101
+ .map(([name]) => name);
102
+ }
103
+
104
+ /**
105
+ * List all configurable variables for a plugin
106
+ * omp config @oh-my-pi/exa
107
+ */
108
+ export async function listConfig(name: string, options: ConfigOptions = {}): Promise<void> {
109
+ const isGlobal = resolveScope(options);
110
+ const pluginsJson = await loadPluginsJson(isGlobal);
111
+
112
+ if (!pluginsJson.plugins[name]) {
113
+ console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+
118
+ const pkgJson = await readPluginPackageJson(name, isGlobal);
119
+ if (!pkgJson) {
120
+ console.log(chalk.red(`Could not read package.json for ${name}`));
121
+ process.exitCode = 1;
122
+ return;
123
+ }
124
+
125
+ const allFeatureNames = Object.keys(pkgJson.omp?.features || {});
126
+ const config = pluginsJson.config?.[name];
127
+ const enabledFeatures = resolveEnabledFeatures(allFeatureNames, config?.features, pkgJson.omp?.features || {});
128
+ const variables = collectVariables(pkgJson, enabledFeatures);
129
+
130
+ if (Object.keys(variables).length === 0) {
131
+ console.log(chalk.yellow(`Plugin "${name}" has no configurable variables.`));
132
+ return;
133
+ }
134
+
135
+ const userVars = config?.variables || {};
136
+
137
+ if (options.json) {
138
+ console.log(
139
+ JSON.stringify(
140
+ {
141
+ plugin: name,
142
+ variables: Object.entries(variables).map(([vname, vdef]) => ({
143
+ name: vname,
144
+ type: vdef.type,
145
+ value: userVars[vname],
146
+ default: vdef.default,
147
+ required: vdef.required,
148
+ env: vdef.env,
149
+ description: vdef.description,
150
+ })),
151
+ },
152
+ null,
153
+ 2,
154
+ ),
155
+ );
156
+ return;
157
+ }
158
+
159
+ console.log(chalk.bold(`\nVariables for ${name}:\n`));
160
+
161
+ for (const [vname, vdef] of Object.entries(variables)) {
162
+ const currentValue = userVars[vname];
163
+ const hasValue = currentValue !== undefined;
164
+ const hasDefault = vdef.default !== undefined;
165
+
166
+ const icon = hasValue ? chalk.green("✓") : hasDefault ? chalk.blue("○") : vdef.required ? chalk.red("!") : chalk.gray("○");
167
+ const requiredStr = vdef.required && !hasValue ? chalk.red(" (required)") : "";
168
+ const envStr = vdef.env ? chalk.dim(` [${vdef.env}]`) : "";
169
+
170
+ console.log(`${icon} ${chalk.bold(vname)}${requiredStr}${envStr}`);
171
+
172
+ if (vdef.description) {
173
+ console.log(chalk.dim(` ${vdef.description}`));
174
+ }
175
+
176
+ console.log(chalk.dim(` Type: ${vdef.type}`));
177
+
178
+ if (hasValue) {
179
+ console.log(` Value: ${formatValue(currentValue, vdef)}`);
180
+ } else if (hasDefault) {
181
+ console.log(` Default: ${formatValue(vdef.default, vdef)}`);
182
+ }
183
+ }
184
+
185
+ console.log();
186
+ console.log(chalk.dim(`Set a value: omp config ${name} <variable> <value>`));
187
+ console.log(chalk.dim(`Delete a value: omp config ${name} <variable> --delete`));
188
+ }
189
+
190
+ /**
191
+ * Get a specific variable value
192
+ * omp config @oh-my-pi/exa apiKey
193
+ */
194
+ export async function getConfig(name: string, key: string, options: ConfigOptions = {}): Promise<void> {
195
+ const isGlobal = resolveScope(options);
196
+ const pluginsJson = await loadPluginsJson(isGlobal);
197
+
198
+ if (!pluginsJson.plugins[name]) {
199
+ console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
200
+ process.exitCode = 1;
201
+ return;
202
+ }
203
+
204
+ const pkgJson = await readPluginPackageJson(name, isGlobal);
205
+ if (!pkgJson) {
206
+ console.log(chalk.red(`Could not read package.json for ${name}`));
207
+ process.exitCode = 1;
208
+ return;
209
+ }
210
+
211
+ const allFeatureNames = Object.keys(pkgJson.omp?.features || {});
212
+ const config = pluginsJson.config?.[name];
213
+ const enabledFeatures = resolveEnabledFeatures(allFeatureNames, config?.features, pkgJson.omp?.features || {});
214
+ const variables = collectVariables(pkgJson, enabledFeatures);
215
+
216
+ const varDef = variables[key];
217
+ if (!varDef) {
218
+ console.log(chalk.red(`Unknown variable "${key}".`));
219
+ console.log(chalk.dim(`Available: ${Object.keys(variables).join(", ") || "(none)"}`));
220
+ process.exitCode = 1;
221
+ return;
222
+ }
223
+
224
+ const userValue = config?.variables?.[key];
225
+ const value = userValue ?? varDef.default;
226
+
227
+ if (options.json) {
228
+ console.log(JSON.stringify({ plugin: name, variable: key, value, default: varDef.default }, null, 2));
229
+ return;
230
+ }
231
+
232
+ if (value !== undefined) {
233
+ console.log(formatValue(value, varDef));
234
+ } else {
235
+ console.log(chalk.dim("(not set)"));
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Set a variable value
241
+ * omp config @oh-my-pi/exa apiKey sk-xxx
242
+ */
243
+ export async function setConfig(name: string, key: string, value: string, options: ConfigOptions = {}): Promise<void> {
244
+ const isGlobal = resolveScope(options);
245
+ const pluginsJson = await loadPluginsJson(isGlobal);
246
+
247
+ if (!pluginsJson.plugins[name]) {
248
+ console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
249
+ process.exitCode = 1;
250
+ return;
251
+ }
252
+
253
+ const pkgJson = await readPluginPackageJson(name, isGlobal);
254
+ if (!pkgJson) {
255
+ console.log(chalk.red(`Could not read package.json for ${name}`));
256
+ process.exitCode = 1;
257
+ return;
258
+ }
259
+
260
+ const allFeatureNames = Object.keys(pkgJson.omp?.features || {});
261
+ const config = pluginsJson.config?.[name];
262
+ const enabledFeatures = resolveEnabledFeatures(allFeatureNames, config?.features, pkgJson.omp?.features || {});
263
+ const variables = collectVariables(pkgJson, enabledFeatures);
264
+
265
+ const varDef = variables[key];
266
+ if (!varDef) {
267
+ console.log(chalk.red(`Unknown variable "${key}".`));
268
+ console.log(chalk.dim(`Available: ${Object.keys(variables).join(", ") || "(none)"}`));
269
+ process.exitCode = 1;
270
+ return;
271
+ }
272
+
273
+ // Parse and validate value
274
+ let parsed: string | number | boolean | string[];
275
+ try {
276
+ parsed = parseValue(value, varDef);
277
+ } catch (err) {
278
+ console.log(chalk.red((err as Error).message));
279
+ process.exitCode = 1;
280
+ return;
281
+ }
282
+
283
+ // Update config
284
+ if (!pluginsJson.config) pluginsJson.config = {};
285
+ if (!pluginsJson.config[name]) pluginsJson.config[name] = {};
286
+ if (!pluginsJson.config[name].variables) pluginsJson.config[name].variables = {};
287
+
288
+ pluginsJson.config[name].variables[key] = parsed;
289
+ await savePluginsJson(pluginsJson, isGlobal);
290
+
291
+ console.log(chalk.green(`✓ Set ${name}.${key} = ${JSON.stringify(parsed)}`));
292
+
293
+ if (varDef.env) {
294
+ console.log(chalk.dim(` Environment variable: ${varDef.env}`));
295
+ console.log(chalk.dim(` Export with: omp env`));
296
+ }
297
+
298
+ if (options.json) {
299
+ console.log(JSON.stringify({ plugin: name, variable: key, value: parsed }, null, 2));
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Delete a variable override (revert to default)
305
+ * omp config @oh-my-pi/exa apiKey --delete
306
+ */
307
+ export async function deleteConfig(name: string, key: string, options: ConfigOptions = {}): Promise<void> {
308
+ const isGlobal = resolveScope(options);
309
+ const pluginsJson = await loadPluginsJson(isGlobal);
310
+
311
+ if (!pluginsJson.plugins[name]) {
312
+ console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
313
+ process.exitCode = 1;
314
+ return;
315
+ }
316
+
317
+ const config = pluginsJson.config?.[name];
318
+ // Check key presence with hasOwnProperty, not truthiness (allows deleting falsy values like false, 0, "", [])
319
+ if (!config?.variables || !Object.prototype.hasOwnProperty.call(config.variables, key)) {
320
+ console.log(chalk.yellow(`Variable "${key}" is not set for ${name}.`));
321
+ return;
322
+ }
323
+
324
+ delete pluginsJson.config![name].variables![key];
325
+
326
+ // Clean up empty objects
327
+ if (Object.keys(pluginsJson.config![name].variables!).length === 0) {
328
+ delete pluginsJson.config![name].variables;
329
+ }
330
+ if (Object.keys(pluginsJson.config![name]).length === 0) {
331
+ delete pluginsJson.config![name];
332
+ }
333
+ if (Object.keys(pluginsJson.config!).length === 0) {
334
+ delete pluginsJson.config;
335
+ }
336
+
337
+ await savePluginsJson(pluginsJson, isGlobal);
338
+
339
+ console.log(chalk.green(`✓ Deleted ${name}.${key} (reverted to default)`));
340
+
341
+ if (options.json) {
342
+ console.log(JSON.stringify({ plugin: name, variable: key, deleted: true }, null, 2));
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Main config command handler
348
+ * Routes to list, get, set, or delete based on arguments
349
+ */
350
+ export async function configCommand(
351
+ name: string,
352
+ keyOrOptions?: string | ConfigOptions,
353
+ valueOrOptions?: string | ConfigOptions,
354
+ options: ConfigOptions = {},
355
+ ): Promise<void> {
356
+ // Handle different argument patterns
357
+ let key: string | undefined;
358
+ let value: string | undefined;
359
+ let opts: ConfigOptions;
360
+
361
+ if (typeof keyOrOptions === "object") {
362
+ // omp config <name> [options]
363
+ opts = keyOrOptions;
364
+ } else if (typeof valueOrOptions === "object") {
365
+ // omp config <name> <key> [options]
366
+ key = keyOrOptions;
367
+ opts = valueOrOptions;
368
+ } else {
369
+ // omp config <name> <key> <value> [options]
370
+ key = keyOrOptions;
371
+ value = valueOrOptions;
372
+ opts = options;
373
+ }
374
+
375
+ if (!key) {
376
+ await listConfig(name, opts);
377
+ } else if (opts.delete) {
378
+ await deleteConfig(name, key, opts);
379
+ } else if (value !== undefined) {
380
+ await setConfig(name, key, value, opts);
381
+ } else {
382
+ await getConfig(name, key, opts);
383
+ }
384
+ }
@@ -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
  }