archbyte 0.3.4 → 0.4.0

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.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Check if a command exists in the system PATH.
3
+ * Uses `which` (Unix) or `where` (Windows).
4
+ */
5
+ export declare function isInPath(cmd: string): boolean;
6
+ /**
7
+ * Mask an API key for safe display.
8
+ * Shows first 6 + last 4 characters: `sk-ant-...7x9z`
9
+ */
10
+ export declare function maskKey(key: string): string;
11
+ /**
12
+ * Whether stdin/stdout support interactive terminal features.
13
+ */
14
+ export declare function isTTY(): boolean;
15
+ /**
16
+ * Resolve the user's home directory, or throw if unresolvable.
17
+ * Prefers $HOME (Unix), falls back to $USERPROFILE (Windows).
18
+ */
19
+ export declare function resolveHome(): string;
20
+ /**
21
+ * Basic email format check. Not exhaustive — just catches obvious typos.
22
+ */
23
+ export declare function isValidEmail(email: string): boolean;
@@ -0,0 +1,52 @@
1
+ // Shared CLI utilities.
2
+ // Single source of truth for helpers used across multiple CLI modules.
3
+ import { execSync } from "child_process";
4
+ /**
5
+ * Check if a command exists in the system PATH.
6
+ * Uses `which` (Unix) or `where` (Windows).
7
+ */
8
+ export function isInPath(cmd) {
9
+ try {
10
+ // Only allow simple command names (no spaces, slashes, or shell metacharacters)
11
+ if (!/^[a-zA-Z0-9._-]+$/.test(cmd))
12
+ return false;
13
+ const which = process.platform === "win32" ? "where" : "which";
14
+ execSync(`${which} ${cmd}`, { stdio: "ignore" });
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * Mask an API key for safe display.
23
+ * Shows first 6 + last 4 characters: `sk-ant-...7x9z`
24
+ */
25
+ export function maskKey(key) {
26
+ if (key.length <= 10)
27
+ return "****";
28
+ return key.slice(0, 6) + "..." + key.slice(-4);
29
+ }
30
+ /**
31
+ * Whether stdin/stdout support interactive terminal features.
32
+ */
33
+ export function isTTY() {
34
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
35
+ }
36
+ /**
37
+ * Resolve the user's home directory, or throw if unresolvable.
38
+ * Prefers $HOME (Unix), falls back to $USERPROFILE (Windows).
39
+ */
40
+ export function resolveHome() {
41
+ const home = process.env.HOME ?? process.env.USERPROFILE;
42
+ if (!home) {
43
+ throw new Error("Cannot determine home directory. Set the HOME environment variable.");
44
+ }
45
+ return home;
46
+ }
47
+ /**
48
+ * Basic email format check. Not exhaustive — just catches obvious typos.
49
+ */
50
+ export function isValidEmail(email) {
51
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
52
+ }
@@ -150,7 +150,7 @@ function createHttpServer() {
150
150
  // Read current architecture file
151
151
  const content = await readFile(config.diagramPath, "utf-8");
152
152
  const arch = JSON.parse(content);
153
- // Apply position/dimension updates
153
+ // Apply position/dimension updates only to moved nodes
154
154
  for (const update of updates) {
155
155
  const node = arch.nodes.find((n) => n.id === update.id);
156
156
  if (node) {
@@ -158,10 +158,9 @@ function createHttpServer() {
158
158
  node.y = update.y;
159
159
  node.width = update.width;
160
160
  node.height = update.height;
161
+ node.userPositioned = true;
161
162
  }
162
163
  }
163
- // Mark layout as user-saved so UI uses these positions
164
- arch.layoutSaved = true;
165
164
  // Write back
166
165
  await writeFile(config.diagramPath, JSON.stringify(arch, null, 2), "utf-8");
167
166
  currentArchitecture = arch;
@@ -380,12 +379,21 @@ function createHttpServer() {
380
379
  }
381
380
  const params = new URL(url, `http://localhost:${config.port}`).searchParams;
382
381
  const format = params.get("format") || "mermaid";
383
- const supported = ["mermaid", "markdown", "json", "plantuml", "dot"];
382
+ const supported = ["mermaid", "markdown", "json", "plantuml", "dot", "html"];
384
383
  if (!supported.includes(format)) {
385
384
  res.writeHead(400, { "Content-Type": "application/json" });
386
385
  res.end(JSON.stringify({ error: `Unknown format: ${format}. Supported: ${supported.join(", ")}` }));
387
386
  return;
388
387
  }
388
+ // HTML export requires Pro tier
389
+ if (format === "html") {
390
+ const license = loadLicenseInfo();
391
+ if (license.tier !== "premium") {
392
+ res.writeHead(403, { "Content-Type": "application/json" });
393
+ res.end(JSON.stringify({ error: "HTML export requires a Pro subscription." }));
394
+ return;
395
+ }
396
+ }
389
397
  const realNodes = arch.nodes;
390
398
  const nodeMap = new Map();
391
399
  for (const nd of realNodes)
@@ -516,6 +524,43 @@ function createHttpServer() {
516
524
  ext = "json";
517
525
  break;
518
526
  }
527
+ case "html": {
528
+ // Build self-contained interactive HTML file
529
+ const uiAssetsDir = path.join(UI_DIST, "assets");
530
+ if (!existsSync(uiAssetsDir)) {
531
+ res.writeHead(500, { "Content-Type": "application/json" });
532
+ res.end(JSON.stringify({ error: "UI build not found. Run: npm run build:ui" }));
533
+ return;
534
+ }
535
+ const { readdirSync } = await import("fs");
536
+ const uiAssetFiles = readdirSync(uiAssetsDir);
537
+ let cssInline = "";
538
+ for (const f of uiAssetFiles.filter((f) => f.endsWith(".css"))) {
539
+ cssInline += readFileSync(path.join(uiAssetsDir, f), "utf-8") + "\n";
540
+ }
541
+ let jsInline = "";
542
+ for (const f of uiAssetFiles.filter((f) => f.endsWith(".js"))) {
543
+ jsInline += readFileSync(path.join(uiAssetsDir, f), "utf-8") + "\n";
544
+ }
545
+ content = `<!DOCTYPE html>
546
+ <html lang="en" data-theme="dark">
547
+ <head>
548
+ <meta charset="UTF-8">
549
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
550
+ <title>${config.name} - ArchByte Architecture</title>
551
+ <style>${cssInline}</style>
552
+ </head>
553
+ <body>
554
+ <script>try{if(localStorage.getItem('archbyte-theme')==='light')document.documentElement.setAttribute('data-theme','light')}catch(e){}</script>
555
+ <div id="root"></div>
556
+ <script>window.__ARCHBYTE_DATA__ = ${JSON.stringify(arch)};</script>
557
+ <script type="module">${jsInline}</script>
558
+ </body>
559
+ </html>`;
560
+ contentType = "text/html";
561
+ ext = "html";
562
+ break;
563
+ }
519
564
  default: { // markdown
520
565
  const lines = [`# Architecture — ${config.name}`, "", `> Generated by ArchByte on ${new Date().toISOString().slice(0, 10)}`, ""];
521
566
  lines.push("## Summary", "");
@@ -726,6 +771,7 @@ function createHttpServer() {
726
771
  node.width = update.width;
727
772
  if (update.height)
728
773
  node.height = update.height;
774
+ node.userPositioned = true;
729
775
  }
730
776
  }
731
777
  // Update timestamp
@@ -1343,7 +1389,15 @@ function loadLicenseInfo() {
1343
1389
  const credPath = path.join(home, ".archbyte", "credentials.json");
1344
1390
  if (!existsSync(credPath))
1345
1391
  return defaults;
1346
- const creds = JSON.parse(readFileSync(credPath, "utf-8"));
1392
+ const raw = JSON.parse(readFileSync(credPath, "utf-8"));
1393
+ // Multi-account format (version 2): extract active account
1394
+ let creds;
1395
+ if (raw?.version === 2 && raw.accounts && typeof raw.active === "string") {
1396
+ creds = raw.accounts[raw.active] ?? {};
1397
+ }
1398
+ else {
1399
+ creds = raw;
1400
+ }
1347
1401
  if (!creds.token)
1348
1402
  return defaults;
1349
1403
  // Check expiry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archbyte",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "ArchByte - See what agents build. As they build it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,6 +45,9 @@
45
45
  "openai": "^6.19.0",
46
46
  "zod": "^4.3.6"
47
47
  },
48
+ "optionalDependencies": {
49
+ "@anthropic-ai/claude-agent-sdk": "^0.1.77"
50
+ },
48
51
  "devDependencies": {
49
52
  "@types/js-yaml": "^4.0.9",
50
53
  "@types/node": "^20.0.0",