@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.
- 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 +243 -138
- package/biome.json +1 -1
- package/bun.lock +59 -0
- package/dist/cli.js +6311 -2900
- package/dist/commands/config.d.ts +32 -0
- package/dist/commands/config.d.ts.map +1 -0
- 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/env.d.ts +14 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/features.d.ts +25 -0
- package/dist/commands/features.d.ts.map +1 -0
- 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 +37 -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 +18 -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 +60 -25
- package/dist/manifest.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 -3
- package/dist/paths.d.ts.map +1 -1
- package/dist/runtime.d.ts +14 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/symlinks.d.ts +43 -7
- package/dist/symlinks.d.ts.map +1 -1
- package/package.json +11 -5
- package/plugins/exa/README.md +153 -0
- package/plugins/exa/package.json +56 -0
- package/plugins/exa/tools/exa/company.ts +35 -0
- package/plugins/exa/tools/exa/index.ts +66 -0
- package/plugins/exa/tools/exa/linkedin.ts +35 -0
- package/plugins/exa/tools/exa/researcher.ts +40 -0
- package/plugins/exa/tools/exa/runtime.json +4 -0
- package/plugins/exa/tools/exa/search.ts +46 -0
- package/plugins/exa/tools/exa/shared.ts +230 -0
- package/plugins/exa/tools/exa/websets.ts +62 -0
- package/plugins/metal-theme/package.json +7 -2
- package/plugins/subagents/package.json +7 -2
- package/plugins/user-prompt/README.md +130 -0
- package/plugins/user-prompt/package.json +19 -0
- package/plugins/user-prompt/tools/user-prompt/index.ts +235 -0
- package/src/cli.ts +133 -58
- package/src/commands/config.ts +384 -0
- package/src/commands/create.ts +51 -1
- package/src/commands/doctor.ts +95 -7
- package/src/commands/enable.ts +25 -8
- package/src/commands/env.ts +38 -0
- package/src/commands/features.ts +295 -0
- package/src/commands/info.ts +41 -5
- package/src/commands/init.ts +20 -2
- package/src/commands/install.ts +453 -80
- package/src/commands/link.ts +60 -9
- package/src/commands/list.ts +122 -7
- 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 +18 -25
- package/src/lock.ts +46 -0
- package/src/lockfile.ts +132 -0
- package/src/manifest.ts +219 -71
- package/src/npm.ts +74 -18
- package/src/paths.ts +77 -12
- package/src/runtime.ts +116 -0
- package/src/symlinks.ts +291 -35
- package/tsconfig.json +7 -3
- package/CHECK.md +0 -352
- package/dist/migrate.d.ts +0 -9
- package/dist/migrate.d.ts.map +0 -1
- package/src/migrate.ts +0 -181
package/src/manifest.ts
CHANGED
|
@@ -3,11 +3,21 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
GLOBAL_PACKAGE_JSON,
|
|
6
|
-
LEGACY_MANIFEST_PATH,
|
|
7
6
|
NODE_MODULES_DIR,
|
|
8
7
|
PLUGINS_DIR,
|
|
8
|
+
PROJECT_PACKAGE_JSON,
|
|
9
9
|
PROJECT_PLUGINS_JSON,
|
|
10
|
-
} from "
|
|
10
|
+
} from "@omp/paths";
|
|
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
|
+
}
|
|
11
21
|
|
|
12
22
|
/**
|
|
13
23
|
* OMP field in package.json - defines what files to install
|
|
@@ -15,10 +25,51 @@ import {
|
|
|
15
25
|
export interface OmpInstallEntry {
|
|
16
26
|
src: string;
|
|
17
27
|
dest: string;
|
|
28
|
+
/** If true, this file is copied (not symlinked) and can be edited by omp */
|
|
29
|
+
copy?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Runtime configuration stored in plugin's runtime.json
|
|
34
|
+
* This file is copied (not symlinked) and edited by omp features/config commands
|
|
35
|
+
*/
|
|
36
|
+
export interface PluginRuntimeConfig {
|
|
37
|
+
features?: string[];
|
|
38
|
+
options?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Runtime variable definition with type, default, and metadata
|
|
43
|
+
*/
|
|
44
|
+
export interface OmpVariable {
|
|
45
|
+
type: "string" | "number" | "boolean" | "string[]";
|
|
46
|
+
default?: string | number | boolean | string[];
|
|
47
|
+
description?: string;
|
|
48
|
+
required?: boolean;
|
|
49
|
+
/** Environment variable name if injected as env (e.g., "EXA_API_KEY") */
|
|
50
|
+
env?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Feature definition - metadata only, no install entries
|
|
55
|
+
* All feature files are always installed; runtime.json controls which are active
|
|
56
|
+
*/
|
|
57
|
+
export interface OmpFeature {
|
|
58
|
+
description?: string;
|
|
59
|
+
/** Runtime variables specific to this feature */
|
|
60
|
+
variables?: Record<string, OmpVariable>;
|
|
61
|
+
/** Default enabled state (default: true) */
|
|
62
|
+
default?: boolean;
|
|
18
63
|
}
|
|
19
64
|
|
|
20
65
|
export interface OmpField {
|
|
66
|
+
/** Top-level install entries (always installed, not feature-gated) */
|
|
21
67
|
install?: OmpInstallEntry[];
|
|
68
|
+
/** Top-level runtime variables (always available) */
|
|
69
|
+
variables?: Record<string, OmpVariable>;
|
|
70
|
+
/** Named features with their own install entries and variables */
|
|
71
|
+
features?: Record<string, OmpFeature>;
|
|
72
|
+
/** Disabled state (managed by omp, not plugin author) */
|
|
22
73
|
disabled?: boolean;
|
|
23
74
|
}
|
|
24
75
|
|
|
@@ -36,48 +87,92 @@ export interface PluginPackageJson {
|
|
|
36
87
|
files?: string[];
|
|
37
88
|
}
|
|
38
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Per-plugin configuration stored in plugins.json
|
|
92
|
+
*/
|
|
93
|
+
export interface PluginConfig {
|
|
94
|
+
/**
|
|
95
|
+
* Enabled feature names:
|
|
96
|
+
* - null/undefined: use plugin defaults (first install = all, reinstall = preserve)
|
|
97
|
+
* - ["*"]: explicitly all features
|
|
98
|
+
* - []: no optional features (core only)
|
|
99
|
+
* - ["f1", "f2"]: specific features
|
|
100
|
+
*/
|
|
101
|
+
features?: string[] | null;
|
|
102
|
+
/** Runtime variable overrides */
|
|
103
|
+
variables?: Record<string, string | number | boolean | string[]>;
|
|
104
|
+
}
|
|
105
|
+
|
|
39
106
|
/**
|
|
40
107
|
* Global/project plugins.json structure
|
|
41
108
|
*/
|
|
42
109
|
export interface PluginsJson {
|
|
43
110
|
plugins: Record<string, string>; // name -> version specifier
|
|
111
|
+
devDependencies?: Record<string, string>; // dev dependencies
|
|
44
112
|
disabled?: string[]; // disabled plugin names
|
|
113
|
+
/** Per-plugin feature and variable config */
|
|
114
|
+
config?: Record<string, PluginConfig>;
|
|
45
115
|
}
|
|
46
116
|
|
|
47
117
|
/**
|
|
48
|
-
*
|
|
118
|
+
* Initialize the global plugins directory with package.json
|
|
49
119
|
*/
|
|
50
|
-
export
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
package?: string;
|
|
54
|
-
path?: string;
|
|
55
|
-
subdir?: string;
|
|
56
|
-
version?: string;
|
|
57
|
-
linked?: boolean;
|
|
58
|
-
installed: string[];
|
|
59
|
-
installedAt: string;
|
|
60
|
-
}
|
|
120
|
+
export async function initGlobalPlugins(): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
await mkdir(PLUGINS_DIR, { recursive: true });
|
|
61
123
|
|
|
62
|
-
|
|
63
|
-
|
|
124
|
+
if (!existsSync(GLOBAL_PACKAGE_JSON)) {
|
|
125
|
+
const packageJson = {
|
|
126
|
+
name: "pi-plugins",
|
|
127
|
+
version: "1.0.0",
|
|
128
|
+
private: true,
|
|
129
|
+
description: "Global pi plugins managed by omp",
|
|
130
|
+
dependencies: {},
|
|
131
|
+
};
|
|
132
|
+
await writeFile(GLOBAL_PACKAGE_JSON, JSON.stringify(packageJson, null, 2));
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
const error = err as NodeJS.ErrnoException;
|
|
136
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
137
|
+
throw new Error(formatPermissionError(error, PLUGINS_DIR));
|
|
138
|
+
}
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
64
141
|
}
|
|
65
142
|
|
|
66
143
|
/**
|
|
67
|
-
* Initialize the
|
|
144
|
+
* Initialize the project-local .pi directory with plugins.json and package.json
|
|
68
145
|
*/
|
|
69
|
-
export async function
|
|
70
|
-
|
|
146
|
+
export async function initProjectPlugins(): Promise<void> {
|
|
147
|
+
const PROJECT_PI_DIR = dirname(PROJECT_PLUGINS_JSON);
|
|
148
|
+
try {
|
|
149
|
+
await mkdir(PROJECT_PI_DIR, { recursive: true });
|
|
71
150
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
151
|
+
// Create plugins.json if it doesn't exist
|
|
152
|
+
if (!existsSync(PROJECT_PLUGINS_JSON)) {
|
|
153
|
+
const pluginsJson = {
|
|
154
|
+
plugins: {},
|
|
155
|
+
};
|
|
156
|
+
await writeFile(PROJECT_PLUGINS_JSON, JSON.stringify(pluginsJson, null, 2));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Create package.json if it doesn't exist (for npm operations)
|
|
160
|
+
if (!existsSync(PROJECT_PACKAGE_JSON)) {
|
|
161
|
+
const packageJson = {
|
|
162
|
+
name: "pi-project-plugins",
|
|
163
|
+
version: "1.0.0",
|
|
164
|
+
private: true,
|
|
165
|
+
description: "Project-local pi plugins managed by omp",
|
|
166
|
+
dependencies: {},
|
|
167
|
+
};
|
|
168
|
+
await writeFile(PROJECT_PACKAGE_JSON, JSON.stringify(packageJson, null, 2));
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
const error = err as NodeJS.ErrnoException;
|
|
172
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
173
|
+
throw new Error(formatPermissionError(error, PROJECT_PI_DIR));
|
|
174
|
+
}
|
|
175
|
+
throw err;
|
|
81
176
|
}
|
|
82
177
|
}
|
|
83
178
|
|
|
@@ -95,53 +190,125 @@ export async function loadPluginsJson(global = true): Promise<PluginsJson> {
|
|
|
95
190
|
// Global uses standard package.json format
|
|
96
191
|
return {
|
|
97
192
|
plugins: parsed.dependencies || {},
|
|
193
|
+
devDependencies: parsed.devDependencies || {},
|
|
98
194
|
disabled: parsed.omp?.disabled || [],
|
|
195
|
+
config: parsed.omp?.config || {},
|
|
99
196
|
};
|
|
100
197
|
}
|
|
101
198
|
|
|
102
199
|
// Project uses plugins.json format
|
|
103
200
|
return {
|
|
104
201
|
plugins: parsed.plugins || {},
|
|
202
|
+
devDependencies: parsed.devDependencies || {},
|
|
105
203
|
disabled: parsed.disabled || [],
|
|
204
|
+
config: parsed.config || {},
|
|
106
205
|
};
|
|
107
206
|
} catch (err) {
|
|
108
207
|
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
109
|
-
return { plugins: {}, disabled: [] };
|
|
208
|
+
return { plugins: {}, devDependencies: {}, disabled: [], config: {} };
|
|
110
209
|
}
|
|
111
210
|
throw err;
|
|
112
211
|
}
|
|
113
212
|
}
|
|
114
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Sync .pi/package.json with plugins.json for npm operations in project-local mode
|
|
216
|
+
*/
|
|
217
|
+
async function syncProjectPackageJson(data: PluginsJson): Promise<void> {
|
|
218
|
+
let existing: Record<string, unknown> = {};
|
|
219
|
+
try {
|
|
220
|
+
existing = JSON.parse(await readFile(PROJECT_PACKAGE_JSON, "utf-8"));
|
|
221
|
+
} catch {
|
|
222
|
+
existing = {
|
|
223
|
+
name: "pi-project-plugins",
|
|
224
|
+
version: "1.0.0",
|
|
225
|
+
private: true,
|
|
226
|
+
description: "Project-local pi plugins managed by omp",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
existing.dependencies = data.plugins;
|
|
231
|
+
if (data.devDependencies && Object.keys(data.devDependencies).length > 0) {
|
|
232
|
+
existing.devDependencies = data.devDependencies;
|
|
233
|
+
} else {
|
|
234
|
+
delete existing.devDependencies;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await writeFile(PROJECT_PACKAGE_JSON, JSON.stringify(existing, null, 2));
|
|
238
|
+
}
|
|
239
|
+
|
|
115
240
|
/**
|
|
116
241
|
* Save plugins.json (global or project)
|
|
117
242
|
*/
|
|
118
243
|
export async function savePluginsJson(data: PluginsJson, global = true): Promise<void> {
|
|
119
244
|
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
245
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
existing.omp = { ...((existing.omp as Record<string, unknown>) || {}), disabled: data.disabled };
|
|
139
|
-
}
|
|
246
|
+
try {
|
|
247
|
+
await mkdir(dirname(path), { recursive: true });
|
|
140
248
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
249
|
+
if (global) {
|
|
250
|
+
// Read existing package.json and update dependencies
|
|
251
|
+
let existing: Record<string, unknown> = {};
|
|
252
|
+
try {
|
|
253
|
+
existing = JSON.parse(await readFile(path, "utf-8"));
|
|
254
|
+
} catch {
|
|
255
|
+
existing = {
|
|
256
|
+
name: "pi-plugins",
|
|
257
|
+
version: "1.0.0",
|
|
258
|
+
private: true,
|
|
259
|
+
description: "Global pi plugins managed by omp",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
existing.dependencies = data.plugins;
|
|
264
|
+
if (data.devDependencies && Object.keys(data.devDependencies).length > 0) {
|
|
265
|
+
existing.devDependencies = data.devDependencies;
|
|
266
|
+
} else {
|
|
267
|
+
delete existing.devDependencies;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Build omp field with disabled and config
|
|
271
|
+
const ompField: Record<string, unknown> = (existing.omp as Record<string, unknown>) || {};
|
|
272
|
+
if (data.disabled?.length) {
|
|
273
|
+
ompField.disabled = data.disabled;
|
|
274
|
+
} else {
|
|
275
|
+
delete ompField.disabled;
|
|
276
|
+
}
|
|
277
|
+
if (data.config && Object.keys(data.config).length > 0) {
|
|
278
|
+
ompField.config = data.config;
|
|
279
|
+
} else {
|
|
280
|
+
delete ompField.config;
|
|
281
|
+
}
|
|
282
|
+
if (Object.keys(ompField).length > 0) {
|
|
283
|
+
existing.omp = ompField;
|
|
284
|
+
} else {
|
|
285
|
+
delete existing.omp;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await writeFile(path, JSON.stringify(existing, null, 2));
|
|
289
|
+
} else {
|
|
290
|
+
// Project uses simple plugins.json format
|
|
291
|
+
const output: Record<string, unknown> = { plugins: data.plugins };
|
|
292
|
+
if (data.devDependencies && Object.keys(data.devDependencies).length > 0) {
|
|
293
|
+
output.devDependencies = data.devDependencies;
|
|
294
|
+
}
|
|
295
|
+
if (data.disabled?.length) {
|
|
296
|
+
output.disabled = data.disabled;
|
|
297
|
+
}
|
|
298
|
+
if (data.config && Object.keys(data.config).length > 0) {
|
|
299
|
+
output.config = data.config;
|
|
300
|
+
}
|
|
301
|
+
await writeFile(path, JSON.stringify(output, null, 2));
|
|
302
|
+
|
|
303
|
+
// Sync .pi/package.json for npm operations
|
|
304
|
+
await syncProjectPackageJson(data);
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
const error = err as NodeJS.ErrnoException;
|
|
308
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
309
|
+
throw new Error(formatPermissionError(error, path));
|
|
310
|
+
}
|
|
311
|
+
throw err;
|
|
145
312
|
}
|
|
146
313
|
}
|
|
147
314
|
|
|
@@ -175,25 +342,6 @@ export function getPluginSourceDir(pluginName: string, global = true): string {
|
|
|
175
342
|
return join(nodeModules, pluginName);
|
|
176
343
|
}
|
|
177
344
|
|
|
178
|
-
/**
|
|
179
|
-
* Check if legacy manifest.json exists
|
|
180
|
-
*/
|
|
181
|
-
export function hasLegacyManifest(): boolean {
|
|
182
|
-
return existsSync(LEGACY_MANIFEST_PATH);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Load legacy manifest.json
|
|
187
|
-
*/
|
|
188
|
-
export async function loadLegacyManifest(): Promise<LegacyManifest> {
|
|
189
|
-
try {
|
|
190
|
-
const data = await readFile(LEGACY_MANIFEST_PATH, "utf-8");
|
|
191
|
-
return JSON.parse(data) as LegacyManifest;
|
|
192
|
-
} catch {
|
|
193
|
-
return { plugins: {} };
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
345
|
/**
|
|
198
346
|
* Get all installed plugins with their info
|
|
199
347
|
*/
|
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");
|
|
@@ -16,15 +17,15 @@ export const GLOBAL_PACKAGE_JSON = join(PLUGINS_DIR, "package.json");
|
|
|
16
17
|
// Global package-lock.json
|
|
17
18
|
export const GLOBAL_LOCK_FILE = join(PLUGINS_DIR, "package-lock.json");
|
|
18
19
|
|
|
19
|
-
// Legacy manifest (for migration)
|
|
20
|
-
export const LEGACY_MANIFEST_PATH = join(PLUGINS_DIR, "manifest.json");
|
|
21
|
-
|
|
22
20
|
// Project-local config directory
|
|
23
21
|
export const PROJECT_PI_DIR = ".pi";
|
|
24
22
|
|
|
25
23
|
// Project-local plugins.json
|
|
26
24
|
export const PROJECT_PLUGINS_JSON = join(PROJECT_PI_DIR, "plugins.json");
|
|
27
25
|
|
|
26
|
+
// Project-local package.json (for npm operations)
|
|
27
|
+
export const PROJECT_PACKAGE_JSON = join(PROJECT_PI_DIR, "package.json");
|
|
28
|
+
|
|
28
29
|
// Project-local lock file
|
|
29
30
|
export const PROJECT_PLUGINS_LOCK = join(PROJECT_PI_DIR, "plugins-lock.json");
|
|
30
31
|
|
|
@@ -32,13 +33,45 @@ export const PROJECT_PLUGINS_LOCK = join(PROJECT_PI_DIR, "plugins-lock.json");
|
|
|
32
33
|
export const PROJECT_NODE_MODULES = join(PROJECT_PI_DIR, "node_modules");
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
|
-
*
|
|
36
|
+
* Find the project root by walking up parent directories looking for .pi/plugins.json.
|
|
37
|
+
* Similar to how git finds .git directories.
|
|
38
|
+
*
|
|
39
|
+
* @returns The absolute path to the project root, or null if not found
|
|
36
40
|
*/
|
|
37
|
-
export function
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
export function findProjectRoot(): string | null {
|
|
42
|
+
let dir = process.cwd();
|
|
43
|
+
const root = resolve("/");
|
|
44
|
+
|
|
45
|
+
while (dir !== root) {
|
|
46
|
+
if (existsSync(join(dir, ".pi", "plugins.json"))) {
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
const parent = dirname(dir);
|
|
50
|
+
if (parent === dir) break; // Reached filesystem root
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a project-local .pi/plugins.json exists in the current directory or any parent
|
|
59
|
+
*/
|
|
60
|
+
export function hasProjectPlugins(): boolean {
|
|
61
|
+
return findProjectRoot() !== null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the project .pi directory path.
|
|
66
|
+
* Uses findProjectRoot() to locate the project, or falls back to cwd.
|
|
67
|
+
*/
|
|
68
|
+
export function getProjectPiDir(): string {
|
|
69
|
+
const projectRoot = findProjectRoot();
|
|
70
|
+
if (projectRoot) {
|
|
71
|
+
return join(projectRoot, ".pi");
|
|
40
72
|
}
|
|
41
|
-
|
|
73
|
+
// Fallback to cwd (e.g., for init command)
|
|
74
|
+
return resolve(PROJECT_PI_DIR);
|
|
42
75
|
}
|
|
43
76
|
|
|
44
77
|
/**
|
|
@@ -48,7 +81,7 @@ export function getPluginsDir(global = true): string {
|
|
|
48
81
|
if (global) {
|
|
49
82
|
return PLUGINS_DIR;
|
|
50
83
|
}
|
|
51
|
-
return
|
|
84
|
+
return getProjectPiDir();
|
|
52
85
|
}
|
|
53
86
|
|
|
54
87
|
/**
|
|
@@ -58,7 +91,7 @@ export function getNodeModulesDir(global = true): string {
|
|
|
58
91
|
if (global) {
|
|
59
92
|
return NODE_MODULES_DIR;
|
|
60
93
|
}
|
|
61
|
-
return
|
|
94
|
+
return join(getProjectPiDir(), "node_modules");
|
|
62
95
|
}
|
|
63
96
|
|
|
64
97
|
/**
|
|
@@ -68,5 +101,37 @@ export function getPackageJsonPath(global = true): string {
|
|
|
68
101
|
if (global) {
|
|
69
102
|
return GLOBAL_PACKAGE_JSON;
|
|
70
103
|
}
|
|
71
|
-
return
|
|
104
|
+
return join(getProjectPiDir(), "plugins.json");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the agent directory (where symlinks are installed)
|
|
109
|
+
*/
|
|
110
|
+
export function getAgentDir(global = true): string {
|
|
111
|
+
if (global) {
|
|
112
|
+
return join(PI_CONFIG_DIR, "agent");
|
|
113
|
+
}
|
|
114
|
+
return join(getProjectPiDir(), "agent");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve whether to use global or local scope based on CLI flags and auto-detection.
|
|
119
|
+
*
|
|
120
|
+
* Logic:
|
|
121
|
+
* - If --global is passed: use global mode
|
|
122
|
+
* - If --local is passed: use local mode
|
|
123
|
+
* - If neither: check if .pi/plugins.json exists in cwd, if so use local, otherwise use global
|
|
124
|
+
*
|
|
125
|
+
* @param options - CLI options containing global and local flags
|
|
126
|
+
* @returns true if global scope should be used, false for local
|
|
127
|
+
*/
|
|
128
|
+
export function resolveScope(options: { global?: boolean; local?: boolean }): boolean {
|
|
129
|
+
if (options.global) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
if (options.local) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
// Auto-detect: if project-local plugins.json exists, use local mode
|
|
136
|
+
return !hasProjectPlugins();
|
|
72
137
|
}
|