viagen 0.0.32 → 0.0.36
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/cli.js +274 -9
- package/dist/index.js +292 -56
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -44,6 +44,57 @@ function extractHost(httpsUrl) {
|
|
|
44
44
|
return "github.com";
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
async function waitForServer(baseUrl, devServer) {
|
|
48
|
+
const timeout = 6e4;
|
|
49
|
+
const start = Date.now();
|
|
50
|
+
const ac = new AbortController();
|
|
51
|
+
let serverOutput = "";
|
|
52
|
+
const logStream = (async () => {
|
|
53
|
+
try {
|
|
54
|
+
for await (const log of devServer.logs({ signal: ac.signal })) {
|
|
55
|
+
const line = log.data;
|
|
56
|
+
serverOutput += line;
|
|
57
|
+
if (log.stream === "stderr") {
|
|
58
|
+
process.stderr.write(` ${line}`);
|
|
59
|
+
} else {
|
|
60
|
+
process.stdout.write(` ${line}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
while (Date.now() - start < timeout) {
|
|
67
|
+
if (devServer.exitCode !== null) {
|
|
68
|
+
ac.abort();
|
|
69
|
+
await logStream.catch(() => {
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: serverOutput.slice(-2e3) || `Process exited with code ${devServer.exitCode}`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const res = await fetch(`${baseUrl}/via/health`, {
|
|
78
|
+
signal: AbortSignal.timeout(3e3)
|
|
79
|
+
});
|
|
80
|
+
if (res.ok) {
|
|
81
|
+
ac.abort();
|
|
82
|
+
await logStream.catch(() => {
|
|
83
|
+
});
|
|
84
|
+
return { ok: true };
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
89
|
+
}
|
|
90
|
+
ac.abort();
|
|
91
|
+
await logStream.catch(() => {
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: serverOutput.slice(-2e3) || "Timed out waiting for dev server"
|
|
96
|
+
};
|
|
97
|
+
}
|
|
47
98
|
async function deploySandbox(opts) {
|
|
48
99
|
const token = randomUUID();
|
|
49
100
|
const useGit = !!opts.git;
|
|
@@ -95,7 +146,11 @@ async function deploySandbox(opts) {
|
|
|
95
146
|
"user.email",
|
|
96
147
|
opts.git.userEmail
|
|
97
148
|
]);
|
|
98
|
-
await sandbox2.runCommand("git", ["checkout", "-B", opts.git.branch]);
|
|
149
|
+
const checkout = await sandbox2.runCommand("git", ["checkout", "-B", opts.git.branch]);
|
|
150
|
+
if (checkout.exitCode !== 0) {
|
|
151
|
+
const err = await checkout.stderr();
|
|
152
|
+
throw new Error(`git checkout failed (exit ${checkout.exitCode}): ${err}`);
|
|
153
|
+
}
|
|
99
154
|
await sandbox2.runCommand("bash", [
|
|
100
155
|
"-c",
|
|
101
156
|
`echo 'https://x-access-token:${opts.git.token}@${extractHost(opts.git.remoteUrl)}' > ~/.git-credentials`
|
|
@@ -157,20 +212,30 @@ async function deploySandbox(opts) {
|
|
|
157
212
|
content: Buffer.from(envLines.join("\n"))
|
|
158
213
|
}
|
|
159
214
|
]);
|
|
160
|
-
|
|
215
|
+
console.log(" Installing dependencies...");
|
|
216
|
+
const install = await sandbox2.runCommand({
|
|
217
|
+
cmd: "npm",
|
|
218
|
+
args: ["install"],
|
|
219
|
+
stdout: process.stdout,
|
|
220
|
+
stderr: process.stderr
|
|
221
|
+
});
|
|
161
222
|
if (install.exitCode !== 0) {
|
|
162
|
-
|
|
163
|
-
throw new Error(
|
|
164
|
-
`npm install failed (exit ${install.exitCode}): ${stderr}`
|
|
165
|
-
);
|
|
223
|
+
throw new Error(`npm install failed (exit ${install.exitCode})`);
|
|
166
224
|
}
|
|
167
|
-
await sandbox2.runCommand({
|
|
225
|
+
const devServer = await sandbox2.runCommand({
|
|
168
226
|
cmd: "npm",
|
|
169
227
|
args: ["run", "dev", "--", "--host", "0.0.0.0"],
|
|
170
228
|
detached: true
|
|
171
229
|
});
|
|
230
|
+
console.log(" Starting dev server...");
|
|
172
231
|
const baseUrl = sandbox2.domain(5173);
|
|
173
232
|
const url = `${baseUrl}/t/${token}`;
|
|
233
|
+
const ready = await waitForServer(baseUrl, devServer);
|
|
234
|
+
if (!ready.ok) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Dev server failed to start: ${ready.error}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
174
239
|
return {
|
|
175
240
|
url,
|
|
176
241
|
token,
|
|
@@ -816,7 +881,7 @@ function parseFlag(args, flag) {
|
|
|
816
881
|
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
817
882
|
return void 0;
|
|
818
883
|
}
|
|
819
|
-
async function sandbox(args) {
|
|
884
|
+
async function sandbox(args, options) {
|
|
820
885
|
const subcommand = args[0];
|
|
821
886
|
if (subcommand === "stop") {
|
|
822
887
|
const sandboxId = args[1];
|
|
@@ -832,9 +897,12 @@ async function sandbox(args) {
|
|
|
832
897
|
const branchOverride = parseFlag(args, "--branch") || parseFlag(args, "-b");
|
|
833
898
|
const timeoutFlag = parseFlag(args, "--timeout") || parseFlag(args, "-t");
|
|
834
899
|
const timeoutMinutes = timeoutFlag ? parseInt(timeoutFlag, 10) : void 0;
|
|
835
|
-
const prompt = parseFlag(args, "--prompt") || parseFlag(args, "-p");
|
|
900
|
+
const prompt = options?.promptOverride || parseFlag(args, "--prompt") || parseFlag(args, "-p");
|
|
836
901
|
const cwd = process.cwd();
|
|
837
902
|
const dotenv = loadDotenv(cwd);
|
|
903
|
+
if (options?.extraEnvVars) {
|
|
904
|
+
Object.assign(dotenv, options.extraEnvVars);
|
|
905
|
+
}
|
|
838
906
|
for (const [key, val] of Object.entries(dotenv)) {
|
|
839
907
|
if (!process.env[key]) process.env[key] = val;
|
|
840
908
|
}
|
|
@@ -1277,6 +1345,43 @@ async function requireClient() {
|
|
|
1277
1345
|
}
|
|
1278
1346
|
return createViagen({ baseUrl: creds.baseUrl, token: creds.token, orgId: creds.orgId });
|
|
1279
1347
|
}
|
|
1348
|
+
async function requireProjectId(client) {
|
|
1349
|
+
const cwd = process.cwd();
|
|
1350
|
+
const env = loadDotenv(cwd);
|
|
1351
|
+
let projectId = env["VIAGEN_PROJECT_ID"];
|
|
1352
|
+
if (projectId) {
|
|
1353
|
+
try {
|
|
1354
|
+
await client.projects.get(projectId);
|
|
1355
|
+
return projectId;
|
|
1356
|
+
} catch {
|
|
1357
|
+
console.log("Project from .env not found. Choose a project:");
|
|
1358
|
+
console.log("");
|
|
1359
|
+
projectId = void 0;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
const projects2 = await client.projects.list();
|
|
1363
|
+
if (projects2.length === 0) {
|
|
1364
|
+
console.error("No projects. Create one with `viagen projects create <name>` or `viagen sync`.");
|
|
1365
|
+
process.exit(1);
|
|
1366
|
+
}
|
|
1367
|
+
if (projects2.length === 1) {
|
|
1368
|
+
return projects2[0].id;
|
|
1369
|
+
}
|
|
1370
|
+
console.log("Select a project:");
|
|
1371
|
+
console.log("");
|
|
1372
|
+
for (let i = 0; i < projects2.length; i++) {
|
|
1373
|
+
const repo = projects2[i].githubRepo ? ` (${projects2[i].githubRepo})` : "";
|
|
1374
|
+
console.log(` ${i + 1}) ${projects2[i].name}${repo}`);
|
|
1375
|
+
}
|
|
1376
|
+
console.log("");
|
|
1377
|
+
const choice = await promptUser("Choose: ");
|
|
1378
|
+
const idx = parseInt(choice, 10) - 1;
|
|
1379
|
+
if (idx < 0 || idx >= projects2.length) {
|
|
1380
|
+
console.error("Invalid selection.");
|
|
1381
|
+
process.exit(1);
|
|
1382
|
+
}
|
|
1383
|
+
return projects2[idx].id;
|
|
1384
|
+
}
|
|
1280
1385
|
async function teams() {
|
|
1281
1386
|
const client = await requireClient();
|
|
1282
1387
|
const memberships = await client.orgs.list() ?? [];
|
|
@@ -1393,6 +1498,156 @@ async function projects(args) {
|
|
|
1393
1498
|
console.log(` ${p.name}${repo} ${p.id}`);
|
|
1394
1499
|
}
|
|
1395
1500
|
}
|
|
1501
|
+
function formatTaskStatus(status) {
|
|
1502
|
+
const icons = {
|
|
1503
|
+
ready: "\u25CB",
|
|
1504
|
+
running: "\u25CF",
|
|
1505
|
+
completed: "\u2713",
|
|
1506
|
+
failed: "\u2717",
|
|
1507
|
+
validating: "\u25CE"
|
|
1508
|
+
};
|
|
1509
|
+
return `${icons[status] || "?"} ${status}`;
|
|
1510
|
+
}
|
|
1511
|
+
async function tasksList(args) {
|
|
1512
|
+
const client = await requireClient();
|
|
1513
|
+
const projectId = await requireProjectId(client);
|
|
1514
|
+
const status = parseFlag(args, "--status") || parseFlag(args, "-s");
|
|
1515
|
+
const list = await client.tasks.list(projectId, status);
|
|
1516
|
+
if (list.length === 0) {
|
|
1517
|
+
console.log(
|
|
1518
|
+
status ? `No tasks with status "${status}".` : "No tasks. Create one with `viagen tasks create <prompt>`."
|
|
1519
|
+
);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
console.log(
|
|
1523
|
+
`${"ID".padEnd(10)} ${"STATUS".padEnd(14)} ${"BRANCH".padEnd(20)} ${"CREATED".padEnd(12)} PROMPT`
|
|
1524
|
+
);
|
|
1525
|
+
console.log("-".repeat(80));
|
|
1526
|
+
for (const t of list) {
|
|
1527
|
+
const id = t.id.slice(0, 8);
|
|
1528
|
+
const st = formatTaskStatus(t.status).padEnd(14);
|
|
1529
|
+
const branch = (t.branch || "-").slice(0, 18).padEnd(20);
|
|
1530
|
+
const created = t.createdAt.slice(0, 10).padEnd(12);
|
|
1531
|
+
const prompt = t.prompt.length > 40 ? t.prompt.slice(0, 37) + "..." : t.prompt;
|
|
1532
|
+
console.log(`${id.padEnd(10)} ${st} ${branch} ${created} ${prompt}`);
|
|
1533
|
+
}
|
|
1534
|
+
console.log("");
|
|
1535
|
+
console.log(`${list.length} task(s)`);
|
|
1536
|
+
}
|
|
1537
|
+
async function tasksCreate(args) {
|
|
1538
|
+
const branch = parseFlag(args, "--branch") || parseFlag(args, "-b");
|
|
1539
|
+
const positional = [];
|
|
1540
|
+
for (let i = 0; i < args.length; i++) {
|
|
1541
|
+
if (args[i] === "--branch" || args[i] === "-b") {
|
|
1542
|
+
i++;
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
positional.push(args[i]);
|
|
1546
|
+
}
|
|
1547
|
+
let prompt = positional.join(" ");
|
|
1548
|
+
if (!prompt) {
|
|
1549
|
+
prompt = await promptUser("Task prompt: ");
|
|
1550
|
+
if (!prompt) {
|
|
1551
|
+
console.log("Cancelled.");
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
const client = await requireClient();
|
|
1556
|
+
const projectId = await requireProjectId(client);
|
|
1557
|
+
const input = { prompt };
|
|
1558
|
+
if (branch) input.branch = branch;
|
|
1559
|
+
const task = await client.tasks.create(projectId, input);
|
|
1560
|
+
console.log(`Task created: ${task.id}`);
|
|
1561
|
+
console.log(` Prompt: ${task.prompt}`);
|
|
1562
|
+
console.log(` Branch: ${task.branch}`);
|
|
1563
|
+
console.log(` Status: ${task.status}`);
|
|
1564
|
+
console.log("");
|
|
1565
|
+
console.log(`Run with: viagen tasks run ${task.id}`);
|
|
1566
|
+
}
|
|
1567
|
+
async function tasksGet(args) {
|
|
1568
|
+
const taskId = args[0];
|
|
1569
|
+
if (!taskId) {
|
|
1570
|
+
console.error("Usage: viagen tasks get <id>");
|
|
1571
|
+
process.exit(1);
|
|
1572
|
+
}
|
|
1573
|
+
const client = await requireClient();
|
|
1574
|
+
const projectId = await requireProjectId(client);
|
|
1575
|
+
const task = await client.tasks.get(projectId, taskId);
|
|
1576
|
+
console.log(` ID: ${task.id}`);
|
|
1577
|
+
console.log(` Status: ${formatTaskStatus(task.status)}`);
|
|
1578
|
+
console.log(` Prompt: ${task.prompt}`);
|
|
1579
|
+
console.log(` Branch: ${task.branch}`);
|
|
1580
|
+
console.log(` Model: ${task.model}`);
|
|
1581
|
+
console.log(` Created: ${task.createdAt}`);
|
|
1582
|
+
console.log(` Created By: ${task.createdBy}`);
|
|
1583
|
+
if (task.startedAt) console.log(` Started: ${task.startedAt}`);
|
|
1584
|
+
if (task.completedAt) console.log(` Completed: ${task.completedAt}`);
|
|
1585
|
+
if (task.durationMs) {
|
|
1586
|
+
const secs = (task.durationMs / 1e3).toFixed(1);
|
|
1587
|
+
console.log(` Duration: ${secs}s`);
|
|
1588
|
+
}
|
|
1589
|
+
if (task.inputTokens || task.outputTokens) {
|
|
1590
|
+
console.log(
|
|
1591
|
+
` Tokens: ${(task.inputTokens || 0).toLocaleString()} in / ${(task.outputTokens || 0).toLocaleString()} out`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
if (task.prUrl) console.log(` PR: ${task.prUrl}`);
|
|
1595
|
+
if (task.result) console.log(` Result: ${task.result}`);
|
|
1596
|
+
if (task.error) console.log(` Error: ${task.error}`);
|
|
1597
|
+
}
|
|
1598
|
+
async function tasksRun(args) {
|
|
1599
|
+
const taskId = args[0];
|
|
1600
|
+
if (!taskId) {
|
|
1601
|
+
console.error("Usage: viagen tasks run <id>");
|
|
1602
|
+
process.exit(1);
|
|
1603
|
+
}
|
|
1604
|
+
const client = await requireClient();
|
|
1605
|
+
const projectId = await requireProjectId(client);
|
|
1606
|
+
const task = await client.tasks.get(projectId, taskId);
|
|
1607
|
+
console.log(`Running task: ${task.id}`);
|
|
1608
|
+
console.log(` Prompt: ${task.prompt}`);
|
|
1609
|
+
console.log(` Branch: ${task.branch}`);
|
|
1610
|
+
console.log("");
|
|
1611
|
+
await client.tasks.update(projectId, taskId, { status: "running" });
|
|
1612
|
+
const sandboxArgs = [];
|
|
1613
|
+
if (task.branch) {
|
|
1614
|
+
sandboxArgs.push("--branch", task.branch);
|
|
1615
|
+
}
|
|
1616
|
+
const timeout = parseFlag(args, "--timeout") || parseFlag(args, "-t");
|
|
1617
|
+
if (timeout) {
|
|
1618
|
+
sandboxArgs.push("--timeout", timeout);
|
|
1619
|
+
}
|
|
1620
|
+
try {
|
|
1621
|
+
await sandbox(sandboxArgs, {
|
|
1622
|
+
promptOverride: task.prompt,
|
|
1623
|
+
extraEnvVars: {
|
|
1624
|
+
VIAGEN_TASK_ID: task.id,
|
|
1625
|
+
VIAGEN_PROJECT_ID: projectId
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
console.error(`Sandbox deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1630
|
+
await client.tasks.update(projectId, taskId, { status: "ready" }).catch(() => {
|
|
1631
|
+
});
|
|
1632
|
+
process.exit(1);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
async function tasks(args) {
|
|
1636
|
+
const sub = args[0];
|
|
1637
|
+
if (sub === "create") {
|
|
1638
|
+
await tasksCreate(args.slice(1));
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
if (sub === "get") {
|
|
1642
|
+
await tasksGet(args.slice(1));
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
if (sub === "run") {
|
|
1646
|
+
await tasksRun(args.slice(1));
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
await tasksList(args);
|
|
1650
|
+
}
|
|
1396
1651
|
function help() {
|
|
1397
1652
|
console.log("viagen \u2014 Claude Code in your Vite dev server");
|
|
1398
1653
|
console.log("");
|
|
@@ -1411,6 +1666,10 @@ function help() {
|
|
|
1411
1666
|
console.log(" projects create <name> Create a new project");
|
|
1412
1667
|
console.log(" projects get <id> Show project details");
|
|
1413
1668
|
console.log(" projects delete <id> Delete a project");
|
|
1669
|
+
console.log(" tasks List tasks for current project");
|
|
1670
|
+
console.log(" tasks create <prompt> Create a new task");
|
|
1671
|
+
console.log(" tasks get <id> Show task details");
|
|
1672
|
+
console.log(" tasks run <id> Run a task in a sandbox");
|
|
1414
1673
|
console.log(" dev Start Vite and open the split view");
|
|
1415
1674
|
console.log(" setup Set up .env with API keys and tokens");
|
|
1416
1675
|
console.log(" sandbox [-b branch] [-t min] Deploy your project to a Vercel Sandbox");
|
|
@@ -1422,6 +1681,10 @@ function help() {
|
|
|
1422
1681
|
console.log(" -b, --branch <name> Branch to clone (default: current branch)");
|
|
1423
1682
|
console.log(" -t, --timeout <min> Sandbox timeout in minutes (default: 30)");
|
|
1424
1683
|
console.log("");
|
|
1684
|
+
console.log("Task options:");
|
|
1685
|
+
console.log(" -s, --status <status> Filter tasks by status (list)");
|
|
1686
|
+
console.log(" -b, --branch <name> Branch for the task (create)");
|
|
1687
|
+
console.log("");
|
|
1425
1688
|
console.log("Getting started:");
|
|
1426
1689
|
console.log(" 1. npm install viagen");
|
|
1427
1690
|
console.log(" 2. Add viagen() to your vite.config.ts plugins");
|
|
@@ -1483,6 +1746,8 @@ async function main() {
|
|
|
1483
1746
|
await orgs(args.slice(1));
|
|
1484
1747
|
} else if (command === "projects") {
|
|
1485
1748
|
await projects(args.slice(1));
|
|
1749
|
+
} else if (command === "tasks") {
|
|
1750
|
+
await tasks(args.slice(1));
|
|
1486
1751
|
} else if (command === "dev") {
|
|
1487
1752
|
dev();
|
|
1488
1753
|
return;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { execSync as execSync2 } from "child_process";
|
|
3
3
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
4
|
-
import { join as
|
|
4
|
+
import { join as join7 } from "path";
|
|
5
5
|
import { loadEnv } from "vite";
|
|
6
6
|
|
|
7
7
|
// src/logger.ts
|
|
@@ -59,6 +59,8 @@ function wrapLogger(logger, buffer) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// src/health.ts
|
|
62
|
+
import { readFileSync } from "fs";
|
|
63
|
+
import { join as join2 } from "path";
|
|
62
64
|
function registerHealthRoutes(server, env, errorRef) {
|
|
63
65
|
server.middlewares.use("/via/error", (_req, res) => {
|
|
64
66
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -84,20 +86,81 @@ function registerHealthRoutes(server, env, errorRef) {
|
|
|
84
86
|
timeoutSeconds: sessionTimeout
|
|
85
87
|
} : null;
|
|
86
88
|
const prompt = env["VIAGEN_PROMPT"] || null;
|
|
89
|
+
const taskId = env["VIAGEN_TASK_ID"] || null;
|
|
90
|
+
const projectId = env["VIAGEN_PROJECT_ID"] || null;
|
|
87
91
|
res.setHeader("Content-Type", "application/json");
|
|
88
92
|
if (configured) {
|
|
89
|
-
res.end(JSON.stringify({ status: "ok", configured: true, git, vercel, branch, session, prompt, missing }));
|
|
93
|
+
res.end(JSON.stringify({ status: "ok", configured: true, git, vercel, branch, session, prompt, taskId, projectId, missing }));
|
|
90
94
|
} else {
|
|
91
95
|
res.end(
|
|
92
|
-
JSON.stringify({ status: "error", configured: false, git, vercel, branch, session, prompt, missing })
|
|
96
|
+
JSON.stringify({ status: "error", configured: false, git, vercel, branch, session, prompt, taskId, projectId, missing })
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
let currentVersion = null;
|
|
101
|
+
let versionCache = null;
|
|
102
|
+
function getCurrentVersion() {
|
|
103
|
+
if (currentVersion) return currentVersion;
|
|
104
|
+
try {
|
|
105
|
+
const pkg = JSON.parse(
|
|
106
|
+
readFileSync(join2(__dirname, "..", "package.json"), "utf-8")
|
|
107
|
+
);
|
|
108
|
+
currentVersion = pkg.version;
|
|
109
|
+
} catch {
|
|
110
|
+
try {
|
|
111
|
+
const pkg = JSON.parse(
|
|
112
|
+
readFileSync(
|
|
113
|
+
join2(__dirname, "package.json"),
|
|
114
|
+
"utf-8"
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
currentVersion = pkg.version;
|
|
118
|
+
} catch {
|
|
119
|
+
currentVersion = "0.0.0";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return currentVersion;
|
|
123
|
+
}
|
|
124
|
+
server.middlewares.use("/via/version", (_req, res) => {
|
|
125
|
+
res.setHeader("Content-Type", "application/json");
|
|
126
|
+
const current = getCurrentVersion();
|
|
127
|
+
if (versionCache && Date.now() - versionCache.ts < 3e5) {
|
|
128
|
+
res.end(
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
current,
|
|
131
|
+
latest: versionCache.latest,
|
|
132
|
+
updateAvailable: versionCache.latest !== current
|
|
133
|
+
})
|
|
93
134
|
);
|
|
135
|
+
return;
|
|
94
136
|
}
|
|
137
|
+
fetch("https://registry.npmjs.org/viagen/latest", {
|
|
138
|
+
headers: { Accept: "application/json" }
|
|
139
|
+
}).then((r) => r.json()).then((data) => {
|
|
140
|
+
const latest = data.version ?? current;
|
|
141
|
+
versionCache = { latest, ts: Date.now() };
|
|
142
|
+
res.end(
|
|
143
|
+
JSON.stringify({
|
|
144
|
+
current,
|
|
145
|
+
latest,
|
|
146
|
+
updateAvailable: latest !== current
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
}).catch(() => {
|
|
150
|
+
res.end(
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
current,
|
|
153
|
+
latest: null,
|
|
154
|
+
updateAvailable: false
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
});
|
|
95
158
|
});
|
|
96
159
|
}
|
|
97
160
|
|
|
98
161
|
// src/chat.ts
|
|
99
|
-
import { readFileSync, writeFileSync as writeFileSync2, appendFileSync, existsSync } from "fs";
|
|
100
|
-
import { join as
|
|
162
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, appendFileSync, existsSync } from "fs";
|
|
163
|
+
import { join as join3 } from "path";
|
|
101
164
|
import {
|
|
102
165
|
query
|
|
103
166
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -197,7 +260,7 @@ var ChatSession = class {
|
|
|
197
260
|
lastUsedToken;
|
|
198
261
|
constructor(opts) {
|
|
199
262
|
this.opts = opts;
|
|
200
|
-
this.chatLogPath =
|
|
263
|
+
this.chatLogPath = join3(opts.projectRoot, ".viagen", "chat.log");
|
|
201
264
|
}
|
|
202
265
|
reset() {
|
|
203
266
|
this.sessionId = void 0;
|
|
@@ -208,7 +271,7 @@ var ChatSession = class {
|
|
|
208
271
|
}
|
|
209
272
|
getHistory() {
|
|
210
273
|
try {
|
|
211
|
-
const raw =
|
|
274
|
+
const raw = readFileSync2(this.chatLogPath, "utf-8");
|
|
212
275
|
const entries = [];
|
|
213
276
|
for (const line of raw.split("\n")) {
|
|
214
277
|
if (!line.trim()) continue;
|
|
@@ -245,9 +308,9 @@ var ChatSession = class {
|
|
|
245
308
|
this.opts.env["CLAUDE_TOKEN_EXPIRES"] = String(
|
|
246
309
|
nowSec + tokens.expires_in
|
|
247
310
|
);
|
|
248
|
-
const envPath =
|
|
311
|
+
const envPath = join3(this.opts.projectRoot, ".env");
|
|
249
312
|
if (existsSync(envPath)) {
|
|
250
|
-
let content =
|
|
313
|
+
let content = readFileSync2(envPath, "utf-8");
|
|
251
314
|
const replacements = {
|
|
252
315
|
CLAUDE_ACCESS_TOKEN: tokens.access_token,
|
|
253
316
|
CLAUDE_REFRESH_TOKEN: tokens.refresh_token,
|
|
@@ -414,7 +477,19 @@ var ChatSession = class {
|
|
|
414
477
|
sink?.({ type: "error", text: err });
|
|
415
478
|
}
|
|
416
479
|
}
|
|
417
|
-
|
|
480
|
+
const doneEvent = { type: "done" };
|
|
481
|
+
if ("total_cost_usd" in msg) {
|
|
482
|
+
doneEvent.costUsd = msg.total_cost_usd;
|
|
483
|
+
}
|
|
484
|
+
if ("duration_ms" in msg) {
|
|
485
|
+
doneEvent.durationMs = msg.duration_ms;
|
|
486
|
+
}
|
|
487
|
+
if ("usage" in msg && msg.usage) {
|
|
488
|
+
const u = msg.usage;
|
|
489
|
+
doneEvent.inputTokens = u.input_tokens ?? 0;
|
|
490
|
+
doneEvent.outputTokens = u.output_tokens ?? 0;
|
|
491
|
+
}
|
|
492
|
+
sink?.(doneEvent);
|
|
418
493
|
this.currentDoneResolve?.();
|
|
419
494
|
this.currentDoneResolve = null;
|
|
420
495
|
this.currentEventSink = null;
|
|
@@ -1374,6 +1449,30 @@ function buildUiHtml(opts) {
|
|
|
1374
1449
|
color: #d4d4d8;
|
|
1375
1450
|
font-size: 11px;
|
|
1376
1451
|
}
|
|
1452
|
+
.update-banner {
|
|
1453
|
+
padding: 6px 12px;
|
|
1454
|
+
border-bottom: 1px solid #27272a;
|
|
1455
|
+
background: #18181b;
|
|
1456
|
+
font-family: ui-monospace, monospace;
|
|
1457
|
+
font-size: 11px;
|
|
1458
|
+
color: #a1a1aa;
|
|
1459
|
+
flex-shrink: 0;
|
|
1460
|
+
display: none;
|
|
1461
|
+
align-items: center;
|
|
1462
|
+
gap: 8px;
|
|
1463
|
+
cursor: pointer;
|
|
1464
|
+
transition: background 0.15s;
|
|
1465
|
+
}
|
|
1466
|
+
.update-banner:hover { background: #1e1e22; }
|
|
1467
|
+
.update-banner .update-badge {
|
|
1468
|
+
font-size: 9px;
|
|
1469
|
+
font-weight: 700;
|
|
1470
|
+
padding: 1px 5px;
|
|
1471
|
+
border-radius: 3px;
|
|
1472
|
+
background: #365314;
|
|
1473
|
+
color: #86efac;
|
|
1474
|
+
text-transform: uppercase;
|
|
1475
|
+
}
|
|
1377
1476
|
.btn {
|
|
1378
1477
|
padding: 5px 10px;
|
|
1379
1478
|
border: 1px solid #3f3f46;
|
|
@@ -1800,13 +1899,14 @@ function buildUiHtml(opts) {
|
|
|
1800
1899
|
</div>` : ""}
|
|
1801
1900
|
<div id="chat-view" style="display:flex;flex-direction:column;flex:1;overflow:hidden;">
|
|
1802
1901
|
<div class="setup-banner" id="setup-banner"></div>
|
|
1902
|
+
<div class="update-banner" id="update-banner"><span class="update-badge">update</span><span id="update-text"></span></div>
|
|
1803
1903
|
<div class="activity-bar" id="activity-bar"></div>
|
|
1804
1904
|
<div class="messages" id="messages"></div>
|
|
1805
1905
|
<div class="input-area">
|
|
1806
1906
|
<input type="text" id="input" placeholder="What do you want to build?" autofocus />
|
|
1807
1907
|
<button class="send-btn" id="send-btn">Send</button>
|
|
1808
1908
|
</div>
|
|
1809
|
-
|
|
1909
|
+
<div class="status-bar" id="status-bar">${hasGit ? '<span id="status-branch"></span><span id="status-diff"></span>' : ""}<span id="status-cost" style="display:none;margin-left:auto;"></span></div>
|
|
1810
1910
|
</div>
|
|
1811
1911
|
${editor ? editor.html : ""}
|
|
1812
1912
|
${hasGit ? `<div id="changes-view" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
|
|
@@ -1895,6 +1995,12 @@ function buildUiHtml(opts) {
|
|
|
1895
1995
|
} catch(e) {}
|
|
1896
1996
|
}
|
|
1897
1997
|
|
|
1998
|
+
function scrollToBottom() {
|
|
1999
|
+
requestAnimationFrame(function() {
|
|
2000
|
+
scrollToBottom();
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1898
2004
|
function formatDuration(ms) {
|
|
1899
2005
|
if (ms < 1000) return ms + 'ms';
|
|
1900
2006
|
var secs = Math.round(ms / 1000);
|
|
@@ -1919,14 +2025,41 @@ function buildUiHtml(opts) {
|
|
|
1919
2025
|
activityTimer = setInterval(updateActivityBar, 1000);
|
|
1920
2026
|
}
|
|
1921
2027
|
|
|
1922
|
-
|
|
2028
|
+
var sessionCostUsd = 0;
|
|
2029
|
+
var sessionInputTokens = 0;
|
|
2030
|
+
var sessionOutputTokens = 0;
|
|
2031
|
+
|
|
2032
|
+
function formatCost(usd) {
|
|
2033
|
+
if (usd < 0.01) return '<$0.01';
|
|
2034
|
+
return '$' + usd.toFixed(2);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function formatTokens(n) {
|
|
2038
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
2039
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
2040
|
+
return String(n);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
function hideActivity(usage) {
|
|
1923
2044
|
if (activityTimer) { clearInterval(activityTimer); activityTimer = null; }
|
|
1924
2045
|
var elapsed = formatDuration(Date.now() - sendStartTime);
|
|
1925
2046
|
var parts = ['Done in ' + elapsed];
|
|
1926
2047
|
if (toolCount > 0) parts.push(toolCount + (toolCount === 1 ? ' action' : ' actions'));
|
|
1927
|
-
|
|
2048
|
+
if (usage && usage.costUsd != null) {
|
|
2049
|
+
parts.push(formatCost(usage.costUsd));
|
|
2050
|
+
sessionCostUsd += usage.costUsd;
|
|
2051
|
+
sessionInputTokens += (usage.inputTokens || 0);
|
|
2052
|
+
sessionOutputTokens += (usage.outputTokens || 0);
|
|
2053
|
+
}
|
|
2054
|
+
activityBar.textContent = parts.join(' \\u00b7 ');
|
|
1928
2055
|
activityBar.classList.add('done');
|
|
1929
2056
|
setTimeout(function() { activityBar.style.display = 'none'; }, 5000);
|
|
2057
|
+
// Update status bar with session total
|
|
2058
|
+
var costEl = document.getElementById('status-cost');
|
|
2059
|
+
if (costEl && sessionCostUsd > 0) {
|
|
2060
|
+
costEl.textContent = formatCost(sessionCostUsd) + ' (' + formatTokens(sessionInputTokens + sessionOutputTokens) + ' tokens)';
|
|
2061
|
+
costEl.style.display = '';
|
|
2062
|
+
}
|
|
1930
2063
|
}
|
|
1931
2064
|
window.addEventListener('beforeunload', function() { unloading = true; stopHistoryPolling(); });
|
|
1932
2065
|
window.addEventListener('pagehide', function() { unloading = true; });
|
|
@@ -2110,7 +2243,7 @@ function buildUiHtml(opts) {
|
|
|
2110
2243
|
chatLog.push({ type: 'user', content: text });
|
|
2111
2244
|
|
|
2112
2245
|
renderUserMessage(text);
|
|
2113
|
-
|
|
2246
|
+
scrollToBottom();
|
|
2114
2247
|
}
|
|
2115
2248
|
|
|
2116
2249
|
function appendText(text) {
|
|
@@ -2131,7 +2264,7 @@ function buildUiHtml(opts) {
|
|
|
2131
2264
|
}
|
|
2132
2265
|
var fullText = chatLog[chatLog.length - 1].content;
|
|
2133
2266
|
currentTextSpan.innerHTML = renderMarkdown(fullText);
|
|
2134
|
-
|
|
2267
|
+
scrollToBottom();
|
|
2135
2268
|
}
|
|
2136
2269
|
|
|
2137
2270
|
function addToolBlock(name, input) {
|
|
@@ -2140,7 +2273,7 @@ function buildUiHtml(opts) {
|
|
|
2140
2273
|
chatLog.push({ type: 'tool', content: label });
|
|
2141
2274
|
|
|
2142
2275
|
renderToolBlock(label);
|
|
2143
|
-
|
|
2276
|
+
scrollToBottom();
|
|
2144
2277
|
}
|
|
2145
2278
|
|
|
2146
2279
|
function renderToolResult(text) {
|
|
@@ -2167,7 +2300,7 @@ function buildUiHtml(opts) {
|
|
|
2167
2300
|
chatLog.push({ type: 'error', content: text });
|
|
2168
2301
|
|
|
2169
2302
|
renderErrorBlock(text);
|
|
2170
|
-
|
|
2303
|
+
scrollToBottom();
|
|
2171
2304
|
}
|
|
2172
2305
|
|
|
2173
2306
|
function setStreaming(v) {
|
|
@@ -2179,18 +2312,7 @@ function buildUiHtml(opts) {
|
|
|
2179
2312
|
else startHistoryPolling();
|
|
2180
2313
|
}
|
|
2181
2314
|
|
|
2182
|
-
async function
|
|
2183
|
-
var text = inputEl.value.trim();
|
|
2184
|
-
if (!text || isStreaming) return;
|
|
2185
|
-
|
|
2186
|
-
addUserMessage(text);
|
|
2187
|
-
inputEl.value = '';
|
|
2188
|
-
setStreaming(true);
|
|
2189
|
-
currentTextSpan = null;
|
|
2190
|
-
sendStartTime = Date.now();
|
|
2191
|
-
toolCount = 0;
|
|
2192
|
-
showActivity();
|
|
2193
|
-
|
|
2315
|
+
async function sendRaw(text) {
|
|
2194
2316
|
try {
|
|
2195
2317
|
var res = await fetch('/via/chat', {
|
|
2196
2318
|
method: 'POST',
|
|
@@ -2201,6 +2323,7 @@ function buildUiHtml(opts) {
|
|
|
2201
2323
|
var reader = res.body.getReader();
|
|
2202
2324
|
var decoder = new TextDecoder();
|
|
2203
2325
|
var buffer = '';
|
|
2326
|
+
var lastUsage = null;
|
|
2204
2327
|
|
|
2205
2328
|
while (true) {
|
|
2206
2329
|
var result = await reader.read();
|
|
@@ -2218,6 +2341,7 @@ function buildUiHtml(opts) {
|
|
|
2218
2341
|
else if (data.type === 'tool_use') { toolCount++; updateActivityBar(); addToolBlock(data.name, data.input); }
|
|
2219
2342
|
else if (data.type === 'tool_result') addToolResult(data.text);
|
|
2220
2343
|
else if (data.type === 'error') addErrorBlock(data.text);
|
|
2344
|
+
else if (data.type === 'done') lastUsage = data;
|
|
2221
2345
|
} catch (e) {}
|
|
2222
2346
|
}
|
|
2223
2347
|
}
|
|
@@ -2225,14 +2349,29 @@ function buildUiHtml(opts) {
|
|
|
2225
2349
|
if (!unloading) addErrorBlock('Connection failed');
|
|
2226
2350
|
}
|
|
2227
2351
|
|
|
2228
|
-
hideActivity();
|
|
2352
|
+
hideActivity(lastUsage);
|
|
2229
2353
|
playDoneSound();
|
|
2230
|
-
// Advance timestamp so polling doesn't re-render messages from this stream
|
|
2231
2354
|
historyTimestamp = Date.now();
|
|
2232
2355
|
setStreaming(false);
|
|
2233
2356
|
inputEl.focus();
|
|
2234
2357
|
}
|
|
2235
2358
|
|
|
2359
|
+
async function send() {
|
|
2360
|
+
var text = inputEl.value.trim();
|
|
2361
|
+
if (!text || isStreaming) return;
|
|
2362
|
+
|
|
2363
|
+
addUserMessage(text);
|
|
2364
|
+
inputEl.value = '';
|
|
2365
|
+
setStreaming(true);
|
|
2366
|
+
currentTextSpan = null;
|
|
2367
|
+
sendStartTime = Date.now();
|
|
2368
|
+
toolCount = 0;
|
|
2369
|
+
showActivity();
|
|
2370
|
+
scrollToBottom();
|
|
2371
|
+
|
|
2372
|
+
await sendRaw(text);
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2236
2375
|
sendBtn.addEventListener('click', send);
|
|
2237
2376
|
inputEl.addEventListener('keydown', function (e) {
|
|
2238
2377
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
|
|
@@ -2383,7 +2522,6 @@ function buildUiHtml(opts) {
|
|
|
2383
2522
|
var statusBar = document.getElementById('status-bar');
|
|
2384
2523
|
var statusBranch = document.getElementById('status-branch');
|
|
2385
2524
|
if (statusBar && statusBranch) {
|
|
2386
|
-
statusBar.style.display = 'flex';
|
|
2387
2525
|
statusBranch.textContent = '\\u2387 ' + d.branch;
|
|
2388
2526
|
if (branchUrl) {
|
|
2389
2527
|
statusBranch.addEventListener('click', function() { window.open(branchUrl, '_blank'); });
|
|
@@ -2416,10 +2554,43 @@ function buildUiHtml(opts) {
|
|
|
2416
2554
|
}).catch(function() {});
|
|
2417
2555
|
}
|
|
2418
2556
|
|
|
2557
|
+
// Check for viagen updates
|
|
2558
|
+
fetch('/via/version').then(function(r) { return r.json(); }).then(function(v) {
|
|
2559
|
+
if (v.updateAvailable && v.latest) {
|
|
2560
|
+
var updateBanner = document.getElementById('update-banner');
|
|
2561
|
+
var updateText = document.getElementById('update-text');
|
|
2562
|
+
if (updateBanner && updateText) {
|
|
2563
|
+
updateText.textContent = 'viagen ' + v.latest + ' available (current: ' + v.current + ')';
|
|
2564
|
+
updateBanner.style.display = 'flex';
|
|
2565
|
+
updateBanner.addEventListener('click', function() {
|
|
2566
|
+
updateBanner.style.display = 'none';
|
|
2567
|
+
inputEl.value = 'Update viagen to v' + v.latest + ' (npm install viagen@' + v.latest + ') and restart the dev server.';
|
|
2568
|
+
send();
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}).catch(function() {});
|
|
2573
|
+
|
|
2419
2574
|
// Only auto-send prompt if no history exists (first boot)
|
|
2420
2575
|
if (data.prompt && data.configured && chatLog.length === 0) {
|
|
2421
|
-
|
|
2422
|
-
|
|
2576
|
+
if (data.taskId) {
|
|
2577
|
+
// Task mode: show link instead of raw prompt
|
|
2578
|
+
var taskUrl = 'https://app.viagen.dev' + (data.projectId ? '/' + data.projectId : '') + '/' + data.taskId;
|
|
2579
|
+
var div = document.createElement('div');
|
|
2580
|
+
div.className = 'msg msg-user';
|
|
2581
|
+
div.innerHTML = '<span class="label">Task</span><span class="text">Received instructions from <a href="' + escapeHtml(taskUrl) + '" target="_blank" style="color:#93c5fd;text-decoration:underline;">Viagen Task</a></span>';
|
|
2582
|
+
messagesEl.appendChild(div);
|
|
2583
|
+
scrollToBottom();
|
|
2584
|
+
// Send the prompt silently (don't show it as a user message)
|
|
2585
|
+
showActivity();
|
|
2586
|
+
setStreaming(true);
|
|
2587
|
+
sendStartTime = Date.now();
|
|
2588
|
+
toolCount = 0;
|
|
2589
|
+
sendRaw(data.prompt);
|
|
2590
|
+
} else {
|
|
2591
|
+
inputEl.value = data.prompt;
|
|
2592
|
+
send();
|
|
2593
|
+
}
|
|
2423
2594
|
}
|
|
2424
2595
|
})
|
|
2425
2596
|
.catch(function() {
|
|
@@ -2804,11 +2975,11 @@ function createAuthMiddleware(token) {
|
|
|
2804
2975
|
// src/files.ts
|
|
2805
2976
|
import {
|
|
2806
2977
|
readdirSync,
|
|
2807
|
-
readFileSync as
|
|
2978
|
+
readFileSync as readFileSync3,
|
|
2808
2979
|
writeFileSync as writeFileSync3,
|
|
2809
2980
|
statSync
|
|
2810
2981
|
} from "fs";
|
|
2811
|
-
import { join as
|
|
2982
|
+
import { join as join4, resolve, relative } from "path";
|
|
2812
2983
|
function readBody2(req) {
|
|
2813
2984
|
return new Promise((resolve3, reject) => {
|
|
2814
2985
|
let body = "";
|
|
@@ -2823,7 +2994,7 @@ function collectFiles(dir, projectRoot) {
|
|
|
2823
2994
|
const results = [];
|
|
2824
2995
|
try {
|
|
2825
2996
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2826
|
-
const fullPath =
|
|
2997
|
+
const fullPath = join4(dir, entry.name);
|
|
2827
2998
|
if (entry.isDirectory()) {
|
|
2828
2999
|
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
2829
3000
|
results.push(...collectFiles(fullPath, projectRoot));
|
|
@@ -2911,7 +3082,7 @@ function registerFileRoutes(server, opts) {
|
|
|
2911
3082
|
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
|
2912
3083
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2913
3084
|
try {
|
|
2914
|
-
const data =
|
|
3085
|
+
const data = readFileSync3(abs);
|
|
2915
3086
|
res.setHeader("Content-Type", mime);
|
|
2916
3087
|
res.setHeader("Cache-Control", "no-cache");
|
|
2917
3088
|
res.end(data);
|
|
@@ -2949,7 +3120,7 @@ function registerFileRoutes(server, opts) {
|
|
|
2949
3120
|
}
|
|
2950
3121
|
const abs = resolve(opts.projectRoot, filePath);
|
|
2951
3122
|
try {
|
|
2952
|
-
const content =
|
|
3123
|
+
const content = readFileSync3(abs, "utf-8");
|
|
2953
3124
|
res.setHeader("Content-Type", "application/json");
|
|
2954
3125
|
res.end(JSON.stringify({ path: filePath, content }));
|
|
2955
3126
|
} catch {
|
|
@@ -3057,8 +3228,8 @@ function createInjectionMiddleware() {
|
|
|
3057
3228
|
}
|
|
3058
3229
|
|
|
3059
3230
|
// src/git.ts
|
|
3060
|
-
import { readFileSync as
|
|
3061
|
-
import { join as
|
|
3231
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3232
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
3062
3233
|
import {
|
|
3063
3234
|
simpleGit
|
|
3064
3235
|
} from "simple-git";
|
|
@@ -3101,7 +3272,7 @@ async function getDiffStats(git, repoRoot, untrackedFiles) {
|
|
|
3101
3272
|
}
|
|
3102
3273
|
for (const filePath of untrackedFiles) {
|
|
3103
3274
|
try {
|
|
3104
|
-
const content =
|
|
3275
|
+
const content = readFileSync4(join5(repoRoot, filePath), "utf-8");
|
|
3105
3276
|
const lines = content.split("\n").length;
|
|
3106
3277
|
stats.set(filePath, { ins: lines, del: 0 });
|
|
3107
3278
|
} catch {
|
|
@@ -3127,7 +3298,7 @@ async function getFileDiff(git, repoRoot, filePath) {
|
|
|
3127
3298
|
if (staged) return staged;
|
|
3128
3299
|
if (unstaged) return unstaged;
|
|
3129
3300
|
try {
|
|
3130
|
-
const content =
|
|
3301
|
+
const content = readFileSync4(join5(repoRoot, filePath), "utf-8");
|
|
3131
3302
|
const lines = content.split("\n");
|
|
3132
3303
|
const added = lines.map((l) => `+${l}`).join("\n");
|
|
3133
3304
|
return `--- /dev/null
|
|
@@ -3332,8 +3503,8 @@ function registerLogRoutes(server, opts) {
|
|
|
3332
3503
|
|
|
3333
3504
|
// src/sandbox.ts
|
|
3334
3505
|
import { randomUUID } from "crypto";
|
|
3335
|
-
import { readFileSync as
|
|
3336
|
-
import { join as
|
|
3506
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
|
|
3507
|
+
import { join as join6, relative as relative2 } from "path";
|
|
3337
3508
|
import { Sandbox } from "@vercel/sandbox";
|
|
3338
3509
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
3339
3510
|
"node_modules",
|
|
@@ -3349,13 +3520,13 @@ function collectFiles2(dir, base) {
|
|
|
3349
3520
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
3350
3521
|
if (entry.name.startsWith(".") && SKIP_DIRS.has(entry.name)) continue;
|
|
3351
3522
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
3352
|
-
const fullPath =
|
|
3523
|
+
const fullPath = join6(dir, entry.name);
|
|
3353
3524
|
const relPath = relative2(base, fullPath);
|
|
3354
3525
|
if (entry.isDirectory()) {
|
|
3355
3526
|
files.push(...collectFiles2(fullPath, base));
|
|
3356
3527
|
} else if (entry.isFile()) {
|
|
3357
3528
|
if (SKIP_FILES.has(entry.name)) continue;
|
|
3358
|
-
files.push({ path: relPath, content:
|
|
3529
|
+
files.push({ path: relPath, content: readFileSync5(fullPath) });
|
|
3359
3530
|
}
|
|
3360
3531
|
}
|
|
3361
3532
|
return files;
|
|
@@ -3367,6 +3538,57 @@ function extractHost(httpsUrl) {
|
|
|
3367
3538
|
return "github.com";
|
|
3368
3539
|
}
|
|
3369
3540
|
}
|
|
3541
|
+
async function waitForServer(baseUrl, devServer) {
|
|
3542
|
+
const timeout = 6e4;
|
|
3543
|
+
const start = Date.now();
|
|
3544
|
+
const ac = new AbortController();
|
|
3545
|
+
let serverOutput = "";
|
|
3546
|
+
const logStream = (async () => {
|
|
3547
|
+
try {
|
|
3548
|
+
for await (const log of devServer.logs({ signal: ac.signal })) {
|
|
3549
|
+
const line = log.data;
|
|
3550
|
+
serverOutput += line;
|
|
3551
|
+
if (log.stream === "stderr") {
|
|
3552
|
+
process.stderr.write(` ${line}`);
|
|
3553
|
+
} else {
|
|
3554
|
+
process.stdout.write(` ${line}`);
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
} catch {
|
|
3558
|
+
}
|
|
3559
|
+
})();
|
|
3560
|
+
while (Date.now() - start < timeout) {
|
|
3561
|
+
if (devServer.exitCode !== null) {
|
|
3562
|
+
ac.abort();
|
|
3563
|
+
await logStream.catch(() => {
|
|
3564
|
+
});
|
|
3565
|
+
return {
|
|
3566
|
+
ok: false,
|
|
3567
|
+
error: serverOutput.slice(-2e3) || `Process exited with code ${devServer.exitCode}`
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
try {
|
|
3571
|
+
const res = await fetch(`${baseUrl}/via/health`, {
|
|
3572
|
+
signal: AbortSignal.timeout(3e3)
|
|
3573
|
+
});
|
|
3574
|
+
if (res.ok) {
|
|
3575
|
+
ac.abort();
|
|
3576
|
+
await logStream.catch(() => {
|
|
3577
|
+
});
|
|
3578
|
+
return { ok: true };
|
|
3579
|
+
}
|
|
3580
|
+
} catch {
|
|
3581
|
+
}
|
|
3582
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
3583
|
+
}
|
|
3584
|
+
ac.abort();
|
|
3585
|
+
await logStream.catch(() => {
|
|
3586
|
+
});
|
|
3587
|
+
return {
|
|
3588
|
+
ok: false,
|
|
3589
|
+
error: serverOutput.slice(-2e3) || "Timed out waiting for dev server"
|
|
3590
|
+
};
|
|
3591
|
+
}
|
|
3370
3592
|
async function deploySandbox(opts) {
|
|
3371
3593
|
const token = randomUUID();
|
|
3372
3594
|
const useGit = !!opts.git;
|
|
@@ -3418,7 +3640,11 @@ async function deploySandbox(opts) {
|
|
|
3418
3640
|
"user.email",
|
|
3419
3641
|
opts.git.userEmail
|
|
3420
3642
|
]);
|
|
3421
|
-
await sandbox.runCommand("git", ["checkout", "-B", opts.git.branch]);
|
|
3643
|
+
const checkout = await sandbox.runCommand("git", ["checkout", "-B", opts.git.branch]);
|
|
3644
|
+
if (checkout.exitCode !== 0) {
|
|
3645
|
+
const err = await checkout.stderr();
|
|
3646
|
+
throw new Error(`git checkout failed (exit ${checkout.exitCode}): ${err}`);
|
|
3647
|
+
}
|
|
3422
3648
|
await sandbox.runCommand("bash", [
|
|
3423
3649
|
"-c",
|
|
3424
3650
|
`echo 'https://x-access-token:${opts.git.token}@${extractHost(opts.git.remoteUrl)}' > ~/.git-credentials`
|
|
@@ -3480,20 +3706,30 @@ async function deploySandbox(opts) {
|
|
|
3480
3706
|
content: Buffer.from(envLines.join("\n"))
|
|
3481
3707
|
}
|
|
3482
3708
|
]);
|
|
3483
|
-
|
|
3709
|
+
console.log(" Installing dependencies...");
|
|
3710
|
+
const install = await sandbox.runCommand({
|
|
3711
|
+
cmd: "npm",
|
|
3712
|
+
args: ["install"],
|
|
3713
|
+
stdout: process.stdout,
|
|
3714
|
+
stderr: process.stderr
|
|
3715
|
+
});
|
|
3484
3716
|
if (install.exitCode !== 0) {
|
|
3485
|
-
|
|
3486
|
-
throw new Error(
|
|
3487
|
-
`npm install failed (exit ${install.exitCode}): ${stderr}`
|
|
3488
|
-
);
|
|
3717
|
+
throw new Error(`npm install failed (exit ${install.exitCode})`);
|
|
3489
3718
|
}
|
|
3490
|
-
await sandbox.runCommand({
|
|
3719
|
+
const devServer = await sandbox.runCommand({
|
|
3491
3720
|
cmd: "npm",
|
|
3492
3721
|
args: ["run", "dev", "--", "--host", "0.0.0.0"],
|
|
3493
3722
|
detached: true
|
|
3494
3723
|
});
|
|
3724
|
+
console.log(" Starting dev server...");
|
|
3495
3725
|
const baseUrl = sandbox.domain(5173);
|
|
3496
3726
|
const url = `${baseUrl}/t/${token}`;
|
|
3727
|
+
const ready = await waitForServer(baseUrl, devServer);
|
|
3728
|
+
if (!ready.ok) {
|
|
3729
|
+
throw new Error(
|
|
3730
|
+
`Dev server failed to start: ${ready.error}`
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3497
3733
|
return {
|
|
3498
3734
|
url,
|
|
3499
3735
|
token,
|
|
@@ -3535,10 +3771,10 @@ function viagen(options) {
|
|
|
3535
3771
|
projectRoot = config.root;
|
|
3536
3772
|
logBuffer.init(projectRoot);
|
|
3537
3773
|
wrapLogger(config.logger, logBuffer);
|
|
3538
|
-
const viagenDir =
|
|
3774
|
+
const viagenDir = join7(projectRoot, ".viagen");
|
|
3539
3775
|
mkdirSync2(viagenDir, { recursive: true });
|
|
3540
3776
|
writeFileSync4(
|
|
3541
|
-
|
|
3777
|
+
join7(viagenDir, "config.json"),
|
|
3542
3778
|
JSON.stringify({
|
|
3543
3779
|
sandboxFiles: options?.sandboxFiles ?? [],
|
|
3544
3780
|
editable: options?.editable ?? []
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viagen",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.36",
|
|
4
4
|
"description": "Vite dev server plugin that exposes endpoints for chatting with Claude Code SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@vercel/sandbox": "^1",
|
|
45
45
|
"lucide-react": "^0.564.0",
|
|
46
46
|
"simple-git": "^3.31.1",
|
|
47
|
-
"viagen-sdk": "^0.0.
|
|
47
|
+
"viagen-sdk": "^0.0.4"
|
|
48
48
|
},
|
|
49
49
|
"license": "MIT",
|
|
50
50
|
"repository": {
|