@hachej/boring-ui-cli 0.1.16 → 0.1.18
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 +203 -10
- package/dist/server/scaffoldPlugin.js +72 -0
- package/dist/server/verifyPlugin.js +235 -0
- package/package.json +6 -5
- package/public/assets/{CodeEditor-DQqOn4xz-KzhiCz--.js → CodeEditor-DQqOn4xz-BpvvkFT7.js} +1 -1
- package/public/assets/{FileTree-Dl-qUAB0-B2la_qys.js → FileTree-DHVB9rpk-FMcsf2w-.js} +10 -10
- package/public/assets/{MarkdownEditor-yc6mFsnI-But_i4l5.js → MarkdownEditor-L1KDH0bM-D-LbfGkW.js} +39 -39
- package/public/assets/{_baseUniq-DdgAi7yq.js → _baseUniq-CMA9Ia5A.js} +1 -1
- package/public/assets/{arc-CCqUtds5.js → arc-tnXyejqK.js} +1 -1
- package/public/assets/{architectureDiagram-Q4EWVU46-B_KUn6cs.js → architectureDiagram-Q4EWVU46-BSOUkwG2.js} +1 -1
- package/public/assets/{blockDiagram-DXYQGD6D-DZgTILG7.js → blockDiagram-DXYQGD6D-beerpyJi.js} +1 -1
- package/public/assets/{c4Diagram-AHTNJAMY-CJflQswY.js → c4Diagram-AHTNJAMY-uB419B41.js} +1 -1
- package/public/assets/channel-C2_9gQqR.js +1 -0
- package/public/assets/{chunk-4BX2VUAB-iDI_gnLq.js → chunk-4BX2VUAB-CpStyzoR.js} +1 -1
- package/public/assets/{chunk-4TB4RGXK-DKWIFYxg.js → chunk-4TB4RGXK-BL531Lcw.js} +1 -1
- package/public/assets/{chunk-55IACEB6-D__oMXZt.js → chunk-55IACEB6-CRDe66a9.js} +1 -1
- package/public/assets/{chunk-EDXVE4YY-Do1Ka_m9.js → chunk-EDXVE4YY-NaBO_txy.js} +1 -1
- package/public/assets/{chunk-FMBD7UC4-lANxhjIk.js → chunk-FMBD7UC4-Dn2Fmb5W.js} +1 -1
- package/public/assets/{chunk-OYMX7WX6-D5V4Ykah.js → chunk-OYMX7WX6-iffOaivX.js} +1 -1
- package/public/assets/{chunk-QZHKN3VN-Qd0rJnNE.js → chunk-QZHKN3VN-BKRi-Sxw.js} +1 -1
- package/public/assets/{chunk-YZCP3GAM-CFwEplp0.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-DfVWRV6i.js → cose-bilkent-S5V4N54A-BFfdAG94.js} +1 -1
- package/public/assets/{dagre-KV5264BT-DwkYud_A.js → dagre-KV5264BT-BvfHllE1.js} +1 -1
- package/public/assets/{diagram-5BDNPKRD-BVmqnZKF.js → diagram-5BDNPKRD-DML8a0lh.js} +1 -1
- package/public/assets/{diagram-G4DWMVQ6-Be8h-c7h.js → diagram-G4DWMVQ6-DL30StUK.js} +1 -1
- package/public/assets/{diagram-MMDJMWI5-CeQIY42x.js → diagram-MMDJMWI5-D3-Kryyd.js} +1 -1
- package/public/assets/{diagram-TYMM5635-2m1E87ZC.js → diagram-TYMM5635-HDTViX5R.js} +1 -1
- package/public/assets/{erDiagram-SMLLAGMA-CEAPDf64.js → erDiagram-SMLLAGMA-DV46TbuX.js} +1 -1
- package/public/assets/{flowDiagram-DWJPFMVM-70-QVqzZ.js → flowDiagram-DWJPFMVM-BXqQu2_i.js} +1 -1
- package/public/assets/{ganttDiagram-T4ZO3ILL-CQUeK6YZ.js → ganttDiagram-T4ZO3ILL-CnPz0E5a.js} +1 -1
- package/public/assets/{gitGraphDiagram-UUTBAWPF-BAxMyzkG.js → gitGraphDiagram-UUTBAWPF-C25lUVfz.js} +1 -1
- package/public/assets/{graph-DBYJAYcR.js → graph-CSby6HJp.js} +1 -1
- package/public/assets/{highlighted-body-OFNGDK62-D32MCa1A.js → highlighted-body-OFNGDK62-D5Bucj_m.js} +1 -1
- package/public/assets/{index-DfV9nLZO.js → index-CrQ5eYxl.js} +301 -295
- package/public/assets/{index-Dnr-P_qO.js → index-DLyovEn7.js} +1 -1
- package/public/assets/index-RUcotPWN.css +1 -0
- package/public/assets/{infoDiagram-42DDH7IO-D4XwdS2Z.js → infoDiagram-42DDH7IO-D1tkIwny.js} +1 -1
- package/public/assets/{ishikawaDiagram-UXIWVN3A-7F_PxA8e.js → ishikawaDiagram-UXIWVN3A-D6zbyxWx.js} +1 -1
- package/public/assets/{journeyDiagram-VCZTEJTY-CBJ4F9tD.js → journeyDiagram-VCZTEJTY-ExQUgQxi.js} +1 -1
- package/public/assets/{kanban-definition-6JOO6SKY-DNxo2cA_.js → kanban-definition-6JOO6SKY-E3gNRZLm.js} +1 -1
- package/public/assets/{layout-xHv827iK.js → layout-BF96CGik.js} +1 -1
- package/public/assets/{linear-C4D-wE9b.js → linear-6x4QueCv.js} +1 -1
- package/public/assets/{min-DRKJR04f.js → min-Cyn_UaFw.js} +1 -1
- package/public/assets/{mindmap-definition-QFDTVHPH-BFEYUDaK.js → mindmap-definition-QFDTVHPH-CcpKJ1lB.js} +1 -1
- package/public/assets/{pieDiagram-DEJITSTG-OgFPC-fQ.js → pieDiagram-DEJITSTG-PVIVMgVu.js} +1 -1
- package/public/assets/{quadrantDiagram-34T5L4WZ-gcFm5ymy.js → quadrantDiagram-34T5L4WZ-BTyRfzd6.js} +1 -1
- package/public/assets/{requirementDiagram-MS252O5E-C-NXT7Xx.js → requirementDiagram-MS252O5E-CU9X0jFW.js} +1 -1
- package/public/assets/{sankeyDiagram-XADWPNL6-BVTpXbQ9.js → sankeyDiagram-XADWPNL6-DyZdSLSv.js} +1 -1
- package/public/assets/{sequenceDiagram-FGHM5R23-6nnB6xSY.js → sequenceDiagram-FGHM5R23-CuJRqb6t.js} +1 -1
- package/public/assets/{stateDiagram-FHFEXIEX-vvjjWHqR.js → stateDiagram-FHFEXIEX-5mIpjfjp.js} +1 -1
- package/public/assets/stateDiagram-v2-QKLJ7IA2-yZsODTx6.js +1 -0
- package/public/assets/{timeline-definition-GMOUNBTQ-DfgoENQV.js → timeline-definition-GMOUNBTQ-DCSOAQyL.js} +1 -1
- package/public/assets/{vennDiagram-DHZGUBPP-CwtXgny7.js → vennDiagram-DHZGUBPP-BXxgH4Ub.js} +1 -1
- package/public/assets/{wardley-RL74JXVD-YWQ--wCx.js → wardley-RL74JXVD-BWSOnFou.js} +1 -1
- package/public/assets/{wardleyDiagram-NUSXRM2D-0iSD2F3A.js → wardleyDiagram-NUSXRM2D-BrsRzGry.js} +1 -1
- package/public/assets/{xychartDiagram-5P7HB3ND-DpVtsfdW.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-DGvkobtu.js +0 -1
- package/public/assets/classDiagram-6PBFFD2Q-CbsNjbES.js +0 -1
- package/public/assets/classDiagram-v2-HSJHXN6E-CbsNjbES.js +0 -1
- package/public/assets/clone-CGN5Up_u.js +0 -1
- package/public/assets/index-CyEGNm7Z.css +0 -1
- package/public/assets/stateDiagram-v2-QKLJ7IA2-UKC2Fz8g.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,16 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
5
16
|
import { parseArgs } from "node:util";
|
|
6
|
-
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
7
17
|
import { createLocalWorkspaceRegistry } from "./localWorkspaces.js";
|
|
18
|
+
import { scaffoldPlugin } from "./scaffoldPlugin.js";
|
|
19
|
+
import { findHintForError, formatVerifyResult, verifyPlugin } from "./verifyPlugin.js";
|
|
8
20
|
const MODE_MAP = {
|
|
9
21
|
"local": "direct",
|
|
10
22
|
// no sandbox, full network access
|
|
11
23
|
"local-sandbox": "local"
|
|
12
24
|
// bwrap isolated, no network (Linux only)
|
|
13
25
|
};
|
|
26
|
+
const require2 = createRequire(import.meta.url);
|
|
27
|
+
const CLI_VERSION = (() => {
|
|
28
|
+
try {
|
|
29
|
+
const pkg = require2("../../package.json");
|
|
30
|
+
return pkg.version ?? "0.0.0";
|
|
31
|
+
} catch {
|
|
32
|
+
return "0.0.0";
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
14
35
|
function httpError(message, statusCode) {
|
|
15
36
|
const error = new Error(message);
|
|
16
37
|
error.statusCode = statusCode;
|
|
@@ -87,7 +108,8 @@ const AUTH_GUIDE = [
|
|
|
87
108
|
" Credentials are saved at ~/.pi/agent/auth.json. Then refresh the browser.",
|
|
88
109
|
""
|
|
89
110
|
].join("\n");
|
|
90
|
-
function checkAuth() {
|
|
111
|
+
async function checkAuth() {
|
|
112
|
+
const { AuthStorage, ModelRegistry } = await import("@mariozechner/pi-coding-agent");
|
|
91
113
|
const authStorage = AuthStorage.create();
|
|
92
114
|
const registry = ModelRegistry.create(authStorage);
|
|
93
115
|
return registry.getAvailable().length;
|
|
@@ -95,7 +117,7 @@ function checkAuth() {
|
|
|
95
117
|
async function startFolderMode(opts) {
|
|
96
118
|
const workspaceRoot = process.env.BORING_AGENT_WORKSPACE_ROOT ?? resolve(opts.folderArg ?? process.cwd());
|
|
97
119
|
const projectName = basename(resolve(workspaceRoot)) || "workspace";
|
|
98
|
-
const modelCount = checkAuth();
|
|
120
|
+
const modelCount = await checkAuth();
|
|
99
121
|
console.log(`
|
|
100
122
|
${projectName}`);
|
|
101
123
|
console.log(` workspace ${workspaceRoot}`);
|
|
@@ -111,7 +133,8 @@ ${projectName}`);
|
|
|
111
133
|
});
|
|
112
134
|
app.get("/api/v1/workspace/meta", async () => ({
|
|
113
135
|
workspaceRoot,
|
|
114
|
-
projectName
|
|
136
|
+
projectName,
|
|
137
|
+
version: CLI_VERSION
|
|
115
138
|
}));
|
|
116
139
|
await registerStatic(app, opts.publicDir);
|
|
117
140
|
await app.listen({ port: opts.port, host: opts.host });
|
|
@@ -186,7 +209,7 @@ async function startWorkspacesMode(opts) {
|
|
|
186
209
|
getWorkspaceId: async (request) => (await workspaceFromRequest(request)).id,
|
|
187
210
|
getWorkspaceRoot: async (workspaceId) => (await requireWorkspace(workspaceId)).path,
|
|
188
211
|
getSessionNamespace: async ({ workspaceId }) => `local-workspace-${workspaceId}`,
|
|
189
|
-
|
|
212
|
+
getPi: async ({ workspaceRoot }) => ({
|
|
190
213
|
additionalSkillPaths: [join(workspaceRoot, ".agents", "skills")]
|
|
191
214
|
}),
|
|
192
215
|
getExtraTools: async ({ workspaceId, workspaceRoot, workspaceFsCapability }) => [
|
|
@@ -200,7 +223,8 @@ async function startWorkspacesMode(opts) {
|
|
|
200
223
|
});
|
|
201
224
|
app.get("/api/v1/workspace/meta", async () => ({
|
|
202
225
|
projectName: "Boring UI",
|
|
203
|
-
workspacesMode: true
|
|
226
|
+
workspacesMode: true,
|
|
227
|
+
version: CLI_VERSION
|
|
204
228
|
}));
|
|
205
229
|
await registerStatic(app, opts.publicDir);
|
|
206
230
|
await app.listen({ port: opts.port, host: opts.host });
|
|
@@ -215,9 +239,112 @@ Boring UI`);
|
|
|
215
239
|
console.log(`
|
|
216
240
|
${initialUrl}
|
|
217
241
|
`);
|
|
218
|
-
if (checkAuth() === 0) console.log(AUTH_GUIDE);
|
|
242
|
+
if (await checkAuth() === 0) console.log(AUTH_GUIDE);
|
|
219
243
|
openBrowser(initialUrl);
|
|
220
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
|
+
}
|
|
221
348
|
async function handleWorkspacesCommand(opts) {
|
|
222
349
|
const registry = createLocalWorkspaceRegistry();
|
|
223
350
|
const subcommand = opts.positionals[1];
|
|
@@ -266,7 +393,8 @@ async function runCli(options) {
|
|
|
266
393
|
port: { type: "string", short: "p" },
|
|
267
394
|
host: { type: "string" },
|
|
268
395
|
mode: { type: "string", short: "m" },
|
|
269
|
-
name: { type: "string", short: "n" }
|
|
396
|
+
name: { type: "string", short: "n" },
|
|
397
|
+
path: { type: "string" }
|
|
270
398
|
},
|
|
271
399
|
allowPositionals: true,
|
|
272
400
|
strict: false
|
|
@@ -286,6 +414,13 @@ async function runCli(options) {
|
|
|
286
414
|
cliMode,
|
|
287
415
|
mode
|
|
288
416
|
};
|
|
417
|
+
if (positionals[0] === "plugin") {
|
|
418
|
+
await handlePluginCommand({
|
|
419
|
+
positionals,
|
|
420
|
+
args: { path: args.path }
|
|
421
|
+
});
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
289
424
|
if (positionals[0] === "workspaces") {
|
|
290
425
|
await handleWorkspacesCommand({
|
|
291
426
|
...base,
|
|
@@ -294,11 +429,69 @@ async function runCli(options) {
|
|
|
294
429
|
});
|
|
295
430
|
return;
|
|
296
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
|
+
}
|
|
297
440
|
await startFolderMode({
|
|
298
441
|
...base,
|
|
299
442
|
folderArg: positionals[0]
|
|
300
443
|
});
|
|
301
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
|
+
}
|
|
302
495
|
export {
|
|
303
496
|
registerStatic,
|
|
304
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.18",
|
|
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.18",
|
|
20
|
+
"@hachej/boring-workspace": "0.1.18",
|
|
21
|
+
"@hachej/boring-ui-kit": "0.1.18"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@tailwindcss/vite": "^4.0.0",
|