@neurodock/cli 0.1.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +125 -2
  2. package/LICENSE +657 -7
  3. package/README.md +148 -25
  4. package/dist/clients/claude-code.d.ts.map +1 -1
  5. package/dist/clients/claude-code.js +3 -1
  6. package/dist/clients/claude-code.js.map +1 -1
  7. package/dist/clients/claude-desktop.d.ts.map +1 -1
  8. package/dist/clients/claude-desktop.js +3 -1
  9. package/dist/clients/claude-desktop.js.map +1 -1
  10. package/dist/clients/cursor.d.ts.map +1 -1
  11. package/dist/clients/cursor.js +3 -1
  12. package/dist/clients/cursor.js.map +1 -1
  13. package/dist/clients/index.d.ts.map +1 -1
  14. package/dist/clients/index.js.map +1 -1
  15. package/dist/commands/doctor.d.ts.map +1 -1
  16. package/dist/commands/doctor.js +59 -4
  17. package/dist/commands/doctor.js.map +1 -1
  18. package/dist/commands/examples.d.ts +28 -0
  19. package/dist/commands/examples.d.ts.map +1 -0
  20. package/dist/commands/examples.js +168 -0
  21. package/dist/commands/examples.js.map +1 -0
  22. package/dist/commands/host.d.ts +11 -0
  23. package/dist/commands/host.d.ts.map +1 -0
  24. package/dist/commands/host.js +63 -0
  25. package/dist/commands/host.js.map +1 -0
  26. package/dist/commands/init.d.ts.map +1 -1
  27. package/dist/commands/init.js +14 -4
  28. package/dist/commands/init.js.map +1 -1
  29. package/dist/commands/install-all.d.ts +78 -0
  30. package/dist/commands/install-all.d.ts.map +1 -0
  31. package/dist/commands/install-all.js +283 -0
  32. package/dist/commands/install-all.js.map +1 -0
  33. package/dist/commands/plugin.d.ts +122 -0
  34. package/dist/commands/plugin.d.ts.map +1 -0
  35. package/dist/commands/plugin.js +545 -0
  36. package/dist/commands/plugin.js.map +1 -0
  37. package/dist/commands/profile.d.ts.map +1 -1
  38. package/dist/commands/profile.js +11 -2
  39. package/dist/commands/profile.js.map +1 -1
  40. package/dist/commands/uninstall.d.ts +36 -0
  41. package/dist/commands/uninstall.d.ts.map +1 -0
  42. package/dist/commands/uninstall.js +188 -0
  43. package/dist/commands/uninstall.js.map +1 -0
  44. package/dist/commands/update.d.ts +24 -0
  45. package/dist/commands/update.d.ts.map +1 -0
  46. package/dist/commands/update.js +179 -0
  47. package/dist/commands/update.js.map +1 -0
  48. package/dist/commands/validate.d.ts +24 -0
  49. package/dist/commands/validate.d.ts.map +1 -0
  50. package/dist/commands/validate.js +163 -0
  51. package/dist/commands/validate.js.map +1 -0
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +236 -6
  55. package/dist/index.js.map +1 -1
  56. package/dist/lib/json-patch.d.ts.map +1 -1
  57. package/dist/lib/json-patch.js +1 -1
  58. package/dist/lib/json-patch.js.map +1 -1
  59. package/dist/lib/mcp-entries.d.ts.map +1 -1
  60. package/dist/lib/mcp-entries.js +25 -3
  61. package/dist/lib/mcp-entries.js.map +1 -1
  62. package/dist/lib/paths.d.ts +10 -0
  63. package/dist/lib/paths.d.ts.map +1 -1
  64. package/dist/lib/paths.js +19 -2
  65. package/dist/lib/paths.js.map +1 -1
  66. package/dist/lib/plugin-schema.d.ts +12 -0
  67. package/dist/lib/plugin-schema.d.ts.map +1 -0
  68. package/dist/lib/plugin-schema.js +71 -0
  69. package/dist/lib/plugin-schema.js.map +1 -0
  70. package/dist/profile/defaults.d.ts.map +1 -1
  71. package/dist/profile/defaults.js +3 -1
  72. package/dist/profile/defaults.js.map +1 -1
  73. package/dist/profile/loader.d.ts.map +1 -1
  74. package/dist/profile/loader.js +3 -1
  75. package/dist/profile/loader.js.map +1 -1
  76. package/dist/profile/validator.d.ts.map +1 -1
  77. package/dist/profile/validator.js +5 -1
  78. package/dist/profile/validator.js.map +1 -1
  79. package/package.json +24 -4
@@ -0,0 +1,122 @@
1
+ import { readEnv } from "../lib/env.js";
2
+ import { type PluginValidationViolation } from "../lib/plugin-schema.js";
3
+ /**
4
+ * Marker file written by `neurodock plugin enable <name>` inside the
5
+ * plugin's install directory. Chosen over a central `plugins.yaml`
6
+ * registry because:
7
+ * - presence-on-disk is the single source of truth for both "is
8
+ * installed" and "is enabled"
9
+ * - filesystem-only state lets the substrate walk
10
+ * `<pluginsDir>/<name>/` exactly per ADR 0007 with no extra file
11
+ * to keep in sync
12
+ * - removing the plugin directory naturally removes the enable state
13
+ */
14
+ export declare const ENABLED_MARKER = ".enabled";
15
+ /**
16
+ * Minimum manifest shape the CLI reads. Forward-compat: unknown fields are
17
+ * preserved on disk (we never re-serialize the manifest), and any field we
18
+ * read but don't recognise is ignored without error.
19
+ */
20
+ export interface PluginManifest {
21
+ readonly name: string;
22
+ readonly type: string;
23
+ readonly version: string;
24
+ readonly description?: string;
25
+ }
26
+ export type ExitCode = 0 | 1 | 2 | 3;
27
+ export interface PluginDependencies {
28
+ readonly envOverrides?: Parameters<typeof readEnv>[0];
29
+ /**
30
+ * Prompt the user with a yes/no question. Tests can stub.
31
+ * Defaults to interactive `prompts` (cancellation returns false).
32
+ */
33
+ readonly confirm?: (message: string) => Promise<boolean>;
34
+ }
35
+ export interface PluginAddOptions {
36
+ readonly source: string;
37
+ readonly yes: boolean;
38
+ readonly dryRun: boolean;
39
+ readonly force: boolean;
40
+ }
41
+ export interface PluginAddResult {
42
+ readonly source: string;
43
+ readonly resolvedSource: string;
44
+ readonly destination: string;
45
+ readonly manifest: PluginManifest | null;
46
+ readonly action: "installed" | "overwritten" | "dry-run" | "aborted" | "fail";
47
+ readonly messages: ReadonlyArray<string>;
48
+ readonly violations: ReadonlyArray<PluginValidationViolation>;
49
+ readonly exitCode: ExitCode;
50
+ }
51
+ export declare function runPluginAdd(options: PluginAddOptions, deps?: PluginDependencies): Promise<PluginAddResult>;
52
+ export interface PluginRemoveOptions {
53
+ readonly name: string;
54
+ readonly yes: boolean;
55
+ readonly dryRun: boolean;
56
+ }
57
+ export interface PluginRemoveResult {
58
+ readonly name: string;
59
+ readonly destination: string;
60
+ readonly action: "removed" | "dry-run" | "aborted" | "missing";
61
+ readonly messages: ReadonlyArray<string>;
62
+ readonly exitCode: 0 | 1;
63
+ }
64
+ export declare function runPluginRemove(options: PluginRemoveOptions, deps?: PluginDependencies): Promise<PluginRemoveResult>;
65
+ export interface PluginListEntry {
66
+ readonly name: string;
67
+ readonly path: string;
68
+ readonly enabled: boolean;
69
+ readonly manifest: PluginManifest | null;
70
+ readonly invalid: boolean;
71
+ }
72
+ export interface PluginListOptions {
73
+ readonly json: boolean;
74
+ }
75
+ export interface PluginListResult {
76
+ readonly root: string;
77
+ readonly plugins: ReadonlyArray<PluginListEntry>;
78
+ readonly messages: ReadonlyArray<string>;
79
+ }
80
+ export declare function runPluginList(options: PluginListOptions, deps?: PluginDependencies): Promise<PluginListResult>;
81
+ export interface PluginEnableOptions {
82
+ readonly name: string;
83
+ }
84
+ export interface PluginEnableResult {
85
+ readonly name: string;
86
+ readonly destination: string;
87
+ readonly markerPath: string;
88
+ readonly action: "enabled" | "already-enabled" | "missing";
89
+ readonly messages: ReadonlyArray<string>;
90
+ readonly exitCode: 0 | 1;
91
+ }
92
+ export declare function runPluginEnable(options: PluginEnableOptions, deps?: PluginDependencies): Promise<PluginEnableResult>;
93
+ export interface PluginDisableOptions {
94
+ readonly name: string;
95
+ }
96
+ export interface PluginDisableResult {
97
+ readonly name: string;
98
+ readonly destination: string;
99
+ readonly markerPath: string;
100
+ readonly action: "disabled" | "already-disabled" | "missing";
101
+ readonly messages: ReadonlyArray<string>;
102
+ readonly exitCode: 0 | 1;
103
+ }
104
+ export declare function runPluginDisable(options: PluginDisableOptions, deps?: PluginDependencies): Promise<PluginDisableResult>;
105
+ export interface PluginValidateOptions {
106
+ readonly source: string;
107
+ readonly json: boolean;
108
+ }
109
+ export interface PluginValidateResult {
110
+ readonly source: string;
111
+ readonly resolvedSource: string;
112
+ readonly manifestPath: string;
113
+ readonly valid: boolean;
114
+ readonly missing: boolean;
115
+ readonly parseError?: string;
116
+ readonly violations: ReadonlyArray<PluginValidationViolation>;
117
+ readonly manifest: PluginManifest | null;
118
+ readonly messages: ReadonlyArray<string>;
119
+ readonly exitCode: 0 | 1 | 2;
120
+ }
121
+ export declare function runPluginValidate(options: PluginValidateOptions, deps?: PluginDependencies): Promise<PluginValidateResult>;
122
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/commands/plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,545 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import prompts from "prompts";
5
+ import { readEnv } from "../lib/env.js";
6
+ import { pluginsDir } from "../lib/paths.js";
7
+ import { validatePluginManifest, } from "../lib/plugin-schema.js";
8
+ /**
9
+ * Marker file written by `neurodock plugin enable <name>` inside the
10
+ * plugin's install directory. Chosen over a central `plugins.yaml`
11
+ * registry because:
12
+ * - presence-on-disk is the single source of truth for both "is
13
+ * installed" and "is enabled"
14
+ * - filesystem-only state lets the substrate walk
15
+ * `<pluginsDir>/<name>/` exactly per ADR 0007 with no extra file
16
+ * to keep in sync
17
+ * - removing the plugin directory naturally removes the enable state
18
+ */
19
+ export const ENABLED_MARKER = ".enabled";
20
+ export async function runPluginAdd(options, deps = {}) {
21
+ const env = readEnv(deps.envOverrides ?? {});
22
+ const messages = [];
23
+ const resolvedSource = isAbsolute(options.source)
24
+ ? options.source
25
+ : resolve(env.cwd, options.source);
26
+ // Validate source dir exists and is a directory.
27
+ if (!existsSync(resolvedSource)) {
28
+ messages.push(`Source path does not exist: ${resolvedSource}`);
29
+ return failResult("fail", options, resolvedSource, "", null, messages, [], 1);
30
+ }
31
+ const sourceStat = statSync(resolvedSource);
32
+ if (!sourceStat.isDirectory()) {
33
+ messages.push(`Source must be a directory: ${resolvedSource}`);
34
+ return failResult("fail", options, resolvedSource, "", null, messages, [], 1);
35
+ }
36
+ // Load + validate manifest.
37
+ const loaded = loadManifest(resolvedSource);
38
+ if (loaded.missing) {
39
+ messages.push(`No plugin.yaml in ${resolvedSource}`);
40
+ return failResult("fail", options, resolvedSource, "", null, messages, [], 1);
41
+ }
42
+ if (loaded.parseError !== undefined) {
43
+ messages.push(`Failed to parse plugin.yaml: ${loaded.parseError}`);
44
+ return failResult("fail", options, resolvedSource, "", null, messages, [], 3);
45
+ }
46
+ if (loaded.manifest === null) {
47
+ messages.push("plugin.yaml is invalid:");
48
+ for (const v of loaded.schemaViolations) {
49
+ messages.push(` ${v.path} (${v.keyword}): ${v.message}`);
50
+ }
51
+ return failResult("fail", options, resolvedSource, "", null, messages, loaded.schemaViolations, 3);
52
+ }
53
+ const manifest = loaded.manifest;
54
+ const destination = join(pluginsDir(env), manifest.name);
55
+ const alreadyInstalled = existsSync(destination);
56
+ if (alreadyInstalled && !options.force) {
57
+ if (options.yes) {
58
+ messages.push(`Plugin '${manifest.name}' is already installed at ${destination}.`);
59
+ messages.push("Re-run with --force to overwrite.");
60
+ return {
61
+ source: options.source,
62
+ resolvedSource,
63
+ destination,
64
+ manifest,
65
+ action: "aborted",
66
+ messages,
67
+ violations: [],
68
+ exitCode: 2,
69
+ };
70
+ }
71
+ const confirm = deps.confirm ?? defaultConfirm;
72
+ const ok = await confirm(`Plugin '${manifest.name}' is already installed. Overwrite ${destination}?`);
73
+ if (!ok) {
74
+ messages.push("Aborted. Existing install left in place.");
75
+ return {
76
+ source: options.source,
77
+ resolvedSource,
78
+ destination,
79
+ manifest,
80
+ action: "aborted",
81
+ messages,
82
+ violations: [],
83
+ exitCode: 2,
84
+ };
85
+ }
86
+ }
87
+ if (options.dryRun) {
88
+ messages.push(`Would install '${manifest.name}' (v${manifest.version}, type=${manifest.type})`);
89
+ messages.push(` from: ${resolvedSource}`);
90
+ messages.push(` to: ${destination}`);
91
+ if (alreadyInstalled) {
92
+ messages.push(" (would overwrite existing install)");
93
+ }
94
+ return {
95
+ source: options.source,
96
+ resolvedSource,
97
+ destination,
98
+ manifest,
99
+ action: "dry-run",
100
+ messages,
101
+ violations: [],
102
+ exitCode: 0,
103
+ };
104
+ }
105
+ // Perform install: ensure parent dir, remove existing if overwriting, copy.
106
+ mkdirSync(pluginsDir(env), { recursive: true });
107
+ if (alreadyInstalled) {
108
+ rmSync(destination, { recursive: true, force: true });
109
+ }
110
+ cpSync(resolvedSource, destination, { recursive: true });
111
+ const action = alreadyInstalled
112
+ ? "overwritten"
113
+ : "installed";
114
+ messages.push(`${alreadyInstalled ? "Overwrote" : "Installed"} plugin '${manifest.name}' (v${manifest.version}, type=${manifest.type})`);
115
+ messages.push(` to: ${destination}`);
116
+ messages.push("");
117
+ messages.push("Next: restart your MCP client to pick up the new plugin.");
118
+ messages.push("Run 'neurodock plugin enable " +
119
+ manifest.name +
120
+ "' to activate it (disabled by default).");
121
+ return {
122
+ source: options.source,
123
+ resolvedSource,
124
+ destination,
125
+ manifest,
126
+ action,
127
+ messages,
128
+ violations: [],
129
+ exitCode: 0,
130
+ };
131
+ }
132
+ function failResult(action, options, resolvedSource, destination, manifest, messages, violations, exitCode) {
133
+ return {
134
+ source: options.source,
135
+ resolvedSource,
136
+ destination,
137
+ manifest,
138
+ action,
139
+ messages,
140
+ violations,
141
+ exitCode,
142
+ };
143
+ }
144
+ export async function runPluginRemove(options, deps = {}) {
145
+ const env = readEnv(deps.envOverrides ?? {});
146
+ const messages = [];
147
+ const destination = join(pluginsDir(env), options.name);
148
+ if (!existsSync(destination)) {
149
+ messages.push(`Plugin '${options.name}' is not installed (no ${destination}).`);
150
+ return {
151
+ name: options.name,
152
+ destination,
153
+ action: "missing",
154
+ messages,
155
+ exitCode: 1,
156
+ };
157
+ }
158
+ if (options.dryRun) {
159
+ messages.push(`Would remove ${destination}`);
160
+ return {
161
+ name: options.name,
162
+ destination,
163
+ action: "dry-run",
164
+ messages,
165
+ exitCode: 0,
166
+ };
167
+ }
168
+ if (!options.yes) {
169
+ const confirm = deps.confirm ?? defaultConfirm;
170
+ const ok = await confirm(`Remove plugin '${options.name}' from ${destination}?`);
171
+ if (!ok) {
172
+ messages.push("Aborted. Plugin left in place.");
173
+ return {
174
+ name: options.name,
175
+ destination,
176
+ action: "aborted",
177
+ messages,
178
+ exitCode: 0,
179
+ };
180
+ }
181
+ }
182
+ rmSync(destination, { recursive: true, force: true });
183
+ messages.push(`Removed plugin '${options.name}' from ${destination}`);
184
+ return {
185
+ name: options.name,
186
+ destination,
187
+ action: "removed",
188
+ messages,
189
+ exitCode: 0,
190
+ };
191
+ }
192
+ export async function runPluginList(options, deps = {}) {
193
+ const env = readEnv(deps.envOverrides ?? {});
194
+ const root = pluginsDir(env);
195
+ const messages = [];
196
+ const plugins = [];
197
+ if (!existsSync(root)) {
198
+ if (options.json) {
199
+ messages.push(JSON.stringify({ root, plugins: [] }, null, 2));
200
+ }
201
+ else {
202
+ messages.push(`No plugins installed (no ${root}).`);
203
+ }
204
+ return { root, plugins, messages };
205
+ }
206
+ const entries = readdirSync(root, { withFileTypes: true })
207
+ .filter((e) => e.isDirectory())
208
+ .map((e) => e.name)
209
+ .sort();
210
+ for (const name of entries) {
211
+ const pluginPath = join(root, name);
212
+ const enabled = existsSync(join(pluginPath, ENABLED_MARKER));
213
+ const loaded = loadManifest(pluginPath);
214
+ plugins.push({
215
+ name,
216
+ path: pluginPath,
217
+ enabled,
218
+ manifest: loaded.manifest,
219
+ invalid: loaded.missing || loaded.manifest === null,
220
+ });
221
+ }
222
+ if (options.json) {
223
+ messages.push(JSON.stringify({
224
+ root,
225
+ plugins: plugins.map((p) => ({
226
+ name: p.name,
227
+ path: p.path,
228
+ enabled: p.enabled,
229
+ invalid: p.invalid,
230
+ ...(p.manifest
231
+ ? {
232
+ manifest: {
233
+ name: p.manifest.name,
234
+ type: p.manifest.type,
235
+ version: p.manifest.version,
236
+ },
237
+ }
238
+ : {}),
239
+ })),
240
+ }, null, 2));
241
+ return { root, plugins, messages };
242
+ }
243
+ if (plugins.length === 0) {
244
+ messages.push(`No plugins installed in ${root}.`);
245
+ return { root, plugins, messages };
246
+ }
247
+ messages.push(`Installed plugins in ${root}:`);
248
+ for (const p of plugins) {
249
+ const state = p.enabled ? "enabled " : "disabled";
250
+ const meta = p.manifest
251
+ ? `${p.manifest.type} v${p.manifest.version}`
252
+ : p.invalid
253
+ ? "(invalid manifest)"
254
+ : "(unknown)";
255
+ messages.push(` [${state}] ${p.name.padEnd(32)} ${meta}`);
256
+ }
257
+ return { root, plugins, messages };
258
+ }
259
+ export async function runPluginEnable(options, deps = {}) {
260
+ const env = readEnv(deps.envOverrides ?? {});
261
+ const messages = [];
262
+ const destination = join(pluginsDir(env), options.name);
263
+ const markerPath = join(destination, ENABLED_MARKER);
264
+ if (!existsSync(destination)) {
265
+ messages.push(`Plugin '${options.name}' is not installed (no ${destination}).`);
266
+ messages.push("Run 'neurodock plugin add <source>' first, then 'plugin enable'.");
267
+ return {
268
+ name: options.name,
269
+ destination,
270
+ markerPath,
271
+ action: "missing",
272
+ messages,
273
+ exitCode: 1,
274
+ };
275
+ }
276
+ if (existsSync(markerPath)) {
277
+ messages.push(`Plugin '${options.name}' is already enabled.`);
278
+ return {
279
+ name: options.name,
280
+ destination,
281
+ markerPath,
282
+ action: "already-enabled",
283
+ messages,
284
+ exitCode: 0,
285
+ };
286
+ }
287
+ writeFileSync(markerPath, `# Created by 'neurodock plugin enable'. Delete (or run\n# 'neurodock plugin disable ${options.name}') to deactivate.\n`, "utf8");
288
+ messages.push(`Enabled plugin '${options.name}'.`);
289
+ messages.push("Restart your MCP client to pick up the change.");
290
+ return {
291
+ name: options.name,
292
+ destination,
293
+ markerPath,
294
+ action: "enabled",
295
+ messages,
296
+ exitCode: 0,
297
+ };
298
+ }
299
+ export async function runPluginDisable(options, deps = {}) {
300
+ const env = readEnv(deps.envOverrides ?? {});
301
+ const messages = [];
302
+ const destination = join(pluginsDir(env), options.name);
303
+ const markerPath = join(destination, ENABLED_MARKER);
304
+ if (!existsSync(destination)) {
305
+ messages.push(`Plugin '${options.name}' is not installed (no ${destination}).`);
306
+ return {
307
+ name: options.name,
308
+ destination,
309
+ markerPath,
310
+ action: "missing",
311
+ messages,
312
+ exitCode: 1,
313
+ };
314
+ }
315
+ if (!existsSync(markerPath)) {
316
+ messages.push(`Plugin '${options.name}' is already disabled.`);
317
+ return {
318
+ name: options.name,
319
+ destination,
320
+ markerPath,
321
+ action: "already-disabled",
322
+ messages,
323
+ exitCode: 0,
324
+ };
325
+ }
326
+ rmSync(markerPath, { force: true });
327
+ messages.push(`Disabled plugin '${options.name}'.`);
328
+ messages.push("Restart your MCP client to pick up the change.");
329
+ return {
330
+ name: options.name,
331
+ destination,
332
+ markerPath,
333
+ action: "disabled",
334
+ messages,
335
+ exitCode: 0,
336
+ };
337
+ }
338
+ export async function runPluginValidate(options, deps = {}) {
339
+ const env = readEnv(deps.envOverrides ?? {});
340
+ const messages = [];
341
+ const resolvedSource = isAbsolute(options.source)
342
+ ? options.source
343
+ : resolve(env.cwd, options.source);
344
+ const manifestPath = join(resolvedSource, "plugin.yaml");
345
+ if (!existsSync(resolvedSource) || !statSync(resolvedSource).isDirectory()) {
346
+ if (options.json) {
347
+ messages.push(JSON.stringify({
348
+ valid: false,
349
+ missing: true,
350
+ manifest_path: manifestPath,
351
+ error: "source directory not found",
352
+ }, null, 2));
353
+ }
354
+ else {
355
+ messages.push(`Source directory not found: ${resolvedSource}`);
356
+ }
357
+ return {
358
+ source: options.source,
359
+ resolvedSource,
360
+ manifestPath,
361
+ valid: false,
362
+ missing: true,
363
+ violations: [],
364
+ manifest: null,
365
+ messages,
366
+ exitCode: 2,
367
+ };
368
+ }
369
+ const loaded = loadManifest(resolvedSource);
370
+ if (loaded.missing) {
371
+ if (options.json) {
372
+ messages.push(JSON.stringify({
373
+ valid: false,
374
+ missing: true,
375
+ manifest_path: manifestPath,
376
+ error: "plugin.yaml not found",
377
+ }, null, 2));
378
+ }
379
+ else {
380
+ messages.push(`Missing: ${manifestPath}`);
381
+ }
382
+ return {
383
+ source: options.source,
384
+ resolvedSource,
385
+ manifestPath,
386
+ valid: false,
387
+ missing: true,
388
+ violations: [],
389
+ manifest: null,
390
+ messages,
391
+ exitCode: 2,
392
+ };
393
+ }
394
+ if (loaded.parseError !== undefined) {
395
+ const violations = [
396
+ { path: "/", message: loaded.parseError, keyword: "parse" },
397
+ ];
398
+ if (options.json) {
399
+ messages.push(JSON.stringify({
400
+ valid: false,
401
+ missing: false,
402
+ manifest_path: manifestPath,
403
+ parse_error: loaded.parseError,
404
+ violations,
405
+ }, null, 2));
406
+ }
407
+ else {
408
+ messages.push(`Parse error in ${manifestPath}: ${loaded.parseError}`);
409
+ }
410
+ return {
411
+ source: options.source,
412
+ resolvedSource,
413
+ manifestPath,
414
+ valid: false,
415
+ missing: false,
416
+ parseError: loaded.parseError,
417
+ violations,
418
+ manifest: null,
419
+ messages,
420
+ exitCode: 1,
421
+ };
422
+ }
423
+ if (loaded.manifest === null) {
424
+ if (options.json) {
425
+ messages.push(JSON.stringify({
426
+ valid: false,
427
+ missing: false,
428
+ manifest_path: manifestPath,
429
+ violations: loaded.schemaViolations,
430
+ }, null, 2));
431
+ }
432
+ else {
433
+ messages.push(`Invalid: ${manifestPath}`);
434
+ for (const v of loaded.schemaViolations) {
435
+ messages.push(` ${v.path} (${v.keyword}): ${v.message}`);
436
+ }
437
+ }
438
+ return {
439
+ source: options.source,
440
+ resolvedSource,
441
+ manifestPath,
442
+ valid: false,
443
+ missing: false,
444
+ violations: loaded.schemaViolations,
445
+ manifest: null,
446
+ messages,
447
+ exitCode: 1,
448
+ };
449
+ }
450
+ if (options.json) {
451
+ messages.push(JSON.stringify({
452
+ valid: true,
453
+ missing: false,
454
+ manifest_path: manifestPath,
455
+ manifest: {
456
+ name: loaded.manifest.name,
457
+ type: loaded.manifest.type,
458
+ version: loaded.manifest.version,
459
+ },
460
+ }, null, 2));
461
+ }
462
+ else {
463
+ messages.push(`Valid: ${manifestPath}`);
464
+ messages.push(` name=${loaded.manifest.name} type=${loaded.manifest.type} version=${loaded.manifest.version}`);
465
+ }
466
+ return {
467
+ source: options.source,
468
+ resolvedSource,
469
+ manifestPath,
470
+ valid: true,
471
+ missing: false,
472
+ violations: [],
473
+ manifest: loaded.manifest,
474
+ messages,
475
+ exitCode: 0,
476
+ };
477
+ }
478
+ // ---------------------------------------------------------------------------
479
+ // shared helpers
480
+ // ---------------------------------------------------------------------------
481
+ function loadManifest(pluginDir) {
482
+ const manifestPath = join(pluginDir, "plugin.yaml");
483
+ if (!existsSync(manifestPath)) {
484
+ return {
485
+ raw: null,
486
+ manifest: null,
487
+ missing: true,
488
+ schemaViolations: [],
489
+ };
490
+ }
491
+ let raw;
492
+ try {
493
+ const text = readFileSync(manifestPath, "utf8");
494
+ raw = parseYaml(text);
495
+ }
496
+ catch (err) {
497
+ const message = err instanceof Error ? err.message : String(err);
498
+ return {
499
+ raw: null,
500
+ manifest: null,
501
+ missing: false,
502
+ parseError: message,
503
+ schemaViolations: [],
504
+ };
505
+ }
506
+ const result = validatePluginManifest(raw);
507
+ if (!result.valid) {
508
+ return {
509
+ raw,
510
+ manifest: null,
511
+ missing: false,
512
+ schemaViolations: result.violations,
513
+ };
514
+ }
515
+ // Schema-valid: extract the fields we use.
516
+ const obj = raw;
517
+ const manifest = {
518
+ name: String(obj["name"]),
519
+ type: String(obj["type"]),
520
+ version: String(obj["version"]),
521
+ ...(typeof obj["description"] === "string"
522
+ ? { description: obj["description"] }
523
+ : {}),
524
+ };
525
+ return {
526
+ raw,
527
+ manifest,
528
+ missing: false,
529
+ schemaViolations: [],
530
+ };
531
+ }
532
+ async function defaultConfirm(message) {
533
+ const response = await prompts({
534
+ type: "confirm",
535
+ name: "ok",
536
+ message,
537
+ initial: false,
538
+ }, {
539
+ onCancel: () => {
540
+ return false;
541
+ },
542
+ });
543
+ return response.ok === true;
544
+ }
545
+ //# sourceMappingURL=plugin.js.map