buildwithnexus 0.5.17 → 0.6.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
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  var __defProp = Object.defineProperty;
3
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
3
  var __esm = (fn, res) => function __init() {
@@ -17,98 +16,1451 @@ __export(banner_exports, {
17
16
  showPhase: () => showPhase,
18
17
  showSecurityPosture: () => showSecurityPosture
19
18
  });
20
- import chalk from "chalk";
19
+ import chalk5 from "chalk";
21
20
  import { readFileSync } from "fs";
22
21
  import { dirname, join } from "path";
23
- import { fileURLToPath } from "url";
22
+ import { fileURLToPath as fileURLToPath2 } from "url";
24
23
  function getVersion() {
25
24
  try {
26
- const __dirname = dirname(fileURLToPath(import.meta.url));
27
- const packagePath = join(__dirname, "..", "package.json");
28
- const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
29
- return packageJson.version;
30
- } catch (e) {
31
- console.error(`[DEBUG] getVersion() failed: ${e instanceof Error ? e.message : String(e)}`);
32
- return "0.5.16";
25
+ const __dirname2 = dirname(fileURLToPath2(import.meta.url));
26
+ const packagePath = join(__dirname2, "..", "package.json");
27
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
28
+ return packageJson.version;
29
+ } catch {
30
+ return true ? "0.6.0" : "0.0.0-unknown";
31
+ }
32
+ }
33
+ function showBanner() {
34
+ console.log(BANNER);
35
+ console.log(chalk5.dim(` v${getVersion()} \xB7 buildwithnexus.dev
36
+ `));
37
+ }
38
+ function showPhase(phase, total, description) {
39
+ const progress = chalk5.cyan(`[${phase}/${total}]`);
40
+ console.log(`
41
+ ${progress} ${chalk5.bold(description)}`);
42
+ }
43
+ function showSecurityPosture() {
44
+ const lines = [
45
+ "",
46
+ chalk5.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),
47
+ chalk5.bold(" \u2551 ") + chalk5.bold.green("Security Posture") + chalk5.bold(" \u2551"),
48
+ chalk5.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
49
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" Triple-nested isolation: Host \u2192 VM \u2192 Docker \u2192 KVM".padEnd(54)) + chalk5.bold("\u2551"),
50
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" Network hardened: UFW deny-all, allow 22/80/443/4200".padEnd(54)) + chalk5.bold("\u2551"),
51
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" All databases encrypted at rest (AES-256-CBC)".padEnd(54)) + chalk5.bold("\u2551"),
52
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" API keys never embedded in VM \u2014 delivered via SCP".padEnd(54)) + chalk5.bold("\u2551"),
53
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" SSH-only communication (no exposed network ports)".padEnd(54)) + chalk5.bold("\u2551"),
54
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" DLP: secret detection, shell escaping, output redaction".padEnd(54)) + chalk5.bold("\u2551"),
55
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" HMAC integrity verification on all key files".padEnd(54)) + chalk5.bold("\u2551"),
56
+ chalk5.bold(" \u2551 ") + chalk5.green("\u2713") + chalk5.white(" Docker: --read-only, no-new-privileges, cap-drop=ALL".padEnd(54)) + chalk5.bold("\u2551"),
57
+ chalk5.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
58
+ chalk5.bold(" \u2551 ") + chalk5.dim("Full details: https://buildwithnexus.dev/security".padEnd(55)) + chalk5.bold("\u2551"),
59
+ chalk5.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),
60
+ ""
61
+ ];
62
+ console.log(lines.join("\n"));
63
+ }
64
+ function showCompletion(urls) {
65
+ const lines = [
66
+ "",
67
+ chalk5.green(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),
68
+ chalk5.green(" \u2551 ") + chalk5.bold.green("NEXUS Runtime is Live!") + chalk5.green(" \u2551"),
69
+ chalk5.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
70
+ chalk5.green(" \u2551 ") + chalk5.white(`Connect: ${urls.ssh}`.padEnd(55)) + chalk5.green("\u2551")
71
+ ];
72
+ if (urls.remote) {
73
+ lines.push(chalk5.green(" \u2551 ") + chalk5.white(`Remote: ${urls.remote}`.padEnd(55)) + chalk5.green("\u2551"));
74
+ }
75
+ lines.push(
76
+ chalk5.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
77
+ chalk5.green(" \u2551 ") + chalk5.dim("Quick Start:".padEnd(55)) + chalk5.green("\u2551"),
78
+ chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus - Interactive shell".padEnd(55)) + chalk5.green("\u2551"),
79
+ chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus brainstorm - Brainstorm ideas".padEnd(55)) + chalk5.green("\u2551"),
80
+ chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus status - Check health".padEnd(55)) + chalk5.green("\u2551"),
81
+ chalk5.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
82
+ chalk5.green(" \u2551 ") + chalk5.dim("All commands:".padEnd(55)) + chalk5.green("\u2551"),
83
+ chalk5.green(" \u2551 ") + chalk5.white(" buildwithnexus stop/start/update/logs/ssh/destroy".padEnd(55)) + chalk5.green("\u2551"),
84
+ chalk5.green(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),
85
+ ""
86
+ );
87
+ console.log(lines.join("\n"));
88
+ }
89
+ var BANNER;
90
+ var init_banner = __esm({
91
+ "src/ui/banner.ts"() {
92
+ "use strict";
93
+ BANNER = `
94
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
95
+ \u2551 ${chalk5.bold.cyan("B U I L D W I T H N E X U S")} \u2551
96
+ \u2551 \u2551
97
+ \u2551 Autonomous AI Runtime \xB7 Nested Isolation \u2551
98
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
99
+ `;
100
+ }
101
+ });
102
+
103
+ // src/bin.ts
104
+ import { program } from "commander";
105
+
106
+ // src/cli/init-command.ts
107
+ import fs from "fs";
108
+ import path from "path";
109
+ import { input, confirm } from "@inquirer/prompts";
110
+ async function deepAgentsInitCommand() {
111
+ console.log(`
112
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
113
+ \u2551 Deep Agents -- First Time Setup \u2551
114
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
115
+ `);
116
+ const envPath = path.join(process.cwd(), ".env.local");
117
+ const hasEnv = fs.existsSync(envPath);
118
+ if (hasEnv) {
119
+ const shouldReset = await confirm({
120
+ message: ".env.local already exists. Reconfigure?",
121
+ default: false
122
+ });
123
+ if (!shouldReset) {
124
+ console.log("Setup complete -- using existing configuration.");
125
+ return;
126
+ }
127
+ }
128
+ console.log(
129
+ "Please provide your LLM API keys:\n(You can set these later by editing .env.local)\n"
130
+ );
131
+ const anthropicKey = await input({
132
+ message: "ANTHROPIC_API_KEY (Claude)",
133
+ default: "",
134
+ validate: (val) => {
135
+ if (!val) {
136
+ return "At least one API key is required";
137
+ }
138
+ return true;
139
+ }
140
+ });
141
+ const openaiKey = await input({
142
+ message: "OPENAI_API_KEY (GPT - optional)",
143
+ default: ""
144
+ });
145
+ const backendUrl = await input({
146
+ message: "Backend URL",
147
+ default: "http://localhost:4200"
148
+ });
149
+ const dashboardPort = await input({
150
+ message: "Dashboard port",
151
+ default: "4201"
152
+ });
153
+ const envContent = `# Deep Agents Configuration
154
+ # Generated by buildwithnexus init
155
+
156
+ # LLM API Keys
157
+ ANTHROPIC_API_KEY=${anthropicKey}
158
+ ${openaiKey ? `OPENAI_API_KEY=${openaiKey}` : "# OPENAI_API_KEY="}
159
+
160
+ # Backend
161
+ BACKEND_URL=${backendUrl}
162
+ DASHBOARD_PORT=${dashboardPort}
163
+
164
+ # Optional
165
+ # LOG_LEVEL=debug
166
+ `;
167
+ fs.writeFileSync(envPath, envContent);
168
+ console.log(`
169
+ Configuration saved to .env.local
170
+
171
+ Next steps:
172
+ 1. Start the backend:
173
+ cd ~/Projects/nexus && python -m src.deep_agents_server
174
+
175
+ 2. Start the dashboard:
176
+ buildwithnexus dashboard
177
+
178
+ 3. Run your first task:
179
+ deep-agents run "List files in the current directory"
180
+
181
+ For help, run:
182
+ deep-agents --help
183
+ `);
184
+ }
185
+
186
+ // src/cli/tui.ts
187
+ import chalk from "chalk";
188
+ var TUI = class {
189
+ taskStartTime = 0;
190
+ eventCount = 0;
191
+ displayHeader(task, agent) {
192
+ console.clear();
193
+ console.log(
194
+ chalk.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")
195
+ );
196
+ console.log(
197
+ chalk.cyan("\u2551") + chalk.bold.white(" \u{1F680} DEEP AGENTS - Autonomous Execution Engine ") + chalk.cyan("\u2551")
198
+ );
199
+ console.log(
200
+ chalk.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")
201
+ );
202
+ console.log("");
203
+ console.log(chalk.bold("\u{1F4CB} Task:"), task);
204
+ console.log(chalk.bold("\u{1F464} Agent:"), chalk.blue(agent));
205
+ console.log(chalk.gray("\u2500".repeat(60)));
206
+ console.log("");
207
+ this.taskStartTime = Date.now();
208
+ }
209
+ displayConnecting() {
210
+ console.log(chalk.yellow("\u23F3 Connecting to backend..."));
211
+ }
212
+ displayConnected(runId) {
213
+ console.log(chalk.green("\u2713 Connected"), chalk.gray(`(Run ID: ${runId})`));
214
+ console.log(chalk.gray("\u2500".repeat(60)));
215
+ console.log("");
216
+ }
217
+ displayStreamStart() {
218
+ console.log(chalk.bold.cyan("\u{1F4E1} Streaming Events:"));
219
+ console.log("");
220
+ }
221
+ displayPlan(task, steps) {
222
+ console.log("");
223
+ console.log(chalk.bold.cyan("\u{1F50D} Chief of Staff Analysis"));
224
+ console.log(chalk.gray("\u2500".repeat(60)));
225
+ steps.forEach((step, i) => {
226
+ console.log(` ${chalk.bold.white(`Step ${i + 1}:`)} ${chalk.white(step)}`);
227
+ });
228
+ console.log("");
229
+ }
230
+ displayEvent(type, data) {
231
+ this.eventCount++;
232
+ const content = data["content"] || "";
233
+ if (type === "agent_working") {
234
+ const agent = data["agent"] || "Agent";
235
+ const agentTask = data["task"] || "";
236
+ console.log("");
237
+ console.log(` ${chalk.bold.blue("\u{1F464}")} ${chalk.bold.blue(agent)} ${chalk.gray("working on:")} ${chalk.white(agentTask)}`);
238
+ return;
239
+ }
240
+ if (type === "agent_result") {
241
+ const result = data["result"] || "";
242
+ let displayResult = result;
243
+ if (displayResult.length > 120) {
244
+ displayResult = displayResult.substring(0, 117) + "...";
245
+ }
246
+ console.log(` ${chalk.green("\u2713")} ${chalk.green(displayResult)}`);
247
+ return;
248
+ }
249
+ const emoji = {
250
+ thought: "\u{1F4AD}",
251
+ action: "\u{1F528}",
252
+ observation: "\u2713",
253
+ started: "\u25B6\uFE0F",
254
+ done: "\u2728",
255
+ execution_complete: "\u2728",
256
+ error: "\u274C"
257
+ };
258
+ const color = {
259
+ thought: chalk.cyan,
260
+ action: chalk.yellow,
261
+ observation: chalk.green,
262
+ started: chalk.blue,
263
+ done: chalk.magenta,
264
+ execution_complete: chalk.magenta,
265
+ error: chalk.red
266
+ };
267
+ const icon = emoji[type] || "\u25CF";
268
+ const colorFn = color[type] || chalk.white;
269
+ let displayContent = content;
270
+ if (displayContent.length > 120) {
271
+ displayContent = displayContent.substring(0, 117) + "...";
272
+ }
273
+ console.log(` ${icon} ${colorFn(displayContent)}`);
274
+ }
275
+ displayResults(summary, todosCompleted) {
276
+ console.log("");
277
+ console.log(chalk.gray("\u2500".repeat(60)));
278
+ console.log(chalk.bold.green("\u2728 Complete!"));
279
+ const lines = summary.split("\n");
280
+ for (const line of lines) {
281
+ console.log(` ${chalk.white(line)}`);
282
+ }
283
+ console.log(chalk.gray(` ${todosCompleted} step(s) completed`));
284
+ console.log("");
285
+ }
286
+ displayError(error) {
287
+ console.log("");
288
+ console.log(chalk.red.bold("\u274C Error Occurred:"));
289
+ console.log(chalk.red(error));
290
+ console.log("");
291
+ }
292
+ displayComplete(duration) {
293
+ console.log("");
294
+ console.log(chalk.gray("\u2500".repeat(60)));
295
+ console.log(
296
+ chalk.green.bold("\u2728 Workflow Complete!") + chalk.gray(` (${duration}ms, ${this.eventCount} events)`)
297
+ );
298
+ console.log("");
299
+ }
300
+ displayBox(title, content) {
301
+ const width = 60;
302
+ const borderColor = chalk.blue;
303
+ console.log(borderColor("\u250C" + "\u2500".repeat(width - 2) + "\u2510"));
304
+ console.log(
305
+ borderColor("\u2502") + chalk.bold.white(` ${title}`.padEnd(width - 3)) + borderColor("\u2502")
306
+ );
307
+ console.log(borderColor("\u251C" + "\u2500".repeat(width - 2) + "\u2524"));
308
+ const lines = content.split("\n");
309
+ for (const line of lines) {
310
+ const padded = line.substring(0, width - 4).padEnd(width - 4);
311
+ console.log(borderColor("\u2502") + " " + padded + borderColor("\u2502"));
312
+ }
313
+ console.log(borderColor("\u2514" + "\u2500".repeat(width - 2) + "\u2518"));
314
+ console.log("");
315
+ }
316
+ getElapsedTime() {
317
+ return Date.now() - this.taskStartTime;
318
+ }
319
+ displayModeBar(current) {
320
+ const modes = ["PLAN", "BUILD", "BRAINSTORM"];
321
+ const modeColor = {
322
+ PLAN: chalk.bold.cyan,
323
+ BUILD: chalk.bold.green,
324
+ BRAINSTORM: chalk.bold.blue
325
+ };
326
+ const parts = modes.map((m) => {
327
+ if (m === current) {
328
+ return modeColor[m](`[${m}]`);
329
+ }
330
+ return chalk.gray(m);
331
+ });
332
+ console.log(chalk.gray("MODE: ") + parts.join(chalk.gray(" | ")));
333
+ console.log(chalk.gray("Shift+Tab to swap modes"));
334
+ console.log(chalk.gray("\u2500".repeat(60)));
335
+ }
336
+ displayModeHeader(mode) {
337
+ const modeColor = {
338
+ PLAN: chalk.bold.cyan,
339
+ BUILD: chalk.bold.green,
340
+ BRAINSTORM: chalk.bold.blue
341
+ };
342
+ const modeIcon = {
343
+ PLAN: "\u{1F4CB}",
344
+ BUILD: "\u2699\uFE0F ",
345
+ BRAINSTORM: "\u{1F4A1}"
346
+ };
347
+ const modeDesc = {
348
+ PLAN: "Plan & review steps before executing",
349
+ BUILD: "Execute immediately with live streaming",
350
+ BRAINSTORM: "Free-form Q&A and idea exploration"
351
+ };
352
+ console.log("");
353
+ console.log(modeColor[mode](`${modeIcon[mode]} ${mode} MODE`));
354
+ console.log(chalk.gray(modeDesc[mode]));
355
+ console.log("");
356
+ }
357
+ displaySuggestedMode(mode, task) {
358
+ const modeColor = {
359
+ PLAN: chalk.cyan,
360
+ BUILD: chalk.green,
361
+ BRAINSTORM: chalk.blue
362
+ };
363
+ console.log("");
364
+ console.log(
365
+ chalk.bold("Suggested mode: ") + modeColor[mode](mode) + chalk.gray(` for: "${task.length > 50 ? task.substring(0, 47) + "..." : task}"`)
366
+ );
367
+ }
368
+ displayBrainstormResponse(response) {
369
+ console.log("");
370
+ console.log(chalk.bold.blue("\u{1F4A1} Agent:"));
371
+ const lines = response.split("\n");
372
+ for (const line of lines) {
373
+ console.log(" " + chalk.white(line));
374
+ }
375
+ console.log("");
376
+ }
377
+ displayPermissionPrompt(message) {
378
+ return chalk.bold.white(message) + chalk.gray(" [Y/n] ");
379
+ }
380
+ };
381
+ var tui = new TUI();
382
+
383
+ // src/cli/run-command.ts
384
+ async function runCommand(task, options) {
385
+ const backendUrl = process.env.BACKEND_URL || "http://localhost:4200";
386
+ tui.displayHeader(task, options.agent);
387
+ tui.displayConnecting();
388
+ try {
389
+ let healthOk = false;
390
+ try {
391
+ const healthResponse = await fetch(`${backendUrl}/health`);
392
+ healthOk = healthResponse.ok;
393
+ } catch {
394
+ }
395
+ if (!healthOk) {
396
+ console.error(
397
+ "Backend not responding. Start it with:\n buildwithnexus server"
398
+ );
399
+ process.exit(1);
400
+ }
401
+ const response = await fetch(`${backendUrl}/api/run`, {
402
+ method: "POST",
403
+ headers: { "Content-Type": "application/json" },
404
+ body: JSON.stringify({
405
+ task,
406
+ agent_role: options.agent,
407
+ agent_goal: options.goal || ""
408
+ })
409
+ });
410
+ if (!response.ok) {
411
+ console.error("Backend error");
412
+ console.error(await response.text());
413
+ process.exit(1);
414
+ }
415
+ const { run_id } = await response.json();
416
+ tui.displayConnected(run_id);
417
+ tui.displayStreamStart();
418
+ const eventSourceUrl = `${backendUrl}/api/stream/${run_id}`;
419
+ try {
420
+ const response2 = await fetch(eventSourceUrl);
421
+ const reader = response2.body?.getReader();
422
+ const decoder = new TextDecoder();
423
+ if (!reader) {
424
+ throw new Error("No response body");
425
+ }
426
+ let buffer = "";
427
+ while (true) {
428
+ const { done, value } = await reader.read();
429
+ if (done) break;
430
+ buffer += decoder.decode(value, { stream: true });
431
+ const lines = buffer.split("\n");
432
+ buffer = lines.pop() || "";
433
+ for (const line of lines) {
434
+ if (line.startsWith("data: ")) {
435
+ try {
436
+ const data = JSON.parse(line.slice(6));
437
+ const type = data.type;
438
+ const content = data.data["content"] || "";
439
+ if (type === "done") {
440
+ tui.displayEvent(type, "Task completed successfully");
441
+ tui.displayComplete(tui.getElapsedTime());
442
+ process.exit(0);
443
+ } else if (type === "error") {
444
+ tui.displayError(content);
445
+ process.exit(1);
446
+ } else {
447
+ tui.displayEvent(type, content);
448
+ }
449
+ } catch {
450
+ }
451
+ }
452
+ }
453
+ }
454
+ } catch (error) {
455
+ console.error(
456
+ "\nStream error. Make sure backend is running:\n buildwithnexus server"
457
+ );
458
+ process.exit(1);
459
+ }
460
+ } catch (error) {
461
+ const message = error instanceof Error ? error.message : String(error);
462
+ console.error("Error:", message);
463
+ process.exit(1);
464
+ }
465
+ }
466
+
467
+ // src/cli/dashboard-command.ts
468
+ import { spawn } from "child_process";
469
+ import { fileURLToPath } from "url";
470
+ import path2 from "path";
471
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
472
+ async function dashboardCommand(options) {
473
+ const port = options.port || "4201";
474
+ console.log(`Starting Deep Agents Dashboard on http://localhost:${port}
475
+ `);
476
+ const dashboardPath = path2.join(__dirname, "../deep-agents/dashboard/server.js");
477
+ const dashboard = spawn("node", [dashboardPath], {
478
+ env: { ...process.env, PORT: port },
479
+ stdio: "inherit"
480
+ });
481
+ dashboard.on("error", (err) => {
482
+ console.error("Failed to start dashboard:", err);
483
+ process.exit(1);
484
+ });
485
+ console.log(`Dashboard ready! Open: http://localhost:${port}`);
486
+ console.log("Press Ctrl+C to stop\n");
487
+ }
488
+
489
+ // src/cli/interactive.ts
490
+ import * as readline from "readline";
491
+ import chalk2 from "chalk";
492
+
493
+ // src/cli/intent-classifier.ts
494
+ var PLAN_KEYWORDS = [
495
+ "design",
496
+ "plan",
497
+ "architect",
498
+ "structure",
499
+ "outline",
500
+ "roadmap",
501
+ "strategy",
502
+ "organize",
503
+ "breakdown",
504
+ "scope",
505
+ "model",
506
+ "schema"
507
+ ];
508
+ var BUILD_KEYWORDS = [
509
+ "build",
510
+ "create",
511
+ "make",
512
+ "write",
513
+ "implement",
514
+ "code",
515
+ "generate",
516
+ "add",
517
+ "fix",
518
+ "update",
519
+ "deploy",
520
+ "run",
521
+ "start",
522
+ "launch",
523
+ "install",
524
+ "set up",
525
+ "setup",
526
+ "refactor",
527
+ "migrate"
528
+ ];
529
+ var BRAINSTORM_KEYWORDS = [
530
+ "what",
531
+ "should",
532
+ "idea",
533
+ "ideas",
534
+ "think",
535
+ "consider",
536
+ "suggest",
537
+ "brainstorm",
538
+ "explore",
539
+ "wonder",
540
+ "might",
541
+ "could",
542
+ "would",
543
+ "how about",
544
+ "what if",
545
+ "options",
546
+ "alternatives",
547
+ "thoughts"
548
+ ];
549
+ function classifyIntent(task) {
550
+ const lower = task.toLowerCase().trim();
551
+ let planScore = 0;
552
+ let buildScore = 0;
553
+ let brainstormScore = 0;
554
+ for (const kw of PLAN_KEYWORDS) {
555
+ if (lower.includes(kw)) planScore++;
556
+ }
557
+ for (const kw of BUILD_KEYWORDS) {
558
+ if (lower.includes(kw)) buildScore++;
559
+ }
560
+ for (const kw of BRAINSTORM_KEYWORDS) {
561
+ if (lower.includes(kw)) brainstormScore++;
562
+ }
563
+ const wordCount = lower.split(/\s+/).length;
564
+ if (wordCount > 12 && planScore === buildScore && buildScore === brainstormScore) {
565
+ return "plan";
566
+ }
567
+ if (brainstormScore > planScore && brainstormScore > buildScore) return "brainstorm";
568
+ if (buildScore > planScore && buildScore > brainstormScore) return "build";
569
+ if (planScore > 0) return "plan";
570
+ return wordCount > 6 ? "plan" : "build";
571
+ }
572
+
573
+ // src/cli/interactive.ts
574
+ async function interactiveMode() {
575
+ const backendUrl = process.env.BACKEND_URL || "http://localhost:4200";
576
+ try {
577
+ const response = await fetch(`${backendUrl}/health`);
578
+ if (!response.ok) {
579
+ console.error(chalk2.red("\u274C Backend not running. Start it with: buildwithnexus server"));
580
+ process.exit(1);
581
+ }
582
+ } catch {
583
+ console.error(chalk2.red("\u274C Cannot connect to backend at " + backendUrl));
584
+ process.exit(1);
585
+ }
586
+ const rl = readline.createInterface({
587
+ input: process.stdin,
588
+ output: process.stdout
589
+ });
590
+ const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
591
+ console.clear();
592
+ console.log(chalk2.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
593
+ console.log(
594
+ chalk2.cyan("\u2551") + chalk2.bold.white(" \u{1F680} DEEP AGENTS - Autonomous Execution Engine ") + chalk2.cyan("\u2551")
595
+ );
596
+ console.log(chalk2.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
597
+ console.log("");
598
+ console.log(chalk2.gray("Welcome! Describe what you want the AI agents to do."));
599
+ console.log(chalk2.gray('Type "exit" to quit.\n'));
600
+ while (true) {
601
+ const task = await ask(chalk2.bold.blue("\u{1F4DD} Task: "));
602
+ if (task.toLowerCase() === "exit") {
603
+ console.log(chalk2.yellow("\nGoodbye! \u{1F44B}\n"));
604
+ rl.close();
605
+ process.exit(0);
606
+ }
607
+ if (!task.trim()) {
608
+ console.log(chalk2.red("Please enter a task.\n"));
609
+ continue;
610
+ }
611
+ const suggestedMode = classifyIntent(task).toUpperCase();
612
+ tui.displaySuggestedMode(suggestedMode, task);
613
+ const currentMode = await selectMode(suggestedMode, ask);
614
+ await runModeLoop(currentMode, task, backendUrl, rl, ask);
615
+ console.log("");
616
+ }
617
+ }
618
+ async function selectMode(suggested, ask) {
619
+ const modeColor = {
620
+ PLAN: chalk2.cyan,
621
+ BUILD: chalk2.green,
622
+ BRAINSTORM: chalk2.blue
623
+ };
624
+ console.log("");
625
+ console.log(
626
+ chalk2.gray("Press ") + chalk2.bold("Enter") + chalk2.gray(" to use ") + modeColor[suggested](suggested) + chalk2.gray(" or type ") + chalk2.bold("p") + chalk2.gray("/") + chalk2.bold("b") + chalk2.gray("/") + chalk2.bold("br") + chalk2.gray(" to switch: ")
627
+ );
628
+ const answer = await ask(chalk2.gray("> "));
629
+ const lower = answer.trim().toLowerCase();
630
+ if (lower === "p" || lower === "plan") return "PLAN";
631
+ if (lower === "b" || lower === "build") return "BUILD";
632
+ if (lower === "br" || lower === "brainstorm") return "BRAINSTORM";
633
+ return suggested;
634
+ }
635
+ async function runModeLoop(mode, task, backendUrl, rl, ask) {
636
+ let currentMode = mode;
637
+ while (true) {
638
+ console.clear();
639
+ printAppHeader();
640
+ tui.displayModeBar(currentMode);
641
+ tui.displayModeHeader(currentMode);
642
+ if (currentMode === "PLAN") {
643
+ const next = await planModeLoop(task, backendUrl, rl, ask);
644
+ if (next === "BUILD") {
645
+ currentMode = "BUILD";
646
+ continue;
647
+ }
648
+ if (next === "switch") {
649
+ currentMode = await promptModeSwitch(currentMode, ask);
650
+ continue;
651
+ }
652
+ return;
653
+ }
654
+ if (currentMode === "BUILD") {
655
+ const next = await buildModeLoop(task, backendUrl, rl, ask);
656
+ if (next === "switch") {
657
+ currentMode = await promptModeSwitch(currentMode, ask);
658
+ continue;
659
+ }
660
+ return;
661
+ }
662
+ if (currentMode === "BRAINSTORM") {
663
+ const next = await brainstormModeLoop(task, backendUrl, rl, ask);
664
+ if (next === "switch") {
665
+ currentMode = await promptModeSwitch(currentMode, ask);
666
+ continue;
667
+ }
668
+ return;
669
+ }
670
+ }
671
+ }
672
+ function printAppHeader() {
673
+ console.log(chalk2.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
674
+ console.log(
675
+ chalk2.cyan("\u2551") + chalk2.bold.white(" \u{1F680} DEEP AGENTS - Autonomous Execution Engine ") + chalk2.cyan("\u2551")
676
+ );
677
+ console.log(chalk2.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
678
+ console.log("");
679
+ }
680
+ async function promptModeSwitch(current, ask) {
681
+ const others = ["PLAN", "BUILD", "BRAINSTORM"].filter((m) => m !== current);
682
+ console.log("");
683
+ console.log(
684
+ chalk2.gray("Switch to: ") + others.map((m, i) => chalk2.bold(`[${i + 1}] ${m}`)).join(chalk2.gray(" ")) + chalk2.gray(" [Enter to stay in ") + chalk2.bold(current) + chalk2.gray("]")
685
+ );
686
+ const answer = await ask(chalk2.gray("> "));
687
+ const n = parseInt(answer.trim(), 10);
688
+ if (n === 1) return others[0];
689
+ if (n === 2) return others[1];
690
+ return current;
691
+ }
692
+ async function planModeLoop(task, backendUrl, rl, ask) {
693
+ console.log(chalk2.bold("Task:"), chalk2.white(task));
694
+ console.log("");
695
+ console.log(chalk2.yellow("\u23F3 Fetching plan from backend..."));
696
+ let steps = [];
697
+ try {
698
+ const response = await fetch(`${backendUrl}/api/run`, {
699
+ method: "POST",
700
+ headers: { "Content-Type": "application/json" },
701
+ body: JSON.stringify({ task, agent_role: "engineer", agent_goal: "" })
702
+ });
703
+ if (!response.ok) {
704
+ console.error(chalk2.red("Backend error \u2014 cannot fetch plan."));
705
+ return "cancel";
706
+ }
707
+ const { run_id } = await response.json();
708
+ tui.displayConnected(run_id);
709
+ const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`);
710
+ const reader = streamResponse.body?.getReader();
711
+ const decoder = new TextDecoder();
712
+ if (!reader) throw new Error("No response body");
713
+ let buffer = "";
714
+ let planReceived = false;
715
+ outer: while (true) {
716
+ const { done, value } = await reader.read();
717
+ if (done) break;
718
+ buffer += decoder.decode(value, { stream: true });
719
+ const lines = buffer.split("\n");
720
+ buffer = lines.pop() || "";
721
+ for (const line of lines) {
722
+ if (!line.startsWith("data: ")) continue;
723
+ try {
724
+ const parsed = JSON.parse(line.slice(6));
725
+ if (parsed.type === "plan") {
726
+ steps = parsed.data["steps"] || [];
727
+ planReceived = true;
728
+ break outer;
729
+ } else if (parsed.type === "error") {
730
+ tui.displayError(parsed.data["content"] || "Unknown error");
731
+ return "cancel";
732
+ }
733
+ } catch {
734
+ }
735
+ }
736
+ }
737
+ reader.cancel();
738
+ if (!planReceived || steps.length === 0) {
739
+ console.log(chalk2.yellow("No plan received from backend."));
740
+ steps = ["(no steps returned \u2014 execute anyway?)"];
741
+ }
742
+ } catch (err) {
743
+ const msg = err instanceof Error ? err.message : String(err);
744
+ console.error(chalk2.red("Error: " + msg));
745
+ return "cancel";
746
+ }
747
+ displayPlanSteps(steps);
748
+ while (true) {
749
+ console.log(chalk2.gray("Options: ") + chalk2.bold("[Y]") + chalk2.gray(" Execute ") + chalk2.bold("[e]") + chalk2.gray(" Edit step ") + chalk2.bold("[s]") + chalk2.gray(" Switch mode ") + chalk2.bold("[Esc/n]") + chalk2.gray(" Cancel"));
750
+ const answer = (await ask(tui.displayPermissionPrompt("Execute this plan?"))).trim().toLowerCase();
751
+ if (answer === "" || answer === "y") {
752
+ return "BUILD";
753
+ }
754
+ if (answer === "n" || answer === "\x1B") {
755
+ console.log(chalk2.yellow("\nExecution cancelled.\n"));
756
+ return "cancel";
757
+ }
758
+ if (answer === "e" || answer === "edit") {
759
+ steps = await editPlanSteps(steps, ask);
760
+ displayPlanSteps(steps);
761
+ continue;
762
+ }
763
+ if (answer === "s" || answer === "switch") {
764
+ return "switch";
765
+ }
766
+ }
767
+ }
768
+ function displayPlanSteps(steps) {
769
+ console.log("");
770
+ console.log(chalk2.bold.cyan("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
771
+ console.log(chalk2.bold.cyan("\u2502") + chalk2.bold.white(" \u{1F4CB} Execution Plan ") + chalk2.bold.cyan("\u2502"));
772
+ console.log(chalk2.bold.cyan("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
773
+ steps.forEach((step, i) => {
774
+ const label = ` Step ${i + 1}: `;
775
+ const maxContentWidth = 57 - label.length;
776
+ const truncated = step.length > maxContentWidth ? step.substring(0, maxContentWidth - 3) + "..." : step;
777
+ const line = label + truncated;
778
+ const padded = line.padEnd(57);
779
+ console.log(chalk2.bold.cyan("\u2502") + chalk2.white(padded) + chalk2.bold.cyan("\u2502"));
780
+ });
781
+ console.log(chalk2.bold.cyan("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
782
+ console.log("");
783
+ }
784
+ async function editPlanSteps(steps, ask) {
785
+ console.log(chalk2.gray("Enter step number to edit, or press Enter to finish editing:"));
786
+ const numStr = await ask(chalk2.bold("Step #: "));
787
+ const n = parseInt(numStr.trim(), 10);
788
+ if (!isNaN(n) && n >= 1 && n <= steps.length) {
789
+ console.log(chalk2.gray(`Current: ${steps[n - 1]}`));
790
+ const updated = await ask(chalk2.bold("New text: "));
791
+ if (updated.trim()) steps[n - 1] = updated.trim();
792
+ }
793
+ return steps;
794
+ }
795
+ async function buildModeLoop(task, backendUrl, rl, ask) {
796
+ console.log(chalk2.bold("Task:"), chalk2.white(task));
797
+ tui.displayConnecting();
798
+ try {
799
+ const response = await fetch(`${backendUrl}/api/run`, {
800
+ method: "POST",
801
+ headers: { "Content-Type": "application/json" },
802
+ body: JSON.stringify({ task, agent_role: "engineer", agent_goal: "" })
803
+ });
804
+ if (!response.ok) {
805
+ console.error(chalk2.red("Backend error"));
806
+ return "done";
807
+ }
808
+ const { run_id } = await response.json();
809
+ tui.displayConnected(run_id);
810
+ console.log(chalk2.bold.green("\u2699\uFE0F Executing..."));
811
+ tui.displayStreamStart();
812
+ const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`);
813
+ const reader = streamResponse.body?.getReader();
814
+ const decoder = new TextDecoder();
815
+ if (!reader) throw new Error("No response body");
816
+ let buffer = "";
817
+ while (true) {
818
+ const { done, value } = await reader.read();
819
+ if (done) break;
820
+ buffer += decoder.decode(value, { stream: true });
821
+ const lines = buffer.split("\n");
822
+ buffer = lines.pop() || "";
823
+ for (const line of lines) {
824
+ if (!line.startsWith("data: ")) continue;
825
+ try {
826
+ const parsed = JSON.parse(line.slice(6));
827
+ const type = parsed.type;
828
+ if (type === "execution_complete") {
829
+ const summary = parsed.data["summary"] || "";
830
+ const count = parsed.data["todos_completed"] || 0;
831
+ tui.displayResults(summary, count);
832
+ tui.displayComplete(tui.getElapsedTime());
833
+ break;
834
+ } else if (type === "done") {
835
+ tui.displayEvent(type, { content: "Task completed successfully" });
836
+ tui.displayComplete(tui.getElapsedTime());
837
+ break;
838
+ } else if (type === "error") {
839
+ tui.displayError(parsed.data["content"] || "Unknown error");
840
+ break;
841
+ } else if (type !== "plan") {
842
+ tui.displayEvent(type, parsed.data);
843
+ }
844
+ } catch {
845
+ }
846
+ }
847
+ }
848
+ } catch (err) {
849
+ const msg = err instanceof Error ? err.message : String(err);
850
+ console.error(chalk2.red("Error: " + msg));
851
+ }
852
+ console.log("");
853
+ console.log(
854
+ chalk2.gray("Options: ") + chalk2.bold("[Enter]") + chalk2.gray(" Done ") + chalk2.bold("[s]") + chalk2.gray(" Switch mode")
855
+ );
856
+ const answer = (await ask(chalk2.bold("> "))).trim().toLowerCase();
857
+ if (answer === "s" || answer === "switch") return "switch";
858
+ return "done";
859
+ }
860
+ async function brainstormModeLoop(task, backendUrl, rl, ask) {
861
+ console.log(chalk2.bold("Starting topic:"), chalk2.white(task));
862
+ console.log(chalk2.gray('Ask follow-up questions. Type "done" to exit, "switch" to change mode.\n'));
863
+ let currentQuestion = task;
864
+ while (true) {
865
+ console.log(chalk2.bold.blue("\u{1F4A1} Thinking..."));
866
+ try {
867
+ const response = await fetch(`${backendUrl}/api/run`, {
868
+ method: "POST",
869
+ headers: { "Content-Type": "application/json" },
870
+ body: JSON.stringify({
871
+ task: currentQuestion,
872
+ agent_role: "brainstorm",
873
+ agent_goal: "Generate ideas, considerations, and suggestions. Be concise and helpful."
874
+ })
875
+ });
876
+ if (response.ok) {
877
+ const { run_id } = await response.json();
878
+ const streamResponse = await fetch(`${backendUrl}/api/stream/${run_id}`);
879
+ const reader = streamResponse.body?.getReader();
880
+ const decoder = new TextDecoder();
881
+ if (reader) {
882
+ let buffer = "";
883
+ let responseText = "";
884
+ outer: while (true) {
885
+ const { done, value } = await reader.read();
886
+ if (done) break;
887
+ buffer += decoder.decode(value, { stream: true });
888
+ const lines = buffer.split("\n");
889
+ buffer = lines.pop() || "";
890
+ for (const line of lines) {
891
+ if (!line.startsWith("data: ")) continue;
892
+ try {
893
+ const parsed = JSON.parse(line.slice(6));
894
+ if (parsed.type === "done" || parsed.type === "execution_complete") {
895
+ const summary = parsed.data["summary"] || "";
896
+ if (summary) responseText = summary;
897
+ break outer;
898
+ } else if (parsed.type === "thought" || parsed.type === "observation") {
899
+ const content = parsed.data["content"] || "";
900
+ if (content) responseText += content + "\n";
901
+ }
902
+ } catch {
903
+ }
904
+ }
905
+ }
906
+ reader.cancel();
907
+ if (responseText.trim()) {
908
+ tui.displayBrainstormResponse(responseText.trim());
909
+ } else {
910
+ console.log(chalk2.gray("(No response received from agent)"));
911
+ }
912
+ }
913
+ } else {
914
+ console.log(chalk2.red("Could not reach backend for brainstorm response."));
915
+ }
916
+ } catch (err) {
917
+ const msg = err instanceof Error ? err.message : String(err);
918
+ console.error(chalk2.red("Error: " + msg));
919
+ }
920
+ const followUp = await ask(chalk2.bold.blue("\u{1F4AC} You: "));
921
+ const lower = followUp.trim().toLowerCase();
922
+ if (lower === "done" || lower === "exit") return "done";
923
+ if (lower === "switch") return "switch";
924
+ if (!followUp.trim()) continue;
925
+ currentQuestion = followUp.trim();
926
+ }
927
+ }
928
+
929
+ // src/cli.ts
930
+ import { Command as Command15 } from "commander";
931
+ import { readFileSync as readFileSync2 } from "fs";
932
+ import { dirname as dirname2, join as join2 } from "path";
933
+ import { fileURLToPath as fileURLToPath4 } from "url";
934
+
935
+ // src/commands/install.ts
936
+ import { Command } from "commander";
937
+ import { execa as execa2 } from "execa";
938
+
939
+ // src/ui/spinner.ts
940
+ import ora from "ora";
941
+ import chalk3 from "chalk";
942
+ function createSpinner(text) {
943
+ return ora({ text, color: "cyan", spinner: "dots" });
944
+ }
945
+ function succeed(spinner, text) {
946
+ spinner.succeed(chalk3.green(text));
947
+ }
948
+ function fail(spinner, text) {
949
+ spinner.fail(chalk3.red(text));
950
+ }
951
+
952
+ // src/ui/logger.ts
953
+ import chalk4 from "chalk";
954
+ var log = {
955
+ step(msg) {
956
+ console.log(chalk4.cyan(" \u2192 ") + msg);
957
+ },
958
+ success(msg) {
959
+ console.log(chalk4.green(" \u2713 ") + msg);
960
+ },
961
+ error(msg) {
962
+ console.error(chalk4.red(" \u2717 ") + msg);
963
+ },
964
+ warn(msg) {
965
+ console.log(chalk4.yellow(" \u26A0 ") + msg);
966
+ },
967
+ dim(msg) {
968
+ console.log(chalk4.dim(" " + msg));
969
+ },
970
+ detail(label, value) {
971
+ console.log(chalk4.dim(" " + label + ": ") + value);
972
+ },
973
+ progress(current, total, label) {
974
+ const pct = Math.round(current / total * 100);
975
+ const filled = Math.round(current / total * 20);
976
+ const bar = chalk4.cyan("\u2588".repeat(filled)) + chalk4.dim("\u2591".repeat(20 - filled));
977
+ process.stdout.write(`\r [${bar}] ${chalk4.bold(`${pct}%`)} ${chalk4.dim(label)}`);
978
+ if (current >= total) process.stdout.write("\n");
979
+ }
980
+ };
981
+
982
+ // src/core/platform.ts
983
+ import os from "os";
984
+ function detectPlatform() {
985
+ const platform = os.platform();
986
+ const arch = os.arch();
987
+ if (platform === "darwin") {
988
+ return {
989
+ os: "mac",
990
+ arch: arch === "arm64" ? "arm64" : "x64",
991
+ dockerPlatform: arch === "arm64" ? "linux/arm64" : "linux/amd64"
992
+ };
993
+ }
994
+ if (platform === "linux") {
995
+ return {
996
+ os: "linux",
997
+ arch: arch === "arm64" ? "arm64" : "x64",
998
+ dockerPlatform: arch === "arm64" ? "linux/arm64" : "linux/amd64"
999
+ };
1000
+ }
1001
+ if (platform === "win32") {
1002
+ return {
1003
+ os: "windows",
1004
+ arch: "x64",
1005
+ dockerPlatform: "linux/amd64"
1006
+ };
1007
+ }
1008
+ throw new Error(`Unsupported platform: ${platform} ${arch}`);
1009
+ }
1010
+
1011
+ // src/core/docker.ts
1012
+ import { existsSync } from "fs";
1013
+ import { execa } from "execa";
1014
+ var CONTAINER_NAME = "nexus";
1015
+ var DOCKER_DESKTOP_APP_PATH = "/Applications/Docker.app";
1016
+ async function isDockerInstalled() {
1017
+ try {
1018
+ await execa("docker", ["info"]);
1019
+ return true;
1020
+ } catch {
1021
+ return false;
1022
+ }
1023
+ }
1024
+ async function isDockerInstalledButNotRunning() {
1025
+ try {
1026
+ await execa("docker", ["--version"]);
1027
+ return true;
1028
+ } catch {
1029
+ return false;
1030
+ }
1031
+ }
1032
+ function dockerDesktopExists() {
1033
+ return existsSync(DOCKER_DESKTOP_APP_PATH);
1034
+ }
1035
+ async function ensureHomebrew() {
1036
+ try {
1037
+ await execa("which", ["brew"]);
1038
+ log.dim("Homebrew is already installed.");
1039
+ return;
1040
+ } catch {
1041
+ }
1042
+ log.step("Installing Homebrew...");
1043
+ try {
1044
+ await execa("/bin/bash", [
1045
+ "-c",
1046
+ "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
1047
+ ], {
1048
+ stdio: "inherit",
1049
+ env: { ...process.env, NONINTERACTIVE: "1" }
1050
+ });
1051
+ } catch {
1052
+ throw new Error(
1053
+ 'Failed to install Homebrew automatically.\n\n Install Homebrew manually:\n /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"\n\n Then re-run:\n buildwithnexus init'
1054
+ );
1055
+ }
1056
+ try {
1057
+ await execa("which", ["brew"]);
1058
+ } catch {
1059
+ const armPath = "/opt/homebrew/bin";
1060
+ const intelPath = "/usr/local/bin";
1061
+ process.env.PATH = `${armPath}:${intelPath}:${process.env.PATH}`;
1062
+ log.dim("Added Homebrew paths to PATH for this session.");
1063
+ }
1064
+ try {
1065
+ await execa("brew", ["--version"]);
1066
+ log.success("Homebrew installed successfully.");
1067
+ } catch {
1068
+ throw new Error(
1069
+ "Homebrew was installed but is not available on PATH.\n\n Try opening a new terminal and re-running:\n buildwithnexus init"
1070
+ );
1071
+ }
1072
+ }
1073
+ async function installDocker(platform) {
1074
+ const p = platform ?? detectPlatform();
1075
+ switch (p.os) {
1076
+ case "mac": {
1077
+ if (await isDockerInstalled()) {
1078
+ log.success("Docker is already running.");
1079
+ return;
1080
+ }
1081
+ if (dockerDesktopExists()) {
1082
+ log.step(`Docker Desktop found at ${DOCKER_DESKTOP_APP_PATH} but not running. Attempting to start...`);
1083
+ let launched = false;
1084
+ log.dim(`Trying: open ${DOCKER_DESKTOP_APP_PATH}`);
1085
+ try {
1086
+ await execa("open", [DOCKER_DESKTOP_APP_PATH]);
1087
+ launched = true;
1088
+ log.dim(`Launch command succeeded via ${DOCKER_DESKTOP_APP_PATH}`);
1089
+ } catch {
1090
+ log.warn(`Could not launch via ${DOCKER_DESKTOP_APP_PATH} \u2014 trying fallback...`);
1091
+ }
1092
+ if (!launched) {
1093
+ log.dim("Trying: open -a Docker");
1094
+ try {
1095
+ await execa("open", ["-a", "Docker"]);
1096
+ launched = true;
1097
+ log.dim("Launch command succeeded via open -a Docker");
1098
+ } catch {
1099
+ log.warn("Both launch attempts failed.");
1100
+ }
1101
+ }
1102
+ if (launched) {
1103
+ log.step("Docker Desktop is starting up. Waiting for the daemon to be ready (up to 120s)...");
1104
+ try {
1105
+ await waitForDockerDaemon(12e4);
1106
+ return;
1107
+ } catch {
1108
+ log.warn("Docker Desktop was launched but the daemon did not become ready in time.");
1109
+ }
1110
+ } else {
1111
+ log.warn("Could not launch Docker Desktop. Will fall back to reinstalling via Homebrew.");
1112
+ }
1113
+ } else {
1114
+ log.step(`Docker Desktop not found at ${DOCKER_DESKTOP_APP_PATH}.`);
1115
+ }
1116
+ log.step("Installing Docker Desktop via Homebrew...");
1117
+ await ensureHomebrew();
1118
+ try {
1119
+ await execa("brew", ["install", "--cask", "docker"], {
1120
+ stdio: "inherit",
1121
+ timeout: 6e4
1122
+ });
1123
+ } catch (err) {
1124
+ const e = err;
1125
+ if (e.killed && e.signal === "SIGINT") {
1126
+ throw new Error("Docker installation cancelled by user (Ctrl+C)");
1127
+ }
1128
+ if (e.timedOut) {
1129
+ throw new Error(
1130
+ "Docker installation via Homebrew timed out after 60 seconds.\n\n The password prompt may be waiting for input. Try installing manually:\n brew install --cask docker\n\n After installing, re-run:\n buildwithnexus init"
1131
+ );
1132
+ }
1133
+ throw new Error(
1134
+ "Failed to install Docker via Homebrew.\n\n Try installing Docker Desktop manually:\n https://www.docker.com/products/docker-desktop\n\n After installing, re-run:\n buildwithnexus init"
1135
+ );
1136
+ }
1137
+ log.step("Launching Docker Desktop...");
1138
+ let postInstallLaunched = false;
1139
+ log.dim(`Trying: open ${DOCKER_DESKTOP_APP_PATH}`);
1140
+ try {
1141
+ await execa("open", [DOCKER_DESKTOP_APP_PATH]);
1142
+ postInstallLaunched = true;
1143
+ log.dim(`Launch command succeeded via ${DOCKER_DESKTOP_APP_PATH}`);
1144
+ } catch {
1145
+ log.warn(`Could not launch via ${DOCKER_DESKTOP_APP_PATH} \u2014 trying fallback...`);
1146
+ }
1147
+ if (!postInstallLaunched) {
1148
+ log.dim("Trying: open -a Docker");
1149
+ try {
1150
+ await execa("open", ["-a", "Docker"]);
1151
+ postInstallLaunched = true;
1152
+ log.dim("Launch command succeeded via open -a Docker");
1153
+ } catch {
1154
+ log.warn("Both launch attempts failed after install.");
1155
+ }
1156
+ }
1157
+ if (!postInstallLaunched) {
1158
+ throw new Error(
1159
+ "Docker Desktop was installed but could not be started automatically.\n\n Next steps:\n 1. Open Docker Desktop manually from your Applications folder\n 2. Wait for the whale icon to appear in the menu bar\n 3. Re-run: buildwithnexus init"
1160
+ );
1161
+ }
1162
+ log.step("Docker Desktop is starting up. Waiting for the daemon to be ready (up to 120s)...");
1163
+ await waitForDockerDaemon(12e4);
1164
+ break;
1165
+ }
1166
+ case "linux": {
1167
+ const linuxBinaryExists = await isDockerInstalledButNotRunning();
1168
+ if (linuxBinaryExists) {
1169
+ log.step("Docker is installed but the daemon is not running.");
1170
+ log.step("Starting Docker daemon...");
1171
+ try {
1172
+ await execa("sudo", ["systemctl", "start", "docker"], { stdio: "inherit" });
1173
+ log.dim("Started Docker daemon via systemctl.");
1174
+ } catch {
1175
+ try {
1176
+ await execa("sudo", ["service", "docker", "start"], { stdio: "inherit" });
1177
+ log.dim("Started Docker daemon via service command.");
1178
+ } catch {
1179
+ throw new Error(
1180
+ "Docker is installed but the daemon could not be started.\n\n Try starting it manually:\n sudo systemctl start docker\n\n Then re-run:\n buildwithnexus init"
1181
+ );
1182
+ }
1183
+ }
1184
+ log.step("Waiting for Docker...");
1185
+ await waitForDockerDaemon(3e4);
1186
+ return;
1187
+ }
1188
+ log.step("Installing Docker...");
1189
+ log.warn("This may require your sudo password.");
1190
+ log.dim("Running official Docker install script from https://get.docker.com ...");
1191
+ try {
1192
+ const { stdout: script } = await execa("curl", ["-fsSL", "https://get.docker.com"]);
1193
+ await execa("sudo", ["sh", "-c", script], { stdio: "inherit" });
1194
+ log.success("Docker installed successfully.");
1195
+ } catch {
1196
+ throw new Error(
1197
+ "Failed to install Docker on Linux.\n\n Try installing manually:\n curl -fsSL https://get.docker.com | sudo sh\n\n After installing, re-run:\n buildwithnexus init"
1198
+ );
1199
+ }
1200
+ log.dim("Adding current user to docker group...");
1201
+ try {
1202
+ const user = (await execa("whoami")).stdout.trim();
1203
+ await execa("sudo", ["usermod", "-aG", "docker", user]);
1204
+ log.dim(`Added user '${user}' to docker group (may require re-login for effect).`);
1205
+ } catch {
1206
+ log.warn("Could not add user to docker group. You may need sudo for docker commands.");
1207
+ }
1208
+ log.step("Starting Docker daemon...");
1209
+ try {
1210
+ await execa("sudo", ["systemctl", "start", "docker"], { stdio: "inherit" });
1211
+ log.dim("Started Docker daemon via systemctl.");
1212
+ } catch {
1213
+ try {
1214
+ await execa("sudo", ["service", "docker", "start"], { stdio: "inherit" });
1215
+ log.dim("Started Docker daemon via service command.");
1216
+ } catch {
1217
+ throw new Error(
1218
+ "Docker was installed but the daemon could not be started.\n\n Try starting it manually:\n sudo systemctl start docker\n\n Then re-run:\n buildwithnexus init"
1219
+ );
1220
+ }
1221
+ }
1222
+ log.step("Waiting for Docker...");
1223
+ await waitForDockerDaemon(3e4);
1224
+ break;
1225
+ }
1226
+ case "windows": {
1227
+ const winBinaryExists = await isDockerInstalledButNotRunning();
1228
+ if (winBinaryExists) {
1229
+ log.step("Docker Desktop is installed but not running. Attempting to start...");
1230
+ log.step("Launching Docker...");
1231
+ try {
1232
+ await execa("powershell", ["-Command", "Start-Process 'Docker Desktop'"], { stdio: "inherit" });
1233
+ log.dim("Docker Desktop launch command sent.");
1234
+ } catch {
1235
+ log.warn("Could not launch Docker Desktop automatically. It may need to be started manually.");
1236
+ }
1237
+ log.step("Waiting for Docker...");
1238
+ try {
1239
+ await waitForDockerDaemon(12e4);
1240
+ } catch {
1241
+ throw new Error(
1242
+ "Docker Desktop did not start within 120 seconds.\n\n Next steps:\n 1. Open Docker Desktop manually from the Start Menu\n 2. Wait for the whale icon to appear in the system tray\n 3. Re-run: buildwithnexus init"
1243
+ );
1244
+ }
1245
+ return;
1246
+ }
1247
+ log.step("Installing Docker Desktop...");
1248
+ let installed = false;
1249
+ try {
1250
+ await execa("choco", ["--version"]);
1251
+ log.dim("Chocolatey detected. Installing Docker Desktop via choco...");
1252
+ try {
1253
+ await execa("choco", ["install", "docker-desktop", "-y"], { stdio: "inherit" });
1254
+ installed = true;
1255
+ log.success("Docker Desktop installed via Chocolatey.");
1256
+ } catch {
1257
+ log.warn("Chocolatey install failed. Falling back to direct download...");
1258
+ }
1259
+ } catch {
1260
+ log.dim("Chocolatey not found. Using direct download...");
1261
+ }
1262
+ if (!installed) {
1263
+ log.dim("Downloading Docker Desktop installer from docker.com...");
1264
+ try {
1265
+ await execa("powershell", [
1266
+ "-Command",
1267
+ "Invoke-WebRequest -Uri 'https://desktop.docker.com/win/main/amd64/Docker Desktop Installer.exe' -OutFile '$env:TEMP\\DockerInstaller.exe'; & '$env:TEMP\\DockerInstaller.exe' Install --quiet; Remove-Item '$env:TEMP\\DockerInstaller.exe' -Force -ErrorAction SilentlyContinue"
1268
+ ], { stdio: "inherit" });
1269
+ installed = true;
1270
+ log.success("Docker Desktop installed via direct download.");
1271
+ } catch {
1272
+ throw new Error(
1273
+ "Failed to install Docker Desktop on Windows.\n\n Please install Docker Desktop manually:\n 1. Download from https://www.docker.com/products/docker-desktop\n 2. Run the installer and follow the prompts\n 3. Start Docker Desktop\n 4. Re-run: buildwithnexus init"
1274
+ );
1275
+ }
1276
+ }
1277
+ log.step("Launching Docker...");
1278
+ try {
1279
+ await execa("powershell", ["-Command", "Start-Process 'Docker Desktop'"], { stdio: "inherit" });
1280
+ log.dim("Docker Desktop launch command sent.");
1281
+ } catch {
1282
+ log.warn("Could not launch Docker Desktop automatically after install.");
1283
+ }
1284
+ log.step("Waiting for Docker...");
1285
+ try {
1286
+ await waitForDockerDaemon(12e4);
1287
+ } catch {
1288
+ throw new Error(
1289
+ "Docker Desktop was installed but did not start within 120 seconds.\n\n Next steps:\n 1. You may need to restart your computer for Docker to work\n 2. Open Docker Desktop from the Start Menu\n 3. Wait for the whale icon to appear in the system tray\n 4. Re-run: buildwithnexus init"
1290
+ );
1291
+ }
1292
+ break;
1293
+ }
1294
+ default:
1295
+ throw new Error(`Unsupported platform: ${p.os}`);
1296
+ }
1297
+ }
1298
+ async function waitForDockerDaemon(timeoutMs) {
1299
+ const start = Date.now();
1300
+ log.step("Waiting for Docker daemon...");
1301
+ while (Date.now() - start < timeoutMs) {
1302
+ try {
1303
+ await execa("docker", ["info"]);
1304
+ log.success("Docker daemon is ready");
1305
+ return;
1306
+ } catch {
1307
+ }
1308
+ await new Promise((r) => setTimeout(r, 3e3));
1309
+ }
1310
+ throw new Error(
1311
+ `Docker daemon did not become ready within ${Math.round(timeoutMs / 1e3)}s.
1312
+
1313
+ Please ensure Docker is running, then re-run:
1314
+ buildwithnexus init`
1315
+ );
1316
+ }
1317
+ async function imageExistsLocally(image, tag) {
1318
+ const ref = `${image}:${tag}`;
1319
+ try {
1320
+ await execa("docker", ["image", "inspect", ref]);
1321
+ return true;
1322
+ } catch {
1323
+ return false;
33
1324
  }
34
1325
  }
35
- function showBanner() {
36
- console.log(BANNER);
37
- console.log(chalk.dim(` v${getVersion()} \xB7 buildwithnexus.dev
38
- `));
1326
+ async function pullImage(image, tag) {
1327
+ const ref = `${image}:${tag}`;
1328
+ log.step(`Pulling image ${ref}...`);
1329
+ try {
1330
+ await execa("docker", ["pull", ref], { stdio: "inherit" });
1331
+ log.success(`Image ${ref} pulled`);
1332
+ } catch (err) {
1333
+ log.error(`Failed to pull image ${ref}`);
1334
+ throw err;
1335
+ }
39
1336
  }
40
- function showPhase(phase, total, description) {
41
- const progress = chalk.cyan(`[${phase}/${total}]`);
42
- console.log(`
43
- ${progress} ${chalk.bold(description)}`);
1337
+ async function startNexus(keys, config) {
1338
+ log.step("Starting NEXUS container...");
1339
+ try {
1340
+ await execa("docker", [
1341
+ "run",
1342
+ "-d",
1343
+ "--name",
1344
+ CONTAINER_NAME,
1345
+ "-e",
1346
+ `ANTHROPIC_API_KEY=${keys.anthropic}`,
1347
+ "-e",
1348
+ `OPENAI_API_KEY=${keys.openai}`,
1349
+ "-p",
1350
+ `${config.port}:${config.port}`,
1351
+ "buildwithnexus/nexus:latest"
1352
+ ]);
1353
+ log.success(`NEXUS container started on port ${config.port}`);
1354
+ } catch (err) {
1355
+ log.error("Failed to start NEXUS container");
1356
+ throw err;
1357
+ }
44
1358
  }
45
- function showSecurityPosture() {
46
- const lines = [
47
- "",
48
- chalk.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),
49
- chalk.bold(" \u2551 ") + chalk.bold.green("Security Posture") + chalk.bold(" \u2551"),
50
- chalk.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
51
- chalk.bold(" \u2551 ") + chalk.green("\u2713") + chalk.white(" Triple-nested isolation: Host \u2192 VM \u2192 Docker \u2192 KVM".padEnd(54)) + chalk.bold("\u2551"),
52
- chalk.bold(" \u2551 ") + chalk.green("\u2713") + chalk.white(" Network hardened: UFW deny-all, allow 22/80/443/4200".padEnd(54)) + chalk.bold("\u2551"),
53
- chalk.bold(" \u2551 ") + chalk.green("\u2713") + chalk.white(" All databases encrypted at rest (AES-256-CBC)".padEnd(54)) + chalk.bold("\u2551"),
54
- chalk.bold(" \u2551 ") + chalk.green("\u2713") + chalk.white(" API keys never embedded in VM \u2014 delivered via SCP".padEnd(54)) + chalk.bold("\u2551"),
55
- chalk.bold(" \u2551 ") + chalk.green("\u2713") + chalk.white(" SSH-only communication (no exposed network ports)".padEnd(54)) + chalk.bold("\u2551"),
56
- chalk.bold(" \u2551 ") + chalk.green("\u2713") + chalk.white(" DLP: secret detection, shell escaping, output redaction".padEnd(54)) + chalk.bold("\u2551"),
57
- chalk.bold(" \u2551 ") + chalk.green("\u2713") + chalk.white(" HMAC integrity verification on all key files".padEnd(54)) + chalk.bold("\u2551"),
58
- chalk.bold(" \u2551 ") + chalk.green("\u2713") + chalk.white(" Docker: --read-only, no-new-privileges, cap-drop=ALL".padEnd(54)) + chalk.bold("\u2551"),
59
- chalk.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
60
- chalk.bold(" \u2551 ") + chalk.dim("Full details: https://buildwithnexus.dev/security".padEnd(55)) + chalk.bold("\u2551"),
61
- chalk.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),
62
- ""
63
- ];
64
- console.log(lines.join("\n"));
1359
+ async function stopNexus() {
1360
+ log.step("Stopping NEXUS container...");
1361
+ try {
1362
+ await execa("docker", ["rm", "-f", CONTAINER_NAME]);
1363
+ log.success("NEXUS container stopped and removed");
1364
+ } catch (err) {
1365
+ log.error("Failed to stop NEXUS container");
1366
+ throw err;
1367
+ }
65
1368
  }
66
- function showCompletion(urls) {
67
- const lines = [
68
- "",
69
- chalk.green(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),
70
- chalk.green(" \u2551 ") + chalk.bold.green("NEXUS Runtime is Live!") + chalk.green(" \u2551"),
71
- chalk.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
72
- chalk.green(" \u2551 ") + chalk.white(`Connect: ${urls.ssh}`.padEnd(55)) + chalk.green("\u2551")
73
- ];
74
- if (urls.remote) {
75
- lines.push(chalk.green(" \u2551 ") + chalk.white(`Remote: ${urls.remote}`.padEnd(55)) + chalk.green("\u2551"));
1369
+ async function dockerExec(command) {
1370
+ try {
1371
+ const { stdout, stderr } = await execa("docker", ["exec", CONTAINER_NAME, "sh", "-c", command]);
1372
+ return { stdout, stderr, code: 0 };
1373
+ } catch (err) {
1374
+ const e = err;
1375
+ return { stdout: e.stdout ?? "", stderr: e.stderr ?? "", code: e.exitCode ?? 1 };
76
1376
  }
77
- lines.push(
78
- chalk.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
79
- chalk.green(" \u2551 ") + chalk.dim("Quick Start:".padEnd(55)) + chalk.green("\u2551"),
80
- chalk.green(" \u2551 ") + chalk.white(" buildwithnexus - Interactive shell".padEnd(55)) + chalk.green("\u2551"),
81
- chalk.green(" \u2551 ") + chalk.white(" buildwithnexus brainstorm - Brainstorm ideas".padEnd(55)) + chalk.green("\u2551"),
82
- chalk.green(" \u2551 ") + chalk.white(" buildwithnexus status - Check health".padEnd(55)) + chalk.green("\u2551"),
83
- chalk.green(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"),
84
- chalk.green(" \u2551 ") + chalk.dim("All commands:".padEnd(55)) + chalk.green("\u2551"),
85
- chalk.green(" \u2551 ") + chalk.white(" buildwithnexus stop/start/update/logs/ssh/destroy".padEnd(55)) + chalk.green("\u2551"),
86
- chalk.green(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),
87
- ""
88
- );
89
- console.log(lines.join("\n"));
90
1377
  }
91
- var BANNER;
92
- var init_banner = __esm({
93
- "src/ui/banner.ts"() {
94
- "use strict";
95
- BANNER = `
96
- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
97
- \u2551 ${chalk.bold.cyan("B U I L D W I T H N E X U S")} \u2551
98
- \u2551 \u2551
99
- \u2551 Autonomous AI Runtime \xB7 Nested Isolation \u2551
100
- \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
101
- `;
1378
+ async function isNexusRunning() {
1379
+ try {
1380
+ const { stdout } = await execa("docker", [
1381
+ "ps",
1382
+ "--filter",
1383
+ `name=^/${CONTAINER_NAME}$`,
1384
+ "--format",
1385
+ "{{.Names}}"
1386
+ ]);
1387
+ return stdout.trim() === CONTAINER_NAME;
1388
+ } catch {
1389
+ return false;
1390
+ }
1391
+ }
1392
+
1393
+ // src/commands/install.ts
1394
+ var installCommand = new Command("install").description("Install Docker and configure system prerequisites").action(async () => {
1395
+ const spinner = createSpinner("");
1396
+ log.step("Checking Docker installation...");
1397
+ const alreadyInstalled = await isDockerInstalled();
1398
+ if (alreadyInstalled) {
1399
+ try {
1400
+ const { stdout } = await execa2("docker", ["--version"]);
1401
+ log.success(`Docker is already installed and running: ${stdout.trim()}`);
1402
+ } catch {
1403
+ log.success("Docker is already installed and running.");
1404
+ }
1405
+ return;
1406
+ }
1407
+ const platform = detectPlatform();
1408
+ log.step(`Installing Docker for ${platform.os} (${platform.arch})...`);
1409
+ try {
1410
+ await installDocker(platform);
1411
+ } catch (err) {
1412
+ const msg = err instanceof Error ? err.message : String(err);
1413
+ log.error(`Docker installation failed: ${msg}`);
1414
+ process.exit(1);
1415
+ }
1416
+ spinner.text = "Verifying Docker installation...";
1417
+ spinner.start();
1418
+ const verified = await isDockerInstalled();
1419
+ if (!verified) {
1420
+ fail(spinner, "Docker installation could not be verified");
1421
+ log.error(
1422
+ "Docker was installed but is not responding.\n\n Please ensure Docker is running, then verify with:\n docker --version\n\n Once Docker is running, you can proceed with:\n buildwithnexus init"
1423
+ );
1424
+ process.exit(1);
1425
+ }
1426
+ try {
1427
+ const { stdout } = await execa2("docker", ["--version"]);
1428
+ succeed(spinner, `Docker verified: ${stdout.trim()}`);
1429
+ } catch {
1430
+ succeed(spinner, "Docker is installed and running");
102
1431
  }
1432
+ log.success("\nDocker setup complete! You can now run:\n\n buildwithnexus init\n");
103
1433
  });
104
1434
 
1435
+ // src/commands/init.ts
1436
+ init_banner();
1437
+ import { Command as Command2 } from "commander";
1438
+ import chalk7 from "chalk";
1439
+
1440
+ // src/ui/prompts.ts
1441
+ import { confirm as confirm2, password } from "@inquirer/prompts";
1442
+ import chalk6 from "chalk";
1443
+
105
1444
  // src/core/dlp.ts
106
1445
  import crypto from "crypto";
107
- import fs from "fs";
108
- import path from "path";
109
- function yamlEscape(value) {
110
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\0/g, "");
111
- }
1446
+ import fs2 from "fs";
1447
+ import path3 from "path";
1448
+ var NEXUS_HOME = path3.join(process.env.HOME || "~", ".buildwithnexus");
1449
+ var SECRET_PATTERNS = [
1450
+ /sk-ant-api03-[A-Za-z0-9_-]{20,}/g,
1451
+ // Anthropic API key
1452
+ /sk-[A-Za-z0-9]{20,}/g,
1453
+ // OpenAI API key
1454
+ /AIza[A-Za-z0-9_-]{35}/g
1455
+ // Google AI API key
1456
+ ];
1457
+ var FORBIDDEN_KEY_CHARS = /[\n\r\t'"\\`${}();&|<>!#%^]/;
1458
+ var KEY_VALIDATORS = {
1459
+ ANTHROPIC_API_KEY: /^sk-ant-[A-Za-z0-9_-]{20,}$/,
1460
+ OPENAI_API_KEY: /^sk-[A-Za-z0-9_-]{20,}$/,
1461
+ GOOGLE_API_KEY: /^AIza[A-Za-z0-9_-]{35,}$/,
1462
+ NEXUS_MASTER_SECRET: /^[A-Za-z0-9_-]{20,64}$/
1463
+ };
112
1464
  function shellEscape(value) {
113
1465
  if (value.includes("\0")) {
114
1466
  throw new DlpViolation("Null byte in shell argument");
@@ -139,6 +1491,12 @@ function redactError(err) {
139
1491
  }
140
1492
  return new Error(redact(String(err)));
141
1493
  }
1494
+ var DlpViolation = class extends Error {
1495
+ constructor(message) {
1496
+ super(message);
1497
+ this.name = "DlpViolation";
1498
+ }
1499
+ };
142
1500
  function validateKeyValue(keyName, value) {
143
1501
  if (FORBIDDEN_KEY_CHARS.test(value)) {
144
1502
  throw new DlpViolation(
@@ -169,21 +1527,22 @@ function validateAllKeys(keys) {
169
1527
  }
170
1528
  return violations;
171
1529
  }
1530
+ var HMAC_PATH = path3.join(NEXUS_HOME, ".keys.hmac");
172
1531
  function computeFileHmac(filePath, secret) {
173
- const content = fs.readFileSync(filePath);
1532
+ const content = fs2.readFileSync(filePath);
174
1533
  return crypto.createHmac("sha256", secret).update(content).digest("hex");
175
1534
  }
176
1535
  function sealKeysFile(keysPath, masterSecret) {
177
1536
  const hmac = computeFileHmac(keysPath, masterSecret);
178
- fs.writeFileSync(HMAC_PATH, hmac, { mode: 384 });
1537
+ fs2.writeFileSync(HMAC_PATH, hmac, { mode: 384 });
179
1538
  }
180
1539
  function verifyKeysFile(keysPath, masterSecret) {
181
- const keysExist = fs.existsSync(keysPath);
182
- const hmacExist = fs.existsSync(HMAC_PATH);
1540
+ const keysExist = fs2.existsSync(keysPath);
1541
+ const hmacExist = fs2.existsSync(HMAC_PATH);
183
1542
  if (!keysExist && !hmacExist) return true;
184
1543
  if (keysExist && !hmacExist) return false;
185
1544
  try {
186
- const stored = fs.readFileSync(HMAC_PATH, "utf-8").trim();
1545
+ const stored = fs2.readFileSync(HMAC_PATH, "utf-8").trim();
187
1546
  const computed = computeFileHmac(keysPath, masterSecret);
188
1547
  return crypto.timingSafeEqual(
189
1548
  Buffer.from(stored, "hex"),
@@ -193,624 +1552,38 @@ function verifyKeysFile(keysPath, masterSecret) {
193
1552
  return false;
194
1553
  }
195
1554
  }
1555
+ var AUDIT_PATH = path3.join(NEXUS_HOME, "audit.log");
1556
+ var MAX_AUDIT_SIZE = 10 * 1024 * 1024;
196
1557
  function audit(event, detail = "") {
197
1558
  try {
198
- const dir = path.dirname(AUDIT_PATH);
199
- if (!fs.existsSync(dir)) return;
200
- if (fs.existsSync(AUDIT_PATH)) {
201
- const stat = fs.statSync(AUDIT_PATH);
1559
+ const dir = path3.dirname(AUDIT_PATH);
1560
+ if (!fs2.existsSync(dir)) return;
1561
+ if (fs2.existsSync(AUDIT_PATH)) {
1562
+ const stat = fs2.statSync(AUDIT_PATH);
202
1563
  if (stat.size > MAX_AUDIT_SIZE) {
203
1564
  const rotated = AUDIT_PATH + ".1";
204
- if (fs.existsSync(rotated)) fs.unlinkSync(rotated);
205
- fs.renameSync(AUDIT_PATH, rotated);
1565
+ if (fs2.existsSync(rotated)) fs2.unlinkSync(rotated);
1566
+ fs2.renameSync(AUDIT_PATH, rotated);
206
1567
  try {
207
- fs.chmodSync(rotated, 384);
1568
+ fs2.chmodSync(rotated, 384);
208
1569
  } catch {
209
1570
  }
210
1571
  }
211
1572
  }
212
1573
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} | ${event} | ${redact(detail)}
213
1574
  `;
214
- fs.appendFileSync(AUDIT_PATH, line, { mode: 384 });
215
- try {
216
- fs.chmodSync(AUDIT_PATH, 384);
217
- } catch {
218
- }
219
- } catch {
220
- }
221
- }
222
- function scrubEnv() {
223
- const clean = { ...process.env };
224
- for (const key of SCRUB_KEYS) {
225
- delete clean[key];
226
- }
227
- for (const [key, value] of Object.entries(clean)) {
228
- if (value) {
229
- for (const pattern of SECRET_PATTERNS) {
230
- pattern.lastIndex = 0;
231
- if (pattern.test(value)) {
232
- delete clean[key];
233
- break;
234
- }
235
- }
236
- }
237
- }
238
- return clean;
239
- }
240
- var NEXUS_HOME, SECRET_PATTERNS, FORBIDDEN_KEY_CHARS, KEY_VALIDATORS, DlpViolation, HMAC_PATH, AUDIT_PATH, MAX_AUDIT_SIZE, SCRUB_KEYS;
241
- var init_dlp = __esm({
242
- "src/core/dlp.ts"() {
243
- "use strict";
244
- NEXUS_HOME = path.join(process.env.HOME || "~", ".buildwithnexus");
245
- SECRET_PATTERNS = [
246
- /sk-ant-api03-[A-Za-z0-9_-]{20,}/g,
247
- // Anthropic API key
248
- /sk-[A-Za-z0-9]{20,}/g,
249
- // OpenAI API key
250
- /AIza[A-Za-z0-9_-]{35}/g
251
- // Google AI API key
252
- ];
253
- FORBIDDEN_KEY_CHARS = /[\n\r\t'"\\`${}();&|<>!#%^]/;
254
- KEY_VALIDATORS = {
255
- ANTHROPIC_API_KEY: /^sk-ant-[A-Za-z0-9_-]{20,}$/,
256
- OPENAI_API_KEY: /^sk-[A-Za-z0-9_-]{20,}$/,
257
- GOOGLE_API_KEY: /^AIza[A-Za-z0-9_-]{35,}$/,
258
- NEXUS_MASTER_SECRET: /^[A-Za-z0-9_-]{20,64}$/
259
- };
260
- DlpViolation = class extends Error {
261
- constructor(message) {
262
- super(message);
263
- this.name = "DlpViolation";
264
- }
265
- };
266
- HMAC_PATH = path.join(NEXUS_HOME, ".keys.hmac");
267
- AUDIT_PATH = path.join(NEXUS_HOME, "audit.log");
268
- MAX_AUDIT_SIZE = 10 * 1024 * 1024;
269
- SCRUB_KEYS = [
270
- "ANTHROPIC_API_KEY",
271
- "OPENAI_API_KEY",
272
- "GOOGLE_API_KEY",
273
- "NEXUS_MASTER_SECRET",
274
- "NEXUS_SECRET",
275
- "AWS_SECRET_ACCESS_KEY",
276
- "AWS_SESSION_TOKEN",
277
- "GITHUB_TOKEN",
278
- "GH_TOKEN",
279
- "NPM_TOKEN",
280
- "DOCKER_PASSWORD",
281
- "CI_JOB_TOKEN"
282
- ];
283
- }
284
- });
285
-
286
- // src/core/secrets.ts
287
- var secrets_exports = {};
288
- __export(secrets_exports, {
289
- CONFIG_PATH: () => CONFIG_PATH,
290
- KEYS_PATH: () => KEYS_PATH,
291
- NEXUS_HOME: () => NEXUS_HOME2,
292
- ensureHome: () => ensureHome,
293
- generateMasterSecret: () => generateMasterSecret,
294
- loadConfig: () => loadConfig,
295
- loadKeys: () => loadKeys,
296
- maskKey: () => maskKey,
297
- saveConfig: () => saveConfig,
298
- saveKeys: () => saveKeys
299
- });
300
- import fs2 from "fs";
301
- import path2 from "path";
302
- import crypto2 from "crypto";
303
- function ensureHome() {
304
- fs2.mkdirSync(NEXUS_HOME2, { recursive: true, mode: 448 });
305
- fs2.mkdirSync(path2.join(NEXUS_HOME2, "vm", "images"), { recursive: true, mode: 448 });
306
- fs2.mkdirSync(path2.join(NEXUS_HOME2, "vm", "configs"), { recursive: true, mode: 448 });
307
- fs2.mkdirSync(path2.join(NEXUS_HOME2, "vm", "logs"), { recursive: true, mode: 448 });
308
- fs2.mkdirSync(path2.join(NEXUS_HOME2, "ssh"), { recursive: true, mode: 448 });
309
- }
310
- function generateMasterSecret() {
311
- return crypto2.randomBytes(32).toString("base64url");
312
- }
313
- function saveConfig(config) {
314
- const { masterSecret: _secret, ...safeConfig } = config;
315
- fs2.writeFileSync(CONFIG_PATH, JSON.stringify(safeConfig, null, 2), { mode: 384 });
316
- }
317
- function loadConfig() {
318
- if (!fs2.existsSync(CONFIG_PATH)) return null;
319
- return JSON.parse(fs2.readFileSync(CONFIG_PATH, "utf-8"));
320
- }
321
- function saveKeys(keys) {
322
- const violations = validateAllKeys(keys);
323
- if (violations.length > 0) {
324
- throw new DlpViolation(`Key validation failed: ${violations.join("; ")}`);
325
- }
326
- const lines = Object.entries(keys).filter(([, v]) => v).map(([k, v]) => `${k}=${v}`);
327
- fs2.writeFileSync(KEYS_PATH, lines.join("\n") + "\n", { mode: 384 });
328
- sealKeysFile(KEYS_PATH, keys.NEXUS_MASTER_SECRET);
329
- audit("keys_saved", `${Object.keys(keys).filter((k) => keys[k]).length} keys saved`);
330
- }
331
- function loadKeys() {
332
- if (!fs2.existsSync(KEYS_PATH)) return null;
333
- const content = fs2.readFileSync(KEYS_PATH, "utf-8");
334
- const keys = {};
335
- for (const line of content.split("\n")) {
336
- const eq = line.indexOf("=");
337
- if (eq > 0) keys[line.slice(0, eq)] = line.slice(eq + 1);
338
- }
339
- const result = keys;
340
- if (result.NEXUS_MASTER_SECRET && !verifyKeysFile(KEYS_PATH, result.NEXUS_MASTER_SECRET)) {
341
- audit("keys_tampered", "HMAC mismatch on .env.keys");
342
- throw new DlpViolation(
343
- ".env.keys has been modified outside of buildwithnexus. Run 'buildwithnexus keys set' to re-enter your keys, or 'buildwithnexus destroy' to start fresh."
344
- );
345
- }
346
- audit("keys_loaded", `${Object.keys(keys).length} keys loaded`);
347
- return result;
348
- }
349
- function maskKey(key) {
350
- if (key.length <= 8) return "***";
351
- const reveal = Math.min(4, Math.floor(key.length * 0.1));
352
- return key.slice(0, reveal) + "..." + key.slice(-reveal);
353
- }
354
- var NEXUS_HOME2, CONFIG_PATH, KEYS_PATH;
355
- var init_secrets = __esm({
356
- "src/core/secrets.ts"() {
357
- "use strict";
358
- init_dlp();
359
- NEXUS_HOME2 = process.env.NEXUS_HOME || path2.join(process.env.HOME || "~", ".buildwithnexus");
360
- CONFIG_PATH = process.env.NEXUS_CONFIG_PATH || path2.join(NEXUS_HOME2, "config.json");
361
- KEYS_PATH = process.env.NEXUS_KEYS_PATH || path2.join(NEXUS_HOME2, ".env.keys");
362
- }
363
- });
364
-
365
- // src/core/qemu.ts
366
- var qemu_exports = {};
367
- __export(qemu_exports, {
368
- createDisk: () => createDisk,
369
- downloadImage: () => downloadImage,
370
- getVmPid: () => getVmPid,
371
- installQemu: () => installQemu,
372
- isQemuInstalled: () => isQemuInstalled,
373
- isVmRunning: () => isVmRunning,
374
- launchVm: () => launchVm,
375
- resolvePortConflicts: () => resolvePortConflicts,
376
- stopVm: () => stopVm
377
- });
378
- import fs3 from "fs";
379
- import net from "net";
380
- import path3 from "path";
381
- import { execa, execaSync } from "execa";
382
- import { select } from "@inquirer/prompts";
383
- import chalk5 from "chalk";
384
- async function isQemuInstalled(platform) {
385
- try {
386
- await execa(platform.qemuBinary, ["--version"], { env: scrubEnv() });
387
- return true;
388
- } catch {
389
- return false;
390
- }
391
- }
392
- async function installQemu(platform) {
393
- if (platform.os === "mac") {
394
- await execa("brew", ["install", "qemu", "cdrtools"], { stdio: "inherit", env: scrubEnv() });
395
- } else if (platform.os === "linux") {
396
- let hasApt = false;
397
- try {
398
- await execa("which", ["apt-get"], { env: scrubEnv() });
399
- hasApt = true;
400
- } catch {
401
- }
402
- if (hasApt) {
403
- await execa("sudo", ["apt-get", "update"], { stdio: "inherit", env: scrubEnv() });
404
- await execa("sudo", ["apt-get", "install", "-y", "qemu-system", "qemu-utils", "genisoimage"], { stdio: "inherit", env: scrubEnv() });
405
- } else {
406
- await execa("sudo", ["yum", "install", "-y", "qemu-system-arm", "qemu-system-x86", "qemu-img", "genisoimage"], { stdio: "inherit", env: scrubEnv() });
407
- }
408
- } else {
409
- throw new Error("Windows: Please install QEMU manually from https://www.qemu.org/download/#windows");
410
- }
411
- }
412
- async function downloadImage(platform) {
413
- const imagePath = path3.join(IMAGES_DIR, platform.ubuntuImage);
414
- if (fs3.existsSync(imagePath)) return imagePath;
415
- const url = `${UBUNTU_BASE_URL}/${platform.ubuntuImage}`;
416
- await execa("curl", ["-L", "-C", "-", "-o", imagePath, "--progress-bar", url], { stdio: "inherit", env: scrubEnv() });
417
- return imagePath;
418
- }
419
- async function createDisk(basePath, sizeGb) {
420
- const diskPath = path3.join(IMAGES_DIR, "nexus-vm-disk.qcow2");
421
- if (fs3.existsSync(diskPath)) return diskPath;
422
- await execa("qemu-img", ["create", "-f", "qcow2", "-b", basePath, "-F", "qcow2", diskPath, `${sizeGb}G`], { env: scrubEnv() });
423
- return diskPath;
424
- }
425
- function tryBind(port, host) {
426
- return new Promise((resolve) => {
427
- const server = net.createServer();
428
- server.once("error", () => resolve(false));
429
- server.once("listening", () => {
430
- server.close(() => resolve(true));
431
- });
432
- server.listen(port, host);
433
- });
434
- }
435
- async function isPortFree(port) {
436
- const free0 = await tryBind(port, "0.0.0.0");
437
- if (!free0) return false;
438
- const free1 = await tryBind(port, "127.0.0.1");
439
- return free1;
440
- }
441
- async function getPortBlocker(port) {
442
- try {
443
- const { stdout } = await execa("lsof", ["-i", `:${port}`, "-t", "-sTCP:LISTEN"], { env: scrubEnv() });
444
- const pid = parseInt(stdout.trim().split("\n")[0], 10);
445
- if (!pid) return null;
446
- try {
447
- const { stdout: psOut } = await execa("ps", ["-p", String(pid), "-o", "comm="], { env: scrubEnv() });
448
- return { port, pid, process: psOut.trim() };
449
- } catch {
450
- return { port, pid, process: "unknown" };
451
- }
452
- } catch {
453
- return null;
454
- }
455
- }
456
- async function findFreePort(preferred, max = 20) {
457
- for (let offset = 0; offset < max; offset++) {
458
- if (await isPortFree(preferred + offset)) return preferred + offset;
459
- }
460
- throw new Error(`No free port found near ${preferred}`);
461
- }
462
- async function resolvePortConflicts(ports) {
463
- const labels = { ssh: "SSH", http: "HTTP", https: "HTTPS" };
464
- const resolved = { ...ports };
465
- for (const [key, port] of Object.entries(ports)) {
466
- if (await isPortFree(port)) continue;
467
- const blocker = await getPortBlocker(port);
468
- const desc = blocker ? `${blocker.process} (PID ${blocker.pid})` : "unknown process";
469
- const altPort = await findFreePort(port + 1).catch(() => null);
470
- const choices = [];
471
- if (blocker) {
472
- choices.push({
473
- name: `Kill ${desc} and use port ${port}`,
474
- value: "kill"
475
- });
476
- }
477
- if (altPort) {
478
- choices.push({
479
- name: `Use alternate port ${altPort} instead`,
480
- value: "alt"
481
- });
482
- }
483
- choices.push({ name: "Abort init", value: "abort" });
484
- console.log("");
485
- console.log(chalk5.yellow(` \u26A0 Port ${port} (${labels[key]}) is in use by ${desc}`));
486
- const action = await select({
487
- message: `How would you like to resolve the ${labels[key]} port conflict?`,
488
- choices
489
- });
490
- if (action === "kill" && blocker) {
491
- try {
492
- process.kill(blocker.pid, "SIGTERM");
493
- await new Promise((r) => setTimeout(r, 1e3));
494
- if (!await isPortFree(port)) {
495
- process.kill(blocker.pid, "SIGKILL");
496
- await new Promise((r) => setTimeout(r, 500));
497
- }
498
- console.log(chalk5.green(` \u2713 Killed ${desc}, port ${port} is now free`));
499
- } catch {
500
- console.log(chalk5.red(` \u2717 Failed to kill PID ${blocker.pid}. Try: sudo kill ${blocker.pid}`));
501
- process.exit(1);
502
- }
503
- } else if (action === "alt" && altPort) {
504
- resolved[key] = altPort;
505
- console.log(chalk5.green(` \u2713 Using port ${altPort} for ${labels[key]}`));
506
- } else {
507
- console.log(chalk5.dim(" Init aborted."));
508
- process.exit(0);
509
- }
510
- }
511
- return resolved;
512
- }
513
- async function launchVm(platform, diskPath, initIsoPath, ram, cpus, ports) {
514
- const machineArgs = platform.os === "mac" ? ["-machine", "virt,gic-version=3"] : ["-machine", "pc"];
515
- const biosArgs = fs3.existsSync(platform.biosPath) ? ["-bios", platform.biosPath] : [];
516
- const buildArgs = (cpuArgs) => [
517
- ...machineArgs,
518
- ...cpuArgs,
519
- "-m",
520
- `${ram}G`,
521
- "-smp",
522
- `${cpus}`,
523
- "-drive",
524
- `file=${diskPath},if=virtio,cache=writethrough`,
525
- "-drive",
526
- `file=${initIsoPath},if=virtio,format=raw,cache=writethrough`,
527
- "-display",
528
- "none",
529
- "-serial",
530
- "none",
531
- "-net",
532
- "nic,model=virtio",
533
- "-net",
534
- `user,hostfwd=tcp::${ports.ssh}-:22,hostfwd=tcp::${ports.http}-:4200,hostfwd=tcp::${ports.https}-:443`,
535
- ...biosArgs,
536
- "-pidfile",
537
- PID_FILE,
538
- "-daemonize"
539
- ];
540
- try {
541
- await execa(platform.qemuBinary, buildArgs(platform.qemuCpuFlag.split(" ")), { env: scrubEnv() });
542
- } catch {
543
- const fallbackCpu = platform.os === "mac" ? ["-cpu", "max"] : ["-cpu", "qemu64"];
544
- await execa(platform.qemuBinary, buildArgs(fallbackCpu), { env: scrubEnv() });
545
- }
546
- return ports;
547
- }
548
- function readValidPid() {
549
- if (!fs3.existsSync(PID_FILE)) return null;
550
- const raw = fs3.readFileSync(PID_FILE, "utf-8").trim();
551
- const pid = parseInt(raw, 10);
552
- if (!Number.isInteger(pid) || pid <= 1 || pid > 4194304) return null;
553
- return pid;
554
- }
555
- function isQemuPid(pid) {
556
- try {
557
- const { stdout } = execaSync("ps", ["-p", String(pid), "-o", "comm="], { env: scrubEnv() });
558
- return stdout.trim().toLowerCase().includes("qemu");
559
- } catch {
560
- return false;
561
- }
562
- }
563
- function isVmRunning() {
564
- const pid = readValidPid();
565
- if (!pid) return false;
566
- try {
567
- process.kill(pid, 0);
568
- return true;
569
- } catch {
570
- return false;
571
- }
572
- }
573
- function stopVm() {
574
- const pid = readValidPid();
575
- if (!pid) return;
576
- if (!isQemuPid(pid)) {
577
- try {
578
- fs3.unlinkSync(PID_FILE);
579
- } catch {
580
- }
581
- return;
582
- }
583
- try {
584
- process.kill(pid, "SIGTERM");
585
- } catch {
586
- }
587
- try {
588
- fs3.unlinkSync(PID_FILE);
589
- } catch {
590
- }
591
- }
592
- function getVmPid() {
593
- return readValidPid();
594
- }
595
- var VM_DIR, IMAGES_DIR, PID_FILE, UBUNTU_BASE_URL;
596
- var init_qemu = __esm({
597
- "src/core/qemu.ts"() {
598
- "use strict";
599
- init_secrets();
600
- init_dlp();
601
- VM_DIR = path3.join(NEXUS_HOME2, "vm");
602
- IMAGES_DIR = path3.join(VM_DIR, "images");
603
- PID_FILE = path3.join(VM_DIR, "qemu.pid");
604
- UBUNTU_BASE_URL = "https://cloud-images.ubuntu.com/jammy/current";
605
- }
606
- });
607
-
608
- // src/core/ssh.ts
609
- var ssh_exports = {};
610
- __export(ssh_exports, {
611
- addSshConfig: () => addSshConfig,
612
- generateSshKey: () => generateSshKey,
613
- getKeyPath: () => getKeyPath,
614
- getPubKey: () => getPubKey,
615
- openInteractiveSsh: () => openInteractiveSsh,
616
- sshExec: () => sshExec,
617
- sshUploadFile: () => sshUploadFile,
618
- waitForSsh: () => waitForSsh
619
- });
620
- import fs4 from "fs";
621
- import path4 from "path";
622
- import crypto3 from "crypto";
623
- import { execa as execa2 } from "execa";
624
- import { NodeSSH } from "node-ssh";
625
- function getHostVerifier() {
626
- if (!fs4.existsSync(PINNED_HOST_KEY)) {
627
- return (key) => {
628
- const fp = crypto3.createHash("sha256").update(key).digest("base64");
629
- fs4.writeFileSync(PINNED_HOST_KEY, fp, { mode: 384 });
630
- audit("ssh_exec", `host key pinned: SHA256:${fp}`);
631
- return true;
632
- };
633
- }
634
- const pinned = fs4.readFileSync(PINNED_HOST_KEY, "utf-8").trim();
635
- return (key) => {
636
- const fp = crypto3.createHash("sha256").update(key).digest("base64");
637
- const match = fp === pinned;
638
- if (!match) audit("ssh_exec", `host key mismatch: expected ${pinned}, got ${fp}`);
639
- return match;
640
- };
641
- }
642
- function getKeyPath() {
643
- return SSH_KEY;
644
- }
645
- function getPubKey() {
646
- return fs4.readFileSync(SSH_PUB_KEY, "utf-8").trim();
647
- }
648
- async function generateSshKey() {
649
- if (fs4.existsSync(SSH_KEY)) return;
650
- fs4.mkdirSync(SSH_DIR, { recursive: true });
651
- await execa2("ssh-keygen", [
652
- "-t",
653
- "ed25519",
654
- "-f",
655
- SSH_KEY,
656
- "-N",
657
- "",
658
- "-C",
659
- "buildwithnexus@localhost",
660
- "-q"
661
- ], { env: scrubEnv() });
662
- fs4.chmodSync(SSH_KEY, 384);
663
- fs4.chmodSync(SSH_PUB_KEY, 420);
664
- }
665
- function addSshConfig(port) {
666
- const sshConfigPath = path4.join(process.env.HOME || "~", ".ssh", "config");
667
- const sshDir = path4.dirname(sshConfigPath);
668
- fs4.mkdirSync(sshDir, { recursive: true });
669
- const block = [
670
- "",
671
- "Host nexus-vm",
672
- " HostName localhost",
673
- ` Port ${port}`,
674
- " User nexus",
675
- ` IdentityFile ${SSH_KEY}`,
676
- " StrictHostKeyChecking accept-new",
677
- ` UserKnownHostsFile ${KNOWN_HOSTS}`,
678
- " ServerAliveInterval 60",
679
- ""
680
- ].join("\n");
681
- if (fs4.existsSync(sshConfigPath)) {
682
- const existing = fs4.readFileSync(sshConfigPath, "utf-8");
683
- if (existing.includes("Host nexus-vm")) return;
684
- fs4.appendFileSync(sshConfigPath, block);
685
- } else {
686
- fs4.writeFileSync(sshConfigPath, block, { mode: 384 });
687
- }
688
- }
689
- async function waitForSsh(port, timeoutMs = 9e5) {
690
- const start = Date.now();
691
- while (Date.now() - start < timeoutMs) {
1575
+ fs2.appendFileSync(AUDIT_PATH, line, { mode: 384 });
692
1576
  try {
693
- const ssh = new NodeSSH();
694
- await ssh.connect({
695
- host: "localhost",
696
- port,
697
- username: "nexus",
698
- privateKeyPath: SSH_KEY,
699
- readyTimeout: 1e4,
700
- hostVerifier: getHostVerifier()
701
- });
702
- ssh.dispose();
703
- return true;
1577
+ fs2.chmodSync(AUDIT_PATH, 384);
704
1578
  } catch {
705
- await new Promise((r) => setTimeout(r, 1e4));
706
1579
  }
1580
+ } catch {
707
1581
  }
708
- return false;
709
- }
710
- async function sshExec(port, command) {
711
- audit("ssh_exec", redact(command));
712
- const ssh = new NodeSSH();
713
- await ssh.connect({
714
- host: "localhost",
715
- port,
716
- username: "nexus",
717
- privateKeyPath: SSH_KEY,
718
- readyTimeout: 3e4,
719
- hostVerifier: getHostVerifier()
720
- });
721
- const result = await ssh.execCommand(command);
722
- ssh.dispose();
723
- return { stdout: redact(result.stdout), stderr: redact(result.stderr), code: result.code ?? 0 };
724
- }
725
- async function sshUploadFile(port, localPath, remotePath) {
726
- const ssh = new NodeSSH();
727
- await ssh.connect({
728
- host: "localhost",
729
- port,
730
- username: "nexus",
731
- privateKeyPath: SSH_KEY,
732
- hostVerifier: getHostVerifier()
733
- });
734
- await ssh.putFile(localPath, remotePath);
735
- ssh.dispose();
736
- }
737
- async function openInteractiveSsh(port) {
738
- await execa2("ssh", ["nexus-vm"], { stdio: "inherit", env: scrubEnv() });
739
- }
740
- var SSH_DIR, SSH_KEY, SSH_PUB_KEY, KNOWN_HOSTS, PINNED_HOST_KEY;
741
- var init_ssh = __esm({
742
- "src/core/ssh.ts"() {
743
- "use strict";
744
- init_secrets();
745
- init_dlp();
746
- SSH_DIR = path4.join(NEXUS_HOME2, "ssh");
747
- SSH_KEY = path4.join(SSH_DIR, "id_nexus_vm");
748
- SSH_PUB_KEY = path4.join(SSH_DIR, "id_nexus_vm.pub");
749
- KNOWN_HOSTS = path4.join(SSH_DIR, "known_hosts_nexus_vm");
750
- PINNED_HOST_KEY = path4.join(SSH_DIR, "vm_host_key.pin");
751
- }
752
- });
753
-
754
- // src/cli.ts
755
- import { Command as Command14 } from "commander";
756
- import { readFileSync as readFileSync2 } from "fs";
757
- import { dirname as dirname2, join as join2 } from "path";
758
- import { fileURLToPath as fileURLToPath4 } from "url";
759
-
760
- // src/commands/init.ts
761
- init_banner();
762
- import { Command } from "commander";
763
- import chalk6 from "chalk";
764
-
765
- // src/ui/spinner.ts
766
- import ora from "ora";
767
- import chalk2 from "chalk";
768
- function createSpinner(text) {
769
- return ora({ text, color: "cyan", spinner: "dots" });
770
- }
771
- function succeed(spinner, text) {
772
- spinner.succeed(chalk2.green(text));
773
- }
774
- function fail(spinner, text) {
775
- spinner.fail(chalk2.red(text));
776
1582
  }
777
1583
 
778
- // src/ui/logger.ts
779
- import chalk3 from "chalk";
780
- var log = {
781
- step(msg) {
782
- console.log(chalk3.cyan(" \u2192 ") + msg);
783
- },
784
- success(msg) {
785
- console.log(chalk3.green(" \u2713 ") + msg);
786
- },
787
- error(msg) {
788
- console.error(chalk3.red(" \u2717 ") + msg);
789
- },
790
- warn(msg) {
791
- console.log(chalk3.yellow(" \u26A0 ") + msg);
792
- },
793
- dim(msg) {
794
- console.log(chalk3.dim(" " + msg));
795
- },
796
- detail(label, value) {
797
- console.log(chalk3.dim(" " + label + ": ") + value);
798
- },
799
- progress(current, total, label) {
800
- const pct = Math.round(current / total * 100);
801
- const filled = Math.round(current / total * 20);
802
- const bar = chalk3.cyan("\u2588".repeat(filled)) + chalk3.dim("\u2591".repeat(20 - filled));
803
- process.stdout.write(`\r [${bar}] ${chalk3.bold(`${pct}%`)} ${chalk3.dim(label)}`);
804
- if (current >= total) process.stdout.write("\n");
805
- }
806
- };
807
-
808
1584
  // src/ui/prompts.ts
809
- init_dlp();
810
- import { input, confirm, password } from "@inquirer/prompts";
811
- import chalk4 from "chalk";
812
1585
  async function promptInitConfig() {
813
- console.log(chalk4.bold("\n API Keys\n"));
1586
+ console.log(chalk6.bold("\n API Keys\n"));
814
1587
  const anthropicKey = await password({
815
1588
  message: "Anthropic API key (required):",
816
1589
  mask: "*",
@@ -851,42 +1624,7 @@ async function promptInitConfig() {
851
1624
  return true;
852
1625
  }
853
1626
  });
854
- console.log(chalk4.bold("\n VM Resources\n"));
855
- const vmRam = Number(
856
- await input({
857
- message: "VM RAM in GB:",
858
- default: "4",
859
- validate: (v) => {
860
- const n = Number(v);
861
- if (!Number.isInteger(n) || n < 2 || n > 256) return "Must be a whole number between 2 and 256";
862
- return true;
863
- }
864
- })
865
- );
866
- const vmCpus = Number(
867
- await input({
868
- message: "VM CPUs:",
869
- default: "2",
870
- validate: (v) => {
871
- const n = Number(v);
872
- if (!Number.isInteger(n) || n < 1 || n > 64) return "Must be a whole number between 1 and 64";
873
- return true;
874
- }
875
- })
876
- );
877
- const vmDisk = Number(
878
- await input({
879
- message: "VM Disk in GB:",
880
- default: "20",
881
- validate: (v) => {
882
- const n = Number(v);
883
- if (!Number.isInteger(n) || n < 10 || n > 2048) return "Must be a whole number between 10 and 2048";
884
- return true;
885
- }
886
- })
887
- );
888
- console.log(chalk4.bold("\n Configuration\n"));
889
- const enableTunnel = await confirm({
1627
+ const enableTunnel = await confirm2({
890
1628
  message: "Enable Cloudflare tunnel for remote access?",
891
1629
  default: true
892
1630
  });
@@ -894,303 +1632,102 @@ async function promptInitConfig() {
894
1632
  anthropicKey,
895
1633
  openaiKey,
896
1634
  googleKey,
897
- vmRam,
898
- vmCpus,
899
- vmDisk,
900
1635
  enableTunnel
901
1636
  };
902
1637
  }
903
1638
 
904
- // src/core/platform.ts
905
- import os from "os";
906
- function detectPlatform() {
907
- const platform = os.platform();
908
- const arch = os.arch();
909
- if (platform === "darwin") {
910
- return {
911
- os: "mac",
912
- arch: arch === "arm64" ? "arm64" : "x64",
913
- qemuBinary: "qemu-system-aarch64",
914
- qemuCpuFlag: "-accel hvf -cpu host",
915
- ubuntuImage: "jammy-server-cloudimg-arm64.img",
916
- biosPath: "/opt/homebrew/share/qemu/edk2-aarch64-code.fd"
917
- };
918
- }
919
- if (platform === "linux") {
920
- return {
921
- os: "linux",
922
- arch: arch === "arm64" ? "arm64" : "x64",
923
- qemuBinary: arch === "arm64" ? "qemu-system-aarch64" : "qemu-system-x86_64",
924
- qemuCpuFlag: "-cpu host -enable-kvm",
925
- ubuntuImage: arch === "arm64" ? "jammy-server-cloudimg-arm64.img" : "jammy-server-cloudimg-amd64.img",
926
- biosPath: arch === "arm64" ? "/usr/share/qemu-efi-aarch64/QEMU_EFI.fd" : "/usr/share/OVMF/OVMF_CODE.fd"
927
- };
928
- }
929
- if (platform === "win32") {
930
- return {
931
- os: "windows",
932
- arch: "x64",
933
- qemuBinary: "qemu-system-x86_64",
934
- qemuCpuFlag: "-cpu qemu64",
935
- ubuntuImage: "jammy-server-cloudimg-amd64.img",
936
- biosPath: "C:\\Program Files\\qemu\\share\\edk2-x86_64-code.fd"
937
- };
938
- }
939
- throw new Error(`Unsupported platform: ${platform} ${arch}`);
1639
+ // src/core/secrets.ts
1640
+ import fs3 from "fs";
1641
+ import path4 from "path";
1642
+ import crypto2 from "crypto";
1643
+ var NEXUS_HOME2 = process.env.NEXUS_HOME || path4.join(process.env.HOME || "~", ".buildwithnexus");
1644
+ var CONFIG_PATH = process.env.NEXUS_CONFIG_PATH || path4.join(NEXUS_HOME2, "config.json");
1645
+ var KEYS_PATH = process.env.NEXUS_KEYS_PATH || path4.join(NEXUS_HOME2, ".env.keys");
1646
+ function ensureHome() {
1647
+ fs3.mkdirSync(NEXUS_HOME2, { recursive: true, mode: 448 });
1648
+ fs3.mkdirSync(path4.join(NEXUS_HOME2, "vm", "images"), { recursive: true, mode: 448 });
1649
+ fs3.mkdirSync(path4.join(NEXUS_HOME2, "vm", "configs"), { recursive: true, mode: 448 });
1650
+ fs3.mkdirSync(path4.join(NEXUS_HOME2, "vm", "logs"), { recursive: true, mode: 448 });
1651
+ fs3.mkdirSync(path4.join(NEXUS_HOME2, "ssh"), { recursive: true, mode: 448 });
940
1652
  }
941
-
942
- // src/commands/init.ts
943
- init_secrets();
944
- init_qemu();
945
- init_ssh();
946
-
947
- // src/core/cloudinit.ts
948
- init_secrets();
949
- init_dlp();
950
- import fs5 from "fs";
951
- import path5 from "path";
952
- import ejs from "ejs";
953
- import { execa as execa3 } from "execa";
954
- var CONFIGS_DIR = path5.join(NEXUS_HOME2, "vm", "configs");
955
- var IMAGES_DIR2 = path5.join(NEXUS_HOME2, "vm", "images");
956
- async function renderCloudInit(data, templateContent) {
957
- const trimmedPubKey = data.sshPubKey.trim();
958
- if (!/^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp\d+) [A-Za-z0-9+/=]+ ?\S*$/.test(trimmedPubKey)) {
959
- throw new DlpViolation("SSH public key has unexpected format \u2014 possible injection attempt");
960
- }
961
- const safeKeys = {};
962
- for (const [k, v] of Object.entries(data.keys)) {
963
- if (v) safeKeys[k] = yamlEscape(v);
964
- }
965
- const safeData = { ...data, sshPubKey: yamlEscape(trimmedPubKey), keys: safeKeys };
966
- const rendered = ejs.render(templateContent, safeData);
967
- const outputPath = path5.join(CONFIGS_DIR, "user-data.yaml");
968
- fs5.writeFileSync(outputPath, rendered, { mode: 384 });
969
- audit("cloudinit_rendered", "user-data.yaml written");
970
- return outputPath;
971
- }
972
- async function createCloudInitIso(userDataPath) {
973
- const metaDataPath = path5.join(CONFIGS_DIR, "meta-data.yaml");
974
- fs5.writeFileSync(metaDataPath, "instance-id: nexus-vm-1\nlocal-hostname: nexus-vm\n", { mode: 384 });
975
- const isoPath = path5.join(IMAGES_DIR2, "init.iso");
976
- const env = scrubEnv();
977
- try {
978
- let created = false;
979
- for (const tool of ["mkisofs", "genisoimage"]) {
980
- if (created) break;
981
- try {
982
- await execa3(tool, [
983
- "-output",
984
- isoPath,
985
- "-volid",
986
- "cidata",
987
- "-joliet",
988
- "-rock",
989
- userDataPath,
990
- metaDataPath
991
- ], { env });
992
- created = true;
993
- } catch {
994
- }
995
- }
996
- if (!created) {
997
- try {
998
- const stagingDir = path5.join(CONFIGS_DIR, "cidata-staging");
999
- fs5.mkdirSync(stagingDir, { recursive: true, mode: 448 });
1000
- fs5.copyFileSync(userDataPath, path5.join(stagingDir, "user-data"));
1001
- fs5.copyFileSync(metaDataPath, path5.join(stagingDir, "meta-data"));
1002
- await execa3("hdiutil", [
1003
- "makehybrid",
1004
- "-o",
1005
- isoPath,
1006
- "-joliet",
1007
- "-iso",
1008
- "-default-volume-name",
1009
- "cidata",
1010
- stagingDir
1011
- ], { env });
1012
- fs5.rmSync(stagingDir, { recursive: true, force: true });
1013
- created = true;
1014
- } catch {
1015
- }
1016
- }
1017
- if (!created) {
1018
- throw new Error(
1019
- "Cannot create cloud-init ISO: none of mkisofs, genisoimage, or hdiutil are available. On macOS, install cdrtools: brew install cdrtools. On Linux: sudo apt install genisoimage"
1020
- );
1021
- }
1022
- fs5.chmodSync(isoPath, 384);
1023
- audit("cloudinit_iso_created", "init.iso created");
1024
- return isoPath;
1025
- } finally {
1026
- try {
1027
- fs5.unlinkSync(userDataPath);
1028
- } catch {
1029
- }
1030
- try {
1031
- fs5.unlinkSync(metaDataPath);
1032
- } catch {
1033
- }
1034
- audit("cloudinit_plaintext_deleted", "user-data.yaml and meta-data.yaml removed");
1035
- }
1653
+ function generateMasterSecret() {
1654
+ return crypto2.randomBytes(32).toString("base64url");
1036
1655
  }
1037
-
1038
- // src/core/health.ts
1039
- init_ssh();
1040
- async function checkHealth(port, vmRunning) {
1041
- const status = {
1042
- vmRunning,
1043
- sshReady: false,
1044
- dockerReady: false,
1045
- serverHealthy: false,
1046
- tunnelUrl: null,
1047
- dockerVersion: null,
1048
- serverVersion: null,
1049
- diskUsagePercent: null,
1050
- uptimeSeconds: null,
1051
- lastChecked: (/* @__PURE__ */ new Date()).toISOString()
1052
- };
1053
- if (!vmRunning) return status;
1054
- try {
1055
- const { code } = await sshExec(port, "echo ok");
1056
- status.sshReady = code === 0;
1057
- } catch {
1058
- return status;
1059
- }
1060
- try {
1061
- const { stdout, code } = await sshExec(port, "docker version --format '{{.Server.Version}}'");
1062
- status.dockerReady = code === 0 && stdout.trim().length > 0;
1063
- if (status.dockerReady) status.dockerVersion = stdout.trim();
1064
- } catch {
1065
- }
1066
- try {
1067
- const { stdout, code } = await sshExec(port, "curl -sf http://localhost:4200/health");
1068
- status.serverHealthy = code === 0 && stdout.includes("ok");
1069
- if (status.serverHealthy) {
1070
- try {
1071
- const parsed = JSON.parse(stdout);
1072
- if (typeof parsed.version === "string") status.serverVersion = parsed.version;
1073
- } catch {
1074
- }
1075
- }
1076
- } catch {
1077
- }
1078
- try {
1079
- const { stdout } = await sshExec(port, "df / --output=pcent | tail -1 | tr -dc '0-9'");
1080
- const pct = parseInt(stdout.trim(), 10);
1081
- if (!isNaN(pct)) status.diskUsagePercent = pct;
1082
- } catch {
1083
- }
1084
- try {
1085
- const { stdout } = await sshExec(port, "awk '{print int($1)}' /proc/uptime 2>/dev/null");
1086
- const up = parseInt(stdout.trim(), 10);
1087
- if (!isNaN(up)) status.uptimeSeconds = up;
1088
- } catch {
1089
- }
1090
- try {
1091
- const { stdout } = await sshExec(port, "cat /home/nexus/.nexus/tunnel-url.txt 2>/dev/null");
1092
- if (stdout.includes("https://")) {
1093
- status.tunnelUrl = stdout.trim();
1094
- }
1095
- } catch {
1096
- }
1097
- return status;
1656
+ function saveConfig(config) {
1657
+ const { masterSecret: _secret, ...safeConfig } = config;
1658
+ fs3.writeFileSync(CONFIG_PATH, JSON.stringify(safeConfig, null, 2), { mode: 384 });
1098
1659
  }
1099
- async function waitForServer(port, timeoutMs = 9e5) {
1100
- const start = Date.now();
1101
- let lastLog = 0;
1102
- let attempt = 0;
1103
- const backoffMs = (n) => Math.min(3e3 * Math.pow(2, n), 3e4);
1104
- while (Date.now() - start < timeoutMs) {
1105
- try {
1106
- const { stdout, code } = await sshExec(port, "curl -sf http://localhost:4200/health");
1107
- if (code === 0 && stdout.includes("ok")) return true;
1108
- } catch {
1109
- }
1110
- const elapsed = Date.now() - start;
1111
- if (elapsed - lastLog >= 3e4) {
1112
- lastLog = elapsed;
1113
- try {
1114
- const { stdout } = await sshExec(port, "systemctl is-active nexus 2>/dev/null || echo 'starting...'");
1115
- process.stderr.write(`
1116
- [server ${Math.round(elapsed / 1e3)}s] ${stdout.trim().slice(0, 120)}
1117
- `);
1118
- } catch {
1119
- }
1120
- }
1121
- const delay = backoffMs(attempt++);
1122
- const remaining = timeoutMs - (Date.now() - start);
1123
- if (remaining <= 0) break;
1124
- await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
1660
+ function loadConfig() {
1661
+ if (!fs3.existsSync(CONFIG_PATH)) return null;
1662
+ return JSON.parse(fs3.readFileSync(CONFIG_PATH, "utf-8"));
1663
+ }
1664
+ function saveKeys(keys) {
1665
+ const violations = validateAllKeys(keys);
1666
+ if (violations.length > 0) {
1667
+ throw new DlpViolation(`Key validation failed: ${violations.join("; ")}`);
1125
1668
  }
1126
- return false;
1669
+ const lines = Object.entries(keys).filter(([, v]) => v).map(([k, v]) => `${k}=${v}`);
1670
+ fs3.writeFileSync(KEYS_PATH, lines.join("\n") + "\n", { mode: 384 });
1671
+ sealKeysFile(KEYS_PATH, keys.NEXUS_MASTER_SECRET);
1672
+ audit("keys_saved", `${Object.keys(keys).filter((k) => keys[k]).length} keys saved`);
1127
1673
  }
1128
- async function waitForCloudInit(port, timeoutMs = 18e5) {
1129
- const start = Date.now();
1130
- let lastLog = 0;
1131
- let attempt = 0;
1132
- const backoffMs = (n) => Math.min(3e3 * Math.pow(2, n), 3e4);
1133
- while (Date.now() - start < timeoutMs) {
1134
- try {
1135
- const { code } = await sshExec(port, "test -f /var/lib/cloud/instance/boot-finished");
1136
- if (code === 0) return true;
1137
- } catch {
1138
- }
1139
- const elapsed = Date.now() - start;
1140
- if (elapsed - lastLog >= 6e4) {
1141
- lastLog = elapsed;
1142
- try {
1143
- const { stdout } = await sshExec(port, "tail -1 /var/log/cloud-init-output.log 2>/dev/null || echo 'waiting...'");
1144
- process.stderr.write(`
1145
- [cloud-init ${Math.round(elapsed / 1e3)}s] ${stdout.trim().slice(0, 120)}
1146
- `);
1147
- } catch {
1148
- }
1149
- }
1150
- const delay = backoffMs(attempt++);
1151
- const remaining = timeoutMs - (Date.now() - start);
1152
- if (remaining <= 0) break;
1153
- await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
1674
+ function loadKeys() {
1675
+ if (!fs3.existsSync(KEYS_PATH)) return null;
1676
+ const content = fs3.readFileSync(KEYS_PATH, "utf-8");
1677
+ const keys = {};
1678
+ for (const line of content.split("\n")) {
1679
+ const eq = line.indexOf("=");
1680
+ if (eq > 0) keys[line.slice(0, eq)] = line.slice(eq + 1);
1154
1681
  }
1155
- return false;
1682
+ const result = keys;
1683
+ if (result.NEXUS_MASTER_SECRET && !verifyKeysFile(KEYS_PATH, result.NEXUS_MASTER_SECRET)) {
1684
+ audit("keys_tampered", "HMAC mismatch on .env.keys");
1685
+ throw new DlpViolation(
1686
+ ".env.keys has been modified outside of buildwithnexus. Run 'buildwithnexus keys set' to re-enter your keys, or 'buildwithnexus destroy' to start fresh."
1687
+ );
1688
+ }
1689
+ audit("keys_loaded", `${Object.keys(keys).length} keys loaded`);
1690
+ return result;
1691
+ }
1692
+ function maskKey(key) {
1693
+ if (key.length <= 8) return "***";
1694
+ const reveal = Math.min(4, Math.floor(key.length * 0.1));
1695
+ return key.slice(0, reveal) + "..." + key.slice(-reveal);
1156
1696
  }
1157
1697
 
1158
1698
  // src/core/tunnel.ts
1159
- init_ssh();
1160
- init_dlp();
1161
1699
  var CLOUDFLARED_VERSION = "2024.12.2";
1162
1700
  var CLOUDFLARED_SHA256 = {
1163
1701
  amd64: "40ec9a0f5b58e3b04183aaf01c4ddd4dbc6af39b0f06be4b7ce8b1011d0a07ab",
1164
1702
  arm64: "5a6c5881743fc84686f23048940ec844848c0f20363e8f76a99bc47e19777de6"
1165
1703
  };
1166
- async function installCloudflared(sshPort, arch) {
1704
+ async function installCloudflared(arch) {
1167
1705
  const debArch = arch === "arm64" ? "arm64" : "amd64";
1168
1706
  const url = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-${debArch}.deb`;
1169
1707
  const sha = CLOUDFLARED_SHA256[debArch];
1170
1708
  const shaCheck = `${sha} /tmp/cloudflared.deb`;
1171
- await sshExec(sshPort, [
1709
+ await dockerExec([
1172
1710
  shellCommand`curl -sL ${url} -o /tmp/cloudflared.deb`,
1173
1711
  shellCommand`echo ${shaCheck} | sha256sum -c -`,
1174
1712
  "sudo dpkg -i /tmp/cloudflared.deb",
1175
1713
  "rm -f /tmp/cloudflared.deb"
1176
1714
  ].join(" && "));
1177
1715
  }
1178
- async function startTunnel(sshPort) {
1179
- await sshExec(
1180
- sshPort,
1716
+ async function startTunnel() {
1717
+ await dockerExec(
1181
1718
  "install -m 600 /dev/null /home/nexus/.nexus/tunnel.log && bash -c 'nohup cloudflared tunnel --no-autoupdate --url http://localhost:4200 > /home/nexus/.nexus/tunnel.log 2>&1 &'"
1182
1719
  );
1183
1720
  const start = Date.now();
1184
1721
  while (Date.now() - start < 12e4) {
1185
1722
  try {
1186
- const { stdout } = await sshExec(sshPort, "grep -oE 'https://[a-z0-9-]+\\.trycloudflare\\.com' /home/nexus/.nexus/tunnel.log 2>/dev/null | head -1");
1723
+ const { stdout } = await dockerExec("grep -oE 'https://[a-z0-9-]+\\.trycloudflare\\.com' /home/nexus/.nexus/tunnel.log 2>/dev/null | head -1");
1187
1724
  if (stdout.includes("https://")) {
1188
1725
  const url = stdout.trim();
1189
1726
  if (!/^https:\/\/[a-z0-9-]+\.trycloudflare\.com$/.test(url)) {
1190
1727
  audit("tunnel_url_rejected", `Invalid URL format: ${url.slice(0, 80)}`);
1191
1728
  return null;
1192
1729
  }
1193
- await sshExec(sshPort, shellCommand`printf '%s\n' ${url} > /home/nexus/.nexus/tunnel-url.txt && chmod 600 /home/nexus/.nexus/tunnel-url.txt`);
1730
+ await dockerExec(shellCommand`printf '%s\n' ${url} > /home/nexus/.nexus/tunnel-url.txt && chmod 600 /home/nexus/.nexus/tunnel-url.txt`);
1194
1731
  audit("tunnel_url_captured", url);
1195
1732
  return url;
1196
1733
  }
@@ -1200,40 +1737,8 @@ async function startTunnel(sshPort) {
1200
1737
  }
1201
1738
  return null;
1202
1739
  }
1203
- async function stopTunnel(sshPort) {
1204
- await sshExec(sshPort, "pkill -f cloudflared || true");
1205
- }
1206
1740
 
1207
1741
  // src/commands/init.ts
1208
- init_dlp();
1209
- init_ssh();
1210
- import fs6 from "fs";
1211
- import path6 from "path";
1212
- import os2 from "os";
1213
- import crypto4 from "crypto";
1214
- import { fileURLToPath as fileURLToPath2 } from "url";
1215
- function getReleaseTarball() {
1216
- const dir = path6.dirname(fileURLToPath2(import.meta.url));
1217
- const possiblePaths = [
1218
- // Current directory (dev)
1219
- path6.join(dir, "nexus-release.tar.gz"),
1220
- // dist folder (when built)
1221
- path6.resolve(dir, "..", "dist", "nexus-release.tar.gz"),
1222
- // node_modules location (when installed from npm)
1223
- path6.resolve(dir, "..", "nexus-release.tar.gz")
1224
- ];
1225
- for (const tarballPath of possiblePaths) {
1226
- if (fs6.existsSync(tarballPath)) return tarballPath;
1227
- }
1228
- throw new Error("nexus-release.tar.gz not found. Run: npm install buildwithnexus@latest");
1229
- }
1230
- function getCloudInitTemplate() {
1231
- const dir = path6.dirname(fileURLToPath2(import.meta.url));
1232
- const templatePath = path6.join(dir, "templates", "cloud-init.yaml.ejs");
1233
- if (fs6.existsSync(templatePath)) return fs6.readFileSync(templatePath, "utf-8");
1234
- const srcPath = path6.resolve(dir, "..", "src", "templates", "cloud-init.yaml.ejs");
1235
- return fs6.readFileSync(srcPath, "utf-8");
1236
- }
1237
1742
  async function withSpinner(spinner, label, fn) {
1238
1743
  spinner.text = label;
1239
1744
  spinner.start();
@@ -1241,24 +1746,39 @@ async function withSpinner(spinner, label, fn) {
1241
1746
  succeed(spinner, label);
1242
1747
  return result;
1243
1748
  }
1749
+ async function waitForHealthy(port, timeoutMs = 12e4) {
1750
+ const start = Date.now();
1751
+ let attempt = 0;
1752
+ const backoffMs = (n) => Math.min(2e3 * Math.pow(2, n), 1e4);
1753
+ while (Date.now() - start < timeoutMs) {
1754
+ try {
1755
+ const res = await fetch(`http://localhost:${port}/health`);
1756
+ if (res.ok) {
1757
+ const body = await res.text();
1758
+ if (body.includes("ok")) return true;
1759
+ }
1760
+ } catch {
1761
+ }
1762
+ const delay = backoffMs(attempt++);
1763
+ const remaining = timeoutMs - (Date.now() - start);
1764
+ if (remaining <= 0) break;
1765
+ await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
1766
+ }
1767
+ return false;
1768
+ }
1244
1769
  var phases = [
1245
- // Phase 1 — Configuration
1770
+ // Phase 1 — Configuration (~30s)
1246
1771
  {
1247
1772
  name: "Configuration",
1248
1773
  run: async (ctx) => {
1249
1774
  showBanner();
1250
1775
  const platform = detectPlatform();
1251
1776
  log.detail("Platform", `${platform.os} ${platform.arch}`);
1252
- log.detail("QEMU", platform.qemuBinary);
1253
1777
  const userConfig = await promptInitConfig();
1254
1778
  ensureHome();
1255
1779
  const masterSecret = generateMasterSecret();
1256
1780
  const config = {
1257
- vmRam: userConfig.vmRam,
1258
- vmCpus: userConfig.vmCpus,
1259
- vmDisk: userConfig.vmDisk,
1260
1781
  enableTunnel: userConfig.enableTunnel,
1261
- sshPort: 2222,
1262
1782
  httpPort: 4200,
1263
1783
  httpsPort: 8443,
1264
1784
  masterSecret
@@ -1277,185 +1797,107 @@ var phases = [
1277
1797
  saveConfig(config);
1278
1798
  saveKeys(keys);
1279
1799
  log.success("Configuration saved");
1280
- ctx.platform = platform;
1281
1800
  ctx.config = config;
1282
- ctx.keys = keys;
1283
- }
1284
- },
1285
- // Phase 2 — QEMU
1286
- {
1287
- name: "QEMU Installation",
1288
- run: async (ctx, spinner) => {
1289
- const { platform } = ctx;
1290
- await withSpinner(spinner, "Checking QEMU...", async () => {
1291
- const hasQemu = await isQemuInstalled(platform);
1292
- if (!hasQemu) {
1293
- spinner.text = "Installing QEMU...";
1294
- await installQemu(platform);
1295
- }
1296
- });
1297
- succeed(spinner, hasQemuLabel(await isQemuInstalled(ctx.platform)));
1298
- }
1299
- },
1300
- // Phase 3 — SSH Keys
1301
- {
1302
- name: "SSH Key Setup",
1303
- run: async (ctx, spinner) => {
1304
- const { config } = ctx;
1305
- await withSpinner(spinner, "Generating SSH key...", async () => {
1306
- await generateSshKey();
1307
- const pinFile = path6.join(path6.dirname(getKeyPath()), "vm_host_key.pin");
1308
- try {
1309
- fs6.unlinkSync(pinFile);
1310
- } catch {
1311
- }
1312
- addSshConfig(config.sshPort);
1313
- });
1801
+ ctx.keys = keys;
1314
1802
  }
1315
1803
  },
1316
- // Phase 4Ubuntu Image
1804
+ // Phase 2Docker Check (install is handled by `buildwithnexus install`)
1317
1805
  {
1318
- name: "VM Image Download",
1319
- run: async (ctx, spinner) => {
1320
- const { platform } = ctx;
1321
- const imagePath = await withSpinner(
1322
- spinner,
1323
- `Downloading Ubuntu 24.04 (${platform.ubuntuImage})...`,
1324
- () => downloadImage(platform)
1806
+ name: "Docker Check",
1807
+ run: async (_ctx, spinner) => {
1808
+ spinner.text = "Checking Docker...";
1809
+ spinner.start();
1810
+ const installed = await isDockerInstalled();
1811
+ if (installed) {
1812
+ succeed(spinner, "Docker is installed and running");
1813
+ return;
1814
+ }
1815
+ fail(spinner, "Docker is not installed or not running");
1816
+ throw new Error(
1817
+ "Docker is required but not available.\n\n Run the following command to install Docker:\n buildwithnexus install\n\n Then re-run:\n buildwithnexus init"
1325
1818
  );
1326
- ctx.imagePath = imagePath;
1327
- }
1328
- },
1329
- // Phase 5 — Cloud-Init
1330
- {
1331
- name: "Cloud-Init Generation",
1332
- run: async (ctx, spinner) => {
1333
- const { keys, config } = ctx;
1334
- const tarballPath = await withSpinner(spinner, "Locating release tarball...", async () => {
1335
- return getReleaseTarball();
1336
- });
1337
- ctx.tarballPath = tarballPath;
1338
- const isoPath = await withSpinner(spinner, "Rendering cloud-init...", async () => {
1339
- const pubKey = getPubKey();
1340
- const template = getCloudInitTemplate();
1341
- const userDataPath = await renderCloudInit({ sshPubKey: pubKey, keys, config }, template);
1342
- return createCloudInitIso(userDataPath);
1343
- });
1344
- ctx.isoPath = isoPath;
1345
1819
  }
1346
1820
  },
1347
- // Phase 6VM Boot
1821
+ // Phase 3Pull Image (~1-2 min)
1348
1822
  {
1349
- name: "VM Launch",
1350
- run: async (ctx, spinner) => {
1351
- const { platform, imagePath, isoPath, config } = ctx;
1352
- spinner.text = "Checking port availability...";
1823
+ name: "Pull Image",
1824
+ run: async (_ctx, spinner) => {
1825
+ spinner.text = "Checking for buildwithnexus/nexus:latest...";
1353
1826
  spinner.start();
1827
+ const localExists = await imageExistsLocally("buildwithnexus/nexus", "latest");
1828
+ if (localExists) {
1829
+ succeed(spinner, "Image found locally: buildwithnexus/nexus:latest (skipping pull)");
1830
+ return;
1831
+ }
1354
1832
  spinner.stop();
1355
- spinner.clear();
1356
- const requestedPorts = { ssh: config.sshPort, http: config.httpPort, https: config.httpsPort };
1357
- const resolvedPorts = await resolvePortConflicts(requestedPorts);
1358
- const diskPath = await withSpinner(spinner, "Creating disk and launching VM...", async () => {
1359
- const disk = await createDisk(imagePath, config.vmDisk);
1360
- await launchVm(platform, disk, isoPath, config.vmRam, config.vmCpus, resolvedPorts);
1361
- return disk;
1362
- });
1363
- config.sshPort = resolvedPorts.ssh;
1364
- config.httpPort = resolvedPorts.http;
1365
- config.httpsPort = resolvedPorts.https;
1366
- saveConfig(config);
1367
- const portNote = resolvedPorts.ssh !== 2222 || resolvedPorts.http !== 4200 || resolvedPorts.https !== 8443 ? ` (ports: SSH=${resolvedPorts.ssh}, HTTP=${resolvedPorts.http}, HTTPS=${resolvedPorts.https})` : "";
1368
- succeed(spinner, `VM launched (daemonized)${portNote}`);
1369
- ctx.diskPath = diskPath;
1370
- ctx.resolvedPorts = resolvedPorts;
1371
- ctx.vmLaunched = true;
1833
+ try {
1834
+ await pullImage("buildwithnexus/nexus", "latest");
1835
+ succeed(spinner, "Image pulled: buildwithnexus/nexus:latest");
1836
+ } catch (err) {
1837
+ fail(spinner, "Failed to pull buildwithnexus/nexus:latest");
1838
+ throw new Error(
1839
+ "Could not pull buildwithnexus/nexus:latest from registry.\n\n If you have built the image locally, you can build it with:\n docker build -f Dockerfile.nexus -t buildwithnexus/nexus:latest .\n\n Then re-run:\n buildwithnexus init"
1840
+ );
1841
+ }
1372
1842
  }
1373
1843
  },
1374
- // Phase 7VM Provisioning
1844
+ // Phase 4Launch Container (~10s)
1375
1845
  {
1376
- name: "VM Provisioning",
1846
+ name: "Launch",
1377
1847
  run: async (ctx, spinner) => {
1378
- const { config, keys, tarballPath } = ctx;
1379
- spinner.text = "Waiting for SSH...";
1380
- spinner.start();
1381
- const sshReady = await waitForSsh(config.sshPort);
1382
- if (!sshReady) {
1383
- fail(spinner, "SSH connection timed out");
1384
- throw new Error("SSH connection timed out");
1385
- }
1386
- succeed(spinner, "SSH connected");
1387
- await withSpinner(
1388
- spinner,
1389
- "Uploading NEXUS release tarball...",
1390
- () => sshUploadFile(config.sshPort, tarballPath, "/tmp/nexus-release.tar.gz")
1391
- );
1392
- await withSpinner(spinner, "Staging API keys...", async () => {
1393
- const keysContent = Object.entries(keys).filter(([, v]) => v).map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
1394
- const tmpKeysPath = path6.join(os2.tmpdir(), `.nexus-keys-${crypto4.randomBytes(8).toString("hex")}`);
1395
- fs6.writeFileSync(tmpKeysPath, keysContent, { mode: 384 });
1396
- try {
1397
- await sshUploadFile(config.sshPort, tmpKeysPath, "/tmp/.nexus-env-keys");
1398
- await sshExec(config.sshPort, "chmod 600 /tmp/.nexus-env-keys");
1399
- } finally {
1400
- try {
1401
- fs6.writeFileSync(tmpKeysPath, "0".repeat(keysContent.length));
1402
- fs6.unlinkSync(tmpKeysPath);
1403
- } catch {
1404
- }
1405
- }
1406
- });
1407
- spinner.text = "Cloud-init provisioning \u2014 this takes 10-20 min (extracting NEXUS, building Docker, installing deps)...";
1408
- spinner.start();
1409
- const cloudInitDone = await waitForCloudInit(config.sshPort);
1410
- if (!cloudInitDone) {
1411
- fail(spinner, "Cloud-init timed out after 30 minutes");
1412
- log.warn("Check progress: buildwithnexus ssh \u2192 tail -f /var/log/cloud-init-output.log");
1413
- throw new Error("Cloud-init timed out");
1848
+ const { config, keys } = ctx;
1849
+ const alreadyRunning = await isNexusRunning();
1850
+ if (alreadyRunning) {
1851
+ await withSpinner(spinner, "Stopping existing NEXUS container...", () => stopNexus());
1414
1852
  }
1415
- succeed(spinner, "VM fully provisioned");
1416
1853
  await withSpinner(
1417
1854
  spinner,
1418
- "Delivering API keys...",
1419
- () => sshExec(
1420
- config.sshPort,
1421
- "mkdir -p /home/nexus/.nexus && mv /tmp/.nexus-env-keys /home/nexus/.nexus/.env.keys && chown -R nexus:nexus /home/nexus/.nexus && chmod 600 /home/nexus/.nexus/.env.keys"
1855
+ "Starting NEXUS container...",
1856
+ () => startNexus(
1857
+ {
1858
+ anthropic: keys.ANTHROPIC_API_KEY,
1859
+ openai: keys.OPENAI_API_KEY || ""
1860
+ },
1861
+ { port: config.httpPort }
1422
1862
  )
1423
1863
  );
1864
+ ctx.containerStarted = true;
1424
1865
  }
1425
1866
  },
1426
- // Phase 8Server Health
1867
+ // Phase 5Health Check (~10s)
1427
1868
  {
1428
- name: "NEXUS Server Startup",
1869
+ name: "Health Check",
1429
1870
  run: async (ctx, spinner) => {
1430
1871
  const { config } = ctx;
1431
- spinner.text = "Waiting for NEXUS server...";
1872
+ spinner.text = `Waiting for NEXUS server on port ${config.httpPort}...`;
1432
1873
  spinner.start();
1433
- const serverReady = await waitForServer(config.sshPort);
1434
- if (!serverReady) {
1435
- fail(spinner, "Server failed to start");
1436
- log.warn("Check logs: buildwithnexus logs");
1437
- throw new Error("NEXUS server failed to start");
1874
+ const healthy = await waitForHealthy(config.httpPort);
1875
+ if (!healthy) {
1876
+ fail(spinner, "Server failed to start within 120s");
1877
+ log.warn("Check logs: docker logs nexus");
1878
+ throw new Error("NEXUS server failed to respond to health checks");
1438
1879
  }
1439
- succeed(spinner, "NEXUS server healthy on port 4200");
1880
+ succeed(spinner, `NEXUS server healthy on port ${config.httpPort}`);
1440
1881
  }
1441
1882
  },
1442
- // Phase 9 — Cloudflare Tunnel
1883
+ // Phase 6 — Cloudflare Tunnel (optional)
1443
1884
  {
1444
1885
  name: "Cloudflare Tunnel",
1445
1886
  run: async (ctx, spinner) => {
1446
- const { config, platform } = ctx;
1887
+ const { config } = ctx;
1447
1888
  if (!config.enableTunnel) {
1448
1889
  log.dim("Skipped (not enabled)");
1449
1890
  return;
1450
1891
  }
1892
+ const platform = detectPlatform();
1451
1893
  await withSpinner(
1452
1894
  spinner,
1453
1895
  "Installing cloudflared...",
1454
- () => installCloudflared(config.sshPort, platform.arch)
1896
+ () => installCloudflared(config.httpPort, platform.arch)
1455
1897
  );
1456
1898
  spinner.text = "Starting tunnel...";
1457
1899
  spinner.start();
1458
- const url = await startTunnel(config.sshPort);
1900
+ const url = await startTunnel(config.httpPort);
1459
1901
  if (url) {
1460
1902
  ctx.tunnelUrl = url;
1461
1903
  succeed(spinner, `Tunnel active: ${url}`);
@@ -1464,28 +1906,24 @@ var phases = [
1464
1906
  }
1465
1907
  }
1466
1908
  },
1467
- // Phase 10 — Complete
1909
+ // Phase 7 — Complete
1468
1910
  {
1469
1911
  name: "Complete",
1470
1912
  run: async (ctx) => {
1471
- showCompletion({ remote: ctx.tunnelUrl, ssh: "buildwithnexus ssh" });
1913
+ showCompletion({
1914
+ remote: ctx.tunnelUrl,
1915
+ ssh: `http://localhost:${ctx.config?.httpPort || 4200}`
1916
+ });
1472
1917
  }
1473
1918
  }
1474
1919
  ];
1475
- function hasQemuLabel(installed) {
1476
- return installed ? "QEMU ready" : "QEMU installed";
1477
- }
1478
1920
  var TOTAL_PHASES = phases.length;
1479
1921
  async function runInit() {
1480
- const ctx = { vmLaunched: false, tunnelUrl: void 0 };
1922
+ const ctx = { containerStarted: false, tunnelUrl: void 0 };
1481
1923
  const spinner = createSpinner("");
1482
1924
  for (let i = 0; i < phases.length; i++) {
1483
1925
  const phase = phases[i];
1484
1926
  showPhase(i + 1, TOTAL_PHASES, phase.name);
1485
- if (phase.skip?.(ctx)) {
1486
- log.dim("Skipped");
1487
- continue;
1488
- }
1489
1927
  try {
1490
1928
  await phase.run(ctx, spinner);
1491
1929
  } catch (err) {
@@ -1493,10 +1931,10 @@ async function runInit() {
1493
1931
  spinner.stop();
1494
1932
  } catch {
1495
1933
  }
1496
- if (ctx.vmLaunched) {
1497
- process.stderr.write(chalk6.dim("\n Stopping VM due to init failure...\n"));
1934
+ if (ctx.containerStarted) {
1935
+ process.stderr.write(chalk7.dim("\n Stopping container due to init failure...\n"));
1498
1936
  try {
1499
- stopVm();
1937
+ await stopNexus();
1500
1938
  } catch {
1501
1939
  }
1502
1940
  }
@@ -1504,7 +1942,7 @@ async function runInit() {
1504
1942
  }
1505
1943
  }
1506
1944
  }
1507
- var initCommand = new Command("init").description("Scaffold and launch a new NEXUS runtime").action(async () => {
1945
+ var initCommand = new Command2("init").description("Scaffold and launch a new NEXUS runtime (Docker)").action(async () => {
1508
1946
  try {
1509
1947
  await runInit();
1510
1948
  } catch (err) {
@@ -1515,45 +1953,64 @@ var initCommand = new Command("init").description("Scaffold and launch a new NEX
1515
1953
  });
1516
1954
 
1517
1955
  // src/commands/start.ts
1518
- import { Command as Command2 } from "commander";
1519
- init_secrets();
1520
- init_qemu();
1521
- init_ssh();
1522
- init_secrets();
1523
- import path7 from "path";
1524
- var startCommand = new Command2("start").description("Start the NEXUS runtime").action(async () => {
1956
+ import { Command as Command3 } from "commander";
1957
+
1958
+ // src/core/health.ts
1959
+ async function waitForServer(timeoutMs = 9e5) {
1960
+ const start = Date.now();
1961
+ let lastLog = 0;
1962
+ let attempt = 0;
1963
+ const backoffMs = (n) => Math.min(3e3 * Math.pow(2, n), 3e4);
1964
+ while (Date.now() - start < timeoutMs) {
1965
+ try {
1966
+ const { stdout, code } = await dockerExec("curl -sf http://localhost:4200/health");
1967
+ if (code === 0 && stdout.includes("ok")) return true;
1968
+ } catch {
1969
+ }
1970
+ const elapsed = Date.now() - start;
1971
+ if (elapsed - lastLog >= 3e4) {
1972
+ lastLog = elapsed;
1973
+ try {
1974
+ const { stdout } = await dockerExec("systemctl is-active nexus 2>/dev/null || echo 'starting...'");
1975
+ process.stderr.write(`
1976
+ [server ${Math.round(elapsed / 1e3)}s] ${stdout.trim().slice(0, 120)}
1977
+ `);
1978
+ } catch {
1979
+ }
1980
+ }
1981
+ const delay = backoffMs(attempt++);
1982
+ const remaining = timeoutMs - (Date.now() - start);
1983
+ if (remaining <= 0) break;
1984
+ await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
1985
+ }
1986
+ return false;
1987
+ }
1988
+
1989
+ // src/commands/start.ts
1990
+ var startCommand = new Command3("start").description("Start the NEXUS runtime").action(async () => {
1525
1991
  const config = loadConfig();
1526
1992
  if (!config) {
1527
1993
  log.error("No NEXUS configuration found. Run: buildwithnexus init");
1528
1994
  process.exit(1);
1529
1995
  }
1530
- if (isVmRunning()) {
1531
- log.success("VM is already running");
1996
+ if (await isNexusRunning()) {
1997
+ log.success("NEXUS is already running");
1532
1998
  return;
1533
1999
  }
1534
- const platform = detectPlatform();
1535
- const diskPath = path7.join(NEXUS_HOME2, "vm", "images", "nexus-vm-disk.qcow2");
1536
- const isoPath = path7.join(NEXUS_HOME2, "vm", "images", "init.iso");
1537
- let spinner = createSpinner("Starting VM...");
2000
+ let spinner = createSpinner("Pulling NEXUS image...");
1538
2001
  spinner.start();
1539
- await launchVm(platform, diskPath, isoPath, config.vmRam, config.vmCpus, {
1540
- ssh: config.sshPort,
1541
- http: config.httpPort,
1542
- https: config.httpsPort
1543
- });
1544
- succeed(spinner, "VM started");
1545
- spinner = createSpinner("Waiting for SSH...");
2002
+ await pullImage("buildwithnexus/nexus", "latest");
2003
+ succeed(spinner, "Image ready");
2004
+ spinner = createSpinner("Starting NEXUS container...");
1546
2005
  spinner.start();
1547
- const sshOk = await waitForSsh(config.sshPort, 12e4);
1548
- if (!sshOk) {
1549
- fail(spinner, "SSH timed out");
1550
- process.exit(1);
1551
- }
1552
- succeed(spinner, "SSH connected");
1553
- spinner = createSpinner("Starting NEXUS server...");
2006
+ await startNexus(
2007
+ { anthropic: config.anthropicKey, openai: config.openaiKey },
2008
+ { port: config.httpPort }
2009
+ );
2010
+ succeed(spinner, "Container started");
2011
+ spinner = createSpinner("Waiting for NEXUS server...");
1554
2012
  spinner.start();
1555
- await sshExec(config.sshPort, "sudo systemctl start nexus");
1556
- const ok = await waitForServer(config.sshPort, 6e4);
2013
+ const ok = await waitForServer(6e4);
1557
2014
  if (ok) {
1558
2015
  succeed(spinner, "NEXUS server running");
1559
2016
  } else {
@@ -1562,7 +2019,7 @@ var startCommand = new Command2("start").description("Start the NEXUS runtime").
1562
2019
  if (config.enableTunnel) {
1563
2020
  spinner = createSpinner("Starting tunnel...");
1564
2021
  spinner.start();
1565
- const url = await startTunnel(config.sshPort);
2022
+ const url = await startTunnel();
1566
2023
  if (url) {
1567
2024
  succeed(spinner, `Tunnel: ${url}`);
1568
2025
  } else {
@@ -1573,127 +2030,167 @@ var startCommand = new Command2("start").description("Start the NEXUS runtime").
1573
2030
  });
1574
2031
 
1575
2032
  // src/commands/stop.ts
1576
- import { Command as Command3 } from "commander";
1577
- init_secrets();
1578
- init_qemu();
1579
- init_ssh();
1580
- var stopCommand = new Command3("stop").description("Gracefully shut down the NEXUS runtime").action(async () => {
1581
- const config = loadConfig();
1582
- if (!config) {
1583
- log.error("No NEXUS configuration found. Run: buildwithnexus init");
1584
- process.exit(1);
2033
+ import { Command as Command4 } from "commander";
2034
+ import { execFile } from "child_process";
2035
+ import { promisify } from "util";
2036
+ var execFileAsync = promisify(execFile);
2037
+ async function containerExists() {
2038
+ try {
2039
+ const { stdout } = await execFileAsync("docker", [
2040
+ "ps",
2041
+ "-a",
2042
+ "--filter",
2043
+ "name=^nexus$",
2044
+ "--format",
2045
+ "{{.Names}}"
2046
+ ]);
2047
+ return stdout.trim() === "nexus";
2048
+ } catch {
2049
+ return false;
2050
+ }
2051
+ }
2052
+ async function isContainerRunning() {
2053
+ try {
2054
+ const { stdout } = await execFileAsync("docker", [
2055
+ "ps",
2056
+ "--filter",
2057
+ "name=^nexus$",
2058
+ "--filter",
2059
+ "status=running",
2060
+ "--format",
2061
+ "{{.Names}}"
2062
+ ]);
2063
+ return stdout.trim() === "nexus";
2064
+ } catch {
2065
+ return false;
1585
2066
  }
1586
- if (!isVmRunning()) {
1587
- log.warn("VM is not running");
2067
+ }
2068
+ var stopCommand = new Command4("stop").description("Gracefully shut down the NEXUS runtime").action(async () => {
2069
+ if (!await containerExists()) {
2070
+ log.warn("NEXUS container does not exist");
1588
2071
  return;
1589
2072
  }
1590
- const spinner = createSpinner("Shutting down...");
2073
+ const spinner = createSpinner("Shutting down NEXUS container...");
1591
2074
  spinner.start();
1592
2075
  try {
1593
- if (config.enableTunnel) {
1594
- spinner.text = "Stopping tunnel...";
1595
- await stopTunnel(config.sshPort);
1596
- }
1597
- spinner.text = "Stopping NEXUS server...";
1598
- await sshExec(config.sshPort, "sudo systemctl stop nexus");
1599
- spinner.text = "Shutting down VM...";
1600
- await sshExec(config.sshPort, "sudo shutdown -h now").catch(() => {
1601
- });
1602
- await new Promise((r) => setTimeout(r, 5e3));
1603
- if (isVmRunning()) {
1604
- stopVm();
2076
+ if (await isContainerRunning()) {
2077
+ spinner.text = "Stopping container...";
2078
+ await execFileAsync("docker", ["stop", "nexus"]);
1605
2079
  }
1606
- succeed(spinner, "NEXUS runtime stopped");
1607
- } catch {
1608
- stopVm();
1609
- succeed(spinner, "VM force-stopped");
2080
+ spinner.text = "Removing container...";
2081
+ await execFileAsync("docker", ["rm", "nexus"]);
2082
+ succeed(spinner, "NEXUS container stopped and removed");
2083
+ } catch (err) {
2084
+ fail(spinner, "Failed to stop NEXUS container");
2085
+ log.error(err instanceof Error ? err.message : String(err));
2086
+ process.exit(1);
1610
2087
  }
1611
2088
  });
1612
2089
 
1613
2090
  // src/commands/status.ts
1614
- import { Command as Command4 } from "commander";
1615
- import chalk7 from "chalk";
1616
- init_secrets();
1617
- init_qemu();
1618
- var statusCommand = new Command4("status").description("Check NEXUS runtime health").option("--json", "Output as JSON").action(async (opts) => {
2091
+ import { Command as Command5 } from "commander";
2092
+ import chalk8 from "chalk";
2093
+ async function checkHttpHealth(port) {
2094
+ try {
2095
+ const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(5e3) });
2096
+ if (!res.ok) return { healthy: false, version: null, uptimeSeconds: null };
2097
+ const text = await res.text();
2098
+ let version2 = null;
2099
+ let uptimeSeconds = null;
2100
+ try {
2101
+ const parsed = JSON.parse(text);
2102
+ if (typeof parsed.version === "string") version2 = parsed.version;
2103
+ if (typeof parsed.uptime === "number") uptimeSeconds = parsed.uptime;
2104
+ } catch {
2105
+ }
2106
+ const healthy = text.includes("ok") || res.status === 200;
2107
+ return { healthy, version: version2, uptimeSeconds };
2108
+ } catch {
2109
+ return { healthy: false, version: null, uptimeSeconds: null };
2110
+ }
2111
+ }
2112
+ function formatUptime(seconds) {
2113
+ if (seconds < 60) return `${seconds}s`;
2114
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
2115
+ const h = Math.floor(seconds / 3600);
2116
+ const m = Math.floor(seconds % 3600 / 60);
2117
+ return `${h}h ${m}m`;
2118
+ }
2119
+ var statusCommand = new Command5("status").description("Check NEXUS runtime health").option("--json", "Output as JSON").action(async (opts) => {
1619
2120
  const config = loadConfig();
1620
2121
  if (!config) {
1621
2122
  log.error("No NEXUS configuration found. Run: buildwithnexus init");
1622
2123
  process.exit(1);
1623
2124
  }
1624
- const vmRunning = isVmRunning();
1625
- const health = await checkHealth(config.sshPort, vmRunning);
2125
+ const containerRunning = await isNexusRunning();
2126
+ const { healthy, version: version2, uptimeSeconds } = containerRunning ? await checkHttpHealth(config.httpPort) : { healthy: false, version: null, uptimeSeconds: null };
1626
2127
  if (opts.json) {
1627
- console.log(JSON.stringify({ ...health, pid: getVmPid(), ports: { ssh: config.sshPort, http: config.httpPort, https: config.httpsPort } }, null, 2));
2128
+ console.log(
2129
+ JSON.stringify(
2130
+ {
2131
+ containerRunning,
2132
+ healthy,
2133
+ version: version2,
2134
+ uptimeSeconds,
2135
+ port: config.httpPort,
2136
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString()
2137
+ },
2138
+ null,
2139
+ 2
2140
+ )
2141
+ );
1628
2142
  return;
1629
2143
  }
1630
- const check = (ok) => ok ? chalk7.green("\u25CF") : chalk7.red("\u25CB");
2144
+ const check = (ok) => ok ? chalk8.green("\u25CF") : chalk8.red("\u25CB");
1631
2145
  console.log("");
1632
- console.log(chalk7.bold(" NEXUS Runtime Status"));
2146
+ console.log(chalk8.bold(" NEXUS Runtime Status"));
1633
2147
  console.log("");
1634
- console.log(` ${check(health.vmRunning)} VM ${health.vmRunning ? chalk7.green("running") + chalk7.dim(` (PID ${getVmPid()})`) : chalk7.red("stopped")}`);
1635
- console.log(` ${check(health.sshReady)} SSH ${health.sshReady ? chalk7.green("connected") + chalk7.dim(` (port ${config.sshPort})`) : chalk7.red("unreachable")}`);
1636
- console.log(` ${check(health.dockerReady)} Docker ${health.dockerReady ? chalk7.green("ready") + (health.dockerVersion ? chalk7.dim(` v${health.dockerVersion}`) : "") : chalk7.red("not ready")}`);
1637
- 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")}`);
1638
- console.log(` ${check(!!health.tunnelUrl)} Tunnel ${health.tunnelUrl ? chalk7.green(health.tunnelUrl) : chalk7.dim("not active")}`);
1639
- if (health.diskUsagePercent !== null) {
1640
- const diskOk = health.diskUsagePercent < 85;
1641
- console.log(` ${check(diskOk)} Disk ${diskOk ? chalk7.green(`${health.diskUsagePercent}% used`) : chalk7.yellow(`${health.diskUsagePercent}% used \u2014 consider cleanup`)}`);
1642
- }
2148
+ console.log(
2149
+ ` ${check(containerRunning)} Container ${containerRunning ? chalk8.green("running") : chalk8.red("stopped")}`
2150
+ );
2151
+ console.log(
2152
+ ` ${check(healthy)} Health ${healthy ? chalk8.green("healthy") + chalk8.dim(` (port ${config.httpPort})`) + (version2 ? chalk8.dim(` v${version2}`) : "") + (uptimeSeconds !== null ? chalk8.dim(` up ${formatUptime(uptimeSeconds)}`) : "") : chalk8.red("unhealthy")}`
2153
+ );
1643
2154
  console.log("");
1644
- if (health.serverHealthy) {
1645
- log.success(`NEXUS CLI ready \u2014 connect via: buildwithnexus ssh`);
2155
+ if (healthy) {
2156
+ log.success("NEXUS is running and healthy");
2157
+ } else if (containerRunning) {
2158
+ log.warn("Container is running but health check failed");
2159
+ } else {
2160
+ log.error("NEXUS container is not running. Start with: buildwithnexus start");
1646
2161
  }
1647
2162
  });
1648
2163
 
1649
2164
  // src/commands/doctor.ts
1650
- import { Command as Command5 } from "commander";
1651
- import chalk8 from "chalk";
1652
- import fs7 from "fs";
1653
- init_qemu();
1654
- init_secrets();
1655
- import path8 from "path";
1656
- import { execa as execa4 } from "execa";
1657
- var doctorCommand = new Command5("doctor").description("Diagnose NEXUS runtime environment").action(async () => {
2165
+ import { Command as Command6 } from "commander";
2166
+ import chalk9 from "chalk";
2167
+ import fs4 from "fs";
2168
+ import path5 from "path";
2169
+ var doctorCommand = new Command6("doctor").description("Diagnose NEXUS runtime environment").action(async () => {
1658
2170
  const platform = detectPlatform();
1659
- const check = (ok) => ok ? chalk8.green("\u2713") : chalk8.red("\u2717");
2171
+ const check = (ok) => ok ? chalk9.green("\u2713") : chalk9.red("\u2717");
1660
2172
  console.log("");
1661
- console.log(chalk8.bold(" NEXUS Doctor"));
2173
+ console.log(chalk9.bold(" NEXUS Doctor"));
1662
2174
  console.log("");
1663
2175
  const nodeOk = Number(process.versions.node.split(".")[0]) >= 18;
1664
- console.log(` ${check(nodeOk)} Node.js ${process.versions.node} ${nodeOk ? "" : chalk8.red("(need >= 18)")}`);
2176
+ console.log(` ${check(nodeOk)} Node.js ${process.versions.node} ${nodeOk ? "" : chalk9.red("(need >= 18)")}`);
1665
2177
  console.log(` ${check(true)} Platform: ${platform.os} ${platform.arch}`);
1666
- const qemuOk = await isQemuInstalled(platform);
1667
- if (qemuOk) {
1668
- const { stdout } = await execa4(platform.qemuBinary, ["--version"]);
1669
- console.log(` ${check(true)} ${stdout.split("\n")[0]}`);
2178
+ const dockerOk = await isDockerInstalled();
2179
+ if (dockerOk) {
2180
+ console.log(` ${check(true)} Docker installed and running`);
1670
2181
  } else {
1671
- console.log(` ${check(false)} QEMU not installed`);
1672
- }
1673
- let isoTool = false;
1674
- try {
1675
- await execa4("mkisofs", ["--version"]);
1676
- isoTool = true;
1677
- } catch {
2182
+ console.log(` ${check(false)} Docker not installed`);
1678
2183
  }
1679
- if (!isoTool) try {
1680
- await execa4("genisoimage", ["--version"]);
1681
- isoTool = true;
1682
- } catch {
1683
- }
1684
- console.log(` ${check(isoTool)} ISO tool (mkisofs/genisoimage)`);
1685
- const keyExists = fs7.existsSync(path8.join(NEXUS_HOME2, "ssh", "id_nexus_vm"));
2184
+ const keyExists = fs4.existsSync(path5.join(NEXUS_HOME2, "ssh", "id_nexus_vm"));
1686
2185
  console.log(` ${check(keyExists)} SSH key`);
1687
2186
  const config = loadConfig();
1688
2187
  console.log(` ${check(!!config)} Configuration`);
1689
- const diskExists = fs7.existsSync(path8.join(NEXUS_HOME2, "vm", "images", "nexus-vm-disk.qcow2"));
1690
- console.log(` ${check(diskExists)} VM disk image`);
1691
2188
  if (config) {
1692
- for (const [name, port] of [["SSH", config.sshPort], ["HTTP", config.httpPort], ["HTTPS", config.httpsPort]]) {
2189
+ for (const [name, port] of [["HTTP", config.httpPort], ["HTTPS", config.httpsPort]]) {
1693
2190
  try {
1694
- const net2 = await import("net");
2191
+ const net = await import("net");
1695
2192
  const available = await new Promise((resolve) => {
1696
- const server = net2.createServer();
2193
+ const server = net.createServer();
1697
2194
  server.once("error", () => resolve(false));
1698
2195
  server.once("listening", () => {
1699
2196
  server.close();
@@ -1701,115 +2198,104 @@ var doctorCommand = new Command5("doctor").description("Diagnose NEXUS runtime e
1701
2198
  });
1702
2199
  server.listen(port);
1703
2200
  });
1704
- console.log(` ${check(available)} Port ${port} (${name}) ${available ? "available" : chalk8.red("in use")}`);
2201
+ console.log(` ${check(available)} Port ${port} (${name}) ${available ? "available" : chalk9.red("in use")}`);
1705
2202
  } catch {
1706
2203
  console.log(` ${check(false)} Port ${port} (${name}) \u2014 check failed`);
1707
2204
  }
1708
2205
  }
1709
2206
  }
1710
- const biosOk = fs7.existsSync(platform.biosPath);
1711
- console.log(` ${check(biosOk)} UEFI firmware ${biosOk ? "" : chalk8.dim(platform.biosPath)}`);
1712
2207
  console.log("");
1713
- if (qemuOk && isoTool && biosOk) {
2208
+ if (dockerOk) {
1714
2209
  log.success("Environment ready for NEXUS");
1715
2210
  } else {
1716
- log.warn("Some prerequisites missing \u2014 fix the items marked \u2717 above");
2211
+ log.warn("Docker is required \u2014 install it from https://docs.docker.com/get-docker/");
1717
2212
  }
1718
2213
  });
1719
2214
 
1720
2215
  // src/commands/logs.ts
1721
- import { Command as Command6 } from "commander";
1722
- init_secrets();
1723
- init_qemu();
1724
- init_ssh();
1725
- init_dlp();
1726
- import { execa as execa5 } from "execa";
1727
- var logsCommand = new Command6("logs").description("View NEXUS server logs").option("-f, --follow", "Follow log output").option("-n, --lines <n>", "Number of lines to show", "50").action(async (opts) => {
1728
- const config = loadConfig();
1729
- if (!config) {
1730
- log.error("No NEXUS configuration found. Run: buildwithnexus init");
2216
+ import { Command as Command7 } from "commander";
2217
+ import { execa as execa3 } from "execa";
2218
+ var logsCommand = new Command7("logs").description("View NEXUS server logs").action(async () => {
2219
+ let containerExists2 = false;
2220
+ try {
2221
+ const { stdout } = await execa3("docker", [
2222
+ "ps",
2223
+ "-a",
2224
+ "--filter",
2225
+ "name=nexus",
2226
+ "--format",
2227
+ "{{.Names}}"
2228
+ ]);
2229
+ containerExists2 = stdout.trim().split("\n").some((name) => name === "nexus");
2230
+ } catch {
2231
+ log.error("Failed to query Docker. Is Docker running?");
1731
2232
  process.exit(1);
1732
2233
  }
1733
- if (!isVmRunning()) {
1734
- log.error("VM is not running. Start with: buildwithnexus start");
2234
+ if (!containerExists2) {
2235
+ log.error("NEXUS container not found. Start with: buildwithnexus start");
1735
2236
  process.exit(1);
1736
2237
  }
1737
- if (opts.follow) {
1738
- const proc = execa5("ssh", [
1739
- "nexus-vm",
1740
- "tail -f /home/nexus/.nexus/logs/server.log"
1741
- ], { stdout: "pipe", stderr: "pipe", env: scrubEnv() });
1742
- proc.stdout?.on("data", (chunk) => process.stdout.write(redact(chunk.toString())));
1743
- proc.stderr?.on("data", (chunk) => process.stderr.write(redact(chunk.toString())));
1744
- await proc;
1745
- } else {
1746
- const lines = /^\d+$/.test(opts.lines) ? parseInt(opts.lines, 10) : 50;
1747
- if (lines < 1 || lines > 1e4) {
1748
- log.error("--lines must be between 1 and 10000");
1749
- process.exit(1);
1750
- }
1751
- const { stdout } = await sshExec(
1752
- config.sshPort,
1753
- `tail -n ${lines} /home/nexus/.nexus/logs/server.log 2>/dev/null || echo "No logs yet"`
1754
- );
1755
- console.log(redact(stdout));
1756
- }
2238
+ const proc = execa3("docker", ["logs", "-f", "nexus"], {
2239
+ stdout: "pipe",
2240
+ stderr: "pipe"
2241
+ });
2242
+ proc.stdout?.on("data", (chunk) => process.stdout.write(chunk));
2243
+ proc.stderr?.on("data", (chunk) => process.stderr.write(chunk));
2244
+ await proc;
1757
2245
  });
1758
2246
 
1759
2247
  // src/commands/update.ts
1760
- import { Command as Command7 } from "commander";
1761
- import path9 from "path";
1762
- import fs8 from "fs";
2248
+ import { Command as Command8 } from "commander";
2249
+ import path6 from "path";
2250
+ import fs5 from "fs";
1763
2251
  import { fileURLToPath as fileURLToPath3 } from "url";
1764
- init_secrets();
1765
- init_qemu();
1766
- init_ssh();
1767
- function getReleaseTarball2() {
1768
- const dir = path9.dirname(fileURLToPath3(import.meta.url));
1769
- const tarballPath = path9.join(dir, "nexus-release.tar.gz");
1770
- if (fs8.existsSync(tarballPath)) return tarballPath;
1771
- const rootPath = path9.resolve(dir, "..", "dist", "nexus-release.tar.gz");
1772
- if (fs8.existsSync(rootPath)) return rootPath;
2252
+ import { execa as execa4 } from "execa";
2253
+ function getReleaseTarball() {
2254
+ const dir = path6.dirname(fileURLToPath3(import.meta.url));
2255
+ const tarballPath = path6.join(dir, "nexus-release.tar.gz");
2256
+ if (fs5.existsSync(tarballPath)) return tarballPath;
2257
+ const rootPath = path6.resolve(dir, "..", "dist", "nexus-release.tar.gz");
2258
+ if (fs5.existsSync(rootPath)) return rootPath;
1773
2259
  throw new Error("nexus-release.tar.gz not found. Reinstall buildwithnexus to get the latest release.");
1774
2260
  }
1775
- var updateCommand = new Command7("update").description("Update NEXUS to the latest bundled release and restart").action(async () => {
2261
+ var updateCommand = new Command8("update").description("Update NEXUS to the latest bundled release and restart").action(async () => {
1776
2262
  const config = loadConfig();
1777
2263
  if (!config) {
1778
2264
  log.error("No NEXUS configuration found. Run: buildwithnexus init");
1779
2265
  process.exit(1);
1780
2266
  }
1781
- if (!isVmRunning()) {
1782
- log.error("VM is not running. Start with: buildwithnexus start");
2267
+ if (!await isNexusRunning()) {
2268
+ log.error("NEXUS is not running. Start with: buildwithnexus start");
1783
2269
  process.exit(1);
1784
2270
  }
1785
2271
  let spinner = createSpinner("Uploading release tarball...");
1786
2272
  spinner.start();
1787
- const tarballPath = getReleaseTarball2();
1788
- await sshUploadFile(config.sshPort, tarballPath, "/tmp/nexus-release.tar.gz");
2273
+ const tarballPath = getReleaseTarball();
2274
+ await execa4("docker", ["cp", tarballPath, "nexus:/tmp/nexus-release.tar.gz"]);
1789
2275
  succeed(spinner, "Tarball uploaded");
1790
2276
  spinner = createSpinner("Stopping NEXUS server...");
1791
2277
  spinner.start();
1792
- await sshExec(config.sshPort, "sudo systemctl stop nexus");
2278
+ await dockerExec("sudo systemctl stop nexus");
1793
2279
  succeed(spinner, "Server stopped");
1794
2280
  spinner = createSpinner("Extracting new release...");
1795
2281
  spinner.start();
1796
- await sshExec(config.sshPort, "rm -rf /home/nexus/nexus/src /home/nexus/nexus/docker");
1797
- await sshExec(config.sshPort, "tar xzf /tmp/nexus-release.tar.gz -C /home/nexus/nexus");
1798
- await sshExec(config.sshPort, "rm -f /tmp/nexus-release.tar.gz");
2282
+ await dockerExec("rm -rf /home/nexus/nexus/src /home/nexus/nexus/docker");
2283
+ await dockerExec("tar xzf /tmp/nexus-release.tar.gz -C /home/nexus/nexus");
2284
+ await dockerExec("rm -f /tmp/nexus-release.tar.gz");
1799
2285
  succeed(spinner, "Release extracted");
1800
2286
  spinner = createSpinner("Installing dependencies...");
1801
2287
  spinner.start();
1802
- await sshExec(config.sshPort, "cd /home/nexus/nexus && .venv/bin/pip install -r requirements.txt -q");
2288
+ await dockerExec("cd /home/nexus/nexus && .venv/bin/pip install -r requirements.txt -q");
1803
2289
  succeed(spinner, "Dependencies installed");
1804
2290
  spinner = createSpinner("Rebuilding Docker sandbox...");
1805
2291
  spinner.start();
1806
- await sshExec(config.sshPort, "docker build -t nexus-cli-sandbox /home/nexus/nexus/docker/cli-sandbox/");
2292
+ await dockerExec("docker build -t nexus-cli-sandbox /home/nexus/nexus/docker/cli-sandbox/");
1807
2293
  succeed(spinner, "Docker image rebuilt");
1808
2294
  spinner = createSpinner("Restarting NEXUS server...");
1809
2295
  spinner.start();
1810
- await sshExec(config.sshPort, "sudo systemctl start nexus");
2296
+ await dockerExec("sudo systemctl start nexus");
1811
2297
  await new Promise((r) => setTimeout(r, 3e3));
1812
- const health = await sshExec(config.sshPort, "curl -sf http://localhost:4200/health");
2298
+ const health = await dockerExec("curl -sf http://localhost:4200/health");
1813
2299
  if (health.code === 0) {
1814
2300
  succeed(spinner, "NEXUS server restarted and healthy");
1815
2301
  } else {
@@ -1818,43 +2304,40 @@ var updateCommand = new Command7("update").description("Update NEXUS to the late
1818
2304
  });
1819
2305
 
1820
2306
  // src/commands/destroy.ts
1821
- import { Command as Command8 } from "commander";
1822
- import chalk9 from "chalk";
1823
- import fs9 from "fs";
2307
+ import { Command as Command9 } from "commander";
2308
+ import chalk10 from "chalk";
2309
+ import fs6 from "fs";
1824
2310
  import { input as input2 } from "@inquirer/prompts";
1825
- init_secrets();
1826
- init_qemu();
1827
- import path10 from "path";
1828
- var destroyCommand = new Command8("destroy").description("Remove NEXUS VM and all data").option("--force", "Skip confirmation").action(async (opts) => {
2311
+ import path7 from "path";
2312
+ var destroyCommand = new Command9("destroy").description("Remove NEXUS VM and all data").option("--force", "Skip confirmation").action(async (opts) => {
1829
2313
  const config = loadConfig();
1830
2314
  if (!opts.force) {
1831
2315
  console.log("");
1832
- console.log(chalk9.red.bold(" This will permanently delete:"));
1833
- console.log(chalk9.red(" - NEXUS VM and all data inside it"));
1834
- console.log(chalk9.red(" - VM disk images"));
1835
- console.log(chalk9.red(" - SSH keys"));
1836
- console.log(chalk9.red(" - Configuration and API keys"));
2316
+ console.log(chalk10.red.bold(" This will permanently delete:"));
2317
+ console.log(chalk10.red(" - NEXUS container and all data inside it"));
2318
+ console.log(chalk10.red(" - SSH keys"));
2319
+ console.log(chalk10.red(" - Configuration and API keys"));
1837
2320
  console.log("");
1838
- const confirm2 = await input2({
2321
+ const confirm3 = await input2({
1839
2322
  message: 'Type "destroy" to confirm:'
1840
2323
  });
1841
- if (confirm2 !== "destroy") {
2324
+ if (confirm3 !== "destroy") {
1842
2325
  log.warn("Aborted");
1843
2326
  return;
1844
2327
  }
1845
2328
  }
1846
2329
  const spinner = createSpinner("Destroying NEXUS runtime...");
1847
2330
  spinner.start();
1848
- if (config && isVmRunning()) {
2331
+ if (config && await isNexusRunning()) {
1849
2332
  try {
1850
- await stopTunnel(config.sshPort);
2333
+ await dockerExec("pkill -f cloudflared || true");
1851
2334
  } catch {
1852
2335
  }
1853
- stopVm();
2336
+ await stopNexus();
1854
2337
  }
1855
- const sshConfigPath = path10.join(process.env.HOME || "~", ".ssh", "config");
1856
- if (fs9.existsSync(sshConfigPath)) {
1857
- const content = fs9.readFileSync(sshConfigPath, "utf-8");
2338
+ const sshConfigPath = path7.join(process.env.HOME || "~", ".ssh", "config");
2339
+ if (fs6.existsSync(sshConfigPath)) {
2340
+ const content = fs6.readFileSync(sshConfigPath, "utf-8");
1858
2341
  const lines = content.split("\n");
1859
2342
  const filtered = [];
1860
2343
  let skip = false;
@@ -1867,30 +2350,28 @@ var destroyCommand = new Command8("destroy").description("Remove NEXUS VM and al
1867
2350
  skip = false;
1868
2351
  filtered.push(line);
1869
2352
  }
1870
- fs9.writeFileSync(sshConfigPath, filtered.join("\n"));
2353
+ fs6.writeFileSync(sshConfigPath, filtered.join("\n"));
1871
2354
  }
1872
- fs9.rmSync(NEXUS_HOME2, { recursive: true, force: true });
2355
+ fs6.rmSync(NEXUS_HOME2, { recursive: true, force: true });
1873
2356
  succeed(spinner, "NEXUS runtime destroyed");
1874
2357
  log.dim("Run 'buildwithnexus init' to set up again");
1875
2358
  });
1876
2359
 
1877
2360
  // src/commands/keys.ts
1878
- import { Command as Command9 } from "commander";
2361
+ import { Command as Command10 } from "commander";
1879
2362
  import { password as password2 } from "@inquirer/prompts";
1880
- import chalk10 from "chalk";
1881
- init_secrets();
1882
- init_dlp();
1883
- var keysCommand = new Command9("keys").description("Manage API keys");
2363
+ import chalk11 from "chalk";
2364
+ var keysCommand = new Command10("keys").description("Manage API keys");
1884
2365
  keysCommand.command("list").description("Show configured API keys (masked)").action(() => {
1885
2366
  const keys = loadKeys();
1886
2367
  if (!keys) {
1887
2368
  log.error("No keys configured. Run: buildwithnexus init");
1888
2369
  process.exit(1);
1889
2370
  }
1890
- console.log(chalk10.bold("\n Configured Keys\n"));
2371
+ console.log(chalk11.bold("\n Configured Keys\n"));
1891
2372
  for (const [name, value] of Object.entries(keys)) {
1892
2373
  if (value) {
1893
- console.log(` ${chalk10.cyan(name.padEnd(24))} ${maskKey(value)}`);
2374
+ console.log(` ${chalk11.cyan(name.padEnd(24))} ${maskKey(value)}`);
1894
2375
  }
1895
2376
  }
1896
2377
  console.log("");
@@ -1935,87 +2416,80 @@ keysCommand.command("set <key>").description("Set or update an API key (e.g. ANT
1935
2416
  });
1936
2417
 
1937
2418
  // src/commands/ssh.ts
1938
- import { Command as Command10 } from "commander";
1939
- init_secrets();
1940
- init_qemu();
1941
- init_ssh();
1942
- var sshCommand = new Command10("ssh").description("Open an SSH session into the NEXUS VM").action(async () => {
1943
- const config = loadConfig();
1944
- if (!config) {
1945
- log.error("No NEXUS configuration found. Run: buildwithnexus init");
1946
- process.exit(1);
1947
- }
1948
- if (!isVmRunning()) {
1949
- log.error("VM is not running. Start it with: buildwithnexus start");
2419
+ import { Command as Command11 } from "commander";
2420
+ import { execa as execa5 } from "execa";
2421
+ var sshCommand = new Command11("ssh").description("Open an interactive shell inside the NEXUS container").action(async () => {
2422
+ const running = await isNexusRunning();
2423
+ if (!running) {
2424
+ log.error("NEXUS container is not running. Start it with: buildwithnexus start");
1950
2425
  process.exit(1);
1951
2426
  }
1952
- log.dim(`Connecting to nexus-vm (port ${config.sshPort})...`);
1953
- await openInteractiveSsh(config.sshPort);
2427
+ log.dim("Opening shell in NEXUS container...");
2428
+ await execa5("docker", ["exec", "-it", "nexus", "/bin/bash"], { stdio: "inherit" });
1954
2429
  });
1955
2430
 
1956
2431
  // src/commands/brainstorm.ts
1957
- import { Command as Command11 } from "commander";
1958
- import chalk11 from "chalk";
2432
+ import { Command as Command12 } from "commander";
2433
+ import chalk12 from "chalk";
1959
2434
  import { input as input3 } from "@inquirer/prompts";
1960
- init_secrets();
1961
- init_qemu();
1962
- init_ssh();
1963
- init_dlp();
1964
- var COS_PREFIX = chalk11.bold.cyan(" Chief of Staff");
1965
- var YOU_PREFIX = chalk11.bold.white(" You");
1966
- var DIVIDER = chalk11.dim(" " + "\u2500".repeat(56));
2435
+ var COS_PREFIX = chalk12.bold.cyan(" Chief of Staff");
2436
+ var YOU_PREFIX = chalk12.bold.white(" You");
2437
+ var DIVIDER = chalk12.dim(" " + "\u2500".repeat(56));
1967
2438
  function formatResponse(text) {
1968
2439
  const lines = text.split("\n");
1969
- return lines.map((line) => chalk11.white(" " + line)).join("\n");
2440
+ return lines.map((line) => chalk12.white(" " + line)).join("\n");
1970
2441
  }
1971
- async function sendMessage(sshPort, message, source) {
1972
- const payload = JSON.stringify({ message, source });
1973
- const escaped = shellEscape(payload);
1974
- const { stdout, code } = await sshExec(
1975
- sshPort,
1976
- `curl -sf -X POST http://localhost:4200/message -H 'Content-Type: application/json' -d ${escaped}`
1977
- );
1978
- if (code !== 0) {
1979
- throw new Error("Server returned a non-zero exit code");
2442
+ async function sendMessage(httpPort, message, source) {
2443
+ const res = await fetch(`http://localhost:${httpPort}/message`, {
2444
+ method: "POST",
2445
+ headers: { "Content-Type": "application/json" },
2446
+ body: JSON.stringify({ message, source })
2447
+ });
2448
+ if (!res.ok) {
2449
+ throw new Error(`Server returned status ${res.status}`);
1980
2450
  }
2451
+ const text = await res.text();
1981
2452
  try {
1982
- const parsed = JSON.parse(stdout);
1983
- return parsed.response ?? parsed.message ?? stdout;
2453
+ const parsed = JSON.parse(text);
2454
+ return parsed.response ?? parsed.message ?? text;
1984
2455
  } catch {
1985
- return stdout;
2456
+ return text;
1986
2457
  }
1987
2458
  }
1988
- var brainstormCommand = new Command11("brainstorm").description("Brainstorm an idea with the NEXUS Chief of Staff").argument("[idea...]", "Your idea or question").action(async (ideaWords) => {
2459
+ var brainstormCommand = new Command12("brainstorm").description("Brainstorm an idea with the NEXUS Chief of Staff").argument("[idea...]", "Your idea or question").action(async (ideaWords) => {
1989
2460
  try {
1990
2461
  const config = loadConfig();
1991
2462
  if (!config) {
1992
2463
  log.error("No NEXUS configuration found. Run: buildwithnexus init");
1993
2464
  process.exit(1);
1994
2465
  }
1995
- if (!isVmRunning()) {
1996
- log.error("VM is not running. Start it with: buildwithnexus start");
2466
+ if (!await isNexusRunning()) {
2467
+ log.error("NEXUS is not running. Start it with: buildwithnexus start");
1997
2468
  process.exit(1);
1998
2469
  }
1999
2470
  const spinner = createSpinner("Connecting to NEXUS...");
2000
2471
  spinner.start();
2001
- const { stdout: healthCheck, code: healthCode } = await sshExec(
2002
- config.sshPort,
2003
- "curl -sf http://localhost:4200/health"
2004
- );
2005
- if (healthCode !== 0 || !healthCheck.includes("ok")) {
2472
+ let healthOk = false;
2473
+ try {
2474
+ const healthRes = await fetch(`http://localhost:${config.httpPort}/health`);
2475
+ const healthText = await healthRes.text();
2476
+ healthOk = healthRes.ok && healthText.includes("ok");
2477
+ } catch {
2478
+ }
2479
+ if (!healthOk) {
2006
2480
  fail(spinner, "NEXUS server is not healthy");
2007
2481
  log.warn("Check status: buildwithnexus status");
2008
2482
  process.exit(1);
2009
2483
  }
2010
2484
  succeed(spinner, "Connected to NEXUS");
2011
2485
  console.log("");
2012
- console.log(chalk11.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
2013
- console.log(chalk11.bold(" \u2551 ") + chalk11.bold.cyan("NEXUS Brainstorm Session") + chalk11.bold(" \u2551"));
2014
- console.log(chalk11.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
2015
- console.log(chalk11.bold(" \u2551 ") + chalk11.dim("The Chief of Staff will discuss your idea with the".padEnd(55)) + chalk11.bold("\u2551"));
2016
- console.log(chalk11.bold(" \u2551 ") + chalk11.dim("NEXUS team and share their recommendations.".padEnd(55)) + chalk11.bold("\u2551"));
2017
- console.log(chalk11.bold(" \u2551 ") + chalk11.dim("Type 'exit' or 'quit' to end the session.".padEnd(55)) + chalk11.bold("\u2551"));
2018
- console.log(chalk11.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
2486
+ console.log(chalk12.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
2487
+ console.log(chalk12.bold(" \u2551 ") + chalk12.bold.cyan("NEXUS Brainstorm Session") + chalk12.bold(" \u2551"));
2488
+ console.log(chalk12.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
2489
+ console.log(chalk12.bold(" \u2551 ") + chalk12.dim("The Chief of Staff will discuss your idea with the".padEnd(55)) + chalk12.bold("\u2551"));
2490
+ console.log(chalk12.bold(" \u2551 ") + chalk12.dim("NEXUS team and share their recommendations.".padEnd(55)) + chalk12.bold("\u2551"));
2491
+ console.log(chalk12.bold(" \u2551 ") + chalk12.dim("Type 'exit' or 'quit' to end the session.".padEnd(55)) + chalk12.bold("\u2551"));
2492
+ console.log(chalk12.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
2019
2493
  console.log("");
2020
2494
  let idea = ideaWords.length > 0 ? ideaWords.join(" ") : "";
2021
2495
  if (!idea) {
@@ -2032,7 +2506,7 @@ var brainstormCommand = new Command11("brainstorm").description("Brainstorm an i
2032
2506
  while (true) {
2033
2507
  turn++;
2034
2508
  if (turn === 1) {
2035
- console.log(`${YOU_PREFIX}: ${chalk11.white(idea)}`);
2509
+ console.log(`${YOU_PREFIX}: ${chalk12.white(idea)}`);
2036
2510
  }
2037
2511
  console.log(DIVIDER);
2038
2512
  const thinking = createSpinner(
@@ -2040,7 +2514,7 @@ var brainstormCommand = new Command11("brainstorm").description("Brainstorm an i
2040
2514
  );
2041
2515
  thinking.start();
2042
2516
  const response = await sendMessage(
2043
- config.sshPort,
2517
+ config.httpPort,
2044
2518
  currentMessage,
2045
2519
  "brainstorm"
2046
2520
  );
@@ -2050,17 +2524,17 @@ var brainstormCommand = new Command11("brainstorm").description("Brainstorm an i
2050
2524
  console.log(formatResponse(redact(response)));
2051
2525
  console.log(DIVIDER);
2052
2526
  const followUp = await input3({
2053
- message: chalk11.bold("You:")
2527
+ message: chalk12.bold("You:")
2054
2528
  });
2055
2529
  const trimmed = followUp.trim().toLowerCase();
2056
2530
  if (!trimmed || trimmed === "exit" || trimmed === "quit" || trimmed === "q") {
2057
2531
  console.log("");
2058
2532
  log.success("Brainstorm session ended");
2059
- console.log(chalk11.dim(" Run again anytime: buildwithnexus brainstorm"));
2533
+ console.log(chalk12.dim(" Run again anytime: buildwithnexus brainstorm"));
2060
2534
  console.log("");
2061
2535
  return;
2062
2536
  }
2063
- console.log(`${YOU_PREFIX}: ${chalk11.white(followUp)}`);
2537
+ console.log(`${YOU_PREFIX}: ${chalk12.white(followUp)}`);
2064
2538
  currentMessage = `[BRAINSTORM FOLLOW-UP] The CEO responds: ${followUp}`;
2065
2539
  }
2066
2540
  } catch (err) {
@@ -2076,19 +2550,15 @@ var brainstormCommand = new Command11("brainstorm").description("Brainstorm an i
2076
2550
  });
2077
2551
 
2078
2552
  // src/commands/ninety-nine.ts
2079
- import { Command as Command12 } from "commander";
2080
- import chalk12 from "chalk";
2553
+ import { Command as Command13 } from "commander";
2554
+ import chalk13 from "chalk";
2081
2555
  import { input as input4 } from "@inquirer/prompts";
2082
- init_secrets();
2083
- init_qemu();
2084
- init_ssh();
2085
- init_dlp();
2086
- var AGENT_PREFIX = chalk12.bold.green(" 99 \u276F");
2087
- var YOU_PREFIX2 = chalk12.bold.white(" You");
2088
- var DIVIDER2 = chalk12.dim(" " + "\u2500".repeat(56));
2556
+ var AGENT_PREFIX = chalk13.bold.green(" 99 \u276F");
2557
+ var YOU_PREFIX2 = chalk13.bold.white(" You");
2558
+ var DIVIDER2 = chalk13.dim(" " + "\u2500".repeat(56));
2089
2559
  function formatAgentActivity(text) {
2090
2560
  const lines = text.split("\n");
2091
- return lines.map((line) => chalk12.white(" " + line)).join("\n");
2561
+ return lines.map((line) => chalk13.white(" " + line)).join("\n");
2092
2562
  }
2093
2563
  function parsePrefixes(instruction) {
2094
2564
  const files = [];
@@ -2106,7 +2576,7 @@ function parsePrefixes(instruction) {
2106
2576
  }
2107
2577
  return { cleaned: remaining.join(" "), files, rules };
2108
2578
  }
2109
- async function sendToNexus(sshPort, instruction, files, rules, cwd) {
2579
+ async function sendToNexus(instruction, files, rules, cwd) {
2110
2580
  const message = `[99] ${instruction}`;
2111
2581
  const payload = JSON.stringify({
2112
2582
  message,
@@ -2114,8 +2584,7 @@ async function sendToNexus(sshPort, instruction, files, rules, cwd) {
2114
2584
  context: { files, rules, cwd }
2115
2585
  });
2116
2586
  const escaped = shellEscape(payload);
2117
- const { stdout, code } = await sshExec(
2118
- sshPort,
2587
+ const { stdout, code } = await dockerExec(
2119
2588
  `curl -sf -X POST http://localhost:4200/message -H 'Content-Type: application/json' -d ${escaped}`
2120
2589
  );
2121
2590
  if (code !== 0) {
@@ -2128,21 +2597,20 @@ async function sendToNexus(sshPort, instruction, files, rules, cwd) {
2128
2597
  return stdout;
2129
2598
  }
2130
2599
  }
2131
- var ninetyNineCommand = new Command12("99").description("AI pair-programming session backed by the full NEXUS agent engine").argument("[instruction...]", "What to build, edit, or debug").option("--edit <file>", "AI-assisted edit for a specific file").option("--search <query>", "Contextual codebase search").option("--debug", "Debug current issue with NEXUS agent assistance").action(async (instructionWords, opts) => {
2600
+ var ninetyNineCommand = new Command13("99").description("AI pair-programming session backed by the full NEXUS agent engine").argument("[instruction...]", "What to build, edit, or debug").option("--edit <file>", "AI-assisted edit for a specific file").option("--search <query>", "Contextual codebase search").option("--debug", "Debug current issue with NEXUS agent assistance").action(async (instructionWords, opts) => {
2132
2601
  try {
2133
2602
  const config = loadConfig();
2134
2603
  if (!config) {
2135
2604
  log.error("No NEXUS configuration found. Run: buildwithnexus init");
2136
2605
  process.exit(1);
2137
2606
  }
2138
- if (!isVmRunning()) {
2139
- log.error("VM is not running. Start it with: buildwithnexus start");
2607
+ if (!await isNexusRunning()) {
2608
+ log.error("NEXUS is not running. Start it with: buildwithnexus start");
2140
2609
  process.exit(1);
2141
2610
  }
2142
2611
  const spinner = createSpinner("Connecting to NEXUS...");
2143
2612
  spinner.start();
2144
- const { stdout: healthCheck, code: healthCode } = await sshExec(
2145
- config.sshPort,
2613
+ const { stdout: healthCheck, code: healthCode } = await dockerExec(
2146
2614
  "curl -sf http://localhost:4200/health"
2147
2615
  );
2148
2616
  if (healthCode !== 0 || !healthCheck.includes("ok")) {
@@ -2152,14 +2620,14 @@ var ninetyNineCommand = new Command12("99").description("AI pair-programming ses
2152
2620
  }
2153
2621
  succeed(spinner, "Connected to NEXUS");
2154
2622
  console.log("");
2155
- console.log(chalk12.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
2156
- console.log(chalk12.bold(" \u2551 ") + chalk12.bold.green("/99 Pair Programming") + chalk12.dim(" \u2014 powered by NEXUS") + chalk12.bold(" \u2551"));
2157
- console.log(chalk12.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
2158
- console.log(chalk12.bold(" \u2551 ") + chalk12.dim("Describe what you want changed. NEXUS engineers".padEnd(55)) + chalk12.bold("\u2551"));
2159
- console.log(chalk12.bold(" \u2551 ") + chalk12.dim("analyze and modify your code in real time.".padEnd(55)) + chalk12.bold("\u2551"));
2160
- console.log(chalk12.bold(" \u2551 ") + chalk12.dim("Use @file to attach context, #rule to load rules.".padEnd(55)) + chalk12.bold("\u2551"));
2161
- console.log(chalk12.bold(" \u2551 ") + chalk12.dim("Type 'exit' or 'quit' to end the session.".padEnd(55)) + chalk12.bold("\u2551"));
2162
- console.log(chalk12.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
2623
+ console.log(chalk13.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
2624
+ console.log(chalk13.bold(" \u2551 ") + chalk13.bold.green("/99 Pair Programming") + chalk13.dim(" \u2014 powered by NEXUS") + chalk13.bold(" \u2551"));
2625
+ console.log(chalk13.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
2626
+ console.log(chalk13.bold(" \u2551 ") + chalk13.dim("Describe what you want changed. NEXUS engineers".padEnd(55)) + chalk13.bold("\u2551"));
2627
+ console.log(chalk13.bold(" \u2551 ") + chalk13.dim("analyze and modify your code in real time.".padEnd(55)) + chalk13.bold("\u2551"));
2628
+ console.log(chalk13.bold(" \u2551 ") + chalk13.dim("Use @file to attach context, #rule to load rules.".padEnd(55)) + chalk13.bold("\u2551"));
2629
+ console.log(chalk13.bold(" \u2551 ") + chalk13.dim("Type 'exit' or 'quit' to end the session.".padEnd(55)) + chalk13.bold("\u2551"));
2630
+ console.log(chalk13.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
2163
2631
  console.log("");
2164
2632
  const cwd = process.cwd();
2165
2633
  if (opts.edit) {
@@ -2168,11 +2636,11 @@ var ninetyNineCommand = new Command12("99").description("AI pair-programming ses
2168
2636
  });
2169
2637
  const { cleaned, files, rules } = parsePrefixes(instruction);
2170
2638
  const fullInstruction = `Edit file ${opts.edit}: ${cleaned}`;
2171
- console.log(`${YOU_PREFIX2}: ${chalk12.white(fullInstruction)}`);
2639
+ console.log(`${YOU_PREFIX2}: ${chalk13.white(fullInstruction)}`);
2172
2640
  console.log(DIVIDER2);
2173
2641
  const thinking = createSpinner("NEXUS engineers analyzing the file...");
2174
2642
  thinking.start();
2175
- const response = await sendToNexus(config.sshPort, fullInstruction, [opts.edit, ...files], rules, cwd);
2643
+ const response = await sendToNexus(fullInstruction, [opts.edit, ...files], rules, cwd);
2176
2644
  thinking.stop();
2177
2645
  thinking.clear();
2178
2646
  console.log(`${AGENT_PREFIX}`);
@@ -2182,11 +2650,11 @@ var ninetyNineCommand = new Command12("99").description("AI pair-programming ses
2182
2650
  }
2183
2651
  if (opts.search) {
2184
2652
  const fullInstruction = `Search the codebase for: ${opts.search}`;
2185
- console.log(`${YOU_PREFIX2}: ${chalk12.white(fullInstruction)}`);
2653
+ console.log(`${YOU_PREFIX2}: ${chalk13.white(fullInstruction)}`);
2186
2654
  console.log(DIVIDER2);
2187
2655
  const thinking = createSpinner("Searching with NEXUS context...");
2188
2656
  thinking.start();
2189
- const response = await sendToNexus(config.sshPort, fullInstruction, [], [], cwd);
2657
+ const response = await sendToNexus(fullInstruction, [], [], cwd);
2190
2658
  thinking.stop();
2191
2659
  thinking.clear();
2192
2660
  console.log(`${AGENT_PREFIX}`);
@@ -2196,11 +2664,11 @@ var ninetyNineCommand = new Command12("99").description("AI pair-programming ses
2196
2664
  }
2197
2665
  if (opts.debug) {
2198
2666
  const fullInstruction = "Debug the current issue \u2014 analyze recent errors, identify the root cause, and propose a fix";
2199
- console.log(`${YOU_PREFIX2}: ${chalk12.white(fullInstruction)}`);
2667
+ console.log(`${YOU_PREFIX2}: ${chalk13.white(fullInstruction)}`);
2200
2668
  console.log(DIVIDER2);
2201
2669
  const thinking = createSpinner("NEXUS debugger agent analyzing...");
2202
2670
  thinking.start();
2203
- const response = await sendToNexus(config.sshPort, fullInstruction, [], [], cwd);
2671
+ const response = await sendToNexus(fullInstruction, [], [], cwd);
2204
2672
  thinking.stop();
2205
2673
  thinking.clear();
2206
2674
  console.log(`${AGENT_PREFIX}`);
@@ -2211,7 +2679,7 @@ var ninetyNineCommand = new Command12("99").description("AI pair-programming ses
2211
2679
  let initialInstruction = instructionWords.length > 0 ? instructionWords.join(" ") : "";
2212
2680
  if (!initialInstruction) {
2213
2681
  initialInstruction = await input4({
2214
- message: chalk12.green("99 \u276F")
2682
+ message: chalk13.green("99 \u276F")
2215
2683
  });
2216
2684
  if (!initialInstruction.trim()) {
2217
2685
  log.warn("No instruction provided");
@@ -2226,37 +2694,37 @@ var ninetyNineCommand = new Command12("99").description("AI pair-programming ses
2226
2694
  const display = currentInstruction;
2227
2695
  const nexusInstruction = cleaned || currentInstruction;
2228
2696
  if (turn === 1) {
2229
- console.log(`${YOU_PREFIX2}: ${chalk12.white(display)}`);
2697
+ console.log(`${YOU_PREFIX2}: ${chalk13.white(display)}`);
2230
2698
  }
2231
2699
  if (files.length > 0) {
2232
- console.log(chalk12.dim(` Attaching: ${files.join(", ")}`));
2700
+ console.log(chalk13.dim(` Attaching: ${files.join(", ")}`));
2233
2701
  }
2234
2702
  if (rules.length > 0) {
2235
- console.log(chalk12.dim(` Rules: ${rules.join(", ")}`));
2703
+ console.log(chalk13.dim(` Rules: ${rules.join(", ")}`));
2236
2704
  }
2237
2705
  console.log(DIVIDER2);
2238
2706
  const thinking = createSpinner(
2239
2707
  turn === 1 ? "NEXUS agents analyzing and implementing..." : "NEXUS agents processing..."
2240
2708
  );
2241
2709
  thinking.start();
2242
- const response = await sendToNexus(config.sshPort, nexusInstruction, files, rules, cwd);
2710
+ const response = await sendToNexus(nexusInstruction, files, rules, cwd);
2243
2711
  thinking.stop();
2244
2712
  thinking.clear();
2245
2713
  console.log(`${AGENT_PREFIX}`);
2246
2714
  console.log(formatAgentActivity(redact(response)));
2247
2715
  console.log(DIVIDER2);
2248
2716
  const followUp = await input4({
2249
- message: chalk12.green("99 \u276F")
2717
+ message: chalk13.green("99 \u276F")
2250
2718
  });
2251
2719
  const trimmed = followUp.trim().toLowerCase();
2252
2720
  if (!trimmed || trimmed === "exit" || trimmed === "quit" || trimmed === "q") {
2253
2721
  console.log("");
2254
2722
  log.success("/99 session ended");
2255
- console.log(chalk12.dim(" Run again: buildwithnexus 99"));
2723
+ console.log(chalk13.dim(" Run again: buildwithnexus 99"));
2256
2724
  console.log("");
2257
2725
  return;
2258
2726
  }
2259
- console.log(`${YOU_PREFIX2}: ${chalk12.white(followUp)}`);
2727
+ console.log(`${YOU_PREFIX2}: ${chalk13.white(followUp)}`);
2260
2728
  currentInstruction = followUp;
2261
2729
  }
2262
2730
  } catch (err) {
@@ -2272,20 +2740,15 @@ var ninetyNineCommand = new Command12("99").description("AI pair-programming ses
2272
2740
  });
2273
2741
 
2274
2742
  // src/commands/shell.ts
2275
- import { Command as Command13 } from "commander";
2276
- import chalk15 from "chalk";
2277
- init_secrets();
2278
- init_qemu();
2279
- init_ssh();
2280
- init_dlp();
2743
+ import { Command as Command14 } from "commander";
2744
+ import chalk16 from "chalk";
2281
2745
 
2282
2746
  // src/ui/repl.ts
2283
- init_secrets();
2284
- import readline from "readline";
2285
- import fs10 from "fs";
2286
- import path11 from "path";
2287
- import chalk13 from "chalk";
2288
- var HISTORY_FILE = path11.join(NEXUS_HOME2, "shell_history");
2747
+ import readline2 from "readline";
2748
+ import fs7 from "fs";
2749
+ import path8 from "path";
2750
+ import chalk14 from "chalk";
2751
+ var HISTORY_FILE = path8.join(NEXUS_HOME2, "shell_history");
2289
2752
  var MAX_HISTORY = 1e3;
2290
2753
  var Repl = class {
2291
2754
  rl = null;
@@ -2301,25 +2764,25 @@ var Repl = class {
2301
2764
  }
2302
2765
  loadHistory() {
2303
2766
  try {
2304
- if (fs10.existsSync(HISTORY_FILE)) {
2305
- this.history = fs10.readFileSync(HISTORY_FILE, "utf-8").split("\n").filter(Boolean).slice(-MAX_HISTORY);
2767
+ if (fs7.existsSync(HISTORY_FILE)) {
2768
+ this.history = fs7.readFileSync(HISTORY_FILE, "utf-8").split("\n").filter(Boolean).slice(-MAX_HISTORY);
2306
2769
  }
2307
2770
  } catch {
2308
2771
  }
2309
2772
  }
2310
2773
  saveHistory() {
2311
2774
  try {
2312
- const dir = path11.dirname(HISTORY_FILE);
2313
- fs10.mkdirSync(dir, { recursive: true });
2314
- fs10.writeFileSync(HISTORY_FILE, this.history.slice(-MAX_HISTORY).join("\n") + "\n", { mode: 384 });
2775
+ const dir = path8.dirname(HISTORY_FILE);
2776
+ fs7.mkdirSync(dir, { recursive: true });
2777
+ fs7.writeFileSync(HISTORY_FILE, this.history.slice(-MAX_HISTORY).join("\n") + "\n", { mode: 384 });
2315
2778
  } catch {
2316
2779
  }
2317
2780
  }
2318
2781
  async start() {
2319
- this.rl = readline.createInterface({
2782
+ this.rl = readline2.createInterface({
2320
2783
  input: process.stdin,
2321
2784
  output: process.stdout,
2322
- prompt: chalk13.bold.cyan("nexus") + chalk13.dim(" \u276F "),
2785
+ prompt: chalk14.bold.cyan("nexus") + chalk14.dim(" \u276F "),
2323
2786
  history: this.history,
2324
2787
  historySize: MAX_HISTORY,
2325
2788
  terminal: true
@@ -2349,25 +2812,25 @@ var Repl = class {
2349
2812
  try {
2350
2813
  await cmd.handler();
2351
2814
  } catch (err) {
2352
- console.log(chalk13.red(` \u2717 Command failed: ${err.message}`));
2815
+ console.log(chalk14.red(` \u2717 Command failed: ${err.message}`));
2353
2816
  }
2354
2817
  this.rl?.prompt();
2355
2818
  return;
2356
2819
  }
2357
- console.log(chalk13.yellow(` Unknown command: /${cmdName}. Type /help for available commands.`));
2820
+ console.log(chalk14.yellow(` Unknown command: /${cmdName}. Type /help for available commands.`));
2358
2821
  this.rl?.prompt();
2359
2822
  return;
2360
2823
  }
2361
2824
  try {
2362
2825
  await this.onMessage(trimmed);
2363
2826
  } catch (err) {
2364
- console.log(chalk13.red(` \u2717 ${err.message}`));
2827
+ console.log(chalk14.red(` \u2717 ${err.message}`));
2365
2828
  }
2366
2829
  this.rl?.prompt();
2367
2830
  });
2368
2831
  this.rl.on("close", () => {
2369
2832
  this.saveHistory();
2370
- console.log(chalk13.dim("\n Session ended."));
2833
+ console.log(chalk14.dim("\n Session ended."));
2371
2834
  process.exit(0);
2372
2835
  });
2373
2836
  this.rl.on("SIGINT", () => {
@@ -2376,21 +2839,21 @@ var Repl = class {
2376
2839
  }
2377
2840
  showHelp() {
2378
2841
  console.log("");
2379
- console.log(chalk13.bold(" Available Commands:"));
2380
- console.log(chalk13.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2842
+ console.log(chalk14.bold(" Available Commands:"));
2843
+ console.log(chalk14.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2381
2844
  for (const [name, cmd] of this.commands) {
2382
- console.log(` ${chalk13.cyan("/" + name.padEnd(14))} ${chalk13.dim(cmd.description)}`);
2845
+ console.log(` ${chalk14.cyan("/" + name.padEnd(14))} ${chalk14.dim(cmd.description)}`);
2383
2846
  }
2384
- console.log(` ${chalk13.cyan("/help".padEnd(15))} ${chalk13.dim("Show this help message")}`);
2385
- console.log(` ${chalk13.cyan("/exit".padEnd(15))} ${chalk13.dim("Exit the shell")}`);
2386
- console.log(chalk13.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2387
- console.log(chalk13.dim(" Type anything else to chat with NEXUS"));
2847
+ console.log(` ${chalk14.cyan("/help".padEnd(15))} ${chalk14.dim("Show this help message")}`);
2848
+ console.log(` ${chalk14.cyan("/exit".padEnd(15))} ${chalk14.dim("Exit the shell")}`);
2849
+ console.log(chalk14.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2850
+ console.log(chalk14.dim(" Type anything else to chat with NEXUS"));
2388
2851
  console.log("");
2389
2852
  }
2390
2853
  write(text) {
2391
2854
  if (this.rl) {
2392
- readline.clearLine(process.stdout, 0);
2393
- readline.cursorTo(process.stdout, 0);
2855
+ readline2.clearLine(process.stdout, 0);
2856
+ readline2.cursorTo(process.stdout, 0);
2394
2857
  console.log(text);
2395
2858
  this.rl.prompt(true);
2396
2859
  } else {
@@ -2407,20 +2870,17 @@ var Repl = class {
2407
2870
  };
2408
2871
 
2409
2872
  // src/core/event-stream.ts
2410
- init_ssh();
2411
- init_secrets();
2412
- init_dlp();
2413
- import chalk14 from "chalk";
2873
+ import chalk15 from "chalk";
2414
2874
  var ROLE_COLORS = {
2415
- "Chief of Staff": chalk14.bold.cyan,
2416
- "VP Engineering": chalk14.bold.blue,
2417
- "VP Product": chalk14.bold.magenta,
2418
- "Senior Engineer": chalk14.bold.green,
2419
- "Engineer": chalk14.green,
2420
- "QA Lead": chalk14.bold.yellow,
2421
- "Security Engineer": chalk14.bold.red,
2422
- "DevOps Engineer": chalk14.bold.hex("#FF8C00"),
2423
- "default": chalk14.bold.white
2875
+ "Chief of Staff": chalk15.bold.cyan,
2876
+ "VP Engineering": chalk15.bold.blue,
2877
+ "VP Product": chalk15.bold.magenta,
2878
+ "Senior Engineer": chalk15.bold.green,
2879
+ "Engineer": chalk15.green,
2880
+ "QA Lead": chalk15.bold.yellow,
2881
+ "Security Engineer": chalk15.bold.red,
2882
+ "DevOps Engineer": chalk15.bold.hex("#FF8C00"),
2883
+ "default": chalk15.bold.white
2424
2884
  };
2425
2885
  function getColor(role) {
2426
2886
  if (!role) return ROLE_COLORS["default"];
@@ -2431,15 +2891,15 @@ function formatEvent(event) {
2431
2891
  const prefix = event.role ? color(` [${event.role}]`) : "";
2432
2892
  switch (event.type) {
2433
2893
  case "agent_thinking":
2434
- return `${prefix} ${chalk14.dim("thinking...")}`;
2894
+ return `${prefix} ${chalk15.dim("thinking...")}`;
2435
2895
  case "agent_response":
2436
2896
  return `${prefix} ${redact(event.content ?? "")}`;
2437
2897
  case "task_delegated":
2438
- return `${prefix} ${chalk14.dim("\u2192")} delegated to ${chalk14.bold(event.target ?? "agent")}`;
2898
+ return `${prefix} ${chalk15.dim("\u2192")} delegated to ${chalk15.bold(event.target ?? "agent")}`;
2439
2899
  case "agent_complete":
2440
- return `${prefix} ${chalk14.green("\u2713")} ${chalk14.dim("complete")}`;
2900
+ return `${prefix} ${chalk15.green("\u2713")} ${chalk15.dim("complete")}`;
2441
2901
  case "error":
2442
- return ` ${chalk14.red("\u2717")} ${redact(event.content ?? "Unknown error")}`;
2902
+ return ` ${chalk15.red("\u2717")} ${redact(event.content ?? "Unknown error")}`;
2443
2903
  case "heartbeat":
2444
2904
  return null;
2445
2905
  default:
@@ -2490,8 +2950,7 @@ var EventStream = class {
2490
2950
  this.pollInterval = setInterval(async () => {
2491
2951
  if (!this.active) return;
2492
2952
  try {
2493
- const { stdout, code } = await sshExec(
2494
- config.sshPort,
2953
+ const { stdout, code } = await dockerExec(
2495
2954
  `curl -sf -H 'Last-Event-ID: ${this.lastId}' http://localhost:4200/events?timeout=1 2>/dev/null || true`
2496
2955
  );
2497
2956
  if (code === 0 && stdout.trim()) {
@@ -2525,51 +2984,66 @@ var EventStream = class {
2525
2984
  };
2526
2985
 
2527
2986
  // src/commands/shell.ts
2528
- async function sendMessage2(sshPort, message) {
2529
- const payload = JSON.stringify({ message, source: "shell" });
2530
- const escaped = shellEscape(payload);
2531
- const { stdout, code } = await sshExec(
2532
- sshPort,
2533
- `curl -sf -X POST http://localhost:4200/message -H 'Content-Type: application/json' -d ${escaped}`
2534
- );
2535
- if (code !== 0) throw new Error("Server returned a non-zero exit code");
2987
+ async function httpPost(httpPort, path10, body) {
2988
+ const res = await fetch(`http://localhost:${httpPort}${path10}`, {
2989
+ method: "POST",
2990
+ headers: { "Content-Type": "application/json" },
2991
+ body: JSON.stringify(body),
2992
+ signal: AbortSignal.timeout(6e4)
2993
+ });
2994
+ if (!res.ok) throw new Error(`Server returned ${res.status}`);
2995
+ const text = await res.text();
2536
2996
  try {
2537
- const parsed = JSON.parse(stdout);
2538
- return parsed.response ?? parsed.message ?? stdout;
2997
+ const parsed = JSON.parse(text);
2998
+ return parsed.response ?? parsed.message ?? text;
2539
2999
  } catch {
2540
- return stdout;
3000
+ return text;
3001
+ }
3002
+ }
3003
+ async function httpGet(httpPort, path10) {
3004
+ try {
3005
+ const res = await fetch(`http://localhost:${httpPort}${path10}`, {
3006
+ signal: AbortSignal.timeout(1e4)
3007
+ });
3008
+ const text = await res.text();
3009
+ return { ok: res.ok, text };
3010
+ } catch {
3011
+ return { ok: false, text: "" };
2541
3012
  }
2542
3013
  }
3014
+ async function sendMessage2(httpPort, message) {
3015
+ return httpPost(httpPort, "/message", { message, source: "shell" });
3016
+ }
2543
3017
  function showShellBanner(health) {
2544
- const check = chalk15.green("\u2713");
2545
- const cross = chalk15.red("\u2717");
3018
+ const check = chalk16.green("\u2713");
3019
+ const cross = chalk16.red("\u2717");
2546
3020
  console.log("");
2547
- console.log(chalk15.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
2548
- console.log(chalk15.bold(" \u2551 ") + chalk15.bold.cyan("NEXUS Interactive Shell") + chalk15.bold(" \u2551"));
2549
- console.log(chalk15.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
2550
- console.log(chalk15.bold(" \u2551 ") + `${health.vmRunning ? check : cross} VM ${health.sshReady ? check : cross} SSH ${health.dockerReady ? check : cross} Docker ${health.serverHealthy ? check : cross} Engine`.padEnd(55) + chalk15.bold("\u2551"));
3021
+ console.log(chalk16.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
3022
+ console.log(chalk16.bold(" \u2551 ") + chalk16.bold.cyan("NEXUS Interactive Shell") + chalk16.bold(" \u2551"));
3023
+ console.log(chalk16.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
3024
+ console.log(chalk16.bold(" \u2551 ") + `${health.containerRunning ? check : cross} Container ${health.serverHealthy ? check : cross} Engine`.padEnd(55) + chalk16.bold("\u2551"));
2551
3025
  if (health.tunnelUrl) {
2552
- console.log(chalk15.bold(" \u2551 ") + chalk15.dim(`Tunnel: ${health.tunnelUrl}`.padEnd(55)) + chalk15.bold("\u2551"));
3026
+ console.log(chalk16.bold(" \u2551 ") + chalk16.dim(`Tunnel: ${health.tunnelUrl}`.padEnd(55)) + chalk16.bold("\u2551"));
2553
3027
  }
2554
- console.log(chalk15.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
2555
- console.log(chalk15.bold(" \u2551 ") + chalk15.dim("Type naturally to chat \xB7 /help for commands".padEnd(55)) + chalk15.bold("\u2551"));
2556
- console.log(chalk15.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
3028
+ console.log(chalk16.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
3029
+ console.log(chalk16.bold(" \u2551 ") + chalk16.dim("Type naturally to chat \xB7 /help for commands".padEnd(55)) + chalk16.bold("\u2551"));
3030
+ console.log(chalk16.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
2557
3031
  console.log("");
2558
3032
  }
2559
- async function getAgentList(sshPort) {
3033
+ async function getAgentList(httpPort) {
2560
3034
  try {
2561
- const { stdout, code } = await sshExec(sshPort, "curl -sf http://localhost:4200/agents");
2562
- if (code !== 0) return "Could not retrieve agent list";
2563
- const agents = JSON.parse(stdout);
2564
- if (!Array.isArray(agents)) return stdout;
3035
+ const { ok, text } = await httpGet(httpPort, "/agents");
3036
+ if (!ok) return " Could not retrieve agent list";
3037
+ const agents = JSON.parse(text);
3038
+ if (!Array.isArray(agents)) return text;
2565
3039
  const lines = [""];
2566
- lines.push(chalk15.bold(" Registered Agents:"));
2567
- lines.push(chalk15.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3040
+ lines.push(chalk16.bold(" Registered Agents:"));
3041
+ lines.push(chalk16.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2568
3042
  for (const agent of agents) {
2569
3043
  const name = agent.name ?? agent.id ?? "unknown";
2570
3044
  const role = agent.role ?? "";
2571
- const status = agent.status === "active" ? chalk15.green("\u25CF") : chalk15.dim("\u25CB");
2572
- lines.push(` ${status} ${chalk15.bold(name.padEnd(24))} ${chalk15.dim(role)}`);
3045
+ const status = agent.status === "active" ? chalk16.green("\u25CF") : chalk16.dim("\u25CB");
3046
+ lines.push(` ${status} ${chalk16.bold(name.padEnd(24))} ${chalk16.dim(role)}`);
2573
3047
  }
2574
3048
  lines.push("");
2575
3049
  return lines.join("\n");
@@ -2577,21 +3051,28 @@ async function getAgentList(sshPort) {
2577
3051
  return " Could not retrieve agent list";
2578
3052
  }
2579
3053
  }
2580
- async function getStatus(sshPort) {
3054
+ async function getStatus(httpPort) {
2581
3055
  try {
2582
- const vmRunning = isVmRunning();
2583
- const health = await checkHealth(sshPort, vmRunning);
2584
- const check = chalk15.green("\u2713");
2585
- const cross = chalk15.red("\u2717");
3056
+ const containerRunning = await isNexusRunning();
3057
+ let serverHealthy = false;
3058
+ let tunnelUrl = null;
3059
+ if (containerRunning) {
3060
+ const { ok, text } = await httpGet(httpPort, "/health");
3061
+ serverHealthy = ok && (text.includes("ok") || true);
3062
+ const tunnelRes = await httpGet(httpPort, "/tunnel-url");
3063
+ if (tunnelRes.ok && tunnelRes.text.includes("https://")) {
3064
+ tunnelUrl = tunnelRes.text.trim();
3065
+ }
3066
+ }
3067
+ const check = chalk16.green("\u2713");
3068
+ const cross = chalk16.red("\u2717");
2586
3069
  const lines = [""];
2587
- lines.push(chalk15.bold(" System Status:"));
2588
- lines.push(chalk15.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2589
- lines.push(` ${health.vmRunning ? check : cross} Virtual Machine`);
2590
- lines.push(` ${health.sshReady ? check : cross} SSH Connection`);
2591
- lines.push(` ${health.dockerReady ? check : cross} Docker Engine`);
2592
- lines.push(` ${health.serverHealthy ? check : cross} NEXUS Engine`);
2593
- if (health.tunnelUrl) {
2594
- lines.push(` ${check} Tunnel: ${health.tunnelUrl}`);
3070
+ lines.push(chalk16.bold(" System Status:"));
3071
+ lines.push(chalk16.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3072
+ lines.push(` ${containerRunning ? check : cross} NEXUS Container`);
3073
+ lines.push(` ${serverHealthy ? check : cross} NEXUS Engine`);
3074
+ if (tunnelUrl) {
3075
+ lines.push(` ${check} Tunnel: ${tunnelUrl}`);
2595
3076
  } else {
2596
3077
  lines.push(` ${cross} Tunnel: not active`);
2597
3078
  }
@@ -2601,23 +3082,23 @@ async function getStatus(sshPort) {
2601
3082
  return " Could not check status";
2602
3083
  }
2603
3084
  }
2604
- async function getCost(sshPort) {
3085
+ async function getCost(httpPort) {
2605
3086
  try {
2606
- const { stdout, code } = await sshExec(sshPort, "curl -sf http://localhost:4200/cost");
2607
- if (code !== 0) return " Could not retrieve cost data";
2608
- const data = JSON.parse(stdout);
3087
+ const { ok, text } = await httpGet(httpPort, "/cost");
3088
+ if (!ok) return " Could not retrieve cost data";
3089
+ const data = JSON.parse(text);
2609
3090
  const lines = [""];
2610
- lines.push(chalk15.bold(" Token Costs:"));
2611
- lines.push(chalk15.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3091
+ lines.push(chalk16.bold(" Token Costs:"));
3092
+ lines.push(chalk16.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2612
3093
  if (data.total !== void 0) {
2613
- lines.push(` Total: ${chalk15.bold.green("$" + Number(data.total).toFixed(4))}`);
3094
+ lines.push(` Total: ${chalk16.bold.green("$" + Number(data.total).toFixed(4))}`);
2614
3095
  }
2615
3096
  if (data.today !== void 0) {
2616
- lines.push(` Today: ${chalk15.bold("$" + Number(data.today).toFixed(4))}`);
3097
+ lines.push(` Today: ${chalk16.bold("$" + Number(data.today).toFixed(4))}`);
2617
3098
  }
2618
3099
  if (data.by_agent && typeof data.by_agent === "object") {
2619
3100
  lines.push("");
2620
- lines.push(chalk15.dim(" By Agent:"));
3101
+ lines.push(chalk16.dim(" By Agent:"));
2621
3102
  for (const [agent, cost] of Object.entries(data.by_agent)) {
2622
3103
  lines.push(` ${agent.padEnd(20)} $${Number(cost).toFixed(4)}`);
2623
3104
  }
@@ -2628,27 +3109,32 @@ async function getCost(sshPort) {
2628
3109
  return " Could not retrieve cost data";
2629
3110
  }
2630
3111
  }
2631
- var shellCommand2 = new Command13("shell").description("Launch the interactive NEXUS shell").action(async () => {
3112
+ var shellCommand2 = new Command14("shell").description("Launch the interactive NEXUS shell").action(async () => {
2632
3113
  try {
2633
3114
  const config = loadConfig();
2634
3115
  if (!config) {
2635
3116
  log.error("No NEXUS configuration found. Run: buildwithnexus init");
2636
3117
  process.exit(1);
2637
3118
  }
2638
- if (!isVmRunning()) {
2639
- log.error("VM is not running. Start it with: buildwithnexus start");
3119
+ if (!await isNexusRunning()) {
3120
+ log.error("NEXUS is not running. Start it with: buildwithnexus start");
2640
3121
  process.exit(1);
2641
3122
  }
2642
3123
  const spinner = createSpinner("Connecting to NEXUS...");
2643
3124
  spinner.start();
2644
- const health = await checkHealth(config.sshPort, true);
2645
- if (!health.serverHealthy) {
3125
+ const { ok: serverHealthy, text: healthText } = await httpGet(config.httpPort, "/health");
3126
+ if (!serverHealthy) {
2646
3127
  fail(spinner, "NEXUS engine is not responding");
2647
3128
  log.warn("Check status: buildwithnexus status");
2648
3129
  process.exit(1);
2649
3130
  }
2650
3131
  succeed(spinner, "Connected to NEXUS engine");
2651
- showShellBanner(health);
3132
+ let tunnelUrl = null;
3133
+ const tunnelRes = await httpGet(config.httpPort, "/tunnel-url");
3134
+ if (tunnelRes.ok && tunnelRes.text.includes("https://")) {
3135
+ tunnelUrl = tunnelRes.text.trim();
3136
+ }
3137
+ showShellBanner({ containerRunning: true, serverHealthy: true, tunnelUrl });
2652
3138
  const eventStream = new EventStream((event) => {
2653
3139
  const formatted = formatEvent(event);
2654
3140
  if (formatted) repl.write(formatted);
@@ -2658,14 +3144,14 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2658
3144
  const thinkingSpinner = createSpinner("Processing...");
2659
3145
  thinkingSpinner.start();
2660
3146
  try {
2661
- const response = await sendMessage2(config.sshPort, text);
3147
+ const response = await sendMessage2(config.httpPort, text);
2662
3148
  thinkingSpinner.stop();
2663
3149
  thinkingSpinner.clear();
2664
3150
  console.log("");
2665
- console.log(chalk15.bold.cyan(" Chief of Staff:"));
3151
+ console.log(chalk16.bold.cyan(" Chief of Staff:"));
2666
3152
  const lines = redact(response).split("\n");
2667
3153
  for (const line of lines) {
2668
- console.log(chalk15.white(" " + line));
3154
+ console.log(chalk16.white(" " + line));
2669
3155
  }
2670
3156
  console.log("");
2671
3157
  } catch (err) {
@@ -2679,14 +3165,14 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2679
3165
  description: "Brainstorm with the full NEXUS org (led by Chief of Staff)",
2680
3166
  handler: async () => {
2681
3167
  console.log("");
2682
- console.log(chalk15.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
2683
- console.log(chalk15.bold(" \u2551 ") + chalk15.bold.cyan("NEXUS Brainstorm Session") + chalk15.bold(" \u2551"));
2684
- console.log(chalk15.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
2685
- console.log(chalk15.bold(" \u2551 ") + chalk15.dim("The Chief of Staff will facilitate a discussion with".padEnd(55)) + chalk15.bold("\u2551"));
2686
- console.log(chalk15.bold(" \u2551 ") + chalk15.dim("the full NEXUS org to refine your idea. When ready,".padEnd(55)) + chalk15.bold("\u2551"));
2687
- console.log(chalk15.bold(" \u2551 ") + chalk15.dim("NEXUS will draft an execution plan for your review.".padEnd(55)) + chalk15.bold("\u2551"));
2688
- console.log(chalk15.bold(" \u2551 ") + chalk15.dim("Type 'exit' to end brainstorm. Type 'plan' to hand off.".padEnd(55)) + chalk15.bold("\u2551"));
2689
- console.log(chalk15.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
3168
+ console.log(chalk16.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
3169
+ console.log(chalk16.bold(" \u2551 ") + chalk16.bold.cyan("NEXUS Brainstorm Session") + chalk16.bold(" \u2551"));
3170
+ console.log(chalk16.bold(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
3171
+ console.log(chalk16.bold(" \u2551 ") + chalk16.dim("The Chief of Staff will facilitate a discussion with".padEnd(55)) + chalk16.bold("\u2551"));
3172
+ console.log(chalk16.bold(" \u2551 ") + chalk16.dim("the full NEXUS org to refine your idea. When ready,".padEnd(55)) + chalk16.bold("\u2551"));
3173
+ console.log(chalk16.bold(" \u2551 ") + chalk16.dim("NEXUS will draft an execution plan for your review.".padEnd(55)) + chalk16.bold("\u2551"));
3174
+ console.log(chalk16.bold(" \u2551 ") + chalk16.dim("Type 'exit' to end brainstorm. Type 'plan' to hand off.".padEnd(55)) + chalk16.bold("\u2551"));
3175
+ console.log(chalk16.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
2690
3176
  console.log("");
2691
3177
  const { input: input5 } = await import("@inquirer/prompts");
2692
3178
  const idea = await input5({ message: "What would you like to brainstorm?" });
@@ -2695,16 +3181,16 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2695
3181
  while (true) {
2696
3182
  const brainstormSpinner = createSpinner("NEXUS team is discussing...");
2697
3183
  brainstormSpinner.start();
2698
- const response = await sendMessage2(config.sshPort, currentMessage);
3184
+ const response = await sendMessage2(config.httpPort, currentMessage);
2699
3185
  brainstormSpinner.stop();
2700
3186
  brainstormSpinner.clear();
2701
3187
  console.log("");
2702
- console.log(chalk15.bold.cyan(" Chief of Staff:"));
3188
+ console.log(chalk16.bold.cyan(" Chief of Staff:"));
2703
3189
  for (const line of redact(response).split("\n")) {
2704
- console.log(chalk15.white(" " + line));
3190
+ console.log(chalk16.white(" " + line));
2705
3191
  }
2706
3192
  console.log("");
2707
- const followUp = await input5({ message: chalk15.bold("You:") });
3193
+ const followUp = await input5({ message: chalk16.bold("You:") });
2708
3194
  const trimmed = followUp.trim().toLowerCase();
2709
3195
  if (!trimmed || trimmed === "exit" || trimmed === "quit") {
2710
3196
  console.log("");
@@ -2714,13 +3200,13 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2714
3200
  if (trimmed === "plan" || trimmed === "execute" || trimmed === "go") {
2715
3201
  const planSpinner = createSpinner("Handing off to NEXUS for execution planning...");
2716
3202
  planSpinner.start();
2717
- const planResponse = await sendMessage2(config.sshPort, `[BRAINSTORM\u2192PLAN] The CEO approves this direction from the brainstorm session. Draft a detailed execution plan with task assignments, timelines, and dependencies. Previous discussion context: ${idea}`);
3203
+ const planResponse = await sendMessage2(config.httpPort, `[BRAINSTORM\u2192PLAN] The CEO approves this direction from the brainstorm session. Draft a detailed execution plan with task assignments, timelines, and dependencies. Previous discussion context: ${idea}`);
2718
3204
  planSpinner.stop();
2719
3205
  planSpinner.clear();
2720
3206
  console.log("");
2721
- console.log(chalk15.bold.green(" Execution Plan:"));
3207
+ console.log(chalk16.bold.green(" Execution Plan:"));
2722
3208
  for (const line of redact(planResponse).split("\n")) {
2723
- console.log(chalk15.white(" " + line));
3209
+ console.log(chalk16.white(" " + line));
2724
3210
  }
2725
3211
  console.log("");
2726
3212
  log.success("Plan drafted. Use the shell to refine or approve.");
@@ -2734,7 +3220,7 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2734
3220
  name: "status",
2735
3221
  description: "Show system health status",
2736
3222
  handler: async () => {
2737
- const result = await getStatus(config.sshPort);
3223
+ const result = await getStatus(config.httpPort);
2738
3224
  console.log(result);
2739
3225
  }
2740
3226
  });
@@ -2742,7 +3228,7 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2742
3228
  name: "agents",
2743
3229
  description: "List registered agents",
2744
3230
  handler: async () => {
2745
- const result = await getAgentList(config.sshPort);
3231
+ const result = await getAgentList(config.httpPort);
2746
3232
  console.log(result);
2747
3233
  }
2748
3234
  });
@@ -2750,31 +3236,39 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2750
3236
  name: "cost",
2751
3237
  description: "Show token usage and costs",
2752
3238
  handler: async () => {
2753
- const result = await getCost(config.sshPort);
3239
+ const result = await getCost(config.httpPort);
2754
3240
  console.log(result);
2755
3241
  }
2756
3242
  });
2757
3243
  repl.registerCommand({
2758
3244
  name: "logs",
2759
- description: "Show recent server logs",
3245
+ description: "Show recent container logs",
2760
3246
  handler: async () => {
2761
- const { stdout } = await sshExec(config.sshPort, "tail -30 /home/nexus/.nexus/logs/server.log 2>/dev/null");
3247
+ const { execa: execa6 } = await import("execa");
2762
3248
  console.log("");
2763
- console.log(chalk15.bold(" Recent Logs:"));
2764
- console.log(chalk15.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2765
- for (const line of redact(stdout).split("\n")) {
2766
- console.log(chalk15.dim(" " + line));
3249
+ console.log(chalk16.bold(" Recent Logs:"));
3250
+ console.log(chalk16.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3251
+ try {
3252
+ const { stdout } = await execa6("docker", ["logs", "--tail", "30", "nexus"]);
3253
+ for (const line of redact(stdout).split("\n")) {
3254
+ console.log(chalk16.dim(" " + line));
3255
+ }
3256
+ } catch {
3257
+ console.log(chalk16.dim(" Could not retrieve logs"));
2767
3258
  }
2768
3259
  console.log("");
2769
3260
  }
2770
3261
  });
2771
3262
  repl.registerCommand({
2772
- name: "ssh",
2773
- description: "Drop into the VM for debugging/inspection",
3263
+ name: "exec",
3264
+ description: "Drop into the container shell for debugging/inspection",
2774
3265
  handler: async () => {
2775
- const { openInteractiveSsh: openInteractiveSsh2 } = await Promise.resolve().then(() => (init_ssh(), ssh_exports));
3266
+ const { execa: execa6 } = await import("execa");
2776
3267
  eventStream.stop();
2777
- await openInteractiveSsh2(config.sshPort);
3268
+ try {
3269
+ await execa6("docker", ["exec", "-it", "nexus", "/bin/sh"], { stdio: "inherit" });
3270
+ } catch {
3271
+ }
2778
3272
  eventStream.start();
2779
3273
  }
2780
3274
  });
@@ -2783,24 +3277,24 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2783
3277
  description: "Display the NEXUS organizational hierarchy",
2784
3278
  handler: async () => {
2785
3279
  console.log("");
2786
- console.log(chalk15.bold(" NEXUS Organizational Hierarchy"));
2787
- console.log(chalk15.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2788
- console.log(` ${chalk15.bold.white("You")} ${chalk15.dim("(CEO)")}`);
2789
- console.log(` \u2514\u2500\u2500 ${chalk15.bold.cyan("Chief of Staff")} ${chalk15.dim("\u2014 orchestrates all work, your direct interface")}`);
2790
- console.log(` \u251C\u2500\u2500 ${chalk15.bold.blue("VP Engineering")} ${chalk15.dim("\u2014 owns technical execution")}`);
2791
- console.log(` \u2502 \u251C\u2500\u2500 ${chalk15.green("Senior Engineer")} ${chalk15.dim("\xD7 8 \u2014 implementation, refactoring")}`);
2792
- console.log(` \u2502 \u251C\u2500\u2500 ${chalk15.green("Engineer")} ${chalk15.dim("\xD7 12 \u2014 feature work, bug fixes")}`);
2793
- console.log(` \u2502 \u2514\u2500\u2500 ${chalk15.hex("#FF8C00")("DevOps Engineer")} ${chalk15.dim("\xD7 4 \u2014 CI/CD, Docker, infra")}`);
2794
- console.log(` \u251C\u2500\u2500 ${chalk15.bold.magenta("VP Product")} ${chalk15.dim("\u2014 owns roadmap and priorities")}`);
2795
- console.log(` \u2502 \u251C\u2500\u2500 ${chalk15.magenta("Product Manager")} ${chalk15.dim("\xD7 3 \u2014 specs, requirements")}`);
2796
- console.log(` \u2502 \u2514\u2500\u2500 ${chalk15.magenta("Designer")} ${chalk15.dim("\xD7 2 \u2014 UI/UX, prototyping")}`);
2797
- console.log(` \u251C\u2500\u2500 ${chalk15.bold.yellow("QA Lead")} ${chalk15.dim("\u2014 owns quality assurance")}`);
2798
- console.log(` \u2502 \u2514\u2500\u2500 ${chalk15.yellow("QA Engineer")} ${chalk15.dim("\xD7 6 \u2014 testing, coverage, validation")}`);
2799
- console.log(` \u251C\u2500\u2500 ${chalk15.bold.red("Security Engineer")} ${chalk15.dim("\xD7 4 \u2014 auth, scanning, compliance")}`);
2800
- console.log(` \u2514\u2500\u2500 ${chalk15.bold.white("Knowledge Manager")} ${chalk15.dim("\u2014 RAG, documentation, learning")}`);
3280
+ console.log(chalk16.bold(" NEXUS Organizational Hierarchy"));
3281
+ console.log(chalk16.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3282
+ console.log(` ${chalk16.bold.white("You")} ${chalk16.dim("(CEO)")}`);
3283
+ console.log(` \u2514\u2500\u2500 ${chalk16.bold.cyan("Chief of Staff")} ${chalk16.dim("\u2014 orchestrates all work, your direct interface")}`);
3284
+ console.log(` \u251C\u2500\u2500 ${chalk16.bold.blue("VP Engineering")} ${chalk16.dim("\u2014 owns technical execution")}`);
3285
+ console.log(` \u2502 \u251C\u2500\u2500 ${chalk16.green("Senior Engineer")} ${chalk16.dim("\xD7 8 \u2014 implementation, refactoring")}`);
3286
+ console.log(` \u2502 \u251C\u2500\u2500 ${chalk16.green("Engineer")} ${chalk16.dim("\xD7 12 \u2014 feature work, bug fixes")}`);
3287
+ console.log(` \u2502 \u2514\u2500\u2500 ${chalk16.hex("#FF8C00")("DevOps Engineer")} ${chalk16.dim("\xD7 4 \u2014 CI/CD, Docker, infra")}`);
3288
+ console.log(` \u251C\u2500\u2500 ${chalk16.bold.magenta("VP Product")} ${chalk16.dim("\u2014 owns roadmap and priorities")}`);
3289
+ console.log(` \u2502 \u251C\u2500\u2500 ${chalk16.magenta("Product Manager")} ${chalk16.dim("\xD7 3 \u2014 specs, requirements")}`);
3290
+ console.log(` \u2502 \u2514\u2500\u2500 ${chalk16.magenta("Designer")} ${chalk16.dim("\xD7 2 \u2014 UI/UX, prototyping")}`);
3291
+ console.log(` \u251C\u2500\u2500 ${chalk16.bold.yellow("QA Lead")} ${chalk16.dim("\u2014 owns quality assurance")}`);
3292
+ console.log(` \u2502 \u2514\u2500\u2500 ${chalk16.yellow("QA Engineer")} ${chalk16.dim("\xD7 6 \u2014 testing, coverage, validation")}`);
3293
+ console.log(` \u251C\u2500\u2500 ${chalk16.bold.red("Security Engineer")} ${chalk16.dim("\xD7 4 \u2014 auth, scanning, compliance")}`);
3294
+ console.log(` \u2514\u2500\u2500 ${chalk16.bold.white("Knowledge Manager")} ${chalk16.dim("\u2014 RAG, documentation, learning")}`);
2801
3295
  console.log("");
2802
- console.log(chalk15.dim(" 56 agents total \xB7 Self-learning ML pipeline"));
2803
- console.log(chalk15.dim(" Full details: https://buildwithnexus.dev/overview"));
3296
+ console.log(chalk16.dim(" 56 agents total \xB7 Self-learning ML pipeline"));
3297
+ console.log(chalk16.dim(" Full details: https://buildwithnexus.dev/overview"));
2804
3298
  console.log("");
2805
3299
  }
2806
3300
  });
@@ -2875,11 +3369,11 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2875
3369
  {
2876
3370
  title: "Monitoring & Control",
2877
3371
  content: [
2878
- " /status \u2014 System health (VM, SSH, Docker, Engine)",
3372
+ " /status \u2014 System health (Container, Engine)",
2879
3373
  " /agents \u2014 List all 56 registered agents",
2880
3374
  " /cost \u2014 Token usage and spend tracking",
2881
- " /logs \u2014 Server logs for debugging",
2882
- " /ssh \u2014 Drop into the VM directly",
3375
+ " /logs \u2014 Container logs for debugging",
3376
+ " /exec \u2014 Drop into the container shell",
2883
3377
  " /security \u2014 View the security posture",
2884
3378
  "",
2885
3379
  "You're in control. NEXUS executes."
@@ -2889,14 +3383,14 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2889
3383
  for (let i = 0; i < steps.length; i++) {
2890
3384
  const step = steps[i];
2891
3385
  console.log("");
2892
- console.log(chalk15.bold(` \u2500\u2500 ${chalk15.cyan(`Step ${i + 1}/${steps.length}`)} \u2500\u2500 ${step.title} \u2500\u2500`));
3386
+ console.log(chalk16.bold(` \u2500\u2500 ${chalk16.cyan(`Step ${i + 1}/${steps.length}`)} \u2500\u2500 ${step.title} \u2500\u2500`));
2893
3387
  console.log("");
2894
3388
  for (const line of step.content) {
2895
- console.log(chalk15.white(" " + line));
3389
+ console.log(chalk16.white(" " + line));
2896
3390
  }
2897
3391
  console.log("");
2898
3392
  if (i < steps.length - 1) {
2899
- const next = await input5({ message: chalk15.dim("Press Enter to continue (or 'skip' to exit)") });
3393
+ const next = await input5({ message: chalk16.dim("Press Enter to continue (or 'skip' to exit)") });
2900
3394
  if (next.trim().toLowerCase() === "skip") {
2901
3395
  log.success("Tutorial ended. Type /help to see all commands.");
2902
3396
  return;
@@ -2928,15 +3422,16 @@ var shellCommand2 = new Command13("shell").description("Launch the interactive N
2928
3422
  // src/cli.ts
2929
3423
  function getVersionStatic() {
2930
3424
  try {
2931
- const __dirname = dirname2(fileURLToPath4(import.meta.url));
2932
- const packagePath = join2(__dirname, "..", "package.json");
3425
+ const __dirname2 = dirname2(fileURLToPath4(import.meta.url));
3426
+ const packagePath = join2(__dirname2, "..", "package.json");
2933
3427
  const packageJson = JSON.parse(readFileSync2(packagePath, "utf-8"));
2934
3428
  return packageJson.version;
2935
3429
  } catch {
2936
- return "0.5.16";
3430
+ return true ? "0.6.0" : "0.0.0-unknown";
2937
3431
  }
2938
3432
  }
2939
- var cli = new Command14().name("buildwithnexus").description("Auto-scaffold and launch a fully autonomous NEXUS runtime").version(getVersionStatic());
3433
+ var cli = new Command15().name("buildwithnexus").description("Auto-scaffold and launch a fully autonomous NEXUS runtime").version(getVersionStatic());
3434
+ cli.addCommand(installCommand);
2940
3435
  cli.addCommand(initCommand);
2941
3436
  cli.addCommand(startCommand);
2942
3437
  cli.addCommand(stopCommand);
@@ -2950,33 +3445,23 @@ cli.addCommand(sshCommand);
2950
3445
  cli.addCommand(brainstormCommand);
2951
3446
  cli.addCommand(ninetyNineCommand);
2952
3447
  cli.addCommand(shellCommand2);
2953
- cli.action(async () => {
2954
- try {
2955
- const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
2956
- const { isVmRunning: isVmRunning2 } = await Promise.resolve().then(() => (init_qemu(), qemu_exports));
2957
- const config = loadConfig2();
2958
- if (config && isVmRunning2()) {
2959
- await shellCommand2.parseAsync([], { from: "user" });
2960
- return;
2961
- }
2962
- } catch {
2963
- }
3448
+ cli.action(() => {
2964
3449
  cli.help();
2965
3450
  });
2966
3451
 
2967
3452
  // src/core/update-notifier.ts
2968
- import fs11 from "fs";
2969
- import path12 from "path";
2970
- import os3 from "os";
3453
+ import fs8 from "fs";
3454
+ import path9 from "path";
3455
+ import os2 from "os";
2971
3456
  import https from "https";
2972
- import chalk16 from "chalk";
3457
+ import chalk17 from "chalk";
2973
3458
  var PACKAGE_NAME = "buildwithnexus";
2974
3459
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
2975
- var STATE_DIR = path12.join(os3.homedir(), ".buildwithnexus");
2976
- var STATE_FILE = path12.join(STATE_DIR, ".update-check.json");
3460
+ var STATE_DIR = path9.join(os2.homedir(), ".buildwithnexus");
3461
+ var STATE_FILE = path9.join(STATE_DIR, ".update-check.json");
2977
3462
  function readState() {
2978
3463
  try {
2979
- const raw = fs11.readFileSync(STATE_FILE, "utf-8");
3464
+ const raw = fs8.readFileSync(STATE_FILE, "utf-8");
2980
3465
  return JSON.parse(raw);
2981
3466
  } catch {
2982
3467
  return { lastCheck: 0, latestVersion: null };
@@ -2984,8 +3469,8 @@ function readState() {
2984
3469
  }
2985
3470
  function writeState(state) {
2986
3471
  try {
2987
- fs11.mkdirSync(STATE_DIR, { recursive: true, mode: 448 });
2988
- fs11.writeFileSync(STATE_FILE, JSON.stringify(state), { mode: 384 });
3472
+ fs8.mkdirSync(STATE_DIR, { recursive: true, mode: 448 });
3473
+ fs8.writeFileSync(STATE_FILE, JSON.stringify(state), { mode: 384 });
2989
3474
  } catch {
2990
3475
  }
2991
3476
  }
@@ -3050,13 +3535,53 @@ async function checkForUpdates(currentVersion) {
3050
3535
  function printUpdateBanner(current, latest) {
3051
3536
  const msg = [
3052
3537
  "",
3053
- chalk16.yellow(` Update available: ${current} \u2192 ${latest}`),
3054
- chalk16.cyan(` Run: npm update -g buildwithnexus`),
3538
+ chalk17.yellow(` Update available: ${current} \u2192 ${latest}`),
3539
+ chalk17.cyan(` Run: npm update -g buildwithnexus`),
3055
3540
  ""
3056
3541
  ].join("\n");
3057
3542
  process.stderr.write(msg + "\n");
3058
3543
  }
3059
3544
 
3060
3545
  // src/bin.ts
3061
- checkForUpdates(cli.version() ?? "0.0.0");
3062
- cli.parse(process.argv);
3546
+ import dotenv from "dotenv";
3547
+ dotenv.config({ path: ".env.local" });
3548
+ var version = true ? "0.6.0" : "0.5.17";
3549
+ checkForUpdates(version);
3550
+ program.name("buildwithnexus").description("Deep Agents - AI-Powered Task Execution").version(version);
3551
+ program.command("da-init").description("Initialize Deep Agents (set up API keys and .env.local)").action(deepAgentsInitCommand);
3552
+ program.command("run <task>").description("Run a task with Deep Agents").option("-a, --agent <name>", "Agent role (engineer, researcher, etc)", "engineer").option("-g, --goal <goal>", "Agent goal").option("-m, --model <model>", "LLM model", "claude-sonnet-4-20250514").action(runCommand);
3553
+ program.command("dashboard").description("Start the Deep Agents dashboard server").option("-p, --port <port>", "Dashboard port", "4201").action(dashboardCommand);
3554
+ program.command("da-status").description("Check Deep Agents backend status").action(async () => {
3555
+ const backendUrl = process.env.BACKEND_URL || "http://localhost:4200";
3556
+ try {
3557
+ const response = await fetch(`${backendUrl}/health`);
3558
+ if (response.ok) {
3559
+ console.log("Backend: Running");
3560
+ console.log(` URL: ${backendUrl}`);
3561
+ } else {
3562
+ console.log("Backend: Not responding (status " + response.status + ")");
3563
+ }
3564
+ } catch {
3565
+ console.log("Backend: Not accessible");
3566
+ console.log(` URL: ${backendUrl}`);
3567
+ console.log("\n Start backend with:");
3568
+ console.log(" cd ~/Projects/nexus && python -m src.deep_agents_server");
3569
+ }
3570
+ });
3571
+ for (const cmd of cli.commands) {
3572
+ const name = cmd.name();
3573
+ if (!["da-init", "run", "dashboard", "da-status"].includes(name)) {
3574
+ program.addCommand(cmd);
3575
+ }
3576
+ }
3577
+ if (!process.argv.slice(2).length) {
3578
+ interactiveMode().catch((err) => {
3579
+ console.error(err);
3580
+ process.exit(1);
3581
+ });
3582
+ } else {
3583
+ program.parse();
3584
+ }
3585
+ if (!process.argv.slice(2).length) {
3586
+ program.outputHelp();
3587
+ }