acrylblends 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/index.js +272 -1
  2. package/dist/index.js.map +1 -1
  3. package/docs/framework/guides/production.md +3 -3
  4. package/lib/{audit-GcLtvNLV.d.ts → audit-BD8_2pkh.d.ts} +25 -3
  5. package/lib/cordis/door.d.ts +2 -2
  6. package/lib/cordis/door.js +79 -6
  7. package/lib/door/host.d.ts +2 -2
  8. package/lib/door/host.js +1 -1
  9. package/lib/door/page.d.ts +24 -1
  10. package/lib/door/page.js +138 -9
  11. package/lib/door/protocol.d.ts +1 -1
  12. package/lib/door/protocol.js +1 -1
  13. package/lib/{protocol-Dt0AZBOz.js → protocol-CQ_if7Dg.js} +4 -0
  14. package/lib/{protocol-CR31M7K_.d.ts → protocol-a2aqrp2X.d.ts} +3 -0
  15. package/lib/{snapshot-text-CtQm3n8n.js → snapshot-text-m13Pk-AI.js} +95 -17
  16. package/package.json +1 -1
  17. package/plugins/acryl-door/plugin.json +3 -0
  18. package/plugins/acryl-door/src/cli/acryl.ts +33 -4
  19. package/plugins/acryl-door/src/host/channel.ts +107 -18
  20. package/plugins/acryl-door/src/host/door.ts +5 -2
  21. package/plugins/acryl-door/src/host/page-socket.ts +78 -0
  22. package/plugins/acryl-door/src/host/shim.ts +2 -1
  23. package/plugins/acryl-door/src/index.ts +10 -0
  24. package/plugins/acryl-door/src/page/channel.ts +67 -5
  25. package/plugins/acryl-door/src/page/driver/sensitivity.ts +39 -3
  26. package/plugins/acryl-door/src/page/index.ts +16 -1
  27. package/plugins/acryl-door/src/page/protocol.ts +3 -0
  28. package/plugins/acryl-door/src/page/socket-source.ts +75 -0
  29. package/plugins/agent-panel/client/main.ts +182 -20
  30. package/plugins/agent-panel/client/markdown.ts +219 -0
  31. package/plugins/agent-panel/headless.ts +20 -0
  32. package/plugins/agent-panel/index.ts +190 -50
  33. package/plugins/desktop/main.ts +50 -3
  34. package/plugins/desktop/menu.ts +60 -0
  35. package/plugins/desktop/window.ts +48 -2
  36. package/plugins/extensions/index.ts +81 -4
  37. package/templates/agent/AGENTS.md +4 -2
  38. package/templates/agent/src/app.ts +9 -1
  39. package/templates/agent/src/routes.ts +13 -3
  40. package/templates/agent/test/app.test.ts +27 -2
package/dist/index.js CHANGED
@@ -4,8 +4,10 @@ import { basename, dirname, join, posix, relative, resolve, sep } from "node:pat
4
4
  import { parseArgs } from "node:util";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createHash } from "node:crypto";
7
- import { spawn } from "node:child_process";
7
+ import { spawn, spawnSync } from "node:child_process";
8
8
  import { createInterface } from "node:readline/promises";
9
+ import { createServer } from "node:net";
10
+ import { homedir } from "node:os";
9
11
  //#region src/templates.ts
10
12
  /** The templates, all Cordis apps: a web app with a page and a client component (nodejs), a notes app and a chat app on the db port (notes, chat), a blank app on the router, a todo list on the router and state, and the blank app with the offline agent. */
11
13
  const TEMPLATES = [
@@ -1262,6 +1264,272 @@ async function kvImport(options, io) {
1262
1264
  }
1263
1265
  }
1264
1266
  //#endregion
1267
+ //#region src/preflight.ts
1268
+ /** The Node the framework needs (the bin refuses an older one before loading anything). */
1269
+ const NODE_MINIMUM = [22, 19];
1270
+ /** The range an app's port is chosen in (plugins/instance: PORT_BASE and PORT_SPAN). */
1271
+ const APP_PORTS = [3100, 3999];
1272
+ const AGENTS = [
1273
+ "claude",
1274
+ "codex",
1275
+ "pi"
1276
+ ];
1277
+ const AGENT_INSTALL = "install one coding agent: npm i -g @openai/codex then codex login, or Claude Code (https://claude.com/claude-code), or npm i -g @earendil-works/pi-coding-agent with DEEPSEEK_API_KEY set";
1278
+ /** Off by default on Windows; a full workshop ran without it, so it is a NOTE: what to do if a path is ever too long. */
1279
+ const LONG_PATHS_NOTE = "off; if an install fails with ENAMETOOLONG or \"path too long\", in PowerShell as administrator: New-ItemProperty -Path HKLM:\\SYSTEM\\CurrentControlSet\\Control\\FileSystem -Name LongPathsEnabled -Value 1 -PropertyType DWORD -Force";
1280
+ const firstLine = (text) => text.trim().split(/\r?\n/)[0]?.trim() ?? "";
1281
+ function checkNode(version) {
1282
+ const [major = 0, minor = 0] = version.split(".").map(Number);
1283
+ const [needMajor, needMinor] = NODE_MINIMUM;
1284
+ return major > needMajor || major === needMajor && minor >= needMinor ? {
1285
+ name: "node",
1286
+ status: "PASS",
1287
+ detail: version
1288
+ } : {
1289
+ name: "node",
1290
+ status: "FIX",
1291
+ detail: `${version} is too old: install Node ${needMajor}.${needMinor} or newer (the LTS) from https://nodejs.org`
1292
+ };
1293
+ }
1294
+ async function checkNpm(probes) {
1295
+ const npm = packageManagerInvocation("npm", ["--version"], probes.platform, probes.env);
1296
+ const result = await probes.run(npm.file, npm.args);
1297
+ return result.code === 0 ? {
1298
+ name: "npm",
1299
+ status: "PASS",
1300
+ detail: firstLine(result.stdout)
1301
+ } : {
1302
+ name: "npm",
1303
+ status: "FIX",
1304
+ detail: "npm did not run: it comes with Node, reinstall Node from https://nodejs.org"
1305
+ };
1306
+ }
1307
+ async function checkGit(probes) {
1308
+ const result = await probes.run("git", ["--version"]);
1309
+ return result.code === 0 ? {
1310
+ name: "git",
1311
+ status: "PASS",
1312
+ detail: firstLine(result.stdout).replace(/^git version /, "")
1313
+ } : {
1314
+ name: "git",
1315
+ status: "FIX",
1316
+ detail: "install git from https://git-scm.com"
1317
+ };
1318
+ }
1319
+ /** Whether pi can reach DeepSeek: the key in the environment, or the one pi stored when its owner logged in (the panel's rule). */
1320
+ function piHasKey(probes) {
1321
+ if (probes.env.DEEPSEEK_API_KEY) return true;
1322
+ const stored = probes.readFile(join(probes.env.PI_CODING_AGENT_DIR ?? join(probes.home, ".pi", "agent"), "auth.json"));
1323
+ if (stored === void 0) return false;
1324
+ try {
1325
+ const key = JSON.parse(stored).deepseek?.key;
1326
+ return typeof key === "string" && key !== "";
1327
+ } catch {
1328
+ return false;
1329
+ }
1330
+ }
1331
+ /**
1332
+ * The coding agents the agent panel would offer: on the PATH, and pi only with a DeepSeek key. On Windows the panel also reads an npm shim to find
1333
+ * its script (plugins/agent-panel/launch.ts); that is plugin code an app carries, not something the command imports, so the PATH is checked here.
1334
+ */
1335
+ const HINT = {
1336
+ claude: "be logged in: run claude once",
1337
+ codex: "be logged in: codex login",
1338
+ pi: "DeepSeek key found"
1339
+ };
1340
+ function checkAgent(probes) {
1341
+ const installed = AGENTS.filter((agent) => probes.onPath(agent));
1342
+ const usable = installed.filter((agent) => agent !== "pi" || piHasKey(probes));
1343
+ if (usable.length > 0) return {
1344
+ name: "agent",
1345
+ status: "PASS",
1346
+ detail: usable.map((agent) => `${agent} (${HINT[agent]})`).join(", ")
1347
+ };
1348
+ if (installed.includes("pi")) return {
1349
+ name: "agent",
1350
+ status: "FIX",
1351
+ detail: "pi has no DeepSeek key: set DEEPSEEK_API_KEY, or log in to pi once, or install another agent"
1352
+ };
1353
+ return {
1354
+ name: "agent",
1355
+ status: "FIX",
1356
+ detail: AGENT_INSTALL
1357
+ };
1358
+ }
1359
+ async function checkPort(probes) {
1360
+ const [from, to] = APP_PORTS;
1361
+ const found = await probes.freePort(from, to);
1362
+ return typeof found === "number" ? {
1363
+ name: "port",
1364
+ status: "PASS",
1365
+ detail: `127.0.0.1:${found} is free (apps use ${from}-${to})`
1366
+ } : {
1367
+ name: "port",
1368
+ status: "FIX",
1369
+ detail: `nothing can listen on 127.0.0.1 in ${from}-${to} (${found}): allow Node in the firewall or security software, or free a port`
1370
+ };
1371
+ }
1372
+ /** Only block 5 needs gh, so it is a NOTE, never a FIX. */
1373
+ async function checkGh(probes) {
1374
+ if (!probes.onPath("gh")) return {
1375
+ name: "gh",
1376
+ status: "NOTE",
1377
+ detail: "not installed; block 5 publishes with it: https://cli.github.com, then gh auth login"
1378
+ };
1379
+ return (await probes.run("gh", ["auth", "status"])).code === 0 ? {
1380
+ name: "gh",
1381
+ status: "PASS",
1382
+ detail: "logged in"
1383
+ } : {
1384
+ name: "gh",
1385
+ status: "NOTE",
1386
+ detail: "not logged in; block 5 needs it: gh auth login"
1387
+ };
1388
+ }
1389
+ async function checkPowerShell(probes) {
1390
+ const pwsh = probes.onPath("pwsh");
1391
+ const result = await probes.run("powershell.exe", [
1392
+ "-NoProfile",
1393
+ "-Command",
1394
+ "$PSVersionTable.PSVersion.ToString()"
1395
+ ]);
1396
+ const version = result.code === 0 ? firstLine(result.stdout) : void 0;
1397
+ if (pwsh) return {
1398
+ name: "powershell",
1399
+ status: "PASS",
1400
+ detail: `pwsh 7 found${version ? `, Windows PowerShell ${version}` : ""}`
1401
+ };
1402
+ if (version !== void 0) return {
1403
+ name: "powershell",
1404
+ status: "PASS",
1405
+ detail: `Windows PowerShell ${version}: use the guide's PowerShell lines where they differ (&& and NODE_ENV=... need PowerShell 7)`
1406
+ };
1407
+ return {
1408
+ name: "powershell",
1409
+ status: "NOTE",
1410
+ detail: "powershell.exe did not run; use cmd.exe lines or install PowerShell 7 (winget install Microsoft.PowerShell)"
1411
+ };
1412
+ }
1413
+ async function checkLongPaths(probes) {
1414
+ const result = await probes.run("reg", [
1415
+ "query",
1416
+ "HKLM\\SYSTEM\\CurrentControlSet\\Control\\FileSystem",
1417
+ "/v",
1418
+ "LongPathsEnabled"
1419
+ ]);
1420
+ const value = /LongPathsEnabled\s+REG_DWORD\s+0x([0-9a-f]+)/i.exec(result.stdout)?.[1];
1421
+ return value !== void 0 && Number.parseInt(value, 16) === 1 ? {
1422
+ name: "long paths",
1423
+ status: "PASS",
1424
+ detail: "enabled"
1425
+ } : {
1426
+ name: "long paths",
1427
+ status: "NOTE",
1428
+ detail: LONG_PATHS_NOTE
1429
+ };
1430
+ }
1431
+ /** Git Bash rewrites a slash-led argument into a Windows path (`/board` becomes `C:/Program Files/Git/board`); the door undoes it from 1.0.7. */
1432
+ function checkGitBash(probes) {
1433
+ if (!probes.env.MSYSTEM) return void 0;
1434
+ return {
1435
+ name: "git bash",
1436
+ status: "NOTE",
1437
+ detail: "this shell rewrites /paths: the door handles it from acrylblends 1.0.7; on an older app run .acryl/bin/acryl from PowerShell, or npx acrylblends@latest upgrade"
1438
+ };
1439
+ }
1440
+ async function preflight(probes) {
1441
+ const checks = [
1442
+ checkNode(probes.nodeVersion),
1443
+ await checkNpm(probes),
1444
+ await checkGit(probes),
1445
+ checkAgent(probes),
1446
+ await checkPort(probes),
1447
+ await checkGh(probes)
1448
+ ];
1449
+ if (probes.platform === "win32") {
1450
+ checks.push(await checkPowerShell(probes), await checkLongPaths(probes));
1451
+ const gitBash = checkGitBash(probes);
1452
+ if (gitBash) checks.push(gitBash);
1453
+ }
1454
+ return checks;
1455
+ }
1456
+ function formatChecks(checks) {
1457
+ const width = Math.max(...checks.map((check) => check.name.length));
1458
+ const lines = checks.map((check) => `${check.status.padEnd(4)} ${check.name.padEnd(width)} ${check.detail}`);
1459
+ const fixes = checks.filter((check) => check.status === "FIX").length;
1460
+ lines.push(fixes === 0 ? "Ready for the workshop." : `${fixes} to fix before the workshop: run each FIX line, then npx acrylblends@latest preflight again.`);
1461
+ return `${lines.join("\n")}\n`;
1462
+ }
1463
+ /** The real machine. A program that cannot start resolves with code null; nothing here throws. */
1464
+ function machineProbes() {
1465
+ return {
1466
+ platform: process.platform,
1467
+ nodeVersion: process.versions.node,
1468
+ env: process.env,
1469
+ home: homedir(),
1470
+ run: (file, args) => new Promise((done) => {
1471
+ let stdout = "";
1472
+ const child = spawn(file, args, {
1473
+ stdio: [
1474
+ "ignore",
1475
+ "pipe",
1476
+ "ignore"
1477
+ ],
1478
+ windowsHide: true
1479
+ });
1480
+ const timer = setTimeout(() => child.kill(), 15e3);
1481
+ child.stdout.on("data", (chunk) => {
1482
+ stdout += chunk.toString();
1483
+ });
1484
+ child.on("error", () => {
1485
+ clearTimeout(timer);
1486
+ done({
1487
+ code: null,
1488
+ stdout
1489
+ });
1490
+ });
1491
+ child.on("close", (code) => {
1492
+ clearTimeout(timer);
1493
+ done({
1494
+ code,
1495
+ stdout
1496
+ });
1497
+ });
1498
+ }),
1499
+ onPath: (program) => spawnSync(process.platform === "win32" ? "where" : "which", [program], { windowsHide: true }).status === 0,
1500
+ readFile: (path) => {
1501
+ try {
1502
+ return readFileSync(path, "utf8");
1503
+ } catch {
1504
+ return;
1505
+ }
1506
+ },
1507
+ freePort: async (from, to) => {
1508
+ let last = "no port tried";
1509
+ for (let port = from; port <= to; port++) {
1510
+ const result = await new Promise((done) => {
1511
+ const server = createServer();
1512
+ server.once("error", (error) => done(error.code ?? error.message));
1513
+ server.listen({
1514
+ port,
1515
+ host: "127.0.0.1",
1516
+ exclusive: true
1517
+ }, () => server.close(() => done(true)));
1518
+ });
1519
+ if (result === true) return port;
1520
+ last = result;
1521
+ if (last !== "EADDRINUSE") return last;
1522
+ }
1523
+ return last;
1524
+ }
1525
+ };
1526
+ }
1527
+ async function runPreflight(io, probes = machineProbes()) {
1528
+ const checks = await preflight(probes);
1529
+ io.stdout.write(formatChecks(checks));
1530
+ return checks.some((check) => check.status === "FIX") ? 1 : 0;
1531
+ }
1532
+ //#endregion
1265
1533
  //#region src/diff.ts
1266
1534
  function ops(a, b) {
1267
1535
  if (a.length * b.length > 6e6) return void 0;
@@ -1563,6 +1831,8 @@ Usage:
1563
1831
  Bring the library plugins an app carries (the router, the server, the door, ...) up to this
1564
1832
  version: untouched files are replaced, an edited file stops it (diff shown) unless --force
1565
1833
  acrylblends plugins List the plugin library
1834
+ acrylblends preflight Check this machine for the workshop: Node, npm, git, a coding agent, a free port
1835
+ (and on Windows PowerShell and long paths); one line per check, PASS or the fix
1566
1836
  acrylblends build-client [root] [out] Compile the client modules once for production (src/client to dist/client)
1567
1837
  acrylblends kv import <kv.json> <db> Move a JSON-file kv store into the kv table of a SQLite database [--overwrite] [--table t] [--migrations dir]
1568
1838
  acrylblends check-deps [dir] Fail when app code imports a database driver or opener outside the file named in
@@ -1603,6 +1873,7 @@ async function runCli(argv, io = processIo()) {
1603
1873
  if (command === "kv") return await kvImport(parseKv(args), io);
1604
1874
  if (command === "check-deps") return checkDeps(parseCheckDeps(args), io);
1605
1875
  if (command === "plugins") return listPlugins(io);
1876
+ if (command === "preflight") return await runPreflight(io);
1606
1877
  throw new UsageError(`unknown command "${command}"`);
1607
1878
  } catch (error) {
1608
1879
  if (error instanceof UsageError) {