@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.
- package/.github/icon.png +0 -0
- package/.github/logo.png +0 -0
- package/.github/workflows/publish.yml +1 -1
- package/LICENSE +21 -0
- package/README.md +131 -145
- package/biome.json +1 -1
- package/dist/cli.js +2032 -1136
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/enable.d.ts +1 -0
- package/dist/commands/enable.d.ts.map +1 -1
- package/dist/commands/info.d.ts +1 -0
- package/dist/commands/info.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/link.d.ts +2 -0
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/outdated.d.ts +1 -0
- package/dist/commands/outdated.d.ts.map +1 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/uninstall.d.ts +1 -0
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/why.d.ts +1 -0
- package/dist/commands/why.d.ts.map +1 -1
- package/dist/conflicts.d.ts +9 -1
- package/dist/conflicts.d.ts.map +1 -1
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +19 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/lock.d.ts +3 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lockfile.d.ts +52 -0
- package/dist/lockfile.d.ts.map +1 -0
- package/dist/manifest.d.ts +5 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/npm.d.ts +14 -2
- package/dist/npm.d.ts.map +1 -1
- package/dist/paths.d.ts +34 -2
- package/dist/paths.d.ts.map +1 -1
- package/dist/symlinks.d.ts +10 -4
- package/dist/symlinks.d.ts.map +1 -1
- package/package.json +7 -2
- package/plugins/metal-theme/package.json +6 -1
- package/plugins/subagents/package.json +6 -1
- package/src/cli.ts +69 -43
- package/src/commands/create.ts +51 -1
- package/src/commands/doctor.ts +95 -7
- package/src/commands/enable.ts +25 -8
- package/src/commands/info.ts +41 -5
- package/src/commands/init.ts +20 -2
- package/src/commands/install.ts +266 -52
- package/src/commands/link.ts +60 -9
- package/src/commands/list.ts +10 -5
- package/src/commands/outdated.ts +17 -6
- package/src/commands/search.ts +20 -3
- package/src/commands/uninstall.ts +57 -6
- package/src/commands/update.ts +67 -9
- package/src/commands/why.ts +47 -16
- package/src/conflicts.ts +33 -1
- package/src/errors.ts +22 -0
- package/src/index.ts +19 -25
- package/src/lock.ts +46 -0
- package/src/lockfile.ts +132 -0
- package/src/manifest.ts +143 -35
- package/src/migrate.ts +14 -3
- package/src/npm.ts +74 -18
- package/src/paths.ts +77 -9
- package/src/symlinks.ts +134 -17
- package/tsconfig.json +7 -3
- 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 {
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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<
|
|
82
|
-
const result:
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
298
|
+
// Normalize the path relative to the base directory
|
|
299
|
+
const baseDir = getBaseDir(global);
|
|
183
300
|
let relativePath = filePath;
|
|
184
|
-
if (filePath.startsWith(
|
|
185
|
-
relativePath = filePath.slice(
|
|
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": "
|
|
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": "
|
|
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
|
-
```
|