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