agent-canvas 0.1.44 → 0.2.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/README.md +14 -0
- package/bin/lib/commands/daemon.ts +3 -6
- package/bin/lib/commands/push.ts +1 -1
- package/bin/lib/daemon-lifecycle.ts +65 -72
- package/bin/lib/helpers.test.ts +1 -0
- package/bin/lib/helpers.ts +30 -74
- package/daemon/build.ts +37 -16
- package/daemon/client/App.tsx +27 -61
- package/daemon/client/CompareView.tsx +4 -4
- package/daemon/client/FileBrowser.tsx +4 -4
- package/daemon/client/FileViewer.tsx +4 -4
- package/daemon/client/OverviewView.tsx +2 -2
- package/daemon/client/RevisionSelect.tsx +1 -1
- package/daemon/client/canvas-runtime.ts +1 -0
- package/daemon/client/clientApi.ts +18 -0
- package/daemon/client/components/index.ts +3 -16
- package/daemon/client/styles.css +4 -457
- package/daemon/dist/annotations-runtime.js +1 -0
- package/daemon/dist/client.css +1 -1
- package/daemon/dist/client.js +76 -76
- package/daemon/dist/components.js +59 -59
- package/daemon/dist/index.html +1 -0
- package/daemon/dist/preact-compat.js +1 -1
- package/daemon/dist/runtime.js +1 -1
- package/daemon/src/components.ts +13 -0
- package/daemon/src/handlers/api.ts +13 -9
- package/daemon/src/handlers/files.ts +2 -3
- package/daemon/src/handlers/static.ts +1 -1
- package/daemon/src/handlers/uploads.ts +2 -3
- package/daemon/src/paths.ts +10 -24
- package/daemon/src/remote-feedback.test.ts +1 -1
- package/daemon/src/remote-feedback.ts +1 -1
- package/daemon/src/secrets-api.test.ts +2 -2
- package/daemon/src/server.ts +11 -10
- package/daemon/src/share-e2e.test.ts +1 -1
- package/daemon/src/share.test.ts +1 -1
- package/daemon/src/share.ts +1 -1
- package/daemon/tsconfig.json +1 -3
- package/package.json +11 -4
- package/.github/dark.png +0 -0
- package/.github/light.png +0 -0
- package/.github/workflows/ci.yml +0 -48
- package/.github/workflows/deploy-worker-prod.yml +0 -19
- package/.github/workflows/deploy-worker-stage.yml +0 -21
- package/.github/workflows/deploy-worker.yml +0 -72
- package/.github/workflows/release.yml +0 -36
- package/CLAUDE.md +0 -86
- package/bun.lock +0 -377
- package/daemon/client/AnnotationEditor.tsx +0 -315
- package/daemon/client/AnnotationProvider.tsx +0 -195
- package/daemon/client/AnnotationSidebar.tsx +0 -627
- package/daemon/client/FileIcon.tsx +0 -76
- package/daemon/client/PlanRenderer.tsx +0 -614
- package/daemon/client/Popover.tsx +0 -350
- package/daemon/client/RenderErrorContext.ts +0 -11
- package/daemon/client/ResponsePreview.tsx +0 -186
- package/daemon/client/SessionContext.tsx +0 -3
- package/daemon/client/annotationContext.ts +0 -114
- package/daemon/client/annotationDom.ts +0 -104
- package/daemon/client/annotationDraft.test.ts +0 -188
- package/daemon/client/annotationDraft.ts +0 -130
- package/daemon/client/compat/preact-all.ts +0 -4
- package/daemon/client/components/Callout.tsx +0 -26
- package/daemon/client/components/Checklist.tsx +0 -37
- package/daemon/client/components/Choice.tsx +0 -176
- package/daemon/client/components/CodeBlock.tsx +0 -35
- package/daemon/client/components/Diff.tsx +0 -65
- package/daemon/client/components/FilePreview.tsx +0 -88
- package/daemon/client/components/Image.tsx +0 -45
- package/daemon/client/components/Markdown.tsx +0 -44
- package/daemon/client/components/Mermaid.tsx +0 -565
- package/daemon/client/components/Note.tsx +0 -14
- package/daemon/client/components/Priority.tsx +0 -19
- package/daemon/client/components/ResponseNote.tsx +0 -19
- package/daemon/client/components/Section.tsx +0 -33
- package/daemon/client/components/Table.tsx +0 -35
- package/daemon/client/components/Task.tsx +0 -76
- package/daemon/client/components/UserInput.tsx +0 -174
- package/daemon/client/components/renderMarkdownChildren.tsx +0 -97
- package/daemon/client/generateMarkdown.ts +0 -273
- package/daemon/client/highlightRange.ts +0 -224
- package/daemon/client/popoverPosition.ts +0 -31
- package/daemon/client/runtime.ts +0 -123
- package/daemon/client/theme.css +0 -66
- package/daemon/client/useTextAnnotation.tsx +0 -188
- package/daemon/client/utils.ts +0 -13
- package/daemon/src/compiler.test.ts +0 -57
- package/daemon/src/compiler.ts +0 -360
- package/daemon/src/handlers/utils.ts +0 -14
- package/daemon/src/router.ts +0 -16
- package/daemon/src/session-secrets.test.ts +0 -83
- package/daemon/src/session.ts +0 -612
- package/daemon/src/watcher.ts +0 -71
- package/daemon/src/websocket.test.ts +0 -99
- package/daemon/src/websocket.ts +0 -211
- package/tsconfig.json +0 -14
- package/workers/canvas-share/README.md +0 -159
- package/workers/canvas-share/oblaka.ts +0 -56
- package/workers/canvas-share/package.json +0 -25
- package/workers/canvas-share/public/assets/.gitkeep +0 -0
- package/workers/canvas-share/public/assets/client.css +0 -2
- package/workers/canvas-share/public/assets/client.js +0 -91
- package/workers/canvas-share/public/assets/components.js +0 -65
- package/workers/canvas-share/public/assets/jsx-runtime.js +0 -3
- package/workers/canvas-share/public/assets/preact-compat.js +0 -1
- package/workers/canvas-share/public/assets/runtime.js +0 -1
- package/workers/canvas-share/public/index.html +0 -385
- package/workers/canvas-share/public/screenshot-dark.png +0 -0
- package/workers/canvas-share/public/screenshot-light.png +0 -0
- package/workers/canvas-share/src/index.test.ts +0 -209
- package/workers/canvas-share/src/index.ts +0 -518
- package/workers/canvas-share/src/types.ts +0 -119
- package/workers/canvas-share/src/util.ts +0 -156
- package/workers/canvas-share/src/validation.ts +0 -160
- package/workers/canvas-share/tsconfig.json +0 -16
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { test, expect } from "bun:test";
|
|
2
|
-
import { compilePlan } from "./compiler";
|
|
3
|
-
|
|
4
|
-
// Regression coverage for the error-reporting path. Bun surfaces JSX syntax
|
|
5
|
-
// errors through three shapes (Transpiler throw, build logs, AggregateError
|
|
6
|
-
// from Bun.build); all must reach the CLI as an actionable message carrying a
|
|
7
|
-
// position + code frame — never a bare "Parse error" / "Bundle failed".
|
|
8
|
-
|
|
9
|
-
test("syntax error reports line, column and a code frame", async () => {
|
|
10
|
-
// A straight double-quote inside a double-quoted attribute terminates the
|
|
11
|
-
// attribute early — the exact mistake that previously printed only
|
|
12
|
-
// "Parse error" with no location.
|
|
13
|
-
const result = await compilePlan(`<Section title="a "b" c">x</Section>`);
|
|
14
|
-
|
|
15
|
-
expect(result.ok).toBe(false);
|
|
16
|
-
if (result.ok) return;
|
|
17
|
-
expect(result.error).toMatch(/\(line \d+, col \d+\)/);
|
|
18
|
-
// Code frame: offending source line + caret.
|
|
19
|
-
expect(result.error).toContain("^");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("syntax error line maps back to the authored file (1-based, not offset by injected imports)", async () => {
|
|
23
|
-
// Bad token on the authored 3rd line; the reported line must be small/positive,
|
|
24
|
-
// not shifted by the prepended component-import preamble.
|
|
25
|
-
const result = await compilePlan(`<Section title="ok">\n <Item id="a" label="x">\n <span a="1 "2"></span>\n </Item>\n</Section>`);
|
|
26
|
-
|
|
27
|
-
expect(result.ok).toBe(false);
|
|
28
|
-
if (result.ok) return;
|
|
29
|
-
const m = result.error.match(/\(line (\d+), col \d+\)/);
|
|
30
|
-
expect(m).not.toBeNull();
|
|
31
|
-
const line = Number(m![1]);
|
|
32
|
-
expect(line).toBeGreaterThan(0);
|
|
33
|
-
expect(line).toBeLessThan(10);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("well-formed canvas compiles", async () => {
|
|
37
|
-
const result = await compilePlan(`<Section title="Hello"><Item id="a" label="b">text</Item></Section>`);
|
|
38
|
-
expect(result.ok).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("runbook secret input compiles", async () => {
|
|
42
|
-
const result = await compilePlan(`<SecretInput id="service-token" label="Service token" env="SERVICE_TOKEN" required />`);
|
|
43
|
-
expect(result.ok).toBe(true);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("missing required component prop fails before the browser renders", async () => {
|
|
47
|
-
const result = await compilePlan(`<Table columns={["Name"]} rows={[["Canvas"]]} />`);
|
|
48
|
-
|
|
49
|
-
expect(result.ok).toBe(false);
|
|
50
|
-
if (result.ok) return;
|
|
51
|
-
expect(result.error).toBe("Runtime error: Table: required prop `headers` must be an array");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("component prop validation accepts valid table props", async () => {
|
|
55
|
-
const result = await compilePlan(`<Table headers={["Name"]} rows={[["Canvas"]]} />`);
|
|
56
|
-
expect(result.ok).toBe(true);
|
|
57
|
-
});
|
package/daemon/src/compiler.ts
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, unlinkSync, readFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { randomUUID } from "crypto";
|
|
4
|
-
import { h, Fragment, type ComponentChildren } from "preact";
|
|
5
|
-
import renderToString from "preact-render-to-string";
|
|
6
|
-
import { parse as parseMermaid } from "mermaid-parser-bundle";
|
|
7
|
-
import { COMPILE_TEMP_DIR } from "./paths";
|
|
8
|
-
|
|
9
|
-
type CompileResult =
|
|
10
|
-
| { ok: true; js: string }
|
|
11
|
-
| { ok: false; error: string };
|
|
12
|
-
|
|
13
|
-
const COMPONENT_IMPORTS = `import React from 'react';
|
|
14
|
-
import * as C from '#canvas/components';
|
|
15
|
-
const { Section, Item, Task, FilePreview, CodeBlock, Callout,
|
|
16
|
-
Mermaid, Table, Priority, Checklist, Note, Diff,
|
|
17
|
-
Choice, MultiChoice, UserInput, RangeInput, SecretInput, ImageView,
|
|
18
|
-
Markdown, useFeedback, useAnnotations } = C;
|
|
19
|
-
`;
|
|
20
|
-
|
|
21
|
-
/** Number of lines in COMPONENT_IMPORTS — used to adjust error line numbers */
|
|
22
|
-
const COMPONENT_IMPORTS_LINES = COMPONENT_IMPORTS.split("\n").length - 1;
|
|
23
|
-
|
|
24
|
-
// --- Validation via Preact --------------------------------------------------
|
|
25
|
-
// After Bun.build() succeeds we strip ESM syntax from the compiled output,
|
|
26
|
-
// provide Preact as the React implementation with lightweight stub components,
|
|
27
|
-
// and actually render the component tree. Any runtime error (undefined vars,
|
|
28
|
-
// broken template literals, …) is caught and reported back to the CLI.
|
|
29
|
-
|
|
30
|
-
/** jsxDEV(type, props, key, isStaticChildren, source, self) → VNode */
|
|
31
|
-
function mockJsxDEV(type: any, props: any, key?: any) {
|
|
32
|
-
const { children, ...rest } = props || {};
|
|
33
|
-
return h(type, { ...rest, key }, children);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface StubProps {
|
|
37
|
-
children?: ComponentChildren;
|
|
38
|
-
[key: string]: unknown;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Passthrough component — just renders its children */
|
|
42
|
-
const Stub = ({ children }: StubProps) => h(Fragment, null, children);
|
|
43
|
-
|
|
44
|
-
type RequiredPropKind = "array" | "string";
|
|
45
|
-
|
|
46
|
-
function validatingStub(component: string, requiredProps: Record<string, RequiredPropKind>) {
|
|
47
|
-
return (props: StubProps) => {
|
|
48
|
-
for (const [prop, kind] of Object.entries(requiredProps)) {
|
|
49
|
-
const value = props[prop];
|
|
50
|
-
const valid = kind === "array" ? Array.isArray(value) : typeof value === kind;
|
|
51
|
-
if (!valid) {
|
|
52
|
-
throw new Error(`${component}: required prop \`${prop}\` must be ${kind === "array" ? "an array" : `a ${kind}`}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return h(Fragment, null, props.children);
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const STUB_COMPONENTS: Record<string, any> = {};
|
|
60
|
-
for (const name of [
|
|
61
|
-
"CodeBlock", "Callout", "Note", "Markdown",
|
|
62
|
-
]) {
|
|
63
|
-
STUB_COMPONENTS[name] = Stub;
|
|
64
|
-
}
|
|
65
|
-
Object.assign(STUB_COMPONENTS, {
|
|
66
|
-
Section: validatingStub("Section", { title: "string" }),
|
|
67
|
-
Item: validatingStub("Item", { id: "string", label: "string" }),
|
|
68
|
-
Task: validatingStub("Task", { id: "string", label: "string" }),
|
|
69
|
-
FilePreview: validatingStub("FilePreview", { path: "string" }),
|
|
70
|
-
Table: validatingStub("Table", { headers: "array", rows: "array" }),
|
|
71
|
-
Priority: validatingStub("Priority", { level: "string" }),
|
|
72
|
-
Checklist: validatingStub("Checklist", { items: "array" }),
|
|
73
|
-
Diff: validatingStub("Diff", { before: "string", after: "string" }),
|
|
74
|
-
Choice: validatingStub("Choice", { id: "string", label: "string", options: "array" }),
|
|
75
|
-
MultiChoice: validatingStub("MultiChoice", { id: "string", label: "string", options: "array" }),
|
|
76
|
-
UserInput: validatingStub("UserInput", { id: "string", label: "string" }),
|
|
77
|
-
RangeInput: validatingStub("RangeInput", { id: "string", label: "string" }),
|
|
78
|
-
SecretInput: validatingStub("SecretInput", { id: "string", label: "string", env: "string" }),
|
|
79
|
-
ImageView: validatingStub("ImageView", { src: "string" }),
|
|
80
|
-
});
|
|
81
|
-
// Mermaid stub collects diagram sources for post-render validation
|
|
82
|
-
let collectedMermaidSources: string[] = [];
|
|
83
|
-
STUB_COMPONENTS.Mermaid = ({ children }: any) => {
|
|
84
|
-
const source = typeof children === "string" ? children : String(children ?? "");
|
|
85
|
-
if (source.trim()) collectedMermaidSources.push(source.trim());
|
|
86
|
-
return null;
|
|
87
|
-
};
|
|
88
|
-
// Hook stubs — return shapes that won't blow up when destructured
|
|
89
|
-
STUB_COMPONENTS.useFeedback = () => ({ submit: () => {}, value: null });
|
|
90
|
-
STUB_COMPONENTS.useAnnotations = () => ({ annotations: [], addAnnotation: () => {} });
|
|
91
|
-
|
|
92
|
-
const MOCK_REACT = {
|
|
93
|
-
createElement: h,
|
|
94
|
-
Fragment,
|
|
95
|
-
useState: (init: any) => [init, () => {}],
|
|
96
|
-
useEffect: () => {},
|
|
97
|
-
useRef: (init: any) => ({ current: init }),
|
|
98
|
-
useMemo: (fn: any) => fn(),
|
|
99
|
-
useCallback: (fn: any) => fn,
|
|
100
|
-
useReducer: (r: any, init: any) => [init, () => {}],
|
|
101
|
-
useContext: () => ({}),
|
|
102
|
-
createContext: () => ({ Provider: Stub, Consumer: Stub }),
|
|
103
|
-
useLayoutEffect: () => {},
|
|
104
|
-
useImperativeHandle: () => {},
|
|
105
|
-
useDebugValue: () => {},
|
|
106
|
-
useDeferredValue: (v: any) => v,
|
|
107
|
-
useTransition: () => [false, (fn: any) => fn()],
|
|
108
|
-
useId: () => "mock",
|
|
109
|
-
useSyncExternalStore: (sub: any, get: any) => get(),
|
|
110
|
-
memo: (c: any) => c,
|
|
111
|
-
forwardRef: (c: any) => c,
|
|
112
|
-
lazy: (c: any) => c,
|
|
113
|
-
Children: { map: (c: any, fn: any) => (Array.isArray(c) ? c : [c]).map(fn), toArray: (c: any) => (Array.isArray(c) ? c : [c]) },
|
|
114
|
-
isValidElement: () => true,
|
|
115
|
-
cloneElement: h,
|
|
116
|
-
Suspense: Stub,
|
|
117
|
-
StrictMode: Stub,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
async function validateCompiledPlan(js: string): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
121
|
-
let code = js;
|
|
122
|
-
|
|
123
|
-
// Strip ESM import statements
|
|
124
|
-
code = code.replace(/import\s+\w+\s+from\s+"[^"]+";?/g, "");
|
|
125
|
-
code = code.replace(/import\s+\*\s+as\s+\w+\s+from\s+"[^"]+";?/g, "");
|
|
126
|
-
code = code.replace(/import\s*\{[^}]*\}\s*from\s+"[^"]+";?/g, "");
|
|
127
|
-
|
|
128
|
-
// Strip export, extract default name
|
|
129
|
-
let defaultName: string | null = null;
|
|
130
|
-
code = code.replace(/export\s*\{\s*(\w+)\s+as\s+default\s*\};?/g, (_, name) => {
|
|
131
|
-
defaultName = name;
|
|
132
|
-
return "";
|
|
133
|
-
});
|
|
134
|
-
if (!defaultName) {
|
|
135
|
-
code = code.replace(/export\s+default\s+(\w+);?/g, (_, name) => {
|
|
136
|
-
defaultName = name;
|
|
137
|
-
return "";
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const callDefault = defaultName
|
|
142
|
-
? `\nreturn typeof ${defaultName} === "function" ? ${defaultName}() : null;`
|
|
143
|
-
: "\nreturn null;";
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
collectedMermaidSources = [];
|
|
147
|
-
const fn = new Function("React", "C", "jsxDEV", "jsx", "Fragment", code + callDefault);
|
|
148
|
-
const vnode = fn(MOCK_REACT, STUB_COMPONENTS, mockJsxDEV, mockJsxDEV, Fragment);
|
|
149
|
-
if (vnode) renderToString(vnode);
|
|
150
|
-
} catch (e: any) {
|
|
151
|
-
return { ok: false, error: e?.message || String(e) };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Validate collected Mermaid diagram sources
|
|
155
|
-
for (const source of collectedMermaidSources) {
|
|
156
|
-
try {
|
|
157
|
-
await parseMermaid(source);
|
|
158
|
-
} catch (e: any) {
|
|
159
|
-
const msg = (e?.message || "Invalid diagram").split("\n")[0];
|
|
160
|
-
return { ok: false, error: `Mermaid syntax error: ${msg}` };
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return { ok: true };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Normalize a Bun build/transpile failure into a precise, actionable string.
|
|
169
|
-
*
|
|
170
|
-
* Bun surfaces JSX syntax errors through three different shapes, all handled
|
|
171
|
-
* here so the CLI never again prints a bare "Parse error" / "Bundle failed":
|
|
172
|
-
* - `Bun.Transpiler.transformSync` throws a `BuildMessage` (has `.position`).
|
|
173
|
-
* - `Bun.build()` returns `{ success: false, logs: [BuildMessage] }`.
|
|
174
|
-
* - `Bun.build()` throws an `AggregateError` whose `.errors[0]` is a
|
|
175
|
-
* `BuildMessage` — the case the old catch discarded, returning only the
|
|
176
|
-
* useless top-level "Bundle failed".
|
|
177
|
-
*
|
|
178
|
-
* `lineOffset` is the number of lines Canvas prepended (component imports +
|
|
179
|
-
* wrapper) so the reported line maps back to the user's authored file. When a
|
|
180
|
-
* position is available we append `(line N, col M)` plus a one-line code frame
|
|
181
|
-
* with a caret — that suffix is also what lets the CLI classify the error as a
|
|
182
|
-
* user error and skip a pointless daemon restart (see push.ts).
|
|
183
|
-
*/
|
|
184
|
-
function formatBuildError(err: any, lineOffset: number): string {
|
|
185
|
-
// Collect every candidate that might carry a `.position`, most specific first.
|
|
186
|
-
const candidates: any[] = [];
|
|
187
|
-
if (err?.position) candidates.push(err);
|
|
188
|
-
if (Array.isArray(err?.errors)) candidates.push(...err.errors);
|
|
189
|
-
if (!candidates.length && err && typeof err[Symbol.iterator] === "function") {
|
|
190
|
-
try { candidates.push(...err); } catch {}
|
|
191
|
-
}
|
|
192
|
-
const detail = candidates.find((c) => c?.position) ?? candidates[0] ?? err;
|
|
193
|
-
const message = String(detail?.message || err?.message || "Syntax error").split("\n")[0];
|
|
194
|
-
|
|
195
|
-
const pos = detail?.position;
|
|
196
|
-
if (!pos || typeof pos.line !== "number") return message;
|
|
197
|
-
|
|
198
|
-
const loc = ` (line ${pos.line - lineOffset}, col ${pos.column})`;
|
|
199
|
-
let frame = "";
|
|
200
|
-
if (typeof pos.lineText === "string") {
|
|
201
|
-
const caret = " ".repeat(Math.max(0, pos.column ?? 0)) + "^";
|
|
202
|
-
frame = `\n ${pos.lineText}\n ${caret}`;
|
|
203
|
-
}
|
|
204
|
-
return `${message}${loc}${frame}`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export async function compilePlan(jsx: string, projectRoot?: string): Promise<CompileResult> {
|
|
208
|
-
// Strip leading pragma / block / line comments (e.g. `/** @jsxImportSource ... */`).
|
|
209
|
-
// Authored JSX sometimes carries a JSX-runtime pragma from another toolchain;
|
|
210
|
-
// Canvas injects its own imports and uses the React automatic runtime, so the
|
|
211
|
-
// pragma is unused. Left in place it sits at the top level of the wrapped
|
|
212
|
-
// fragment as plain text (JSX only treats `{/* */}` as a comment) and renders
|
|
213
|
-
// literally on the page.
|
|
214
|
-
let stripped: string;
|
|
215
|
-
do {
|
|
216
|
-
stripped = jsx;
|
|
217
|
-
jsx = jsx.replace(/^\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*)\s*/, "");
|
|
218
|
-
} while (jsx !== stripped);
|
|
219
|
-
|
|
220
|
-
// Resolve file contents at compile time
|
|
221
|
-
if (projectRoot) {
|
|
222
|
-
jsx = resolveFilePreviews(jsx, projectRoot);
|
|
223
|
-
jsx = resolveMarkdownFiles(jsx, projectRoot);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Strip template literals and strings so `export default` inside CodeBlock
|
|
227
|
-
// content (e.g. {`export default defineConfig(...)`}) doesn't false-positive.
|
|
228
|
-
const strippedJsx = jsx
|
|
229
|
-
.replace(/\{`[^`]*`\}/gs, "") // {`...`} template expressions
|
|
230
|
-
.replace(/`[^`]*`/gs, "") // standalone template literals
|
|
231
|
-
.replace(/"[^"]*"/g, "") // double-quoted strings
|
|
232
|
-
.replace(/'[^']*'/g, ""); // single-quoted strings
|
|
233
|
-
const hasDefaultExport = /export\s+default\b/.test(strippedJsx);
|
|
234
|
-
|
|
235
|
-
const source = hasDefaultExport
|
|
236
|
-
? `${COMPONENT_IMPORTS}\n${jsx}`
|
|
237
|
-
: `${COMPONENT_IMPORTS}\nexport default function Plan() {\n return (<>${jsx}</>);\n}\n`;
|
|
238
|
-
|
|
239
|
-
// Offset: COMPONENT_IMPORTS lines + wrapper lines (export default function
|
|
240
|
-
// Plan() + return). Used to map Bun's line numbers back to the authored file.
|
|
241
|
-
const wrapperLines = hasDefaultExport ? 1 : 2;
|
|
242
|
-
const lineOffset = COMPONENT_IMPORTS_LINES + wrapperLines;
|
|
243
|
-
|
|
244
|
-
// Pre-validate syntax with Bun.Transpiler — gives precise error positions,
|
|
245
|
-
// unlike Bun.build() which throws opaque "Bundle failed" for syntax errors.
|
|
246
|
-
try {
|
|
247
|
-
const transpiler = new Bun.Transpiler({ loader: "jsx" });
|
|
248
|
-
transpiler.transformSync(source);
|
|
249
|
-
} catch (syntaxError: any) {
|
|
250
|
-
return { ok: false, error: formatBuildError(syntaxError, lineOffset) };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const tmpFile = join(COMPILE_TEMP_DIR, `plan-${randomUUID()}.jsx`);
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
writeFileSync(tmpFile, source);
|
|
257
|
-
|
|
258
|
-
try {
|
|
259
|
-
const result = await Bun.build({
|
|
260
|
-
entrypoints: [tmpFile],
|
|
261
|
-
format: "esm",
|
|
262
|
-
external: ["react", "react-dom", "#canvas/components", "#canvas/runtime"],
|
|
263
|
-
jsx: { runtime: "automatic", importSource: "react" },
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
if (!result.success) {
|
|
267
|
-
const errorLogs = result.logs.filter((l) => l.level === "error");
|
|
268
|
-
const errors = errorLogs
|
|
269
|
-
.map((l) => formatBuildError(l, lineOffset))
|
|
270
|
-
.join("\n");
|
|
271
|
-
return { ok: false, error: errors || "Bundle failed" };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const js = await result.outputs[0].text();
|
|
275
|
-
const validation = await validateCompiledPlan(js);
|
|
276
|
-
if (!validation.ok) {
|
|
277
|
-
return { ok: false, error: `Runtime error: ${validation.error}` };
|
|
278
|
-
}
|
|
279
|
-
return { ok: true, js };
|
|
280
|
-
} catch (buildError: any) {
|
|
281
|
-
// A real syntax error surfaces as an AggregateError carrying BuildMessages
|
|
282
|
-
// (with positions) — format it precisely. The opaque "Unknown Error, TODO"
|
|
283
|
-
// corruption case (internal bundler state gets corrupted in long-running
|
|
284
|
-
// processes) has no sub-errors; pass it through so the CLI restarts the
|
|
285
|
-
// daemon and retries.
|
|
286
|
-
const hasDetail =
|
|
287
|
-
buildError?.position ||
|
|
288
|
-
(Array.isArray(buildError?.errors) && buildError.errors.length > 0);
|
|
289
|
-
if (hasDetail) {
|
|
290
|
-
return { ok: false, error: formatBuildError(buildError, lineOffset) };
|
|
291
|
-
}
|
|
292
|
-
return { ok: false, error: buildError?.message || "Unknown Error" };
|
|
293
|
-
}
|
|
294
|
-
} catch (e: any) {
|
|
295
|
-
return { ok: false, error: e.message };
|
|
296
|
-
} finally {
|
|
297
|
-
try { unlinkSync(tmpFile); } catch {}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Find <Markdown file="..." /> tags and inject __content with the file contents
|
|
303
|
-
* resolved at compile time.
|
|
304
|
-
*/
|
|
305
|
-
function resolveMarkdownFiles(jsx: string, projectRoot: string): string {
|
|
306
|
-
return jsx.replace(
|
|
307
|
-
/<Markdown\b([^>]*?)\/>/g,
|
|
308
|
-
(match, attrs: string) => {
|
|
309
|
-
const fileMatch = attrs.match(/file=["']([^"']+)["']/);
|
|
310
|
-
if (!fileMatch) return match;
|
|
311
|
-
|
|
312
|
-
const filePath = fileMatch[1];
|
|
313
|
-
const absPath = join(projectRoot, filePath);
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
const content = readFileSync(absPath, "utf-8");
|
|
317
|
-
const escaped = JSON.stringify(content);
|
|
318
|
-
return `<Markdown${attrs} __content={${escaped}} />`;
|
|
319
|
-
} catch {
|
|
320
|
-
return match;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Find <FilePreview path="..." /> tags and inject __content with the file contents
|
|
328
|
-
* resolved at compile time so the browser doesn't need to fetch.
|
|
329
|
-
*/
|
|
330
|
-
function resolveFilePreviews(jsx: string, projectRoot: string): string {
|
|
331
|
-
// Match <FilePreview ... /> (self-closing)
|
|
332
|
-
return jsx.replace(
|
|
333
|
-
/<FilePreview\b([^>]*?)\/>/g,
|
|
334
|
-
(match, attrs: string) => {
|
|
335
|
-
const pathMatch = attrs.match(/path=["']([^"']+)["']/);
|
|
336
|
-
if (!pathMatch) return match;
|
|
337
|
-
|
|
338
|
-
const filePath = pathMatch[1];
|
|
339
|
-
const absPath = join(projectRoot, filePath);
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
let content = readFileSync(absPath, "utf-8");
|
|
343
|
-
|
|
344
|
-
// Apply lines filter if present
|
|
345
|
-
const linesMatch = attrs.match(/lines=\{?\[(\d+)\s*,\s*(\d+)\]\}?/);
|
|
346
|
-
if (linesMatch) {
|
|
347
|
-
const start = parseInt(linesMatch[1], 10);
|
|
348
|
-
const end = parseInt(linesMatch[2], 10);
|
|
349
|
-
content = content.split("\n").slice(start - 1, end).join("\n");
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Escape for embedding in JSX string
|
|
353
|
-
const escaped = JSON.stringify(content);
|
|
354
|
-
return `<FilePreview${attrs} __content={${escaped}} />`;
|
|
355
|
-
} catch {
|
|
356
|
-
return match;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
);
|
|
360
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export function jsonResponse(data: unknown, status = 200): Response {
|
|
2
|
-
return new Response(JSON.stringify(data), {
|
|
3
|
-
status,
|
|
4
|
-
headers: { "Content-Type": "application/json" },
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function corsHeaders(port: number): Record<string, string> {
|
|
9
|
-
return {
|
|
10
|
-
"Access-Control-Allow-Origin": "http://localhost:" + port,
|
|
11
|
-
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
12
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
13
|
-
};
|
|
14
|
-
}
|
package/daemon/src/router.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export type RouteHandler = (req: Request, url: URL, match: URLPatternResult) => Response | Promise<Response>;
|
|
2
|
-
|
|
3
|
-
export interface Route {
|
|
4
|
-
method: string;
|
|
5
|
-
pattern: URLPattern;
|
|
6
|
-
handler: RouteHandler;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function dispatch(routes: Route[], req: Request, url: URL): Response | Promise<Response> | null {
|
|
10
|
-
for (const route of routes) {
|
|
11
|
-
if (route.method !== req.method) continue;
|
|
12
|
-
const match = route.pattern.exec(url);
|
|
13
|
-
if (match) return route.handler(req, url, match);
|
|
14
|
-
}
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "fs";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import { SessionManager } from "./session";
|
|
6
|
-
|
|
7
|
-
const testDirectories: string[] = [];
|
|
8
|
-
|
|
9
|
-
function createManager(): SessionManager {
|
|
10
|
-
const directory = mkdtempSync(join(tmpdir(), "agent-canvas-secrets-"));
|
|
11
|
-
testDirectories.push(directory);
|
|
12
|
-
return new SessionManager(directory);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function createSession(manager: SessionManager, id = "runbook"): void {
|
|
16
|
-
manager.upsert(id, new Map([["runbook.jsx", "<Section title=\"Runbook\" />"]]), process.cwd());
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function readTextFiles(directory: string): string {
|
|
20
|
-
const parts: string[] = [];
|
|
21
|
-
for (const entry of readdirSync(directory)) {
|
|
22
|
-
const path = join(directory, entry);
|
|
23
|
-
if (statSync(path).isDirectory()) {
|
|
24
|
-
parts.push(readTextFiles(path));
|
|
25
|
-
} else {
|
|
26
|
-
parts.push(readFileSync(path, "utf-8"));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return parts.join("\n");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
afterEach(() => {
|
|
33
|
-
for (const directory of testDirectories.splice(0)) {
|
|
34
|
-
rmSync(directory, { recursive: true, force: true });
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe("session secret vault", () => {
|
|
39
|
-
test("resolves a deposited secret without persisting it", () => {
|
|
40
|
-
const manager = createManager();
|
|
41
|
-
createSession(manager);
|
|
42
|
-
|
|
43
|
-
expect(manager.setSecret("runbook", "service-token", "test-secret-value")).toBe(true);
|
|
44
|
-
expect(manager.hasSecret("runbook", "service-token")).toBe(true);
|
|
45
|
-
expect(manager.resolveSecrets("runbook", ["service-token"])).toEqual({
|
|
46
|
-
ok: true,
|
|
47
|
-
values: new Map([["service-token", "test-secret-value"]]),
|
|
48
|
-
});
|
|
49
|
-
expect(readTextFiles(testDirectories[0])).not.toContain("test-secret-value");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("reports every missing field without returning partial values", () => {
|
|
53
|
-
const manager = createManager();
|
|
54
|
-
createSession(manager);
|
|
55
|
-
manager.setSecret("runbook", "ready-token", "available");
|
|
56
|
-
|
|
57
|
-
expect(manager.resolveSecrets("runbook", ["ready-token", "missing-token"])).toEqual({
|
|
58
|
-
ok: false,
|
|
59
|
-
missing: ["missing-token"],
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("clears secrets when a new revision is pushed", () => {
|
|
64
|
-
const manager = createManager();
|
|
65
|
-
createSession(manager);
|
|
66
|
-
manager.setSecret("runbook", "service-token", "test-secret-value");
|
|
67
|
-
|
|
68
|
-
manager.upsert("runbook", new Map([["runbook.jsx", "<Section title=\"Updated\" />"]]), process.cwd());
|
|
69
|
-
|
|
70
|
-
expect(manager.hasSecret("runbook", "service-token")).toBe(false);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("clears one field without affecting the rest", () => {
|
|
74
|
-
const manager = createManager();
|
|
75
|
-
createSession(manager);
|
|
76
|
-
manager.setSecret("runbook", "first-token", "first");
|
|
77
|
-
manager.setSecret("runbook", "second-token", "second");
|
|
78
|
-
|
|
79
|
-
expect(manager.clearSecret("runbook", "first-token")).toBe(true);
|
|
80
|
-
expect(manager.hasSecret("runbook", "first-token")).toBe(false);
|
|
81
|
-
expect(manager.hasSecret("runbook", "second-token")).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
});
|