viagen 0.0.10 → 0.0.12
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 +13 -78
- package/dist/cli.js +161 -27
- package/dist/index.d.ts +5 -1
- package/dist/index.js +94 -51
- package/package.json +5 -22
- package/site/.viagen/server.log +1 -0
- package/site/icon.png +0 -0
- package/site/icon.svg +2 -0
- package/site/index.html +598 -0
- package/site/vite.config.ts +7 -0
- package/dist/webpack.cjs +0 -1442
- package/dist/webpack.d.cts +0 -62
- package/dist/webpack.d.ts +0 -62
- package/dist/webpack.js +0 -1422
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# viagen
|
|
2
2
|
|
|
3
|
-
A dev server plugin
|
|
3
|
+
A Vite dev server plugin and CLI tool that enables you to use Claude Code in a sandbox — instantly.
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
@@ -14,8 +14,6 @@ A dev server plugin (Vite, webpack, Next.js) and CLI tool that enables you to us
|
|
|
14
14
|
npm install --save-dev viagen
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
### Vite
|
|
18
|
-
|
|
19
17
|
```ts
|
|
20
18
|
// vite.config.ts
|
|
21
19
|
import { defineConfig } from 'vite'
|
|
@@ -26,66 +24,6 @@ export default defineConfig({
|
|
|
26
24
|
})
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
### webpack
|
|
30
|
-
|
|
31
|
-
```js
|
|
32
|
-
// webpack.config.js
|
|
33
|
-
const { setupViagen } = require('viagen/webpack')
|
|
34
|
-
|
|
35
|
-
module.exports = {
|
|
36
|
-
devServer: {
|
|
37
|
-
setupMiddlewares: (middlewares, devServer) => {
|
|
38
|
-
setupViagen(devServer)
|
|
39
|
-
return middlewares
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### Next.js
|
|
46
|
-
|
|
47
|
-
Next.js uses its own dev server, so viagen runs via a custom server file.
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
npm install --save-dev viagen connect
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
// server.ts
|
|
55
|
-
import http from 'node:http'
|
|
56
|
-
import next from 'next'
|
|
57
|
-
import connect from 'connect'
|
|
58
|
-
import { setupViagen } from 'viagen/webpack'
|
|
59
|
-
|
|
60
|
-
const dev = process.env.NODE_ENV !== 'production'
|
|
61
|
-
const app = next({ dev, hostname: 'localhost', port: 3000 })
|
|
62
|
-
const handle = app.getRequestHandler()
|
|
63
|
-
|
|
64
|
-
app.prepare().then(() => {
|
|
65
|
-
const server = connect()
|
|
66
|
-
|
|
67
|
-
setupViagen({
|
|
68
|
-
app: server,
|
|
69
|
-
compiler: { hooks: { done: { tap: () => {} } } },
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
server.use((req, res) => handle(req, res))
|
|
73
|
-
http.createServer(server).listen(3000, () => {
|
|
74
|
-
console.log('> Ready on http://localhost:3000')
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Then update your dev script:
|
|
80
|
-
|
|
81
|
-
```json
|
|
82
|
-
{
|
|
83
|
-
"scripts": {
|
|
84
|
-
"dev": "npx tsx server.ts"
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
27
|
## Step 2 — Setup
|
|
90
28
|
|
|
91
29
|
```bash
|
|
@@ -115,7 +53,7 @@ npx viagen sandbox --timeout 60
|
|
|
115
53
|
npx viagen sandbox stop <sandboxId>
|
|
116
54
|
```
|
|
117
55
|
|
|
118
|
-
##
|
|
56
|
+
## Plugin Options
|
|
119
57
|
|
|
120
58
|
```ts
|
|
121
59
|
viagen({
|
|
@@ -136,8 +74,17 @@ help build and modify the app. Files you edit will trigger Vite HMR
|
|
|
136
74
|
automatically. You can read .viagen/server.log to check recent Vite dev server
|
|
137
75
|
output (compile errors, HMR updates, warnings). When running in a sandbox with
|
|
138
76
|
git, the gh CLI is available and authenticated — you can create pull requests,
|
|
139
|
-
comment on issues, and manage releases.
|
|
140
|
-
|
|
77
|
+
comment on issues, and manage releases.
|
|
78
|
+
|
|
79
|
+
Publishing workflow:
|
|
80
|
+
- If you are on a feature branch (not main/master): commit your changes, push
|
|
81
|
+
to the remote, and create a pull request using "gh pr create". Share the PR URL.
|
|
82
|
+
- If you are on main/master and Vercel credentials are set ($VERCEL_TOKEN):
|
|
83
|
+
commit, push, and run "vercel deploy" to publish a preview. Share the preview URL.
|
|
84
|
+
- Check your current branch with "git branch --show-current" before deciding
|
|
85
|
+
which workflow to use.
|
|
86
|
+
|
|
87
|
+
Be concise.
|
|
141
88
|
```
|
|
142
89
|
|
|
143
90
|
Recent build errors are automatically appended to give Claude context about what went wrong. To customize the prompt, you can replace it entirely or extend the default:
|
|
@@ -154,18 +101,6 @@ viagen({
|
|
|
154
101
|
})
|
|
155
102
|
```
|
|
156
103
|
|
|
157
|
-
## webpack Options
|
|
158
|
-
|
|
159
|
-
```js
|
|
160
|
-
setupViagen(devServer, {
|
|
161
|
-
position: 'bottom-right', // toggle button position
|
|
162
|
-
model: 'sonnet', // claude model
|
|
163
|
-
panelWidth: 420, // chat panel width in px
|
|
164
|
-
ui: true, // inject chat panel into pages
|
|
165
|
-
systemPrompt: '...', // custom system prompt
|
|
166
|
-
})
|
|
167
|
-
```
|
|
168
|
-
|
|
169
104
|
## API
|
|
170
105
|
|
|
171
106
|
Every viagen endpoint is available as an API. Build your own UI, integrate with CI, or script Claude from the command line.
|
package/dist/cli.js
CHANGED
|
@@ -48,20 +48,39 @@ async function deploySandbox(opts) {
|
|
|
48
48
|
const token = randomUUID();
|
|
49
49
|
const useGit = !!opts.git;
|
|
50
50
|
const timeoutMs = (opts.timeoutMinutes ?? 30) * 60 * 1e3;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
const sourceOpts = opts.git ? {
|
|
52
|
+
source: {
|
|
53
|
+
type: "git",
|
|
54
|
+
url: opts.git.remoteUrl,
|
|
55
|
+
username: "x-access-token",
|
|
56
|
+
password: opts.git.token,
|
|
57
|
+
...opts.git.revision ? { revision: opts.git.revision } : {}
|
|
58
|
+
}
|
|
59
|
+
} : {};
|
|
60
|
+
let sandbox2;
|
|
61
|
+
try {
|
|
62
|
+
sandbox2 = await Sandbox.create({
|
|
63
|
+
runtime: "node22",
|
|
64
|
+
ports: [5173],
|
|
65
|
+
timeout: timeoutMs,
|
|
66
|
+
...sourceOpts
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error("\nSandbox creation failed.");
|
|
70
|
+
if (opts.git) {
|
|
71
|
+
console.error(` URL: ${opts.git.remoteUrl}`);
|
|
72
|
+
console.error(` Revision: ${opts.git.revision ?? "(default branch)"}`);
|
|
73
|
+
console.error(` Branch: ${opts.git.branch}`);
|
|
74
|
+
}
|
|
75
|
+
const apiErr = err;
|
|
76
|
+
if (apiErr.message) console.error(` Message: ${apiErr.message}`);
|
|
77
|
+
if (apiErr.json) {
|
|
78
|
+
console.error(` Response: ${JSON.stringify(apiErr.json, null, 2)}`);
|
|
79
|
+
} else if (apiErr.text) {
|
|
80
|
+
console.error(` Response: ${apiErr.text}`);
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
65
84
|
try {
|
|
66
85
|
if (useGit && opts.git) {
|
|
67
86
|
await sandbox2.runCommand("git", [
|
|
@@ -108,26 +127,27 @@ async function deploySandbox(opts) {
|
|
|
108
127
|
await sandbox2.writeFiles(files);
|
|
109
128
|
}
|
|
110
129
|
}
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
];
|
|
130
|
+
const envMap = { ...opts.envVars ?? {} };
|
|
131
|
+
envMap["VIAGEN_AUTH_TOKEN"] = token;
|
|
132
|
+
envMap["VIAGEN_SESSION_START"] = String(Math.floor(Date.now() / 1e3));
|
|
133
|
+
envMap["VIAGEN_SESSION_TIMEOUT"] = String((opts.timeoutMinutes ?? 30) * 60);
|
|
116
134
|
if (opts.apiKey) {
|
|
117
|
-
|
|
135
|
+
envMap["ANTHROPIC_API_KEY"] = opts.apiKey;
|
|
118
136
|
} else if (opts.oauth) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
137
|
+
envMap["CLAUDE_ACCESS_TOKEN"] = opts.oauth.accessToken;
|
|
138
|
+
envMap["CLAUDE_REFRESH_TOKEN"] = opts.oauth.refreshToken;
|
|
139
|
+
envMap["CLAUDE_TOKEN_EXPIRES"] = opts.oauth.tokenExpires;
|
|
122
140
|
}
|
|
123
141
|
if (opts.git) {
|
|
124
|
-
|
|
142
|
+
envMap["GITHUB_TOKEN"] = opts.git.token;
|
|
143
|
+
envMap["VIAGEN_BRANCH"] = opts.git.branch;
|
|
125
144
|
}
|
|
126
145
|
if (opts.vercel) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
146
|
+
envMap["VERCEL_TOKEN"] = opts.vercel.token;
|
|
147
|
+
envMap["VERCEL_ORG_ID"] = opts.vercel.teamId;
|
|
148
|
+
envMap["VERCEL_PROJECT_ID"] = opts.vercel.projectId;
|
|
130
149
|
}
|
|
150
|
+
const envLines = Object.entries(envMap).map(([k, v]) => `${k}=${v}`);
|
|
131
151
|
await sandbox2.writeFiles([
|
|
132
152
|
{
|
|
133
153
|
path: ".env",
|
|
@@ -297,6 +317,22 @@ async function oauthConsoleFlow() {
|
|
|
297
317
|
const keyData = await keyRes.json();
|
|
298
318
|
return keyData.raw_key;
|
|
299
319
|
}
|
|
320
|
+
async function refreshAccessToken(refresh) {
|
|
321
|
+
const res = await fetch(TOKEN_ENDPOINT, {
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: { "Content-Type": "application/json" },
|
|
324
|
+
body: JSON.stringify({
|
|
325
|
+
grant_type: "refresh_token",
|
|
326
|
+
client_id: CLIENT_ID,
|
|
327
|
+
refresh_token: refresh
|
|
328
|
+
})
|
|
329
|
+
});
|
|
330
|
+
if (!res.ok) {
|
|
331
|
+
const text = await res.text();
|
|
332
|
+
throw new Error(`Token refresh failed (${res.status}): ${text}`);
|
|
333
|
+
}
|
|
334
|
+
return await res.json();
|
|
335
|
+
}
|
|
300
336
|
|
|
301
337
|
// src/cli.ts
|
|
302
338
|
function loadDotenv(dir) {
|
|
@@ -334,6 +370,18 @@ function writeEnvVars(dir, vars) {
|
|
|
334
370
|
content += toAdd.join("\n") + "\n";
|
|
335
371
|
writeFileSync(envPath, content);
|
|
336
372
|
}
|
|
373
|
+
function updateEnvVars(dir, vars) {
|
|
374
|
+
const envPath = join2(dir, ".env");
|
|
375
|
+
if (!existsSync(envPath)) return;
|
|
376
|
+
let content = readFileSync2(envPath, "utf-8");
|
|
377
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
378
|
+
const re = new RegExp(`^${key}=.*$`, "m");
|
|
379
|
+
if (re.test(content)) {
|
|
380
|
+
content = content.replace(re, `${key}=${val}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
writeFileSync(envPath, content);
|
|
384
|
+
}
|
|
337
385
|
function openBrowser2(url) {
|
|
338
386
|
try {
|
|
339
387
|
const platform = process.platform;
|
|
@@ -384,6 +432,42 @@ function getGitInfo(cwd) {
|
|
|
384
432
|
return null;
|
|
385
433
|
}
|
|
386
434
|
}
|
|
435
|
+
function remoteBranchExists(remoteUrl, branch, token) {
|
|
436
|
+
try {
|
|
437
|
+
const url = new URL(remoteUrl);
|
|
438
|
+
url.username = "x-access-token";
|
|
439
|
+
url.password = token;
|
|
440
|
+
const out = execSync2(
|
|
441
|
+
`git ls-remote --heads ${url.toString()} refs/heads/${branch}`,
|
|
442
|
+
{ encoding: "utf-8", stdio: "pipe" }
|
|
443
|
+
).trim();
|
|
444
|
+
return out.length > 0;
|
|
445
|
+
} catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function repoNwo(remoteUrl) {
|
|
450
|
+
const m = remoteUrl.match(/github\.com[/:]([^/]+\/[^/.]+?)(?:\.git)?$/);
|
|
451
|
+
return m ? m[1] : null;
|
|
452
|
+
}
|
|
453
|
+
function createRemoteBranch(remoteUrl, branch, token) {
|
|
454
|
+
const nwo = repoNwo(remoteUrl);
|
|
455
|
+
if (!nwo) return false;
|
|
456
|
+
try {
|
|
457
|
+
const sha = execSync2(
|
|
458
|
+
`gh api repos/${nwo}/git/ref/heads/main --jq .object.sha`,
|
|
459
|
+
{ encoding: "utf-8", stdio: "pipe", env: { ...process.env, GH_TOKEN: token } }
|
|
460
|
+
).trim();
|
|
461
|
+
if (!sha) return false;
|
|
462
|
+
execSync2(
|
|
463
|
+
`gh api repos/${nwo}/git/refs -f ref=refs/heads/${branch} -f sha=${sha}`,
|
|
464
|
+
{ encoding: "utf-8", stdio: "pipe", env: { ...process.env, GH_TOKEN: token } }
|
|
465
|
+
);
|
|
466
|
+
return true;
|
|
467
|
+
} catch {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
387
471
|
function promptUser(question) {
|
|
388
472
|
if (!process.stdin.isTTY) return Promise.resolve("");
|
|
389
473
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -650,6 +734,31 @@ async function sandbox(args) {
|
|
|
650
734
|
);
|
|
651
735
|
process.exit(1);
|
|
652
736
|
}
|
|
737
|
+
if (hasOAuth && env["CLAUDE_REFRESH_TOKEN"]) {
|
|
738
|
+
const expires = parseInt(env["CLAUDE_TOKEN_EXPIRES"] || "0", 10);
|
|
739
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
740
|
+
if (nowSec > expires - 300) {
|
|
741
|
+
console.log("Refreshing Claude OAuth tokens...");
|
|
742
|
+
try {
|
|
743
|
+
const tokens = await refreshAccessToken(env["CLAUDE_REFRESH_TOKEN"]);
|
|
744
|
+
env["CLAUDE_ACCESS_TOKEN"] = tokens.access_token;
|
|
745
|
+
env["CLAUDE_REFRESH_TOKEN"] = tokens.refresh_token;
|
|
746
|
+
env["CLAUDE_TOKEN_EXPIRES"] = String(nowSec + tokens.expires_in);
|
|
747
|
+
updateEnvVars(cwd, {
|
|
748
|
+
CLAUDE_ACCESS_TOKEN: tokens.access_token,
|
|
749
|
+
CLAUDE_REFRESH_TOKEN: tokens.refresh_token,
|
|
750
|
+
CLAUDE_TOKEN_EXPIRES: String(nowSec + tokens.expires_in)
|
|
751
|
+
});
|
|
752
|
+
console.log(" Tokens refreshed.");
|
|
753
|
+
} catch (err) {
|
|
754
|
+
console.error(
|
|
755
|
+
"Failed to refresh OAuth tokens. Run `npx viagen setup` to re-authenticate."
|
|
756
|
+
);
|
|
757
|
+
console.error(` ${err instanceof Error ? err.message : String(err)}`);
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
653
762
|
const hasOidc = !!env["VERCEL_OIDC_TOKEN"];
|
|
654
763
|
const hasToken = !!env["VERCEL_TOKEN"] && !!env["VERCEL_TEAM_ID"] && !!env["VERCEL_PROJECT_ID"];
|
|
655
764
|
if (!hasOidc && !hasToken) {
|
|
@@ -664,9 +773,33 @@ async function sandbox(args) {
|
|
|
664
773
|
let overlayFiles;
|
|
665
774
|
const branch = branchOverride || (gitInfo ? gitInfo.branch : "main");
|
|
666
775
|
if (gitInfo && githubToken) {
|
|
776
|
+
let branchExistsOnRemote = remoteBranchExists(
|
|
777
|
+
gitInfo.remoteUrl,
|
|
778
|
+
branch,
|
|
779
|
+
githubToken
|
|
780
|
+
);
|
|
781
|
+
if (!branchExistsOnRemote) {
|
|
782
|
+
console.log(
|
|
783
|
+
`Branch "${branch}" not found on remote \u2014 creating it...`
|
|
784
|
+
);
|
|
785
|
+
const created = createRemoteBranch(
|
|
786
|
+
gitInfo.remoteUrl,
|
|
787
|
+
branch,
|
|
788
|
+
githubToken
|
|
789
|
+
);
|
|
790
|
+
if (created) {
|
|
791
|
+
console.log(` Branch "${branch}" created on remote (from main).`);
|
|
792
|
+
branchExistsOnRemote = true;
|
|
793
|
+
} else {
|
|
794
|
+
console.log(
|
|
795
|
+
` Could not create branch on remote \u2014 will clone default branch and create locally.`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
667
799
|
const makeGitInfo = () => ({
|
|
668
800
|
remoteUrl: gitInfo.remoteUrl,
|
|
669
801
|
branch,
|
|
802
|
+
revision: branchExistsOnRemote ? branch : void 0,
|
|
670
803
|
userName: gitInfo.userName,
|
|
671
804
|
userEmail: gitInfo.userEmail,
|
|
672
805
|
token: githubToken
|
|
@@ -718,6 +851,7 @@ async function sandbox(args) {
|
|
|
718
851
|
} : void 0,
|
|
719
852
|
git: deployGit,
|
|
720
853
|
overlayFiles,
|
|
854
|
+
envVars: dotenv,
|
|
721
855
|
vercel: env["VERCEL_TOKEN"] && env["VERCEL_TEAM_ID"] && env["VERCEL_PROJECT_ID"] ? {
|
|
722
856
|
token: env["VERCEL_TOKEN"],
|
|
723
857
|
teamId: env["VERCEL_TEAM_ID"],
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
2
|
|
|
3
|
-
declare const DEFAULT_SYSTEM_PROMPT = "You are embedded in a Vite dev server as the \"viagen\" plugin. Your job is to help build and modify the app. Files you edit will trigger Vite HMR automatically. You can read .viagen/server.log to check recent Vite dev server output (compile errors, HMR updates, warnings). When running in a sandbox with git, the gh CLI is available and authenticated \u2014 you can create pull requests, comment on issues, and manage releases. If Vercel credentials are set,
|
|
3
|
+
declare const DEFAULT_SYSTEM_PROMPT = "You are embedded in a Vite dev server as the \"viagen\" plugin. Your job is to help build and modify the app. Files you edit will trigger Vite HMR automatically. You can read .viagen/server.log to check recent Vite dev server output (compile errors, HMR updates, warnings). When running in a sandbox with git, the gh CLI is available and authenticated \u2014 you can create pull requests, comment on issues, and manage releases.\n\nPublishing workflow:\n- If you are on a feature branch (not main/master): commit your changes, push to the remote, and create a pull request using \"gh pr create\". Share the PR URL.\n- If you are on main/master and Vercel credentials are set ($VERCEL_TOKEN): commit, push, and run \"vercel deploy\" to publish a preview. Share the preview URL.\n- Check your current branch with \"git branch --show-current\" before deciding which workflow to use.\n\nBe concise.";
|
|
4
4
|
|
|
5
5
|
interface GitInfo {
|
|
6
6
|
/** HTTPS remote URL (transformed from SSH if needed). */
|
|
7
7
|
remoteUrl: string;
|
|
8
8
|
/** Branch to check out. */
|
|
9
9
|
branch: string;
|
|
10
|
+
/** Revision to clone (branch/tag/sha). If omitted, clones the default branch. */
|
|
11
|
+
revision?: string;
|
|
10
12
|
/** Git user name for commits. */
|
|
11
13
|
userName: string;
|
|
12
14
|
/** Git user email for commits. */
|
|
@@ -40,6 +42,8 @@ interface DeploySandboxOptions {
|
|
|
40
42
|
};
|
|
41
43
|
/** Sandbox timeout in minutes (default: 30, max depends on Vercel plan). */
|
|
42
44
|
timeoutMinutes?: number;
|
|
45
|
+
/** User's .env variables to forward into the sandbox. */
|
|
46
|
+
envVars?: Record<string, string>;
|
|
43
47
|
}
|
|
44
48
|
interface DeploySandboxResult {
|
|
45
49
|
/** Full URL with auth token in query string. */
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
1
|
// src/index.ts
|
|
2
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3
|
+
import { dirname, join as join3 } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
9
5
|
import { loadEnv } from "vite";
|
|
10
6
|
|
|
11
7
|
// src/logger.ts
|
|
@@ -59,14 +55,15 @@ function wrapLogger(logger, buffer) {
|
|
|
59
55
|
}
|
|
60
56
|
|
|
61
57
|
// src/health.ts
|
|
62
|
-
function registerHealthRoutes(
|
|
63
|
-
|
|
58
|
+
function registerHealthRoutes(server, env, errorRef) {
|
|
59
|
+
server.middlewares.use("/via/error", (_req, res) => {
|
|
64
60
|
res.setHeader("Content-Type", "application/json");
|
|
65
61
|
res.end(JSON.stringify({ error: errorRef.get() }));
|
|
66
62
|
});
|
|
67
|
-
|
|
63
|
+
server.middlewares.use("/via/health", (_req, res) => {
|
|
68
64
|
const configured = !!env["ANTHROPIC_API_KEY"] || !!env["CLAUDE_ACCESS_TOKEN"];
|
|
69
65
|
const git = !!env["GITHUB_TOKEN"];
|
|
66
|
+
const branch = env["VIAGEN_BRANCH"] || null;
|
|
70
67
|
const sessionStart = env["VIAGEN_SESSION_START"] ? parseInt(env["VIAGEN_SESSION_START"], 10) : null;
|
|
71
68
|
const sessionTimeout = env["VIAGEN_SESSION_TIMEOUT"] ? parseInt(env["VIAGEN_SESSION_TIMEOUT"], 10) : null;
|
|
72
69
|
const session = sessionStart && sessionTimeout ? {
|
|
@@ -76,10 +73,10 @@ function registerHealthRoutes(app, env, errorRef) {
|
|
|
76
73
|
} : null;
|
|
77
74
|
res.setHeader("Content-Type", "application/json");
|
|
78
75
|
if (configured) {
|
|
79
|
-
res.end(JSON.stringify({ status: "ok", configured: true, git, session }));
|
|
76
|
+
res.end(JSON.stringify({ status: "ok", configured: true, git, branch, session }));
|
|
80
77
|
} else {
|
|
81
78
|
res.end(
|
|
82
|
-
JSON.stringify({ status: "error", configured: false, git, session })
|
|
79
|
+
JSON.stringify({ status: "error", configured: false, git, branch, session })
|
|
83
80
|
);
|
|
84
81
|
}
|
|
85
82
|
});
|
|
@@ -123,20 +120,27 @@ function readBody(req) {
|
|
|
123
120
|
req.on("error", reject);
|
|
124
121
|
});
|
|
125
122
|
}
|
|
126
|
-
var DEFAULT_SYSTEM_PROMPT = `You are embedded in a Vite dev server as the "viagen" plugin. Your job is to help build and modify the app. Files you edit will trigger Vite HMR automatically. You can read .viagen/server.log to check recent Vite dev server output (compile errors, HMR updates, warnings). When running in a sandbox with git, the gh CLI is available and authenticated \u2014 you can create pull requests, comment on issues, and manage releases.
|
|
123
|
+
var DEFAULT_SYSTEM_PROMPT = `You are embedded in a Vite dev server as the "viagen" plugin. Your job is to help build and modify the app. Files you edit will trigger Vite HMR automatically. You can read .viagen/server.log to check recent Vite dev server output (compile errors, HMR updates, warnings). When running in a sandbox with git, the gh CLI is available and authenticated \u2014 you can create pull requests, comment on issues, and manage releases.
|
|
124
|
+
|
|
125
|
+
Publishing workflow:
|
|
126
|
+
- If you are on a feature branch (not main/master): commit your changes, push to the remote, and create a pull request using "gh pr create". Share the PR URL.
|
|
127
|
+
- If you are on main/master and Vercel credentials are set ($VERCEL_TOKEN): commit, push, and run "vercel deploy" to publish a preview. Share the preview URL.
|
|
128
|
+
- Check your current branch with "git branch --show-current" before deciding which workflow to use.
|
|
129
|
+
|
|
130
|
+
Be concise.`;
|
|
127
131
|
function findClaudeBin() {
|
|
128
|
-
const _require =
|
|
132
|
+
const _require = createRequire(import.meta.url);
|
|
129
133
|
const pkgPath = _require.resolve("@anthropic-ai/claude-code/package.json");
|
|
130
134
|
return pkgPath.replace("package.json", "cli.js");
|
|
131
135
|
}
|
|
132
|
-
function registerChatRoutes(
|
|
136
|
+
function registerChatRoutes(server, opts) {
|
|
133
137
|
let sessionId;
|
|
134
|
-
|
|
138
|
+
server.middlewares.use("/via/chat/reset", (_req, res) => {
|
|
135
139
|
sessionId = void 0;
|
|
136
140
|
res.setHeader("Content-Type", "application/json");
|
|
137
141
|
res.end(JSON.stringify({ status: "ok" }));
|
|
138
142
|
});
|
|
139
|
-
|
|
143
|
+
server.middlewares.use("/via/chat", async (req, res) => {
|
|
140
144
|
if (req.method !== "POST") {
|
|
141
145
|
res.statusCode = 405;
|
|
142
146
|
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
@@ -321,7 +325,7 @@ function buildClientScript(opts) {
|
|
|
321
325
|
/* js */
|
|
322
326
|
`
|
|
323
327
|
(function() {
|
|
324
|
-
var OVERLAY_ENABLED = ${opts.overlay
|
|
328
|
+
var OVERLAY_ENABLED = ${opts.overlay};
|
|
325
329
|
var EMBED_MODE = ${opts.embedMode ? "true" : "false"};
|
|
326
330
|
|
|
327
331
|
/* ---- Error overlay: inject Fix button into shadow DOM ---- */
|
|
@@ -387,7 +391,7 @@ function buildClientScript(opts) {
|
|
|
387
391
|
var errorData = await errorRes.json();
|
|
388
392
|
if (!errorData.error) { win.classList.remove('viagen-fixing'); return; }
|
|
389
393
|
var e = errorData.error;
|
|
390
|
-
var prompt = 'Fix this build error in ' +
|
|
394
|
+
var prompt = 'Fix this Vite build error in ' +
|
|
391
395
|
(e.loc ? e.loc.file + ':' + e.loc.line : 'unknown file') +
|
|
392
396
|
':\\n\\n' + e.message +
|
|
393
397
|
(e.frame ? '\\n\\nCode frame:\\n' + e.frame : '');
|
|
@@ -1136,7 +1140,11 @@ function buildUiHtml() {
|
|
|
1136
1140
|
});
|
|
1137
1141
|
publishBtn.addEventListener('click', function () {
|
|
1138
1142
|
if (isStreaming) return;
|
|
1139
|
-
|
|
1143
|
+
if (publishBtn.dataset.branch && publishBtn.dataset.branch !== 'main' && publishBtn.dataset.branch !== 'master') {
|
|
1144
|
+
inputEl.value = 'Commit all changes, push to the remote branch, and create a pull request using gh pr create. Share the PR URL.';
|
|
1145
|
+
} else {
|
|
1146
|
+
inputEl.value = 'Commit all changes, push to the remote repository, and run vercel deploy to get a preview URL';
|
|
1147
|
+
}
|
|
1140
1148
|
send();
|
|
1141
1149
|
});
|
|
1142
1150
|
|
|
@@ -1186,7 +1194,10 @@ function buildUiHtml() {
|
|
|
1186
1194
|
var banner = document.getElementById('setup-banner');
|
|
1187
1195
|
if (data.configured) {
|
|
1188
1196
|
dot.className = 'status-dot ok';
|
|
1189
|
-
if (data.git)
|
|
1197
|
+
if (data.git) {
|
|
1198
|
+
publishBtn.style.display = '';
|
|
1199
|
+
if (data.branch) publishBtn.dataset.branch = data.branch;
|
|
1200
|
+
}
|
|
1190
1201
|
} else {
|
|
1191
1202
|
dot.className = 'status-dot error';
|
|
1192
1203
|
inputEl.disabled = true;
|
|
@@ -1276,7 +1287,7 @@ function createAuthMiddleware(token) {
|
|
|
1276
1287
|
const cleanUrl = url.pathname + (url.search || "");
|
|
1277
1288
|
res.setHeader(
|
|
1278
1289
|
"Set-Cookie",
|
|
1279
|
-
`viagen_session=${token}; HttpOnly; SameSite=
|
|
1290
|
+
`viagen_session=${token}; HttpOnly; SameSite=Lax; Path=/; Secure`
|
|
1280
1291
|
);
|
|
1281
1292
|
res.writeHead(302, { Location: cleanUrl });
|
|
1282
1293
|
res.end();
|
|
@@ -1329,20 +1340,39 @@ async function deploySandbox(opts) {
|
|
|
1329
1340
|
const token = randomUUID();
|
|
1330
1341
|
const useGit = !!opts.git;
|
|
1331
1342
|
const timeoutMs = (opts.timeoutMinutes ?? 30) * 60 * 1e3;
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1343
|
+
const sourceOpts = opts.git ? {
|
|
1344
|
+
source: {
|
|
1345
|
+
type: "git",
|
|
1346
|
+
url: opts.git.remoteUrl,
|
|
1347
|
+
username: "x-access-token",
|
|
1348
|
+
password: opts.git.token,
|
|
1349
|
+
...opts.git.revision ? { revision: opts.git.revision } : {}
|
|
1350
|
+
}
|
|
1351
|
+
} : {};
|
|
1352
|
+
let sandbox;
|
|
1353
|
+
try {
|
|
1354
|
+
sandbox = await Sandbox.create({
|
|
1355
|
+
runtime: "node22",
|
|
1356
|
+
ports: [5173],
|
|
1357
|
+
timeout: timeoutMs,
|
|
1358
|
+
...sourceOpts
|
|
1359
|
+
});
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
console.error("\nSandbox creation failed.");
|
|
1362
|
+
if (opts.git) {
|
|
1363
|
+
console.error(` URL: ${opts.git.remoteUrl}`);
|
|
1364
|
+
console.error(` Revision: ${opts.git.revision ?? "(default branch)"}`);
|
|
1365
|
+
console.error(` Branch: ${opts.git.branch}`);
|
|
1366
|
+
}
|
|
1367
|
+
const apiErr = err;
|
|
1368
|
+
if (apiErr.message) console.error(` Message: ${apiErr.message}`);
|
|
1369
|
+
if (apiErr.json) {
|
|
1370
|
+
console.error(` Response: ${JSON.stringify(apiErr.json, null, 2)}`);
|
|
1371
|
+
} else if (apiErr.text) {
|
|
1372
|
+
console.error(` Response: ${apiErr.text}`);
|
|
1373
|
+
}
|
|
1374
|
+
throw err;
|
|
1375
|
+
}
|
|
1346
1376
|
try {
|
|
1347
1377
|
if (useGit && opts.git) {
|
|
1348
1378
|
await sandbox.runCommand("git", [
|
|
@@ -1389,26 +1419,27 @@ async function deploySandbox(opts) {
|
|
|
1389
1419
|
await sandbox.writeFiles(files);
|
|
1390
1420
|
}
|
|
1391
1421
|
}
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
];
|
|
1422
|
+
const envMap = { ...opts.envVars ?? {} };
|
|
1423
|
+
envMap["VIAGEN_AUTH_TOKEN"] = token;
|
|
1424
|
+
envMap["VIAGEN_SESSION_START"] = String(Math.floor(Date.now() / 1e3));
|
|
1425
|
+
envMap["VIAGEN_SESSION_TIMEOUT"] = String((opts.timeoutMinutes ?? 30) * 60);
|
|
1397
1426
|
if (opts.apiKey) {
|
|
1398
|
-
|
|
1427
|
+
envMap["ANTHROPIC_API_KEY"] = opts.apiKey;
|
|
1399
1428
|
} else if (opts.oauth) {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1429
|
+
envMap["CLAUDE_ACCESS_TOKEN"] = opts.oauth.accessToken;
|
|
1430
|
+
envMap["CLAUDE_REFRESH_TOKEN"] = opts.oauth.refreshToken;
|
|
1431
|
+
envMap["CLAUDE_TOKEN_EXPIRES"] = opts.oauth.tokenExpires;
|
|
1403
1432
|
}
|
|
1404
1433
|
if (opts.git) {
|
|
1405
|
-
|
|
1434
|
+
envMap["GITHUB_TOKEN"] = opts.git.token;
|
|
1435
|
+
envMap["VIAGEN_BRANCH"] = opts.git.branch;
|
|
1406
1436
|
}
|
|
1407
1437
|
if (opts.vercel) {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1438
|
+
envMap["VERCEL_TOKEN"] = opts.vercel.token;
|
|
1439
|
+
envMap["VERCEL_ORG_ID"] = opts.vercel.teamId;
|
|
1440
|
+
envMap["VERCEL_PROJECT_ID"] = opts.vercel.projectId;
|
|
1411
1441
|
}
|
|
1442
|
+
const envLines = Object.entries(envMap).map(([k, v]) => `${k}=${v}`);
|
|
1412
1443
|
await sandbox.writeFiles([
|
|
1413
1444
|
{
|
|
1414
1445
|
path: ".env",
|
|
@@ -1443,6 +1474,14 @@ async function deploySandbox(opts) {
|
|
|
1443
1474
|
}
|
|
1444
1475
|
|
|
1445
1476
|
// src/index.ts
|
|
1477
|
+
var docsHtmlCache;
|
|
1478
|
+
function getDocsHtml() {
|
|
1479
|
+
if (!docsHtmlCache) {
|
|
1480
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
1481
|
+
docsHtmlCache = readFileSync2(join3(dir, "..", "site", "index.html"), "utf-8");
|
|
1482
|
+
}
|
|
1483
|
+
return docsHtmlCache;
|
|
1484
|
+
}
|
|
1446
1485
|
function viagen(options) {
|
|
1447
1486
|
const opts = {
|
|
1448
1487
|
position: options?.position ?? "bottom-right",
|
|
@@ -1536,10 +1575,14 @@ ${payload.err.frame || ""}`
|
|
|
1536
1575
|
res.setHeader("Content-Type", "text/html");
|
|
1537
1576
|
res.end(buildIframeHtml({ panelWidth: opts.panelWidth }));
|
|
1538
1577
|
});
|
|
1539
|
-
|
|
1578
|
+
server.middlewares.use("/via/docs", (_req, res) => {
|
|
1579
|
+
res.setHeader("Content-Type", "text/html");
|
|
1580
|
+
res.end(getDocsHtml());
|
|
1581
|
+
});
|
|
1582
|
+
registerHealthRoutes(server, env, {
|
|
1540
1583
|
get: () => lastError
|
|
1541
1584
|
});
|
|
1542
|
-
registerChatRoutes(server
|
|
1585
|
+
registerChatRoutes(server, {
|
|
1543
1586
|
env,
|
|
1544
1587
|
projectRoot,
|
|
1545
1588
|
logBuffer,
|