viagen 0.0.16 → 0.0.18
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/README.md +29 -3
- package/dist/cli.js +343 -12
- package/dist/index.js +506 -45
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ export default defineConfig({
|
|
|
30
30
|
npx viagen setup
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The setup wizard authenticates with Claude
|
|
33
|
+
The setup wizard authenticates with Claude, detects your GitHub and Vercel credentials, and captures your git remote info — all written to your local `.env`. This ensures sandboxes clone the correct repo instead of inferring it at runtime.
|
|
34
34
|
|
|
35
35
|
You can now run `npm run dev` to start the local dev server. At this point you can launch viagen and chat with Claude to make changes to your app.
|
|
36
36
|
|
|
@@ -68,17 +68,41 @@ viagen({
|
|
|
68
68
|
})
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
### SSR Frameworks (React Router, Remix, SvelteKit, etc.)
|
|
72
|
+
|
|
73
|
+
For plain Vite apps, the chat panel is injected automatically. SSR frameworks render their own HTML, so you need to add one script tag to your root layout:
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<script src="/via/client.js" defer></script>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For React Router, add it to `app/root.tsx`:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
export default function Root() {
|
|
83
|
+
return (
|
|
84
|
+
<html>
|
|
85
|
+
<head>
|
|
86
|
+
<script src="/via/client.js" defer />
|
|
87
|
+
{/* ... */}
|
|
88
|
+
</head>
|
|
89
|
+
{/* ... */}
|
|
90
|
+
</html>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
71
95
|
### Editable Files
|
|
72
96
|
|
|
73
97
|
Add a file editor panel to the chat UI:
|
|
74
98
|
|
|
75
99
|
```ts
|
|
76
100
|
viagen({
|
|
77
|
-
editable: ['src/components', '
|
|
101
|
+
editable: ['src/components', 'vite.config.ts']
|
|
78
102
|
})
|
|
79
103
|
```
|
|
80
104
|
|
|
81
|
-
Paths can be files or directories (directories include all files within). The editor appears as a "Files" tab in the chat panel.
|
|
105
|
+
Paths can be files or directories (directories include all files within). The editor appears as a "Files" tab in the chat panel.
|
|
82
106
|
|
|
83
107
|
The default system prompt:
|
|
84
108
|
|
|
@@ -129,6 +153,8 @@ GET /via/iframe — split view (app + chat side by side)
|
|
|
129
153
|
GET /via/files — list editable files (when configured)
|
|
130
154
|
GET /via/file?path= — read file content
|
|
131
155
|
POST /via/file — write file content { path, content }
|
|
156
|
+
GET /via/git/status — list changed files (git status)
|
|
157
|
+
GET /via/git/diff — full diff, or single file with ?path=
|
|
132
158
|
```
|
|
133
159
|
|
|
134
160
|
When `VIAGEN_AUTH_TOKEN` is set (always on in sandboxes), pass the token as a `Bearer` header or `?token=` query param.
|
package/dist/cli.js
CHANGED
|
@@ -335,6 +335,13 @@ async function refreshAccessToken(refresh) {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
// src/cli.ts
|
|
338
|
+
import {
|
|
339
|
+
createViagen,
|
|
340
|
+
saveCredentials,
|
|
341
|
+
loadCredentials,
|
|
342
|
+
clearCredentials
|
|
343
|
+
} from "viagen-sdk";
|
|
344
|
+
import { spawn as spawnChild } from "child_process";
|
|
338
345
|
function loadDotenv(dir) {
|
|
339
346
|
const envPath = join2(dir, ".env");
|
|
340
347
|
if (!existsSync(envPath)) return {};
|
|
@@ -515,9 +522,34 @@ async function setup() {
|
|
|
515
522
|
const newVars = {};
|
|
516
523
|
console.log("viagen setup");
|
|
517
524
|
console.log("");
|
|
518
|
-
|
|
525
|
+
let claudeExpired = false;
|
|
526
|
+
if (existing["CLAUDE_ACCESS_TOKEN"] && existing["CLAUDE_TOKEN_EXPIRES"]) {
|
|
527
|
+
const expires = parseInt(existing["CLAUDE_TOKEN_EXPIRES"], 10);
|
|
528
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
529
|
+
if (nowSec > expires) {
|
|
530
|
+
if (existing["CLAUDE_REFRESH_TOKEN"]) {
|
|
531
|
+
console.log("Claude auth ... token expired, attempting refresh...");
|
|
532
|
+
try {
|
|
533
|
+
const tokens = await refreshAccessToken(existing["CLAUDE_REFRESH_TOKEN"]);
|
|
534
|
+
newVars["CLAUDE_ACCESS_TOKEN"] = tokens.access_token;
|
|
535
|
+
newVars["CLAUDE_REFRESH_TOKEN"] = tokens.refresh_token;
|
|
536
|
+
newVars["CLAUDE_TOKEN_EXPIRES"] = String(nowSec + tokens.expires_in);
|
|
537
|
+
console.log("Claude auth ... refreshed");
|
|
538
|
+
} catch {
|
|
539
|
+
console.log("Claude auth ... refresh failed, need to re-authenticate");
|
|
540
|
+
claudeExpired = true;
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
console.log("Claude auth ... token expired, need to re-authenticate");
|
|
544
|
+
claudeExpired = true;
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
console.log("Claude auth ... already configured");
|
|
548
|
+
}
|
|
549
|
+
} else if (existing["ANTHROPIC_API_KEY"]) {
|
|
519
550
|
console.log("Claude auth ... already configured");
|
|
520
|
-
}
|
|
551
|
+
}
|
|
552
|
+
if (claudeExpired || !existing["ANTHROPIC_API_KEY"] && !existing["CLAUDE_ACCESS_TOKEN"]) {
|
|
521
553
|
console.log("How do you want to authenticate with Claude?");
|
|
522
554
|
console.log("");
|
|
523
555
|
console.log(" 1) Log in with Claude Max/Pro (recommended)");
|
|
@@ -584,8 +616,8 @@ async function setup() {
|
|
|
584
616
|
console.log("gh CLI is installed but not logged in.");
|
|
585
617
|
console.log("Without it, sandboxes can't commit or push changes.");
|
|
586
618
|
console.log("");
|
|
587
|
-
const
|
|
588
|
-
if (
|
|
619
|
+
const login2 = await promptUser("Run gh auth login now? [y/n]: ");
|
|
620
|
+
if (login2 === "y" || login2 === "yes") {
|
|
589
621
|
try {
|
|
590
622
|
execSync2("gh auth login", { stdio: "inherit" });
|
|
591
623
|
const token = shellOutput("gh auth token");
|
|
@@ -601,6 +633,46 @@ async function setup() {
|
|
|
601
633
|
}
|
|
602
634
|
}
|
|
603
635
|
console.log("");
|
|
636
|
+
{
|
|
637
|
+
const detectedGit = getGitInfo(cwd);
|
|
638
|
+
const savedUrl = existing["GIT_REMOTE_URL"];
|
|
639
|
+
if (savedUrl && detectedGit && savedUrl !== detectedGit.remoteUrl) {
|
|
640
|
+
console.log(`Git repo ... mismatch!`);
|
|
641
|
+
console.log(` .env: ${savedUrl}`);
|
|
642
|
+
console.log(` local: ${detectedGit.remoteUrl}`);
|
|
643
|
+
console.log("");
|
|
644
|
+
const fix = await promptUser("Update .env to match local remote? [y/n]: ");
|
|
645
|
+
if (fix === "y" || fix === "yes" || !fix) {
|
|
646
|
+
newVars["GIT_REMOTE_URL"] = detectedGit.remoteUrl;
|
|
647
|
+
newVars["GIT_BRANCH"] = detectedGit.branch;
|
|
648
|
+
newVars["GIT_USER_NAME"] = detectedGit.userName;
|
|
649
|
+
newVars["GIT_USER_EMAIL"] = detectedGit.userEmail;
|
|
650
|
+
console.log("Git repo ... updated");
|
|
651
|
+
} else {
|
|
652
|
+
console.log("Git repo ... keeping .env value");
|
|
653
|
+
}
|
|
654
|
+
} else if (savedUrl) {
|
|
655
|
+
console.log(`Git repo ... ${savedUrl}`);
|
|
656
|
+
} else if (detectedGit) {
|
|
657
|
+
console.log(`Detected git remote: ${detectedGit.remoteUrl}`);
|
|
658
|
+
console.log(` Branch: ${detectedGit.branch}`);
|
|
659
|
+
console.log(` User: ${detectedGit.userName} <${detectedGit.userEmail}>`);
|
|
660
|
+
console.log("");
|
|
661
|
+
const useIt = await promptUser("Save this to .env? [y/n]: ");
|
|
662
|
+
if (useIt === "y" || useIt === "yes" || !useIt) {
|
|
663
|
+
newVars["GIT_REMOTE_URL"] = detectedGit.remoteUrl;
|
|
664
|
+
newVars["GIT_BRANCH"] = detectedGit.branch;
|
|
665
|
+
newVars["GIT_USER_NAME"] = detectedGit.userName;
|
|
666
|
+
newVars["GIT_USER_EMAIL"] = detectedGit.userEmail;
|
|
667
|
+
console.log("Git repo ... saved");
|
|
668
|
+
} else {
|
|
669
|
+
console.log("Git repo ... skipped");
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
console.log("Git repo ... not detected (not a git repo or no remote)");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
console.log("");
|
|
604
676
|
const hasVercel = existing["VERCEL_TOKEN"] && existing["VERCEL_TEAM_ID"] && existing["VERCEL_PROJECT_ID"];
|
|
605
677
|
if (hasVercel) {
|
|
606
678
|
console.log("Vercel ... already configured");
|
|
@@ -652,8 +724,8 @@ async function setup() {
|
|
|
652
724
|
console.log("Vercel CLI is installed but not logged in.");
|
|
653
725
|
console.log("Sandbox deployment requires Vercel auth.");
|
|
654
726
|
console.log("");
|
|
655
|
-
const
|
|
656
|
-
if (
|
|
727
|
+
const login2 = await promptUser("Run vercel login now? [y/n]: ");
|
|
728
|
+
if (login2 === "y" || login2 === "yes") {
|
|
657
729
|
try {
|
|
658
730
|
execSync2("vercel login", { stdio: "inherit" });
|
|
659
731
|
} catch {
|
|
@@ -685,7 +757,17 @@ async function setup() {
|
|
|
685
757
|
}
|
|
686
758
|
console.log("");
|
|
687
759
|
if (Object.keys(newVars).length > 0) {
|
|
688
|
-
|
|
760
|
+
const toUpdate = {};
|
|
761
|
+
const toAdd = {};
|
|
762
|
+
for (const [key, val] of Object.entries(newVars)) {
|
|
763
|
+
if (existing[key]) {
|
|
764
|
+
toUpdate[key] = val;
|
|
765
|
+
} else {
|
|
766
|
+
toAdd[key] = val;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (Object.keys(toAdd).length > 0) writeEnvVars(cwd, toAdd);
|
|
770
|
+
if (Object.keys(toUpdate).length > 0) updateEnvVars(cwd, toUpdate);
|
|
689
771
|
console.log("Wrote to .env:");
|
|
690
772
|
for (const key of Object.keys(newVars)) {
|
|
691
773
|
const display = key.includes("TOKEN") || key.includes("KEY") || key.includes("SECRET") ? newVars[key].slice(0, 8) + "..." : newVars[key];
|
|
@@ -699,6 +781,32 @@ async function setup() {
|
|
|
699
781
|
console.log(" npm run dev Start the dev server");
|
|
700
782
|
console.log(" npx viagen sandbox Deploy to a sandbox");
|
|
701
783
|
}
|
|
784
|
+
function dev() {
|
|
785
|
+
const child = spawnChild("npx", ["vite"], {
|
|
786
|
+
cwd: process.cwd(),
|
|
787
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
788
|
+
shell: true
|
|
789
|
+
});
|
|
790
|
+
let opened = false;
|
|
791
|
+
child.stdout?.on("data", (chunk) => {
|
|
792
|
+
const text = chunk.toString();
|
|
793
|
+
process.stdout.write(text);
|
|
794
|
+
if (!opened) {
|
|
795
|
+
const match = text.match(/Local:\s+(https?:\/\/[^\s]+)/);
|
|
796
|
+
if (match) {
|
|
797
|
+
opened = true;
|
|
798
|
+
const baseUrl = match[1].replace(/\/$/, "");
|
|
799
|
+
const iframeUrl = `${baseUrl}/via/iframe`;
|
|
800
|
+
setTimeout(() => openBrowser2(iframeUrl), 500);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
child.on("close", (code) => {
|
|
805
|
+
process.exit(code ?? 0);
|
|
806
|
+
});
|
|
807
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
808
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
809
|
+
}
|
|
702
810
|
function parseFlag(args, flag) {
|
|
703
811
|
const idx = args.indexOf(flag);
|
|
704
812
|
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
@@ -768,7 +876,21 @@ async function sandbox(args) {
|
|
|
768
876
|
process.exit(1);
|
|
769
877
|
}
|
|
770
878
|
const githubToken = env["GITHUB_TOKEN"];
|
|
771
|
-
const
|
|
879
|
+
const envRemoteUrl = env["GIT_REMOTE_URL"];
|
|
880
|
+
const envBranch = env["GIT_BRANCH"];
|
|
881
|
+
const envUserName = env["GIT_USER_NAME"];
|
|
882
|
+
const envUserEmail = env["GIT_USER_EMAIL"];
|
|
883
|
+
const gitInfo = envRemoteUrl ? {
|
|
884
|
+
remoteUrl: envRemoteUrl,
|
|
885
|
+
branch: envBranch || "main",
|
|
886
|
+
userName: envUserName || "viagen",
|
|
887
|
+
userEmail: envUserEmail || "noreply@viagen.dev",
|
|
888
|
+
isDirty: false
|
|
889
|
+
// can't know from env, assume clean
|
|
890
|
+
} : getGitInfo(cwd);
|
|
891
|
+
if (envRemoteUrl) {
|
|
892
|
+
console.log("Using git info from .env");
|
|
893
|
+
}
|
|
772
894
|
let deployGit;
|
|
773
895
|
let overlayFiles;
|
|
774
896
|
const branch = branchOverride || (gitInfo ? gitInfo.branch : "main");
|
|
@@ -804,7 +926,7 @@ async function sandbox(args) {
|
|
|
804
926
|
userEmail: gitInfo.userEmail,
|
|
805
927
|
token: githubToken
|
|
806
928
|
});
|
|
807
|
-
if (gitInfo.isDirty && !branchOverride) {
|
|
929
|
+
if (!envRemoteUrl && gitInfo.isDirty && !branchOverride) {
|
|
808
930
|
console.log("");
|
|
809
931
|
console.log("Your working tree has uncommitted changes.");
|
|
810
932
|
console.log("");
|
|
@@ -882,8 +1004,8 @@ async function sandbox(args) {
|
|
|
882
1004
|
} : void 0,
|
|
883
1005
|
timeoutMinutes
|
|
884
1006
|
});
|
|
885
|
-
const iframeUrl = result.url.replace("?token=", "via/iframe?token=");
|
|
886
|
-
const chatUrl = result.url.replace("?token=", "via/ui?token=");
|
|
1007
|
+
const iframeUrl = result.url.replace("?token=", "/via/iframe?token=");
|
|
1008
|
+
const chatUrl = result.url.replace("?token=", "/via/ui?token=");
|
|
887
1009
|
console.log("");
|
|
888
1010
|
console.log("Sandbox deployed!");
|
|
889
1011
|
console.log("");
|
|
@@ -900,6 +1022,179 @@ async function sandbox(args) {
|
|
|
900
1022
|
console.log(`Stop with: npx viagen sandbox stop ${result.sandboxId}`);
|
|
901
1023
|
openBrowser2(iframeUrl);
|
|
902
1024
|
}
|
|
1025
|
+
var PLATFORM_URL = "http://localhost:5175";
|
|
1026
|
+
async function login() {
|
|
1027
|
+
const existing = await loadCredentials();
|
|
1028
|
+
if (existing) {
|
|
1029
|
+
const client2 = createViagen({
|
|
1030
|
+
baseUrl: existing.baseUrl,
|
|
1031
|
+
token: existing.token
|
|
1032
|
+
});
|
|
1033
|
+
try {
|
|
1034
|
+
const user2 = await client2.auth.me();
|
|
1035
|
+
if (user2) {
|
|
1036
|
+
console.log(`Already logged in as ${user2.email}`);
|
|
1037
|
+
const answer = await promptUser("Log in again? [y/n]: ");
|
|
1038
|
+
if (answer !== "y" && answer !== "yes") return;
|
|
1039
|
+
}
|
|
1040
|
+
} catch {
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
console.log("viagen login");
|
|
1044
|
+
console.log("");
|
|
1045
|
+
const client = createViagen({ baseUrl: PLATFORM_URL });
|
|
1046
|
+
console.log("Opening browser to authorize...");
|
|
1047
|
+
const { token, expiresAt } = await client.auth.loginCli({
|
|
1048
|
+
onOpenUrl: (url) => {
|
|
1049
|
+
openBrowser2(url);
|
|
1050
|
+
console.log("");
|
|
1051
|
+
console.log("If the browser didn't open, visit:");
|
|
1052
|
+
console.log(` ${url}`);
|
|
1053
|
+
console.log("");
|
|
1054
|
+
console.log("Waiting for authorization...");
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
await saveCredentials({ token, baseUrl: PLATFORM_URL, expiresAt });
|
|
1058
|
+
const authed = createViagen({ baseUrl: PLATFORM_URL, token });
|
|
1059
|
+
const user = await authed.auth.me();
|
|
1060
|
+
console.log("");
|
|
1061
|
+
if (user) {
|
|
1062
|
+
console.log(`Logged in as ${user.email}`);
|
|
1063
|
+
} else {
|
|
1064
|
+
console.log("Logged in.");
|
|
1065
|
+
}
|
|
1066
|
+
console.log("Credentials saved to ~/.config/viagen/credentials.json");
|
|
1067
|
+
}
|
|
1068
|
+
async function logout() {
|
|
1069
|
+
const existing = await loadCredentials();
|
|
1070
|
+
if (!existing) {
|
|
1071
|
+
console.log("Not logged in.");
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
await clearCredentials();
|
|
1075
|
+
console.log("Logged out. Credentials removed.");
|
|
1076
|
+
}
|
|
1077
|
+
async function whoami() {
|
|
1078
|
+
const existing = await loadCredentials();
|
|
1079
|
+
if (!existing) {
|
|
1080
|
+
console.log("Not logged in. Run `viagen login` to authenticate.");
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const client = createViagen({
|
|
1084
|
+
baseUrl: existing.baseUrl,
|
|
1085
|
+
token: existing.token
|
|
1086
|
+
});
|
|
1087
|
+
try {
|
|
1088
|
+
const user = await client.auth.me();
|
|
1089
|
+
if (user) {
|
|
1090
|
+
console.log(`${user.email}${user.name ? ` (${user.name})` : ""}`);
|
|
1091
|
+
if (user.organizations.length > 0) {
|
|
1092
|
+
console.log(
|
|
1093
|
+
`Orgs: ${user.organizations.map((o) => o.name).join(", ")}`
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
} else {
|
|
1097
|
+
console.log("Session expired. Run `viagen login` to re-authenticate.");
|
|
1098
|
+
}
|
|
1099
|
+
} catch {
|
|
1100
|
+
console.log("Session expired. Run `viagen login` to re-authenticate.");
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
async function requireClient() {
|
|
1104
|
+
const creds = await loadCredentials();
|
|
1105
|
+
if (!creds) {
|
|
1106
|
+
console.error("Not logged in. Run `viagen login` first.");
|
|
1107
|
+
process.exit(1);
|
|
1108
|
+
}
|
|
1109
|
+
return createViagen({ baseUrl: creds.baseUrl, token: creds.token });
|
|
1110
|
+
}
|
|
1111
|
+
async function orgs(args) {
|
|
1112
|
+
const sub = args[0];
|
|
1113
|
+
if (sub === "create") {
|
|
1114
|
+
const name = args.slice(1).join(" ");
|
|
1115
|
+
if (!name) {
|
|
1116
|
+
console.error("Usage: viagen orgs create <name>");
|
|
1117
|
+
process.exit(1);
|
|
1118
|
+
}
|
|
1119
|
+
const client2 = await requireClient();
|
|
1120
|
+
const org = await client2.orgs.create({ name });
|
|
1121
|
+
console.log(`Created org "${org.name}" (${org.id})`);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (sub === "invite") {
|
|
1125
|
+
const email = args[1];
|
|
1126
|
+
if (!email) {
|
|
1127
|
+
console.error("Usage: viagen orgs invite <email>");
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
const client2 = await requireClient();
|
|
1131
|
+
await client2.orgs.addMember({ email });
|
|
1132
|
+
console.log(`Invited ${email}`);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
const client = await requireClient();
|
|
1136
|
+
const memberships = await client.orgs.list() ?? [];
|
|
1137
|
+
if (memberships.length === 0) {
|
|
1138
|
+
console.log("No organizations. Create one with `viagen orgs create <name>`.");
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
for (const m of memberships) {
|
|
1142
|
+
const role = m.role ? ` (${m.role})` : "";
|
|
1143
|
+
console.log(` ${m.organizationName}${role}`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
async function projects(args) {
|
|
1147
|
+
const sub = args[0];
|
|
1148
|
+
if (sub === "create") {
|
|
1149
|
+
const name = args.slice(1).join(" ");
|
|
1150
|
+
if (!name) {
|
|
1151
|
+
console.error("Usage: viagen projects create <name>");
|
|
1152
|
+
process.exit(1);
|
|
1153
|
+
}
|
|
1154
|
+
const client2 = await requireClient();
|
|
1155
|
+
const project = await client2.projects.create({ name });
|
|
1156
|
+
console.log(`Created project "${project.name}" (${project.id})`);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (sub === "get") {
|
|
1160
|
+
const id = args[1];
|
|
1161
|
+
if (!id) {
|
|
1162
|
+
console.error("Usage: viagen projects get <id>");
|
|
1163
|
+
process.exit(1);
|
|
1164
|
+
}
|
|
1165
|
+
const client2 = await requireClient();
|
|
1166
|
+
const project = await client2.projects.get(id);
|
|
1167
|
+
console.log(` Name: ${project.name}`);
|
|
1168
|
+
console.log(` ID: ${project.id}`);
|
|
1169
|
+
if (project.githubRepo) console.log(` GitHub: ${project.githubRepo}`);
|
|
1170
|
+
if (project.templateId) console.log(` Template: ${project.templateId}`);
|
|
1171
|
+
console.log(` Created: ${project.createdAt}`);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (sub === "delete") {
|
|
1175
|
+
const id = args[1];
|
|
1176
|
+
if (!id) {
|
|
1177
|
+
console.error("Usage: viagen projects delete <id>");
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
const answer = await promptUser(`Delete project ${id}? [y/n]: `);
|
|
1181
|
+
if (answer !== "y" && answer !== "yes") return;
|
|
1182
|
+
const client2 = await requireClient();
|
|
1183
|
+
await client2.projects.delete(id);
|
|
1184
|
+
console.log("Project deleted.");
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const client = await requireClient();
|
|
1188
|
+
const list = await client.projects.list() ?? [];
|
|
1189
|
+
if (list.length === 0) {
|
|
1190
|
+
console.log("No projects. Create one with `viagen projects create <name>`.");
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
for (const p of list) {
|
|
1194
|
+
const repo = p.githubRepo ? ` (${p.githubRepo})` : "";
|
|
1195
|
+
console.log(` ${p.name}${repo} ${p.id}`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
903
1198
|
function help() {
|
|
904
1199
|
console.log("viagen \u2014 Claude Code in your Vite dev server");
|
|
905
1200
|
console.log("");
|
|
@@ -907,6 +1202,17 @@ function help() {
|
|
|
907
1202
|
console.log(" viagen <command>");
|
|
908
1203
|
console.log("");
|
|
909
1204
|
console.log("Commands:");
|
|
1205
|
+
console.log(" login Log in to the viagen platform");
|
|
1206
|
+
console.log(" logout Log out and remove credentials");
|
|
1207
|
+
console.log(" whoami Show current user");
|
|
1208
|
+
console.log(" orgs List your organizations");
|
|
1209
|
+
console.log(" orgs create <name> Create a new organization");
|
|
1210
|
+
console.log(" orgs invite <email> Invite a member to the org");
|
|
1211
|
+
console.log(" projects List projects in current org");
|
|
1212
|
+
console.log(" projects create <name> Create a new project");
|
|
1213
|
+
console.log(" projects get <id> Show project details");
|
|
1214
|
+
console.log(" projects delete <id> Delete a project");
|
|
1215
|
+
console.log(" dev Start Vite and open the split view");
|
|
910
1216
|
console.log(" setup Set up .env with API keys and tokens");
|
|
911
1217
|
console.log(" sandbox [-b branch] [-t min] Deploy your project to a Vercel Sandbox");
|
|
912
1218
|
console.log(" sandbox stop <id> Stop a running sandbox");
|
|
@@ -932,6 +1238,18 @@ function help() {
|
|
|
932
1238
|
console.log(
|
|
933
1239
|
" GITHUB_TOKEN Enables git commit+push from sandbox."
|
|
934
1240
|
);
|
|
1241
|
+
console.log(
|
|
1242
|
+
" GIT_REMOTE_URL Git remote (from setup, overrides runtime detection)."
|
|
1243
|
+
);
|
|
1244
|
+
console.log(
|
|
1245
|
+
" GIT_BRANCH Git branch for sandbox."
|
|
1246
|
+
);
|
|
1247
|
+
console.log(
|
|
1248
|
+
" GIT_USER_NAME Git user name for sandbox commits."
|
|
1249
|
+
);
|
|
1250
|
+
console.log(
|
|
1251
|
+
" GIT_USER_EMAIL Git user email for sandbox commits."
|
|
1252
|
+
);
|
|
935
1253
|
console.log(
|
|
936
1254
|
" VIAGEN_AUTH_TOKEN Protects all endpoints with token auth."
|
|
937
1255
|
);
|
|
@@ -950,7 +1268,20 @@ function help() {
|
|
|
950
1268
|
async function main() {
|
|
951
1269
|
const args = process.argv.slice(2);
|
|
952
1270
|
const command = args[0];
|
|
953
|
-
if (command === "
|
|
1271
|
+
if (command === "login") {
|
|
1272
|
+
await login();
|
|
1273
|
+
} else if (command === "logout") {
|
|
1274
|
+
await logout();
|
|
1275
|
+
} else if (command === "whoami") {
|
|
1276
|
+
await whoami();
|
|
1277
|
+
} else if (command === "orgs") {
|
|
1278
|
+
await orgs(args.slice(1));
|
|
1279
|
+
} else if (command === "projects") {
|
|
1280
|
+
await projects(args.slice(1));
|
|
1281
|
+
} else if (command === "dev") {
|
|
1282
|
+
dev();
|
|
1283
|
+
return;
|
|
1284
|
+
} else if (command === "setup") {
|
|
954
1285
|
await setup();
|
|
955
1286
|
} else if (command === "sandbox") {
|
|
956
1287
|
await sandbox(args.slice(1));
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
3
|
-
import { join as
|
|
3
|
+
import { join as join6 } from "path";
|
|
4
4
|
import { loadEnv } from "vite";
|
|
5
5
|
|
|
6
6
|
// src/logger.ts
|
|
@@ -112,12 +112,12 @@ async function refreshAccessToken(refresh) {
|
|
|
112
112
|
|
|
113
113
|
// src/chat.ts
|
|
114
114
|
function readBody(req) {
|
|
115
|
-
return new Promise((
|
|
115
|
+
return new Promise((resolve3, reject) => {
|
|
116
116
|
let body = "";
|
|
117
117
|
req.on("data", (chunk) => {
|
|
118
118
|
body += chunk.toString();
|
|
119
119
|
});
|
|
120
|
-
req.on("end", () =>
|
|
120
|
+
req.on("end", () => resolve3(body));
|
|
121
121
|
req.on("error", reject);
|
|
122
122
|
});
|
|
123
123
|
}
|
|
@@ -343,8 +343,10 @@ function buildClientScript(opts) {
|
|
|
343
343
|
/* js */
|
|
344
344
|
`
|
|
345
345
|
(function() {
|
|
346
|
+
if (document.getElementById('viagen-toggle')) return;
|
|
347
|
+
|
|
346
348
|
var OVERLAY_ENABLED = ${opts.overlay};
|
|
347
|
-
var EMBED_MODE =
|
|
349
|
+
var EMBED_MODE = new URLSearchParams(window.location.search).has('_viagen_embed');
|
|
348
350
|
|
|
349
351
|
/* ---- Error overlay: inject Fix button into shadow DOM ---- */
|
|
350
352
|
if (OVERLAY_ENABLED) {
|
|
@@ -492,6 +494,8 @@ function buildClientScript(opts) {
|
|
|
492
494
|
// src/ui.ts
|
|
493
495
|
function buildUiHtml(opts) {
|
|
494
496
|
const hasEditor = opts?.editable ?? false;
|
|
497
|
+
const hasGit = opts?.git ?? false;
|
|
498
|
+
const hasTabs = hasEditor || hasGit;
|
|
495
499
|
return `<!DOCTYPE html>
|
|
496
500
|
<html lang="en">
|
|
497
501
|
<head>
|
|
@@ -605,7 +609,7 @@ function buildUiHtml(opts) {
|
|
|
605
609
|
gap: 8px;
|
|
606
610
|
}
|
|
607
611
|
.messages:empty::after {
|
|
608
|
-
content: 'Ask Claude to build something...';
|
|
612
|
+
content: 'Ask Claude to build features or change something...';
|
|
609
613
|
color: #3f3f46;
|
|
610
614
|
font-size: 13px;
|
|
611
615
|
text-align: center;
|
|
@@ -877,6 +881,86 @@ function buildUiHtml(opts) {
|
|
|
877
881
|
background: #0a0a0c;
|
|
878
882
|
border-right: 1px solid #1e1e22;
|
|
879
883
|
}
|
|
884
|
+
.changes-file {
|
|
885
|
+
padding: 8px 16px;
|
|
886
|
+
font-family: ui-monospace, monospace;
|
|
887
|
+
font-size: 12px;
|
|
888
|
+
color: #a1a1aa;
|
|
889
|
+
cursor: pointer;
|
|
890
|
+
display: flex;
|
|
891
|
+
align-items: center;
|
|
892
|
+
gap: 8px;
|
|
893
|
+
transition: background 0.1s;
|
|
894
|
+
border-bottom: 1px solid #1e1e22;
|
|
895
|
+
}
|
|
896
|
+
.changes-file:hover { background: #18181b; color: #e4e4e7; }
|
|
897
|
+
.changes-file .file-path { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
898
|
+
.changes-dot {
|
|
899
|
+
width: 6px;
|
|
900
|
+
height: 6px;
|
|
901
|
+
border-radius: 50%;
|
|
902
|
+
flex-shrink: 0;
|
|
903
|
+
}
|
|
904
|
+
.changes-dot.M { background: #facc15; }
|
|
905
|
+
.changes-dot.A { background: #4ade80; }
|
|
906
|
+
.changes-dot.q { background: #4ade80; }
|
|
907
|
+
.changes-dot.D { background: #f87171; }
|
|
908
|
+
.changes-dot.R { background: #60a5fa; }
|
|
909
|
+
.changes-badge {
|
|
910
|
+
font-size: 10px;
|
|
911
|
+
color: #52525b;
|
|
912
|
+
font-family: ui-monospace, monospace;
|
|
913
|
+
}
|
|
914
|
+
.changes-summary {
|
|
915
|
+
padding: 8px 16px;
|
|
916
|
+
border-bottom: 1px solid #27272a;
|
|
917
|
+
background: #18181b;
|
|
918
|
+
font-family: ui-monospace, monospace;
|
|
919
|
+
font-size: 11px;
|
|
920
|
+
color: #71717a;
|
|
921
|
+
display: flex;
|
|
922
|
+
align-items: center;
|
|
923
|
+
gap: 10px;
|
|
924
|
+
flex-shrink: 0;
|
|
925
|
+
}
|
|
926
|
+
.changes-summary .stat-add { color: #4ade80; }
|
|
927
|
+
.changes-summary .stat-del { color: #f87171; }
|
|
928
|
+
.changes-summary .stat-files { color: #a1a1aa; }
|
|
929
|
+
.file-delta {
|
|
930
|
+
font-family: ui-monospace, monospace;
|
|
931
|
+
font-size: 10px;
|
|
932
|
+
display: flex;
|
|
933
|
+
gap: 4px;
|
|
934
|
+
flex-shrink: 0;
|
|
935
|
+
}
|
|
936
|
+
.file-delta .d-add { color: #4ade80; }
|
|
937
|
+
.file-delta .d-del { color: #f87171; }
|
|
938
|
+
.diff-view {
|
|
939
|
+
flex: 1;
|
|
940
|
+
overflow-y: auto;
|
|
941
|
+
padding: 0;
|
|
942
|
+
font-family: ui-monospace, monospace;
|
|
943
|
+
font-size: 11px;
|
|
944
|
+
line-height: 1.6;
|
|
945
|
+
}
|
|
946
|
+
.diff-line {
|
|
947
|
+
padding: 0 12px;
|
|
948
|
+
white-space: pre-wrap;
|
|
949
|
+
word-break: break-all;
|
|
950
|
+
}
|
|
951
|
+
.diff-add { color: #4ade80; background: rgba(74,222,128,0.08); }
|
|
952
|
+
.diff-del { color: #f87171; background: rgba(248,113,113,0.08); }
|
|
953
|
+
.diff-hunk { color: #a78bfa; background: rgba(167,139,250,0.06); padding-top: 6px; margin-top: 4px; }
|
|
954
|
+
.diff-meta { color: #52525b; }
|
|
955
|
+
.diff-ctx { color: #71717a; }
|
|
956
|
+
.changes-empty {
|
|
957
|
+
padding: 16px;
|
|
958
|
+
color: #52525b;
|
|
959
|
+
font-size: 12px;
|
|
960
|
+
font-family: ui-monospace, monospace;
|
|
961
|
+
text-align: center;
|
|
962
|
+
margin-top: 40%;
|
|
963
|
+
}
|
|
880
964
|
</style>
|
|
881
965
|
</head>
|
|
882
966
|
<body>
|
|
@@ -893,9 +977,10 @@ function buildUiHtml(opts) {
|
|
|
893
977
|
<button class="btn" id="reset-btn">Reset</button>
|
|
894
978
|
</div>
|
|
895
979
|
</div>
|
|
896
|
-
${
|
|
980
|
+
${hasTabs ? `<div class="tab-bar" id="tab-bar">
|
|
897
981
|
<button class="tab active" data-tab="chat">Chat</button>
|
|
898
|
-
<button class="tab" data-tab="files">Files</button>
|
|
982
|
+
${hasEditor ? '<button class="tab" data-tab="files">Files</button>' : ""}
|
|
983
|
+
${hasGit ? '<button class="tab" data-tab="changes" id="changes-tab">Changes</button>' : ""}
|
|
899
984
|
</div>` : ""}
|
|
900
985
|
<div id="chat-view" style="display:flex;flex-direction:column;flex:1;overflow:hidden;">
|
|
901
986
|
<div class="setup-banner" id="setup-banner"></div>
|
|
@@ -922,6 +1007,19 @@ function buildUiHtml(opts) {
|
|
|
922
1007
|
</div>
|
|
923
1008
|
</div>
|
|
924
1009
|
</div>` : ""}
|
|
1010
|
+
${hasGit ? `<div id="changes-view" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
|
|
1011
|
+
<div class="changes-summary" id="changes-summary" style="display:none;"></div>
|
|
1012
|
+
<div id="changes-list-view" style="flex:1;overflow-y:auto;">
|
|
1013
|
+
<div id="changes-list" style="padding:0;"></div>
|
|
1014
|
+
</div>
|
|
1015
|
+
<div id="changes-diff-view" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
|
|
1016
|
+
<div class="editor-header">
|
|
1017
|
+
<button class="editor-back" id="diff-back" title="Back to changes">←</button>
|
|
1018
|
+
<span class="editor-filename" id="diff-filename"></span>
|
|
1019
|
+
</div>
|
|
1020
|
+
<div class="diff-view" id="diff-content"></div>
|
|
1021
|
+
</div>
|
|
1022
|
+
</div>` : ""}
|
|
925
1023
|
<script>
|
|
926
1024
|
var STORAGE_KEY = 'viagen_chatLog';
|
|
927
1025
|
var SOUND_KEY = 'viagen_sound';
|
|
@@ -1369,36 +1467,45 @@ function buildUiHtml(opts) {
|
|
|
1369
1467
|
|
|
1370
1468
|
loadHistory();
|
|
1371
1469
|
|
|
1372
|
-
// \u2500\u2500
|
|
1373
|
-
${
|
|
1470
|
+
// \u2500\u2500 Tab switching \u2500\u2500
|
|
1471
|
+
${hasTabs ? `
|
|
1374
1472
|
(function() {
|
|
1375
1473
|
var chatView = document.getElementById('chat-view');
|
|
1376
1474
|
var filesView = document.getElementById('files-view');
|
|
1475
|
+
var changesView = document.getElementById('changes-view');
|
|
1377
1476
|
var tabs = document.querySelectorAll('.tab');
|
|
1378
|
-
var fileListView = document.getElementById('file-list-view');
|
|
1379
|
-
var fileEditorView = document.getElementById('file-editor-view');
|
|
1380
|
-
var editorTextarea = document.getElementById('editor-textarea');
|
|
1381
|
-
var lineNumbersEl = document.getElementById('line-numbers');
|
|
1382
|
-
var editorWrap = document.getElementById('editor-wrap');
|
|
1383
|
-
var editorSave = document.getElementById('editor-save');
|
|
1384
|
-
var editorFilename = document.getElementById('editor-filename');
|
|
1385
|
-
|
|
1386
|
-
var editorState = { path: '', original: '', modified: false };
|
|
1387
1477
|
|
|
1388
|
-
// Tab switching
|
|
1389
1478
|
tabs.forEach(function(tab) {
|
|
1390
1479
|
tab.addEventListener('click', function() {
|
|
1391
1480
|
tabs.forEach(function(t) { t.classList.remove('active'); });
|
|
1392
1481
|
tab.classList.add('active');
|
|
1393
1482
|
var target = tab.dataset.tab;
|
|
1394
1483
|
chatView.style.display = target === 'chat' ? 'flex' : 'none';
|
|
1395
|
-
filesView.style.display = target === 'files' ? 'flex' : 'none';
|
|
1396
|
-
if (target === '
|
|
1484
|
+
if (filesView) filesView.style.display = target === 'files' ? 'flex' : 'none';
|
|
1485
|
+
if (changesView) changesView.style.display = target === 'changes' ? 'flex' : 'none';
|
|
1486
|
+
if (target === 'files' && window._viagenLoadFiles) window._viagenLoadFiles();
|
|
1487
|
+
if (target === 'changes' && window._viagenLoadChanges) window._viagenLoadChanges();
|
|
1397
1488
|
if (target === 'chat') inputEl.focus();
|
|
1398
1489
|
});
|
|
1399
1490
|
});
|
|
1491
|
+
})();
|
|
1492
|
+
` : ""}
|
|
1493
|
+
|
|
1494
|
+
// \u2500\u2500 File editor panel \u2500\u2500
|
|
1495
|
+
${hasEditor ? `
|
|
1496
|
+
(function() {
|
|
1497
|
+
var fileListView = document.getElementById('file-list-view');
|
|
1498
|
+
var fileEditorView = document.getElementById('file-editor-view');
|
|
1499
|
+
var editorTextarea = document.getElementById('editor-textarea');
|
|
1500
|
+
var lineNumbersEl = document.getElementById('line-numbers');
|
|
1501
|
+
var editorWrap = document.getElementById('editor-wrap');
|
|
1502
|
+
var editorSave = document.getElementById('editor-save');
|
|
1503
|
+
var editorFilename = document.getElementById('editor-filename');
|
|
1504
|
+
|
|
1505
|
+
var editorState = { path: '', original: '', modified: false };
|
|
1400
1506
|
|
|
1401
1507
|
// File list
|
|
1508
|
+
window._viagenLoadFiles = loadFileList;
|
|
1402
1509
|
async function loadFileList() {
|
|
1403
1510
|
var listEl = document.getElementById('file-list');
|
|
1404
1511
|
listEl.innerHTML = '<div style="padding:16px;color:#52525b;font-size:12px;font-family:ui-monospace,monospace;">Loading...</div>';
|
|
@@ -1533,6 +1640,129 @@ function buildUiHtml(opts) {
|
|
|
1533
1640
|
|
|
1534
1641
|
})();
|
|
1535
1642
|
` : ""}
|
|
1643
|
+
|
|
1644
|
+
// \u2500\u2500 Changes panel (git diff) \u2500\u2500
|
|
1645
|
+
${hasGit ? `
|
|
1646
|
+
(function() {
|
|
1647
|
+
var changesListView = document.getElementById('changes-list-view');
|
|
1648
|
+
var changesDiffView = document.getElementById('changes-diff-view');
|
|
1649
|
+
var changesListEl = document.getElementById('changes-list');
|
|
1650
|
+
var diffContent = document.getElementById('diff-content');
|
|
1651
|
+
var diffFilename = document.getElementById('diff-filename');
|
|
1652
|
+
var changesTab = document.getElementById('changes-tab');
|
|
1653
|
+
var changesSummary = document.getElementById('changes-summary');
|
|
1654
|
+
|
|
1655
|
+
window._viagenLoadChanges = loadChanges;
|
|
1656
|
+
|
|
1657
|
+
async function loadChanges() {
|
|
1658
|
+
changesListView.style.display = 'block';
|
|
1659
|
+
changesDiffView.style.display = 'none';
|
|
1660
|
+
changesSummary.style.display = 'none';
|
|
1661
|
+
changesListEl.innerHTML = '<div style="padding:16px;color:#52525b;font-size:12px;font-family:ui-monospace,monospace;">Loading...</div>';
|
|
1662
|
+
try {
|
|
1663
|
+
var res = await fetch('/via/git/status');
|
|
1664
|
+
var data = await res.json();
|
|
1665
|
+
if (!data.git) {
|
|
1666
|
+
changesListEl.innerHTML = '<div class="changes-empty">Not a git repository</div>';
|
|
1667
|
+
if (changesTab) changesTab.textContent = 'Changes';
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
renderSummary(data);
|
|
1671
|
+
renderChanges(data.files);
|
|
1672
|
+
} catch(e) {
|
|
1673
|
+
changesListEl.innerHTML = '<div style="padding:16px;color:#f87171;font-size:12px;">Failed to load changes</div>';
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function renderSummary(data) {
|
|
1678
|
+
var ins = data.insertions || 0;
|
|
1679
|
+
var del = data.deletions || 0;
|
|
1680
|
+
var count = data.files ? data.files.length : 0;
|
|
1681
|
+
if (count === 0) { changesSummary.style.display = 'none'; return; }
|
|
1682
|
+
changesSummary.style.display = 'flex';
|
|
1683
|
+
changesSummary.innerHTML =
|
|
1684
|
+
'<span class="stat-files">' + count + (count === 1 ? ' file' : ' files') + '</span>' +
|
|
1685
|
+
(ins > 0 ? '<span class="stat-add">+' + ins + '</span>' : '') +
|
|
1686
|
+
(del > 0 ? '<span class="stat-del">-' + del + '</span>' : '');
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function renderChanges(files) {
|
|
1690
|
+
changesListEl.innerHTML = '';
|
|
1691
|
+
if (changesTab) changesTab.textContent = files.length > 0 ? 'Changes (' + files.length + ')' : 'Changes';
|
|
1692
|
+
if (files.length === 0) {
|
|
1693
|
+
changesListEl.innerHTML = '<div class="changes-empty">No changes</div>';
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
files.forEach(function(f) {
|
|
1697
|
+
var item = document.createElement('div');
|
|
1698
|
+
item.className = 'changes-file';
|
|
1699
|
+
var dotClass = f.status === '?' ? 'q' : f.status;
|
|
1700
|
+
var statusLabel = f.status === '?' ? 'Untracked' : f.status === 'M' ? 'Modified' : f.status === 'A' ? 'Added' : f.status === 'D' ? 'Deleted' : f.status === 'R' ? 'Renamed' : f.status;
|
|
1701
|
+
var deltaHtml = '';
|
|
1702
|
+
if (f.insertions > 0 || f.deletions > 0) {
|
|
1703
|
+
deltaHtml = '<span class="file-delta">' +
|
|
1704
|
+
(f.insertions > 0 ? '<span class="d-add">+' + f.insertions + '</span>' : '') +
|
|
1705
|
+
(f.deletions > 0 ? '<span class="d-del">-' + f.deletions + '</span>' : '') +
|
|
1706
|
+
'</span>';
|
|
1707
|
+
}
|
|
1708
|
+
item.innerHTML = '<span class="changes-dot ' + dotClass + '" title="' + statusLabel + '"></span>' +
|
|
1709
|
+
'<span class="file-path" title="' + escapeHtml(f.path) + '">' + escapeHtml(f.path) + '</span>' +
|
|
1710
|
+
deltaHtml;
|
|
1711
|
+
item.addEventListener('click', function() { openDiff(f.path); });
|
|
1712
|
+
changesListEl.appendChild(item);
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
async function openDiff(path) {
|
|
1717
|
+
changesListView.style.display = 'none';
|
|
1718
|
+
changesDiffView.style.display = 'flex';
|
|
1719
|
+
diffFilename.textContent = path;
|
|
1720
|
+
diffContent.innerHTML = '<div style="padding:16px;color:#52525b;font-size:12px;font-family:ui-monospace,monospace;">Loading diff...</div>';
|
|
1721
|
+
|
|
1722
|
+
try {
|
|
1723
|
+
var res = await fetch('/via/git/diff?path=' + encodeURIComponent(path));
|
|
1724
|
+
var data = await res.json();
|
|
1725
|
+
renderDiff(data.diff);
|
|
1726
|
+
} catch(e) {
|
|
1727
|
+
diffContent.innerHTML = '<div style="padding:16px;color:#f87171;font-size:12px;">Failed to load diff</div>';
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function renderDiff(diff) {
|
|
1732
|
+
diffContent.innerHTML = '';
|
|
1733
|
+
if (!diff) {
|
|
1734
|
+
diffContent.innerHTML = '<div class="changes-empty">No diff available</div>';
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
var lines = diff.split('\\n');
|
|
1738
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1739
|
+
var line = lines[i];
|
|
1740
|
+
var div = document.createElement('div');
|
|
1741
|
+
div.className = 'diff-line';
|
|
1742
|
+
if (line.charAt(0) === '+' && !line.startsWith('+++')) {
|
|
1743
|
+
div.className += ' diff-add';
|
|
1744
|
+
} else if (line.charAt(0) === '-' && !line.startsWith('---')) {
|
|
1745
|
+
div.className += ' diff-del';
|
|
1746
|
+
} else if (line.startsWith('@@')) {
|
|
1747
|
+
div.className += ' diff-hunk';
|
|
1748
|
+
} else if (line.startsWith('diff ') || line.startsWith('index ') || line.startsWith('---') || line.startsWith('+++')) {
|
|
1749
|
+
div.className += ' diff-meta';
|
|
1750
|
+
} else {
|
|
1751
|
+
div.className += ' diff-ctx';
|
|
1752
|
+
}
|
|
1753
|
+
div.textContent = line;
|
|
1754
|
+
diffContent.appendChild(div);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// Back button
|
|
1759
|
+
document.getElementById('diff-back').addEventListener('click', function() {
|
|
1760
|
+
changesDiffView.style.display = 'none';
|
|
1761
|
+
changesListView.style.display = 'block';
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
})();
|
|
1765
|
+
` : ""}
|
|
1536
1766
|
</script>
|
|
1537
1767
|
</body>
|
|
1538
1768
|
</html>`;
|
|
@@ -1688,12 +1918,12 @@ import {
|
|
|
1688
1918
|
} from "fs";
|
|
1689
1919
|
import { join as join3, resolve, relative } from "path";
|
|
1690
1920
|
function readBody2(req) {
|
|
1691
|
-
return new Promise((
|
|
1921
|
+
return new Promise((resolve3, reject) => {
|
|
1692
1922
|
let body = "";
|
|
1693
1923
|
req.on("data", (chunk) => {
|
|
1694
1924
|
body += chunk.toString();
|
|
1695
1925
|
});
|
|
1696
|
-
req.on("end", () =>
|
|
1926
|
+
req.on("end", () => resolve3(body));
|
|
1697
1927
|
req.on("error", reject);
|
|
1698
1928
|
});
|
|
1699
1929
|
}
|
|
@@ -1830,10 +2060,240 @@ function registerFileRoutes(server, opts) {
|
|
|
1830
2060
|
});
|
|
1831
2061
|
}
|
|
1832
2062
|
|
|
2063
|
+
// src/inject.ts
|
|
2064
|
+
var SCRIPT_TAG = '<script src="/via/client.js" defer></script>';
|
|
2065
|
+
var MARKER = "viagen-toggle";
|
|
2066
|
+
function createInjectionMiddleware() {
|
|
2067
|
+
return function injectMiddleware(req, res, next) {
|
|
2068
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
2069
|
+
return next();
|
|
2070
|
+
}
|
|
2071
|
+
const url = req.url ?? "/";
|
|
2072
|
+
if (url.startsWith("/via/")) {
|
|
2073
|
+
return next();
|
|
2074
|
+
}
|
|
2075
|
+
const accept = req.headers.accept ?? "";
|
|
2076
|
+
if (!accept.includes("text/html")) {
|
|
2077
|
+
return next();
|
|
2078
|
+
}
|
|
2079
|
+
const originalWrite = res.write;
|
|
2080
|
+
const originalEnd = res.end;
|
|
2081
|
+
let injected = false;
|
|
2082
|
+
function isHtmlResponse() {
|
|
2083
|
+
const ct = res.getHeader("content-type");
|
|
2084
|
+
return !!ct && String(ct).includes("text/html");
|
|
2085
|
+
}
|
|
2086
|
+
function tryInject(chunk) {
|
|
2087
|
+
if (injected || !chunk) return chunk;
|
|
2088
|
+
if (!isHtmlResponse()) return chunk;
|
|
2089
|
+
const str = typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString("utf-8") : null;
|
|
2090
|
+
if (!str) return chunk;
|
|
2091
|
+
if (str.includes(MARKER) || str.includes("/via/client.js")) {
|
|
2092
|
+
injected = true;
|
|
2093
|
+
return chunk;
|
|
2094
|
+
}
|
|
2095
|
+
let idx = str.indexOf("</head>");
|
|
2096
|
+
if (idx === -1) idx = str.indexOf("</body>");
|
|
2097
|
+
if (idx === -1) return chunk;
|
|
2098
|
+
injected = true;
|
|
2099
|
+
if (!res.headersSent) {
|
|
2100
|
+
res.removeHeader("content-length");
|
|
2101
|
+
}
|
|
2102
|
+
const result = str.slice(0, idx) + SCRIPT_TAG + str.slice(idx);
|
|
2103
|
+
return typeof chunk === "string" ? result : Buffer.from(result, "utf-8");
|
|
2104
|
+
}
|
|
2105
|
+
res.write = function(chunk, ...args) {
|
|
2106
|
+
return originalWrite.call(
|
|
2107
|
+
res,
|
|
2108
|
+
tryInject(chunk),
|
|
2109
|
+
...args
|
|
2110
|
+
);
|
|
2111
|
+
};
|
|
2112
|
+
res.end = function(chunk, ...args) {
|
|
2113
|
+
return originalEnd.call(
|
|
2114
|
+
res,
|
|
2115
|
+
tryInject(chunk),
|
|
2116
|
+
...args
|
|
2117
|
+
);
|
|
2118
|
+
};
|
|
2119
|
+
next();
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// src/git.ts
|
|
2124
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2125
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
2126
|
+
import {
|
|
2127
|
+
simpleGit
|
|
2128
|
+
} from "simple-git";
|
|
2129
|
+
function mapStatus(result, stats) {
|
|
2130
|
+
const files = [];
|
|
2131
|
+
const push = (path, status) => {
|
|
2132
|
+
const s = stats.get(path) ?? { ins: 0, del: 0 };
|
|
2133
|
+
files.push({ path, status, insertions: s.ins, deletions: s.del });
|
|
2134
|
+
};
|
|
2135
|
+
for (const f of result.modified) push(f, "M");
|
|
2136
|
+
for (const f of result.created) push(f, "A");
|
|
2137
|
+
for (const f of result.deleted) push(f, "D");
|
|
2138
|
+
for (const f of result.renamed) push(f.to, "R");
|
|
2139
|
+
for (const f of result.not_added) push(f, "?");
|
|
2140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2141
|
+
return files.filter((f) => {
|
|
2142
|
+
if (seen.has(f.path)) return false;
|
|
2143
|
+
seen.add(f.path);
|
|
2144
|
+
return true;
|
|
2145
|
+
}).sort((a, b) => a.path.localeCompare(b.path));
|
|
2146
|
+
}
|
|
2147
|
+
async function getDiffStats(git, repoRoot, untrackedFiles) {
|
|
2148
|
+
const stats = /* @__PURE__ */ new Map();
|
|
2149
|
+
let totalInsertions = 0;
|
|
2150
|
+
let totalDeletions = 0;
|
|
2151
|
+
try {
|
|
2152
|
+
const [staged, unstaged] = await Promise.all([
|
|
2153
|
+
git.diffSummary(["--cached"]),
|
|
2154
|
+
git.diffSummary()
|
|
2155
|
+
]);
|
|
2156
|
+
for (const summary of [staged, unstaged]) {
|
|
2157
|
+
for (const f of summary.files) {
|
|
2158
|
+
if (f.binary) continue;
|
|
2159
|
+
const tf = f;
|
|
2160
|
+
const existing = stats.get(tf.file) ?? { ins: 0, del: 0 };
|
|
2161
|
+
existing.ins += tf.insertions;
|
|
2162
|
+
existing.del += tf.deletions;
|
|
2163
|
+
stats.set(tf.file, existing);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
for (const filePath of untrackedFiles) {
|
|
2167
|
+
try {
|
|
2168
|
+
const content = readFileSync3(join4(repoRoot, filePath), "utf-8");
|
|
2169
|
+
const lines = content.split("\n").length;
|
|
2170
|
+
stats.set(filePath, { ins: lines, del: 0 });
|
|
2171
|
+
} catch {
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
for (const { ins, del } of stats.values()) {
|
|
2175
|
+
totalInsertions += ins;
|
|
2176
|
+
totalDeletions += del;
|
|
2177
|
+
}
|
|
2178
|
+
} catch {
|
|
2179
|
+
}
|
|
2180
|
+
return { stats, totalInsertions, totalDeletions };
|
|
2181
|
+
}
|
|
2182
|
+
async function getFileDiff(git, repoRoot, filePath) {
|
|
2183
|
+
const abs = resolve2(repoRoot, filePath);
|
|
2184
|
+
if (!abs.startsWith(repoRoot + "/") && abs !== repoRoot) {
|
|
2185
|
+
return "";
|
|
2186
|
+
}
|
|
2187
|
+
try {
|
|
2188
|
+
const staged = await git.diff(["--cached", "--", filePath]);
|
|
2189
|
+
const unstaged = await git.diff(["--", filePath]);
|
|
2190
|
+
if (staged && unstaged) return staged + "\n" + unstaged;
|
|
2191
|
+
if (staged) return staged;
|
|
2192
|
+
if (unstaged) return unstaged;
|
|
2193
|
+
try {
|
|
2194
|
+
const content = readFileSync3(join4(repoRoot, filePath), "utf-8");
|
|
2195
|
+
const lines = content.split("\n");
|
|
2196
|
+
const added = lines.map((l) => `+${l}`).join("\n");
|
|
2197
|
+
return `--- /dev/null
|
|
2198
|
+
+++ b/${filePath}
|
|
2199
|
+
@@ -0,0 +1,${lines.length} @@
|
|
2200
|
+
${added}`;
|
|
2201
|
+
} catch {
|
|
2202
|
+
return "";
|
|
2203
|
+
}
|
|
2204
|
+
} catch {
|
|
2205
|
+
return "";
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
async function findRepoRoot(cwd) {
|
|
2209
|
+
try {
|
|
2210
|
+
const root = await simpleGit(cwd).revparse(["--show-toplevel"]);
|
|
2211
|
+
return root.trim();
|
|
2212
|
+
} catch {
|
|
2213
|
+
return null;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
function registerGitRoutes(server, opts) {
|
|
2217
|
+
let repoRoot = null;
|
|
2218
|
+
let git = null;
|
|
2219
|
+
async function ensureGit() {
|
|
2220
|
+
if (git && repoRoot) return { git, root: repoRoot };
|
|
2221
|
+
repoRoot = await findRepoRoot(opts.projectRoot);
|
|
2222
|
+
if (!repoRoot) return null;
|
|
2223
|
+
git = simpleGit(repoRoot);
|
|
2224
|
+
return { git, root: repoRoot };
|
|
2225
|
+
}
|
|
2226
|
+
server.middlewares.use("/via/git/status", (req, res) => {
|
|
2227
|
+
if (req.method !== "GET") {
|
|
2228
|
+
res.statusCode = 405;
|
|
2229
|
+
res.setHeader("Content-Type", "application/json");
|
|
2230
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
res.setHeader("Content-Type", "application/json");
|
|
2234
|
+
ensureGit().then(async (ctx) => {
|
|
2235
|
+
if (!ctx) {
|
|
2236
|
+
res.end(JSON.stringify({ files: [], git: false }));
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const result = await ctx.git.status();
|
|
2240
|
+
const { stats, totalInsertions, totalDeletions } = await getDiffStats(
|
|
2241
|
+
ctx.git,
|
|
2242
|
+
ctx.root,
|
|
2243
|
+
result.not_added
|
|
2244
|
+
);
|
|
2245
|
+
const files = mapStatus(result, stats);
|
|
2246
|
+
res.end(
|
|
2247
|
+
JSON.stringify({
|
|
2248
|
+
files,
|
|
2249
|
+
git: true,
|
|
2250
|
+
insertions: totalInsertions,
|
|
2251
|
+
deletions: totalDeletions
|
|
2252
|
+
})
|
|
2253
|
+
);
|
|
2254
|
+
}).catch(() => {
|
|
2255
|
+
res.end(JSON.stringify({ files: [], git: false }));
|
|
2256
|
+
});
|
|
2257
|
+
});
|
|
2258
|
+
server.middlewares.use("/via/git/diff", (req, res) => {
|
|
2259
|
+
if (req.method !== "GET") {
|
|
2260
|
+
res.statusCode = 405;
|
|
2261
|
+
res.setHeader("Content-Type", "application/json");
|
|
2262
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
res.setHeader("Content-Type", "application/json");
|
|
2266
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2267
|
+
const filePath = url.searchParams.get("path");
|
|
2268
|
+
ensureGit().then(async (ctx) => {
|
|
2269
|
+
if (!ctx) {
|
|
2270
|
+
res.end(JSON.stringify({ diff: "", git: false }));
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
if (filePath) {
|
|
2274
|
+
if (filePath.startsWith("/")) {
|
|
2275
|
+
res.statusCode = 400;
|
|
2276
|
+
res.end(JSON.stringify({ error: "Absolute paths not allowed" }));
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
const diff = await getFileDiff(ctx.git, ctx.root, filePath);
|
|
2280
|
+
res.end(JSON.stringify({ diff, path: filePath }));
|
|
2281
|
+
} else {
|
|
2282
|
+
const staged = await ctx.git.diff(["--cached"]);
|
|
2283
|
+
const unstaged = await ctx.git.diff();
|
|
2284
|
+
const combined = staged && unstaged ? staged + "\n" + unstaged : staged || unstaged || "";
|
|
2285
|
+
res.end(JSON.stringify({ diff: combined }));
|
|
2286
|
+
}
|
|
2287
|
+
}).catch(() => {
|
|
2288
|
+
res.end(JSON.stringify({ diff: "" }));
|
|
2289
|
+
});
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
|
|
1833
2293
|
// src/sandbox.ts
|
|
1834
2294
|
import { randomUUID } from "crypto";
|
|
1835
|
-
import { readFileSync as
|
|
1836
|
-
import { join as
|
|
2295
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
2296
|
+
import { join as join5, relative as relative2 } from "path";
|
|
1837
2297
|
import { Sandbox } from "@vercel/sandbox";
|
|
1838
2298
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1839
2299
|
"node_modules",
|
|
@@ -1849,13 +2309,13 @@ function collectFiles2(dir, base) {
|
|
|
1849
2309
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
1850
2310
|
if (entry.name.startsWith(".") && SKIP_DIRS.has(entry.name)) continue;
|
|
1851
2311
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1852
|
-
const fullPath =
|
|
2312
|
+
const fullPath = join5(dir, entry.name);
|
|
1853
2313
|
const relPath = relative2(base, fullPath);
|
|
1854
2314
|
if (entry.isDirectory()) {
|
|
1855
2315
|
files.push(...collectFiles2(fullPath, base));
|
|
1856
2316
|
} else if (entry.isFile()) {
|
|
1857
2317
|
if (SKIP_FILES.has(entry.name)) continue;
|
|
1858
|
-
files.push({ path: relPath, content:
|
|
2318
|
+
files.push({ path: relPath, content: readFileSync4(fullPath) });
|
|
1859
2319
|
}
|
|
1860
2320
|
}
|
|
1861
2321
|
return files;
|
|
@@ -2032,10 +2492,10 @@ function viagen(options) {
|
|
|
2032
2492
|
claudeBin = findClaudeBin();
|
|
2033
2493
|
logBuffer.init(projectRoot);
|
|
2034
2494
|
wrapLogger(config.logger, logBuffer);
|
|
2035
|
-
const viagenDir =
|
|
2495
|
+
const viagenDir = join6(projectRoot, ".viagen");
|
|
2036
2496
|
mkdirSync2(viagenDir, { recursive: true });
|
|
2037
2497
|
writeFileSync4(
|
|
2038
|
-
|
|
2498
|
+
join6(viagenDir, "config.json"),
|
|
2039
2499
|
JSON.stringify({
|
|
2040
2500
|
sandboxFiles: options?.sandboxFiles ?? [],
|
|
2041
2501
|
editable: options?.editable ?? []
|
|
@@ -2046,21 +2506,7 @@ function viagen(options) {
|
|
|
2046
2506
|
if (!opts.ui) return [];
|
|
2047
2507
|
const url = new URL(ctx.originalUrl || ctx.path, "http://localhost");
|
|
2048
2508
|
const isEmbed = url.searchParams.has("_viagen_embed");
|
|
2049
|
-
if (isEmbed)
|
|
2050
|
-
if (!opts.overlay) return [];
|
|
2051
|
-
return [
|
|
2052
|
-
{
|
|
2053
|
-
tag: "script",
|
|
2054
|
-
children: buildClientScript({
|
|
2055
|
-
position: opts.position,
|
|
2056
|
-
panelWidth: opts.panelWidth,
|
|
2057
|
-
overlay: true,
|
|
2058
|
-
embedMode: true
|
|
2059
|
-
}),
|
|
2060
|
-
injectTo: "body"
|
|
2061
|
-
}
|
|
2062
|
-
];
|
|
2063
|
-
}
|
|
2509
|
+
if (isEmbed && !opts.overlay) return [];
|
|
2064
2510
|
return [
|
|
2065
2511
|
{
|
|
2066
2512
|
tag: "script",
|
|
@@ -2097,9 +2543,18 @@ ${payload.err.frame || ""}`
|
|
|
2097
2543
|
server.middlewares.use(createAuthMiddleware(authToken));
|
|
2098
2544
|
}
|
|
2099
2545
|
const hasEditor = !!(options?.editable && options.editable.length > 0);
|
|
2546
|
+
const clientJs = buildClientScript({
|
|
2547
|
+
position: opts.position,
|
|
2548
|
+
panelWidth: opts.panelWidth,
|
|
2549
|
+
overlay: opts.overlay
|
|
2550
|
+
});
|
|
2551
|
+
server.middlewares.use("/via/client.js", (_req, res) => {
|
|
2552
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
2553
|
+
res.end(clientJs);
|
|
2554
|
+
});
|
|
2100
2555
|
server.middlewares.use("/via/ui", (_req, res) => {
|
|
2101
2556
|
res.setHeader("Content-Type", "text/html");
|
|
2102
|
-
res.end(buildUiHtml({ editable: hasEditor }));
|
|
2557
|
+
res.end(buildUiHtml({ editable: hasEditor, git: true }));
|
|
2103
2558
|
});
|
|
2104
2559
|
server.middlewares.use("/via/iframe", (_req, res) => {
|
|
2105
2560
|
res.setHeader("Content-Type", "text/html");
|
|
@@ -2122,6 +2577,12 @@ ${payload.err.frame || ""}`
|
|
|
2122
2577
|
projectRoot
|
|
2123
2578
|
});
|
|
2124
2579
|
}
|
|
2580
|
+
registerGitRoutes(server, { projectRoot });
|
|
2581
|
+
if (opts.ui) {
|
|
2582
|
+
return () => {
|
|
2583
|
+
server.middlewares.use(createInjectionMiddleware());
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2125
2586
|
}
|
|
2126
2587
|
};
|
|
2127
2588
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viagen",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
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",
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@anthropic-ai/claude-code": "^2.1.42",
|
|
43
43
|
"@vercel/sandbox": "^1",
|
|
44
|
-
"lucide-react": "^0.564.0"
|
|
44
|
+
"lucide-react": "^0.564.0",
|
|
45
|
+
"simple-git": "^3.31.1",
|
|
46
|
+
"viagen-sdk": "^0.0.0"
|
|
45
47
|
},
|
|
46
48
|
"license": "MIT",
|
|
47
49
|
"repository": {
|