@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,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,36 @@
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";
41
34
  export {
42
35
  npmInfo,
43
36
  npmInstall,
@@ -45,9 +38,9 @@ export {
45
38
  npmSearch,
46
39
  npmUninstall,
47
40
  npmUpdate,
48
- } from "./npm.js";
41
+ } from "@omp/npm";
49
42
  export {
50
43
  checkPluginSymlinks,
51
44
  createPluginSymlinks,
52
45
  removePluginSymlinks,
53
- } from "./symlinks.js";
46
+ } 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
+ }
@@ -0,0 +1,132 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { GLOBAL_LOCK_FILE, PROJECT_PLUGINS_LOCK } from "@omp/paths";
4
+ import chalk from "chalk";
5
+
6
+ /**
7
+ * Lock file schema version
8
+ */
9
+ export const LOCKFILE_VERSION = 1;
10
+
11
+ /**
12
+ * Package entry in the lock file
13
+ */
14
+ export interface LockFilePackage {
15
+ version: string;
16
+ resolved?: string;
17
+ integrity?: string;
18
+ dependencies?: Record<string, string>;
19
+ }
20
+
21
+ /**
22
+ * Lock file structure
23
+ */
24
+ export interface LockFile {
25
+ lockfileVersion: number;
26
+ packages: Record<string, LockFilePackage>;
27
+ }
28
+
29
+ /**
30
+ * Load and validate a lock file.
31
+ *
32
+ * Returns null if:
33
+ * - File doesn't exist
34
+ * - File contains invalid JSON (corrupted)
35
+ * - File has invalid/incompatible schema
36
+ */
37
+ export async function loadLockFile(global = true): Promise<LockFile | null> {
38
+ const path = global ? GLOBAL_LOCK_FILE : PROJECT_PLUGINS_LOCK;
39
+ try {
40
+ if (!existsSync(path)) return null;
41
+ const data = await readFile(path, "utf-8");
42
+ const parsed = JSON.parse(data);
43
+
44
+ // Validate schema
45
+ if (typeof parsed.lockfileVersion !== "number" || typeof parsed.packages !== "object") {
46
+ console.log(chalk.yellow(`Warning: ${path} has invalid schema, ignoring`));
47
+ return null;
48
+ }
49
+
50
+ // Check for incompatible version
51
+ if (parsed.lockfileVersion > LOCKFILE_VERSION) {
52
+ console.log(
53
+ chalk.yellow(
54
+ `Warning: ${path} was created by a newer version of omp (lockfile v${parsed.lockfileVersion}), ignoring`,
55
+ ),
56
+ );
57
+ return null;
58
+ }
59
+
60
+ return parsed as LockFile;
61
+ } catch (err) {
62
+ if ((err as Error).name === "SyntaxError") {
63
+ console.log(chalk.yellow(`Warning: ${path} is corrupted (invalid JSON), ignoring`));
64
+ }
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Save lock file
71
+ */
72
+ export async function saveLockFile(lockFile: LockFile, global = true): Promise<void> {
73
+ const path = global ? GLOBAL_LOCK_FILE : PROJECT_PLUGINS_LOCK;
74
+ await writeFile(path, JSON.stringify(lockFile, null, 2));
75
+ }
76
+
77
+ /**
78
+ * Create a new empty lock file
79
+ */
80
+ export function createLockFile(): LockFile {
81
+ return {
82
+ lockfileVersion: LOCKFILE_VERSION,
83
+ packages: {},
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Validate and optionally regenerate a corrupted lock file.
89
+ *
90
+ * @returns The loaded lock file, a new empty lock file if corrupted/missing, or null if validation fails
91
+ */
92
+ export async function validateOrRegenerateLockFile(global = true): Promise<LockFile> {
93
+ const existing = await loadLockFile(global);
94
+ if (existing) {
95
+ return existing;
96
+ }
97
+
98
+ // Lock file is missing or corrupted - create a fresh one
99
+ const path = global ? GLOBAL_LOCK_FILE : PROJECT_PLUGINS_LOCK;
100
+ if (existsSync(path)) {
101
+ console.log(chalk.yellow(`Regenerating corrupted lock file: ${path}`));
102
+ }
103
+
104
+ return createLockFile();
105
+ }
106
+
107
+ /**
108
+ * Get the locked version for a package, if it exists in the lock file.
109
+ */
110
+ export async function getLockedVersion(packageName: string, global = true): Promise<string | null> {
111
+ const lockFile = await loadLockFile(global);
112
+ if (!lockFile) return null;
113
+
114
+ const entry = lockFile.packages[packageName];
115
+ return entry?.version ?? null;
116
+ }
117
+
118
+ /**
119
+ * Update the lock file with a package's exact version.
120
+ */
121
+ export async function updateLockFile(packageName: string, version: string, global = true): Promise<void> {
122
+ let lockFile = await loadLockFile(global);
123
+ if (!lockFile) {
124
+ lockFile = createLockFile();
125
+ }
126
+
127
+ lockFile.packages[packageName] = {
128
+ version,
129
+ };
130
+
131
+ await saveLockFile(lockFile, global);
132
+ }