@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/lockfile.ts
ADDED
|
@@ -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
|
+
}
|
package/src/manifest.ts
CHANGED
|
@@ -6,8 +6,19 @@ import {
|
|
|
6
6
|
LEGACY_MANIFEST_PATH,
|
|
7
7
|
NODE_MODULES_DIR,
|
|
8
8
|
PLUGINS_DIR,
|
|
9
|
+
PROJECT_PACKAGE_JSON,
|
|
9
10
|
PROJECT_PLUGINS_JSON,
|
|
10
|
-
} from "
|
|
11
|
+
} from "@omp/paths";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format permission-related errors with actionable guidance
|
|
15
|
+
*/
|
|
16
|
+
function formatPermissionError(err: NodeJS.ErrnoException, path: string): string {
|
|
17
|
+
if (err.code === "EACCES" || err.code === "EPERM") {
|
|
18
|
+
return `Permission denied: Cannot write to ${path}. Check directory permissions or run with appropriate privileges.`;
|
|
19
|
+
}
|
|
20
|
+
return err.message;
|
|
21
|
+
}
|
|
11
22
|
|
|
12
23
|
/**
|
|
13
24
|
* OMP field in package.json - defines what files to install
|
|
@@ -41,6 +52,7 @@ export interface PluginPackageJson {
|
|
|
41
52
|
*/
|
|
42
53
|
export interface PluginsJson {
|
|
43
54
|
plugins: Record<string, string>; // name -> version specifier
|
|
55
|
+
devDependencies?: Record<string, string>; // dev dependencies
|
|
44
56
|
disabled?: string[]; // disabled plugin names
|
|
45
57
|
}
|
|
46
58
|
|
|
@@ -67,17 +79,61 @@ export interface LegacyManifest {
|
|
|
67
79
|
* Initialize the global plugins directory with package.json
|
|
68
80
|
*/
|
|
69
81
|
export async function initGlobalPlugins(): Promise<void> {
|
|
70
|
-
|
|
82
|
+
try {
|
|
83
|
+
await mkdir(PLUGINS_DIR, { recursive: true });
|
|
71
84
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
if (!existsSync(GLOBAL_PACKAGE_JSON)) {
|
|
86
|
+
const packageJson = {
|
|
87
|
+
name: "pi-plugins",
|
|
88
|
+
version: "1.0.0",
|
|
89
|
+
private: true,
|
|
90
|
+
description: "Global pi plugins managed by omp",
|
|
91
|
+
dependencies: {},
|
|
92
|
+
};
|
|
93
|
+
await writeFile(GLOBAL_PACKAGE_JSON, JSON.stringify(packageJson, null, 2));
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const error = err as NodeJS.ErrnoException;
|
|
97
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
98
|
+
throw new Error(formatPermissionError(error, PLUGINS_DIR));
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Initialize the project-local .pi directory with plugins.json and package.json
|
|
106
|
+
*/
|
|
107
|
+
export async function initProjectPlugins(): Promise<void> {
|
|
108
|
+
const PROJECT_PI_DIR = dirname(PROJECT_PLUGINS_JSON);
|
|
109
|
+
try {
|
|
110
|
+
await mkdir(PROJECT_PI_DIR, { recursive: true });
|
|
111
|
+
|
|
112
|
+
// Create plugins.json if it doesn't exist
|
|
113
|
+
if (!existsSync(PROJECT_PLUGINS_JSON)) {
|
|
114
|
+
const pluginsJson = {
|
|
115
|
+
plugins: {},
|
|
116
|
+
};
|
|
117
|
+
await writeFile(PROJECT_PLUGINS_JSON, JSON.stringify(pluginsJson, null, 2));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Create package.json if it doesn't exist (for npm operations)
|
|
121
|
+
if (!existsSync(PROJECT_PACKAGE_JSON)) {
|
|
122
|
+
const packageJson = {
|
|
123
|
+
name: "pi-project-plugins",
|
|
124
|
+
version: "1.0.0",
|
|
125
|
+
private: true,
|
|
126
|
+
description: "Project-local pi plugins managed by omp",
|
|
127
|
+
dependencies: {},
|
|
128
|
+
};
|
|
129
|
+
await writeFile(PROJECT_PACKAGE_JSON, JSON.stringify(packageJson, null, 2));
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const error = err as NodeJS.ErrnoException;
|
|
133
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
134
|
+
throw new Error(formatPermissionError(error, PROJECT_PI_DIR));
|
|
135
|
+
}
|
|
136
|
+
throw err;
|
|
81
137
|
}
|
|
82
138
|
}
|
|
83
139
|
|
|
@@ -95,6 +151,7 @@ export async function loadPluginsJson(global = true): Promise<PluginsJson> {
|
|
|
95
151
|
// Global uses standard package.json format
|
|
96
152
|
return {
|
|
97
153
|
plugins: parsed.dependencies || {},
|
|
154
|
+
devDependencies: parsed.devDependencies || {},
|
|
98
155
|
disabled: parsed.omp?.disabled || [],
|
|
99
156
|
};
|
|
100
157
|
}
|
|
@@ -102,46 +159,97 @@ export async function loadPluginsJson(global = true): Promise<PluginsJson> {
|
|
|
102
159
|
// Project uses plugins.json format
|
|
103
160
|
return {
|
|
104
161
|
plugins: parsed.plugins || {},
|
|
162
|
+
devDependencies: parsed.devDependencies || {},
|
|
105
163
|
disabled: parsed.disabled || [],
|
|
106
164
|
};
|
|
107
165
|
} catch (err) {
|
|
108
166
|
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
109
|
-
return { plugins: {}, disabled: [] };
|
|
167
|
+
return { plugins: {}, devDependencies: {}, disabled: [] };
|
|
110
168
|
}
|
|
111
169
|
throw err;
|
|
112
170
|
}
|
|
113
171
|
}
|
|
114
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Sync .pi/package.json with plugins.json for npm operations in project-local mode
|
|
175
|
+
*/
|
|
176
|
+
async function syncProjectPackageJson(data: PluginsJson): Promise<void> {
|
|
177
|
+
let existing: Record<string, unknown> = {};
|
|
178
|
+
try {
|
|
179
|
+
existing = JSON.parse(await readFile(PROJECT_PACKAGE_JSON, "utf-8"));
|
|
180
|
+
} catch {
|
|
181
|
+
existing = {
|
|
182
|
+
name: "pi-project-plugins",
|
|
183
|
+
version: "1.0.0",
|
|
184
|
+
private: true,
|
|
185
|
+
description: "Project-local pi plugins managed by omp",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
existing.dependencies = data.plugins;
|
|
190
|
+
if (data.devDependencies && Object.keys(data.devDependencies).length > 0) {
|
|
191
|
+
existing.devDependencies = data.devDependencies;
|
|
192
|
+
} else {
|
|
193
|
+
delete existing.devDependencies;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await writeFile(PROJECT_PACKAGE_JSON, JSON.stringify(existing, null, 2));
|
|
197
|
+
}
|
|
198
|
+
|
|
115
199
|
/**
|
|
116
200
|
* Save plugins.json (global or project)
|
|
117
201
|
*/
|
|
118
202
|
export async function savePluginsJson(data: PluginsJson, global = true): Promise<void> {
|
|
119
203
|
const path = global ? GLOBAL_PACKAGE_JSON : PROJECT_PLUGINS_JSON;
|
|
120
|
-
await mkdir(dirname(path), { recursive: true });
|
|
121
|
-
|
|
122
|
-
if (global) {
|
|
123
|
-
// Read existing package.json and update dependencies
|
|
124
|
-
let existing: Record<string, unknown> = {};
|
|
125
|
-
try {
|
|
126
|
-
existing = JSON.parse(await readFile(path, "utf-8"));
|
|
127
|
-
} catch {
|
|
128
|
-
existing = {
|
|
129
|
-
name: "pi-plugins",
|
|
130
|
-
version: "1.0.0",
|
|
131
|
-
private: true,
|
|
132
|
-
description: "Global pi plugins managed by omp",
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
204
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
existing.omp = { ...((existing.omp as Record<string, unknown>) || {}), disabled: data.disabled };
|
|
139
|
-
}
|
|
205
|
+
try {
|
|
206
|
+
await mkdir(dirname(path), { recursive: true });
|
|
140
207
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
208
|
+
if (global) {
|
|
209
|
+
// Read existing package.json and update dependencies
|
|
210
|
+
let existing: Record<string, unknown> = {};
|
|
211
|
+
try {
|
|
212
|
+
existing = JSON.parse(await readFile(path, "utf-8"));
|
|
213
|
+
} catch {
|
|
214
|
+
existing = {
|
|
215
|
+
name: "pi-plugins",
|
|
216
|
+
version: "1.0.0",
|
|
217
|
+
private: true,
|
|
218
|
+
description: "Global pi plugins managed by omp",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
existing.dependencies = data.plugins;
|
|
223
|
+
if (data.devDependencies && Object.keys(data.devDependencies).length > 0) {
|
|
224
|
+
existing.devDependencies = data.devDependencies;
|
|
225
|
+
} else {
|
|
226
|
+
delete existing.devDependencies;
|
|
227
|
+
}
|
|
228
|
+
if (data.disabled?.length) {
|
|
229
|
+
existing.omp = { ...((existing.omp as Record<string, unknown>) || {}), disabled: data.disabled };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await writeFile(path, JSON.stringify(existing, null, 2));
|
|
233
|
+
} else {
|
|
234
|
+
// Project uses simple plugins.json format
|
|
235
|
+
const output: Record<string, unknown> = { plugins: data.plugins };
|
|
236
|
+
if (data.devDependencies && Object.keys(data.devDependencies).length > 0) {
|
|
237
|
+
output.devDependencies = data.devDependencies;
|
|
238
|
+
}
|
|
239
|
+
if (data.disabled?.length) {
|
|
240
|
+
output.disabled = data.disabled;
|
|
241
|
+
}
|
|
242
|
+
await writeFile(path, JSON.stringify(output, null, 2));
|
|
243
|
+
|
|
244
|
+
// Sync .pi/package.json for npm operations
|
|
245
|
+
await syncProjectPackageJson(data);
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
const error = err as NodeJS.ErrnoException;
|
|
249
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
250
|
+
throw new Error(formatPermissionError(error, path));
|
|
251
|
+
}
|
|
252
|
+
throw err;
|
|
145
253
|
}
|
|
146
254
|
}
|
|
147
255
|
|
package/src/migrate.ts
CHANGED
|
@@ -2,16 +2,18 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
|
-
import chalk from "chalk";
|
|
6
5
|
import {
|
|
7
6
|
hasLegacyManifest,
|
|
8
7
|
type LegacyPluginInfo,
|
|
9
8
|
loadLegacyManifest,
|
|
10
9
|
type PluginPackageJson,
|
|
11
10
|
type PluginsJson,
|
|
11
|
+
readPluginPackageJson,
|
|
12
12
|
savePluginsJson,
|
|
13
|
-
} from "
|
|
14
|
-
import { LEGACY_MANIFEST_PATH, NODE_MODULES_DIR, PLUGINS_DIR } from "
|
|
13
|
+
} from "@omp/manifest";
|
|
14
|
+
import { LEGACY_MANIFEST_PATH, NODE_MODULES_DIR, PLUGINS_DIR } from "@omp/paths";
|
|
15
|
+
import { createPluginSymlinks } from "@omp/symlinks";
|
|
16
|
+
import chalk from "chalk";
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Prompt user for migration
|
|
@@ -104,6 +106,15 @@ export async function migrateToNpm(): Promise<boolean> {
|
|
|
104
106
|
// Archive legacy manifest
|
|
105
107
|
await archiveLegacyManifest();
|
|
106
108
|
|
|
109
|
+
// Re-create symlinks for migrated plugins
|
|
110
|
+
console.log(chalk.dim(" Creating symlinks..."));
|
|
111
|
+
for (const [name] of Object.entries(newPluginsJson.plugins)) {
|
|
112
|
+
const pkgJson = await readPluginPackageJson(name, true); // global mode
|
|
113
|
+
if (pkgJson?.omp?.install?.length) {
|
|
114
|
+
await createPluginSymlinks(name, pkgJson, true);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
console.log();
|
|
108
119
|
console.log(chalk.green(`✓ Migrated ${migrated.length} plugin(s)`));
|
|
109
120
|
if (failed.length > 0) {
|
package/src/npm.ts
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import type { OmpField } from "@omp/manifest";
|
|
3
|
+
|
|
4
|
+
export interface NpmAvailability {
|
|
5
|
+
available: boolean;
|
|
6
|
+
version?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check npm availability and version
|
|
12
|
+
*/
|
|
13
|
+
export function checkNpmAvailable(): NpmAvailability {
|
|
14
|
+
try {
|
|
15
|
+
const version = execFileSync("npm", ["--version"], {
|
|
16
|
+
encoding: "utf-8",
|
|
17
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
18
|
+
}).trim();
|
|
19
|
+
|
|
20
|
+
// Parse version and check minimum (npm 7+)
|
|
21
|
+
const major = parseInt(version.split(".")[0], 10);
|
|
22
|
+
if (major < 7) {
|
|
23
|
+
return {
|
|
24
|
+
available: false,
|
|
25
|
+
version,
|
|
26
|
+
error: `npm version ${version} is too old. Please upgrade to npm 7 or later.`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { available: true, version };
|
|
31
|
+
} catch {
|
|
32
|
+
return {
|
|
33
|
+
available: false,
|
|
34
|
+
error: "npm is not installed or not in PATH. Please install Node.js/npm.",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
2
38
|
|
|
3
39
|
export interface NpmPackageInfo {
|
|
4
40
|
name: string;
|
|
@@ -10,6 +46,8 @@ export interface NpmPackageInfo {
|
|
|
10
46
|
repository?: { type: string; url: string } | string;
|
|
11
47
|
versions?: string[];
|
|
12
48
|
"dist-tags"?: Record<string, string>;
|
|
49
|
+
omp?: OmpField;
|
|
50
|
+
dependencies?: Record<string, string>;
|
|
13
51
|
}
|
|
14
52
|
|
|
15
53
|
export interface NpmSearchResult {
|
|
@@ -21,23 +59,45 @@ export interface NpmSearchResult {
|
|
|
21
59
|
author?: { name: string };
|
|
22
60
|
}
|
|
23
61
|
|
|
62
|
+
const DEFAULT_TIMEOUT_MS = 60000; // 60 seconds
|
|
63
|
+
|
|
24
64
|
/**
|
|
25
65
|
* Execute npm command and return output
|
|
26
66
|
*/
|
|
27
|
-
export function npmExec(args: string[], cwd?: string): string {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
67
|
+
export function npmExec(args: string[], cwd?: string, timeout = DEFAULT_TIMEOUT_MS): string {
|
|
68
|
+
try {
|
|
69
|
+
return execFileSync("npm", args, {
|
|
70
|
+
cwd,
|
|
71
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
timeout,
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const error = err as { killed?: boolean; code?: string; message: string };
|
|
77
|
+
if (error.killed || error.code === "ETIMEDOUT") {
|
|
78
|
+
throw new Error(`npm operation timed out after ${timeout / 1000} seconds`);
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
34
82
|
}
|
|
35
83
|
|
|
36
84
|
/**
|
|
37
85
|
* Execute npm command with prefix (for installing to specific directory)
|
|
38
86
|
*/
|
|
39
|
-
export function npmExecWithPrefix(args: string[], prefix: string): string {
|
|
40
|
-
|
|
87
|
+
export function npmExecWithPrefix(args: string[], prefix: string, timeout = DEFAULT_TIMEOUT_MS): string {
|
|
88
|
+
try {
|
|
89
|
+
return execFileSync("npm", ["--prefix", prefix, ...args], {
|
|
90
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
91
|
+
encoding: "utf-8",
|
|
92
|
+
timeout,
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const error = err as { killed?: boolean; code?: string; message: string };
|
|
96
|
+
if (error.killed || error.code === "ETIMEDOUT") {
|
|
97
|
+
throw new Error(`npm operation timed out after ${timeout / 1000} seconds`);
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
41
101
|
}
|
|
42
102
|
|
|
43
103
|
/**
|
|
@@ -84,14 +144,10 @@ export async function npmInfo(packageName: string): Promise<NpmPackageInfo | nul
|
|
|
84
144
|
* Search npm for packages with a keyword
|
|
85
145
|
*/
|
|
86
146
|
export async function npmSearch(query: string, keyword = "omp-plugin"): Promise<NpmSearchResult[]> {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return JSON.parse(output);
|
|
92
|
-
} catch {
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
147
|
+
// Search for packages with the omp-plugin keyword
|
|
148
|
+
const searchTerm = keyword ? `keywords:${keyword} ${query}` : query;
|
|
149
|
+
const output = npmExec(["search", searchTerm, "--json"]);
|
|
150
|
+
return JSON.parse(output);
|
|
95
151
|
}
|
|
96
152
|
|
|
97
153
|
/**
|
package/src/paths.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
4
|
|
|
4
5
|
// Global pi configuration directory
|
|
5
6
|
export const PI_CONFIG_DIR = join(homedir(), ".pi");
|
|
@@ -25,6 +26,9 @@ export const PROJECT_PI_DIR = ".pi";
|
|
|
25
26
|
// Project-local plugins.json
|
|
26
27
|
export const PROJECT_PLUGINS_JSON = join(PROJECT_PI_DIR, "plugins.json");
|
|
27
28
|
|
|
29
|
+
// Project-local package.json (for npm operations)
|
|
30
|
+
export const PROJECT_PACKAGE_JSON = join(PROJECT_PI_DIR, "package.json");
|
|
31
|
+
|
|
28
32
|
// Project-local lock file
|
|
29
33
|
export const PROJECT_PLUGINS_LOCK = join(PROJECT_PI_DIR, "plugins-lock.json");
|
|
30
34
|
|
|
@@ -32,13 +36,45 @@ export const PROJECT_PLUGINS_LOCK = join(PROJECT_PI_DIR, "plugins-lock.json");
|
|
|
32
36
|
export const PROJECT_NODE_MODULES = join(PROJECT_PI_DIR, "node_modules");
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
|
-
*
|
|
39
|
+
* Find the project root by walking up parent directories looking for .pi/plugins.json.
|
|
40
|
+
* Similar to how git finds .git directories.
|
|
41
|
+
*
|
|
42
|
+
* @returns The absolute path to the project root, or null if not found
|
|
36
43
|
*/
|
|
37
|
-
export function
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
export function findProjectRoot(): string | null {
|
|
45
|
+
let dir = process.cwd();
|
|
46
|
+
const root = resolve("/");
|
|
47
|
+
|
|
48
|
+
while (dir !== root) {
|
|
49
|
+
if (existsSync(join(dir, ".pi", "plugins.json"))) {
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
const parent = dirname(dir);
|
|
53
|
+
if (parent === dir) break; // Reached filesystem root
|
|
54
|
+
dir = parent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if a project-local .pi/plugins.json exists in the current directory or any parent
|
|
62
|
+
*/
|
|
63
|
+
export function hasProjectPlugins(): boolean {
|
|
64
|
+
return findProjectRoot() !== null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the project .pi directory path.
|
|
69
|
+
* Uses findProjectRoot() to locate the project, or falls back to cwd.
|
|
70
|
+
*/
|
|
71
|
+
export function getProjectPiDir(): string {
|
|
72
|
+
const projectRoot = findProjectRoot();
|
|
73
|
+
if (projectRoot) {
|
|
74
|
+
return join(projectRoot, ".pi");
|
|
40
75
|
}
|
|
41
|
-
|
|
76
|
+
// Fallback to cwd (e.g., for init command)
|
|
77
|
+
return resolve(PROJECT_PI_DIR);
|
|
42
78
|
}
|
|
43
79
|
|
|
44
80
|
/**
|
|
@@ -48,7 +84,7 @@ export function getPluginsDir(global = true): string {
|
|
|
48
84
|
if (global) {
|
|
49
85
|
return PLUGINS_DIR;
|
|
50
86
|
}
|
|
51
|
-
return
|
|
87
|
+
return getProjectPiDir();
|
|
52
88
|
}
|
|
53
89
|
|
|
54
90
|
/**
|
|
@@ -58,7 +94,7 @@ export function getNodeModulesDir(global = true): string {
|
|
|
58
94
|
if (global) {
|
|
59
95
|
return NODE_MODULES_DIR;
|
|
60
96
|
}
|
|
61
|
-
return
|
|
97
|
+
return join(getProjectPiDir(), "node_modules");
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
/**
|
|
@@ -68,5 +104,37 @@ export function getPackageJsonPath(global = true): string {
|
|
|
68
104
|
if (global) {
|
|
69
105
|
return GLOBAL_PACKAGE_JSON;
|
|
70
106
|
}
|
|
71
|
-
return
|
|
107
|
+
return join(getProjectPiDir(), "plugins.json");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get the agent directory (where symlinks are installed)
|
|
112
|
+
*/
|
|
113
|
+
export function getAgentDir(global = true): string {
|
|
114
|
+
if (global) {
|
|
115
|
+
return join(PI_CONFIG_DIR, "agent");
|
|
116
|
+
}
|
|
117
|
+
return join(getProjectPiDir(), "agent");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve whether to use global or local scope based on CLI flags and auto-detection.
|
|
122
|
+
*
|
|
123
|
+
* Logic:
|
|
124
|
+
* - If --global is passed: use global mode
|
|
125
|
+
* - If --local is passed: use local mode
|
|
126
|
+
* - If neither: check if .pi/plugins.json exists in cwd, if so use local, otherwise use global
|
|
127
|
+
*
|
|
128
|
+
* @param options - CLI options containing global and local flags
|
|
129
|
+
* @returns true if global scope should be used, false for local
|
|
130
|
+
*/
|
|
131
|
+
export function resolveScope(options: { global?: boolean; local?: boolean }): boolean {
|
|
132
|
+
if (options.global) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (options.local) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
// Auto-detect: if project-local plugins.json exists, use local mode
|
|
139
|
+
return !hasProjectPlugins();
|
|
72
140
|
}
|