viagen 0.1.2 → 0.1.3
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 +189 -0
- package/dist/cli.js +24 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +527 -78
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# viagen
|
|
2
|
+
|
|
3
|
+
A Vite dev server plugin and CLI tool that enables you to use Claude Code in a sandbox — instantly.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- [Claude](https://claude.ai/signup) — Max, Pro, or API plan. The setup wizard handles auth.
|
|
8
|
+
- [Vercel](https://vercel.com/signup) — Free plan works. Sandboxes last 45 min on Hobby, 5 hours on Pro.
|
|
9
|
+
- [GitHub CLI](https://cli.github.com) — Enables git clone and push from sandboxes.
|
|
10
|
+
|
|
11
|
+
## Quick Setup (Claude Code Plugin)
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/plugin marketplace add viagen-dev/viagen-claude-plugin
|
|
15
|
+
/plugin install viagen@viagen-marketplace
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Restart Claude Code to load the plugin.**
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
/viagen-install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The plugin will handle npm installation, vite config updates, and run the setup wizard for you.
|
|
25
|
+
|
|
26
|
+
## Manual Setup
|
|
27
|
+
|
|
28
|
+
### Step 1 — Add viagen to your app
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install --save-dev viagen
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// vite.config.ts
|
|
36
|
+
import { defineConfig } from 'vite'
|
|
37
|
+
import { viagen } from 'viagen'
|
|
38
|
+
|
|
39
|
+
export default defineConfig({
|
|
40
|
+
plugins: [viagen()],
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Step 2 — Setup
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx viagen setup
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
52
|
+
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.
|
|
53
|
+
|
|
54
|
+
### Step 3 — Sandbox
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx viagen sandbox
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Deploys your dev server to a remote Vercel Sandbox — an isolated VM-like environment where Claude can read, write, and push code.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Deploy on a specific branch
|
|
64
|
+
npx viagen sandbox --branch feature/my-thing
|
|
65
|
+
|
|
66
|
+
# Set a longer timeout (default: 30 min)
|
|
67
|
+
npx viagen sandbox --timeout 60
|
|
68
|
+
|
|
69
|
+
# Auto-send a prompt on load
|
|
70
|
+
npx viagen sandbox --prompt "build me a landing page"
|
|
71
|
+
|
|
72
|
+
# Stop a running sandbox
|
|
73
|
+
npx viagen sandbox stop <sandboxId>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## Plugin Options
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
viagen({
|
|
81
|
+
position: 'bottom-right', // toggle button position
|
|
82
|
+
model: 'sonnet', // claude model
|
|
83
|
+
panelWidth: 375, // chat panel width in px
|
|
84
|
+
overlay: true, // fix button on error overlay
|
|
85
|
+
ui: true, // inject chat panel into pages
|
|
86
|
+
sandboxFiles: [...], // copy files manually into sandbox
|
|
87
|
+
systemPrompt: '...', // custom system prompt (see below)
|
|
88
|
+
editable: ['src','conf'], // files/dirs editable in the UI
|
|
89
|
+
mcpServers: { ... }, // additional MCP servers for Claude
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
### Custom MCP Servers
|
|
95
|
+
|
|
96
|
+
Pass additional [MCP server](https://modelcontextprotocol.io) configurations to give Claude access to custom tools:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
viagen({
|
|
100
|
+
mcpServers: {
|
|
101
|
+
'my-db': {
|
|
102
|
+
command: 'npx',
|
|
103
|
+
args: ['-y', '@my-org/db-mcp-server'],
|
|
104
|
+
env: { DATABASE_URL: process.env.DATABASE_URL },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
These are merged with viagen's built-in platform tools (when connected). User-provided servers take precedence if names collide.
|
|
111
|
+
|
|
112
|
+
### Editable Files
|
|
113
|
+
|
|
114
|
+
Add a file editor panel to the chat UI:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
viagen({
|
|
118
|
+
editable: ['src/components', 'vite.config.ts']
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Paths can be files or directories (directories include all files within). The editor appears as a "Files" tab in the chat panel with a collapsible directory tree, syntax highlighting (TypeScript, JavaScript, CSS, HTML, JSON, Markdown), and image preview.
|
|
123
|
+
|
|
124
|
+
The default system prompt tells Claude it's embedded in a Vite dev server, that file edits trigger HMR, and how to check server logs. Recent build errors are automatically appended to give Claude context about what went wrong.
|
|
125
|
+
|
|
126
|
+
To customize the prompt, you can replace it entirely or extend the default:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { viagen, DEFAULT_SYSTEM_PROMPT } from 'viagen'
|
|
130
|
+
|
|
131
|
+
viagen({
|
|
132
|
+
// Replace entirely
|
|
133
|
+
systemPrompt: 'You are a React expert. Only use TypeScript.',
|
|
134
|
+
|
|
135
|
+
// Or extend the default
|
|
136
|
+
systemPrompt: DEFAULT_SYSTEM_PROMPT + '\nAlways use Tailwind for styling.',
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## API
|
|
141
|
+
|
|
142
|
+
Every viagen endpoint is available as an API. Build your own UI, integrate with CI, or script Claude from the command line.
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
POST /via/chat — send a message, streamed SSE response
|
|
146
|
+
POST /via/chat/reset — clear conversation history
|
|
147
|
+
GET /via/health — check API key status
|
|
148
|
+
GET /via/error — latest build error (if any)
|
|
149
|
+
GET /via/ui — standalone chat interface
|
|
150
|
+
GET /via/iframe — split view (app + chat side by side)
|
|
151
|
+
GET /via/files — list editable files (when configured)
|
|
152
|
+
GET /via/file?path= — read file content
|
|
153
|
+
POST /via/file — write file content { path, content }
|
|
154
|
+
GET /via/file/raw — serve raw file (images, etc.) with correct MIME type
|
|
155
|
+
GET /via/git/status — list changed files (git status)
|
|
156
|
+
GET /via/git/diff — full diff, or single file with ?path=
|
|
157
|
+
GET /via/git/branch — current branch, remote URL, open PR info
|
|
158
|
+
GET /via/logs — dev server log entries, optional ?since=<timestamp>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
When `VIAGEN_AUTH_TOKEN` is set (always on in sandboxes), pass the token as a `Bearer` header, a `/t/:token` path segment, or a `?token=` query param.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# With curl
|
|
165
|
+
curl -X POST http://localhost:5173/via/chat \
|
|
166
|
+
-H "Authorization: Bearer $VIAGEN_AUTH_TOKEN" \
|
|
167
|
+
-H "Content-Type: application/json" \
|
|
168
|
+
-d '{"message": "add a hello world route"}'
|
|
169
|
+
|
|
170
|
+
# Or pass the token in the URL path (sets a session cookie)
|
|
171
|
+
open "http://localhost:5173/via/ui/t/$VIAGEN_AUTH_TOKEN"
|
|
172
|
+
|
|
173
|
+
# ?token= query param also works (fallback for backwards compat)
|
|
174
|
+
open "http://localhost:5173/via/ui?token=$VIAGEN_AUTH_TOKEN"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npm install
|
|
181
|
+
npm run dev # Dev server (site)
|
|
182
|
+
npm run build # Build with tsup
|
|
183
|
+
npm run test # Run tests
|
|
184
|
+
npm run typecheck # Type check
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
[MIT](LICENSE)
|
package/dist/cli.js
CHANGED
|
@@ -103,6 +103,11 @@ async function waitForServer(baseUrl, token, devServer) {
|
|
|
103
103
|
async function deploySandbox(opts) {
|
|
104
104
|
const token = randomUUID();
|
|
105
105
|
const useGit = !!opts.git;
|
|
106
|
+
const rootDir = opts.rootDir ?? null;
|
|
107
|
+
const rootPrefix = rootDir ? `${rootDir}/` : "";
|
|
108
|
+
if (rootDir) {
|
|
109
|
+
console.log(` Monorepo root dir: ${rootDir}`);
|
|
110
|
+
}
|
|
106
111
|
const timeoutMs = (opts.timeoutMinutes ?? 30) * 60 * 1e3;
|
|
107
112
|
const sourceOpts = opts.git ? {
|
|
108
113
|
source: {
|
|
@@ -219,21 +224,23 @@ async function deploySandbox(opts) {
|
|
|
219
224
|
const envLines = Object.entries(envMap).map(([k, v]) => `${k}=${v}`);
|
|
220
225
|
await sandbox2.writeFiles([
|
|
221
226
|
{
|
|
222
|
-
path:
|
|
227
|
+
path: `${rootPrefix}.env`,
|
|
223
228
|
content: Buffer.from(envLines.join("\n"))
|
|
224
229
|
}
|
|
225
230
|
]);
|
|
226
231
|
const dots = startDots(" Installing dependencies");
|
|
227
|
-
const
|
|
232
|
+
const installCmd = rootDir ? `cd ${rootDir} && npm install` : "npm install";
|
|
233
|
+
const install = await sandbox2.runCommand("bash", ["-c", installCmd]);
|
|
228
234
|
dots.stop();
|
|
229
235
|
if (install.exitCode !== 0) {
|
|
230
236
|
const stderr = await install.stderr();
|
|
231
237
|
console.error(stderr);
|
|
232
238
|
throw new Error(`npm install failed (exit ${install.exitCode})`);
|
|
233
239
|
}
|
|
240
|
+
const devCmd = rootDir ? `cd ${rootDir} && npm run dev` : "npm run dev";
|
|
234
241
|
const devServer = await sandbox2.runCommand({
|
|
235
|
-
cmd: "
|
|
236
|
-
args: ["
|
|
242
|
+
cmd: "bash",
|
|
243
|
+
args: ["-c", devCmd],
|
|
237
244
|
detached: true
|
|
238
245
|
});
|
|
239
246
|
const baseUrl = sandbox2.domain(5173);
|
|
@@ -972,8 +979,13 @@ async function sandbox(args, options) {
|
|
|
972
979
|
branch: envBranch || "main",
|
|
973
980
|
userName: envUserName || "viagen",
|
|
974
981
|
userEmail: envUserEmail || "noreply@viagen.dev",
|
|
975
|
-
isDirty:
|
|
976
|
-
|
|
982
|
+
isDirty: (() => {
|
|
983
|
+
try {
|
|
984
|
+
return execSync2("git status --porcelain", { cwd, encoding: "utf-8" }).trim().length > 0;
|
|
985
|
+
} catch {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
})()
|
|
977
989
|
} : getGitInfo(cwd);
|
|
978
990
|
if (envRemoteUrl) {
|
|
979
991
|
console.log("Using git info from .env");
|
|
@@ -1013,7 +1025,7 @@ async function sandbox(args, options) {
|
|
|
1013
1025
|
userEmail: gitInfo.userEmail,
|
|
1014
1026
|
token: githubToken
|
|
1015
1027
|
});
|
|
1016
|
-
if (
|
|
1028
|
+
if (gitInfo.isDirty && !branchOverride) {
|
|
1017
1029
|
console.log("");
|
|
1018
1030
|
console.log("Your working tree has uncommitted changes.");
|
|
1019
1031
|
console.log("");
|
|
@@ -1072,6 +1084,9 @@ async function sandbox(args, options) {
|
|
|
1072
1084
|
console.log(` Repo: ${deployGit.remoteUrl}`);
|
|
1073
1085
|
console.log(` Branch: ${deployGit.branch}`);
|
|
1074
1086
|
}
|
|
1087
|
+
if (env["SANDBOX_ROOT_DIR"]) {
|
|
1088
|
+
console.log(` Root: ${env["SANDBOX_ROOT_DIR"]}`);
|
|
1089
|
+
}
|
|
1075
1090
|
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1076
1091
|
let frameIdx = 0;
|
|
1077
1092
|
const spinner = setInterval(() => {
|
|
@@ -1095,7 +1110,8 @@ async function sandbox(args, options) {
|
|
|
1095
1110
|
} : void 0,
|
|
1096
1111
|
fling: existsSync(join2(homedir(), ".fling", "token")) ? { token: readFileSync2(join2(homedir(), ".fling", "token"), "utf-8").trim() } : void 0,
|
|
1097
1112
|
timeoutMinutes,
|
|
1098
|
-
prompt
|
|
1113
|
+
prompt,
|
|
1114
|
+
rootDir: env["SANDBOX_ROOT_DIR"] || void 0
|
|
1099
1115
|
});
|
|
1100
1116
|
clearInterval(spinner);
|
|
1101
1117
|
process.stdout.write("\r \u2713 Sandbox ready! \n");
|
package/dist/index.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ interface DeploySandboxOptions {
|
|
|
51
51
|
envVars?: Record<string, string>;
|
|
52
52
|
/** Initial prompt to auto-send in the chat UI on load. */
|
|
53
53
|
prompt?: string;
|
|
54
|
+
/** Subdirectory to use as the working directory (for monorepos). */
|
|
55
|
+
rootDir?: string;
|
|
54
56
|
}
|
|
55
57
|
interface DeploySandboxResult {
|
|
56
58
|
/** Full URL with auth token in query string. */
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,8 @@ var __export = (target, all) => {
|
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
8
|
import { execSync as execSync2 } from "child_process";
|
|
9
|
-
import { mkdirSync as
|
|
10
|
-
import { join as
|
|
9
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
10
|
+
import { join as join7 } from "path";
|
|
11
11
|
import { loadEnv } from "vite";
|
|
12
12
|
|
|
13
13
|
// src/logger.ts
|
|
@@ -111,7 +111,7 @@ function registerHealthRoutes(server, env, errorRef) {
|
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
|
-
const currentVersion = true ? "0.1.
|
|
114
|
+
const currentVersion = true ? "0.1.3" : "0.0.0";
|
|
115
115
|
debug("health", `version resolved: ${currentVersion}`);
|
|
116
116
|
let versionCache = null;
|
|
117
117
|
server.middlewares.use("/via/version", (_req, res) => {
|
|
@@ -4200,15 +4200,17 @@ function buildUiHtml(opts) {
|
|
|
4200
4200
|
// src/iframe.ts
|
|
4201
4201
|
function buildIframeHtml(opts) {
|
|
4202
4202
|
const pw = opts.panelWidth;
|
|
4203
|
+
const appSrc = opts.appUrl ?? "/?_viagen_embed=1";
|
|
4203
4204
|
return `<!DOCTYPE html>
|
|
4204
4205
|
<html lang="en">
|
|
4205
4206
|
<head>
|
|
4206
4207
|
<meta charset="UTF-8">
|
|
4207
4208
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4209
|
+
<meta name="color-scheme" content="light dark">
|
|
4208
4210
|
<title>viagen</title>
|
|
4209
4211
|
<style>
|
|
4210
4212
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
4211
|
-
body { display: flex; height: 100vh; background: #
|
|
4213
|
+
body { display: flex; height: 100vh; background: #f5f5f5; overflow: hidden; }
|
|
4212
4214
|
#app-frame { flex: 1; border: none; height: 100%; min-width: 200px; }
|
|
4213
4215
|
#divider {
|
|
4214
4216
|
width: 1px;
|
|
@@ -4230,24 +4232,67 @@ function buildIframeHtml(opts) {
|
|
|
4230
4232
|
#divider:hover, #divider.active { background: #a3a3a3; }
|
|
4231
4233
|
#chat-frame { width: ${pw}px; border: none; height: 100%; min-width: 280px; background: #ffffff; }
|
|
4232
4234
|
.dragging iframe { pointer-events: none; }
|
|
4235
|
+
#loading {
|
|
4236
|
+
position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
4237
|
+
background: #f5f5f5; color: #666; font-family: system-ui; z-index: 100;
|
|
4238
|
+
transition: opacity 0.3s;
|
|
4239
|
+
}
|
|
4240
|
+
#loading.hidden { opacity: 0; pointer-events: none; }
|
|
4241
|
+
.spinner { width: 20px; height: 20px; border: 2px solid #ddd; border-top-color: #888;
|
|
4242
|
+
border-radius: 50%; animation: spin .6s linear infinite; margin-right: 12px; }
|
|
4243
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
4244
|
+
body.ready { background: #ffffff; }
|
|
4245
|
+
@media (prefers-color-scheme: dark) {
|
|
4246
|
+
body { background: #0a0a0a; }
|
|
4247
|
+
#loading { background: #0a0a0a; color: #888; }
|
|
4248
|
+
.spinner { border-color: #333; border-top-color: #999; }
|
|
4249
|
+
#divider { background: #333; }
|
|
4250
|
+
#divider:hover, #divider.active { background: #555; }
|
|
4251
|
+
#chat-frame { background: #0a0a0a; }
|
|
4252
|
+
body.ready { background: #0a0a0a; }
|
|
4253
|
+
}
|
|
4233
4254
|
</style>
|
|
4234
4255
|
</head>
|
|
4235
4256
|
<body>
|
|
4236
|
-
<
|
|
4257
|
+
<div id="loading"><div class="spinner"></div><span>Starting dev server\u2026</span></div>
|
|
4258
|
+
<iframe id="app-frame"></iframe>
|
|
4237
4259
|
<div id="divider"></div>
|
|
4238
|
-
<iframe id="chat-frame"
|
|
4260
|
+
<iframe id="chat-frame"></iframe>
|
|
4239
4261
|
<script>
|
|
4262
|
+
var appFrame = document.getElementById('app-frame');
|
|
4263
|
+
var chatFrame = document.getElementById('chat-frame');
|
|
4264
|
+
var loading = document.getElementById('loading');
|
|
4265
|
+
|
|
4266
|
+
// Wait for Vite to be fully initialized before loading iframes.
|
|
4267
|
+
// We probe /@vite/client which goes through Vite's transform pipeline
|
|
4268
|
+
// and only succeeds after the dev server has finished starting up.
|
|
4269
|
+
// Static middleware routes like /via/ui return 200 instantly (before
|
|
4270
|
+
// Vite is ready), so they can't be used as readiness probes.
|
|
4271
|
+
(async function() {
|
|
4272
|
+
for (var i = 0; i < 120; i++) {
|
|
4273
|
+
try {
|
|
4274
|
+
var r = await fetch('/@vite/client', { credentials: 'same-origin' });
|
|
4275
|
+
if (r.ok) break;
|
|
4276
|
+
} catch(e) {}
|
|
4277
|
+
await new Promise(function(resolve) { setTimeout(resolve, 500); });
|
|
4278
|
+
}
|
|
4279
|
+
// Set iframe srcs \u2014 now Vite should be ready
|
|
4280
|
+
chatFrame.src = '/via/ui';
|
|
4281
|
+
appFrame.src = ${JSON.stringify(appSrc)};
|
|
4282
|
+
// Hide loading overlay once chat frame loads
|
|
4283
|
+
chatFrame.addEventListener('load', function() {
|
|
4284
|
+
loading.classList.add('hidden');
|
|
4285
|
+
document.body.classList.add('ready');
|
|
4286
|
+
chatFrame.contentWindow.postMessage({ type: 'viagen:context', iframe: true }, '*');
|
|
4287
|
+
});
|
|
4288
|
+
})();
|
|
4289
|
+
|
|
4240
4290
|
// Relay postMessage from app iframe to chat iframe (e.g. "Fix This Error")
|
|
4241
4291
|
window.addEventListener('message', function(ev) {
|
|
4242
4292
|
if (ev.data && ev.data.type === 'viagen:send') {
|
|
4243
|
-
|
|
4293
|
+
chatFrame.contentWindow.postMessage(ev.data, '*');
|
|
4244
4294
|
}
|
|
4245
4295
|
});
|
|
4246
|
-
// Tell chat iframe it's in split-view mode (hides pop-out button)
|
|
4247
|
-
var chatFrame = document.getElementById('chat-frame');
|
|
4248
|
-
chatFrame.addEventListener('load', function() {
|
|
4249
|
-
chatFrame.contentWindow.postMessage({ type: 'viagen:context', iframe: true }, '*');
|
|
4250
|
-
});
|
|
4251
4296
|
|
|
4252
4297
|
// Drag-resizable divider
|
|
4253
4298
|
var divider = document.getElementById('divider');
|
|
@@ -4293,21 +4338,26 @@ function buildWaitPage(targetUrl) {
|
|
|
4293
4338
|
<html><head><meta charset="utf-8"><title>Loading\u2026</title>
|
|
4294
4339
|
<style>
|
|
4295
4340
|
body { margin: 0; display: flex; align-items: center; justify-content: center;
|
|
4296
|
-
height: 100vh; background: #
|
|
4297
|
-
.spinner { width: 20px; height: 20px; border: 2px solid #
|
|
4341
|
+
height: 100vh; background: #f5f5f5; color: #666; font-family: system-ui; }
|
|
4342
|
+
.spinner { width: 20px; height: 20px; border: 2px solid #ddd; border-top-color: #888;
|
|
4298
4343
|
border-radius: 50%; animation: spin .6s linear infinite; margin-right: 12px; }
|
|
4299
4344
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
4345
|
+
@media (prefers-color-scheme: dark) {
|
|
4346
|
+
body { background: #0a0a0a; color: #888; }
|
|
4347
|
+
.spinner { border-color: #333; border-top-color: #999; }
|
|
4348
|
+
}
|
|
4300
4349
|
</style></head>
|
|
4301
4350
|
<body><div class="spinner"></div><span>Starting dev server\u2026</span>
|
|
4302
4351
|
<script>
|
|
4303
4352
|
(async () => {
|
|
4304
4353
|
const target = ${JSON.stringify(targetUrl)};
|
|
4354
|
+
// Probe /@vite/client \u2014 it goes through Vite's transform pipeline and
|
|
4355
|
+
// only succeeds once the dev server is fully initialized. Static routes
|
|
4356
|
+
// like /via/iframe return 200 too early.
|
|
4305
4357
|
for (let i = 0; i < 60; i++) {
|
|
4306
4358
|
try {
|
|
4307
|
-
const r = await fetch(
|
|
4308
|
-
|
|
4309
|
-
// Vite is ready when we get a non-empty HTML response
|
|
4310
|
-
if (r.ok && text.length > 0) { window.location.replace(target); return; }
|
|
4359
|
+
const r = await fetch('/@vite/client', { credentials: 'same-origin' });
|
|
4360
|
+
if (r.ok) { window.location.replace(target); return; }
|
|
4311
4361
|
} catch {}
|
|
4312
4362
|
await new Promise(r => setTimeout(r, 500));
|
|
4313
4363
|
}
|
|
@@ -18686,8 +18736,9 @@ import {
|
|
|
18686
18736
|
tool
|
|
18687
18737
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
18688
18738
|
function createViagenTools(config2) {
|
|
18689
|
-
const { client, projectId } = config2;
|
|
18739
|
+
const { client, projectId, processManager } = config2;
|
|
18690
18740
|
const taskId = process.env["VIAGEN_TASK_ID"];
|
|
18741
|
+
debug("tools", `MCP tools created (projectId: ${projectId}, taskId: ${taskId || "none"})`);
|
|
18691
18742
|
const tools = [
|
|
18692
18743
|
tool(
|
|
18693
18744
|
"viagen_update_task",
|
|
@@ -18703,6 +18754,7 @@ function createViagenTools(config2) {
|
|
|
18703
18754
|
prReviewStatus: external_exports.string().optional().describe("PR review outcome \u2014 e.g. 'pass', 'flag', or 'fail'.")
|
|
18704
18755
|
},
|
|
18705
18756
|
async (args) => {
|
|
18757
|
+
debug("tools", `viagen_update_task called (taskId: ${args.taskId || taskId || "none"}, status: ${args.status || "none"}, projectId: ${projectId})`);
|
|
18706
18758
|
const id = args.taskId || taskId;
|
|
18707
18759
|
if (!id) {
|
|
18708
18760
|
return {
|
|
@@ -18724,6 +18776,9 @@ function createViagenTools(config2) {
|
|
|
18724
18776
|
};
|
|
18725
18777
|
} catch (err) {
|
|
18726
18778
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
18779
|
+
const status = err?.status;
|
|
18780
|
+
const detail = err?.detail;
|
|
18781
|
+
debug("tools", `viagen_update_task FAILED: ${status || "?"} ${message}${detail ? ` (${detail})` : ""}`);
|
|
18727
18782
|
return {
|
|
18728
18783
|
content: [{ type: "text", text: `Error updating task: ${message}` }]
|
|
18729
18784
|
};
|
|
@@ -18737,15 +18792,25 @@ function createViagenTools(config2) {
|
|
|
18737
18792
|
status: external_exports.enum(["ready", "running", "validating", "completed", "timed_out"]).optional().describe("Filter tasks by status.")
|
|
18738
18793
|
},
|
|
18739
18794
|
async (args) => {
|
|
18740
|
-
|
|
18741
|
-
|
|
18742
|
-
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18746
|
-
|
|
18747
|
-
|
|
18748
|
-
|
|
18795
|
+
debug("tools", `viagen_list_tasks called (projectId: ${projectId}, status: ${args.status || "all"})`);
|
|
18796
|
+
try {
|
|
18797
|
+
const tasks = await client.tasks.list(projectId, args.status);
|
|
18798
|
+
debug("tools", `viagen_list_tasks returned ${tasks.length} tasks`);
|
|
18799
|
+
return {
|
|
18800
|
+
content: [
|
|
18801
|
+
{
|
|
18802
|
+
type: "text",
|
|
18803
|
+
text: JSON.stringify(tasks, null, 2)
|
|
18804
|
+
}
|
|
18805
|
+
]
|
|
18806
|
+
};
|
|
18807
|
+
} catch (err) {
|
|
18808
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
18809
|
+
debug("tools", `viagen_list_tasks FAILED: ${message}`);
|
|
18810
|
+
return {
|
|
18811
|
+
content: [{ type: "text", text: `Error listing tasks: ${message}` }]
|
|
18812
|
+
};
|
|
18813
|
+
}
|
|
18749
18814
|
}
|
|
18750
18815
|
),
|
|
18751
18816
|
tool(
|
|
@@ -18755,15 +18820,26 @@ function createViagenTools(config2) {
|
|
|
18755
18820
|
taskId: external_exports.string().describe("The task ID to retrieve.")
|
|
18756
18821
|
},
|
|
18757
18822
|
async (args) => {
|
|
18758
|
-
|
|
18759
|
-
|
|
18760
|
-
|
|
18761
|
-
|
|
18762
|
-
|
|
18763
|
-
|
|
18764
|
-
|
|
18765
|
-
|
|
18766
|
-
|
|
18823
|
+
debug("tools", `viagen_get_task called (projectId: ${projectId}, taskId: ${args.taskId})`);
|
|
18824
|
+
try {
|
|
18825
|
+
const task = await client.tasks.get(projectId, args.taskId);
|
|
18826
|
+
debug("tools", `viagen_get_task success (status: ${task.status})`);
|
|
18827
|
+
return {
|
|
18828
|
+
content: [
|
|
18829
|
+
{
|
|
18830
|
+
type: "text",
|
|
18831
|
+
text: JSON.stringify(task, null, 2)
|
|
18832
|
+
}
|
|
18833
|
+
]
|
|
18834
|
+
};
|
|
18835
|
+
} catch (err) {
|
|
18836
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
18837
|
+
const status = err?.status;
|
|
18838
|
+
debug("tools", `viagen_get_task FAILED: ${status || "?"} ${message}`);
|
|
18839
|
+
return {
|
|
18840
|
+
content: [{ type: "text", text: `Error getting task: ${message}` }]
|
|
18841
|
+
};
|
|
18842
|
+
}
|
|
18767
18843
|
}
|
|
18768
18844
|
),
|
|
18769
18845
|
tool(
|
|
@@ -18775,22 +18851,58 @@ function createViagenTools(config2) {
|
|
|
18775
18851
|
type: external_exports.enum(["task", "plan"]).optional().describe("Task type: 'task' for code changes, 'plan' for implementation plans.")
|
|
18776
18852
|
},
|
|
18777
18853
|
async (args) => {
|
|
18778
|
-
|
|
18779
|
-
|
|
18780
|
-
|
|
18781
|
-
|
|
18782
|
-
|
|
18783
|
-
|
|
18784
|
-
|
|
18785
|
-
|
|
18786
|
-
|
|
18787
|
-
|
|
18788
|
-
|
|
18789
|
-
|
|
18790
|
-
|
|
18854
|
+
debug("tools", `viagen_create_task called (projectId: ${projectId}, type: ${args.type || "task"}, prompt: "${args.prompt.slice(0, 80)}...")`);
|
|
18855
|
+
try {
|
|
18856
|
+
const task = await client.tasks.create(projectId, {
|
|
18857
|
+
prompt: args.prompt,
|
|
18858
|
+
branch: args.branch,
|
|
18859
|
+
type: args.type
|
|
18860
|
+
});
|
|
18861
|
+
debug("tools", `viagen_create_task success (taskId: ${task.id})`);
|
|
18862
|
+
return {
|
|
18863
|
+
content: [
|
|
18864
|
+
{
|
|
18865
|
+
type: "text",
|
|
18866
|
+
text: JSON.stringify(task, null, 2)
|
|
18867
|
+
}
|
|
18868
|
+
]
|
|
18869
|
+
};
|
|
18870
|
+
} catch (err) {
|
|
18871
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
18872
|
+
const status = err?.status;
|
|
18873
|
+
const detail = err?.detail;
|
|
18874
|
+
debug("tools", `viagen_create_task FAILED: ${status || "?"} ${message}${detail ? ` (${detail})` : ""}`);
|
|
18875
|
+
return {
|
|
18876
|
+
content: [{ type: "text", text: `Error creating task: ${message}` }]
|
|
18877
|
+
};
|
|
18878
|
+
}
|
|
18791
18879
|
}
|
|
18792
18880
|
)
|
|
18793
18881
|
];
|
|
18882
|
+
if (processManager) {
|
|
18883
|
+
tools.push(
|
|
18884
|
+
tool(
|
|
18885
|
+
"viagen_restart_preview",
|
|
18886
|
+
"Restart the preview server. Use after installing packages, changing configs, or when the preview is broken. Optionally provide a new command to run.",
|
|
18887
|
+
{
|
|
18888
|
+
command: external_exports.string().optional().describe("New command to use for the preview server (e.g. 'npm run build && npm run start'). If omitted, restarts with the current command.")
|
|
18889
|
+
},
|
|
18890
|
+
async (args) => {
|
|
18891
|
+
try {
|
|
18892
|
+
const result = await processManager.restart(args.command);
|
|
18893
|
+
return {
|
|
18894
|
+
content: [{ type: "text", text: result }]
|
|
18895
|
+
};
|
|
18896
|
+
} catch (err) {
|
|
18897
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
18898
|
+
return {
|
|
18899
|
+
content: [{ type: "text", text: `Error restarting preview: ${message}` }]
|
|
18900
|
+
};
|
|
18901
|
+
}
|
|
18902
|
+
}
|
|
18903
|
+
)
|
|
18904
|
+
);
|
|
18905
|
+
}
|
|
18794
18906
|
return createSdkMcpServer({ name: "viagen", tools });
|
|
18795
18907
|
}
|
|
18796
18908
|
var PLAN_MODE_DISALLOWED_TOOLS = ["Edit", "NotebookEdit"];
|
|
@@ -18820,23 +18932,313 @@ Constraints:
|
|
|
18820
18932
|
- Only create new files inside the plans/ directory.
|
|
18821
18933
|
- Your plan should include: context, proposed changes (with file paths and descriptions), implementation order, and potential risks.
|
|
18822
18934
|
`;
|
|
18823
|
-
|
|
18824
|
-
|
|
18825
|
-
|
|
18826
|
-
|
|
18827
|
-
|
|
18828
|
-
|
|
18829
|
-
|
|
18830
|
-
|
|
18831
|
-
|
|
18935
|
+
function buildTaskToolsPrompt(opts) {
|
|
18936
|
+
const parts = [];
|
|
18937
|
+
parts.push(`
|
|
18938
|
+
## Viagen Platform
|
|
18939
|
+
|
|
18940
|
+
You are connected to the viagen development platform. This session is part of a viagen project (ID: ${opts.projectId}).${opts.taskId ? ` You are currently working on task ${opts.taskId}.` : ""}${opts.branch ? ` Current branch: ${opts.branch}.` : ""}
|
|
18941
|
+
|
|
18942
|
+
When users ask about the project, tasks, work history, or anything related to the viagen platform \u2014 **always use your viagen MCP tools** to answer. Do NOT try to grep the codebase or guess \u2014 the platform is your source of truth for project and task information.
|
|
18943
|
+
|
|
18944
|
+
### Available viagen tools
|
|
18945
|
+
|
|
18946
|
+
- **viagen_list_tasks** \u2014 List tasks in this project. Filter by status: ready, running, validating, completed, timed_out. Call this when users ask "what tasks are there?", "what's been done?", "what's pending?", etc.
|
|
18947
|
+
- **viagen_get_task** \u2014 Get full details of a specific task including its prompt, status, branch, PR URL, and results.
|
|
18948
|
+
- **viagen_create_task** \u2014 Create a new task. Use when you identify follow-up work or when the user asks you to create a task.
|
|
18949
|
+
- **viagen_update_task** \u2014 Update a task's status. Use 'review' after creating a PR, 'completed' when fully done. Defaults to the current task if one is set.`);
|
|
18950
|
+
if (opts.hasProcessManager) {
|
|
18951
|
+
parts.push(`
|
|
18952
|
+
- **viagen_restart_preview** \u2014 Restart the app preview server. Use after installing packages, changing configs, or when the preview is broken.`);
|
|
18953
|
+
}
|
|
18954
|
+
parts.push(`
|
|
18955
|
+
|
|
18956
|
+
### How to respond to platform questions
|
|
18957
|
+
|
|
18958
|
+
- "What tasks are there?" \u2192 call viagen_list_tasks, summarize the results conversationally
|
|
18959
|
+
- "What's the status of task X?" \u2192 call viagen_get_task with the ID
|
|
18960
|
+
- "What work has been done?" \u2192 call viagen_list_tasks with status "completed" or "validating"
|
|
18961
|
+
- "What's pending?" \u2192 call viagen_list_tasks with status "ready"
|
|
18962
|
+
- "Tell me about this project" \u2192 call viagen_list_tasks (no filter) to give an overview of all work
|
|
18963
|
+
|
|
18964
|
+
Always present the results in a friendly, conversational way \u2014 not raw JSON.`);
|
|
18965
|
+
return parts.join("");
|
|
18966
|
+
}
|
|
18967
|
+
var TASK_TOOLS_PROMPT = buildTaskToolsPrompt({ projectId: "unknown" });
|
|
18832
18968
|
|
|
18833
18969
|
// src/index.ts
|
|
18834
18970
|
import { createViagen } from "viagen-sdk";
|
|
18835
18971
|
|
|
18972
|
+
// src/process-manager.ts
|
|
18973
|
+
import { spawn } from "child_process";
|
|
18974
|
+
import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, readSync, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
18975
|
+
import { join as join5 } from "path";
|
|
18976
|
+
var debug2 = (label, ...args) => debug(`pm:${label}`, ...args);
|
|
18977
|
+
var ProcessManager = class {
|
|
18978
|
+
child = null;
|
|
18979
|
+
adoptedPid = null;
|
|
18980
|
+
command;
|
|
18981
|
+
cwd;
|
|
18982
|
+
env;
|
|
18983
|
+
logBuffer;
|
|
18984
|
+
appPort;
|
|
18985
|
+
viagenDir;
|
|
18986
|
+
pidFile;
|
|
18987
|
+
stdoutLog;
|
|
18988
|
+
stderrLog;
|
|
18989
|
+
logTailInterval = null;
|
|
18990
|
+
stdoutOffset = 0;
|
|
18991
|
+
stderrOffset = 0;
|
|
18992
|
+
restarting = false;
|
|
18993
|
+
restartCount = 0;
|
|
18994
|
+
constructor(opts) {
|
|
18995
|
+
this.command = opts.command;
|
|
18996
|
+
this.cwd = opts.cwd;
|
|
18997
|
+
this.env = opts.env ?? {};
|
|
18998
|
+
this.logBuffer = opts.logBuffer;
|
|
18999
|
+
this.appPort = opts.appPort ?? 5173;
|
|
19000
|
+
this.viagenDir = join5(this.cwd, ".viagen");
|
|
19001
|
+
mkdirSync3(this.viagenDir, { recursive: true });
|
|
19002
|
+
this.pidFile = join5(this.viagenDir, "app.pid");
|
|
19003
|
+
this.stdoutLog = join5(this.viagenDir, "app.stdout.log");
|
|
19004
|
+
this.stderrLog = join5(this.viagenDir, "app.stderr.log");
|
|
19005
|
+
}
|
|
19006
|
+
/** Start the app process. If already running (or adopted from a previous session), does nothing. */
|
|
19007
|
+
start() {
|
|
19008
|
+
if (this.child && !this.child.killed) {
|
|
19009
|
+
debug2("start", "process already running (spawned), skipping");
|
|
19010
|
+
return;
|
|
19011
|
+
}
|
|
19012
|
+
const existingPid = this.readPid();
|
|
19013
|
+
if (existingPid && this.isProcessAlive(existingPid)) {
|
|
19014
|
+
debug2("start", `adopting existing app process (pid: ${existingPid})`);
|
|
19015
|
+
this.logBuffer.push("info", `[viagen:pm] App already running from previous session (pid: ${existingPid})`);
|
|
19016
|
+
this.adoptedPid = existingPid;
|
|
19017
|
+
return;
|
|
19018
|
+
}
|
|
19019
|
+
if (existingPid) {
|
|
19020
|
+
debug2("start", `stale pid file (pid: ${existingPid} not alive), removing`);
|
|
19021
|
+
this.removePid();
|
|
19022
|
+
}
|
|
19023
|
+
writeFileSync4(this.stdoutLog, "");
|
|
19024
|
+
writeFileSync4(this.stderrLog, "");
|
|
19025
|
+
this.stdoutOffset = 0;
|
|
19026
|
+
this.stderrOffset = 0;
|
|
19027
|
+
debug2("start", `spawning: ${this.command} (cwd: ${this.cwd}, port: ${this.appPort})`);
|
|
19028
|
+
this.logBuffer.push("info", `[viagen:pm] Starting app: ${this.command} (PORT=${this.appPort})`);
|
|
19029
|
+
const stdoutFd = openSync(this.stdoutLog, "a");
|
|
19030
|
+
const stderrFd = openSync(this.stderrLog, "a");
|
|
19031
|
+
this.child = spawn(this.command, [], {
|
|
19032
|
+
cwd: this.cwd,
|
|
19033
|
+
env: { ...process.env, ...this.env, PORT: String(this.appPort), __VIAGEN_CHILD: "1" },
|
|
19034
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
19035
|
+
shell: true,
|
|
19036
|
+
// Detached so the app survives if the viagen chat server restarts
|
|
19037
|
+
// (e.g. during a viagen dependency update in a sandbox).
|
|
19038
|
+
detached: true
|
|
19039
|
+
});
|
|
19040
|
+
const pid = this.child.pid;
|
|
19041
|
+
debug2("start", `process started (pid: ${pid})`);
|
|
19042
|
+
this.logBuffer.push("info", `[viagen:pm] App process started (pid: ${pid})`);
|
|
19043
|
+
if (pid) {
|
|
19044
|
+
this.writePid(pid);
|
|
19045
|
+
}
|
|
19046
|
+
this.child.unref();
|
|
19047
|
+
this.startLogTail();
|
|
19048
|
+
this.child.on("exit", (code, signal) => {
|
|
19049
|
+
debug2("exit", `process exited (code: ${code}, signal: ${signal})`);
|
|
19050
|
+
this.logBuffer.push(
|
|
19051
|
+
code === 0 ? "info" : "warn",
|
|
19052
|
+
`[viagen:pm] App process exited (code: ${code}, signal: ${signal})`
|
|
19053
|
+
);
|
|
19054
|
+
this.child = null;
|
|
19055
|
+
this.stopLogTail();
|
|
19056
|
+
this.removePid();
|
|
19057
|
+
if (!this.restarting && code !== 0 && code !== null) {
|
|
19058
|
+
this.restartCount++;
|
|
19059
|
+
if (this.restartCount <= 3) {
|
|
19060
|
+
const delay = this.restartCount * 2e3;
|
|
19061
|
+
this.logBuffer.push("warn", `[viagen:pm] Auto-restarting in ${delay / 1e3}s (attempt ${this.restartCount}/3)`);
|
|
19062
|
+
setTimeout(() => this.start(), delay);
|
|
19063
|
+
} else {
|
|
19064
|
+
this.logBuffer.push("error", `[viagen:pm] App crashed ${this.restartCount} times, giving up`);
|
|
19065
|
+
}
|
|
19066
|
+
}
|
|
19067
|
+
});
|
|
19068
|
+
this.child.on("error", (err) => {
|
|
19069
|
+
debug2("error", `spawn error: ${err.message}`);
|
|
19070
|
+
this.logBuffer.push("error", `[viagen:pm] Failed to start app: ${err.message}`);
|
|
19071
|
+
this.child = null;
|
|
19072
|
+
this.stopLogTail();
|
|
19073
|
+
this.removePid();
|
|
19074
|
+
});
|
|
19075
|
+
}
|
|
19076
|
+
/** Stop the app process. Returns when the process has exited. */
|
|
19077
|
+
async stop() {
|
|
19078
|
+
if (this.adoptedPid) {
|
|
19079
|
+
const pid2 = this.adoptedPid;
|
|
19080
|
+
debug2("stop", `killing adopted process (pid: ${pid2})`);
|
|
19081
|
+
this.logBuffer.push("info", `[viagen:pm] Stopping adopted app (pid: ${pid2})`);
|
|
19082
|
+
try {
|
|
19083
|
+
process.kill(-pid2, "SIGTERM");
|
|
19084
|
+
} catch {
|
|
19085
|
+
}
|
|
19086
|
+
this.adoptedPid = null;
|
|
19087
|
+
this.removePid();
|
|
19088
|
+
await new Promise((resolve3) => {
|
|
19089
|
+
const check2 = () => {
|
|
19090
|
+
if (!this.isProcessAlive(pid2)) {
|
|
19091
|
+
resolve3();
|
|
19092
|
+
return;
|
|
19093
|
+
}
|
|
19094
|
+
try {
|
|
19095
|
+
process.kill(-pid2, "SIGKILL");
|
|
19096
|
+
} catch {
|
|
19097
|
+
}
|
|
19098
|
+
resolve3();
|
|
19099
|
+
};
|
|
19100
|
+
setTimeout(check2, 5e3);
|
|
19101
|
+
});
|
|
19102
|
+
return;
|
|
19103
|
+
}
|
|
19104
|
+
if (!this.child || this.child.killed) {
|
|
19105
|
+
debug2("stop", "no process to stop");
|
|
19106
|
+
return;
|
|
19107
|
+
}
|
|
19108
|
+
const pid = this.child.pid;
|
|
19109
|
+
debug2("stop", `killing process (pid: ${pid})`);
|
|
19110
|
+
this.logBuffer.push("info", `[viagen:pm] Stopping app (pid: ${pid})`);
|
|
19111
|
+
return new Promise((resolve3) => {
|
|
19112
|
+
const timeout = setTimeout(() => {
|
|
19113
|
+
debug2("stop", "SIGTERM timeout, sending SIGKILL");
|
|
19114
|
+
if (pid) {
|
|
19115
|
+
try {
|
|
19116
|
+
process.kill(-pid, "SIGKILL");
|
|
19117
|
+
} catch {
|
|
19118
|
+
}
|
|
19119
|
+
}
|
|
19120
|
+
}, 5e3);
|
|
19121
|
+
this.child.once("exit", () => {
|
|
19122
|
+
clearTimeout(timeout);
|
|
19123
|
+
this.child = null;
|
|
19124
|
+
this.removePid();
|
|
19125
|
+
debug2("stop", "process stopped");
|
|
19126
|
+
this.logBuffer.push("info", "[viagen:pm] App stopped");
|
|
19127
|
+
resolve3();
|
|
19128
|
+
});
|
|
19129
|
+
if (pid) {
|
|
19130
|
+
try {
|
|
19131
|
+
process.kill(-pid, "SIGTERM");
|
|
19132
|
+
} catch {
|
|
19133
|
+
}
|
|
19134
|
+
} else {
|
|
19135
|
+
this.child.kill("SIGTERM");
|
|
19136
|
+
}
|
|
19137
|
+
});
|
|
19138
|
+
}
|
|
19139
|
+
/** Restart the app process, optionally with a new command. */
|
|
19140
|
+
async restart(newCommand) {
|
|
19141
|
+
this.restarting = true;
|
|
19142
|
+
this.restartCount = 0;
|
|
19143
|
+
if (newCommand) {
|
|
19144
|
+
debug2("restart", `changing command to: ${newCommand}`);
|
|
19145
|
+
this.logBuffer.push("info", `[viagen:pm] Changing app command to: ${newCommand}`);
|
|
19146
|
+
this.command = newCommand;
|
|
19147
|
+
}
|
|
19148
|
+
debug2("restart", "restarting app process");
|
|
19149
|
+
this.logBuffer.push("info", "[viagen:pm] Restarting app...");
|
|
19150
|
+
await this.stop();
|
|
19151
|
+
this.start();
|
|
19152
|
+
this.restarting = false;
|
|
19153
|
+
return `App restarted with: ${this.command}`;
|
|
19154
|
+
}
|
|
19155
|
+
/** Check if the process is currently running. */
|
|
19156
|
+
get running() {
|
|
19157
|
+
if (this.adoptedPid) return this.isProcessAlive(this.adoptedPid);
|
|
19158
|
+
return this.child !== null && !this.child.killed;
|
|
19159
|
+
}
|
|
19160
|
+
/** Get the current command. */
|
|
19161
|
+
get currentCommand() {
|
|
19162
|
+
return this.command;
|
|
19163
|
+
}
|
|
19164
|
+
// ── Log tailing ───────────────────────────────────────────────
|
|
19165
|
+
/** Poll log files and feed new lines into the log buffer. */
|
|
19166
|
+
startLogTail() {
|
|
19167
|
+
this.stopLogTail();
|
|
19168
|
+
this.logTailInterval = setInterval(() => this.tailLogs(), 1e3);
|
|
19169
|
+
}
|
|
19170
|
+
stopLogTail() {
|
|
19171
|
+
if (this.logTailInterval) {
|
|
19172
|
+
clearInterval(this.logTailInterval);
|
|
19173
|
+
this.logTailInterval = null;
|
|
19174
|
+
}
|
|
19175
|
+
this.tailLogs();
|
|
19176
|
+
}
|
|
19177
|
+
tailLogs() {
|
|
19178
|
+
this.stdoutOffset = this.tailFile(this.stdoutLog, this.stdoutOffset, "info");
|
|
19179
|
+
this.stderrOffset = this.tailFile(this.stderrLog, this.stderrOffset, "error");
|
|
19180
|
+
}
|
|
19181
|
+
/** Read new bytes from a log file starting at offset. Returns new offset. */
|
|
19182
|
+
tailFile(filePath, offset, level) {
|
|
19183
|
+
try {
|
|
19184
|
+
if (!existsSync2(filePath)) return offset;
|
|
19185
|
+
const stat = statSync2(filePath);
|
|
19186
|
+
if (stat.size <= offset) return offset;
|
|
19187
|
+
const buf = Buffer.alloc(stat.size - offset);
|
|
19188
|
+
const fd = openSync(filePath, "r");
|
|
19189
|
+
const bytesRead = readSync(fd, buf, 0, buf.length, offset);
|
|
19190
|
+
closeSync(fd);
|
|
19191
|
+
if (bytesRead > 0) {
|
|
19192
|
+
const lines = buf.toString("utf-8", 0, bytesRead).split("\n").filter(Boolean);
|
|
19193
|
+
for (const line of lines) {
|
|
19194
|
+
debug2(level === "info" ? "stdout" : "stderr", line);
|
|
19195
|
+
this.logBuffer.push(level, `[preview] ${line}`);
|
|
19196
|
+
}
|
|
19197
|
+
}
|
|
19198
|
+
return offset + bytesRead;
|
|
19199
|
+
} catch {
|
|
19200
|
+
return offset;
|
|
19201
|
+
}
|
|
19202
|
+
}
|
|
19203
|
+
// ── PID file helpers ──────────────────────────────────────────
|
|
19204
|
+
writePid(pid) {
|
|
19205
|
+
try {
|
|
19206
|
+
writeFileSync4(this.pidFile, String(pid));
|
|
19207
|
+
debug2("pid", `wrote pid ${pid} to ${this.pidFile}`);
|
|
19208
|
+
} catch (err) {
|
|
19209
|
+
debug2("pid", `failed to write pid file: ${err}`);
|
|
19210
|
+
}
|
|
19211
|
+
}
|
|
19212
|
+
readPid() {
|
|
19213
|
+
try {
|
|
19214
|
+
if (!existsSync2(this.pidFile)) return null;
|
|
19215
|
+
const raw = readFileSync4(this.pidFile, "utf-8").trim();
|
|
19216
|
+
const pid = parseInt(raw, 10);
|
|
19217
|
+
return Number.isFinite(pid) ? pid : null;
|
|
19218
|
+
} catch {
|
|
19219
|
+
return null;
|
|
19220
|
+
}
|
|
19221
|
+
}
|
|
19222
|
+
removePid() {
|
|
19223
|
+
try {
|
|
19224
|
+
if (existsSync2(this.pidFile)) unlinkSync(this.pidFile);
|
|
19225
|
+
} catch {
|
|
19226
|
+
}
|
|
19227
|
+
}
|
|
19228
|
+
isProcessAlive(pid) {
|
|
19229
|
+
try {
|
|
19230
|
+
process.kill(pid, 0);
|
|
19231
|
+
return true;
|
|
19232
|
+
} catch {
|
|
19233
|
+
return false;
|
|
19234
|
+
}
|
|
19235
|
+
}
|
|
19236
|
+
};
|
|
19237
|
+
|
|
18836
19238
|
// src/sandbox.ts
|
|
18837
19239
|
import { randomUUID } from "crypto";
|
|
18838
|
-
import { readFileSync as
|
|
18839
|
-
import { join as
|
|
19240
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
|
|
19241
|
+
import { join as join6, relative as relative2 } from "path";
|
|
18840
19242
|
import { Sandbox } from "@vercel/sandbox";
|
|
18841
19243
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
18842
19244
|
"node_modules",
|
|
@@ -18852,13 +19254,13 @@ function collectFiles2(dir, base) {
|
|
|
18852
19254
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
18853
19255
|
if (entry.name.startsWith(".") && SKIP_DIRS.has(entry.name)) continue;
|
|
18854
19256
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
18855
|
-
const fullPath =
|
|
19257
|
+
const fullPath = join6(dir, entry.name);
|
|
18856
19258
|
const relPath = relative2(base, fullPath);
|
|
18857
19259
|
if (entry.isDirectory()) {
|
|
18858
19260
|
files.push(...collectFiles2(fullPath, base));
|
|
18859
19261
|
} else if (entry.isFile()) {
|
|
18860
19262
|
if (SKIP_FILES.has(entry.name)) continue;
|
|
18861
|
-
files.push({ path: relPath, content:
|
|
19263
|
+
files.push({ path: relPath, content: readFileSync5(fullPath) });
|
|
18862
19264
|
}
|
|
18863
19265
|
}
|
|
18864
19266
|
return files;
|
|
@@ -18929,6 +19331,11 @@ async function waitForServer(baseUrl, token, devServer) {
|
|
|
18929
19331
|
async function deploySandbox(opts) {
|
|
18930
19332
|
const token = randomUUID();
|
|
18931
19333
|
const useGit = !!opts.git;
|
|
19334
|
+
const rootDir = opts.rootDir ?? null;
|
|
19335
|
+
const rootPrefix = rootDir ? `${rootDir}/` : "";
|
|
19336
|
+
if (rootDir) {
|
|
19337
|
+
console.log(` Monorepo root dir: ${rootDir}`);
|
|
19338
|
+
}
|
|
18932
19339
|
const timeoutMs = (opts.timeoutMinutes ?? 30) * 60 * 1e3;
|
|
18933
19340
|
const sourceOpts = opts.git ? {
|
|
18934
19341
|
source: {
|
|
@@ -19045,21 +19452,23 @@ async function deploySandbox(opts) {
|
|
|
19045
19452
|
const envLines = Object.entries(envMap).map(([k, v]) => `${k}=${v}`);
|
|
19046
19453
|
await sandbox.writeFiles([
|
|
19047
19454
|
{
|
|
19048
|
-
path:
|
|
19455
|
+
path: `${rootPrefix}.env`,
|
|
19049
19456
|
content: Buffer.from(envLines.join("\n"))
|
|
19050
19457
|
}
|
|
19051
19458
|
]);
|
|
19052
19459
|
const dots = startDots(" Installing dependencies");
|
|
19053
|
-
const
|
|
19460
|
+
const installCmd = rootDir ? `cd ${rootDir} && npm install` : "npm install";
|
|
19461
|
+
const install = await sandbox.runCommand("bash", ["-c", installCmd]);
|
|
19054
19462
|
dots.stop();
|
|
19055
19463
|
if (install.exitCode !== 0) {
|
|
19056
19464
|
const stderr = await install.stderr();
|
|
19057
19465
|
console.error(stderr);
|
|
19058
19466
|
throw new Error(`npm install failed (exit ${install.exitCode})`);
|
|
19059
19467
|
}
|
|
19468
|
+
const devCmd = rootDir ? `cd ${rootDir} && npm run dev` : "npm run dev";
|
|
19060
19469
|
const devServer = await sandbox.runCommand({
|
|
19061
|
-
cmd: "
|
|
19062
|
-
args: ["
|
|
19470
|
+
cmd: "bash",
|
|
19471
|
+
args: ["-c", devCmd],
|
|
19063
19472
|
detached: true
|
|
19064
19473
|
});
|
|
19065
19474
|
const baseUrl = sandbox.domain(5173);
|
|
@@ -19109,8 +19518,18 @@ function viagen(options) {
|
|
|
19109
19518
|
name: "viagen",
|
|
19110
19519
|
config(_, { mode }) {
|
|
19111
19520
|
const e = loadEnv(mode, process.cwd(), "");
|
|
19521
|
+
const serverConfig = {};
|
|
19112
19522
|
if (e["VIAGEN_AUTH_TOKEN"] || e["VIAGEN_USER_TOKEN"]) {
|
|
19113
|
-
|
|
19523
|
+
serverConfig.host = true;
|
|
19524
|
+
serverConfig.allowedHosts = true;
|
|
19525
|
+
}
|
|
19526
|
+
if (e["VIAGEN_APP_COMMAND"] && !process.env["__VIAGEN_CHILD"]) {
|
|
19527
|
+
const viagenPort = parseInt(e["VIAGEN_SERVER_PORT"] || "5199", 10);
|
|
19528
|
+
serverConfig.port = viagenPort;
|
|
19529
|
+
serverConfig.strictPort = true;
|
|
19530
|
+
}
|
|
19531
|
+
if (Object.keys(serverConfig).length > 0) {
|
|
19532
|
+
return { server: serverConfig };
|
|
19114
19533
|
}
|
|
19115
19534
|
},
|
|
19116
19535
|
configResolved(config2) {
|
|
@@ -19122,6 +19541,10 @@ function viagen(options) {
|
|
|
19122
19541
|
debug("init", "plugin initializing");
|
|
19123
19542
|
debug("init", `projectRoot: ${projectRoot}`);
|
|
19124
19543
|
debug("init", `mode: ${config2.mode}`);
|
|
19544
|
+
debug("init", `server port: ${config2.server.port}`);
|
|
19545
|
+
debug("init", `VIAGEN_APP_COMMAND: ${env["VIAGEN_APP_COMMAND"] || "(not set)"}`);
|
|
19546
|
+
debug("init", `VIAGEN_APP_PORT: ${env["VIAGEN_APP_PORT"] || "(not set)"}`);
|
|
19547
|
+
debug("init", `__VIAGEN_CHILD: ${process.env["__VIAGEN_CHILD"] || "no"}`);
|
|
19125
19548
|
debug("init", `ANTHROPIC_API_KEY: ${env["ANTHROPIC_API_KEY"] ? "set (" + env["ANTHROPIC_API_KEY"].slice(0, 8) + "...)" : "NOT SET"}`);
|
|
19126
19549
|
debug("init", `CLAUDE_ACCESS_TOKEN: ${env["CLAUDE_ACCESS_TOKEN"] ? "set" : "NOT SET"}`);
|
|
19127
19550
|
debug("init", `GITHUB_TOKEN: ${env["GITHUB_TOKEN"] ? "set" : "NOT SET"}`);
|
|
@@ -19134,10 +19557,10 @@ function viagen(options) {
|
|
|
19134
19557
|
debug("init", `ui: ${opts.ui}, overlay: ${opts.overlay}, position: ${opts.position}`);
|
|
19135
19558
|
logBuffer.init(projectRoot);
|
|
19136
19559
|
wrapLogger(config2.logger, logBuffer);
|
|
19137
|
-
const viagenDir =
|
|
19138
|
-
|
|
19139
|
-
|
|
19140
|
-
|
|
19560
|
+
const viagenDir = join7(projectRoot, ".viagen");
|
|
19561
|
+
mkdirSync4(viagenDir, { recursive: true });
|
|
19562
|
+
writeFileSync5(
|
|
19563
|
+
join7(viagenDir, "config.json"),
|
|
19141
19564
|
JSON.stringify({
|
|
19142
19565
|
sandboxFiles: options?.sandboxFiles ?? [],
|
|
19143
19566
|
editable: options?.editable ?? []
|
|
@@ -19215,9 +19638,10 @@ ${payload.err.frame || ""}`
|
|
|
19215
19638
|
const platformUrl = env["VIAGEN_PLATFORM_URL"] || "https://app.viagen.dev";
|
|
19216
19639
|
const projectId = env["VIAGEN_PROJECT_ID"];
|
|
19217
19640
|
let viagenClient = null;
|
|
19641
|
+
const orgId = env["VIAGEN_ORG_ID"];
|
|
19218
19642
|
if (platformToken) {
|
|
19219
|
-
viagenClient = createViagen({ token: platformToken, baseUrl: platformUrl });
|
|
19220
|
-
debug("server", `platform client created (baseUrl: ${platformUrl})`);
|
|
19643
|
+
viagenClient = createViagen({ token: platformToken, baseUrl: platformUrl, orgId });
|
|
19644
|
+
debug("server", `platform client created (baseUrl: ${platformUrl}, orgId: ${orgId || "none \u2014 will use default org"})`);
|
|
19221
19645
|
}
|
|
19222
19646
|
const hasEditor = !!(options?.editable && options.editable.length > 0);
|
|
19223
19647
|
const clientJs = buildClientScript({
|
|
@@ -19237,9 +19661,11 @@ ${payload.err.frame || ""}`
|
|
|
19237
19661
|
res.writeHead(302, { Location: "/?_viagen_chat" });
|
|
19238
19662
|
res.end();
|
|
19239
19663
|
});
|
|
19240
|
-
server.middlewares.use("/via/iframe", (
|
|
19664
|
+
server.middlewares.use("/via/iframe", (req, res) => {
|
|
19665
|
+
const url2 = new URL(req.url || "/", "http://localhost");
|
|
19666
|
+
const appUrl = url2.searchParams.get("appUrl") || void 0;
|
|
19241
19667
|
res.setHeader("Content-Type", "text/html");
|
|
19242
|
-
res.end(buildIframeHtml({ panelWidth: opts.panelWidth }));
|
|
19668
|
+
res.end(buildIframeHtml({ panelWidth: opts.panelWidth, appUrl }));
|
|
19243
19669
|
});
|
|
19244
19670
|
if (previewEnabled) {
|
|
19245
19671
|
const previewJs = buildPreviewScript();
|
|
@@ -19307,13 +19733,31 @@ Page URL: ${pageUrl}`);
|
|
|
19307
19733
|
});
|
|
19308
19734
|
const resolvedModel = env["VIAGEN_MODEL"] || opts.model;
|
|
19309
19735
|
debug("server", `creating ChatSession (model: ${resolvedModel})`);
|
|
19736
|
+
let processManager;
|
|
19737
|
+
const appCommand = env["VIAGEN_APP_COMMAND"];
|
|
19738
|
+
const isChildProcess = process.env["__VIAGEN_CHILD"] === "1";
|
|
19739
|
+
if (isChildProcess) {
|
|
19740
|
+
debug("server", "skipping process manager (running as child process)");
|
|
19741
|
+
} else if (appCommand) {
|
|
19742
|
+
const appPort = parseInt(env["VIAGEN_APP_PORT"] || "5173", 10);
|
|
19743
|
+
debug("server", `preview process manager enabled: "${appCommand}" on port ${appPort}`);
|
|
19744
|
+
logBuffer.push("info", `[viagen] Preview process manager: ${appCommand} (port ${appPort})`);
|
|
19745
|
+
processManager = new ProcessManager({
|
|
19746
|
+
command: appCommand,
|
|
19747
|
+
cwd: projectRoot,
|
|
19748
|
+
logBuffer,
|
|
19749
|
+
appPort
|
|
19750
|
+
});
|
|
19751
|
+
processManager.start();
|
|
19752
|
+
}
|
|
19310
19753
|
let mcpServers;
|
|
19311
19754
|
const hasPlatformContext = !!(viagenClient && projectId);
|
|
19312
19755
|
if (hasPlatformContext) {
|
|
19313
19756
|
debug("server", "creating viagen MCP tools (platform connected)");
|
|
19314
19757
|
const viagenMcp = createViagenTools({
|
|
19315
19758
|
client: viagenClient,
|
|
19316
|
-
projectId
|
|
19759
|
+
projectId,
|
|
19760
|
+
processManager
|
|
19317
19761
|
});
|
|
19318
19762
|
mcpServers = { [viagenMcp.name]: viagenMcp };
|
|
19319
19763
|
}
|
|
@@ -19327,7 +19771,12 @@ Page URL: ${pageUrl}`);
|
|
|
19327
19771
|
debug("server", "plan mode active \u2014 restricting tools");
|
|
19328
19772
|
systemPrompt = PLAN_SYSTEM_PROMPT;
|
|
19329
19773
|
} else if (hasPlatformContext) {
|
|
19330
|
-
systemPrompt = (systemPrompt || "") +
|
|
19774
|
+
systemPrompt = (systemPrompt || "") + buildTaskToolsPrompt({
|
|
19775
|
+
projectId,
|
|
19776
|
+
taskId: env["VIAGEN_TASK_ID"] || void 0,
|
|
19777
|
+
branch: env["VIAGEN_BRANCH"] || void 0,
|
|
19778
|
+
hasProcessManager: !!processManager
|
|
19779
|
+
});
|
|
19331
19780
|
}
|
|
19332
19781
|
const chatSession = new ChatSession({
|
|
19333
19782
|
env,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viagen",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"agent-browser": "^0.20.12",
|
|
46
46
|
"lucide-react": "^0.564.0",
|
|
47
47
|
"simple-git": "^3.31.1",
|
|
48
|
-
"viagen-sdk": "^0.1.
|
|
48
|
+
"viagen-sdk": "^0.1.2"
|
|
49
49
|
},
|
|
50
50
|
"license": "MIT",
|
|
51
51
|
"repository": {
|