@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/symlinks.ts CHANGED
@@ -1,24 +1,63 @@
1
1
  import { existsSync, lstatSync } from "node:fs";
2
2
  import { mkdir, readlink, rm, symlink } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
3
+ import { platform } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import type { OmpInstallEntry, PluginPackageJson } from "@omp/manifest";
6
+ import { getPluginSourceDir } from "@omp/manifest";
7
+ import { PI_CONFIG_DIR, PROJECT_PI_DIR } from "@omp/paths";
4
8
  import chalk from "chalk";
5
- import type { OmpInstallEntry, PluginPackageJson } from "./manifest.js";
6
- import { getPluginSourceDir } from "./manifest.js";
7
- import { PI_CONFIG_DIR } from "./paths.js";
9
+
10
+ const isWindows = platform() === "win32";
11
+
12
+ /**
13
+ * Format permission-related errors with actionable guidance
14
+ */
15
+ function formatPermissionError(err: NodeJS.ErrnoException, path: string): string {
16
+ if (err.code === "EACCES" || err.code === "EPERM") {
17
+ return `Permission denied: Cannot write to ${path}. Check directory permissions or run with appropriate privileges.`;
18
+ }
19
+ return err.message;
20
+ }
21
+
22
+ /**
23
+ * Validates that a target path stays within the base directory.
24
+ * Prevents path traversal attacks via malicious dest entries like '../../../etc/passwd'.
25
+ */
26
+ function isPathWithinBase(basePath: string, targetPath: string): boolean {
27
+ const normalizedBase = resolve(basePath);
28
+ const resolvedTarget = resolve(basePath, targetPath);
29
+ // Must start with base path followed by separator (or be exactly the base)
30
+ return resolvedTarget === normalizedBase || resolvedTarget.startsWith(`${normalizedBase}/`);
31
+ }
32
+
33
+ /**
34
+ * Get the base directory for symlink destinations based on scope
35
+ */
36
+ function getBaseDir(global: boolean): string {
37
+ return global ? PI_CONFIG_DIR : PROJECT_PI_DIR;
38
+ }
8
39
 
9
40
  export interface SymlinkResult {
10
41
  created: string[];
11
42
  errors: string[];
12
43
  }
13
44
 
45
+ export interface SymlinkRemovalResult {
46
+ removed: string[];
47
+ errors: string[];
48
+ skippedNonSymlinks: string[]; // Files that exist but aren't symlinks
49
+ }
50
+
14
51
  /**
15
52
  * Create symlinks for a plugin's omp.install entries
53
+ * @param skipDestinations - Set of destination paths to skip (e.g., due to conflict resolution)
16
54
  */
17
55
  export async function createPluginSymlinks(
18
56
  pluginName: string,
19
57
  pkgJson: PluginPackageJson,
20
58
  global = true,
21
59
  verbose = true,
60
+ skipDestinations?: Set<string>,
22
61
  ): Promise<SymlinkResult> {
23
62
  const result: SymlinkResult = { created: [], errors: [] };
24
63
  const sourceDir = getPluginSourceDir(pluginName, global);
@@ -30,10 +69,30 @@ export async function createPluginSymlinks(
30
69
  return result;
31
70
  }
32
71
 
72
+ const baseDir = getBaseDir(global);
73
+
33
74
  for (const entry of pkgJson.omp.install) {
75
+ // Skip destinations that the user chose to keep from existing plugins
76
+ if (skipDestinations?.has(entry.dest)) {
77
+ if (verbose) {
78
+ console.log(chalk.dim(` Skipped: ${entry.dest} (conflict resolved to existing plugin)`));
79
+ }
80
+ continue;
81
+ }
82
+
83
+ // Validate dest path stays within base directory (prevents path traversal attacks)
84
+ if (!isPathWithinBase(baseDir, entry.dest)) {
85
+ const msg = `Path traversal blocked: ${entry.dest} escapes base directory`;
86
+ result.errors.push(msg);
87
+ if (verbose) {
88
+ console.log(chalk.red(` ✗ ${msg}`));
89
+ }
90
+ continue;
91
+ }
92
+
34
93
  try {
35
94
  const src = join(sourceDir, entry.src);
36
- const dest = join(PI_CONFIG_DIR, entry.dest);
95
+ const dest = join(baseDir, entry.dest);
37
96
 
38
97
  // Check if source exists
39
98
  if (!existsSync(src)) {
@@ -52,18 +111,41 @@ export async function createPluginSymlinks(
52
111
  await rm(dest, { force: true, recursive: true });
53
112
  } catch {}
54
113
 
55
- // Create symlink
56
- await symlink(src, dest);
114
+ // Create symlink (use junctions on Windows for directories to avoid admin requirement)
115
+ try {
116
+ if (isWindows) {
117
+ const stats = lstatSync(src);
118
+ if (stats.isDirectory()) {
119
+ await symlink(src, dest, "junction");
120
+ } else {
121
+ await symlink(src, dest, "file");
122
+ }
123
+ } else {
124
+ await symlink(src, dest);
125
+ }
126
+ } catch (symlinkErr) {
127
+ const error = symlinkErr as NodeJS.ErrnoException;
128
+ if (isWindows && error.code === "EPERM") {
129
+ console.log(chalk.red(` Permission denied creating symlink.`));
130
+ console.log(chalk.dim(" On Windows, enable Developer Mode or run as Administrator."));
131
+ console.log(chalk.dim(" Settings > Update & Security > For developers > Developer Mode"));
132
+ }
133
+ throw symlinkErr;
134
+ }
57
135
  result.created.push(entry.dest);
58
136
 
59
137
  if (verbose) {
60
138
  console.log(chalk.dim(` Linked: ${entry.dest} → ${entry.src}`));
61
139
  }
62
140
  } catch (err) {
63
- const msg = `Failed to link ${entry.dest}: ${(err as Error).message}`;
141
+ const error = err as NodeJS.ErrnoException;
142
+ const msg = `Failed to link ${entry.dest}: ${formatPermissionError(error, join(baseDir, entry.dest))}`;
64
143
  result.errors.push(msg);
65
144
  if (verbose) {
66
145
  console.log(chalk.red(` ✗ ${msg}`));
146
+ if (error.code === "EACCES" || error.code === "EPERM") {
147
+ console.log(chalk.dim(" Check directory permissions or run with appropriate privileges."));
148
+ }
67
149
  }
68
150
  }
69
151
  }
@@ -77,30 +159,56 @@ export async function createPluginSymlinks(
77
159
  export async function removePluginSymlinks(
78
160
  _pluginName: string,
79
161
  pkgJson: PluginPackageJson,
162
+ global = true,
80
163
  verbose = true,
81
- ): Promise<SymlinkResult> {
82
- const result: SymlinkResult = { created: [], errors: [] };
164
+ ): Promise<SymlinkRemovalResult> {
165
+ const result: SymlinkRemovalResult = { removed: [], errors: [], skippedNonSymlinks: [] };
83
166
 
84
167
  if (!pkgJson.omp?.install?.length) {
85
168
  return result;
86
169
  }
87
170
 
171
+ const baseDir = getBaseDir(global);
172
+
88
173
  for (const entry of pkgJson.omp.install) {
89
- const dest = join(PI_CONFIG_DIR, entry.dest);
174
+ // Validate dest path stays within base directory (prevents path traversal attacks)
175
+ if (!isPathWithinBase(baseDir, entry.dest)) {
176
+ const msg = `Path traversal blocked: ${entry.dest} escapes base directory`;
177
+ result.errors.push(msg);
178
+ if (verbose) {
179
+ console.log(chalk.red(` ✗ ${msg}`));
180
+ }
181
+ continue;
182
+ }
183
+
184
+ const dest = join(baseDir, entry.dest);
90
185
 
91
186
  try {
92
187
  if (existsSync(dest)) {
188
+ const stats = lstatSync(dest);
189
+ if (!stats.isSymbolicLink()) {
190
+ result.skippedNonSymlinks.push(dest);
191
+ if (verbose) {
192
+ console.log(chalk.yellow(` ⚠ Skipping ${entry.dest}: not a symlink (may contain user data)`));
193
+ }
194
+ continue;
195
+ }
196
+
93
197
  await rm(dest, { force: true, recursive: true });
94
- result.created.push(entry.dest);
198
+ result.removed.push(entry.dest);
95
199
  if (verbose) {
96
200
  console.log(chalk.dim(` Removed: ${entry.dest}`));
97
201
  }
98
202
  }
99
203
  } catch (err) {
100
- const msg = `Failed to remove ${entry.dest}: ${(err as Error).message}`;
204
+ const error = err as NodeJS.ErrnoException;
205
+ const msg = `Failed to remove ${entry.dest}: ${formatPermissionError(error, dest)}`;
101
206
  result.errors.push(msg);
102
207
  if (verbose) {
103
208
  console.log(chalk.yellow(` ⚠ ${msg}`));
209
+ if (error.code === "EACCES" || error.code === "EPERM") {
210
+ console.log(chalk.dim(" Check directory permissions or run with appropriate privileges."));
211
+ }
104
212
  }
105
213
  }
106
214
  }
@@ -118,14 +226,21 @@ export async function checkPluginSymlinks(
118
226
  ): Promise<{ valid: string[]; broken: string[]; missing: string[] }> {
119
227
  const result = { valid: [] as string[], broken: [] as string[], missing: [] as string[] };
120
228
  const sourceDir = getPluginSourceDir(pluginName, global);
229
+ const baseDir = getBaseDir(global);
121
230
 
122
231
  if (!pkgJson.omp?.install?.length) {
123
232
  return result;
124
233
  }
125
234
 
126
235
  for (const entry of pkgJson.omp.install) {
236
+ // Skip entries with path traversal (treat as broken)
237
+ if (!isPathWithinBase(baseDir, entry.dest)) {
238
+ result.broken.push(entry.dest);
239
+ continue;
240
+ }
241
+
127
242
  const src = join(sourceDir, entry.src);
128
- const dest = join(PI_CONFIG_DIR, entry.dest);
243
+ const dest = join(baseDir, entry.dest);
129
244
 
130
245
  if (!existsSync(dest)) {
131
246
  result.missing.push(entry.dest);
@@ -178,11 +293,13 @@ export async function getPluginForSymlink(
178
293
  export async function traceInstalledFile(
179
294
  filePath: string,
180
295
  installedPlugins: Map<string, PluginPackageJson>,
296
+ global = true,
181
297
  ): Promise<{ plugin: string; entry: OmpInstallEntry } | null> {
182
- // Normalize the path relative to PI_CONFIG_DIR
298
+ // Normalize the path relative to the base directory
299
+ const baseDir = getBaseDir(global);
183
300
  let relativePath = filePath;
184
- if (filePath.startsWith(PI_CONFIG_DIR)) {
185
- relativePath = filePath.slice(PI_CONFIG_DIR.length + 1);
301
+ if (filePath.startsWith(baseDir)) {
302
+ relativePath = filePath.slice(baseDir.length + 1);
186
303
  }
187
304
 
188
305
  for (const [name, pkgJson] of installedPlugins) {
package/tsconfig.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
- "module": "Node16",
4
+ "module": "ESNext",
5
5
  "lib": ["ES2022"],
6
6
  "strict": true,
7
7
  "esModuleInterop": true,
@@ -12,12 +12,16 @@
12
12
  "sourceMap": true,
13
13
  "inlineSources": true,
14
14
  "inlineSourceMap": false,
15
- "moduleResolution": "Node16",
15
+ "moduleResolution": "bundler",
16
16
  "resolveJsonModule": true,
17
17
  "allowImportingTsExtensions": false,
18
18
  "outDir": "./dist",
19
19
  "rootDir": "./src",
20
- "types": ["node", "bun-types"]
20
+ "types": ["node", "bun-types"],
21
+ "baseUrl": ".",
22
+ "paths": {
23
+ "@omp/*": ["src/*"]
24
+ }
21
25
  },
22
26
  "include": ["src/**/*"],
23
27
  "exclude": ["node_modules", "dist"]
package/CHECK.md DELETED
@@ -1,352 +0,0 @@
1
- # Implementation Review Checklist
2
-
3
- Review the oh-my-pi implementation against these requirements. For each item, verify it works correctly and check the listed edge cases.
4
-
5
- ---
6
-
7
- ## 1. Install Command (`omp install`)
8
-
9
- ### Core functionality
10
- - [ ] `omp install pkg-name` installs from npm
11
- - [ ] `omp install pkg@version` respects version specifier
12
- - [ ] `omp install @scope/pkg` handles scoped packages
13
- - [ ] `omp install ./path` copies local directory
14
- - [ ] `omp install ~/path` expands home directory
15
- - [ ] `omp install` (no args) installs from plugins.json
16
- - [ ] Creates symlinks for each `omp.install` entry
17
-
18
- ### Edge cases
19
- - [ ] Package not found on npm → clear error message
20
- - [ ] Invalid version specifier → graceful failure
21
- - [ ] Local path doesn't exist → error before any changes
22
- - [ ] `omp.install` has missing source file → skip with warning, continue others
23
- - [ ] Circular dependencies between plugins → handled without infinite loop
24
- - [ ] Nested dependencies with omp field → all get symlinked
25
- - [ ] Package has no `omp` field → installs but no symlinks (warning?)
26
- - [ ] Network failure mid-install → rollback partial install
27
- - [ ] Disk full → appropriate error
28
- - [ ] Symlink target directory doesn't exist → creates parent dirs
29
- - [ ] `omp install` with empty plugins.json → helpful message
30
-
31
- ### Flags
32
- - [ ] `--global` installs to ~/.pi/plugins/
33
- - [ ] `--save` adds to plugins.json
34
- - [ ] `--save-dev` adds to devDependencies
35
- - [ ] `--force` overwrites conflicts without prompting
36
- - [ ] `--json` outputs JSON format
37
-
38
- ---
39
-
40
- ## 2. Conflict Detection
41
-
42
- ### Core functionality
43
- - [ ] Detects when two plugins install to same destination
44
- - [ ] Interactive prompt offers choices: plugin1, plugin2, abort
45
- - [ ] User can select which plugin wins
46
- - [ ] Abort rolls back the install
47
-
48
- ### Edge cases
49
- - [ ] Three+ plugins with same destination → all listed
50
- - [ ] Conflict with already-installed plugin → prompt before npm install
51
- - [ ] Conflict within same plugin's install array (duplicate dest) → error
52
- - [ ] `--force` skips all prompts → overwrites
53
- - [ ] Non-interactive terminal (CI) → fail or use --force
54
-
55
- ---
56
-
57
- ## 3. Uninstall Command (`omp uninstall`)
58
-
59
- ### Core functionality
60
- - [ ] `omp uninstall pkg` removes package
61
- - [ ] Removes all symlinks from `omp.install`
62
- - [ ] Removes from package.json/plugins.json
63
-
64
- ### Edge cases
65
- - [ ] Plugin not installed → clear error
66
- - [ ] Symlink already deleted manually → graceful (ENOENT ignored)
67
- - [ ] Symlink replaced by real file → remove anyway or warn?
68
- - [ ] Plugin has dependencies that other plugins also use → don't break them
69
- - [ ] Partial uninstall (some symlinks fail) → report what failed
70
-
71
- ---
72
-
73
- ## 4. Update Command (`omp update`)
74
-
75
- ### Core functionality
76
- - [ ] `omp update` updates all plugins
77
- - [ ] `omp update pkg` updates specific plugin
78
- - [ ] Respects semver ranges from plugins.json
79
- - [ ] Re-creates symlinks after update
80
-
81
- ### Edge cases
82
- - [ ] Package already at latest → no-op with message
83
- - [ ] Major version available but range is `^` → doesn't update
84
- - [ ] Package removed from npm → error handling
85
- - [ ] Update changes `omp.install` entries → old symlinks removed, new created
86
- - [ ] Linked (dev) plugins → skipped with message
87
-
88
- ---
89
-
90
- ## 5. List Command (`omp list`)
91
-
92
- ### Core functionality
93
- - [ ] Shows all installed plugins
94
- - [ ] Shows version for each
95
- - [ ] Shows (linked) badge for dev plugins
96
-
97
- ### Edge cases
98
- - [ ] No plugins installed → helpful message
99
- - [ ] Plugin in plugins.json but not in node_modules → show as broken
100
- - [ ] Corrupt package.json → graceful error
101
- - [ ] `--json` output is valid JSON
102
-
103
- ---
104
-
105
- ## 6. Link Command (`omp link`)
106
-
107
- ### Core functionality
108
- - [ ] `omp link ./path` creates symlink to source directory
109
- - [ ] Marks as linked in manifest
110
- - [ ] Creates symlinks from `omp.install`
111
-
112
- ### Edge cases
113
- - [ ] Path doesn't exist → error
114
- - [ ] Path has no package.json → uses directory name, creates minimal
115
- - [ ] Path has omp.json (legacy) → converts to package.json format
116
- - [ ] Already linked → re-link or error?
117
- - [ ] `-n, --name` overrides plugin name
118
- - [ ] Linked plugin updated externally → symlinks still work
119
-
120
- ---
121
-
122
- ## 7. Init Command (`omp init`)
123
-
124
- ### Core functionality
125
- - [ ] Creates `.pi/plugins.json` in current directory
126
- - [ ] Correct initial structure: `{ "plugins": {}, "disabled": [] }`
127
-
128
- ### Edge cases
129
- - [ ] File already exists → error unless `--force`
130
- - [ ] `.pi/` directory doesn't exist → creates it
131
- - [ ] No write permission → clear error
132
-
133
- ---
134
-
135
- ## 8. Search Command (`omp search`)
136
-
137
- ### Core functionality
138
- - [ ] `omp search query` searches npm for `keywords:omp-plugin`
139
- - [ ] Shows name, version, description
140
-
141
- ### Edge cases
142
- - [ ] No results → helpful message
143
- - [ ] npm registry unavailable → timeout with error
144
- - [ ] Very long description → truncated
145
- - [ ] `--limit` respected
146
- - [ ] Special characters in query → properly escaped
147
-
148
- ---
149
-
150
- ## 9. Info Command (`omp info`)
151
-
152
- ### Core functionality
153
- - [ ] `omp info pkg` shows package details
154
- - [ ] Shows: name, version, description, dependencies, omp.install entries
155
-
156
- ### Edge cases
157
- - [ ] Package not found → clear error
158
- - [ ] Package exists but no omp field → show warning
159
- - [ ] `--versions` shows available versions
160
- - [ ] `--json` valid JSON output
161
-
162
- ---
163
-
164
- ## 10. Outdated Command (`omp outdated`)
165
-
166
- ### Core functionality
167
- - [ ] Lists plugins with newer versions available
168
- - [ ] Shows current, wanted, latest columns
169
-
170
- ### Edge cases
171
- - [ ] All up to date → "All plugins up to date"
172
- - [ ] npm outdated returns exit code 1 → still parses output
173
- - [ ] Plugin not on npm anymore → shown as error row
174
- - [ ] Linked plugins → excluded from check
175
-
176
- ---
177
-
178
- ## 11. Doctor Command (`omp doctor`)
179
-
180
- ### Core functionality
181
- - [ ] Checks for broken symlinks
182
- - [ ] Checks for conflicts
183
- - [ ] Reports missing dependencies
184
-
185
- ### Edge cases
186
- - [ ] No issues → "All good"
187
- - [ ] Symlink target deleted → reported as broken
188
- - [ ] Symlink replaced by real file → reported
189
- - [ ] `--fix` repairs broken symlinks
190
- - [ ] `--fix` can't fix conflict → reports
191
-
192
- ---
193
-
194
- ## 12. Create Command (`omp create`)
195
-
196
- ### Core functionality
197
- - [ ] `omp create my-plugin` scaffolds new plugin
198
- - [ ] Creates package.json with omp field
199
- - [ ] Creates directory structure
200
-
201
- ### Edge cases
202
- - [ ] Directory already exists → error
203
- - [ ] Invalid plugin name (spaces, special chars) → normalized or error
204
- - [ ] `--description` sets description
205
- - [ ] `--author` sets author
206
-
207
- ---
208
-
209
- ## 13. Why Command (`omp why`)
210
-
211
- ### Core functionality
212
- - [ ] `omp why agent/themes/dark.json` shows which plugin installed it
213
- - [ ] Shows plugin name and source path
214
-
215
- ### Edge cases
216
- - [ ] File not installed by any plugin → "Not installed by omp"
217
- - [ ] File is manually created (not symlink) → detect and report
218
- - [ ] Relative vs absolute path → both work
219
- - [ ] File doesn't exist → appropriate error
220
-
221
- ---
222
-
223
- ## 14. Enable/Disable Commands
224
-
225
- ### Core functionality
226
- - [ ] `omp disable pkg` removes symlinks, keeps in node_modules
227
- - [ ] `omp enable pkg` restores symlinks
228
- - [ ] Disabled list persisted
229
-
230
- ### Edge cases
231
- - [ ] Disable already-disabled → no-op or error
232
- - [ ] Enable not-disabled → no-op
233
- - [ ] Enable not-installed → error
234
- - [ ] Symlinks manually restored → enable detects and handles
235
-
236
- ---
237
-
238
- ## 15. Project-Local vs Global
239
-
240
- ### Core functionality
241
- - [ ] Default is global (~/.pi/)
242
- - [ ] With `.pi/plugins.json` in cwd, uses project-local
243
- - [ ] `--global` forces global even with local config
244
- - [ ] Symlinks go to correct location per scope
245
-
246
- ### Edge cases
247
- - [ ] Nested projects (parent and child have .pi/) → uses closest
248
- - [ ] Project-local install then run from parent dir → correct behavior
249
- - [ ] Same plugin installed globally and locally → local takes precedence?
250
-
251
- ---
252
-
253
- ## 16. Migration (`omp migrate`)
254
-
255
- ### Core functionality
256
- - [ ] Detects legacy manifest.json
257
- - [ ] Converts to package.json format
258
- - [ ] Preserves all plugin info
259
- - [ ] Re-creates symlinks
260
-
261
- ### Edge cases
262
- - [ ] No legacy manifest → "Nothing to migrate"
263
- - [ ] Partial migration (some plugins fail) → reports
264
- - [ ] Legacy plugin source gone → reports but continues
265
- - [ ] Backup of manifest.json created
266
-
267
- ---
268
-
269
- ## 17. Lock File
270
-
271
- ### Core functionality
272
- - [ ] package-lock.json generated for global
273
- - [ ] plugins-lock.json generated for project-local
274
- - [ ] Reproducible installs with lock file
275
-
276
- ### Edge cases
277
- - [ ] Lock file corrupt → regenerate with warning
278
- - [ ] Lock file deleted → works but warns about reproducibility
279
-
280
- ---
281
-
282
- ## 18. Symlinks Module
283
-
284
- ### Core functionality
285
- - [ ] Creates symlinks correctly
286
- - [ ] Removes symlinks on uninstall
287
- - [ ] Validates symlink health
288
-
289
- ### Edge cases
290
- - [ ] **BUG CHECK: Project-local uses correct base path (.pi/ not ~/.pi/)**
291
- - [ ] Destination is a directory → symlink whole directory
292
- - [ ] Destination is a file → symlink file
293
- - [ ] Parent directories created recursively
294
- - [ ] Windows compatibility (if applicable) → use junctions or requires admin
295
-
296
- ---
297
-
298
- ## 19. npm Module
299
-
300
- ### Core functionality
301
- - [ ] npm install works with prefix
302
- - [ ] npm info fetches package data
303
- - [ ] npm search returns results
304
- - [ ] npm outdated parsed correctly
305
-
306
- ### Edge cases
307
- - [ ] npm not installed → clear error
308
- - [ ] npm version too old → check and warn
309
- - [ ] Private registry → respects .npmrc
310
- - [ ] Scoped package with private registry → works
311
-
312
- ---
313
-
314
- ## 20. General Edge Cases
315
-
316
- ### Error handling
317
- - [ ] All commands have try/catch at top level
318
- - [ ] Errors include actionable messages
319
- - [ ] Stack traces hidden unless DEBUG
320
-
321
- ### Permissions
322
- - [ ] No sudo required for ~/.pi
323
- - [ ] Handles permission denied gracefully
324
- - [ ] Doesn't follow symlinks outside allowed paths (security)
325
-
326
- ### Concurrency
327
- - [ ] Two `omp install` at once → lock file prevents corruption
328
- - [ ] Interrupted install → recoverable state
329
-
330
- ### Unicode/i18n
331
- - [ ] Plugin names with unicode → handled
332
- - [ ] Paths with spaces → quoted correctly
333
- - [ ] Paths with unicode → work on all platforms
334
-
335
- ---
336
-
337
- ## Verification Steps
338
-
339
- For each section above:
340
- 1. Read the relevant source file
341
- 2. Trace the code path for the happy case
342
- 3. Check each edge case is handled
343
- 4. Note any missing error handling
344
- 5. Run the command if possible to verify
345
-
346
- Report findings as:
347
- ```
348
- ## [Command Name]
349
- ✓ Working: [list of working features]
350
- ✗ Missing: [list of unimplemented features]
351
- ⚠ Issues: [list of bugs or edge cases not handled]
352
- ```