@hachej/boring-ui-cli 0.1.17 → 0.1.20
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/README.md +8 -0
- package/dist/server/cli.js +189 -8
- package/dist/server/scaffoldPlugin.js +72 -0
- package/dist/server/verifyPlugin.js +235 -0
- package/package.json +6 -5
- package/public/assets/{CodeEditor-DQqOn4xz-B6a7H7rM.js → CodeEditor-DQqOn4xz-BpvvkFT7.js} +1 -1
- package/public/assets/{FileTree-Dvaud3jU-DNCQ4FDl.js → FileTree-DHVB9rpk-FMcsf2w-.js} +10 -10
- package/public/assets/{MarkdownEditor-sLkqTXDj-lFnLm1Im.js → MarkdownEditor-L1KDH0bM-D-LbfGkW.js} +1 -1
- package/public/assets/{_baseUniq-DdPJOgPg.js → _baseUniq-CMA9Ia5A.js} +1 -1
- package/public/assets/{arc-LgyX0a8W.js → arc-tnXyejqK.js} +1 -1
- package/public/assets/{architectureDiagram-Q4EWVU46-GZeUAJwF.js → architectureDiagram-Q4EWVU46-BSOUkwG2.js} +1 -1
- package/public/assets/{blockDiagram-DXYQGD6D-OSYK-_zS.js → blockDiagram-DXYQGD6D-beerpyJi.js} +1 -1
- package/public/assets/{c4Diagram-AHTNJAMY-BCBP7uHE.js → c4Diagram-AHTNJAMY-uB419B41.js} +1 -1
- package/public/assets/channel-C2_9gQqR.js +1 -0
- package/public/assets/{chunk-4BX2VUAB-CRy_H01e.js → chunk-4BX2VUAB-CpStyzoR.js} +1 -1
- package/public/assets/{chunk-4TB4RGXK-Dpk7ncuw.js → chunk-4TB4RGXK-BL531Lcw.js} +1 -1
- package/public/assets/{chunk-55IACEB6-CZYv6OBA.js → chunk-55IACEB6-CRDe66a9.js} +1 -1
- package/public/assets/{chunk-EDXVE4YY-NqKsVs05.js → chunk-EDXVE4YY-NaBO_txy.js} +1 -1
- package/public/assets/{chunk-FMBD7UC4-BgIQ6c_c.js → chunk-FMBD7UC4-Dn2Fmb5W.js} +1 -1
- package/public/assets/{chunk-OYMX7WX6-SyRoLN54.js → chunk-OYMX7WX6-iffOaivX.js} +1 -1
- package/public/assets/{chunk-QZHKN3VN-DzGRRm9f.js → chunk-QZHKN3VN-BKRi-Sxw.js} +1 -1
- package/public/assets/{chunk-YZCP3GAM-CfIqY3Yu.js → chunk-YZCP3GAM-CFQPe6Lu.js} +1 -1
- package/public/assets/classDiagram-6PBFFD2Q-BX66CTk2.js +1 -0
- package/public/assets/classDiagram-v2-HSJHXN6E-BX66CTk2.js +1 -0
- package/public/assets/clone-BUVN29h2.js +1 -0
- package/public/assets/{cose-bilkent-S5V4N54A-DTcdWPD5.js → cose-bilkent-S5V4N54A-BFfdAG94.js} +1 -1
- package/public/assets/{dagre-KV5264BT-k2lWP3qf.js → dagre-KV5264BT-BvfHllE1.js} +1 -1
- package/public/assets/{diagram-5BDNPKRD-DTUoxviQ.js → diagram-5BDNPKRD-DML8a0lh.js} +1 -1
- package/public/assets/{diagram-G4DWMVQ6-CLo507Hm.js → diagram-G4DWMVQ6-DL30StUK.js} +1 -1
- package/public/assets/{diagram-MMDJMWI5-Ben6XGgC.js → diagram-MMDJMWI5-D3-Kryyd.js} +1 -1
- package/public/assets/{diagram-TYMM5635-BsJA5XC_.js → diagram-TYMM5635-HDTViX5R.js} +1 -1
- package/public/assets/{erDiagram-SMLLAGMA-BLbGPA6K.js → erDiagram-SMLLAGMA-DV46TbuX.js} +1 -1
- package/public/assets/{flowDiagram-DWJPFMVM-D4Sv719o.js → flowDiagram-DWJPFMVM-BXqQu2_i.js} +1 -1
- package/public/assets/{ganttDiagram-T4ZO3ILL-D6kGugJe.js → ganttDiagram-T4ZO3ILL-CnPz0E5a.js} +1 -1
- package/public/assets/{gitGraphDiagram-UUTBAWPF-CVyzhaSE.js → gitGraphDiagram-UUTBAWPF-C25lUVfz.js} +1 -1
- package/public/assets/{graph-kiXKPMn9.js → graph-CSby6HJp.js} +1 -1
- package/public/assets/{highlighted-body-OFNGDK62-DcMPgvKJ.js → highlighted-body-OFNGDK62-D5Bucj_m.js} +1 -1
- package/public/assets/{index-BHlQtZ6_.js → index-CrQ5eYxl.js} +301 -295
- package/public/assets/{index-BTJucb1M.js → index-DLyovEn7.js} +1 -1
- package/public/assets/index-RUcotPWN.css +1 -0
- package/public/assets/{infoDiagram-42DDH7IO-BH27HU_c.js → infoDiagram-42DDH7IO-D1tkIwny.js} +1 -1
- package/public/assets/{ishikawaDiagram-UXIWVN3A-CdGOupZp.js → ishikawaDiagram-UXIWVN3A-D6zbyxWx.js} +1 -1
- package/public/assets/{journeyDiagram-VCZTEJTY-iJT59osB.js → journeyDiagram-VCZTEJTY-ExQUgQxi.js} +1 -1
- package/public/assets/{kanban-definition-6JOO6SKY-CZ5wWUdF.js → kanban-definition-6JOO6SKY-E3gNRZLm.js} +1 -1
- package/public/assets/{layout-BTIHgp6K.js → layout-BF96CGik.js} +1 -1
- package/public/assets/{linear-CCrC51HM.js → linear-6x4QueCv.js} +1 -1
- package/public/assets/{min-CrbJ9M-f.js → min-Cyn_UaFw.js} +1 -1
- package/public/assets/{mindmap-definition-QFDTVHPH-diXcpv4F.js → mindmap-definition-QFDTVHPH-CcpKJ1lB.js} +1 -1
- package/public/assets/{pieDiagram-DEJITSTG-CPIefI_d.js → pieDiagram-DEJITSTG-PVIVMgVu.js} +1 -1
- package/public/assets/{quadrantDiagram-34T5L4WZ-oif1uGqW.js → quadrantDiagram-34T5L4WZ-BTyRfzd6.js} +1 -1
- package/public/assets/{requirementDiagram-MS252O5E-7TD_ygCy.js → requirementDiagram-MS252O5E-CU9X0jFW.js} +1 -1
- package/public/assets/{sankeyDiagram-XADWPNL6-CMu1tHyh.js → sankeyDiagram-XADWPNL6-DyZdSLSv.js} +1 -1
- package/public/assets/{sequenceDiagram-FGHM5R23-B9mC8imJ.js → sequenceDiagram-FGHM5R23-CuJRqb6t.js} +1 -1
- package/public/assets/{stateDiagram-FHFEXIEX-D6PL1POV.js → stateDiagram-FHFEXIEX-5mIpjfjp.js} +1 -1
- package/public/assets/stateDiagram-v2-QKLJ7IA2-yZsODTx6.js +1 -0
- package/public/assets/{timeline-definition-GMOUNBTQ-DuQkd9Ua.js → timeline-definition-GMOUNBTQ-DCSOAQyL.js} +1 -1
- package/public/assets/{vennDiagram-DHZGUBPP-dF0MNyOc.js → vennDiagram-DHZGUBPP-BXxgH4Ub.js} +1 -1
- package/public/assets/{wardley-RL74JXVD-BS64Vt29.js → wardley-RL74JXVD-BWSOnFou.js} +1 -1
- package/public/assets/{wardleyDiagram-NUSXRM2D-BRkEc3lh.js → wardleyDiagram-NUSXRM2D-BrsRzGry.js} +1 -1
- package/public/assets/{xychartDiagram-5P7HB3ND-CQm5c61-.js → xychartDiagram-5P7HB3ND-CLF12YPc.js} +1 -1
- package/public/index.html +2 -2
- package/templates/front-canonical.tsx +62 -0
- package/templates/package-canonical.json +19 -0
- package/templates/plugin/README.md +64 -0
- package/templates/plugin/package.json +67 -0
- package/templates/plugin/src/front/__tests__/samplePlugin.test.ts +53 -0
- package/templates/plugin/src/front/index.ts +45 -0
- package/templates/plugin/src/front/panels.tsx +8 -0
- package/templates/plugin/src/front/surfaceResolver.ts +19 -0
- package/templates/plugin/src/server/index.ts +29 -0
- package/templates/plugin/src/shared/constants.ts +2 -0
- package/templates/plugin/src/shared/index.ts +2 -0
- package/templates/plugin/src/shared/types.ts +3 -0
- package/templates/plugin/src/test-setup.ts +43 -0
- package/templates/plugin/tsconfig.json +23 -0
- package/templates/plugin/tsup.config.ts +21 -0
- package/templates/plugin/vitest.config.ts +20 -0
- package/templates/server-canonical.ts +42 -0
- package/public/assets/channel-BJh30Beu.js +0 -1
- package/public/assets/classDiagram-6PBFFD2Q-P4A3FztV.js +0 -1
- package/public/assets/classDiagram-v2-HSJHXN6E-P4A3FztV.js +0 -1
- package/public/assets/clone-DWkLJZp8.js +0 -1
- package/public/assets/index-CyEGNm7Z.css +0 -1
- package/public/assets/stateDiagram-v2-QKLJ7IA2-DyoW21-W.js +0 -1
package/README.md
CHANGED
|
@@ -92,6 +92,14 @@ pnpm --filter @hachej/boring-ui-cli build
|
|
|
92
92
|
npx ./packages/cli/dist/index.js
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
### Scaffold a Plugin
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
boring-ui plugin create my-plugin --path plugins
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This copies the bundled plugin template from `templates/plugin/`, renames the sample identifiers, and creates `plugins/my-plugin`.
|
|
102
|
+
|
|
95
103
|
---
|
|
96
104
|
|
|
97
105
|
## Authentication
|
package/dist/server/cli.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync
|
|
12
|
+
} from "node:fs";
|
|
4
13
|
import { createRequire } from "node:module";
|
|
5
|
-
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
14
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
6
16
|
import { parseArgs } from "node:util";
|
|
7
|
-
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
8
17
|
import { createLocalWorkspaceRegistry } from "./localWorkspaces.js";
|
|
18
|
+
import { scaffoldPlugin } from "./scaffoldPlugin.js";
|
|
19
|
+
import { findHintForError, formatVerifyResult, verifyPlugin } from "./verifyPlugin.js";
|
|
9
20
|
const MODE_MAP = {
|
|
10
21
|
"local": "direct",
|
|
11
22
|
// no sandbox, full network access
|
|
@@ -97,7 +108,8 @@ const AUTH_GUIDE = [
|
|
|
97
108
|
" Credentials are saved at ~/.pi/agent/auth.json. Then refresh the browser.",
|
|
98
109
|
""
|
|
99
110
|
].join("\n");
|
|
100
|
-
function checkAuth() {
|
|
111
|
+
async function checkAuth() {
|
|
112
|
+
const { AuthStorage, ModelRegistry } = await import("@mariozechner/pi-coding-agent");
|
|
101
113
|
const authStorage = AuthStorage.create();
|
|
102
114
|
const registry = ModelRegistry.create(authStorage);
|
|
103
115
|
return registry.getAvailable().length;
|
|
@@ -105,7 +117,7 @@ function checkAuth() {
|
|
|
105
117
|
async function startFolderMode(opts) {
|
|
106
118
|
const workspaceRoot = process.env.BORING_AGENT_WORKSPACE_ROOT ?? resolve(opts.folderArg ?? process.cwd());
|
|
107
119
|
const projectName = basename(resolve(workspaceRoot)) || "workspace";
|
|
108
|
-
const modelCount = checkAuth();
|
|
120
|
+
const modelCount = await checkAuth();
|
|
109
121
|
console.log(`
|
|
110
122
|
${projectName}`);
|
|
111
123
|
console.log(` workspace ${workspaceRoot}`);
|
|
@@ -197,7 +209,7 @@ async function startWorkspacesMode(opts) {
|
|
|
197
209
|
getWorkspaceId: async (request) => (await workspaceFromRequest(request)).id,
|
|
198
210
|
getWorkspaceRoot: async (workspaceId) => (await requireWorkspace(workspaceId)).path,
|
|
199
211
|
getSessionNamespace: async ({ workspaceId }) => `local-workspace-${workspaceId}`,
|
|
200
|
-
|
|
212
|
+
getPi: async ({ workspaceRoot }) => ({
|
|
201
213
|
additionalSkillPaths: [join(workspaceRoot, ".agents", "skills")]
|
|
202
214
|
}),
|
|
203
215
|
getExtraTools: async ({ workspaceId, workspaceRoot, workspaceFsCapability }) => [
|
|
@@ -227,9 +239,112 @@ Boring UI`);
|
|
|
227
239
|
console.log(`
|
|
228
240
|
${initialUrl}
|
|
229
241
|
`);
|
|
230
|
-
if (checkAuth() === 0) console.log(AUTH_GUIDE);
|
|
242
|
+
if (await checkAuth() === 0) console.log(AUTH_GUIDE);
|
|
231
243
|
openBrowser(initialUrl);
|
|
232
244
|
}
|
|
245
|
+
function findRepoRoot(from) {
|
|
246
|
+
let current = from;
|
|
247
|
+
while (true) {
|
|
248
|
+
if (existsSync(join(current, "pnpm-workspace.yaml"))) return current;
|
|
249
|
+
const parent = dirname(current);
|
|
250
|
+
if (parent === current) return null;
|
|
251
|
+
current = parent;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function walkDir(dir, base, out = []) {
|
|
255
|
+
for (const entry of readdirSync(dir)) {
|
|
256
|
+
if (entry === "node_modules" || entry === ".git" || entry === "dist") continue;
|
|
257
|
+
const fullPath = join(dir, entry);
|
|
258
|
+
const stat = statSync(fullPath);
|
|
259
|
+
if (stat.isDirectory()) {
|
|
260
|
+
walkDir(fullPath, base, out);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
out.push(relative(base, fullPath));
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
function replaceInFile(filePath, replacements) {
|
|
268
|
+
let content = readFileSync(filePath, "utf8");
|
|
269
|
+
for (const [from, to] of Object.entries(replacements)) {
|
|
270
|
+
content = content.replaceAll(from, to);
|
|
271
|
+
}
|
|
272
|
+
writeFileSync(filePath, content, "utf8");
|
|
273
|
+
}
|
|
274
|
+
async function handlePluginCommand(opts) {
|
|
275
|
+
const subcommand = opts.positionals[1];
|
|
276
|
+
if (subcommand !== "create") {
|
|
277
|
+
console.log("Usage: boring-ui plugin create <name> [--path <dir>]");
|
|
278
|
+
console.log("");
|
|
279
|
+
console.log("Scaffold a new plugin from the template.");
|
|
280
|
+
console.log("");
|
|
281
|
+
console.log("Arguments:");
|
|
282
|
+
console.log(" <name> Plugin name (e.g. my-plugin)");
|
|
283
|
+
console.log(" --path Parent directory for the new plugin (default: plugins/)");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const name = opts.positionals[2];
|
|
287
|
+
if (!name) throw new Error("usage: boring-ui plugin create <name>");
|
|
288
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
289
|
+
const packageRoot = resolve(__dirname, "..", "..");
|
|
290
|
+
const templateDir = join(packageRoot, "templates", "plugin");
|
|
291
|
+
if (!existsSync(templateDir)) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Plugin template not found at ${templateDir}.
|
|
294
|
+
This build may not include the plugin template.`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const repoRoot = findRepoRoot(process.cwd());
|
|
298
|
+
const customPath = opts.args.path;
|
|
299
|
+
const targetParent = customPath ? resolve(customPath) : join(repoRoot ?? process.cwd(), "plugins");
|
|
300
|
+
const targetDir = join(targetParent, name);
|
|
301
|
+
if (existsSync(targetDir)) {
|
|
302
|
+
throw new Error(`Directory already exists: ${targetDir}`);
|
|
303
|
+
}
|
|
304
|
+
const id = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
|
|
305
|
+
if (!id) throw new Error(`invalid plugin name: ${name}`);
|
|
306
|
+
const symbolBase = id.replace(/-plugin$/, "") || id;
|
|
307
|
+
const pascalBase = symbolBase.split(/[-_]+/).map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1)).join("");
|
|
308
|
+
const camelBase = pascalBase.charAt(0).toLowerCase() + pascalBase.slice(1);
|
|
309
|
+
const upperBase = symbolBase.replace(/-/g, "_").toUpperCase();
|
|
310
|
+
console.log(`Scaffolding plugin "${id}" at ${targetDir}`);
|
|
311
|
+
mkdirSync(targetParent, { recursive: true });
|
|
312
|
+
cpSync(templateDir, targetDir, { recursive: true });
|
|
313
|
+
const files = walkDir(targetDir, targetDir);
|
|
314
|
+
const pkgName = `@hachej/boring-${id}`;
|
|
315
|
+
for (const file of files) {
|
|
316
|
+
const fullPath = join(targetDir, file);
|
|
317
|
+
replaceInFile(fullPath, {
|
|
318
|
+
"@hachej/boring-plugin-template": pkgName,
|
|
319
|
+
"sample-plugin": id,
|
|
320
|
+
"sample-panel": `${id}-panel`,
|
|
321
|
+
"sample.open": `${id}.open`,
|
|
322
|
+
"sample:": `${id}:`,
|
|
323
|
+
'"sample"': `"${id}"`,
|
|
324
|
+
"SAMPLE": upperBase,
|
|
325
|
+
"Sample": pascalBase,
|
|
326
|
+
"sampleSurfaceResolver": `${camelBase}SurfaceResolver`,
|
|
327
|
+
"samplePanel": `${camelBase}Panel`
|
|
328
|
+
});
|
|
329
|
+
if (file.includes("samplePlugin")) {
|
|
330
|
+
const newFile = file.replace(/samplePlugin/g, `${camelBase}Plugin`);
|
|
331
|
+
const oldPath = join(targetDir, file);
|
|
332
|
+
const newPath = join(targetDir, newFile);
|
|
333
|
+
if (oldPath !== newPath) {
|
|
334
|
+
mkdirSync(dirname(newPath), { recursive: true });
|
|
335
|
+
renameSync(oldPath, newPath);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
console.log("");
|
|
340
|
+
console.log(`\u2713 Created plugin \`${id}\` at ${relative(process.cwd(), targetDir)}`);
|
|
341
|
+
console.log("");
|
|
342
|
+
console.log("Next steps:");
|
|
343
|
+
console.log(` cd ${relative(process.cwd(), targetDir)}`);
|
|
344
|
+
console.log(" pnpm install");
|
|
345
|
+
console.log(` pnpm --filter ${pkgName} typecheck`);
|
|
346
|
+
console.log(` pnpm --filter ${pkgName} test`);
|
|
347
|
+
}
|
|
233
348
|
async function handleWorkspacesCommand(opts) {
|
|
234
349
|
const registry = createLocalWorkspaceRegistry();
|
|
235
350
|
const subcommand = opts.positionals[1];
|
|
@@ -278,7 +393,8 @@ async function runCli(options) {
|
|
|
278
393
|
port: { type: "string", short: "p" },
|
|
279
394
|
host: { type: "string" },
|
|
280
395
|
mode: { type: "string", short: "m" },
|
|
281
|
-
name: { type: "string", short: "n" }
|
|
396
|
+
name: { type: "string", short: "n" },
|
|
397
|
+
path: { type: "string" }
|
|
282
398
|
},
|
|
283
399
|
allowPositionals: true,
|
|
284
400
|
strict: false
|
|
@@ -298,6 +414,13 @@ async function runCli(options) {
|
|
|
298
414
|
cliMode,
|
|
299
415
|
mode
|
|
300
416
|
};
|
|
417
|
+
if (positionals[0] === "plugin") {
|
|
418
|
+
await handlePluginCommand({
|
|
419
|
+
positionals,
|
|
420
|
+
args: { path: args.path }
|
|
421
|
+
});
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
301
424
|
if (positionals[0] === "workspaces") {
|
|
302
425
|
await handleWorkspacesCommand({
|
|
303
426
|
...base,
|
|
@@ -306,11 +429,69 @@ async function runCli(options) {
|
|
|
306
429
|
});
|
|
307
430
|
return;
|
|
308
431
|
}
|
|
432
|
+
if (positionals[0] === "scaffold-plugin") {
|
|
433
|
+
handleScaffoldPluginCommand({ positionals });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (positionals[0] === "verify-plugin") {
|
|
437
|
+
handleVerifyPluginCommand({ positionals });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
309
440
|
await startFolderMode({
|
|
310
441
|
...base,
|
|
311
442
|
folderArg: positionals[0]
|
|
312
443
|
});
|
|
313
444
|
}
|
|
445
|
+
function defaultWorkspaceRoot() {
|
|
446
|
+
return process.env.BORING_AGENT_WORKSPACE_ROOT ?? process.cwd();
|
|
447
|
+
}
|
|
448
|
+
function handleVerifyPluginCommand(opts) {
|
|
449
|
+
const maybeName = opts.positionals[1];
|
|
450
|
+
const maybeWorkspace = opts.positionals[2];
|
|
451
|
+
const looksLikePath = maybeName && (maybeName.includes("/") || maybeName.startsWith("."));
|
|
452
|
+
const name = looksLikePath ? void 0 : maybeName;
|
|
453
|
+
const workspaceRoot = resolve(maybeWorkspace ?? (looksLikePath ? maybeName : defaultWorkspaceRoot()));
|
|
454
|
+
const result = verifyPlugin({ workspaceRoot, ...name ? { name } : {} });
|
|
455
|
+
console.log(formatVerifyResult(result));
|
|
456
|
+
if (!result.ok) {
|
|
457
|
+
const hints = [];
|
|
458
|
+
for (const outcome of result.outcomes) {
|
|
459
|
+
for (const err of outcome.errors) {
|
|
460
|
+
const hint = findHintForError(err);
|
|
461
|
+
if (hint) hints.push(` hint (${outcome.id}): ${hint}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (hints.length > 0) {
|
|
465
|
+
console.log("");
|
|
466
|
+
console.log("Suggestions:");
|
|
467
|
+
for (const hint of hints) console.log(hint);
|
|
468
|
+
}
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function handleScaffoldPluginCommand(opts) {
|
|
473
|
+
const name = opts.positionals[1];
|
|
474
|
+
if (!name) {
|
|
475
|
+
throw new Error("usage: boring-ui scaffold-plugin <name> [workspace]");
|
|
476
|
+
}
|
|
477
|
+
const workspaceRoot = resolve(opts.positionals[2] ?? defaultWorkspaceRoot());
|
|
478
|
+
const result = scaffoldPlugin({ name, workspaceRoot });
|
|
479
|
+
console.log(`scaffolded ${name}`);
|
|
480
|
+
console.log(` dir ${result.pluginDir}`);
|
|
481
|
+
for (const file of result.filesCreated) {
|
|
482
|
+
console.log(` + ${file}`);
|
|
483
|
+
}
|
|
484
|
+
console.log("");
|
|
485
|
+
console.log("Next steps:");
|
|
486
|
+
console.log(` 1. edit front/index.tsx for UI panels/commands/resolvers`);
|
|
487
|
+
console.log(` 2. add pi.extensions / skills for hot-reloadable agent behavior`);
|
|
488
|
+
console.log(` 3. bash \`boring-ui verify-plugin\` \u2014 confirms manifests + files are valid`);
|
|
489
|
+
console.log(` 4. ask the user: /reload`);
|
|
490
|
+
console.log("");
|
|
491
|
+
console.log("Advanced server integration:");
|
|
492
|
+
console.log(" boring.server is boot-time/static composition only. It is NOT hot-registered");
|
|
493
|
+
console.log(" by /reload for .pi/extensions user plugins; use Pi extensions for agent tools.");
|
|
494
|
+
}
|
|
314
495
|
export {
|
|
315
496
|
registerStatic,
|
|
316
497
|
runCli
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const KEBAB_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
6
|
+
function scaffoldPlugin(opts) {
|
|
7
|
+
if (!KEBAB_RE.test(opts.name)) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
`invalid plugin name "${opts.name}" \u2014 must be kebab-case (e.g. "my-plugin")`
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
const workspaceRoot = resolve(opts.workspaceRoot);
|
|
13
|
+
const pluginDir = join(workspaceRoot, ".pi", "extensions", opts.name);
|
|
14
|
+
mkdirSync(join(workspaceRoot, ".pi", "extensions"), { recursive: true });
|
|
15
|
+
try {
|
|
16
|
+
mkdirSync(pluginDir, { recursive: false });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const code = error.code;
|
|
19
|
+
if (code === "EEXIST") {
|
|
20
|
+
throw new Error(`plugin already exists at ${pluginDir}`);
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
const templatesDir = opts.templatesDir ?? resolveBundledTemplatesDir();
|
|
25
|
+
if (!templatesDir) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"could not locate bundled CLI templates \u2014 pass `templatesDir` explicitly. (this usually indicates a broken @hachej/boring-ui-cli install)"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const tplFront = join(templatesDir, "front-canonical.tsx");
|
|
31
|
+
const tplPackage = join(templatesDir, "package-canonical.json");
|
|
32
|
+
for (const tpl of [tplFront, tplPackage]) {
|
|
33
|
+
if (!existsSync(tpl)) {
|
|
34
|
+
throw new Error(`canonical template missing: ${tpl}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const label = labelFromName(opts.name);
|
|
38
|
+
const filesCreated = [];
|
|
39
|
+
const write = (relPath, contents) => {
|
|
40
|
+
const target = join(pluginDir, relPath);
|
|
41
|
+
mkdirSync(join(target, ".."), { recursive: true });
|
|
42
|
+
writeFileSync(target, contents, "utf8");
|
|
43
|
+
filesCreated.push(target);
|
|
44
|
+
};
|
|
45
|
+
const pkgRaw = JSON.parse(readFileSync(tplPackage, "utf8"));
|
|
46
|
+
delete pkgRaw._doc_;
|
|
47
|
+
const pkgJson = substitute(JSON.stringify(pkgRaw, null, 2), opts.name, label);
|
|
48
|
+
write("package.json", `${pkgJson}
|
|
49
|
+
`);
|
|
50
|
+
const frontSource = substitute(readFileSync(tplFront, "utf8"), opts.name, label);
|
|
51
|
+
write("front/index.tsx", frontSource);
|
|
52
|
+
write(".gitignore", "# Machine-managed sidecars written by the boring-ui plugin runtime.\n.boring-signature.json\n");
|
|
53
|
+
return { pluginDir, filesCreated };
|
|
54
|
+
}
|
|
55
|
+
function labelFromName(name) {
|
|
56
|
+
return name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
57
|
+
}
|
|
58
|
+
function substitute(source, name, label) {
|
|
59
|
+
const paneName = `${pascalCase(name)}Pane`;
|
|
60
|
+
return source.replaceAll("<kebab-name>", name).replaceAll("<kebab-name>", name).replaceAll("<Label>", label).replaceAll(/\bMyPane\b/g, paneName);
|
|
61
|
+
}
|
|
62
|
+
function pascalCase(name) {
|
|
63
|
+
return name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
64
|
+
}
|
|
65
|
+
function resolveBundledTemplatesDir() {
|
|
66
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
67
|
+
const candidate = resolve(here, "..", "..", "templates");
|
|
68
|
+
return existsSync(candidate) ? candidate : void 0;
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
scaffoldPlugin
|
|
72
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
isSafePluginRelativePath,
|
|
6
|
+
validateBoringPluginManifest
|
|
7
|
+
} from "@hachej/boring-workspace/plugin";
|
|
8
|
+
function pluginFileSignature(path) {
|
|
9
|
+
if (!path || !existsSync(path)) return "missing";
|
|
10
|
+
const stat = statSync(path);
|
|
11
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
12
|
+
}
|
|
13
|
+
function readPluginSignatureCache(pluginRootDir) {
|
|
14
|
+
const path = join(pluginRootDir, ".boring-signature.json");
|
|
15
|
+
if (!existsSync(path)) return null;
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
23
|
+
const obj = parsed;
|
|
24
|
+
if (obj.version !== 1) return null;
|
|
25
|
+
const sig = obj.serverSignature;
|
|
26
|
+
if (sig !== null && typeof sig !== "string") return null;
|
|
27
|
+
const loadedAt = typeof obj.loadedAt === "number" ? obj.loadedAt : 0;
|
|
28
|
+
return { version: 1, serverSignature: sig, loadedAt };
|
|
29
|
+
}
|
|
30
|
+
function verifyPlugin(opts) {
|
|
31
|
+
const workspaceRoot = resolve(opts.workspaceRoot);
|
|
32
|
+
const extensionsDir = join(workspaceRoot, ".pi", "extensions");
|
|
33
|
+
if (!existsSync(extensionsDir)) {
|
|
34
|
+
return {
|
|
35
|
+
outcomes: [],
|
|
36
|
+
ok: true,
|
|
37
|
+
extensionsDir,
|
|
38
|
+
extensionsDirMissing: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const targets = [];
|
|
42
|
+
if (opts.name) {
|
|
43
|
+
const dir = join(extensionsDir, opts.name);
|
|
44
|
+
if (!existsSync(dir)) {
|
|
45
|
+
return {
|
|
46
|
+
outcomes: [
|
|
47
|
+
{ id: opts.name, dir, ok: false, errors: [`plugin directory not found: ${dir}`], warnings: [] }
|
|
48
|
+
],
|
|
49
|
+
ok: false,
|
|
50
|
+
extensionsDir,
|
|
51
|
+
extensionsDirMissing: false
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
targets.push(dir);
|
|
55
|
+
} else {
|
|
56
|
+
for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
57
|
+
if (!entry.isDirectory()) continue;
|
|
58
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("preflight-")) continue;
|
|
59
|
+
targets.push(join(extensionsDir, entry.name));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const outcomes = targets.map((dir) => verifySinglePlugin(dir));
|
|
63
|
+
return {
|
|
64
|
+
outcomes,
|
|
65
|
+
ok: outcomes.every((o) => o.ok),
|
|
66
|
+
extensionsDir,
|
|
67
|
+
extensionsDirMissing: false
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function isInsideRoot(rootReal, targetReal) {
|
|
71
|
+
const rel = relative(rootReal, targetReal);
|
|
72
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
73
|
+
}
|
|
74
|
+
function resolveExistingContainedPath(pluginDir, value, field) {
|
|
75
|
+
if (!isSafePluginRelativePath(value)) {
|
|
76
|
+
return { path: null, error: `${field} must be a safe relative path inside the plugin root: ${JSON.stringify(value)}` };
|
|
77
|
+
}
|
|
78
|
+
const abs = resolve(pluginDir, value);
|
|
79
|
+
if (!existsSync(abs)) return { path: null, error: `${field} points at "${value}" but that file does not exist (looked at ${abs})` };
|
|
80
|
+
const rootReal = realpathSync(pluginDir);
|
|
81
|
+
const targetReal = realpathSync(abs);
|
|
82
|
+
if (!isInsideRoot(rootReal, targetReal)) {
|
|
83
|
+
return { path: null, error: `${field} points at "${value}" but resolves outside the plugin root (looked at ${abs})` };
|
|
84
|
+
}
|
|
85
|
+
return { path: targetReal };
|
|
86
|
+
}
|
|
87
|
+
function verifySinglePlugin(pluginDir) {
|
|
88
|
+
const id = pluginDir.split(/[\\/]/).pop() ?? "<unknown>";
|
|
89
|
+
const errors = [];
|
|
90
|
+
const warnings = [];
|
|
91
|
+
const pkgPath = join(pluginDir, "package.json");
|
|
92
|
+
if (!existsSync(pkgPath)) {
|
|
93
|
+
return { id, dir: pluginDir, ok: false, errors: ["package.json missing"], warnings };
|
|
94
|
+
}
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
return { id, dir: pluginDir, ok: false, errors: [`package.json is not valid JSON: ${message}`], warnings };
|
|
101
|
+
}
|
|
102
|
+
const result = validateBoringPluginManifest(parsed);
|
|
103
|
+
if (!result.valid) {
|
|
104
|
+
for (const issue of result.issues) errors.push(formatIssue(issue));
|
|
105
|
+
return { id, dir: pluginDir, ok: false, errors, warnings };
|
|
106
|
+
}
|
|
107
|
+
const manifest = result.packageJson;
|
|
108
|
+
const boring = manifest.boring;
|
|
109
|
+
if (boring?.front) {
|
|
110
|
+
const resolved = resolveExistingContainedPath(pluginDir, boring.front, "boring.front");
|
|
111
|
+
if (resolved.error) errors.push(resolved.error);
|
|
112
|
+
}
|
|
113
|
+
let serverPathAbs = null;
|
|
114
|
+
if (typeof boring?.server === "string") {
|
|
115
|
+
const resolved = resolveExistingContainedPath(pluginDir, boring.server, "boring.server");
|
|
116
|
+
if (resolved.error) {
|
|
117
|
+
errors.push(resolved.error);
|
|
118
|
+
} else {
|
|
119
|
+
serverPathAbs = resolved.path;
|
|
120
|
+
warnings.push(
|
|
121
|
+
"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."
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const cache = readPluginSignatureCache(pluginDir);
|
|
126
|
+
if (cache) {
|
|
127
|
+
const currentServerSig = serverPathAbs ? pluginFileSignature(serverPathAbs) : null;
|
|
128
|
+
const cachedSig = cache.serverSignature;
|
|
129
|
+
if (cachedSig !== currentServerSig) {
|
|
130
|
+
warnings.push(
|
|
131
|
+
"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."
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const piExt = manifest.pi?.extensions;
|
|
136
|
+
if (Array.isArray(piExt)) {
|
|
137
|
+
for (const ext of piExt) {
|
|
138
|
+
if (typeof ext !== "string") continue;
|
|
139
|
+
const resolved = resolveExistingContainedPath(pluginDir, ext, "pi.extensions entry");
|
|
140
|
+
if (resolved.error) errors.push(resolved.error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const piSkills = manifest.pi?.skills;
|
|
144
|
+
if (Array.isArray(piSkills)) {
|
|
145
|
+
for (const skill of piSkills) {
|
|
146
|
+
if (typeof skill !== "string") continue;
|
|
147
|
+
const resolved = resolveExistingContainedPath(pluginDir, skill, "pi.skills entry");
|
|
148
|
+
if (resolved.error) errors.push(resolved.error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { id, dir: pluginDir, ok: errors.length === 0, errors, warnings };
|
|
152
|
+
}
|
|
153
|
+
function formatIssue(issue) {
|
|
154
|
+
return `${issue.code}: ${issue.field}: ${issue.message}`;
|
|
155
|
+
}
|
|
156
|
+
function formatVerifyResult(result) {
|
|
157
|
+
if (result.extensionsDirMissing) {
|
|
158
|
+
return [
|
|
159
|
+
`WARNING \u2014 no plugins to verify. Scanned: ${result.extensionsDir} (directory does not exist).`,
|
|
160
|
+
"",
|
|
161
|
+
"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/`."
|
|
162
|
+
].join("\n");
|
|
163
|
+
}
|
|
164
|
+
if (result.outcomes.length === 0) {
|
|
165
|
+
return [
|
|
166
|
+
`WARNING \u2014 scanned ${result.extensionsDir} but found NO plugin directories.`,
|
|
167
|
+
"",
|
|
168
|
+
"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)."
|
|
169
|
+
].join("\n");
|
|
170
|
+
}
|
|
171
|
+
const lines = [];
|
|
172
|
+
const failures = result.outcomes.filter((o) => !o.ok);
|
|
173
|
+
const withWarnings = result.outcomes.filter((o) => o.warnings.length > 0);
|
|
174
|
+
if (failures.length === 0) {
|
|
175
|
+
lines.push(`OK \u2014 ${result.outcomes.length} plugin(s) have valid manifests + present files. (scanned ${result.extensionsDir})`);
|
|
176
|
+
for (const outcome of result.outcomes) {
|
|
177
|
+
const tag = outcome.warnings.length > 0 ? "\u26A0" : "\u2713";
|
|
178
|
+
lines.push(` ${tag} ${outcome.id}`);
|
|
179
|
+
for (const w of outcome.warnings) {
|
|
180
|
+
lines.push(` WARN: ${w}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
lines.push("");
|
|
184
|
+
if (withWarnings.length > 0) {
|
|
185
|
+
const n = withWarnings.length;
|
|
186
|
+
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.`);
|
|
187
|
+
lines.push("");
|
|
188
|
+
}
|
|
189
|
+
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.");
|
|
190
|
+
} else {
|
|
191
|
+
lines.push(`FAILED \u2014 ${failures.length} of ${result.outcomes.length} plugin(s) have errors. (scanned ${result.extensionsDir})`);
|
|
192
|
+
lines.push("");
|
|
193
|
+
for (const outcome of result.outcomes) {
|
|
194
|
+
if (outcome.ok) {
|
|
195
|
+
const tag = outcome.warnings.length > 0 ? "\u26A0" : "\u2713";
|
|
196
|
+
lines.push(` ${tag} ${outcome.id}`);
|
|
197
|
+
for (const w of outcome.warnings) {
|
|
198
|
+
lines.push(` WARN: ${w}`);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
lines.push(` \u2717 ${outcome.id} (${outcome.dir})`);
|
|
202
|
+
for (const err of outcome.errors) {
|
|
203
|
+
lines.push(` ${err}`);
|
|
204
|
+
}
|
|
205
|
+
for (const w of outcome.warnings) {
|
|
206
|
+
lines.push(` WARN: ${w}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
lines.push("");
|
|
211
|
+
lines.push("Fix the errors above and run `boring-ui verify-plugin` again. Once it reports OK, ask the user to run /reload for front/Pi assets; boring.server changes require static composition plus restart.");
|
|
212
|
+
}
|
|
213
|
+
return lines.join("\n");
|
|
214
|
+
}
|
|
215
|
+
const COMMON_MISTAKE_HINTS = [
|
|
216
|
+
{
|
|
217
|
+
pattern: /boring\.server must be a safe relative path or false/i,
|
|
218
|
+
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.'
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
pattern: /package\.json manifest must be an object/i,
|
|
222
|
+
hint: "package.json must parse as a JSON object \u2014 check for trailing commas or unquoted keys."
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
function findHintForError(message) {
|
|
226
|
+
for (const m of COMMON_MISTAKE_HINTS) {
|
|
227
|
+
if (m.pattern.test(message)) return m.hint;
|
|
228
|
+
}
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
export {
|
|
232
|
+
findHintForError,
|
|
233
|
+
formatVerifyResult,
|
|
234
|
+
verifyPlugin
|
|
235
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-ui-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Turn an agent into an app",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/",
|
|
12
|
-
"public/"
|
|
12
|
+
"public/",
|
|
13
|
+
"templates/"
|
|
13
14
|
],
|
|
14
15
|
"dependencies": {
|
|
15
16
|
"@fastify/static": "^8.0.0",
|
|
16
17
|
"fastify": "^5.4.0",
|
|
17
18
|
"lucide-react": "^1.8.0",
|
|
18
|
-
"@hachej/boring-agent": "0.1.
|
|
19
|
-
"@hachej/boring-workspace": "0.1.
|
|
20
|
-
"@hachej/boring-ui-kit": "0.1.
|
|
19
|
+
"@hachej/boring-agent": "0.1.20",
|
|
20
|
+
"@hachej/boring-workspace": "0.1.20",
|
|
21
|
+
"@hachej/boring-ui-kit": "0.1.20"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@tailwindcss/vite": "^4.0.0",
|