agent-canvas 0.1.0
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/bin/agent-canvas.js +14 -0
- package/bin/agent-canvas.ts +377 -0
- package/daemon/build.ts +185 -0
- package/daemon/client/AnnotationProvider.tsx +185 -0
- package/daemon/client/AnnotationSidebar.tsx +314 -0
- package/daemon/client/App.tsx +611 -0
- package/daemon/client/CompareView.tsx +333 -0
- package/daemon/client/FileBrowser.tsx +188 -0
- package/daemon/client/FileIcon.tsx +76 -0
- package/daemon/client/FileViewer.tsx +207 -0
- package/daemon/client/PlanRenderer.tsx +665 -0
- package/daemon/client/Popover.tsx +285 -0
- package/daemon/client/ResponsePreview.tsx +260 -0
- package/daemon/client/RevisionSelect.tsx +97 -0
- package/daemon/client/SessionContext.tsx +3 -0
- package/daemon/client/SessionSwitcher.tsx +72 -0
- package/daemon/client/annotationContext.ts +114 -0
- package/daemon/client/components/Callout.tsx +25 -0
- package/daemon/client/components/Checklist.tsx +37 -0
- package/daemon/client/components/Choice.tsx +164 -0
- package/daemon/client/components/CodeBlock.tsx +35 -0
- package/daemon/client/components/Diff.tsx +65 -0
- package/daemon/client/components/FilePreview.tsx +88 -0
- package/daemon/client/components/Image.tsx +45 -0
- package/daemon/client/components/Mermaid.tsx +277 -0
- package/daemon/client/components/Note.tsx +13 -0
- package/daemon/client/components/Priority.tsx +19 -0
- package/daemon/client/components/ResponseNote.tsx +24 -0
- package/daemon/client/components/Section.tsx +32 -0
- package/daemon/client/components/Table.tsx +35 -0
- package/daemon/client/components/Task.tsx +75 -0
- package/daemon/client/components/UserInput.tsx +180 -0
- package/daemon/client/components/index.ts +15 -0
- package/daemon/client/domDiff.ts +336 -0
- package/daemon/client/exportMarkdown.ts +267 -0
- package/daemon/client/generateMarkdown.ts +194 -0
- package/daemon/client/highlightRange.ts +233 -0
- package/daemon/client/index.html +26 -0
- package/daemon/client/lcs.ts +19 -0
- package/daemon/client/popoverPosition.ts +31 -0
- package/daemon/client/runtime.ts +89 -0
- package/daemon/client/styles.css +306 -0
- package/daemon/client/theme.css +66 -0
- package/daemon/client/unifiedDiff.ts +604 -0
- package/daemon/dist/client.css +2 -0
- package/daemon/dist/client.js +68 -0
- package/daemon/dist/components.js +5 -0
- package/daemon/dist/index.html +108 -0
- package/daemon/dist/jsx-dev-runtime-shim.js +17 -0
- package/daemon/dist/jsx-runtime-shim.js +17 -0
- package/daemon/dist/react-dom-client-shim.js +4 -0
- package/daemon/dist/react-dom-shim.js +4 -0
- package/daemon/dist/react-shim.js +4 -0
- package/daemon/dist/runtime.js +1 -0
- package/daemon/dist/theme.css +66 -0
- package/daemon/src/compiler.ts +92 -0
- package/daemon/src/paths.ts +22 -0
- package/daemon/src/server.ts +448 -0
- package/daemon/src/session.ts +274 -0
- package/daemon/src/watcher.ts +59 -0
- package/hooks/session-start.sh +11 -0
- package/package.json +39 -0
- package/skills/canvas/SKILL.md +174 -0
- package/skills/canvas/components.md +349 -0
- package/skills/canvas/flows.md +406 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require("child_process");
|
|
4
|
+
const { join } = require("path");
|
|
5
|
+
|
|
6
|
+
const script = join(__dirname, "agent-canvas.ts");
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
execFileSync("bun", [script, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
10
|
+
} catch (e) {
|
|
11
|
+
if (e.status != null) process.exit(e.status);
|
|
12
|
+
console.error("Failed to run agent-canvas. Is Bun installed? https://bun.sh");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync, cpSync } from "fs";
|
|
5
|
+
import { join, resolve, dirname } from "path";
|
|
6
|
+
import { homedir, tmpdir } from "os";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
|
|
9
|
+
const PACKAGE_ROOT = resolve(join(dirname(import.meta.path), ".."));
|
|
10
|
+
const DATA_DIR = join(homedir(), ".claude", "agent-canvas");
|
|
11
|
+
const TEMP_DIR = join(tmpdir(), "agent-canvas");
|
|
12
|
+
const DAEMON_PORT = parseInt(process.env.CANVAS_PORT || "19400", 10);
|
|
13
|
+
const BASE_URL = `http://localhost:${DAEMON_PORT}`;
|
|
14
|
+
const WS_URL = `ws://localhost:${DAEMON_PORT}`;
|
|
15
|
+
const TIMEOUT_MS = parseInt(process.env.CANVAS_TIMEOUT || String(60 * 60 * 1000), 10);
|
|
16
|
+
const PID_FILE = join(TEMP_DIR, "daemon.pid");
|
|
17
|
+
|
|
18
|
+
function getSessionId(): string {
|
|
19
|
+
return process.env.CANVAS_SESSION_ID || (() => {
|
|
20
|
+
const id = randomUUID();
|
|
21
|
+
console.error(`Warning: CANVAS_SESSION_ID not set, using generated ID: ${id}`);
|
|
22
|
+
return id;
|
|
23
|
+
})();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
|
|
29
|
+
if (args.length === 0) {
|
|
30
|
+
printUsage();
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const command = args[0];
|
|
35
|
+
|
|
36
|
+
switch (command) {
|
|
37
|
+
case "push": return handlePush(args.slice(1));
|
|
38
|
+
case "wait": return handleWait(args.slice(1));
|
|
39
|
+
case "install": return handleInstall(args.slice(1));
|
|
40
|
+
case "daemon": return handleDaemon(args.slice(1));
|
|
41
|
+
default:
|
|
42
|
+
console.error(`Unknown command: ${command}`);
|
|
43
|
+
printUsage();
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printUsage() {
|
|
49
|
+
console.error(`agent-canvas — Interactive visual canvas for Claude Code
|
|
50
|
+
|
|
51
|
+
Commands:
|
|
52
|
+
agent-canvas install [local|global] Install skill & hooks for Claude Code
|
|
53
|
+
agent-canvas push <file.jsx> Push a canvas, open browser
|
|
54
|
+
agent-canvas wait Wait for user feedback (prints to stdout)
|
|
55
|
+
agent-canvas daemon status Show daemon status
|
|
56
|
+
agent-canvas daemon stop Stop the daemon
|
|
57
|
+
|
|
58
|
+
Environment:
|
|
59
|
+
CANVAS_SESSION_ID Session ID (set automatically by hooks)
|
|
60
|
+
CANVAS_PROJECT_ROOT Project root (set automatically by hooks)
|
|
61
|
+
CANVAS_PORT Daemon port (default: 19400)`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── install ──
|
|
65
|
+
|
|
66
|
+
async function handleInstall(args: string[]) {
|
|
67
|
+
let mode = args[0] as "local" | "global" | undefined;
|
|
68
|
+
|
|
69
|
+
if (!mode) {
|
|
70
|
+
// Interactive prompt
|
|
71
|
+
process.stderr.write("Install canvas skill and hooks for Claude Code.\n\n");
|
|
72
|
+
process.stderr.write(" local — install to .claude/ in current project\n");
|
|
73
|
+
process.stderr.write(" global — install to ~/.claude/ for all projects\n\n");
|
|
74
|
+
process.stderr.write("Choose [local/global]: ");
|
|
75
|
+
|
|
76
|
+
const input = await readLine();
|
|
77
|
+
mode = input.trim().toLowerCase() as "local" | "global";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (mode !== "local" && mode !== "global") {
|
|
81
|
+
console.error("Error: specify 'local' or 'global'");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const targetBase = mode === "global"
|
|
86
|
+
? join(homedir(), ".claude")
|
|
87
|
+
: join(process.cwd(), ".claude");
|
|
88
|
+
|
|
89
|
+
// Install skill
|
|
90
|
+
const skillTarget = join(targetBase, "skills", "canvas");
|
|
91
|
+
mkdirSync(skillTarget, { recursive: true });
|
|
92
|
+
|
|
93
|
+
const skillSrc = join(PACKAGE_ROOT, "skills", "canvas");
|
|
94
|
+
for (const file of ["SKILL.md", "components.md", "flows.md"]) {
|
|
95
|
+
const src = join(skillSrc, file);
|
|
96
|
+
if (existsSync(src)) {
|
|
97
|
+
cpSync(src, join(skillTarget, file));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.error(` Skill installed to ${skillTarget}`);
|
|
102
|
+
|
|
103
|
+
// Install hook script
|
|
104
|
+
const hooksDir = join(targetBase, "agent-canvas");
|
|
105
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
106
|
+
const hookSrc = join(PACKAGE_ROOT, "hooks", "session-start.sh");
|
|
107
|
+
const hookDest = join(hooksDir, "session-start.sh");
|
|
108
|
+
cpSync(hookSrc, hookDest);
|
|
109
|
+
try { require("fs").chmodSync(hookDest, 0o755); } catch {}
|
|
110
|
+
console.error(` Hook script installed to ${hookDest}`);
|
|
111
|
+
|
|
112
|
+
// Register hook in settings.json
|
|
113
|
+
const settingsPath = join(targetBase, "settings.json");
|
|
114
|
+
let settings: any = {};
|
|
115
|
+
if (existsSync(settingsPath)) {
|
|
116
|
+
try { settings = JSON.parse(readFileSync(settingsPath, "utf-8")); } catch {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!settings.hooks) settings.hooks = {};
|
|
120
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
121
|
+
|
|
122
|
+
const hasHook = settings.hooks.SessionStart.some((h: any) =>
|
|
123
|
+
h.hooks?.some((hh: any) => hh.command?.includes("CANVAS_SESSION_ID"))
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (!hasHook) {
|
|
127
|
+
settings.hooks.SessionStart.push({
|
|
128
|
+
matcher: "*",
|
|
129
|
+
hooks: [{
|
|
130
|
+
type: "command",
|
|
131
|
+
command: `bash "${hookDest}"`,
|
|
132
|
+
}],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
136
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
137
|
+
console.error(` Hook registered in ${settingsPath}`);
|
|
138
|
+
} else {
|
|
139
|
+
console.error(` Hook already registered in ${settingsPath}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.error(`\nInstalled! The /canvas command is now available in Claude Code.`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── push ──
|
|
146
|
+
|
|
147
|
+
async function handlePush(args: string[]) {
|
|
148
|
+
const fromHook = args.includes("--from-hook");
|
|
149
|
+
let jsx: string;
|
|
150
|
+
let filePath: string | undefined;
|
|
151
|
+
|
|
152
|
+
if (fromHook) {
|
|
153
|
+
jsx = await readStdin();
|
|
154
|
+
} else {
|
|
155
|
+
filePath = args.find((a) => !a.startsWith("--"));
|
|
156
|
+
if (!filePath) {
|
|
157
|
+
console.error("Error: No file specified. Usage: agent-canvas push <file.jsx>");
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
filePath = resolve(filePath);
|
|
161
|
+
if (!existsSync(filePath)) {
|
|
162
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
jsx = readFileSync(filePath, "utf-8");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const sessionId = getSessionId();
|
|
169
|
+
const projectRoot = process.env.CANVAS_PROJECT_ROOT || process.cwd();
|
|
170
|
+
|
|
171
|
+
// Extract --label flag
|
|
172
|
+
const labelIdx = args.indexOf("--label");
|
|
173
|
+
const label = labelIdx !== -1 && args[labelIdx + 1] ? args[labelIdx + 1] : undefined;
|
|
174
|
+
const autoLabel = !label && filePath ? filePath.split("/").pop()?.replace(/\.jsx$/, "") : undefined;
|
|
175
|
+
|
|
176
|
+
await ensureDaemon();
|
|
177
|
+
|
|
178
|
+
const response = await fetch(`${BASE_URL}/api/session/${sessionId}/plan`, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "Content-Type": "application/json" },
|
|
181
|
+
body: JSON.stringify({ jsx, projectRoot, label: label || autoLabel, sourceFile: filePath ? filePath.split("/").pop() : undefined }),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const result = await response.json() as any;
|
|
185
|
+
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
console.error(`Compilation error:\n${result.error}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (result.isNew) {
|
|
192
|
+
openBrowser(result.browserUrl);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log(JSON.stringify({
|
|
196
|
+
ok: true,
|
|
197
|
+
browserUrl: result.browserUrl,
|
|
198
|
+
revision: result.revision,
|
|
199
|
+
sessionId,
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── wait ──
|
|
204
|
+
|
|
205
|
+
async function handleWait(args: string[]) {
|
|
206
|
+
const sessionId = getSessionId();
|
|
207
|
+
|
|
208
|
+
await ensureDaemon();
|
|
209
|
+
|
|
210
|
+
// Check if feedback already exists
|
|
211
|
+
try {
|
|
212
|
+
const metaRes = await fetch(`${BASE_URL}/api/session/${sessionId}/meta`);
|
|
213
|
+
const meta = await metaRes.json() as any;
|
|
214
|
+
if (meta.currentRevision && meta.revisions) {
|
|
215
|
+
const currentRev = meta.revisions.find((r: any) => r.revision === meta.currentRevision);
|
|
216
|
+
if (currentRev?.hasFeedback) {
|
|
217
|
+
const fbRes = await fetch(`${BASE_URL}/api/session/${sessionId}/revision/${meta.currentRevision}/feedback`);
|
|
218
|
+
const fbData = await fbRes.json() as any;
|
|
219
|
+
if (fbData.feedback) {
|
|
220
|
+
process.stdout.write(fbData.feedback);
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch {}
|
|
226
|
+
|
|
227
|
+
await waitForFeedback(sessionId);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── daemon ──
|
|
231
|
+
|
|
232
|
+
async function handleDaemon(args: string[]) {
|
|
233
|
+
const subcommand = args[0];
|
|
234
|
+
|
|
235
|
+
if (subcommand === "status") {
|
|
236
|
+
try {
|
|
237
|
+
const res = await fetch(`${BASE_URL}/health`, { signal: AbortSignal.timeout(2000) });
|
|
238
|
+
const data = await res.json() as any;
|
|
239
|
+
console.log(`Daemon: running on port ${DAEMON_PORT}`);
|
|
240
|
+
console.log(`Sessions: ${data.sessions.length > 0 ? data.sessions.join(", ") : "none"}`);
|
|
241
|
+
} catch {
|
|
242
|
+
console.log("Daemon: not running");
|
|
243
|
+
}
|
|
244
|
+
} else if (subcommand === "stop") {
|
|
245
|
+
if (existsSync(PID_FILE)) {
|
|
246
|
+
const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
247
|
+
try {
|
|
248
|
+
process.kill(pid, "SIGTERM");
|
|
249
|
+
console.log("Daemon stopped.");
|
|
250
|
+
} catch {
|
|
251
|
+
console.log("Daemon was not running (stale PID file).");
|
|
252
|
+
}
|
|
253
|
+
try { unlinkSync(PID_FILE); } catch {}
|
|
254
|
+
} else {
|
|
255
|
+
console.log("No daemon PID file found.");
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
console.error("Usage: agent-canvas daemon [status|stop]");
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── helpers ──
|
|
264
|
+
|
|
265
|
+
async function ensureDaemon(): Promise<void> {
|
|
266
|
+
if (await isDaemonRunning()) return;
|
|
267
|
+
|
|
268
|
+
console.error("Starting canvas daemon...");
|
|
269
|
+
mkdirSync(TEMP_DIR, { recursive: true });
|
|
270
|
+
|
|
271
|
+
const daemonScript = join(PACKAGE_ROOT, "daemon", "src", "server.ts");
|
|
272
|
+
|
|
273
|
+
const child = spawn("bun", ["run", daemonScript], {
|
|
274
|
+
detached: true,
|
|
275
|
+
stdio: "ignore",
|
|
276
|
+
cwd: join(PACKAGE_ROOT, "daemon"),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
child.unref();
|
|
280
|
+
|
|
281
|
+
if (child.pid) {
|
|
282
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (let i = 0; i < 30; i++) {
|
|
286
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
287
|
+
if (await isDaemonRunning()) {
|
|
288
|
+
console.error("Daemon started.");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.error("Error: Daemon failed to start within 3 seconds.");
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function isDaemonRunning(): Promise<boolean> {
|
|
298
|
+
try {
|
|
299
|
+
const res = await fetch(`${BASE_URL}/health`, { signal: AbortSignal.timeout(1000) });
|
|
300
|
+
const data = await res.json() as any;
|
|
301
|
+
return data.ok === true;
|
|
302
|
+
} catch {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function openBrowser(url: string) {
|
|
308
|
+
const candidates = process.platform === "darwin"
|
|
309
|
+
? ["open"]
|
|
310
|
+
: ["xdg-open", "wslview", "sensible-browser"];
|
|
311
|
+
|
|
312
|
+
for (const cmd of candidates) {
|
|
313
|
+
const which = Bun.spawnSync(["which", cmd]);
|
|
314
|
+
if (which.exitCode === 0) {
|
|
315
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
console.error(`Open this URL in your browser: ${url}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function waitForFeedback(sessionId: string): Promise<void> {
|
|
323
|
+
return new Promise((resolveP, rejectP) => {
|
|
324
|
+
const ws = new WebSocket(`${WS_URL}/ws/wait/${sessionId}`);
|
|
325
|
+
const timeout = setTimeout(() => {
|
|
326
|
+
ws.close();
|
|
327
|
+
console.error("Error: Timeout waiting for feedback.");
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}, TIMEOUT_MS);
|
|
330
|
+
|
|
331
|
+
ws.onmessage = (event) => {
|
|
332
|
+
try {
|
|
333
|
+
const data = JSON.parse(typeof event.data === "string" ? event.data : "");
|
|
334
|
+
if (data.type === "submit") {
|
|
335
|
+
clearTimeout(timeout);
|
|
336
|
+
process.stdout.write(data.feedback);
|
|
337
|
+
ws.close();
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
} catch {}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
ws.onerror = () => {
|
|
344
|
+
clearTimeout(timeout);
|
|
345
|
+
console.error("Error: WebSocket connection failed.");
|
|
346
|
+
process.exit(1);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
ws.onclose = () => {
|
|
350
|
+
clearTimeout(timeout);
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function readStdin(): Promise<string> {
|
|
356
|
+
const chunks: Buffer[] = [];
|
|
357
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
358
|
+
chunks.push(Buffer.from(chunk));
|
|
359
|
+
}
|
|
360
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function readLine(): Promise<string> {
|
|
364
|
+
const buf: number[] = [];
|
|
365
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
366
|
+
for (const byte of chunk) {
|
|
367
|
+
if (byte === 10) return Buffer.from(buf).toString("utf-8");
|
|
368
|
+
buf.push(byte);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return Buffer.from(buf).toString("utf-8");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
main().catch((e) => {
|
|
375
|
+
console.error(e);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
});
|
package/daemon/build.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { mkdirSync, cpSync, writeFileSync, readFileSync, existsSync, watch as fsWatch } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
|
|
5
|
+
const ROOT = dirname(import.meta.path);
|
|
6
|
+
const DIST = join(ROOT, "dist");
|
|
7
|
+
|
|
8
|
+
async function build() {
|
|
9
|
+
console.log("Building canvas client...");
|
|
10
|
+
|
|
11
|
+
mkdirSync(DIST, { recursive: true });
|
|
12
|
+
|
|
13
|
+
const REACT_EXTERNALS = ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "react/jsx-dev-runtime"];
|
|
14
|
+
|
|
15
|
+
// 1. Build runtime (shared context between app and components)
|
|
16
|
+
console.log(" Building #canvas/runtime...");
|
|
17
|
+
const runtimeResult = await Bun.build({
|
|
18
|
+
entrypoints: [join(ROOT, "client/runtime.ts")],
|
|
19
|
+
outdir: DIST,
|
|
20
|
+
format: "esm",
|
|
21
|
+
external: [...REACT_EXTERNALS],
|
|
22
|
+
naming: "runtime.js",
|
|
23
|
+
minify: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!runtimeResult.success) {
|
|
27
|
+
console.error("Runtime build failed:", runtimeResult.logs);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Build the components library (ESM, React + runtime external)
|
|
32
|
+
console.log(" Building #canvas/components...");
|
|
33
|
+
const componentsResult = await Bun.build({
|
|
34
|
+
entrypoints: [join(ROOT, "client/components/index.ts")],
|
|
35
|
+
outdir: DIST,
|
|
36
|
+
format: "esm",
|
|
37
|
+
external: [...REACT_EXTERNALS, "#canvas/runtime"],
|
|
38
|
+
naming: "components.js",
|
|
39
|
+
minify: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!componentsResult.success) {
|
|
43
|
+
console.error("Components build failed:", componentsResult.logs);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Build the main client app (ESM, React + components + runtime external)
|
|
48
|
+
console.log(" Building client app...");
|
|
49
|
+
const clientResult = await Bun.build({
|
|
50
|
+
entrypoints: [join(ROOT, "client/App.tsx")],
|
|
51
|
+
outdir: DIST,
|
|
52
|
+
format: "esm",
|
|
53
|
+
external: [...REACT_EXTERNALS, "#canvas/components", "#canvas/runtime"],
|
|
54
|
+
naming: "client.js",
|
|
55
|
+
minify: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!clientResult.success) {
|
|
59
|
+
console.error("Client build failed:", clientResult.logs);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. Build Tailwind CSS
|
|
64
|
+
console.log(" Building CSS...");
|
|
65
|
+
await $`cd ${ROOT} && npx @tailwindcss/cli -i client/styles.css -o dist/client.css --minify`.quiet();
|
|
66
|
+
|
|
67
|
+
// 5. Read theme.css to inline into HTML for browser Tailwind runtime
|
|
68
|
+
const themeCss = readFileSync(join(ROOT, "client/theme.css"), "utf-8");
|
|
69
|
+
|
|
70
|
+
// 6. Create React shims for import maps
|
|
71
|
+
// These re-export from the global React loaded via UMD
|
|
72
|
+
console.log(" Creating React shims...");
|
|
73
|
+
|
|
74
|
+
// Write the index.html to use CDN React
|
|
75
|
+
const indexHtml = `<!DOCTYPE html>
|
|
76
|
+
<html lang="en" data-theme="dark">
|
|
77
|
+
<head>
|
|
78
|
+
<meta charset="utf-8" />
|
|
79
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
80
|
+
<title>Agent Canvas</title>
|
|
81
|
+
<script>document.documentElement.dataset.theme = localStorage.getItem('canvas-theme') || 'dark';</script>
|
|
82
|
+
<link rel="stylesheet" href="/assets/client.css" />
|
|
83
|
+
<link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.11.1/styles/github-dark-dimmed.min.css" />
|
|
84
|
+
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
85
|
+
<style type="text/tailwindcss">
|
|
86
|
+
${themeCss}
|
|
87
|
+
</style>
|
|
88
|
+
</head>
|
|
89
|
+
<body class="min-h-screen">
|
|
90
|
+
<div id="root"></div>
|
|
91
|
+
<script src="https://unpkg.com/@highlightjs/cdn-assets@11.11.1/highlight.min.js"></script>
|
|
92
|
+
<script src="https://unpkg.com/mermaid@11.4.1/dist/mermaid.min.js"></script>
|
|
93
|
+
<script>mermaid.initialize({ startOnLoad: false, theme: document.documentElement.dataset.theme === 'light' ? 'default' : 'dark' });</script>
|
|
94
|
+
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
|
|
95
|
+
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
|
|
96
|
+
<script>
|
|
97
|
+
// Create ESM modules from UMD globals
|
|
98
|
+
window.__REACT_ESM__ = Object.keys(React).reduce((m, k) => { m[k] = React[k]; return m; }, { default: React, __esModule: true });
|
|
99
|
+
window.__REACT_DOM_ESM__ = Object.keys(ReactDOM).reduce((m, k) => { m[k] = ReactDOM[k]; return m; }, { default: ReactDOM, __esModule: true });
|
|
100
|
+
</script>
|
|
101
|
+
<script type="importmap">
|
|
102
|
+
{
|
|
103
|
+
"imports": {
|
|
104
|
+
"react": "/assets/react-shim.js",
|
|
105
|
+
"react-dom": "/assets/react-dom-shim.js",
|
|
106
|
+
"react-dom/client": "/assets/react-dom-client-shim.js",
|
|
107
|
+
"react/jsx-runtime": "/assets/jsx-runtime-shim.js",
|
|
108
|
+
"react/jsx-dev-runtime": "/assets/jsx-dev-runtime-shim.js",
|
|
109
|
+
"#canvas/components": "/assets/components.js",
|
|
110
|
+
"#canvas/runtime": "/assets/runtime.js"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
<script type="module" src="/assets/client.js"></script>
|
|
115
|
+
</body>
|
|
116
|
+
</html>`;
|
|
117
|
+
|
|
118
|
+
writeFileSync(join(DIST, "index.html"), indexHtml);
|
|
119
|
+
|
|
120
|
+
// React ESM shims
|
|
121
|
+
writeFileSync(join(DIST, "react-shim.js"), `
|
|
122
|
+
const R = window.React;
|
|
123
|
+
export default R;
|
|
124
|
+
export const { useState, useEffect, useRef, useCallback, useContext, useMemo, useReducer, createContext, createElement, Fragment, Children, cloneElement, isValidElement, memo, forwardRef, lazy, Suspense, startTransition, useTransition, useDeferredValue, useId, useSyncExternalStore, useInsertionEffect, useImperativeHandle, useLayoutEffect, useDebugValue, Component, PureComponent } = R;
|
|
125
|
+
`);
|
|
126
|
+
|
|
127
|
+
writeFileSync(join(DIST, "react-dom-shim.js"), `
|
|
128
|
+
const RD = window.ReactDOM;
|
|
129
|
+
export default RD;
|
|
130
|
+
export const { createPortal, flushSync, hydrate, render, unmountComponentAtNode, findDOMNode, unstable_batchedUpdates } = RD;
|
|
131
|
+
`);
|
|
132
|
+
|
|
133
|
+
writeFileSync(join(DIST, "react-dom-client-shim.js"), `
|
|
134
|
+
const RD = window.ReactDOM;
|
|
135
|
+
export const { createRoot, hydrateRoot } = RD;
|
|
136
|
+
export default { createRoot: RD.createRoot, hydrateRoot: RD.hydrateRoot };
|
|
137
|
+
`);
|
|
138
|
+
|
|
139
|
+
// jsxDEV signature: (type, props, key, isStaticChildren, source, self)
|
|
140
|
+
// jsx/jsxs signature: (type, props, key)
|
|
141
|
+
// createElement signature: (type, props, ...children)
|
|
142
|
+
// We need to adapt: extract children from props and pass to createElement
|
|
143
|
+
const jsxShimCode = `
|
|
144
|
+
const R = window.React;
|
|
145
|
+
export const Fragment = R.Fragment;
|
|
146
|
+
function _jsx(type, props, key) {
|
|
147
|
+
if (props && 'children' in props) {
|
|
148
|
+
const { children, ...rest } = props;
|
|
149
|
+
if (key !== undefined && key !== null) rest.key = key;
|
|
150
|
+
return Array.isArray(children)
|
|
151
|
+
? R.createElement(type, rest, ...children)
|
|
152
|
+
: R.createElement(type, rest, children);
|
|
153
|
+
}
|
|
154
|
+
if (key !== undefined && key !== null) props = { ...props, key };
|
|
155
|
+
return R.createElement(type, props);
|
|
156
|
+
}
|
|
157
|
+
export const jsx = _jsx;
|
|
158
|
+
export const jsxs = _jsx;
|
|
159
|
+
export function jsxDEV(type, props, key) { return _jsx(type, props, key); }
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
writeFileSync(join(DIST, "jsx-runtime-shim.js"), jsxShimCode);
|
|
163
|
+
writeFileSync(join(DIST, "jsx-dev-runtime-shim.js"), jsxShimCode);
|
|
164
|
+
|
|
165
|
+
console.log("Build complete! Output in dist/");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
build().then(() => {
|
|
169
|
+
if (process.argv.includes("--watch")) {
|
|
170
|
+
const clientDir = join(ROOT, "client");
|
|
171
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
172
|
+
console.log("Watching for changes in client/...");
|
|
173
|
+
fsWatch(clientDir, { recursive: true }, (_event: string, filename: string | null) => {
|
|
174
|
+
if (!filename || filename.endsWith("~")) return;
|
|
175
|
+
if (timeout) clearTimeout(timeout);
|
|
176
|
+
timeout = setTimeout(() => {
|
|
177
|
+
console.log(`\n Changed: ${filename}`);
|
|
178
|
+
build().catch((e) => console.error("Rebuild failed:", e));
|
|
179
|
+
}, 200);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}).catch((e) => {
|
|
183
|
+
console.error("Build failed:", e);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|