@hachej/boring-ui-plugin-cli 0.1.31 → 0.1.32

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.
@@ -1,565 +0,0 @@
1
- // src/server/index.ts
2
- import { join as join3, resolve as resolve3 } from "path";
3
-
4
- // src/server/scaffoldPlugin.ts
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
6
- import { dirname, join, resolve } from "path";
7
- import { fileURLToPath } from "url";
8
- var KEBAB_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
9
- function scaffoldPlugin(opts) {
10
- if (!KEBAB_RE.test(opts.name)) {
11
- throw new Error(
12
- `invalid plugin name "${opts.name}" \u2014 must be kebab-case (e.g. "my-plugin")`
13
- );
14
- }
15
- const workspaceRoot = resolve(opts.workspaceRoot);
16
- const pluginDir = join(workspaceRoot, ".pi", "extensions", opts.name);
17
- mkdirSync(join(workspaceRoot, ".pi", "extensions"), { recursive: true });
18
- try {
19
- mkdirSync(pluginDir, { recursive: false });
20
- } catch (error) {
21
- const code = error.code;
22
- if (code === "EEXIST") {
23
- throw new Error(`plugin already exists at ${pluginDir}`);
24
- }
25
- throw error;
26
- }
27
- const templatesDir = opts.templatesDir ?? resolveBundledTemplatesDir();
28
- if (!templatesDir) {
29
- throw new Error(
30
- "could not locate bundled plugin templates \u2014 pass `templatesDir` explicitly. (this usually indicates a broken @hachej/boring-ui-plugin-cli install)"
31
- );
32
- }
33
- const tplFront = join(templatesDir, "front-canonical.tsx");
34
- const tplPackage = join(templatesDir, "package-canonical.json");
35
- for (const tpl of [tplFront, tplPackage]) {
36
- if (!existsSync(tpl)) {
37
- throw new Error(`canonical template missing: ${tpl}`);
38
- }
39
- }
40
- const label = labelFromName(opts.name);
41
- const filesCreated = [];
42
- const write = (relPath, contents) => {
43
- const target = join(pluginDir, relPath);
44
- mkdirSync(join(target, ".."), { recursive: true });
45
- writeFileSync(target, contents, "utf8");
46
- filesCreated.push(target);
47
- };
48
- const pkgRaw = JSON.parse(readFileSync(tplPackage, "utf8"));
49
- delete pkgRaw._doc_;
50
- const pkgJson = substitute(JSON.stringify(pkgRaw, null, 2), opts.name, label);
51
- write("package.json", `${pkgJson}
52
- `);
53
- const frontSource = substitute(readFileSync(tplFront, "utf8"), opts.name, label);
54
- write("front/index.tsx", frontSource);
55
- write(".gitignore", "# Machine-managed sidecars written by the boring-ui plugin runtime.\n.boring-signature.json\n");
56
- return { pluginDir, filesCreated };
57
- }
58
- function labelFromName(name) {
59
- return name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
60
- }
61
- function substitute(source, name, label) {
62
- const paneName = `${pascalCase(name)}Pane`;
63
- return source.replaceAll("<kebab-name>", name).replaceAll("&lt;kebab-name&gt;", name).replaceAll("<Label>", label).replaceAll(/\bMyPane\b/g, paneName);
64
- }
65
- function pascalCase(name) {
66
- return name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
67
- }
68
- function resolveBundledTemplatesDir() {
69
- const here = dirname(fileURLToPath(import.meta.url));
70
- const candidates = [
71
- resolve(here, "..", "templates"),
72
- resolve(here, "..", "..", "templates")
73
- ];
74
- return candidates.find((candidate) => existsSync(candidate));
75
- }
76
-
77
- // src/server/verifyPlugin.ts
78
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, realpathSync, statSync } from "fs";
79
- import { isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
80
-
81
- // src/manifest.ts
82
- var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
83
- var PLUGIN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
84
- function isValidBoringPluginId(id) {
85
- return typeof id === "string" && id.length > 0 && PLUGIN_ID_RE.test(id);
86
- }
87
- function isSafePluginRelativePath(value) {
88
- return typeof value === "string" && value.length > 0 && value !== "." && !value.includes("\0") && !value.includes("\\") && !value.startsWith("/") && !value.startsWith("//") && !/^[A-Za-z]:[\\/]/.test(value) && !value.split("/").includes("..");
89
- }
90
- function issue(code, field, message) {
91
- return { code, field, message };
92
- }
93
- function isRecord(value) {
94
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
95
- }
96
- function validateStringArray(issues, value, field, pathLike) {
97
- if (value === void 0) return;
98
- if (!Array.isArray(value)) {
99
- issues.push(issue("INVALID_FIELD", field, `${field} must be an array`));
100
- return;
101
- }
102
- value.forEach((entry, index) => {
103
- const itemField = `${field}[${index}]`;
104
- if (typeof entry !== "string" || entry.length === 0) {
105
- issues.push(issue("INVALID_FIELD", itemField, `${itemField} must be a non-empty string`));
106
- return;
107
- }
108
- if (pathLike && !isSafePluginRelativePath(entry)) {
109
- issues.push(issue("INVALID_PATH", itemField, `${itemField} must be a safe relative path`));
110
- }
111
- });
112
- }
113
- var REMOVED_BORING_UI_FIELDS = ["outputs", "panels", "commands", "leftTabs", "surfaceResolvers", "providers", "bindings", "catalogs"];
114
- function validateBoringField(issues, boring) {
115
- if (boring === void 0) return void 0;
116
- if (!isRecord(boring)) {
117
- issues.push(issue("INVALID_FIELD", "boring", "boring must be an object when provided"));
118
- return void 0;
119
- }
120
- for (const field of REMOVED_BORING_UI_FIELDS) {
121
- if (boring[field] !== void 0) {
122
- issues.push(issue(
123
- "INVALID_FIELD",
124
- `boring.${field}`,
125
- `boring.${field} is not supported; declare front contributions in boring.front via definePlugin({ ... })`
126
- ));
127
- }
128
- }
129
- if (boring.id !== void 0 && (typeof boring.id !== "string" || !isValidBoringPluginId(boring.id))) {
130
- issues.push(issue("INVALID_ID", "boring.id", "boring.id must start with a letter or number and use only letters, numbers, dot, underscore, colon, or dash"));
131
- }
132
- const front = boring.front;
133
- if (front !== void 0 && (typeof front !== "string" || !isSafePluginRelativePath(front))) {
134
- issues.push(issue("INVALID_PATH", "boring.front", "boring.front must be a safe relative path"));
135
- }
136
- const server = boring.server;
137
- if (server !== void 0 && server !== false && (typeof server !== "string" || !isSafePluginRelativePath(server))) {
138
- issues.push(issue("INVALID_PATH", "boring.server", "boring.server must be a safe relative path or false"));
139
- }
140
- if (boring.label !== void 0 && typeof boring.label !== "string") {
141
- issues.push(issue("INVALID_FIELD", "boring.label", "boring.label must be a string when provided"));
142
- }
143
- return {
144
- ...typeof boring.id === "string" ? { id: boring.id } : {},
145
- ...typeof boring.front === "string" ? { front: boring.front } : {},
146
- ...typeof boring.server === "string" || boring.server === false ? { server: boring.server } : {},
147
- ...typeof boring.label === "string" ? { label: boring.label } : {}
148
- };
149
- }
150
- var REMOTE_PI_PACKAGE_PREFIXES = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
151
- function isRemotePiPackageSource(value) {
152
- return REMOTE_PI_PACKAGE_PREFIXES.some((prefix) => value.startsWith(prefix));
153
- }
154
- function isSafePiPackageSource(value) {
155
- if (value.length === 0) return false;
156
- if (isRemotePiPackageSource(value)) return true;
157
- const path = value.startsWith("file:") ? value.slice("file:".length) : value;
158
- if (path === "." || path === "./") return true;
159
- const normalized = path.startsWith("./") ? path.slice(2) : path;
160
- return isSafePluginRelativePath(normalized);
161
- }
162
- function validatePiPackages(issues, value) {
163
- if (value === void 0) return;
164
- if (!Array.isArray(value)) {
165
- issues.push(issue("INVALID_FIELD", "pi.packages", "pi.packages must be an array when provided"));
166
- return;
167
- }
168
- value.forEach((entry, index) => {
169
- const field = `pi.packages[${index}]`;
170
- if (typeof entry === "string") {
171
- if (!isSafePiPackageSource(entry)) {
172
- issues.push(issue("INVALID_PATH", field, `${field} must be a safe package source`));
173
- }
174
- return;
175
- }
176
- if (!isRecord(entry)) {
177
- issues.push(issue("INVALID_FIELD", field, `${field} must be a string or package source object`));
178
- return;
179
- }
180
- if (typeof entry.source !== "string" || entry.source.length === 0) {
181
- issues.push(issue("INVALID_FIELD", `${field}.source`, `${field}.source must be a non-empty string`));
182
- } else if (!isSafePiPackageSource(entry.source)) {
183
- issues.push(issue("INVALID_PATH", `${field}.source`, `${field}.source must be a safe package source`));
184
- }
185
- });
186
- }
187
- function validatePiField(issues, pi) {
188
- if (pi === void 0) return void 0;
189
- if (!isRecord(pi)) {
190
- issues.push(issue("INVALID_FIELD", "pi", "pi must be an object when provided"));
191
- return void 0;
192
- }
193
- validateStringArray(issues, pi.extensions, "pi.extensions", true);
194
- validateStringArray(issues, pi.skills, "pi.skills", true);
195
- validatePiPackages(issues, pi.packages);
196
- if (pi.systemPrompt !== void 0 && typeof pi.systemPrompt !== "string") {
197
- issues.push(issue("INVALID_FIELD", "pi.systemPrompt", "pi.systemPrompt must be a string when provided"));
198
- }
199
- return pi;
200
- }
201
- function validateBoringPluginManifest(raw) {
202
- const issues = [];
203
- if (!isRecord(raw)) {
204
- return {
205
- valid: false,
206
- issues: [issue("INVALID_FIELD", "<root>", "package.json manifest must be an object")]
207
- };
208
- }
209
- if (raw.name !== void 0 && typeof raw.name !== "string") {
210
- issues.push(issue("INVALID_FIELD", "name", "name must be a string when provided"));
211
- }
212
- if (raw.version !== void 0 && typeof raw.version !== "string") {
213
- issues.push(issue("INVALID_VERSION", "version", "version must be a string when provided"));
214
- } else if (typeof raw.version === "string" && raw.version.length > 0 && !SEMVER_RE.test(raw.version)) {
215
- issues.push(issue("INVALID_VERSION", "version", "version must be a valid semver string"));
216
- }
217
- const boring = validateBoringField(issues, raw.boring);
218
- const pi = validatePiField(issues, raw.pi);
219
- if (!boring && !pi) {
220
- issues.push(issue("MISSING_REQUIRED_FIELD", "boring|pi", "package.json must include boring and/or pi plugin metadata"));
221
- }
222
- if (issues.length > 0) return { valid: false, issues };
223
- return {
224
- valid: true,
225
- packageJson: {
226
- ...typeof raw.name === "string" ? { name: raw.name } : {},
227
- ...typeof raw.version === "string" ? { version: raw.version } : {},
228
- ...boring ? { boring } : {},
229
- ...pi ? { pi } : {}
230
- }
231
- };
232
- }
233
-
234
- // src/server/verifyPlugin.ts
235
- function pluginFileSignature(path) {
236
- if (!path || !existsSync2(path)) return "missing";
237
- const stat = statSync(path);
238
- return `${stat.mtimeMs}:${stat.size}`;
239
- }
240
- function readPluginSignatureCache(pluginRootDir) {
241
- const path = join2(pluginRootDir, ".boring-signature.json");
242
- if (!existsSync2(path)) return null;
243
- let parsed;
244
- try {
245
- parsed = JSON.parse(readFileSync2(path, "utf8"));
246
- } catch {
247
- return null;
248
- }
249
- if (!parsed || typeof parsed !== "object") return null;
250
- const obj = parsed;
251
- if (obj.version !== 1) return null;
252
- const sig = obj.serverSignature;
253
- if (sig !== null && typeof sig !== "string") return null;
254
- const loadedAt = typeof obj.loadedAt === "number" ? obj.loadedAt : 0;
255
- return { version: 1, serverSignature: sig, loadedAt };
256
- }
257
- function verifyPlugin(opts) {
258
- const workspaceRoot = resolve2(opts.workspaceRoot);
259
- const extensionsDir = join2(workspaceRoot, ".pi", "extensions");
260
- if (!existsSync2(extensionsDir)) {
261
- return {
262
- outcomes: [],
263
- ok: true,
264
- extensionsDir,
265
- extensionsDirMissing: true
266
- };
267
- }
268
- const targets = [];
269
- if (opts.name) {
270
- const dir = join2(extensionsDir, opts.name);
271
- if (!existsSync2(dir)) {
272
- return {
273
- outcomes: [
274
- { id: opts.name, dir, ok: false, errors: [`plugin directory not found: ${dir}`], warnings: [] }
275
- ],
276
- ok: false,
277
- extensionsDir,
278
- extensionsDirMissing: false
279
- };
280
- }
281
- targets.push(dir);
282
- } else {
283
- for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
284
- if (!entry.isDirectory()) continue;
285
- if (entry.name.startsWith(".") || entry.name.startsWith("preflight-")) continue;
286
- targets.push(join2(extensionsDir, entry.name));
287
- }
288
- }
289
- const outcomes = targets.map((dir) => verifySinglePlugin(dir));
290
- return {
291
- outcomes,
292
- ok: outcomes.every((o) => o.ok),
293
- extensionsDir,
294
- extensionsDirMissing: false
295
- };
296
- }
297
- function isInsideRoot(rootReal, targetReal) {
298
- const rel = relative(rootReal, targetReal);
299
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
300
- }
301
- function resolveExistingContainedPath(pluginDir, value, field) {
302
- if (!isSafePluginRelativePath(value)) {
303
- return { path: null, error: `${field} must be a safe relative path inside the plugin root: ${JSON.stringify(value)}` };
304
- }
305
- const abs = resolve2(pluginDir, value);
306
- if (!existsSync2(abs)) return { path: null, error: `${field} points at "${value}" but that file does not exist (looked at ${abs})` };
307
- const rootReal = realpathSync(pluginDir);
308
- const targetReal = realpathSync(abs);
309
- if (!isInsideRoot(rootReal, targetReal)) {
310
- return { path: null, error: `${field} points at "${value}" but resolves outside the plugin root (looked at ${abs})` };
311
- }
312
- return { path: targetReal };
313
- }
314
- function verifySinglePlugin(pluginDir) {
315
- const id = pluginDir.split(/[\\/]/).pop() ?? "<unknown>";
316
- const errors = [];
317
- const warnings = [];
318
- const pkgPath = join2(pluginDir, "package.json");
319
- if (!existsSync2(pkgPath)) {
320
- return { id, dir: pluginDir, ok: false, errors: ["package.json missing"], warnings };
321
- }
322
- let parsed;
323
- try {
324
- parsed = JSON.parse(readFileSync2(pkgPath, "utf8"));
325
- } catch (error) {
326
- const message = error instanceof Error ? error.message : String(error);
327
- return { id, dir: pluginDir, ok: false, errors: [`package.json is not valid JSON: ${message}`], warnings };
328
- }
329
- const result = validateBoringPluginManifest(parsed);
330
- if (!result.valid) {
331
- for (const issue2 of result.issues) errors.push(formatIssue(issue2));
332
- return { id, dir: pluginDir, ok: false, errors, warnings };
333
- }
334
- const manifest = result.packageJson;
335
- const boring = manifest.boring;
336
- if (boring?.front) {
337
- const resolved = resolveExistingContainedPath(pluginDir, boring.front, "boring.front");
338
- if (resolved.error) errors.push(resolved.error);
339
- }
340
- let serverPathAbs = null;
341
- if (typeof boring?.server === "string") {
342
- const resolved = resolveExistingContainedPath(pluginDir, boring.server, "boring.server");
343
- if (resolved.error) {
344
- errors.push(resolved.error);
345
- } else {
346
- serverPathAbs = resolved.path;
347
- warnings.push(
348
- "boring.server file is valid, but workspace server entries are boot-time/static composition only. /reload does not import or register .pi/extensions server routes/agentTools; restart the workspace with this package passed via defaultPluginPackages or explicit plugins to activate server code."
349
- );
350
- }
351
- }
352
- const cache = readPluginSignatureCache(pluginDir);
353
- if (cache) {
354
- const currentServerSig = serverPathAbs ? pluginFileSignature(serverPathAbs) : null;
355
- const cachedSig = cache.serverSignature;
356
- if (cachedSig !== currentServerSig) {
357
- warnings.push(
358
- "server file changed since the workspace last loaded this plugin \u2014 /reload will hot-swap the front, but routes and agent tools stay on the previously loaded code. Stop and restart the workspace process (Ctrl-C, then re-run your dev command) to pick up the new code."
359
- );
360
- }
361
- }
362
- const piExt = manifest.pi?.extensions;
363
- if (Array.isArray(piExt)) {
364
- for (const ext of piExt) {
365
- if (typeof ext !== "string") continue;
366
- const resolved = resolveExistingContainedPath(pluginDir, ext, "pi.extensions entry");
367
- if (resolved.error) errors.push(resolved.error);
368
- }
369
- }
370
- const piSkills = manifest.pi?.skills;
371
- if (Array.isArray(piSkills)) {
372
- for (const skill of piSkills) {
373
- if (typeof skill !== "string") continue;
374
- const resolved = resolveExistingContainedPath(pluginDir, skill, "pi.skills entry");
375
- if (resolved.error) errors.push(resolved.error);
376
- }
377
- }
378
- return { id, dir: pluginDir, ok: errors.length === 0, errors, warnings };
379
- }
380
- function formatIssue(issue2) {
381
- return `${issue2.code}: ${issue2.field}: ${issue2.message}`;
382
- }
383
- function formatVerifyResult(result) {
384
- if (result.extensionsDirMissing) {
385
- return [
386
- `WARNING \u2014 no plugins to verify. Scanned: ${result.extensionsDir} (directory does not exist).`,
387
- "",
388
- "If you just wrote a plugin: check that you put it under THIS workspace's `.pi/extensions/` and not a different cwd. The scanned path above is `<cwd>/.pi/extensions/`."
389
- ].join("\n");
390
- }
391
- if (result.outcomes.length === 0) {
392
- return [
393
- `WARNING \u2014 scanned ${result.extensionsDir} but found NO plugin directories.`,
394
- "",
395
- "If you just wrote a plugin: it is NOT in this dir. Either you wrote to a different `.pi/extensions/` (check your cwd), or the dir name starts with `.` / `preflight-` (skipped)."
396
- ].join("\n");
397
- }
398
- const lines = [];
399
- const failures = result.outcomes.filter((o) => !o.ok);
400
- const withWarnings = result.outcomes.filter((o) => o.warnings.length > 0);
401
- if (failures.length === 0) {
402
- lines.push(`OK \u2014 ${result.outcomes.length} plugin(s) have valid manifests + present files. (scanned ${result.extensionsDir})`);
403
- for (const outcome of result.outcomes) {
404
- const tag = outcome.warnings.length > 0 ? "\u26A0" : "\u2713";
405
- lines.push(` ${tag} ${outcome.id}`);
406
- for (const w of outcome.warnings) {
407
- lines.push(` WARN: ${w}`);
408
- }
409
- }
410
- lines.push("");
411
- if (withWarnings.length > 0) {
412
- const n = withWarnings.length;
413
- lines.push(`Manifests are valid, but ${n} plugin${n === 1 ? "" : "s"} need a workspace restart (NOT just /reload) to pick up server-side changes \u2014 see WARN lines above.`);
414
- lines.push("");
415
- }
416
- lines.push("Note: this validator does NOT execute plugin code. Front/Pi syntax / type / runtime errors surface on /reload; boring.server files require static composition plus a process restart.");
417
- } else {
418
- lines.push(`FAILED \u2014 ${failures.length} of ${result.outcomes.length} plugin(s) have errors. (scanned ${result.extensionsDir})`);
419
- lines.push("");
420
- for (const outcome of result.outcomes) {
421
- if (outcome.ok) {
422
- const tag = outcome.warnings.length > 0 ? "\u26A0" : "\u2713";
423
- lines.push(` ${tag} ${outcome.id}`);
424
- for (const w of outcome.warnings) {
425
- lines.push(` WARN: ${w}`);
426
- }
427
- } else {
428
- lines.push(` \u2717 ${outcome.id} (${outcome.dir})`);
429
- for (const err of outcome.errors) {
430
- lines.push(` ${err}`);
431
- }
432
- for (const w of outcome.warnings) {
433
- lines.push(` WARN: ${w}`);
434
- }
435
- }
436
- }
437
- lines.push("");
438
- lines.push("Fix the errors above and run `boring-ui-plugin verify` again. Once it reports OK, ask the user to run /reload for front/Pi assets; boring.server changes require static composition plus restart.");
439
- }
440
- return lines.join("\n");
441
- }
442
- var COMMON_MISTAKE_HINTS = [
443
- {
444
- pattern: /boring\.server must be a safe relative path or false/i,
445
- hint: 'Set `boring.server` to a string path like "server/index.ts", or to `false` (or omit the field). It is NOT a boolean true. For hot-reloadable agent behavior in .pi/extensions, prefer pi.extensions instead.'
446
- },
447
- {
448
- pattern: /package\.json manifest must be an object/i,
449
- hint: "package.json must parse as a JSON object \u2014 check for trailing commas or unquoted keys."
450
- }
451
- ];
452
- function findHintForError(message) {
453
- for (const m of COMMON_MISTAKE_HINTS) {
454
- if (m.pattern.test(message)) return m.hint;
455
- }
456
- return void 0;
457
- }
458
-
459
- // src/server/index.ts
460
- function defaultWorkspaceRoot() {
461
- return process.env.BORING_AGENT_WORKSPACE_ROOT ?? process.cwd();
462
- }
463
- function workspaceLocalPluginRootsEnabled() {
464
- const raw = process.env.BORING_AGENT_WORKSPACE_LOCAL_PLUGIN_ROOTS;
465
- if (raw == null || raw.trim() === "") return true;
466
- return !["0", "false", "no", "off"].includes(raw.trim().toLowerCase());
467
- }
468
- function buildPluginStatus(workspaceRoot = defaultWorkspaceRoot()) {
469
- const resolvedRoot = resolve3(workspaceRoot);
470
- const enabled = workspaceLocalPluginRootsEnabled();
471
- return {
472
- workspaceLocalPluginRoots: enabled,
473
- workspaceRoot: resolvedRoot,
474
- extensionsDir: join3(resolvedRoot, ".pi", "extensions"),
475
- reloadSupported: enabled,
476
- ...enabled ? {} : {
477
- reason: "This runtime writes to a remote sandbox; host-side plugin discovery cannot load .pi/extensions from there."
478
- }
479
- };
480
- }
481
- function parseVerifyArgs(positionals, workspaceRoot = defaultWorkspaceRoot()) {
482
- const maybeName = positionals[0];
483
- const maybeWorkspace = positionals[1];
484
- const looksLikePath = maybeName && (maybeName.includes("/") || maybeName.startsWith("."));
485
- const name = looksLikePath ? void 0 : maybeName;
486
- return {
487
- ...name ? { name } : {},
488
- workspaceRoot: resolve3(maybeWorkspace ?? (looksLikePath ? maybeName : workspaceRoot))
489
- };
490
- }
491
- function parseScaffoldArgs(positionals, workspaceRoot = defaultWorkspaceRoot()) {
492
- const name = positionals[0];
493
- if (!name) throw new Error("usage: boring-ui-plugin scaffold <name> [workspace]");
494
- return { name, workspaceRoot: resolve3(positionals[1] ?? workspaceRoot) };
495
- }
496
-
497
- // src/index.ts
498
- function pluginCommandUsage() {
499
- return [
500
- "usage: boring-ui-plugin <command>",
501
- "",
502
- "Commands:",
503
- " boring-ui-plugin status [--json]",
504
- " boring-ui-plugin scaffold <name> [workspace]",
505
- " boring-ui-plugin verify [name] [workspace]"
506
- ].join("\n");
507
- }
508
- function handleStatus(json) {
509
- const status = buildPluginStatus();
510
- if (json) {
511
- console.log(JSON.stringify(status, null, 2));
512
- return;
513
- }
514
- console.log(status.workspaceLocalPluginRoots ? `workspace-local plugin roots enabled: ${status.extensionsDir}` : `workspace-local plugin roots disabled: ${status.reason}`);
515
- }
516
- function handleScaffold(positionals) {
517
- const args = parseScaffoldArgs(positionals);
518
- const status = buildPluginStatus(args.workspaceRoot);
519
- if (!workspaceLocalPluginRootsEnabled()) {
520
- throw new Error(`${status.reason} Do not scaffold into .pi/extensions in this runtime.`);
521
- }
522
- const result = scaffoldPlugin(args);
523
- console.log(`scaffolded ${args.name}`);
524
- console.log(` dir ${result.pluginDir}`);
525
- for (const file of result.filesCreated) console.log(` + ${file}`);
526
- console.log("");
527
- console.log("Next steps:");
528
- console.log(" 1. edit front/index.tsx for UI panels/commands/resolvers");
529
- console.log(" 2. add pi.extensions / skills for hot-reloadable agent behavior");
530
- console.log(" 3. bash `boring-ui-plugin verify` \u2014 confirms manifests + files are valid");
531
- console.log(" 4. ask the user: /reload");
532
- }
533
- function handleVerify(positionals) {
534
- const result = verifyPlugin(parseVerifyArgs(positionals));
535
- console.log(formatVerifyResult(result));
536
- if (result.ok) return;
537
- const hints = [];
538
- for (const outcome of result.outcomes) {
539
- for (const err of outcome.errors) {
540
- const hint = findHintForError(err);
541
- if (hint) hints.push(` hint (${outcome.id}): ${hint}`);
542
- }
543
- }
544
- if (hints.length > 0) {
545
- console.log("");
546
- console.log("Suggestions:");
547
- for (const hint of hints) console.log(hint);
548
- }
549
- process.exit(1);
550
- }
551
- function runBoringUiPluginCli(argv = process.argv.slice(2)) {
552
- const positionals = argv.filter((arg) => !arg.startsWith("-"));
553
- const json = argv.includes("--json");
554
- const command = positionals[0];
555
- const rest = positionals.slice(1);
556
- if (command === "status") return handleStatus(json);
557
- if (command === "scaffold") return handleScaffold(rest);
558
- if (command === "verify") return handleVerify(rest);
559
- console.log(pluginCommandUsage());
560
- }
561
-
562
- export {
563
- pluginCommandUsage,
564
- runBoringUiPluginCli
565
- };