buildwithnexus 0.3.8 → 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.
package/dist/bin.js CHANGED
@@ -341,9 +341,9 @@ var init_secrets = __esm({
341
341
  "src/core/secrets.ts"() {
342
342
  "use strict";
343
343
  init_dlp();
344
- NEXUS_HOME2 = path2.join(process.env.HOME || "~", ".buildwithnexus");
345
- CONFIG_PATH = path2.join(NEXUS_HOME2, "config.json");
346
- KEYS_PATH = path2.join(NEXUS_HOME2, ".env.keys");
344
+ NEXUS_HOME2 = process.env.NEXUS_HOME || path2.join(process.env.HOME || "~", ".buildwithnexus");
345
+ CONFIG_PATH = process.env.NEXUS_CONFIG_PATH || path2.join(NEXUS_HOME2, "config.json");
346
+ KEYS_PATH = process.env.NEXUS_KEYS_PATH || path2.join(NEXUS_HOME2, ".env.keys");
347
347
  }
348
348
  });
349
349
 
@@ -438,59 +438,59 @@ async function getPortBlocker(port) {
438
438
  return null;
439
439
  }
440
440
  }
441
+ async function findFreePort(preferred, max = 20) {
442
+ for (let offset = 0; offset < max; offset++) {
443
+ if (await isPortFree(preferred + offset)) return preferred + offset;
444
+ }
445
+ throw new Error(`No free port found near ${preferred}`);
446
+ }
441
447
  async function resolvePortConflicts(ports) {
442
448
  const labels = { ssh: "SSH", http: "HTTP", https: "HTTPS" };
443
449
  const resolved = { ...ports };
444
- const claimed = /* @__PURE__ */ new Set();
445
450
  for (const [key, port] of Object.entries(ports)) {
446
- if (await isPortFree(port) && !claimed.has(port)) {
447
- claimed.add(port);
448
- continue;
449
- }
450
- let altPort = null;
451
- for (let offset = 1; offset <= 50; offset++) {
452
- const candidate = port + offset;
453
- if (!claimed.has(candidate) && await isPortFree(candidate)) {
454
- altPort = candidate;
455
- break;
456
- }
451
+ if (await isPortFree(port)) continue;
452
+ const blocker = await getPortBlocker(port);
453
+ const desc = blocker ? `${blocker.process} (PID ${blocker.pid})` : "unknown process";
454
+ const altPort = await findFreePort(port + 1).catch(() => null);
455
+ const choices = [];
456
+ if (blocker) {
457
+ choices.push({
458
+ name: `Kill ${desc} and use port ${port}`,
459
+ value: "kill"
460
+ });
457
461
  }
458
462
  if (altPort) {
459
- resolved[key] = altPort;
460
- claimed.add(altPort);
461
- const blocker = await getPortBlocker(port);
462
- const desc = blocker ? `${blocker.process} (PID ${blocker.pid})` : "another process";
463
- console.log(chalk5.yellow(` \u26A0 Port ${port} (${labels[key]}) in use by ${desc} \u2192 using ${altPort}`));
464
- } else {
465
- const blocker = await getPortBlocker(port);
466
- const desc = blocker ? `${blocker.process} (PID ${blocker.pid})` : "unknown process";
467
- console.log("");
468
- console.log(chalk5.red(` \u2717 Port ${port} (${labels[key]}) is in use by ${desc} and no free port found nearby`));
469
- const action = await select({
470
- message: `No free ${labels[key]} port found. Kill ${desc} to free port ${port}?`,
471
- choices: [
472
- ...blocker ? [{ name: `Kill ${desc} and use port ${port}`, value: "kill" }] : [],
473
- { name: "Abort init", value: "abort" }
474
- ]
463
+ choices.push({
464
+ name: `Use alternate port ${altPort} instead`,
465
+ value: "alt"
475
466
  });
476
- if (action === "kill" && blocker) {
477
- try {
478
- process.kill(blocker.pid, "SIGTERM");
479
- await new Promise((r) => setTimeout(r, 1e3));
480
- if (!await isPortFree(port)) {
481
- process.kill(blocker.pid, "SIGKILL");
482
- await new Promise((r) => setTimeout(r, 500));
483
- }
484
- claimed.add(port);
485
- console.log(chalk5.green(` \u2713 Killed ${desc}, port ${port} is now free`));
486
- } catch {
487
- console.log(chalk5.red(` \u2717 Failed to kill PID ${blocker.pid}. Try: sudo kill ${blocker.pid}`));
488
- process.exit(1);
467
+ }
468
+ choices.push({ name: "Abort init", value: "abort" });
469
+ console.log("");
470
+ console.log(chalk5.yellow(` \u26A0 Port ${port} (${labels[key]}) is in use by ${desc}`));
471
+ const action = await select({
472
+ message: `How would you like to resolve the ${labels[key]} port conflict?`,
473
+ choices
474
+ });
475
+ if (action === "kill" && blocker) {
476
+ try {
477
+ process.kill(blocker.pid, "SIGTERM");
478
+ await new Promise((r) => setTimeout(r, 1e3));
479
+ if (!await isPortFree(port)) {
480
+ process.kill(blocker.pid, "SIGKILL");
481
+ await new Promise((r) => setTimeout(r, 500));
489
482
  }
490
- } else {
491
- console.log(chalk5.dim(" Init aborted."));
492
- process.exit(0);
483
+ console.log(chalk5.green(` \u2713 Killed ${desc}, port ${port} is now free`));
484
+ } catch {
485
+ console.log(chalk5.red(` \u2717 Failed to kill PID ${blocker.pid}. Try: sudo kill ${blocker.pid}`));
486
+ process.exit(1);
493
487
  }
488
+ } else if (action === "alt" && altPort) {
489
+ resolved[key] = altPort;
490
+ console.log(chalk5.green(` \u2713 Using port ${altPort} for ${labels[key]}`));
491
+ } else {
492
+ console.log(chalk5.dim(" Init aborted."));
493
+ process.exit(0);
494
494
  }
495
495
  }
496
496
  return resolved;
@@ -671,11 +671,13 @@ function addSshConfig(port) {
671
671
  fs4.writeFileSync(sshConfigPath, block, { mode: 384 });
672
672
  }
673
673
  }
674
- async function waitForSsh(port, timeoutMs = 6e5) {
674
+ async function waitForSsh(port, timeoutMs = 3e5) {
675
675
  const start = Date.now();
676
+ let attempt = 0;
677
+ const backoffMs = (n) => Math.min(3e3 * Math.pow(2, n), 3e4);
676
678
  while (Date.now() - start < timeoutMs) {
679
+ const ssh = new NodeSSH();
677
680
  try {
678
- const ssh = new NodeSSH();
679
681
  await ssh.connect({
680
682
  host: "localhost",
681
683
  port,
@@ -687,7 +689,14 @@ async function waitForSsh(port, timeoutMs = 6e5) {
687
689
  ssh.dispose();
688
690
  return true;
689
691
  } catch {
690
- await new Promise((r) => setTimeout(r, 5e3));
692
+ try {
693
+ ssh.dispose();
694
+ } catch {
695
+ }
696
+ const delay = backoffMs(attempt++);
697
+ const remaining = timeoutMs - (Date.now() - start);
698
+ if (remaining <= 0) break;
699
+ await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
691
700
  }
692
701
  }
693
702
  return false;
@@ -737,7 +746,6 @@ var init_ssh = __esm({
737
746
 
738
747
  // src/cli.ts
739
748
  import { Command as Command14 } from "commander";
740
- import { createRequire } from "module";
741
749
 
742
750
  // src/commands/init.ts
743
751
  init_banner();
@@ -777,6 +785,13 @@ var log = {
777
785
  },
778
786
  detail(label, value) {
779
787
  console.log(chalk3.dim(" " + label + ": ") + value);
788
+ },
789
+ progress(current, total, label) {
790
+ const pct = Math.round(current / total * 100);
791
+ const filled = Math.round(current / total * 20);
792
+ const bar = chalk3.cyan("\u2588".repeat(filled)) + chalk3.dim("\u2591".repeat(20 - filled));
793
+ process.stdout.write(`\r [${bar}] ${chalk3.bold(`${pct}%`)} ${chalk3.dim(label)}`);
794
+ if (current >= total) process.stdout.write("\n");
780
795
  }
781
796
  };
782
797
 
@@ -939,13 +954,13 @@ async function renderCloudInit(data, templateContent) {
939
954
  }
940
955
  const safeData = { ...data, sshPubKey: yamlEscape(trimmedPubKey), keys: safeKeys };
941
956
  const rendered = ejs.render(templateContent, safeData);
942
- const outputPath = path5.join(CONFIGS_DIR, "user-data");
957
+ const outputPath = path5.join(CONFIGS_DIR, "user-data.yaml");
943
958
  fs5.writeFileSync(outputPath, rendered, { mode: 384 });
944
- audit("cloudinit_rendered", "user-data written");
959
+ audit("cloudinit_rendered", "user-data.yaml written");
945
960
  return outputPath;
946
961
  }
947
962
  async function createCloudInitIso(userDataPath) {
948
- const metaDataPath = path5.join(CONFIGS_DIR, "meta-data");
963
+ const metaDataPath = path5.join(CONFIGS_DIR, "meta-data.yaml");
949
964
  fs5.writeFileSync(metaDataPath, "instance-id: nexus-vm-1\nlocal-hostname: nexus-vm\n", { mode: 384 });
950
965
  const isoPath = path5.join(IMAGES_DIR2, "init.iso");
951
966
  const env = scrubEnv();
@@ -1006,7 +1021,7 @@ async function createCloudInitIso(userDataPath) {
1006
1021
  fs5.unlinkSync(metaDataPath);
1007
1022
  } catch {
1008
1023
  }
1009
- audit("cloudinit_plaintext_deleted", "user-data and meta-data removed");
1024
+ audit("cloudinit_plaintext_deleted", "user-data.yaml and meta-data.yaml removed");
1010
1025
  }
1011
1026
  }
1012
1027
 
@@ -1018,7 +1033,12 @@ async function checkHealth(port, vmRunning) {
1018
1033
  sshReady: false,
1019
1034
  dockerReady: false,
1020
1035
  serverHealthy: false,
1021
- tunnelUrl: null
1036
+ tunnelUrl: null,
1037
+ dockerVersion: null,
1038
+ serverVersion: null,
1039
+ diskUsagePercent: null,
1040
+ uptimeSeconds: null,
1041
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString()
1022
1042
  };
1023
1043
  if (!vmRunning) return status;
1024
1044
  try {
@@ -1028,13 +1048,33 @@ async function checkHealth(port, vmRunning) {
1028
1048
  return status;
1029
1049
  }
1030
1050
  try {
1031
- const { code } = await sshExec(port, "docker version --format '{{.Server.Version}}'");
1032
- status.dockerReady = code === 0;
1051
+ const { stdout, code } = await sshExec(port, "docker version --format '{{.Server.Version}}'");
1052
+ status.dockerReady = code === 0 && stdout.trim().length > 0;
1053
+ if (status.dockerReady) status.dockerVersion = stdout.trim();
1033
1054
  } catch {
1034
1055
  }
1035
1056
  try {
1036
1057
  const { stdout, code } = await sshExec(port, "curl -sf http://localhost:4200/health");
1037
1058
  status.serverHealthy = code === 0 && stdout.includes("ok");
1059
+ if (status.serverHealthy) {
1060
+ try {
1061
+ const parsed = JSON.parse(stdout);
1062
+ if (typeof parsed.version === "string") status.serverVersion = parsed.version;
1063
+ } catch {
1064
+ }
1065
+ }
1066
+ } catch {
1067
+ }
1068
+ try {
1069
+ const { stdout } = await sshExec(port, "df / --output=pcent | tail -1 | tr -dc '0-9'");
1070
+ const pct = parseInt(stdout.trim(), 10);
1071
+ if (!isNaN(pct)) status.diskUsagePercent = pct;
1072
+ } catch {
1073
+ }
1074
+ try {
1075
+ const { stdout } = await sshExec(port, "awk '{print int($1)}' /proc/uptime 2>/dev/null");
1076
+ const up = parseInt(stdout.trim(), 10);
1077
+ if (!isNaN(up)) status.uptimeSeconds = up;
1038
1078
  } catch {
1039
1079
  }
1040
1080
  try {
@@ -1046,9 +1086,11 @@ async function checkHealth(port, vmRunning) {
1046
1086
  }
1047
1087
  return status;
1048
1088
  }
1049
- async function waitForServer(port, timeoutMs = 36e5) {
1089
+ async function waitForServer(port, timeoutMs = 9e5) {
1050
1090
  const start = Date.now();
1051
1091
  let lastLog = 0;
1092
+ let attempt = 0;
1093
+ const backoffMs = (n) => Math.min(3e3 * Math.pow(2, n), 3e4);
1052
1094
  while (Date.now() - start < timeoutMs) {
1053
1095
  try {
1054
1096
  const { stdout, code } = await sshExec(port, "curl -sf http://localhost:4200/health");
@@ -1066,13 +1108,18 @@ async function waitForServer(port, timeoutMs = 36e5) {
1066
1108
  } catch {
1067
1109
  }
1068
1110
  }
1069
- await new Promise((r) => setTimeout(r, 5e3));
1111
+ const delay = backoffMs(attempt++);
1112
+ const remaining = timeoutMs - (Date.now() - start);
1113
+ if (remaining <= 0) break;
1114
+ await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
1070
1115
  }
1071
1116
  return false;
1072
1117
  }
1073
- async function waitForCloudInit(port, timeoutMs = 72e5) {
1118
+ async function waitForCloudInit(port, timeoutMs = 18e5) {
1074
1119
  const start = Date.now();
1075
1120
  let lastLog = 0;
1121
+ let attempt = 0;
1122
+ const backoffMs = (n) => Math.min(3e3 * Math.pow(2, n), 3e4);
1076
1123
  while (Date.now() - start < timeoutMs) {
1077
1124
  try {
1078
1125
  const { code } = await sshExec(port, "test -f /var/lib/cloud/instance/boot-finished");
@@ -1090,7 +1137,10 @@ async function waitForCloudInit(port, timeoutMs = 72e5) {
1090
1137
  } catch {
1091
1138
  }
1092
1139
  }
1093
- await new Promise((r) => setTimeout(r, 2e4));
1140
+ const delay = backoffMs(attempt++);
1141
+ const remaining = timeoutMs - (Date.now() - start);
1142
+ if (remaining <= 0) break;
1143
+ await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
1094
1144
  }
1095
1145
  return false;
1096
1146
  }
@@ -1306,8 +1356,6 @@ var phases = [
1306
1356
  name: "VM Provisioning",
1307
1357
  run: async (ctx, spinner) => {
1308
1358
  const { config, keys, tarballPath } = ctx;
1309
- const pinPath = path6.join(process.env.HOME || "~", ".buildwithnexus", "ssh", "vm_host_key.pin");
1310
- if (fs6.existsSync(pinPath)) fs6.unlinkSync(pinPath);
1311
1359
  spinner.text = "Waiting for SSH...";
1312
1360
  spinner.start();
1313
1361
  const sshReady = await waitForSsh(config.sshPort);
@@ -1340,7 +1388,7 @@ var phases = [
1340
1388
  spinner.start();
1341
1389
  const cloudInitDone = await waitForCloudInit(config.sshPort);
1342
1390
  if (!cloudInitDone) {
1343
- fail(spinner, "Cloud-init timed out after 2 hours");
1391
+ fail(spinner, "Cloud-init timed out after 30 minutes");
1344
1392
  log.warn("Check progress: buildwithnexus ssh \u2192 tail -f /var/log/cloud-init-output.log");
1345
1393
  throw new Error("Cloud-init timed out");
1346
1394
  }
@@ -1565,9 +1613,13 @@ var statusCommand = new Command4("status").description("Check NEXUS runtime heal
1565
1613
  console.log("");
1566
1614
  console.log(` ${check(health.vmRunning)} VM ${health.vmRunning ? chalk7.green("running") + chalk7.dim(` (PID ${getVmPid()})`) : chalk7.red("stopped")}`);
1567
1615
  console.log(` ${check(health.sshReady)} SSH ${health.sshReady ? chalk7.green("connected") + chalk7.dim(` (port ${config.sshPort})`) : chalk7.red("unreachable")}`);
1568
- console.log(` ${check(health.dockerReady)} Docker ${health.dockerReady ? chalk7.green("ready") : chalk7.red("not ready")}`);
1569
- console.log(` ${check(health.serverHealthy)} Server ${health.serverHealthy ? chalk7.green("healthy") + chalk7.dim(` (port ${config.httpPort})`) : chalk7.red("unhealthy")}`);
1616
+ console.log(` ${check(health.dockerReady)} Docker ${health.dockerReady ? chalk7.green("ready") + (health.dockerVersion ? chalk7.dim(` v${health.dockerVersion}`) : "") : chalk7.red("not ready")}`);
1617
+ console.log(` ${check(health.serverHealthy)} Server ${health.serverHealthy ? chalk7.green("healthy") + chalk7.dim(` (port ${config.httpPort})`) + (health.serverVersion ? chalk7.dim(` v${health.serverVersion}`) : "") : chalk7.red("unhealthy")}`);
1570
1618
  console.log(` ${check(!!health.tunnelUrl)} Tunnel ${health.tunnelUrl ? chalk7.green(health.tunnelUrl) : chalk7.dim("not active")}`);
1619
+ if (health.diskUsagePercent !== null) {
1620
+ const diskOk = health.diskUsagePercent < 85;
1621
+ console.log(` ${check(diskOk)} Disk ${diskOk ? chalk7.green(`${health.diskUsagePercent}% used`) : chalk7.yellow(`${health.diskUsagePercent}% used \u2014 consider cleanup`)}`);
1622
+ }
1571
1623
  console.log("");
1572
1624
  if (health.serverHealthy) {
1573
1625
  log.success(`NEXUS CLI ready \u2014 connect via: buildwithnexus ssh`);
@@ -2399,9 +2451,18 @@ var EventStream = class {
2399
2451
  lastId = "0";
2400
2452
  onEvent;
2401
2453
  pollInterval = null;
2454
+ consecutiveErrors = 0;
2455
+ lastError = null;
2402
2456
  constructor(onEvent) {
2403
2457
  this.onEvent = onEvent;
2404
2458
  }
2459
+ getStatus() {
2460
+ return {
2461
+ active: this.active,
2462
+ consecutiveErrors: this.consecutiveErrors,
2463
+ lastError: this.lastError
2464
+ };
2465
+ }
2405
2466
  async start() {
2406
2467
  this.active = true;
2407
2468
  const config = loadConfig();
@@ -2414,13 +2475,23 @@ var EventStream = class {
2414
2475
  `curl -sf -H 'Last-Event-ID: ${this.lastId}' http://localhost:4200/events?timeout=1 2>/dev/null || true`
2415
2476
  );
2416
2477
  if (code === 0 && stdout.trim()) {
2478
+ this.consecutiveErrors = 0;
2479
+ this.lastError = null;
2417
2480
  const events = parseSSEData(stdout);
2418
2481
  for (const event of events) {
2419
2482
  if (event.id) this.lastId = event.id;
2420
2483
  this.onEvent(event);
2421
2484
  }
2422
2485
  }
2423
- } catch {
2486
+ } catch (err) {
2487
+ this.consecutiveErrors++;
2488
+ this.lastError = err instanceof Error ? err.message : String(err);
2489
+ if (this.consecutiveErrors >= 5) {
2490
+ this.onEvent({
2491
+ type: "error",
2492
+ content: `Event stream disconnected after ${this.consecutiveErrors} attempts: ${this.lastError}`
2493
+ });
2494
+ }
2424
2495
  }
2425
2496
  }, 2e3);
2426
2497
  }
@@ -2835,9 +2906,7 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2835
2906
  });
2836
2907
 
2837
2908
  // src/cli.ts
2838
- var require2 = createRequire(import.meta.url);
2839
- var { version } = require2("../package.json");
2840
- var cli = new Command14().name("buildwithnexus").description("Auto-scaffold and launch a fully autonomous NEXUS runtime").version(version);
2909
+ var cli = new Command14().name("buildwithnexus").description("Auto-scaffold and launch a fully autonomous NEXUS runtime").version("0.3.1");
2841
2910
  cli.addCommand(initCommand);
2842
2911
  cli.addCommand(startCommand);
2843
2912
  cli.addCommand(stopCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buildwithnexus",
3
- "version": "0.3.8",
3
+ "version": "0.4.0",
4
4
  "description": "Launch an autonomous AI runtime with triple-nested VM isolation in one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "test": "vitest run",
20
20
  "test:watch": "vitest",
21
21
  "test:coverage": "vitest run --coverage",
22
- "prepublishOnly": "npm run build && npm run bundle"
22
+ "prepublishOnly": "npm run build"
23
23
  },
24
24
  "engines": {
25
25
  "node": ">=18"
Binary file