@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
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import type { OmpVariable } from "@omp/manifest";
|
|
2
|
+
import { loadPluginsJson, readPluginPackageJson, savePluginsJson } from "@omp/manifest";
|
|
3
|
+
import { resolveScope } from "@omp/paths";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
|
|
6
|
+
export interface ConfigOptions {
|
|
7
|
+
global?: boolean;
|
|
8
|
+
local?: boolean;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
delete?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Collect all variables from a plugin (top-level + enabled features)
|
|
15
|
+
*/
|
|
16
|
+
function collectVariables(
|
|
17
|
+
pkgJson: { omp?: { variables?: Record<string, OmpVariable>; features?: Record<string, { variables?: Record<string, OmpVariable> }> } },
|
|
18
|
+
enabledFeatures: string[],
|
|
19
|
+
): Record<string, OmpVariable> {
|
|
20
|
+
const vars: Record<string, OmpVariable> = {};
|
|
21
|
+
|
|
22
|
+
// Top-level variables
|
|
23
|
+
if (pkgJson.omp?.variables) {
|
|
24
|
+
Object.assign(vars, pkgJson.omp.variables);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Variables from enabled features
|
|
28
|
+
if (pkgJson.omp?.features) {
|
|
29
|
+
for (const fname of enabledFeatures) {
|
|
30
|
+
const feature = pkgJson.omp.features[fname];
|
|
31
|
+
if (feature?.variables) {
|
|
32
|
+
Object.assign(vars, feature.variables);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return vars;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a string value to the appropriate type based on variable definition
|
|
42
|
+
*/
|
|
43
|
+
function parseValue(value: string, varDef: OmpVariable): string | number | boolean | string[] {
|
|
44
|
+
switch (varDef.type) {
|
|
45
|
+
case "number":
|
|
46
|
+
const num = Number(value);
|
|
47
|
+
if (isNaN(num)) {
|
|
48
|
+
throw new Error(`Invalid number: ${value}`);
|
|
49
|
+
}
|
|
50
|
+
return num;
|
|
51
|
+
case "boolean":
|
|
52
|
+
if (value === "true" || value === "1" || value === "yes") return true;
|
|
53
|
+
if (value === "false" || value === "0" || value === "no") return false;
|
|
54
|
+
throw new Error(`Invalid boolean: ${value}. Use true/false, 1/0, or yes/no`);
|
|
55
|
+
case "string[]":
|
|
56
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
57
|
+
default:
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format a value for display
|
|
64
|
+
*/
|
|
65
|
+
function formatValue(value: unknown, varDef: OmpVariable): string {
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
return chalk.dim("(not set)");
|
|
68
|
+
}
|
|
69
|
+
if (varDef.type === "string[]" && Array.isArray(value)) {
|
|
70
|
+
return value.join(", ");
|
|
71
|
+
}
|
|
72
|
+
if (typeof value === "string" && varDef.env) {
|
|
73
|
+
// Mask sensitive values (likely API keys)
|
|
74
|
+
if (value.length > 8) {
|
|
75
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return String(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve which features are currently enabled
|
|
83
|
+
*
|
|
84
|
+
* - null/undefined: use plugin defaults (features with default !== false)
|
|
85
|
+
* - ["*"]: explicitly all features
|
|
86
|
+
* - []: no optional features
|
|
87
|
+
* - ["f1", "f2"]: specific features
|
|
88
|
+
*/
|
|
89
|
+
function resolveEnabledFeatures(
|
|
90
|
+
allFeatureNames: string[],
|
|
91
|
+
storedFeatures: string[] | null | undefined,
|
|
92
|
+
pluginFeatures: Record<string, { default?: boolean }>,
|
|
93
|
+
): string[] {
|
|
94
|
+
// Explicit "all features" request
|
|
95
|
+
if (Array.isArray(storedFeatures) && storedFeatures.includes("*")) return allFeatureNames;
|
|
96
|
+
// Explicit feature list (including empty array = no features)
|
|
97
|
+
if (Array.isArray(storedFeatures)) return storedFeatures;
|
|
98
|
+
// null/undefined = use defaults
|
|
99
|
+
return Object.entries(pluginFeatures)
|
|
100
|
+
.filter(([_, f]) => f.default !== false)
|
|
101
|
+
.map(([name]) => name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* List all configurable variables for a plugin
|
|
106
|
+
* omp config @oh-my-pi/exa
|
|
107
|
+
*/
|
|
108
|
+
export async function listConfig(name: string, options: ConfigOptions = {}): Promise<void> {
|
|
109
|
+
const isGlobal = resolveScope(options);
|
|
110
|
+
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
111
|
+
|
|
112
|
+
if (!pluginsJson.plugins[name]) {
|
|
113
|
+
console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const pkgJson = await readPluginPackageJson(name, isGlobal);
|
|
119
|
+
if (!pkgJson) {
|
|
120
|
+
console.log(chalk.red(`Could not read package.json for ${name}`));
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const allFeatureNames = Object.keys(pkgJson.omp?.features || {});
|
|
126
|
+
const config = pluginsJson.config?.[name];
|
|
127
|
+
const enabledFeatures = resolveEnabledFeatures(allFeatureNames, config?.features, pkgJson.omp?.features || {});
|
|
128
|
+
const variables = collectVariables(pkgJson, enabledFeatures);
|
|
129
|
+
|
|
130
|
+
if (Object.keys(variables).length === 0) {
|
|
131
|
+
console.log(chalk.yellow(`Plugin "${name}" has no configurable variables.`));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const userVars = config?.variables || {};
|
|
136
|
+
|
|
137
|
+
if (options.json) {
|
|
138
|
+
console.log(
|
|
139
|
+
JSON.stringify(
|
|
140
|
+
{
|
|
141
|
+
plugin: name,
|
|
142
|
+
variables: Object.entries(variables).map(([vname, vdef]) => ({
|
|
143
|
+
name: vname,
|
|
144
|
+
type: vdef.type,
|
|
145
|
+
value: userVars[vname],
|
|
146
|
+
default: vdef.default,
|
|
147
|
+
required: vdef.required,
|
|
148
|
+
env: vdef.env,
|
|
149
|
+
description: vdef.description,
|
|
150
|
+
})),
|
|
151
|
+
},
|
|
152
|
+
null,
|
|
153
|
+
2,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(chalk.bold(`\nVariables for ${name}:\n`));
|
|
160
|
+
|
|
161
|
+
for (const [vname, vdef] of Object.entries(variables)) {
|
|
162
|
+
const currentValue = userVars[vname];
|
|
163
|
+
const hasValue = currentValue !== undefined;
|
|
164
|
+
const hasDefault = vdef.default !== undefined;
|
|
165
|
+
|
|
166
|
+
const icon = hasValue ? chalk.green("✓") : hasDefault ? chalk.blue("○") : vdef.required ? chalk.red("!") : chalk.gray("○");
|
|
167
|
+
const requiredStr = vdef.required && !hasValue ? chalk.red(" (required)") : "";
|
|
168
|
+
const envStr = vdef.env ? chalk.dim(` [${vdef.env}]`) : "";
|
|
169
|
+
|
|
170
|
+
console.log(`${icon} ${chalk.bold(vname)}${requiredStr}${envStr}`);
|
|
171
|
+
|
|
172
|
+
if (vdef.description) {
|
|
173
|
+
console.log(chalk.dim(` ${vdef.description}`));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(chalk.dim(` Type: ${vdef.type}`));
|
|
177
|
+
|
|
178
|
+
if (hasValue) {
|
|
179
|
+
console.log(` Value: ${formatValue(currentValue, vdef)}`);
|
|
180
|
+
} else if (hasDefault) {
|
|
181
|
+
console.log(` Default: ${formatValue(vdef.default, vdef)}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log();
|
|
186
|
+
console.log(chalk.dim(`Set a value: omp config ${name} <variable> <value>`));
|
|
187
|
+
console.log(chalk.dim(`Delete a value: omp config ${name} <variable> --delete`));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get a specific variable value
|
|
192
|
+
* omp config @oh-my-pi/exa apiKey
|
|
193
|
+
*/
|
|
194
|
+
export async function getConfig(name: string, key: string, options: ConfigOptions = {}): Promise<void> {
|
|
195
|
+
const isGlobal = resolveScope(options);
|
|
196
|
+
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
197
|
+
|
|
198
|
+
if (!pluginsJson.plugins[name]) {
|
|
199
|
+
console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const pkgJson = await readPluginPackageJson(name, isGlobal);
|
|
205
|
+
if (!pkgJson) {
|
|
206
|
+
console.log(chalk.red(`Could not read package.json for ${name}`));
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const allFeatureNames = Object.keys(pkgJson.omp?.features || {});
|
|
212
|
+
const config = pluginsJson.config?.[name];
|
|
213
|
+
const enabledFeatures = resolveEnabledFeatures(allFeatureNames, config?.features, pkgJson.omp?.features || {});
|
|
214
|
+
const variables = collectVariables(pkgJson, enabledFeatures);
|
|
215
|
+
|
|
216
|
+
const varDef = variables[key];
|
|
217
|
+
if (!varDef) {
|
|
218
|
+
console.log(chalk.red(`Unknown variable "${key}".`));
|
|
219
|
+
console.log(chalk.dim(`Available: ${Object.keys(variables).join(", ") || "(none)"}`));
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const userValue = config?.variables?.[key];
|
|
225
|
+
const value = userValue ?? varDef.default;
|
|
226
|
+
|
|
227
|
+
if (options.json) {
|
|
228
|
+
console.log(JSON.stringify({ plugin: name, variable: key, value, default: varDef.default }, null, 2));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (value !== undefined) {
|
|
233
|
+
console.log(formatValue(value, varDef));
|
|
234
|
+
} else {
|
|
235
|
+
console.log(chalk.dim("(not set)"));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Set a variable value
|
|
241
|
+
* omp config @oh-my-pi/exa apiKey sk-xxx
|
|
242
|
+
*/
|
|
243
|
+
export async function setConfig(name: string, key: string, value: string, options: ConfigOptions = {}): Promise<void> {
|
|
244
|
+
const isGlobal = resolveScope(options);
|
|
245
|
+
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
246
|
+
|
|
247
|
+
if (!pluginsJson.plugins[name]) {
|
|
248
|
+
console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const pkgJson = await readPluginPackageJson(name, isGlobal);
|
|
254
|
+
if (!pkgJson) {
|
|
255
|
+
console.log(chalk.red(`Could not read package.json for ${name}`));
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const allFeatureNames = Object.keys(pkgJson.omp?.features || {});
|
|
261
|
+
const config = pluginsJson.config?.[name];
|
|
262
|
+
const enabledFeatures = resolveEnabledFeatures(allFeatureNames, config?.features, pkgJson.omp?.features || {});
|
|
263
|
+
const variables = collectVariables(pkgJson, enabledFeatures);
|
|
264
|
+
|
|
265
|
+
const varDef = variables[key];
|
|
266
|
+
if (!varDef) {
|
|
267
|
+
console.log(chalk.red(`Unknown variable "${key}".`));
|
|
268
|
+
console.log(chalk.dim(`Available: ${Object.keys(variables).join(", ") || "(none)"}`));
|
|
269
|
+
process.exitCode = 1;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Parse and validate value
|
|
274
|
+
let parsed: string | number | boolean | string[];
|
|
275
|
+
try {
|
|
276
|
+
parsed = parseValue(value, varDef);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.log(chalk.red((err as Error).message));
|
|
279
|
+
process.exitCode = 1;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Update config
|
|
284
|
+
if (!pluginsJson.config) pluginsJson.config = {};
|
|
285
|
+
if (!pluginsJson.config[name]) pluginsJson.config[name] = {};
|
|
286
|
+
if (!pluginsJson.config[name].variables) pluginsJson.config[name].variables = {};
|
|
287
|
+
|
|
288
|
+
pluginsJson.config[name].variables[key] = parsed;
|
|
289
|
+
await savePluginsJson(pluginsJson, isGlobal);
|
|
290
|
+
|
|
291
|
+
console.log(chalk.green(`✓ Set ${name}.${key} = ${JSON.stringify(parsed)}`));
|
|
292
|
+
|
|
293
|
+
if (varDef.env) {
|
|
294
|
+
console.log(chalk.dim(` Environment variable: ${varDef.env}`));
|
|
295
|
+
console.log(chalk.dim(` Export with: omp env`));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (options.json) {
|
|
299
|
+
console.log(JSON.stringify({ plugin: name, variable: key, value: parsed }, null, 2));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Delete a variable override (revert to default)
|
|
305
|
+
* omp config @oh-my-pi/exa apiKey --delete
|
|
306
|
+
*/
|
|
307
|
+
export async function deleteConfig(name: string, key: string, options: ConfigOptions = {}): Promise<void> {
|
|
308
|
+
const isGlobal = resolveScope(options);
|
|
309
|
+
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
310
|
+
|
|
311
|
+
if (!pluginsJson.plugins[name]) {
|
|
312
|
+
console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
|
|
313
|
+
process.exitCode = 1;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const config = pluginsJson.config?.[name];
|
|
318
|
+
// Check key presence with hasOwnProperty, not truthiness (allows deleting falsy values like false, 0, "", [])
|
|
319
|
+
if (!config?.variables || !Object.prototype.hasOwnProperty.call(config.variables, key)) {
|
|
320
|
+
console.log(chalk.yellow(`Variable "${key}" is not set for ${name}.`));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
delete pluginsJson.config![name].variables![key];
|
|
325
|
+
|
|
326
|
+
// Clean up empty objects
|
|
327
|
+
if (Object.keys(pluginsJson.config![name].variables!).length === 0) {
|
|
328
|
+
delete pluginsJson.config![name].variables;
|
|
329
|
+
}
|
|
330
|
+
if (Object.keys(pluginsJson.config![name]).length === 0) {
|
|
331
|
+
delete pluginsJson.config![name];
|
|
332
|
+
}
|
|
333
|
+
if (Object.keys(pluginsJson.config!).length === 0) {
|
|
334
|
+
delete pluginsJson.config;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await savePluginsJson(pluginsJson, isGlobal);
|
|
338
|
+
|
|
339
|
+
console.log(chalk.green(`✓ Deleted ${name}.${key} (reverted to default)`));
|
|
340
|
+
|
|
341
|
+
if (options.json) {
|
|
342
|
+
console.log(JSON.stringify({ plugin: name, variable: key, deleted: true }, null, 2));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Main config command handler
|
|
348
|
+
* Routes to list, get, set, or delete based on arguments
|
|
349
|
+
*/
|
|
350
|
+
export async function configCommand(
|
|
351
|
+
name: string,
|
|
352
|
+
keyOrOptions?: string | ConfigOptions,
|
|
353
|
+
valueOrOptions?: string | ConfigOptions,
|
|
354
|
+
options: ConfigOptions = {},
|
|
355
|
+
): Promise<void> {
|
|
356
|
+
// Handle different argument patterns
|
|
357
|
+
let key: string | undefined;
|
|
358
|
+
let value: string | undefined;
|
|
359
|
+
let opts: ConfigOptions;
|
|
360
|
+
|
|
361
|
+
if (typeof keyOrOptions === "object") {
|
|
362
|
+
// omp config <name> [options]
|
|
363
|
+
opts = keyOrOptions;
|
|
364
|
+
} else if (typeof valueOrOptions === "object") {
|
|
365
|
+
// omp config <name> <key> [options]
|
|
366
|
+
key = keyOrOptions;
|
|
367
|
+
opts = valueOrOptions;
|
|
368
|
+
} else {
|
|
369
|
+
// omp config <name> <key> <value> [options]
|
|
370
|
+
key = keyOrOptions;
|
|
371
|
+
value = valueOrOptions;
|
|
372
|
+
opts = options;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!key) {
|
|
376
|
+
await listConfig(name, opts);
|
|
377
|
+
} else if (opts.delete) {
|
|
378
|
+
await deleteConfig(name, key, opts);
|
|
379
|
+
} else if (value !== undefined) {
|
|
380
|
+
await setConfig(name, key, value, opts);
|
|
381
|
+
} else {
|
|
382
|
+
await getConfig(name, key, opts);
|
|
383
|
+
}
|
|
384
|
+
}
|
package/src/commands/create.ts
CHANGED
|
@@ -8,16 +8,65 @@ export interface CreateOptions {
|
|
|
8
8
|
author?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const VALID_NPM_CHARS = new Set("abcdefghijklmnopqrstuvwxyz0123456789-_.");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validate that a name conforms to npm naming rules
|
|
15
|
+
*/
|
|
16
|
+
function isValidNpmName(name: string): boolean {
|
|
17
|
+
if (!name || name.length === 0) return false;
|
|
18
|
+
if (name.startsWith(".") || name.startsWith("_")) return false;
|
|
19
|
+
if (name.includes(" ")) return false;
|
|
20
|
+
for (const char of name) {
|
|
21
|
+
if (!VALID_NPM_CHARS.has(char)) return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize a string to be a valid npm package name
|
|
28
|
+
*/
|
|
29
|
+
function normalizePluginName(name: string): string {
|
|
30
|
+
let normalized = name.toLowerCase().split(" ").join("-");
|
|
31
|
+
|
|
32
|
+
// Remove invalid characters (keep alphanumeric, -, _, .)
|
|
33
|
+
normalized = Array.from(normalized)
|
|
34
|
+
.filter((char) => VALID_NPM_CHARS.has(char))
|
|
35
|
+
.join("");
|
|
36
|
+
|
|
37
|
+
// Can't start with . or _ or -
|
|
38
|
+
while (normalized.startsWith(".") || normalized.startsWith("_") || normalized.startsWith("-")) {
|
|
39
|
+
normalized = normalized.slice(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
|
|
11
45
|
/**
|
|
12
46
|
* Scaffold a new plugin from template
|
|
13
47
|
*/
|
|
14
48
|
export async function createPlugin(name: string, options: CreateOptions = {}): Promise<void> {
|
|
15
49
|
// Ensure name follows conventions
|
|
16
|
-
|
|
50
|
+
let pluginName = name.startsWith("omp-") ? name : `omp-${name}`;
|
|
51
|
+
|
|
52
|
+
// Validate and normalize the plugin name
|
|
53
|
+
if (!isValidNpmName(pluginName)) {
|
|
54
|
+
const normalized = normalizePluginName(pluginName);
|
|
55
|
+
if (!normalized || normalized === "omp-" || normalized === "omp") {
|
|
56
|
+
console.log(chalk.red(`Error: Invalid plugin name "${name}" cannot be normalized to a valid npm name`));
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Ensure omp- prefix after normalization
|
|
61
|
+
const finalName = normalized.startsWith("omp-") ? normalized : `omp-${normalized}`;
|
|
62
|
+
console.log(chalk.yellow(`Invalid plugin name. Normalized to: ${finalName}`));
|
|
63
|
+
pluginName = finalName;
|
|
64
|
+
}
|
|
17
65
|
const pluginDir = pluginName;
|
|
18
66
|
|
|
19
67
|
if (existsSync(pluginDir)) {
|
|
20
68
|
console.log(chalk.red(`Error: Directory ${pluginDir} already exists`));
|
|
69
|
+
process.exitCode = 1;
|
|
21
70
|
return;
|
|
22
71
|
}
|
|
23
72
|
|
|
@@ -149,5 +198,6 @@ Provide instructions for the agent here.
|
|
|
149
198
|
console.log(chalk.dim(" 5. Publish: npm publish"));
|
|
150
199
|
} catch (err) {
|
|
151
200
|
console.log(chalk.red(`Error creating plugin: ${(err as Error).message}`));
|
|
201
|
+
process.exitCode = 1;
|
|
152
202
|
}
|
|
153
203
|
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { detectAllConflicts, formatConflicts } from "@omp/conflicts";
|
|
3
|
+
import { getInstalledPlugins, loadPluginsJson, readPluginPackageJson, savePluginsJson } from "@omp/manifest";
|
|
4
|
+
import {
|
|
5
|
+
GLOBAL_PACKAGE_JSON,
|
|
6
|
+
NODE_MODULES_DIR,
|
|
7
|
+
PLUGINS_DIR,
|
|
8
|
+
PROJECT_NODE_MODULES,
|
|
9
|
+
PROJECT_PLUGINS_JSON,
|
|
10
|
+
resolveScope,
|
|
11
|
+
} from "@omp/paths";
|
|
12
|
+
import { checkPluginSymlinks, createPluginSymlinks } from "@omp/symlinks";
|
|
2
13
|
import chalk from "chalk";
|
|
3
|
-
import { detectAllConflicts, formatConflicts } from "../conflicts.js";
|
|
4
|
-
import { getInstalledPlugins, loadPluginsJson, readPluginPackageJson } from "../manifest.js";
|
|
5
|
-
import { GLOBAL_PACKAGE_JSON, NODE_MODULES_DIR, PLUGINS_DIR } from "../paths.js";
|
|
6
|
-
import { checkPluginSymlinks } from "../symlinks.js";
|
|
7
14
|
|
|
8
15
|
export interface DoctorOptions {
|
|
9
16
|
global?: boolean;
|
|
17
|
+
local?: boolean;
|
|
10
18
|
fix?: boolean;
|
|
11
19
|
json?: boolean;
|
|
12
20
|
}
|
|
@@ -22,7 +30,7 @@ interface DiagnosticResult {
|
|
|
22
30
|
* Run health checks on the plugin system
|
|
23
31
|
*/
|
|
24
32
|
export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
|
|
25
|
-
const isGlobal = options
|
|
33
|
+
const isGlobal = resolveScope(options);
|
|
26
34
|
const results: DiagnosticResult[] = [];
|
|
27
35
|
|
|
28
36
|
console.log(chalk.blue("Running health checks...\n"));
|
|
@@ -45,7 +53,7 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
|
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
// 2. Check package.json exists
|
|
48
|
-
const packageJsonPath = isGlobal ? GLOBAL_PACKAGE_JSON :
|
|
56
|
+
const packageJsonPath = isGlobal ? GLOBAL_PACKAGE_JSON : PROJECT_PLUGINS_JSON;
|
|
49
57
|
if (!existsSync(packageJsonPath)) {
|
|
50
58
|
results.push({
|
|
51
59
|
check: "Package manifest",
|
|
@@ -62,7 +70,7 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
|
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
// 3. Check node_modules exists
|
|
65
|
-
const nodeModules = isGlobal ? NODE_MODULES_DIR :
|
|
73
|
+
const nodeModules = isGlobal ? NODE_MODULES_DIR : PROJECT_NODE_MODULES;
|
|
66
74
|
if (!existsSync(nodeModules)) {
|
|
67
75
|
results.push({
|
|
68
76
|
check: "Node modules",
|
|
@@ -152,6 +160,38 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
|
|
|
152
160
|
});
|
|
153
161
|
}
|
|
154
162
|
|
|
163
|
+
// 7. Check for missing omp dependencies
|
|
164
|
+
const missingDeps: string[] = [];
|
|
165
|
+
for (const [name, pkgJson] of installedPlugins) {
|
|
166
|
+
if (pkgJson.dependencies) {
|
|
167
|
+
for (const depName of Object.keys(pkgJson.dependencies)) {
|
|
168
|
+
const depPkgJson = await readPluginPackageJson(depName, isGlobal);
|
|
169
|
+
if (!depPkgJson) {
|
|
170
|
+
// Dependency not found in node_modules
|
|
171
|
+
// Check if it's supposed to be an omp plugin by looking in the plugins manifest
|
|
172
|
+
if (pluginsJson.plugins[depName]) {
|
|
173
|
+
missingDeps.push(`${name} requires ${depName} (not in node_modules)`);
|
|
174
|
+
}
|
|
175
|
+
} else if (depPkgJson.omp?.install && depPkgJson.omp.install.length > 0) {
|
|
176
|
+
// Dependency is an omp plugin (has install entries) and is present - that's fine
|
|
177
|
+
// But check if it's registered in the plugins manifest
|
|
178
|
+
if (!pluginsJson.plugins[depName]) {
|
|
179
|
+
missingDeps.push(`${name} requires omp plugin ${depName} (installed but not in manifest)`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (missingDeps.length > 0) {
|
|
187
|
+
results.push({
|
|
188
|
+
check: "Missing omp dependencies",
|
|
189
|
+
status: "warning",
|
|
190
|
+
message: missingDeps.join("; "),
|
|
191
|
+
fix: isGlobal ? "Run: npm install in ~/.pi/plugins" : "Run: npm install in .pi",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
155
195
|
// Output results
|
|
156
196
|
if (options.json) {
|
|
157
197
|
console.log(JSON.stringify({ results }, null, 2));
|
|
@@ -194,6 +234,7 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
|
|
|
194
234
|
} else {
|
|
195
235
|
if (errors.length > 0) {
|
|
196
236
|
console.log(chalk.red(`${errors.length} error(s) found`));
|
|
237
|
+
process.exitCode = 1;
|
|
197
238
|
}
|
|
198
239
|
if (warnings.length > 0) {
|
|
199
240
|
console.log(chalk.yellow(`${warnings.length} warning(s) found`));
|
|
@@ -214,4 +255,51 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
|
|
|
214
255
|
console.log(chalk.dim(` - ${s}`));
|
|
215
256
|
}
|
|
216
257
|
}
|
|
258
|
+
|
|
259
|
+
// Apply fixes if --fix flag was passed
|
|
260
|
+
if (options.fix) {
|
|
261
|
+
let fixedAnything = false;
|
|
262
|
+
|
|
263
|
+
// Fix broken/missing symlinks by re-creating them
|
|
264
|
+
if (brokenSymlinks.length > 0 || missingSymlinks.length > 0) {
|
|
265
|
+
console.log(chalk.blue("\nAttempting to fix broken/missing symlinks..."));
|
|
266
|
+
for (const [name, pkgJson] of installedPlugins) {
|
|
267
|
+
const symlinkResult = await createPluginSymlinks(name, pkgJson, isGlobal, false);
|
|
268
|
+
if (symlinkResult.created.length > 0) {
|
|
269
|
+
fixedAnything = true;
|
|
270
|
+
console.log(chalk.green(` ✓ Re-created symlinks for ${name}`));
|
|
271
|
+
}
|
|
272
|
+
if (symlinkResult.errors.length > 0) {
|
|
273
|
+
for (const err of symlinkResult.errors) {
|
|
274
|
+
console.log(chalk.red(` ✗ ${name}: ${err}`));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Remove orphaned manifest entries
|
|
281
|
+
if (orphaned.length > 0) {
|
|
282
|
+
console.log(chalk.blue("\nRemoving orphaned entries from manifest..."));
|
|
283
|
+
for (const name of orphaned) {
|
|
284
|
+
delete pluginsJson.plugins[name];
|
|
285
|
+
console.log(chalk.green(` ✓ Removed ${name}`));
|
|
286
|
+
}
|
|
287
|
+
await savePluginsJson(pluginsJson, isGlobal);
|
|
288
|
+
fixedAnything = true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Conflicts cannot be auto-fixed
|
|
292
|
+
if (conflicts.length > 0) {
|
|
293
|
+
console.log(chalk.yellow("\nConflicts cannot be auto-fixed. Please resolve manually:"));
|
|
294
|
+
for (const conflict of formatConflicts(conflicts)) {
|
|
295
|
+
console.log(chalk.dim(` - ${conflict}`));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (fixedAnything) {
|
|
300
|
+
console.log(chalk.green("\n✓ Fixes applied. Run 'omp doctor' again to verify."));
|
|
301
|
+
} else if (conflicts.length === 0) {
|
|
302
|
+
console.log(chalk.dim("\nNo fixable issues found."));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
217
305
|
}
|