@novastorm-ai/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/nova.js +12 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-FYSTZ6K6.js +231 -0
- package/dist/chunk-NFNZMCLQ.js +1753 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +21 -0
- package/dist/package-3YCVE5UE.js +43 -0
- package/dist/setup-3KREUXRO.js +7 -0
- package/package.json +37 -0
|
@@ -0,0 +1,1753 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigReader,
|
|
3
|
+
runSetup
|
|
4
|
+
} from "./chunk-FYSTZ6K6.js";
|
|
5
|
+
import {
|
|
6
|
+
__require
|
|
7
|
+
} from "./chunk-3RG5ZIWI.js";
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
|
|
15
|
+
// src/commands/start.ts
|
|
16
|
+
import { exec } from "child_process";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
import chalk6 from "chalk";
|
|
19
|
+
import ora2 from "ora";
|
|
20
|
+
import { resolve as resolve2 } from "path";
|
|
21
|
+
import {
|
|
22
|
+
NovaEventBus,
|
|
23
|
+
NovaDir,
|
|
24
|
+
ProjectIndexer,
|
|
25
|
+
Brain,
|
|
26
|
+
ProviderFactory,
|
|
27
|
+
ExecutorPool,
|
|
28
|
+
Lane1Executor,
|
|
29
|
+
Lane2Executor,
|
|
30
|
+
GitManager,
|
|
31
|
+
AgentPromptLoader,
|
|
32
|
+
PathGuard,
|
|
33
|
+
ManifestStore,
|
|
34
|
+
EnvDetector
|
|
35
|
+
} from "@novastorm-ai/core";
|
|
36
|
+
import {
|
|
37
|
+
DevServerRunner,
|
|
38
|
+
ProxyServer,
|
|
39
|
+
WebSocketServer
|
|
40
|
+
} from "@novastorm-ai/proxy";
|
|
41
|
+
import { LicenseChecker, Telemetry, NudgeRenderer } from "@novastorm-ai/licensing";
|
|
42
|
+
|
|
43
|
+
// src/logger.ts
|
|
44
|
+
import chalk from "chalk";
|
|
45
|
+
var PREFIX = "[Nova]";
|
|
46
|
+
var NovaLogger = class {
|
|
47
|
+
logObservation(observation) {
|
|
48
|
+
const action = observation.transcript ?? "click";
|
|
49
|
+
const screenshotSize = observation.screenshot?.length ?? 0;
|
|
50
|
+
const url = observation.currentUrl || "(unknown)";
|
|
51
|
+
console.log(
|
|
52
|
+
chalk.yellow(`${PREFIX} \u{1F4E1} Observation: "${action}" at ${url}`)
|
|
53
|
+
);
|
|
54
|
+
console.log(
|
|
55
|
+
chalk.dim(`${PREFIX} Screenshot: ${screenshotSize} bytes, DOM: ${observation.domSnapshot ? "yes" : "no"}, Errors: ${observation.consoleErrors?.length ?? 0}`)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
logAnalyzing(transcript) {
|
|
59
|
+
const suffix = transcript ? ` ${transcript}` : "";
|
|
60
|
+
console.log(chalk.yellow(`${PREFIX} \u{1F9E0} Analyzing...${suffix}`));
|
|
61
|
+
}
|
|
62
|
+
logTasks(tasks) {
|
|
63
|
+
console.log(chalk.green(`${PREFIX} \u2705 ${tasks.length} task(s) detected`));
|
|
64
|
+
for (const task of tasks) {
|
|
65
|
+
console.log(chalk.dim(` \u2192 ${task.description} (Lane ${task.lane})`));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
logTaskStarted(task) {
|
|
69
|
+
console.log(
|
|
70
|
+
chalk.cyan(`${PREFIX} \u26A1 Executing: ${task.description} (Lane ${task.lane})`)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
logTaskCompleted(task) {
|
|
74
|
+
console.log(
|
|
75
|
+
chalk.green(`${PREFIX} \u2705 Done: ${task.description} \u2014 ${task.commitHash ?? "no hash"}`)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
logTaskFailed(task) {
|
|
79
|
+
console.log(
|
|
80
|
+
chalk.red(`${PREFIX} \u274C Failed: ${task.description} \u2014 ${task.error ?? "unknown error"}`)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
logFileChanged(filePath) {
|
|
84
|
+
console.log(chalk.dim(`${PREFIX} \u{1F4DD} Modified: ${filePath}`));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/scaffold.ts
|
|
89
|
+
import chalk2 from "chalk";
|
|
90
|
+
import ora from "ora";
|
|
91
|
+
import { readFile, writeFile } from "fs/promises";
|
|
92
|
+
import { join } from "path";
|
|
93
|
+
import { select, input } from "@inquirer/prompts";
|
|
94
|
+
import { Separator } from "@inquirer/prompts";
|
|
95
|
+
import { ProjectScaffolder, SCAFFOLD_PRESETS } from "@novastorm-ai/core";
|
|
96
|
+
async function promptAndScaffold(projectPath) {
|
|
97
|
+
console.log(
|
|
98
|
+
chalk2.yellow("\nNo project detected.") + " What would you like to create?\n"
|
|
99
|
+
);
|
|
100
|
+
let selection;
|
|
101
|
+
try {
|
|
102
|
+
selection = await select({
|
|
103
|
+
message: "Select a project template:",
|
|
104
|
+
choices: [
|
|
105
|
+
...SCAFFOLD_PRESETS.map((p) => ({ name: p.label, value: p.label })),
|
|
106
|
+
new Separator(),
|
|
107
|
+
{ name: "Other (type your own command)", value: "__other__" },
|
|
108
|
+
{ name: "Empty (I'll set up manually)", value: "__empty__" }
|
|
109
|
+
]
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
console.log("\nCancelled.");
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
if (selection === "__empty__") {
|
|
116
|
+
const scaffolder = new ProjectScaffolder();
|
|
117
|
+
await scaffolder.scaffoldEmpty(projectPath);
|
|
118
|
+
console.log(
|
|
119
|
+
chalk2.green("\nCreated nova.toml.") + " Configure your project and run " + chalk2.cyan("nova") + " again."
|
|
120
|
+
);
|
|
121
|
+
return { scaffolded: false };
|
|
122
|
+
}
|
|
123
|
+
let command;
|
|
124
|
+
let needsInstall = false;
|
|
125
|
+
let label;
|
|
126
|
+
let frontend;
|
|
127
|
+
let backends;
|
|
128
|
+
if (selection === "__other__") {
|
|
129
|
+
let description;
|
|
130
|
+
try {
|
|
131
|
+
description = await input({
|
|
132
|
+
message: 'Describe the project (e.g. "React + Tailwind", "Django REST API", "Go fiber server"):'
|
|
133
|
+
});
|
|
134
|
+
} catch {
|
|
135
|
+
console.log("\nCancelled.");
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
if (!description.trim()) {
|
|
139
|
+
console.log(chalk2.red("No description provided. Exiting."));
|
|
140
|
+
return { scaffolded: false };
|
|
141
|
+
}
|
|
142
|
+
const mapped = mapDescriptionToCommand(description.trim());
|
|
143
|
+
command = mapped.command;
|
|
144
|
+
needsInstall = mapped.needsInstall;
|
|
145
|
+
frontend = mapped.frontend;
|
|
146
|
+
backends = mapped.backends;
|
|
147
|
+
label = description.trim();
|
|
148
|
+
} else {
|
|
149
|
+
const preset = SCAFFOLD_PRESETS.find((p) => p.label === selection);
|
|
150
|
+
if (!preset) {
|
|
151
|
+
console.log(chalk2.red("Unknown template. Exiting."));
|
|
152
|
+
return { scaffolded: false };
|
|
153
|
+
}
|
|
154
|
+
command = preset.command;
|
|
155
|
+
needsInstall = preset.needsInstall;
|
|
156
|
+
label = preset.label;
|
|
157
|
+
}
|
|
158
|
+
const spinner = ora(`Scaffolding ${label}...`).start();
|
|
159
|
+
try {
|
|
160
|
+
const scaffolder = new ProjectScaffolder();
|
|
161
|
+
await scaffolder.scaffold(projectPath, command, needsInstall);
|
|
162
|
+
spinner.succeed(`Project scaffolded: ${label}`);
|
|
163
|
+
if (frontend || backends) {
|
|
164
|
+
const tomlPath = join(projectPath, "nova.toml");
|
|
165
|
+
let toml = "";
|
|
166
|
+
try {
|
|
167
|
+
toml = await readFile(tomlPath, "utf-8");
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
const lines = [];
|
|
171
|
+
if (frontend && !toml.includes("frontend =")) {
|
|
172
|
+
lines.push(`frontend = "${frontend}"`);
|
|
173
|
+
}
|
|
174
|
+
if (backends && backends.length > 0 && !toml.includes("backends =")) {
|
|
175
|
+
lines.push(`backends = [${backends.map((b) => `"${b}"`).join(", ")}]`);
|
|
176
|
+
}
|
|
177
|
+
if (lines.length > 0) {
|
|
178
|
+
if (toml.includes("[project]")) {
|
|
179
|
+
toml = toml.replace("[project]", `[project]
|
|
180
|
+
${lines.join("\n")}`);
|
|
181
|
+
} else {
|
|
182
|
+
toml += `
|
|
183
|
+
[project]
|
|
184
|
+
${lines.join("\n")}
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
await writeFile(tomlPath, toml, "utf-8");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { scaffolded: true, frontend, backends };
|
|
191
|
+
} catch (err) {
|
|
192
|
+
spinner.fail("Failed to scaffold project.");
|
|
193
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
194
|
+
console.error(chalk2.red(`
|
|
195
|
+
Error: ${message}`));
|
|
196
|
+
console.error(
|
|
197
|
+
chalk2.dim("Make sure npx/npm is available and you have an internet connection.")
|
|
198
|
+
);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
var KNOWN_TECHS = [
|
|
203
|
+
// Frontend
|
|
204
|
+
{ keywords: ["next"], command: (d) => `npx create-next-app@latest ${d} --typescript --tailwind --eslint --app --use-npm --no-git --no-src-dir --yes`, needsInstall: false, type: "frontend" },
|
|
205
|
+
{ keywords: ["remix"], command: (d) => `npx create-remix@latest ${d} --no-git-init --no-install`, needsInstall: true, type: "frontend" },
|
|
206
|
+
{ keywords: ["react", "vite"], command: (d) => `npm create vite@latest ${d} -- --template react-ts`, needsInstall: true, type: "frontend" },
|
|
207
|
+
{ keywords: ["nuxt"], command: (d) => `npx nuxi@latest init ${d} --no-install --gitInit false`, needsInstall: true, type: "frontend" },
|
|
208
|
+
{ keywords: ["vue"], command: (d) => `npm create vite@latest ${d} -- --template vue-ts`, needsInstall: true, type: "frontend" },
|
|
209
|
+
{ keywords: ["svelte"], command: (d) => `npx sv create ${d} --template minimal --types ts --no-install --no-add-ons`, needsInstall: true, type: "frontend" },
|
|
210
|
+
{ keywords: ["astro"], command: (d) => `npm create astro@latest ${d} -- --template basics --install --no-git --typescript strict --yes`, needsInstall: false, type: "frontend" },
|
|
211
|
+
{ keywords: ["solid"], command: (d) => `npx degit solidjs/templates/ts ${d}`, needsInstall: true, type: "frontend" },
|
|
212
|
+
// Backend
|
|
213
|
+
{ keywords: [".net", "dotnet", "c#", "csharp"], command: (d) => `dotnet new webapi -o ${d}`, needsInstall: false, type: "backend" },
|
|
214
|
+
{ keywords: ["express"], command: (d) => `mkdir -p ${d} && cd ${d} && npm init -y && npm install express && npm install -D typescript @types/express @types/node tsx`, needsInstall: false, type: "backend" },
|
|
215
|
+
{ keywords: ["fastify"], command: (d) => `mkdir -p ${d} && cd ${d} && npm init -y && npm install fastify && npm install -D typescript @types/node tsx`, needsInstall: false, type: "backend" },
|
|
216
|
+
{ keywords: ["hono"], command: (d) => `npm create hono@latest ${d} -- --template nodejs`, needsInstall: true, type: "backend" },
|
|
217
|
+
{ keywords: ["django"], command: (d) => `pip install django && django-admin startproject app ${d}`, needsInstall: false, type: "backend" },
|
|
218
|
+
{ keywords: ["fastapi", "fast api"], command: (d) => `mkdir -p ${d}/app && pip install fastapi uvicorn && echo "from fastapi import FastAPI\\napp = FastAPI()" > ${d}/app/main.py`, needsInstall: false, type: "backend" },
|
|
219
|
+
{ keywords: ["flask"], command: (d) => `mkdir -p ${d} && pip install flask && echo "from flask import Flask\\napp = Flask(__name__)" > ${d}/app.py`, needsInstall: false, type: "backend" },
|
|
220
|
+
{ keywords: ["go", "fiber"], command: (d) => `mkdir -p ${d} && cd ${d} && go mod init app && go get github.com/gofiber/fiber/v2`, needsInstall: false, type: "backend" },
|
|
221
|
+
{ keywords: ["go", "gin"], command: (d) => `mkdir -p ${d} && cd ${d} && go mod init app && go get github.com/gin-gonic/gin`, needsInstall: false, type: "backend" },
|
|
222
|
+
{ keywords: ["go"], command: (d) => `mkdir -p ${d} && cd ${d} && go mod init app`, needsInstall: false, type: "backend" }
|
|
223
|
+
];
|
|
224
|
+
function mapDescriptionToCommand(desc) {
|
|
225
|
+
const d = desc.toLowerCase();
|
|
226
|
+
const matched = [];
|
|
227
|
+
for (const tech of KNOWN_TECHS) {
|
|
228
|
+
if (tech.keywords.some((kw) => d.includes(kw))) {
|
|
229
|
+
const alreadyHasType = matched.some((m) => m.type === tech.type && m.keywords.some((k) => tech.keywords.includes(k)));
|
|
230
|
+
if (!alreadyHasType) {
|
|
231
|
+
matched.push(tech);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (matched.length === 0) {
|
|
236
|
+
return { command: "npm init -y", needsInstall: false };
|
|
237
|
+
}
|
|
238
|
+
if (matched.length === 1) {
|
|
239
|
+
const result2 = { command: matched[0].command("."), needsInstall: matched[0].needsInstall };
|
|
240
|
+
if (matched[0].type === "backend") {
|
|
241
|
+
result2.backends = ["."];
|
|
242
|
+
}
|
|
243
|
+
return result2;
|
|
244
|
+
}
|
|
245
|
+
const frontend = matched.find((m) => m.type === "frontend");
|
|
246
|
+
const backend = matched.find((m) => m.type === "backend");
|
|
247
|
+
const commands = [];
|
|
248
|
+
let needsInstall = false;
|
|
249
|
+
if (frontend) {
|
|
250
|
+
commands.push(frontend.command("."));
|
|
251
|
+
if (frontend.needsInstall) needsInstall = true;
|
|
252
|
+
}
|
|
253
|
+
if (backend) {
|
|
254
|
+
commands.push(backend.command("backend"));
|
|
255
|
+
if (backend.needsInstall) needsInstall = true;
|
|
256
|
+
}
|
|
257
|
+
if (!frontend && !backend) {
|
|
258
|
+
for (const m of matched) {
|
|
259
|
+
commands.push(m.command("."));
|
|
260
|
+
if (m.needsInstall) needsInstall = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const result = { command: commands.join(" && "), needsInstall };
|
|
264
|
+
if (frontend) result.frontend = ".";
|
|
265
|
+
if (backend) result.backends = ["backend"];
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/autofix.ts
|
|
270
|
+
import chalk3 from "chalk";
|
|
271
|
+
import { Lane3Executor } from "@novastorm-ai/core";
|
|
272
|
+
var ERROR_PATTERNS = [
|
|
273
|
+
/Module not found: Can't resolve '([^']+)'/,
|
|
274
|
+
/Invalid src prop.*next\/image/i,
|
|
275
|
+
/hostname.*is not configured under images/i,
|
|
276
|
+
/SyntaxError:\s+(.+)/,
|
|
277
|
+
/TypeError:\s+(.+)/,
|
|
278
|
+
/Build error/i,
|
|
279
|
+
/Compilation failed/i,
|
|
280
|
+
/Failed to compile/i,
|
|
281
|
+
/Error boundary caught/i
|
|
282
|
+
];
|
|
283
|
+
var IMAGE_PATTERNS = [
|
|
284
|
+
/Module not found.*\.(png|jpg|jpeg|gif|svg|webp|ico)/i,
|
|
285
|
+
/Invalid src prop.*next\/image/i,
|
|
286
|
+
/hostname.*is not configured under images/i,
|
|
287
|
+
/Image with src.*unsplash|picsum|placeholder/i,
|
|
288
|
+
/Cannot find.*image/i,
|
|
289
|
+
/Failed to load.*\.(png|jpg|jpeg|gif|svg|webp)/i,
|
|
290
|
+
/ENOENT.*\.(png|jpg|jpeg|gif|svg|webp|ico)/i,
|
|
291
|
+
/next\/image.*not configured/i
|
|
292
|
+
];
|
|
293
|
+
var ErrorAutoFixer = class {
|
|
294
|
+
constructor(projectPath, llmClient, gitManager, eventBus, wsServer, projectMap) {
|
|
295
|
+
this.projectPath = projectPath;
|
|
296
|
+
this.llmClient = llmClient;
|
|
297
|
+
this.gitManager = gitManager;
|
|
298
|
+
this.eventBus = eventBus;
|
|
299
|
+
this.wsServer = wsServer;
|
|
300
|
+
this.projectMap = projectMap;
|
|
301
|
+
}
|
|
302
|
+
isFixing = false;
|
|
303
|
+
errorBuffer = "";
|
|
304
|
+
debounceTimer = null;
|
|
305
|
+
DEBOUNCE_MS = 2e3;
|
|
306
|
+
fixAttempts = 0;
|
|
307
|
+
MAX_FIX_ATTEMPTS = 3;
|
|
308
|
+
lastErrorSignature = "";
|
|
309
|
+
cooldownUntil = 0;
|
|
310
|
+
/**
|
|
311
|
+
* Process dev server output. Call this for every stdout/stderr chunk.
|
|
312
|
+
*/
|
|
313
|
+
handleOutput(output) {
|
|
314
|
+
const hasError = ERROR_PATTERNS.some((p) => p.test(output)) || IMAGE_PATTERNS.some((p) => p.test(output));
|
|
315
|
+
if (!hasError) return;
|
|
316
|
+
if (this.isFixing) {
|
|
317
|
+
console.log(chalk3.dim("[Nova] AutoFixer: already fixing, queuing..."));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (Date.now() < this.cooldownUntil) {
|
|
321
|
+
console.log(chalk3.dim("[Nova] AutoFixer: in cooldown, skipping"));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.errorBuffer += output;
|
|
325
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
326
|
+
this.debounceTimer = setTimeout(() => {
|
|
327
|
+
void this.attemptAutoFix(this.errorBuffer);
|
|
328
|
+
this.errorBuffer = "";
|
|
329
|
+
}, this.DEBOUNCE_MS);
|
|
330
|
+
}
|
|
331
|
+
/** Force an immediate fix attempt, bypassing debounce and pattern check. */
|
|
332
|
+
forceFixNow(errorOutput) {
|
|
333
|
+
if (this.isFixing) {
|
|
334
|
+
console.log(chalk3.dim("[Nova] AutoFixer: already fixing, skipping forced fix"));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
void this.attemptAutoFix(errorOutput);
|
|
338
|
+
}
|
|
339
|
+
async attemptAutoFix(errorOutput) {
|
|
340
|
+
if (this.isFixing) return;
|
|
341
|
+
const errorSig = errorOutput.slice(0, 200);
|
|
342
|
+
if (errorSig === this.lastErrorSignature) {
|
|
343
|
+
this.fixAttempts++;
|
|
344
|
+
} else {
|
|
345
|
+
this.lastErrorSignature = errorSig;
|
|
346
|
+
this.fixAttempts = 1;
|
|
347
|
+
}
|
|
348
|
+
if (this.fixAttempts > this.MAX_FIX_ATTEMPTS) {
|
|
349
|
+
console.log(chalk3.yellow(`[Nova] AutoFixer: same error after ${this.MAX_FIX_ATTEMPTS} attempts, stopping. Fix manually.`));
|
|
350
|
+
this.cooldownUntil = Date.now() + 6e4;
|
|
351
|
+
this.wsServer.sendEvent({ type: "status", data: { message: "autofix_failed" } });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
this.isFixing = true;
|
|
355
|
+
const safetyTimer = setTimeout(() => {
|
|
356
|
+
if (this.isFixing) {
|
|
357
|
+
console.log(chalk3.dim("[Nova] AutoFixer: safety timeout, resetting"));
|
|
358
|
+
this.isFixing = false;
|
|
359
|
+
}
|
|
360
|
+
}, 3e5);
|
|
361
|
+
try {
|
|
362
|
+
const isImageError = IMAGE_PATTERNS.some((p) => p.test(errorOutput));
|
|
363
|
+
if (isImageError) {
|
|
364
|
+
await this.fixImageError(errorOutput);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
await this.fixCompilationError(errorOutput);
|
|
368
|
+
} finally {
|
|
369
|
+
clearTimeout(safetyTimer);
|
|
370
|
+
this.isFixing = false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async fixImageError(errorOutput) {
|
|
374
|
+
console.log(
|
|
375
|
+
chalk3.yellow(
|
|
376
|
+
"[Nova] Detected image loading error \u2014 replacing with placeholders"
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
this.wsServer.sendEvent({
|
|
380
|
+
type: "status",
|
|
381
|
+
data: {
|
|
382
|
+
message: "Image error detected. Replacing with placeholders..."
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
const hostnameMatch = errorOutput.match(/hostname "([^"]+)" is not configured/);
|
|
386
|
+
const invalidSrcMatch = errorOutput.match(/Invalid src prop \(([^)]+)\)/);
|
|
387
|
+
let description;
|
|
388
|
+
if (hostnameMatch || invalidSrcMatch) {
|
|
389
|
+
const hostname = hostnameMatch?.[1] ?? "unknown";
|
|
390
|
+
description = `Fix next/image error. Two options (pick the simpler one):
|
|
391
|
+
1. Replace all next/image <Image> tags that use external URLs with regular <img> tags.
|
|
392
|
+
2. OR add the hostname "${hostname}" to images.remotePatterns in next.config.ts/next.config.mjs.
|
|
393
|
+
Also: replace any invalid/fake image URLs (like https://invalid-url.com/*) with working placeholder URLs from https://picsum.photos (e.g. https://picsum.photos/800/600).
|
|
394
|
+
Error: ${errorOutput.slice(0, 300)}`;
|
|
395
|
+
} else {
|
|
396
|
+
description = `Fix image loading errors. Replace all broken/missing image references with working placeholder URLs from https://picsum.photos (e.g. https://picsum.photos/800/600 for large, https://picsum.photos/400/300 for medium). Use regular <img> tags instead of next/image <Image> for external URLs. Error: ${errorOutput.slice(0, 200)}`;
|
|
397
|
+
}
|
|
398
|
+
const task = {
|
|
399
|
+
id: crypto.randomUUID(),
|
|
400
|
+
description,
|
|
401
|
+
files: [],
|
|
402
|
+
type: "multi_file",
|
|
403
|
+
lane: 3,
|
|
404
|
+
status: "pending"
|
|
405
|
+
};
|
|
406
|
+
const executor = new Lane3Executor(
|
|
407
|
+
this.projectPath,
|
|
408
|
+
this.llmClient,
|
|
409
|
+
this.gitManager,
|
|
410
|
+
this.eventBus
|
|
411
|
+
);
|
|
412
|
+
console.log(chalk3.cyan("[Nova] Auto-fixing image errors..."));
|
|
413
|
+
this.wsServer.sendEvent({ type: "status", data: { message: "autofix_start" } });
|
|
414
|
+
this.eventBus.emit({ type: "task_started", data: { taskId: task.id } });
|
|
415
|
+
this.wsServer.sendEvent({ type: "task_created", data: task });
|
|
416
|
+
const result = await executor.execute(task, this.projectMap);
|
|
417
|
+
if (result.success) {
|
|
418
|
+
console.log(chalk3.green("[Nova] Image errors fixed automatically"));
|
|
419
|
+
this.eventBus.emit({
|
|
420
|
+
type: "task_completed",
|
|
421
|
+
data: { taskId: task.id, diff: result.diff ?? "", commitHash: result.commitHash ?? "" }
|
|
422
|
+
});
|
|
423
|
+
this.wsServer.sendEvent({ type: "status", data: { message: "autofix_end" } });
|
|
424
|
+
} else {
|
|
425
|
+
console.log(chalk3.red(`[Nova] Failed to fix image errors: ${result.error}`));
|
|
426
|
+
const failEvent = { type: "task_failed", data: { taskId: task.id, error: result.error ?? "Image fix failed" } };
|
|
427
|
+
this.eventBus.emit(failEvent);
|
|
428
|
+
this.wsServer.sendEvent(failEvent);
|
|
429
|
+
this.wsServer.sendEvent({ type: "status", data: { message: "autofix_failed" } });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async fixCompilationError(errorOutput) {
|
|
433
|
+
const truncatedError = errorOutput.slice(0, 500);
|
|
434
|
+
console.log(
|
|
435
|
+
chalk3.yellow("[Nova] Detected compilation error \u2014 attempting auto-fix")
|
|
436
|
+
);
|
|
437
|
+
this.wsServer.sendEvent({
|
|
438
|
+
type: "status",
|
|
439
|
+
data: { message: "Compilation error detected. Auto-fixing..." }
|
|
440
|
+
});
|
|
441
|
+
const task = {
|
|
442
|
+
id: crypto.randomUUID(),
|
|
443
|
+
description: `Fix the following compilation/build error in the project. Read the error carefully and fix the root cause:
|
|
444
|
+
${truncatedError}`,
|
|
445
|
+
files: [],
|
|
446
|
+
type: "multi_file",
|
|
447
|
+
lane: 3,
|
|
448
|
+
status: "pending"
|
|
449
|
+
};
|
|
450
|
+
const executor = new Lane3Executor(
|
|
451
|
+
this.projectPath,
|
|
452
|
+
this.llmClient,
|
|
453
|
+
this.gitManager,
|
|
454
|
+
this.eventBus
|
|
455
|
+
);
|
|
456
|
+
console.log(chalk3.cyan("[Nova] Auto-fixing compilation error..."));
|
|
457
|
+
this.wsServer.sendEvent({ type: "status", data: { message: "autofix_start" } });
|
|
458
|
+
this.eventBus.emit({ type: "task_started", data: { taskId: task.id } });
|
|
459
|
+
this.wsServer.sendEvent({ type: "task_created", data: task });
|
|
460
|
+
const result = await executor.execute(task, this.projectMap);
|
|
461
|
+
if (result.success) {
|
|
462
|
+
console.log(chalk3.green("[Nova] Compilation error fixed automatically"));
|
|
463
|
+
this.eventBus.emit({
|
|
464
|
+
type: "task_completed",
|
|
465
|
+
data: { taskId: task.id, diff: result.diff ?? "", commitHash: result.commitHash ?? "" }
|
|
466
|
+
});
|
|
467
|
+
this.wsServer.sendEvent({ type: "status", data: { message: "autofix_end" } });
|
|
468
|
+
} else {
|
|
469
|
+
console.log(chalk3.red(`[Nova] Auto-fix failed: ${result.error}`));
|
|
470
|
+
const failEvent = { type: "task_failed", data: { taskId: task.id, error: result.error ?? "Auto-fix failed" } };
|
|
471
|
+
this.eventBus.emit(failEvent);
|
|
472
|
+
this.wsServer.sendEvent(failEvent);
|
|
473
|
+
this.wsServer.sendEvent({ type: "status", data: { message: "autofix_failed" } });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// src/chat.ts
|
|
479
|
+
import * as readline from "readline";
|
|
480
|
+
import chalk4 from "chalk";
|
|
481
|
+
var SLASH_COMMANDS = {
|
|
482
|
+
"/settings": "settings",
|
|
483
|
+
"/help": "help",
|
|
484
|
+
"/status": "status",
|
|
485
|
+
"/map": "map",
|
|
486
|
+
"/yes": "confirm",
|
|
487
|
+
"/y": "confirm",
|
|
488
|
+
"/no": "cancel",
|
|
489
|
+
"/n": "cancel"
|
|
490
|
+
};
|
|
491
|
+
var NovaChat = class {
|
|
492
|
+
rl = null;
|
|
493
|
+
handlers = [];
|
|
494
|
+
prompt = chalk4.cyan("nova> ");
|
|
495
|
+
start() {
|
|
496
|
+
if (!process.stdin.isTTY) return;
|
|
497
|
+
this.rl = readline.createInterface({
|
|
498
|
+
input: process.stdin,
|
|
499
|
+
output: process.stdout,
|
|
500
|
+
prompt: this.prompt,
|
|
501
|
+
terminal: true
|
|
502
|
+
});
|
|
503
|
+
this.rl.on("line", (line) => {
|
|
504
|
+
const trimmed = line.trim();
|
|
505
|
+
if (!trimmed) {
|
|
506
|
+
this.rl?.prompt();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const cmd = this.parse(trimmed);
|
|
510
|
+
for (const handler of this.handlers) {
|
|
511
|
+
handler(cmd);
|
|
512
|
+
}
|
|
513
|
+
this.rl?.prompt();
|
|
514
|
+
});
|
|
515
|
+
this.rl.on("close", () => {
|
|
516
|
+
process.kill(process.pid, "SIGINT");
|
|
517
|
+
});
|
|
518
|
+
this.rl.prompt();
|
|
519
|
+
}
|
|
520
|
+
onCommand(handler) {
|
|
521
|
+
this.handlers.push(handler);
|
|
522
|
+
}
|
|
523
|
+
showPrompt() {
|
|
524
|
+
this.rl?.prompt();
|
|
525
|
+
}
|
|
526
|
+
log(message) {
|
|
527
|
+
if (this.rl) {
|
|
528
|
+
readline.clearLine(process.stdout, 0);
|
|
529
|
+
readline.cursorTo(process.stdout, 0);
|
|
530
|
+
}
|
|
531
|
+
console.log(message);
|
|
532
|
+
this.rl?.prompt(true);
|
|
533
|
+
}
|
|
534
|
+
stop() {
|
|
535
|
+
this.rl?.close();
|
|
536
|
+
this.rl = null;
|
|
537
|
+
}
|
|
538
|
+
parse(input2) {
|
|
539
|
+
const lower = input2.toLowerCase();
|
|
540
|
+
for (const [prefix, type] of Object.entries(SLASH_COMMANDS)) {
|
|
541
|
+
if (lower === prefix || lower.startsWith(prefix + " ")) {
|
|
542
|
+
const args = input2.slice(prefix.length).trim();
|
|
543
|
+
return { type, args };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (lower === "y" || lower === "yes" || lower === "execute") {
|
|
547
|
+
return { type: "confirm", args: "" };
|
|
548
|
+
}
|
|
549
|
+
if (lower === "n" || lower === "no" || lower === "cancel") {
|
|
550
|
+
return { type: "cancel", args: "" };
|
|
551
|
+
}
|
|
552
|
+
return { type: "text", args: input2 };
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/settings.ts
|
|
557
|
+
import chalk5 from "chalk";
|
|
558
|
+
var SETTABLE_FIELDS = [
|
|
559
|
+
{ path: "apiKeys.provider", description: "LLM provider", type: "string", options: ["openrouter", "anthropic", "openai", "ollama", "claude-cli"] },
|
|
560
|
+
{ path: "apiKeys.key", description: "API key (saved to .nova/config.toml)", type: "string", secret: true },
|
|
561
|
+
{ path: "models.fast", description: "Fast model", type: "string" },
|
|
562
|
+
{ path: "models.strong", description: "Strong model", type: "string" },
|
|
563
|
+
{ path: "models.local", description: "Use local models", type: "boolean" },
|
|
564
|
+
{ path: "project.devCommand", description: "Dev command", type: "string" },
|
|
565
|
+
{ path: "project.port", description: "Dev server port", type: "number" },
|
|
566
|
+
{ path: "project.frontend", description: "Frontend directory", type: "string" },
|
|
567
|
+
{ path: "project.backends", description: "Backend directories", type: "string[]" },
|
|
568
|
+
{ path: "behavior.autoCommit", description: "Auto-commit changes", type: "boolean" },
|
|
569
|
+
{ path: "behavior.branchPrefix", description: "Git branch prefix", type: "string" },
|
|
570
|
+
{ path: "behavior.passiveSuggestions", description: "Passive suggestions", type: "boolean" },
|
|
571
|
+
{ path: "voice.enabled", description: "Voice enabled", type: "boolean" },
|
|
572
|
+
{ path: "voice.engine", description: "Voice engine", type: "string", options: ["web", "whisper"] },
|
|
573
|
+
{ path: "telemetry.enabled", description: "Telemetry enabled", type: "boolean" }
|
|
574
|
+
];
|
|
575
|
+
function getNestedValue(obj, path4) {
|
|
576
|
+
const parts = path4.split(".");
|
|
577
|
+
let current = obj;
|
|
578
|
+
for (const part of parts) {
|
|
579
|
+
if (current === null || current === void 0 || typeof current !== "object") return void 0;
|
|
580
|
+
current = current[part];
|
|
581
|
+
}
|
|
582
|
+
return current;
|
|
583
|
+
}
|
|
584
|
+
function setNestedValue(obj, path4, value) {
|
|
585
|
+
const parts = path4.split(".");
|
|
586
|
+
let current = obj;
|
|
587
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
588
|
+
if (!current[parts[i]] || typeof current[parts[i]] !== "object") {
|
|
589
|
+
current[parts[i]] = {};
|
|
590
|
+
}
|
|
591
|
+
current = current[parts[i]];
|
|
592
|
+
}
|
|
593
|
+
current[parts[parts.length - 1]] = value;
|
|
594
|
+
}
|
|
595
|
+
function formatSettings(config) {
|
|
596
|
+
const lines = [chalk5.bold("\nNova Settings\n")];
|
|
597
|
+
let lastSection = "";
|
|
598
|
+
for (const field of SETTABLE_FIELDS) {
|
|
599
|
+
const section = field.path.split(".")[0];
|
|
600
|
+
if (section !== lastSection) {
|
|
601
|
+
lines.push(chalk5.dim(` [${section}]`));
|
|
602
|
+
lastSection = section;
|
|
603
|
+
}
|
|
604
|
+
const value = getNestedValue(config, field.path);
|
|
605
|
+
const displayValue = value === void 0 ? chalk5.dim("(not set)") : typeof value === "string" && field.path.includes("key") && value.length > 8 ? chalk5.yellow(value.slice(0, 4) + "..." + value.slice(-4)) : chalk5.green(JSON.stringify(value));
|
|
606
|
+
lines.push(` ${chalk5.cyan(field.path.padEnd(28))} ${displayValue} ${chalk5.dim(field.description)}`);
|
|
607
|
+
if (field.options) {
|
|
608
|
+
lines.push(` ${"".padEnd(28)} ${chalk5.dim(`options: ${field.options.join(", ")}`)}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
lines.push("");
|
|
612
|
+
lines.push(chalk5.dim(" Usage: /settings <key> <value>"));
|
|
613
|
+
lines.push(chalk5.dim(" Example: /settings apiKeys.provider ollama"));
|
|
614
|
+
lines.push(chalk5.dim(" Example: /settings models.fast claude-sonnet-4-6"));
|
|
615
|
+
lines.push("");
|
|
616
|
+
return lines.join("\n");
|
|
617
|
+
}
|
|
618
|
+
async function handleSettingsCommand(args, config, configReader, cwd) {
|
|
619
|
+
if (!args) {
|
|
620
|
+
return formatSettings(config);
|
|
621
|
+
}
|
|
622
|
+
const parts = args.split(/\s+/);
|
|
623
|
+
const key = parts[0];
|
|
624
|
+
const valueStr = parts.slice(1).join(" ");
|
|
625
|
+
if (!valueStr) {
|
|
626
|
+
const value = getNestedValue(config, key);
|
|
627
|
+
if (value === void 0) {
|
|
628
|
+
return chalk5.red(`Unknown setting: ${key}`);
|
|
629
|
+
}
|
|
630
|
+
return `${chalk5.cyan(key)} = ${chalk5.green(JSON.stringify(value))}`;
|
|
631
|
+
}
|
|
632
|
+
const field = SETTABLE_FIELDS.find((f) => f.path === key);
|
|
633
|
+
if (!field) {
|
|
634
|
+
return chalk5.red(`Unknown setting: ${key}
|
|
635
|
+
Available: ${SETTABLE_FIELDS.map((f) => f.path).join(", ")}`);
|
|
636
|
+
}
|
|
637
|
+
if (field.options && !field.options.includes(valueStr)) {
|
|
638
|
+
return chalk5.red(`Invalid value for ${key}. Options: ${field.options.join(", ")}`);
|
|
639
|
+
}
|
|
640
|
+
let parsedValue;
|
|
641
|
+
switch (field.type) {
|
|
642
|
+
case "number": {
|
|
643
|
+
parsedValue = parseInt(valueStr, 10);
|
|
644
|
+
if (isNaN(parsedValue)) {
|
|
645
|
+
return chalk5.red(`Invalid number: ${valueStr}`);
|
|
646
|
+
}
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
case "boolean": {
|
|
650
|
+
if (["true", "1", "yes", "on"].includes(valueStr.toLowerCase())) {
|
|
651
|
+
parsedValue = true;
|
|
652
|
+
} else if (["false", "0", "no", "off"].includes(valueStr.toLowerCase())) {
|
|
653
|
+
parsedValue = false;
|
|
654
|
+
} else {
|
|
655
|
+
return chalk5.red(`Invalid boolean: ${valueStr}. Use true/false`);
|
|
656
|
+
}
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case "string[]": {
|
|
660
|
+
parsedValue = valueStr.split(",").map((s) => s.trim());
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
default:
|
|
664
|
+
parsedValue = valueStr;
|
|
665
|
+
}
|
|
666
|
+
setNestedValue(config, key, parsedValue);
|
|
667
|
+
try {
|
|
668
|
+
if (field.secret) {
|
|
669
|
+
const secretConfig = {};
|
|
670
|
+
setNestedValue(secretConfig, key, parsedValue);
|
|
671
|
+
await configReader.writeLocal(cwd, secretConfig);
|
|
672
|
+
return chalk5.green(`${key} = ${JSON.stringify(parsedValue).slice(0, 4)}... (saved to .nova/config.toml)`);
|
|
673
|
+
}
|
|
674
|
+
await configReader.write(cwd, config);
|
|
675
|
+
return chalk5.green(`${key} = ${JSON.stringify(parsedValue)} (saved to nova.toml)`);
|
|
676
|
+
} catch (err) {
|
|
677
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
678
|
+
return chalk5.red(`Failed to save: ${msg}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/commands/start.ts
|
|
683
|
+
var PROXY_PORT_OFFSET = 1;
|
|
684
|
+
function findOverlayScript() {
|
|
685
|
+
const candidates = [
|
|
686
|
+
// From cli/dist/ (when imported as module)
|
|
687
|
+
path.resolve(import.meta.dirname, "..", "..", "overlay", "dist", "nova-overlay.global.js"),
|
|
688
|
+
// From cli/dist/bin/ (when run as binary)
|
|
689
|
+
path.resolve(import.meta.dirname, "..", "..", "..", "overlay", "dist", "nova-overlay.global.js"),
|
|
690
|
+
// From cli/src/commands/ (dev mode)
|
|
691
|
+
path.resolve(import.meta.dirname, "..", "..", "..", "..", "overlay", "dist", "nova-overlay.global.js")
|
|
692
|
+
];
|
|
693
|
+
for (const p of candidates) {
|
|
694
|
+
try {
|
|
695
|
+
if (__require("fs").existsSync(p)) return p;
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return candidates[0];
|
|
700
|
+
}
|
|
701
|
+
var OVERLAY_SCRIPT_PATH = findOverlayScript();
|
|
702
|
+
async function startCommand() {
|
|
703
|
+
const cwd = process.cwd();
|
|
704
|
+
const eventBus = new NovaEventBus();
|
|
705
|
+
const configReader = new ConfigReader();
|
|
706
|
+
const novaDir = new NovaDir();
|
|
707
|
+
const devServer = new DevServerRunner();
|
|
708
|
+
const proxyServer = new ProxyServer();
|
|
709
|
+
const wsServer = new WebSocketServer();
|
|
710
|
+
const licenseChecker = new LicenseChecker();
|
|
711
|
+
const indexer = new ProjectIndexer();
|
|
712
|
+
const logger = new NovaLogger();
|
|
713
|
+
const taskMap = /* @__PURE__ */ new Map();
|
|
714
|
+
let pendingTasks = [];
|
|
715
|
+
let lastObservation = null;
|
|
716
|
+
const spinner = ora2("Reading configuration...").start();
|
|
717
|
+
const config = await configReader.read(cwd);
|
|
718
|
+
spinner.succeed("Configuration loaded.");
|
|
719
|
+
spinner.start("Checking license...");
|
|
720
|
+
const license = await licenseChecker.check(cwd, config);
|
|
721
|
+
if (!license.valid) {
|
|
722
|
+
spinner.warn(
|
|
723
|
+
chalk6.yellow(
|
|
724
|
+
`License warning: ${license.message ?? "Invalid license."} Running in degraded mode.`
|
|
725
|
+
)
|
|
726
|
+
);
|
|
727
|
+
} else {
|
|
728
|
+
spinner.succeed(`License OK (${license.tier}, ${license.devCount} dev(s)).`);
|
|
729
|
+
}
|
|
730
|
+
if (config.telemetry.enabled && process.env["NOVA_TELEMETRY"] !== "false") {
|
|
731
|
+
const { createHash } = await import("crypto");
|
|
732
|
+
const os = await import("os");
|
|
733
|
+
const { execFile } = await import("child_process");
|
|
734
|
+
const mac = Object.values(os.networkInterfaces()).flat().find((i) => !i?.internal && i?.mac !== "00:00:00:00:00:00")?.mac ?? "";
|
|
735
|
+
const machineId = createHash("sha256").update(os.hostname() + os.userInfo().username + mac).digest("hex");
|
|
736
|
+
let projectHash;
|
|
737
|
+
try {
|
|
738
|
+
const remoteUrl = await new Promise((resolve4, reject) => {
|
|
739
|
+
execFile("git", ["remote", "get-url", "origin"], { cwd }, (err, stdout3) => {
|
|
740
|
+
if (err) reject(err);
|
|
741
|
+
else resolve4(stdout3.trim());
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
projectHash = createHash("sha256").update(remoteUrl).digest("hex");
|
|
745
|
+
} catch {
|
|
746
|
+
projectHash = createHash("sha256").update(cwd).digest("hex");
|
|
747
|
+
}
|
|
748
|
+
const telemetry = new Telemetry();
|
|
749
|
+
const cliPkg = await import("./package-3YCVE5UE.js").catch(
|
|
750
|
+
() => ({ default: { version: "0.0.1" } })
|
|
751
|
+
);
|
|
752
|
+
telemetry.send({
|
|
753
|
+
machineId,
|
|
754
|
+
gitAuthors90d: license.devCount,
|
|
755
|
+
projectHash,
|
|
756
|
+
cliVersion: cliPkg.default.version ?? "0.0.1",
|
|
757
|
+
os: process.platform,
|
|
758
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
759
|
+
licenseKey: config.license?.key ?? process.env["NOVA_LICENSE_KEY"] ?? null
|
|
760
|
+
}).then((response) => {
|
|
761
|
+
if (response && response.nudgeLevel > 0) {
|
|
762
|
+
const nudgeRenderer = new NudgeRenderer();
|
|
763
|
+
const nudgeMessage = nudgeRenderer.render({
|
|
764
|
+
level: response.nudgeLevel,
|
|
765
|
+
devCount: license.devCount,
|
|
766
|
+
tier: license.tier,
|
|
767
|
+
hasLicense: license.valid && license.tier !== "free"
|
|
768
|
+
});
|
|
769
|
+
if (nudgeMessage) {
|
|
770
|
+
console.log(chalk6.yellow(`
|
|
771
|
+
${nudgeMessage}
|
|
772
|
+
`));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}).catch(() => {
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
spinner.start("Detecting project...");
|
|
779
|
+
const { StackDetector } = await import("@novastorm-ai/core");
|
|
780
|
+
const stackDetector = new StackDetector();
|
|
781
|
+
let stack = await stackDetector.detectStack(cwd);
|
|
782
|
+
let detectedDevCommand = await stackDetector.detectDevCommand(stack, cwd);
|
|
783
|
+
let detectedPort = await stackDetector.detectPort(stack, cwd);
|
|
784
|
+
spinner.succeed(`Detecting project... ${chalk6.cyan(stack.framework || "unknown")} + ${chalk6.cyan(stack.typescript ? "TypeScript" : stack.language || "unknown")}`);
|
|
785
|
+
let devCommand = config.project.devCommand || detectedDevCommand;
|
|
786
|
+
let devPort = config.project.port || detectedPort;
|
|
787
|
+
if (!devCommand) {
|
|
788
|
+
if (novaDir.exists(cwd)) {
|
|
789
|
+
await novaDir.clean(cwd);
|
|
790
|
+
}
|
|
791
|
+
const scaffoldInfo = await promptAndScaffold(cwd);
|
|
792
|
+
if (!scaffoldInfo.scaffolded) {
|
|
793
|
+
process.exit(0);
|
|
794
|
+
}
|
|
795
|
+
if (scaffoldInfo.frontend) config.project.frontend = scaffoldInfo.frontend;
|
|
796
|
+
if (scaffoldInfo.backends) config.project.backends = scaffoldInfo.backends;
|
|
797
|
+
spinner.start("Re-detecting project...");
|
|
798
|
+
stack = await stackDetector.detectStack(cwd);
|
|
799
|
+
detectedDevCommand = await stackDetector.detectDevCommand(stack, cwd);
|
|
800
|
+
detectedPort = await stackDetector.detectPort(stack, cwd);
|
|
801
|
+
spinner.succeed(
|
|
802
|
+
`Detecting project... ${chalk6.cyan(stack.framework || "unknown")} + ${chalk6.cyan(stack.typescript ? "TypeScript" : stack.language || "unknown")}`
|
|
803
|
+
);
|
|
804
|
+
devCommand = config.project.devCommand || detectedDevCommand;
|
|
805
|
+
devPort = config.project.port || detectedPort;
|
|
806
|
+
if (!devCommand) {
|
|
807
|
+
console.error(
|
|
808
|
+
chalk6.red('No dev command found after scaffolding. Set project.devCommand in nova.toml or ensure package.json has a "dev" script.')
|
|
809
|
+
);
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
spinner.start("Initializing .nova/ directory...");
|
|
814
|
+
await novaDir.init(cwd);
|
|
815
|
+
spinner.succeed(".nova/ directory ready.");
|
|
816
|
+
spinner.start("Indexing project...");
|
|
817
|
+
let projectMap;
|
|
818
|
+
try {
|
|
819
|
+
projectMap = await indexer.index(cwd, { frontend: config.project.frontend, backends: config.project.backends });
|
|
820
|
+
} catch (err) {
|
|
821
|
+
spinner.fail("Failed to index project.");
|
|
822
|
+
throw err;
|
|
823
|
+
}
|
|
824
|
+
spinner.succeed("Project indexed.");
|
|
825
|
+
const { ProjectAnalyzer, RagIndexer, createEmbeddingService } = await import("@novastorm-ai/core");
|
|
826
|
+
const { ProjectMapApi } = await import("@novastorm-ai/proxy");
|
|
827
|
+
const projectAnalyzer = new ProjectAnalyzer();
|
|
828
|
+
spinner.start("Analyzing project structure...");
|
|
829
|
+
const analysis = await projectAnalyzer.analyze(cwd, projectMap);
|
|
830
|
+
spinner.succeed(`Project analyzed: ${analysis.fileCount} files, ${analysis.methods.length} methods.`);
|
|
831
|
+
let ragIndexer = null;
|
|
832
|
+
try {
|
|
833
|
+
const { VectorStore } = await import("@novastorm-ai/core");
|
|
834
|
+
let embeddingProvider = "tfidf";
|
|
835
|
+
let embeddingApiKey;
|
|
836
|
+
let embeddingBaseUrl;
|
|
837
|
+
try {
|
|
838
|
+
const res = await fetch("http://127.0.0.1:11434/api/tags");
|
|
839
|
+
if (res.ok) {
|
|
840
|
+
embeddingProvider = "ollama";
|
|
841
|
+
embeddingBaseUrl = "http://127.0.0.1:11434";
|
|
842
|
+
}
|
|
843
|
+
} catch {
|
|
844
|
+
}
|
|
845
|
+
if (embeddingProvider === "tfidf") {
|
|
846
|
+
const openaiKey = config.apiKeys.provider === "openai" ? config.apiKeys.key : process.env.OPENAI_API_KEY;
|
|
847
|
+
if (openaiKey) {
|
|
848
|
+
embeddingProvider = "openai";
|
|
849
|
+
embeddingApiKey = openaiKey;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const embeddingService = createEmbeddingService({
|
|
853
|
+
provider: embeddingProvider,
|
|
854
|
+
apiKey: embeddingApiKey,
|
|
855
|
+
baseUrl: embeddingBaseUrl
|
|
856
|
+
});
|
|
857
|
+
const vectorStore = new VectorStore();
|
|
858
|
+
ragIndexer = new RagIndexer(embeddingService, vectorStore);
|
|
859
|
+
const providerLabel = embeddingProvider === "openai" ? "OpenAI" : embeddingProvider === "ollama" ? "Ollama" : "TF-IDF (offline)";
|
|
860
|
+
spinner.start(`Building RAG index (${providerLabel})...`);
|
|
861
|
+
await ragIndexer.index(cwd, projectMap);
|
|
862
|
+
spinner.succeed(`RAG index built: ${vectorStore.getRecordCount()} chunks (${providerLabel}).`);
|
|
863
|
+
} catch (err) {
|
|
864
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
865
|
+
spinner.warn(`RAG indexing skipped: ${msg}`);
|
|
866
|
+
ragIndexer = null;
|
|
867
|
+
}
|
|
868
|
+
const projectMapApi = new ProjectMapApi();
|
|
869
|
+
const proxyPort = devPort + PROXY_PORT_OFFSET;
|
|
870
|
+
spinner.start(`Starting dev server (${chalk6.dim(devCommand)})...`);
|
|
871
|
+
try {
|
|
872
|
+
await devServer.spawn(devCommand, cwd, devPort);
|
|
873
|
+
} catch (err) {
|
|
874
|
+
spinner.fail("Dev server failed to start.");
|
|
875
|
+
throw err;
|
|
876
|
+
}
|
|
877
|
+
spinner.succeed("Starting dev server... done");
|
|
878
|
+
spinner.start("Starting proxy server...");
|
|
879
|
+
try {
|
|
880
|
+
await proxyServer.start(devPort, proxyPort, OVERLAY_SCRIPT_PATH);
|
|
881
|
+
} catch (err) {
|
|
882
|
+
spinner.fail("Proxy server failed to start.");
|
|
883
|
+
await devServer.kill();
|
|
884
|
+
throw err;
|
|
885
|
+
}
|
|
886
|
+
spinner.succeed(`Proxy ready at ${chalk6.green(`localhost:${proxyPort}`)}`);
|
|
887
|
+
const httpServer = proxyServer.getHttpServer();
|
|
888
|
+
if (httpServer) {
|
|
889
|
+
wsServer.start(httpServer);
|
|
890
|
+
}
|
|
891
|
+
proxyServer.setProjectMapApi(projectMapApi);
|
|
892
|
+
const { GraphStore: GS, SearchRouter: SR } = await import("@novastorm-ai/core");
|
|
893
|
+
const novaPath = novaDir.getPath(cwd);
|
|
894
|
+
const graphStoreForApi = new GS(novaPath);
|
|
895
|
+
const searchRouterForApi = new SR(graphStoreForApi);
|
|
896
|
+
projectMapApi.setGraphStore(graphStoreForApi);
|
|
897
|
+
projectMapApi.setSearchRouter(searchRouterForApi);
|
|
898
|
+
projectMapApi.setAnalysis(analysis);
|
|
899
|
+
setTimeout(() => {
|
|
900
|
+
wsServer.sendEvent({
|
|
901
|
+
type: "analysis_complete",
|
|
902
|
+
data: { fileCount: analysis.fileCount, methodCount: analysis.methods.length }
|
|
903
|
+
});
|
|
904
|
+
}, 2e3);
|
|
905
|
+
console.log(chalk6.dim("Opening browser..."));
|
|
906
|
+
const openUrl = `http://127.0.0.1:${proxyPort}`;
|
|
907
|
+
if (process.platform === "darwin") {
|
|
908
|
+
exec(`open -a "Google Chrome" "${openUrl}" 2>/dev/null || open -a "Chromium" "${openUrl}" 2>/dev/null || open "${openUrl}"`);
|
|
909
|
+
} else if (process.platform === "win32") {
|
|
910
|
+
exec(`start chrome "${openUrl}" 2>nul || start "${openUrl}"`);
|
|
911
|
+
} else {
|
|
912
|
+
exec(`google-chrome "${openUrl}" 2>/dev/null || chromium "${openUrl}" 2>/dev/null || xdg-open "${openUrl}"`);
|
|
913
|
+
}
|
|
914
|
+
if (!config.apiKeys.key && config.apiKeys.provider !== "ollama" && config.apiKeys.provider !== "claude-cli") {
|
|
915
|
+
console.log(chalk6.yellow("\nNo API key configured. Running setup...\n"));
|
|
916
|
+
const { runSetup: runSetup2 } = await import("./setup-3KREUXRO.js");
|
|
917
|
+
await runSetup2(cwd);
|
|
918
|
+
const updatedConfig = await configReader.read(cwd);
|
|
919
|
+
config.apiKeys = updatedConfig.apiKeys;
|
|
920
|
+
}
|
|
921
|
+
const providerFactory = new ProviderFactory();
|
|
922
|
+
let llmClient;
|
|
923
|
+
try {
|
|
924
|
+
llmClient = providerFactory.create(config.apiKeys.provider, config.apiKeys.key);
|
|
925
|
+
} catch (err) {
|
|
926
|
+
console.log(chalk6.yellow("\nAI provider not configured. Nova is running without AI analysis."));
|
|
927
|
+
console.log(chalk6.dim('Run "nova setup" to configure your API key.\n'));
|
|
928
|
+
llmClient = null;
|
|
929
|
+
}
|
|
930
|
+
const brain = llmClient ? new Brain(llmClient, eventBus) : null;
|
|
931
|
+
try {
|
|
932
|
+
const { execSync } = await import("child_process");
|
|
933
|
+
execSync("git rev-parse --git-dir", { cwd, stdio: "ignore" });
|
|
934
|
+
} catch {
|
|
935
|
+
const { execSync } = await import("child_process");
|
|
936
|
+
execSync("git init", { cwd, stdio: "ignore" });
|
|
937
|
+
execSync('git add -A && git commit -m "Initial commit (before Nova)" --allow-empty', { cwd, stdio: "ignore", shell: "/bin/sh" });
|
|
938
|
+
console.log(chalk6.dim("Initialized git repository."));
|
|
939
|
+
}
|
|
940
|
+
const gitManager = new GitManager(cwd);
|
|
941
|
+
try {
|
|
942
|
+
const branch = await gitManager.createBranch(config.behavior.branchPrefix);
|
|
943
|
+
console.log(chalk6.dim(`Working on branch: ${branch}`));
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
let executorPool = null;
|
|
947
|
+
if (llmClient) {
|
|
948
|
+
const pathGuard = new PathGuard(cwd);
|
|
949
|
+
if (config.project.frontend) pathGuard.allow(resolve2(cwd, config.project.frontend));
|
|
950
|
+
for (const b of config.project.backends ?? []) pathGuard.allow(resolve2(cwd, b));
|
|
951
|
+
const manifestStore = new ManifestStore();
|
|
952
|
+
const manifest = await manifestStore.load(cwd);
|
|
953
|
+
if (manifest?.boundaries) {
|
|
954
|
+
pathGuard.loadBoundaries(manifest.boundaries);
|
|
955
|
+
}
|
|
956
|
+
const agentPromptLoader = new AgentPromptLoader();
|
|
957
|
+
const lane1 = new Lane1Executor(cwd, pathGuard);
|
|
958
|
+
const lane2 = new Lane2Executor(cwd, llmClient, gitManager, pathGuard);
|
|
959
|
+
executorPool = new ExecutorPool(lane1, lane2, eventBus, llmClient, gitManager, cwd, config.models.fast, config.models.strong, agentPromptLoader, pathGuard);
|
|
960
|
+
}
|
|
961
|
+
let autoFixer = null;
|
|
962
|
+
if (llmClient) {
|
|
963
|
+
autoFixer = new ErrorAutoFixer(cwd, llmClient, gitManager, eventBus, wsServer, projectMap);
|
|
964
|
+
}
|
|
965
|
+
devServer.onOutput((output) => {
|
|
966
|
+
autoFixer?.handleOutput(output);
|
|
967
|
+
});
|
|
968
|
+
const envDetector = new EnvDetector();
|
|
969
|
+
wsServer.onSecretsSubmit((secrets) => {
|
|
970
|
+
console.log(chalk6.cyan(`[Nova] Saving ${Object.keys(secrets).length} secret(s) to .env.local`));
|
|
971
|
+
envDetector.writeEnvLocal(cwd, secrets);
|
|
972
|
+
envDetector.ensureGitignored(cwd);
|
|
973
|
+
wsServer.sendEvent({ type: "status", data: { message: `Saved ${Object.keys(secrets).length} secret(s) to .env.local` } });
|
|
974
|
+
});
|
|
975
|
+
wsServer.onBrowserError((error) => {
|
|
976
|
+
console.log(chalk6.yellow(`[Nova] Browser error: ${error.slice(0, 150)}`));
|
|
977
|
+
autoFixer?.handleOutput(error);
|
|
978
|
+
});
|
|
979
|
+
wsServer.onObservation((observation, _autoExecute) => {
|
|
980
|
+
logger.logObservation(observation);
|
|
981
|
+
eventBus.emit({ type: "observation", data: observation });
|
|
982
|
+
});
|
|
983
|
+
eventBus.on("observation", async (event) => {
|
|
984
|
+
if (!brain) {
|
|
985
|
+
console.log(chalk6.yellow('Observation received but no AI configured. Run "nova setup" to add an API key.'));
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
try {
|
|
989
|
+
lastObservation = event.data;
|
|
990
|
+
const transcript = event.data.transcript ?? "click";
|
|
991
|
+
if (/\b(revert|верни|откати|undo|отмени последн|верни назад|откатить)\b/i.test(transcript)) {
|
|
992
|
+
console.log(chalk6.cyan("[Nova] Detected revert request \u2014 using git revert"));
|
|
993
|
+
wsServer.sendEvent({ type: "status", data: { message: "Reverting last commit..." } });
|
|
994
|
+
try {
|
|
995
|
+
const log = await gitManager.getLog();
|
|
996
|
+
if (log.length > 0) {
|
|
997
|
+
const lastCommit = log[0];
|
|
998
|
+
console.log(chalk6.cyan(`[Nova] Reverting commit: ${lastCommit.hash} \u2014 ${lastCommit.message}`));
|
|
999
|
+
await gitManager.rollback(lastCommit.hash);
|
|
1000
|
+
console.log(chalk6.green(`[Nova] Reverted successfully!`));
|
|
1001
|
+
wsServer.sendEvent({ type: "status", data: { message: `Reverted: ${lastCommit.message.slice(0, 80)}` } });
|
|
1002
|
+
setTimeout(() => {
|
|
1003
|
+
wsServer.sendEvent({ type: "status", data: { message: "autofix_end" } });
|
|
1004
|
+
}, 1500);
|
|
1005
|
+
} else {
|
|
1006
|
+
console.log(chalk6.yellow("[Nova] No commits to revert"));
|
|
1007
|
+
wsServer.sendEvent({ type: "status", data: { message: "No commits to revert." } });
|
|
1008
|
+
}
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1011
|
+
console.log(chalk6.red(`[Nova] Revert failed: ${msg}`));
|
|
1012
|
+
wsServer.sendEvent({ type: "status", data: { message: `Revert failed: ${msg}` } });
|
|
1013
|
+
}
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
logger.logAnalyzing(transcript);
|
|
1017
|
+
wsServer.sendEvent({ type: "status", data: { message: `\u{1F9E0} AI is thinking about: "${transcript.slice(0, 80)}"...` } });
|
|
1018
|
+
const analyzeSpinner = ora2({ text: chalk6.yellow("AI is thinking..."), spinner: "dots" }).start();
|
|
1019
|
+
const tasks = await brain.analyze(event.data, projectMap);
|
|
1020
|
+
analyzeSpinner.succeed(chalk6.green(`AI produced ${tasks.length} task(s)`));
|
|
1021
|
+
logger.logTasks(tasks);
|
|
1022
|
+
if (tasks.length === 0) {
|
|
1023
|
+
console.log(chalk6.dim("[Nova] No tasks produced \u2014 AI may have asked a question"));
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
wsServer.sendEvent({ type: "status", data: { message: `AI produced ${tasks.length} task(s)` } });
|
|
1027
|
+
console.log(chalk6.green(`Auto-executing ${tasks.length} task(s)...`));
|
|
1028
|
+
wsServer.sendEvent({ type: "status", data: { message: `Auto-executing ${tasks.length} task(s)...` } });
|
|
1029
|
+
wsServer.sendEvent({ type: "status", data: { message: "Confirmed! Executing tasks..." } });
|
|
1030
|
+
for (const task of tasks) {
|
|
1031
|
+
eventBus.emit({ type: "task_created", data: task });
|
|
1032
|
+
}
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1035
|
+
console.error(chalk6.red(`Analysis error: ${message}`));
|
|
1036
|
+
wsServer.sendEvent({ type: "status", data: { message: `Analysis error: ${message}` } });
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
wsServer.onConfirm(() => {
|
|
1040
|
+
if (pendingTasks.length === 0) {
|
|
1041
|
+
console.log(chalk6.dim("No pending tasks to confirm."));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
console.log(chalk6.green(`Confirmed ${pendingTasks.length} task(s). Executing...`));
|
|
1045
|
+
wsServer.sendEvent({ type: "status", data: { message: "Confirmed! Executing tasks..." } });
|
|
1046
|
+
for (const task of pendingTasks) {
|
|
1047
|
+
eventBus.emit({ type: "task_created", data: task });
|
|
1048
|
+
}
|
|
1049
|
+
pendingTasks = [];
|
|
1050
|
+
});
|
|
1051
|
+
wsServer.onCancel(() => {
|
|
1052
|
+
if (pendingTasks.length === 0) {
|
|
1053
|
+
console.log(chalk6.dim("No pending tasks to cancel."));
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
console.log(chalk6.yellow(`Cancelled ${pendingTasks.length} task(s).`));
|
|
1057
|
+
wsServer.sendEvent({ type: "status", data: { message: "Tasks cancelled." } });
|
|
1058
|
+
pendingTasks = [];
|
|
1059
|
+
});
|
|
1060
|
+
wsServer.onAppend(async (text) => {
|
|
1061
|
+
if (!brain || !lastObservation) return;
|
|
1062
|
+
console.log(chalk6.cyan(`[Nova] Appending to request: "${text}"`));
|
|
1063
|
+
const originalTranscript = lastObservation.transcript ?? "";
|
|
1064
|
+
const mergedTranscript = `${originalTranscript}. Additionally: ${text}`;
|
|
1065
|
+
const updatedObservation = {
|
|
1066
|
+
...lastObservation,
|
|
1067
|
+
transcript: mergedTranscript
|
|
1068
|
+
};
|
|
1069
|
+
pendingTasks = [];
|
|
1070
|
+
wsServer.sendEvent({ type: "status", data: { message: `Re-analyzing with: "${text}"...` } });
|
|
1071
|
+
try {
|
|
1072
|
+
logger.logAnalyzing(mergedTranscript);
|
|
1073
|
+
const tasks = await brain.analyze(updatedObservation, projectMap);
|
|
1074
|
+
logger.logTasks(tasks);
|
|
1075
|
+
if (tasks.length === 0) {
|
|
1076
|
+
wsServer.sendEvent({ type: "status", data: { message: "No tasks generated." } });
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
pendingTasks = tasks;
|
|
1080
|
+
const taskDescriptions = tasks.map((t, i) => `${i + 1}. ${t.description}`).join("; ");
|
|
1081
|
+
const pendingMessage = `Pending: ${tasks.length} task(s) \u2014 ${taskDescriptions}. Say "yes"/"execute" to proceed or "no"/"cancel" to discard.`;
|
|
1082
|
+
console.log(chalk6.yellow(`
|
|
1083
|
+
${pendingMessage}
|
|
1084
|
+
`));
|
|
1085
|
+
wsServer.sendEvent({ type: "status", data: { message: pendingMessage, tasks: tasks.map((t) => ({ id: t.id, description: t.description, lane: t.lane })) } });
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1088
|
+
console.error(chalk6.red(`Analysis error: ${message}`));
|
|
1089
|
+
wsServer.sendEvent({ type: "status", data: { message: `Analysis error: ${message}` } });
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
eventBus.on("task_created", async (event) => {
|
|
1093
|
+
taskMap.set(event.data.id, event.data);
|
|
1094
|
+
logger.logTaskStarted(event.data);
|
|
1095
|
+
wsServer.sendEvent(event);
|
|
1096
|
+
if (executorPool) {
|
|
1097
|
+
try {
|
|
1098
|
+
await executorPool.execute(event.data, projectMap);
|
|
1099
|
+
} catch {
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
eventBus.on("task_completed", (event) => {
|
|
1104
|
+
const task = taskMap.get(event.data.taskId);
|
|
1105
|
+
if (task) {
|
|
1106
|
+
task.commitHash = event.data.commitHash;
|
|
1107
|
+
logger.logTaskCompleted(task);
|
|
1108
|
+
}
|
|
1109
|
+
wsServer.sendEvent(event);
|
|
1110
|
+
setTimeout(async () => {
|
|
1111
|
+
const logs = devServer.getLogs();
|
|
1112
|
+
const recentLogs = logs.slice(-2e3);
|
|
1113
|
+
const hasLogError = /error|Error|failed|Failed|Module not found|SyntaxError|TypeError/i.test(recentLogs) && !/Successfully compiled|Compiled/.test(recentLogs.slice(-500));
|
|
1114
|
+
if (hasLogError && autoFixer) {
|
|
1115
|
+
const errorLines = recentLogs.split("\n").filter((l) => /error|Error|failed|Module not found/i.test(l)).slice(-5).join("\n");
|
|
1116
|
+
if (errorLines.trim()) {
|
|
1117
|
+
console.log(chalk6.yellow(`[Nova] Post-task health check: build errors detected, auto-fixing...`));
|
|
1118
|
+
wsServer.sendEvent({ type: "status", data: { message: "Post-task check: fixing build errors..." } });
|
|
1119
|
+
autoFixer.forceFixNow(errorLines);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
const http = await import("http");
|
|
1125
|
+
const res = await new Promise((resolve4) => {
|
|
1126
|
+
const req = http.get(`http://127.0.0.1:${devPort}`, resolve4);
|
|
1127
|
+
req.on("error", () => resolve4({ statusCode: 0 }));
|
|
1128
|
+
req.setTimeout(5e3, () => {
|
|
1129
|
+
req.destroy();
|
|
1130
|
+
resolve4({ statusCode: 0 });
|
|
1131
|
+
});
|
|
1132
|
+
});
|
|
1133
|
+
if (res.statusCode && res.statusCode >= 500) {
|
|
1134
|
+
console.log(chalk6.yellow(`[Nova] Post-task health check: HTTP ${res.statusCode}, auto-fixing...`));
|
|
1135
|
+
wsServer.sendEvent({ type: "status", data: { message: `Site returned ${res.statusCode}, auto-fixing...` } });
|
|
1136
|
+
autoFixer?.forceFixNow(`Dev server returned HTTP ${res.statusCode} after code changes`);
|
|
1137
|
+
}
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
}, 3e3);
|
|
1141
|
+
});
|
|
1142
|
+
eventBus.on("task_failed", (event) => {
|
|
1143
|
+
const task = taskMap.get(event.data.taskId);
|
|
1144
|
+
if (task) {
|
|
1145
|
+
task.error = event.data.error;
|
|
1146
|
+
logger.logTaskFailed(task);
|
|
1147
|
+
}
|
|
1148
|
+
wsServer.sendEvent(event);
|
|
1149
|
+
});
|
|
1150
|
+
eventBus.on("file_changed", (event) => {
|
|
1151
|
+
logger.logFileChanged(event.data.filePath);
|
|
1152
|
+
});
|
|
1153
|
+
eventBus.on("llm_chunk", (event) => {
|
|
1154
|
+
wsServer.sendEvent(event);
|
|
1155
|
+
});
|
|
1156
|
+
eventBus.on("secrets_required", (event) => {
|
|
1157
|
+
wsServer.sendEvent(event);
|
|
1158
|
+
});
|
|
1159
|
+
eventBus.on("status", (event) => {
|
|
1160
|
+
wsServer.sendEvent(event);
|
|
1161
|
+
});
|
|
1162
|
+
console.log(
|
|
1163
|
+
chalk6.bold.green("\nReady! Click elements or speak to start building.")
|
|
1164
|
+
);
|
|
1165
|
+
console.log(chalk6.dim("Type commands below, or use /help for available commands.\n"));
|
|
1166
|
+
setTimeout(async () => {
|
|
1167
|
+
const startupLogs = devServer.getLogs();
|
|
1168
|
+
const startupErrors = startupLogs.split("\n").filter((l) => /error|Error|failed|Module not found|SyntaxError|Cannot find/i.test(l)).filter((l) => !/warning|warn|deprecat|DeprecationWarning/i.test(l)).slice(-10).join("\n").trim();
|
|
1169
|
+
if (!startupErrors || !llmClient) return;
|
|
1170
|
+
console.log(chalk6.red("\n[Nova] Build errors detected at startup:"));
|
|
1171
|
+
console.log(chalk6.dim(startupErrors.slice(0, 500)));
|
|
1172
|
+
const fixTask = {
|
|
1173
|
+
id: crypto.randomUUID(),
|
|
1174
|
+
description: `Fix build errors at startup:
|
|
1175
|
+
${startupErrors.slice(0, 500)}`,
|
|
1176
|
+
files: [],
|
|
1177
|
+
type: "multi_file",
|
|
1178
|
+
lane: 3,
|
|
1179
|
+
status: "pending"
|
|
1180
|
+
};
|
|
1181
|
+
pendingTasks = [fixTask];
|
|
1182
|
+
const pendingMessage = `Pending: Build errors detected at startup. Fix them? 1. ${fixTask.description.slice(0, 100)}`;
|
|
1183
|
+
console.log(chalk6.yellow(`
|
|
1184
|
+
${pendingMessage}`));
|
|
1185
|
+
console.log(chalk6.dim("Press Y/Enter to fix, N to skip"));
|
|
1186
|
+
wsServer.sendEvent({
|
|
1187
|
+
type: "status",
|
|
1188
|
+
data: {
|
|
1189
|
+
message: pendingMessage,
|
|
1190
|
+
tasks: [{ id: fixTask.id, description: "Fix startup build errors", lane: 3 }]
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
}, 4e3);
|
|
1194
|
+
let shuttingDown = false;
|
|
1195
|
+
let chat = null;
|
|
1196
|
+
const shutdown = async () => {
|
|
1197
|
+
if (shuttingDown) return;
|
|
1198
|
+
shuttingDown = true;
|
|
1199
|
+
console.log(chalk6.dim("\n\nShutting down Nova..."));
|
|
1200
|
+
chat?.stop();
|
|
1201
|
+
try {
|
|
1202
|
+
await proxyServer.stop();
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
await devServer.kill();
|
|
1207
|
+
} catch {
|
|
1208
|
+
}
|
|
1209
|
+
console.log(chalk6.dim("Goodbye!"));
|
|
1210
|
+
process.exit(0);
|
|
1211
|
+
};
|
|
1212
|
+
devServer.onError((error) => {
|
|
1213
|
+
if (!shuttingDown) {
|
|
1214
|
+
console.error(chalk6.red(`
|
|
1215
|
+
Dev server error: ${error}`));
|
|
1216
|
+
wsServer.sendEvent({ type: "status", data: { message: `Dev server error: ${error}` } });
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
chat = new NovaChat();
|
|
1220
|
+
chat.onCommand(async (cmd) => {
|
|
1221
|
+
switch (cmd.type) {
|
|
1222
|
+
case "text": {
|
|
1223
|
+
if (!brain) {
|
|
1224
|
+
chat.log(chalk6.yellow("AI not configured. Run /settings apiKeys.provider <provider> to set up."));
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
const observation = {
|
|
1228
|
+
screenshot: Buffer.alloc(0),
|
|
1229
|
+
transcript: cmd.args,
|
|
1230
|
+
currentUrl: `file://${cwd}`,
|
|
1231
|
+
timestamp: Date.now()
|
|
1232
|
+
};
|
|
1233
|
+
lastObservation = observation;
|
|
1234
|
+
logger.logObservation(observation);
|
|
1235
|
+
eventBus.emit({ type: "observation", data: observation });
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
case "confirm": {
|
|
1239
|
+
if (pendingTasks.length === 0) {
|
|
1240
|
+
chat.log(chalk6.dim("No pending tasks to confirm."));
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
chat.log(chalk6.green(`Confirmed ${pendingTasks.length} task(s). Executing...`));
|
|
1244
|
+
wsServer.sendEvent({ type: "status", data: { message: "Confirmed! Executing tasks..." } });
|
|
1245
|
+
for (const task of pendingTasks) {
|
|
1246
|
+
eventBus.emit({ type: "task_created", data: task });
|
|
1247
|
+
}
|
|
1248
|
+
pendingTasks = [];
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
case "cancel": {
|
|
1252
|
+
if (pendingTasks.length === 0) {
|
|
1253
|
+
chat.log(chalk6.dim("No pending tasks to cancel."));
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
chat.log(chalk6.yellow(`Cancelled ${pendingTasks.length} task(s).`));
|
|
1257
|
+
wsServer.sendEvent({ type: "status", data: { message: "Tasks cancelled." } });
|
|
1258
|
+
pendingTasks = [];
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
case "settings": {
|
|
1262
|
+
const result = await handleSettingsCommand(cmd.args, config, configReader, cwd);
|
|
1263
|
+
chat.log(result);
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
case "help": {
|
|
1267
|
+
chat.log([
|
|
1268
|
+
chalk6.bold("\nNova Commands\n"),
|
|
1269
|
+
` ${chalk6.cyan("any text")} Send as a code change request (like voice in UI)`,
|
|
1270
|
+
` ${chalk6.cyan("/settings")} View all settings`,
|
|
1271
|
+
` ${chalk6.cyan("/settings k v")} Change a setting`,
|
|
1272
|
+
` ${chalk6.cyan("/status")} Show current status`,
|
|
1273
|
+
` ${chalk6.cyan("/map")} Open project map in browser`,
|
|
1274
|
+
` ${chalk6.cyan("/help")} Show this help`,
|
|
1275
|
+
` ${chalk6.cyan("y / yes")} Confirm pending tasks`,
|
|
1276
|
+
` ${chalk6.cyan("n / no")} Cancel pending tasks`,
|
|
1277
|
+
` ${chalk6.cyan("Ctrl+C")} Shutdown Nova`,
|
|
1278
|
+
""
|
|
1279
|
+
].join("\n"));
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
case "status": {
|
|
1283
|
+
const parts = [chalk6.bold("\nNova Status\n")];
|
|
1284
|
+
parts.push(` ${chalk6.dim("Project:")} ${cwd}`);
|
|
1285
|
+
parts.push(` ${chalk6.dim("Stack:")} ${projectMap.stack.framework} (${projectMap.stack.language})`);
|
|
1286
|
+
parts.push(` ${chalk6.dim("Dev server:")} localhost:${devPort}`);
|
|
1287
|
+
parts.push(` ${chalk6.dim("Proxy:")} localhost:${proxyPort}`);
|
|
1288
|
+
parts.push(` ${chalk6.dim("Overlay clients:")} ${wsServer.getClientCount()}`);
|
|
1289
|
+
parts.push(` ${chalk6.dim("AI:")} ${llmClient ? `${config.apiKeys.provider}` : "not configured"}`);
|
|
1290
|
+
parts.push(` ${chalk6.dim("RAG:")} ${ragIndexer ? "active" : "disabled"}`);
|
|
1291
|
+
parts.push(` ${chalk6.dim("Pending tasks:")} ${pendingTasks.length}`);
|
|
1292
|
+
parts.push("");
|
|
1293
|
+
chat.log(parts.join("\n"));
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
case "map": {
|
|
1297
|
+
const url = `http://127.0.0.1:${proxyPort}/nova-project-map`;
|
|
1298
|
+
chat.log(chalk6.cyan(`Opening project map: ${url}`));
|
|
1299
|
+
const { exec: execCmd } = await import("child_process");
|
|
1300
|
+
if (process.platform === "darwin") {
|
|
1301
|
+
execCmd(`open "${url}"`);
|
|
1302
|
+
} else if (process.platform === "win32") {
|
|
1303
|
+
execCmd(`start "${url}"`);
|
|
1304
|
+
} else {
|
|
1305
|
+
execCmd(`xdg-open "${url}"`);
|
|
1306
|
+
}
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
chat.start();
|
|
1312
|
+
process.on("SIGINT", () => {
|
|
1313
|
+
shutdown().catch(() => process.exit(1));
|
|
1314
|
+
});
|
|
1315
|
+
process.on("SIGTERM", () => {
|
|
1316
|
+
shutdown().catch(() => process.exit(1));
|
|
1317
|
+
});
|
|
1318
|
+
let forceCount = 0;
|
|
1319
|
+
process.on("SIGINT", () => {
|
|
1320
|
+
forceCount++;
|
|
1321
|
+
if (forceCount >= 2) {
|
|
1322
|
+
console.log(chalk6.dim("\nForce exit."));
|
|
1323
|
+
process.exit(1);
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
await new Promise(() => {
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/commands/chat.ts
|
|
1331
|
+
async function chatCommand() {
|
|
1332
|
+
console.log("Chat mode coming in v0.2");
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/commands/init.ts
|
|
1336
|
+
import * as path2 from "path";
|
|
1337
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
1338
|
+
import { stdin, stdout } from "process";
|
|
1339
|
+
import { DEFAULT_CONFIG } from "@novastorm-ai/core";
|
|
1340
|
+
async function initCommand() {
|
|
1341
|
+
const cwd = process.cwd();
|
|
1342
|
+
const configReader = new ConfigReader();
|
|
1343
|
+
const exists = await configReader.exists(cwd);
|
|
1344
|
+
if (exists) {
|
|
1345
|
+
console.log("nova.toml already exists in this directory.");
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const rl = createInterface2({ input: stdin, output: stdout });
|
|
1349
|
+
const config = { ...DEFAULT_CONFIG };
|
|
1350
|
+
try {
|
|
1351
|
+
const frontend = await rl.question("Where is your frontend? (default: ./) ");
|
|
1352
|
+
const frontendPath = frontend.trim() || void 0;
|
|
1353
|
+
if (frontendPath && frontendPath !== "./") {
|
|
1354
|
+
config.project = { ...config.project, frontend: frontendPath };
|
|
1355
|
+
}
|
|
1356
|
+
const backendsInput = await rl.question("Do you have backend services? Specify paths separated by commas (leave empty to skip): ");
|
|
1357
|
+
const backendsRaw = backendsInput.trim();
|
|
1358
|
+
if (backendsRaw) {
|
|
1359
|
+
const backends = backendsRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1360
|
+
if (backends.length > 0) {
|
|
1361
|
+
config.project = { ...config.project, backends };
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
} finally {
|
|
1365
|
+
rl.close();
|
|
1366
|
+
}
|
|
1367
|
+
await configReader.write(cwd, config);
|
|
1368
|
+
console.log(`Created ${path2.join(cwd, "nova.toml")} with default configuration.`);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// src/commands/status.ts
|
|
1372
|
+
import * as fs from "fs/promises";
|
|
1373
|
+
import * as path3 from "path";
|
|
1374
|
+
async function statusCommand() {
|
|
1375
|
+
const cwd = process.cwd();
|
|
1376
|
+
const configReader = new ConfigReader();
|
|
1377
|
+
const exists = await configReader.exists(cwd);
|
|
1378
|
+
if (!exists) {
|
|
1379
|
+
console.log('No nova.toml found. Run "nova init" to create one.');
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const config = await configReader.read(cwd);
|
|
1383
|
+
console.log("--- Nova Architect Status ---");
|
|
1384
|
+
console.log("");
|
|
1385
|
+
console.log(`Stack: provider=${config.apiKeys.provider}, fast=${config.models.fast}, strong=${config.models.strong}`);
|
|
1386
|
+
console.log(`Port: ${config.project.port}`);
|
|
1387
|
+
console.log(`Dev cmd: ${config.project.devCommand || "(not set)"}`);
|
|
1388
|
+
console.log("");
|
|
1389
|
+
const novaDir = path3.join(cwd, ".nova");
|
|
1390
|
+
let indexStatus = "not created";
|
|
1391
|
+
let pendingTasks = "none";
|
|
1392
|
+
try {
|
|
1393
|
+
await fs.stat(path3.join(novaDir, "index.json"));
|
|
1394
|
+
indexStatus = "exists";
|
|
1395
|
+
} catch {
|
|
1396
|
+
}
|
|
1397
|
+
try {
|
|
1398
|
+
const tasksRaw = await fs.readFile(path3.join(novaDir, "tasks.json"), "utf-8");
|
|
1399
|
+
const tasks = JSON.parse(tasksRaw);
|
|
1400
|
+
const pending = tasks.filter((t) => t.status === "pending");
|
|
1401
|
+
pendingTasks = pending.length > 0 ? `${pending.length} pending` : "none";
|
|
1402
|
+
} catch {
|
|
1403
|
+
}
|
|
1404
|
+
console.log(`Index: ${indexStatus}`);
|
|
1405
|
+
console.log(`Tasks: ${pendingTasks}`);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/commands/tasks.ts
|
|
1409
|
+
async function tasksCommand() {
|
|
1410
|
+
console.log("Tasks management coming soon.");
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/commands/review.ts
|
|
1414
|
+
async function reviewCommand() {
|
|
1415
|
+
console.log("Code review coming soon.");
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/commands/watch.ts
|
|
1419
|
+
async function watchCommand() {
|
|
1420
|
+
console.log("Watch mode coming soon.");
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/commands/license.ts
|
|
1424
|
+
import chalk7 from "chalk";
|
|
1425
|
+
import { LicenseChecker as LicenseChecker2, TeamDetector } from "@novastorm-ai/licensing";
|
|
1426
|
+
var KEY_PATTERN = /^NOVA-([A-Z2-7]+)-([a-f0-9]{4})$/;
|
|
1427
|
+
var VALIDATE_ENDPOINT = "https://api.nova-architect.dev/v1/license/validate";
|
|
1428
|
+
var TIMEOUT_MS = 5e3;
|
|
1429
|
+
async function licenseCommand(subcommand, key) {
|
|
1430
|
+
const cwd = process.cwd();
|
|
1431
|
+
const configReader = new ConfigReader();
|
|
1432
|
+
const config = await configReader.read(cwd);
|
|
1433
|
+
if (!subcommand || subcommand === "status") {
|
|
1434
|
+
await showStatus(cwd, config);
|
|
1435
|
+
} else if (subcommand === "activate") {
|
|
1436
|
+
if (!key) {
|
|
1437
|
+
console.error(chalk7.red("Usage: nova license activate <key>"));
|
|
1438
|
+
process.exit(1);
|
|
1439
|
+
}
|
|
1440
|
+
await activateKey(cwd, configReader, key);
|
|
1441
|
+
} else {
|
|
1442
|
+
console.error(chalk7.red(`Unknown subcommand: ${subcommand}`));
|
|
1443
|
+
console.log("Usage: nova license [status|activate <key>]");
|
|
1444
|
+
process.exit(1);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
async function showStatus(cwd, config) {
|
|
1448
|
+
const licenseChecker = new LicenseChecker2();
|
|
1449
|
+
const teamDetector = new TeamDetector();
|
|
1450
|
+
const [license, teamInfo] = await Promise.all([
|
|
1451
|
+
licenseChecker.check(cwd, config),
|
|
1452
|
+
teamDetector.detect(cwd)
|
|
1453
|
+
]);
|
|
1454
|
+
const configKey = config.license?.key;
|
|
1455
|
+
const envKey = process.env["NOVA_LICENSE_KEY"];
|
|
1456
|
+
const activeKey = configKey ?? envKey ?? null;
|
|
1457
|
+
console.log(chalk7.bold("\nNova Architect License Status\n"));
|
|
1458
|
+
console.log(` Tier: ${chalk7.cyan(license.tier)}`);
|
|
1459
|
+
console.log(` Valid: ${license.valid ? chalk7.green("yes") : chalk7.red("no")}`);
|
|
1460
|
+
console.log(` Developers: ${chalk7.cyan(String(teamInfo.devCount))} (${teamInfo.windowDays}-day window)`);
|
|
1461
|
+
console.log(` Bots filtered: ${chalk7.dim(String(teamInfo.botsFiltered))}`);
|
|
1462
|
+
console.log(` License key: ${activeKey ? chalk7.green("configured") : chalk7.dim("not set")}`);
|
|
1463
|
+
if (activeKey) {
|
|
1464
|
+
const source = configKey ? "config (nova.toml)" : "environment (NOVA_LICENSE_KEY)";
|
|
1465
|
+
console.log(` Key source: ${chalk7.dim(source)}`);
|
|
1466
|
+
}
|
|
1467
|
+
if (license.message) {
|
|
1468
|
+
console.log(`
|
|
1469
|
+
${chalk7.yellow(license.message)}`);
|
|
1470
|
+
}
|
|
1471
|
+
console.log("");
|
|
1472
|
+
}
|
|
1473
|
+
async function activateKey(cwd, configReader, key) {
|
|
1474
|
+
if (!KEY_PATTERN.test(key)) {
|
|
1475
|
+
console.error(chalk7.red("Invalid key format. Expected: NOVA-{BASE32}-{CHECKSUM}"));
|
|
1476
|
+
process.exit(1);
|
|
1477
|
+
}
|
|
1478
|
+
console.log(chalk7.dim("Validating license key..."));
|
|
1479
|
+
let serverValid = true;
|
|
1480
|
+
try {
|
|
1481
|
+
const controller = new AbortController();
|
|
1482
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
1483
|
+
try {
|
|
1484
|
+
const response = await fetch(VALIDATE_ENDPOINT, {
|
|
1485
|
+
method: "POST",
|
|
1486
|
+
headers: { "Content-Type": "application/json" },
|
|
1487
|
+
body: JSON.stringify({ key }),
|
|
1488
|
+
signal: controller.signal
|
|
1489
|
+
});
|
|
1490
|
+
if (response.ok) {
|
|
1491
|
+
const data = await response.json();
|
|
1492
|
+
serverValid = data.valid !== false;
|
|
1493
|
+
}
|
|
1494
|
+
} finally {
|
|
1495
|
+
clearTimeout(timeout);
|
|
1496
|
+
}
|
|
1497
|
+
} catch {
|
|
1498
|
+
console.log(chalk7.dim("Server unreachable, accepting key based on local validation."));
|
|
1499
|
+
}
|
|
1500
|
+
if (!serverValid) {
|
|
1501
|
+
console.error(chalk7.red("License key rejected by server."));
|
|
1502
|
+
process.exit(1);
|
|
1503
|
+
}
|
|
1504
|
+
const config = await configReader.read(cwd);
|
|
1505
|
+
await configReader.write(cwd, {
|
|
1506
|
+
...config,
|
|
1507
|
+
license: { key }
|
|
1508
|
+
});
|
|
1509
|
+
console.log(chalk7.green("License key activated and saved to nova.toml."));
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// src/commands/entity.ts
|
|
1513
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
1514
|
+
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
1515
|
+
import chalk8 from "chalk";
|
|
1516
|
+
import { ManifestStore as ManifestStore2 } from "@novastorm-ai/core";
|
|
1517
|
+
import { NovaDir as NovaDir2 } from "@novastorm-ai/core";
|
|
1518
|
+
var SERVICE_TYPES = ["frontend", "backend", "worker", "gateway"];
|
|
1519
|
+
var ENTITY_TYPES = ["module", "external-service", "library", "shared-package"];
|
|
1520
|
+
async function entityCommand(subcommand, name) {
|
|
1521
|
+
const cwd = process.cwd();
|
|
1522
|
+
const store = new ManifestStore2();
|
|
1523
|
+
const novaDir = new NovaDir2();
|
|
1524
|
+
if (!novaDir.exists(cwd)) {
|
|
1525
|
+
await novaDir.init(cwd);
|
|
1526
|
+
}
|
|
1527
|
+
switch (subcommand) {
|
|
1528
|
+
case "add":
|
|
1529
|
+
await entityAdd(cwd, store);
|
|
1530
|
+
break;
|
|
1531
|
+
case "list":
|
|
1532
|
+
await entityList(cwd, store);
|
|
1533
|
+
break;
|
|
1534
|
+
case "remove":
|
|
1535
|
+
if (!name) {
|
|
1536
|
+
console.log(chalk8.red("Usage: nova entity remove <name>"));
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
await entityRemove(cwd, store, name);
|
|
1540
|
+
break;
|
|
1541
|
+
default:
|
|
1542
|
+
console.log("Usage: nova entity <add|list|remove> [name]");
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
async function entityAdd(cwd, store) {
|
|
1547
|
+
const rl = createInterface3({ input: stdin2, output: stdout2 });
|
|
1548
|
+
try {
|
|
1549
|
+
const kindRaw = await rl.question("Type? (service / database / entity) ");
|
|
1550
|
+
const kind = kindRaw.trim().toLowerCase();
|
|
1551
|
+
if (kind === "service") {
|
|
1552
|
+
const name = (await rl.question("Name? ")).trim();
|
|
1553
|
+
if (!name) {
|
|
1554
|
+
console.log(chalk8.red("Name is required."));
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
const typeRaw = (await rl.question(`Role? (${SERVICE_TYPES.join(" / ")}) `)).trim().toLowerCase();
|
|
1558
|
+
if (!SERVICE_TYPES.includes(typeRaw)) {
|
|
1559
|
+
console.log(chalk8.red(`Invalid role. Choose from: ${SERVICE_TYPES.join(", ")}`));
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const path4 = (await rl.question("Path? ")).trim();
|
|
1563
|
+
if (!path4) {
|
|
1564
|
+
console.log(chalk8.red("Path is required."));
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const framework = (await rl.question("Framework? (optional) ")).trim() || void 0;
|
|
1568
|
+
const language = (await rl.question("Language? (optional) ")).trim() || void 0;
|
|
1569
|
+
const service = { name, type: typeRaw, path: path4, framework, language };
|
|
1570
|
+
await store.addService(cwd, service);
|
|
1571
|
+
console.log(chalk8.green(`Added service "${name}" to .nova/manifest.toml`));
|
|
1572
|
+
} else if (kind === "database") {
|
|
1573
|
+
const name = (await rl.question("Name? ")).trim();
|
|
1574
|
+
if (!name) {
|
|
1575
|
+
console.log(chalk8.red("Name is required."));
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const engine = (await rl.question("Engine? (postgresql / mysql / sqlite / mongodb / redis) ")).trim();
|
|
1579
|
+
if (!engine) {
|
|
1580
|
+
console.log(chalk8.red("Engine is required."));
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
const schemaPath = (await rl.question("Schema path? (optional) ")).trim() || void 0;
|
|
1584
|
+
const connectionEnv = (await rl.question("Connection env var? (optional) ")).trim() || void 0;
|
|
1585
|
+
await store.addDatabase(cwd, { name, engine, schema_path: schemaPath, connection_env: connectionEnv });
|
|
1586
|
+
console.log(chalk8.green(`Added database "${name}" to .nova/manifest.toml`));
|
|
1587
|
+
} else if (kind === "entity") {
|
|
1588
|
+
const name = (await rl.question("Name? ")).trim();
|
|
1589
|
+
if (!name) {
|
|
1590
|
+
console.log(chalk8.red("Name is required."));
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const typeRaw = (await rl.question(`Type? (${ENTITY_TYPES.join(" / ")}) `)).trim().toLowerCase();
|
|
1594
|
+
if (!ENTITY_TYPES.includes(typeRaw)) {
|
|
1595
|
+
console.log(chalk8.red(`Invalid type. Choose from: ${ENTITY_TYPES.join(", ")}`));
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const description = (await rl.question("Description? (optional) ")).trim() || void 0;
|
|
1599
|
+
const filesRaw = (await rl.question("Files? (comma-separated, optional) ")).trim();
|
|
1600
|
+
const files = filesRaw ? filesRaw.split(",").map((f) => f.trim()).filter(Boolean) : void 0;
|
|
1601
|
+
const entity = { name, type: typeRaw, description, files };
|
|
1602
|
+
await store.addEntity(cwd, entity);
|
|
1603
|
+
console.log(chalk8.green(`Added entity "${name}" to .nova/manifest.toml`));
|
|
1604
|
+
} else {
|
|
1605
|
+
console.log(chalk8.red("Unknown type. Choose: service, database, entity"));
|
|
1606
|
+
}
|
|
1607
|
+
} finally {
|
|
1608
|
+
rl.close();
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
async function entityList(cwd, store) {
|
|
1612
|
+
const manifest = await store.load(cwd);
|
|
1613
|
+
if (!manifest) {
|
|
1614
|
+
console.log(chalk8.yellow('No manifest found. Run "nova entity add" to create one.'));
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (manifest.project.name) {
|
|
1618
|
+
console.log(chalk8.bold(`
|
|
1619
|
+
Project: ${manifest.project.name}`));
|
|
1620
|
+
if (manifest.project.description) console.log(` ${manifest.project.description}`);
|
|
1621
|
+
}
|
|
1622
|
+
if (manifest.services.length > 0) {
|
|
1623
|
+
console.log(chalk8.bold("\nServices:"));
|
|
1624
|
+
for (const s of manifest.services) {
|
|
1625
|
+
const parts = [chalk8.cyan(s.name), `[${s.type}]`, s.path];
|
|
1626
|
+
if (s.framework) parts.push(`(${s.framework})`);
|
|
1627
|
+
console.log(` ${parts.join(" ")}`);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
if (manifest.databases.length > 0) {
|
|
1631
|
+
console.log(chalk8.bold("\nDatabases:"));
|
|
1632
|
+
for (const d of manifest.databases) {
|
|
1633
|
+
const parts = [chalk8.cyan(d.name), `[${d.engine}]`];
|
|
1634
|
+
if (d.connection_env) parts.push(`env: ${d.connection_env}`);
|
|
1635
|
+
console.log(` ${parts.join(" ")}`);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
if (manifest.entities.length > 0) {
|
|
1639
|
+
console.log(chalk8.bold("\nEntities:"));
|
|
1640
|
+
for (const e of manifest.entities) {
|
|
1641
|
+
const parts = [chalk8.cyan(e.name), `[${e.type}]`];
|
|
1642
|
+
if (e.description) parts.push(e.description);
|
|
1643
|
+
console.log(` ${parts.join(" ")}`);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
if (manifest.boundaries.writable?.length || manifest.boundaries.readonly?.length || manifest.boundaries.ignored?.length) {
|
|
1647
|
+
console.log(chalk8.bold("\nBoundaries:"));
|
|
1648
|
+
if (manifest.boundaries.writable?.length) console.log(` Writable: ${manifest.boundaries.writable.join(", ")}`);
|
|
1649
|
+
if (manifest.boundaries.readonly?.length) console.log(` Readonly: ${manifest.boundaries.readonly.join(", ")}`);
|
|
1650
|
+
if (manifest.boundaries.ignored?.length) console.log(` Ignored: ${manifest.boundaries.ignored.join(", ")}`);
|
|
1651
|
+
}
|
|
1652
|
+
const total = manifest.services.length + manifest.databases.length + manifest.entities.length;
|
|
1653
|
+
if (total === 0) {
|
|
1654
|
+
console.log(chalk8.yellow('\nManifest is empty. Run "nova entity add" to register entities.'));
|
|
1655
|
+
}
|
|
1656
|
+
console.log("");
|
|
1657
|
+
}
|
|
1658
|
+
async function entityRemove(cwd, store, name) {
|
|
1659
|
+
const removed = await store.removeByName(cwd, name);
|
|
1660
|
+
if (removed) {
|
|
1661
|
+
console.log(chalk8.green(`Removed "${name}" from .nova/manifest.toml`));
|
|
1662
|
+
} else {
|
|
1663
|
+
console.log(chalk8.yellow(`"${name}" not found in manifest.`));
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/index.ts
|
|
1668
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1669
|
+
var pkg = JSON.parse(
|
|
1670
|
+
readFileSync(resolve3(__dirname, "..", "package.json"), "utf-8")
|
|
1671
|
+
);
|
|
1672
|
+
function createCli() {
|
|
1673
|
+
const program = new Command();
|
|
1674
|
+
program.name("nova").description("Nova Architect - AI-powered site creation assistant").version(pkg.version).option("--no-telemetry", "Disable telemetry for this run");
|
|
1675
|
+
program.command("start", { isDefault: true }).description("Start Nova Architect").action(async () => {
|
|
1676
|
+
if (!program.opts().telemetry) {
|
|
1677
|
+
process.env["NOVA_TELEMETRY"] = "false";
|
|
1678
|
+
}
|
|
1679
|
+
await startCommand();
|
|
1680
|
+
});
|
|
1681
|
+
program.command("chat").description("Open interactive chat mode").action(async () => {
|
|
1682
|
+
await chatCommand();
|
|
1683
|
+
});
|
|
1684
|
+
program.command("init").description("Initialize nova.toml with default configuration").action(async () => {
|
|
1685
|
+
await initCommand();
|
|
1686
|
+
});
|
|
1687
|
+
program.command("setup").description("Run first-time interactive setup").option("-p, --provider <provider>", "AI provider: claude-cli, anthropic, openrouter, openai, ollama").option("-k, --key <key>", "API key").action(async (opts) => {
|
|
1688
|
+
if (opts.provider && (opts.key || opts.provider === "ollama" || opts.provider === "claude-cli")) {
|
|
1689
|
+
const fs2 = await import("fs/promises");
|
|
1690
|
+
const path4 = await import("path");
|
|
1691
|
+
const TOML = (await import("@iarna/toml")).default;
|
|
1692
|
+
const cwd = process.cwd();
|
|
1693
|
+
const novaDir = path4.join(cwd, ".nova");
|
|
1694
|
+
await fs2.mkdir(novaDir, { recursive: true });
|
|
1695
|
+
const config = { apiKeys: { provider: opts.provider, key: opts.key } };
|
|
1696
|
+
await fs2.writeFile(
|
|
1697
|
+
path4.join(novaDir, "config.toml"),
|
|
1698
|
+
TOML.stringify(config),
|
|
1699
|
+
"utf-8"
|
|
1700
|
+
);
|
|
1701
|
+
console.log(`Saved ${opts.provider} config to .nova/config.toml`);
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
await runSetup();
|
|
1705
|
+
});
|
|
1706
|
+
program.command("status").description("Show project status: stack, index, pending tasks").action(async () => {
|
|
1707
|
+
await statusCommand();
|
|
1708
|
+
});
|
|
1709
|
+
program.command("tasks").description("Manage tasks").action(async () => {
|
|
1710
|
+
await tasksCommand();
|
|
1711
|
+
});
|
|
1712
|
+
program.command("review").description("Run code review").action(async () => {
|
|
1713
|
+
await reviewCommand();
|
|
1714
|
+
});
|
|
1715
|
+
program.command("watch").description("Watch for file changes").action(async () => {
|
|
1716
|
+
await watchCommand();
|
|
1717
|
+
});
|
|
1718
|
+
program.command("license [subcommand] [key]").description("Manage license: nova license [status|activate <key>]").action(async (subcommand, key) => {
|
|
1719
|
+
await licenseCommand(subcommand, key);
|
|
1720
|
+
});
|
|
1721
|
+
program.command("entity [subcommand] [name]").description("Manage manifest entities: nova entity <add|list|remove> [name]").action(async (subcommand, name) => {
|
|
1722
|
+
await entityCommand(subcommand, name);
|
|
1723
|
+
});
|
|
1724
|
+
return program;
|
|
1725
|
+
}
|
|
1726
|
+
var BANNER = `\x1B[96m
|
|
1727
|
+
\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557
|
|
1728
|
+
\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551
|
|
1729
|
+
\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551
|
|
1730
|
+
\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551
|
|
1731
|
+
\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551
|
|
1732
|
+
\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
|
|
1733
|
+
\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\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
1734
|
+
\u2551 A M B I E N T D E V E L O P M E N T T O O L \u2551
|
|
1735
|
+
\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\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
1736
|
+
\x1B[0m`;
|
|
1737
|
+
async function run(argv = process.argv) {
|
|
1738
|
+
const args = argv.slice(2);
|
|
1739
|
+
const suppressBanner = args.includes("--version") || args.includes("-V") || args.includes("--help") || args.includes("-h");
|
|
1740
|
+
if (!suppressBanner) {
|
|
1741
|
+
console.log(BANNER);
|
|
1742
|
+
}
|
|
1743
|
+
const program = createCli();
|
|
1744
|
+
await program.parseAsync(argv);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
export {
|
|
1748
|
+
NovaLogger,
|
|
1749
|
+
promptAndScaffold,
|
|
1750
|
+
ErrorAutoFixer,
|
|
1751
|
+
createCli,
|
|
1752
|
+
run
|
|
1753
|
+
};
|