@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
@@ -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
  }
@@ -1,13 +1,14 @@
1
1
  import { existsSync, lstatSync } from "node:fs";
2
2
  import { readlink } from "node:fs/promises";
3
- import { join, relative } from "node:path";
3
+ import { join, relative, resolve } from "node:path";
4
+ import { getInstalledPlugins, getPluginSourceDir, readPluginPackageJson } from "@omp/manifest";
5
+ import { PI_CONFIG_DIR, PROJECT_PI_DIR, resolveScope } from "@omp/paths";
6
+ import { traceInstalledFile } from "@omp/symlinks";
4
7
  import chalk from "chalk";
5
- import { getInstalledPlugins, readPluginPackageJson } from "../manifest.js";
6
- import { PI_CONFIG_DIR } from "../paths.js";
7
- import { traceInstalledFile } from "../symlinks.js";
8
8
 
9
9
  export interface WhyOptions {
10
10
  global?: boolean;
11
+ local?: boolean;
11
12
  json?: boolean;
12
13
  }
13
14
 
@@ -15,31 +16,45 @@ export interface WhyOptions {
15
16
  * Show which plugin installed a file
16
17
  */
17
18
  export async function whyFile(filePath: string, options: WhyOptions = {}): Promise<void> {
18
- const isGlobal = options.global !== false;
19
+ const isGlobal = resolveScope(options);
19
20
 
20
- // Normalize path - make it relative to PI_CONFIG_DIR if it's absolute
21
+ // Determine the base directory based on scope
22
+ const baseDir = isGlobal ? PI_CONFIG_DIR : resolve(PROJECT_PI_DIR);
23
+
24
+ // Normalize path - make it relative to the appropriate base directory
21
25
  let relativePath = filePath;
22
- if (filePath.startsWith(PI_CONFIG_DIR)) {
23
- relativePath = relative(PI_CONFIG_DIR, filePath);
24
- } else if (filePath.startsWith("~/.pi/")) {
25
- relativePath = filePath.slice(6); // Remove ~/.pi/
26
+ if (isGlobal) {
27
+ if (filePath.startsWith(PI_CONFIG_DIR)) {
28
+ relativePath = relative(PI_CONFIG_DIR, filePath);
29
+ } else if (filePath.startsWith("~/.pi/")) {
30
+ relativePath = filePath.slice(6); // Remove ~/.pi/
31
+ }
32
+ } else {
33
+ // Project-local mode
34
+ const resolvedProjectDir = resolve(PROJECT_PI_DIR);
35
+ if (filePath.startsWith(resolvedProjectDir)) {
36
+ relativePath = relative(resolvedProjectDir, filePath);
37
+ } else if (filePath.startsWith(".pi/")) {
38
+ relativePath = filePath.slice(4); // Remove .pi/
39
+ }
26
40
  }
27
41
 
28
42
  // Check if it's a path in agent/ directory
29
43
  if (!relativePath.startsWith("agent/")) {
30
44
  // Try prepending agent/
31
45
  const withAgent = `agent/${relativePath}`;
32
- const fullWithAgent = join(PI_CONFIG_DIR, withAgent);
46
+ const fullWithAgent = join(baseDir, withAgent);
33
47
  if (existsSync(fullWithAgent)) {
34
48
  relativePath = withAgent;
35
49
  }
36
50
  }
37
51
 
38
- const fullPath = join(PI_CONFIG_DIR, relativePath);
52
+ const fullPath = join(baseDir, relativePath);
39
53
 
40
54
  // Check if file exists
41
55
  if (!existsSync(fullPath)) {
42
56
  console.log(chalk.yellow(`File not found: ${fullPath}`));
57
+ process.exitCode = 1;
43
58
  return;
44
59
  }
45
60
 
@@ -54,7 +69,7 @@ export async function whyFile(filePath: string, options: WhyOptions = {}): Promi
54
69
 
55
70
  // Search through installed plugins
56
71
  const installedPlugins = await getInstalledPlugins(isGlobal);
57
- const result = await traceInstalledFile(relativePath, installedPlugins);
72
+ const result = await traceInstalledFile(relativePath, installedPlugins, isGlobal);
58
73
 
59
74
  if (options.json) {
60
75
  console.log(
@@ -85,9 +100,25 @@ export async function whyFile(filePath: string, options: WhyOptions = {}): Promi
85
100
  }
86
101
 
87
102
  if (result) {
88
- console.log(chalk.green(`✓ Installed by: ${result.plugin}`));
89
- console.log(chalk.dim(` Source: ${result.entry.src}`));
90
- console.log(chalk.dim(` Dest: ${result.entry.dest}`));
103
+ // Verify it's actually a symlink pointing to the right place
104
+ if (!isSymlink) {
105
+ console.log(chalk.yellow("⚠ This file exists but is not a symlink"));
106
+ console.log(chalk.dim(" It may have been manually created or the symlink was replaced."));
107
+ console.log(chalk.dim(` Expected to be installed by: ${result.plugin}`));
108
+ } else {
109
+ // Verify symlink points to correct source
110
+ const expectedSrc = join(getPluginSourceDir(result.plugin, isGlobal), result.entry.src);
111
+ if (target !== expectedSrc) {
112
+ console.log(chalk.yellow("⚠ Symlink target does not match expected source"));
113
+ console.log(chalk.dim(` Expected: ${expectedSrc}`));
114
+ console.log(chalk.dim(` Actual: ${target}`));
115
+ console.log(chalk.dim(` Expected to be installed by: ${result.plugin}`));
116
+ } else {
117
+ console.log(chalk.green(`✓ Installed by: ${result.plugin}`));
118
+ console.log(chalk.dim(` Source: ${result.entry.src}`));
119
+ console.log(chalk.dim(` Dest: ${result.entry.dest}`));
120
+ }
121
+ }
91
122
 
92
123
  // Get plugin info
93
124
  const pkgJson = await readPluginPackageJson(result.plugin, isGlobal);
package/src/conflicts.ts CHANGED
@@ -1,10 +1,42 @@
1
- import type { PluginPackageJson } from "./manifest.js";
1
+ import type { PluginPackageJson } from "@omp/manifest";
2
2
 
3
3
  export interface Conflict {
4
4
  dest: string;
5
5
  plugins: Array<{ name: string; src: string }>;
6
6
  }
7
7
 
8
+ export interface IntraPluginDuplicate {
9
+ dest: string;
10
+ sources: string[];
11
+ }
12
+
13
+ /**
14
+ * Detect duplicate destinations within a single plugin's omp.install array
15
+ */
16
+ export function detectIntraPluginDuplicates(pkgJson: PluginPackageJson): IntraPluginDuplicate[] {
17
+ const duplicates: IntraPluginDuplicate[] = [];
18
+
19
+ if (!pkgJson.omp?.install?.length) {
20
+ return duplicates;
21
+ }
22
+
23
+ const destMap = new Map<string, string[]>();
24
+
25
+ for (const entry of pkgJson.omp.install) {
26
+ const sources = destMap.get(entry.dest) || [];
27
+ sources.push(entry.src);
28
+ destMap.set(entry.dest, sources);
29
+ }
30
+
31
+ for (const [dest, sources] of destMap) {
32
+ if (sources.length > 1) {
33
+ duplicates.push({ dest, sources });
34
+ }
35
+ }
36
+
37
+ return duplicates;
38
+ }
39
+
8
40
  /**
9
41
  * Detect conflicts between a new plugin and existing plugins
10
42
  */
package/src/errors.ts ADDED
@@ -0,0 +1,22 @@
1
+ import chalk from "chalk";
2
+
3
+ /**
4
+ * Wraps a command function with consistent error handling.
5
+ * - Catches errors and logs user-friendly messages
6
+ * - Shows stack trace only when DEBUG env var is set
7
+ * - Sets non-zero exit code on error
8
+ */
9
+ export function withErrorHandling<T extends (...args: any[]) => Promise<void>>(fn: T): T {
10
+ return (async (...args: any[]) => {
11
+ try {
12
+ await fn(...args);
13
+ } catch (err) {
14
+ const error = err as Error;
15
+ console.log(chalk.red(`Error: ${error.message}`));
16
+ if (process.env.DEBUG) {
17
+ console.log(chalk.dim(error.stack));
18
+ }
19
+ process.exitCode = 1;
20
+ }
21
+ }) as T;
22
+ }
package/src/index.ts CHANGED
@@ -1,43 +1,37 @@
1
- // Core commands
2
-
3
- export { createPlugin } from "./commands/create.js";
4
- export { runDoctor } from "./commands/doctor.js";
5
- export { disablePlugin, enablePlugin } from "./commands/enable.js";
6
- export { showInfo } from "./commands/info.js";
7
- // New commands
8
- export { initProject } from "./commands/init.js";
9
- export { installPlugin } from "./commands/install.js";
10
- export { linkPlugin } from "./commands/link.js";
11
- export { listPlugins } from "./commands/list.js";
12
- export { showOutdated } from "./commands/outdated.js";
13
- export { searchPlugins } from "./commands/search.js";
14
- export { uninstallPlugin } from "./commands/uninstall.js";
15
- export { updatePlugin } from "./commands/update.js";
16
- export { whyFile } from "./commands/why.js";
1
+ export { createPlugin } from "@omp/commands/create";
2
+ export { runDoctor } from "@omp/commands/doctor";
3
+ export { disablePlugin, enablePlugin } from "@omp/commands/enable";
4
+ export { showInfo } from "@omp/commands/info";
5
+ export { initProject } from "@omp/commands/init";
6
+ export { installPlugin } from "@omp/commands/install";
7
+ export { linkPlugin } from "@omp/commands/link";
8
+ export { listPlugins } from "@omp/commands/list";
9
+ export { showOutdated } from "@omp/commands/outdated";
10
+ export { searchPlugins } from "@omp/commands/search";
11
+ export { uninstallPlugin } from "@omp/commands/uninstall";
12
+ export { updatePlugin } from "@omp/commands/update";
13
+ export { whyFile } from "@omp/commands/why";
17
14
  export {
18
15
  detectAllConflicts,
19
16
  detectConflicts,
20
17
  formatConflicts,
21
- } from "./conflicts.js";
18
+ } from "@omp/conflicts";
22
19
 
23
- // Types
24
20
  export type {
25
21
  OmpField,
26
22
  OmpInstallEntry,
27
23
  PluginPackageJson,
28
24
  PluginsJson,
29
- } from "./manifest.js";
25
+ } from "@omp/manifest";
30
26
 
31
- // Utilities
32
27
  export {
33
28
  getInstalledPlugins,
34
29
  initGlobalPlugins,
35
30
  loadPluginsJson,
36
31
  readPluginPackageJson,
37
32
  savePluginsJson,
38
- } from "./manifest.js";
39
- // Migration
40
- export { checkMigration, migrateToNpm } from "./migrate.js";
33
+ } from "@omp/manifest";
34
+ export { checkMigration, migrateToNpm } from "@omp/migrate";
41
35
  export {
42
36
  npmInfo,
43
37
  npmInstall,
@@ -45,9 +39,9 @@ export {
45
39
  npmSearch,
46
40
  npmUninstall,
47
41
  npmUpdate,
48
- } from "./npm.js";
42
+ } from "@omp/npm";
49
43
  export {
50
44
  checkPluginSymlinks,
51
45
  createPluginSymlinks,
52
46
  removePluginSymlinks,
53
- } from "./symlinks.js";
47
+ } from "@omp/symlinks";
package/src/lock.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { PI_CONFIG_DIR, PROJECT_PI_DIR } from "@omp/paths";
5
+
6
+ const LOCK_TIMEOUT_MS = 60000; // 1 minute
7
+
8
+ export async function acquireLock(global = true): Promise<boolean> {
9
+ const lockPath = global ? join(PI_CONFIG_DIR, ".lock") : join(PROJECT_PI_DIR, ".lock");
10
+
11
+ try {
12
+ await mkdir(dirname(lockPath), { recursive: true });
13
+
14
+ // Check for existing lock
15
+ if (existsSync(lockPath)) {
16
+ const content = await readFile(lockPath, "utf-8");
17
+ const { pid, timestamp } = JSON.parse(content);
18
+
19
+ // Check if stale (older than timeout)
20
+ if (Date.now() - timestamp > LOCK_TIMEOUT_MS) {
21
+ // Stale lock, remove it
22
+ await rm(lockPath, { force: true });
23
+ } else {
24
+ // Check if process is still alive
25
+ try {
26
+ process.kill(pid, 0); // Signal 0 = check existence
27
+ return false; // Process alive, can't acquire
28
+ } catch {
29
+ // Process dead, remove stale lock
30
+ await rm(lockPath, { force: true });
31
+ }
32
+ }
33
+ }
34
+
35
+ // Create lock
36
+ await writeFile(lockPath, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ export async function releaseLock(global = true): Promise<void> {
44
+ const lockPath = global ? join(PI_CONFIG_DIR, ".lock") : join(PROJECT_PI_DIR, ".lock");
45
+ await rm(lockPath, { force: true });
46
+ }