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