agent-canvas 0.2.1 → 0.7.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 +42 -17
- package/bin/agent-canvas.js +7 -7
- package/bin/agent-canvas.ts +48 -37
- package/bin/lib/commands/daemon.ts +46 -43
- package/bin/lib/commands/exec.ts +100 -100
- package/bin/lib/commands/fetch.ts +15 -15
- package/bin/lib/commands/install.ts +36 -36
- package/bin/lib/commands/instructions.ts +51 -51
- package/bin/lib/commands/push.ts +115 -97
- package/bin/lib/commands/review.ts +190 -0
- package/bin/lib/commands/watch.ts +24 -24
- package/bin/lib/config.ts +12 -12
- package/bin/lib/daemon-lifecycle.ts +47 -51
- package/bin/lib/helpers.ts +82 -76
- package/bin/lib/instructions/app-review.md +88 -0
- package/bin/lib/instructions/component-checklist.md +7 -5
- package/bin/lib/instructions/component-custom.md +20 -14
- package/bin/lib/instructions/component-diff.md +4 -3
- package/bin/lib/instructions/component-item.md +18 -8
- package/bin/lib/instructions/component-markdown.md +4 -2
- package/bin/lib/instructions/component-mermaid.md +12 -6
- package/bin/lib/instructions/component-multichoice.md +6 -2
- package/bin/lib/instructions/component-note.md +1 -1
- package/bin/lib/instructions/component-priority.md +1 -1
- package/bin/lib/instructions/component-rangeinput.md +9 -2
- package/bin/lib/instructions/component-secretinput.md +12 -11
- package/bin/lib/instructions/component-svg.md +62 -16
- package/bin/lib/instructions/component-table.md +6 -6
- package/bin/lib/instructions/component-usefeedback.md +8 -8
- package/bin/lib/instructions/component-userinput.md +6 -2
- package/bin/lib/instructions/flow-feature.md +22 -5
- package/bin/lib/instructions/flow-plan.md +5 -5
- package/bin/lib/instructions/flow-runbook.md +28 -25
- package/bin/lib/instructions/overview.md +27 -18
- package/bin/lib/instructions/theming.md +5 -0
- package/daemon/build.ts +171 -152
- package/daemon/client/App.tsx +1205 -1077
- package/daemon/client/CompareView.tsx +478 -458
- package/daemon/client/FileBrowser.tsx +241 -203
- package/daemon/client/FileViewer.tsx +220 -211
- package/daemon/client/OverviewView.tsx +283 -267
- package/daemon/client/ReviewerIdentityDialog.tsx +72 -71
- package/daemon/client/RevisionSelect.tsx +111 -95
- package/daemon/client/SessionSwitcher.tsx +81 -81
- package/daemon/client/ShareDialog.tsx +211 -212
- package/daemon/client/apiSchemas.ts +78 -0
- package/daemon/client/canvas-runtime.ts +1 -1
- package/daemon/client/clientApi.ts +194 -206
- package/daemon/client/components/SecretInput.tsx +138 -138
- package/daemon/client/components/index.ts +2 -2
- package/daemon/client/domDiff.ts +341 -333
- package/daemon/client/errorMessage.ts +3 -0
- package/daemon/client/exportMarkdown.ts +222 -201
- package/daemon/client/lcs.ts +19 -14
- package/daemon/client/shareCrypto.ts +57 -57
- package/daemon/client/unifiedDiff.ts +660 -649
- package/daemon/dist/annotations-runtime.js +1 -1
- package/daemon/dist/client.css +2 -2
- package/daemon/dist/client.js +77 -80
- package/daemon/dist/components.js +59 -59
- package/daemon/dist/preact-compat.js +1 -1
- package/daemon/dist/review.js +361 -0
- package/daemon/dist/runtime.js +1 -1
- package/daemon/langMap.ts +24 -6
- package/daemon/review-widget/anchor.ts +187 -0
- package/daemon/review-widget/client.ts +95 -0
- package/daemon/review-widget/index.tsx +36 -0
- package/daemon/review-widget/markup.ts +71 -0
- package/daemon/review-widget/screenshot.ts +91 -0
- package/daemon/review-widget/source.ts +103 -0
- package/daemon/review-widget/styles.css +359 -0
- package/daemon/review-widget/text-modules.d.ts +4 -0
- package/daemon/review-widget/tsconfig.json +16 -0
- package/daemon/review-widget/widget.tsx +436 -0
- package/daemon/src/components.ts +3 -3
- package/daemon/src/handlers/api.ts +330 -328
- package/daemon/src/handlers/files.ts +112 -106
- package/daemon/src/handlers/static.ts +47 -46
- package/daemon/src/handlers/uploads.ts +61 -56
- package/daemon/src/paths.ts +13 -10
- package/daemon/src/remote-feedback.ts +192 -192
- package/daemon/src/review/markdown.ts +52 -0
- package/daemon/src/review/model.ts +132 -0
- package/daemon/src/review/proxy.ts +177 -0
- package/daemon/src/review/server.ts +199 -0
- package/daemon/src/review/store.ts +151 -0
- package/daemon/src/server.ts +127 -86
- package/daemon/src/share.ts +163 -169
- package/daemon/src/stubFetch.ts +7 -0
- package/daemon/tsconfig.json +19 -19
- package/package.json +47 -47
- package/skills/canvas/SKILL.md +5 -1
- package/bin/lib/commands/exec.test.ts +0 -27
- package/bin/lib/helpers.test.ts +0 -79
- package/daemon/src/files-api.test.ts +0 -78
- package/daemon/src/remote-feedback.test.ts +0 -165
- package/daemon/src/secrets-api.test.ts +0 -123
- package/daemon/src/share-e2e.test.ts +0 -277
- package/daemon/src/share.test.ts +0 -289
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Interactive browser-based visual canvas for [Claude Code](https://docs.anthropic
|
|
|
5
5
|

|
|
6
6
|

|
|
7
7
|
|
|
8
|
-

|
|
9
9
|
|
|
10
10
|
## What it does
|
|
11
11
|
|
|
@@ -27,6 +27,7 @@ bunx agent-canvas install local # for current project only
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
This installs:
|
|
30
|
+
|
|
30
31
|
- The `/canvas` skill so Claude knows how to use it
|
|
31
32
|
- A `SessionStart` hook that sets up session IDs automatically
|
|
32
33
|
|
|
@@ -48,14 +49,14 @@ Claude creates a JSX document, pushes it to a local daemon, and opens it in your
|
|
|
48
49
|
|
|
49
50
|
Claude uses different canvas flows depending on the task:
|
|
50
51
|
|
|
51
|
-
| Flow
|
|
52
|
-
|
|
53
|
-
| **Feature**
|
|
54
|
-
| **Plan**
|
|
55
|
-
| **Explain**
|
|
56
|
-
| **Review**
|
|
57
|
-
| **Decision** | Choosing between options
|
|
58
|
-
| **Runbook**
|
|
52
|
+
| Flow | When | Rounds |
|
|
53
|
+
| ------------ | --------------------------------------------------- | ---------------------------------------------------------- |
|
|
54
|
+
| **Feature** | Vague requests ("add auth") | Discovery → Requirements → Plan → Implementation → Summary |
|
|
55
|
+
| **Plan** | Clear requirements | Plan → Implementation → Summary |
|
|
56
|
+
| **Explain** | "How does X work?" | Single canvas |
|
|
57
|
+
| **Review** | Code/architecture review | Findings → Fix Plan → Summary |
|
|
58
|
+
| **Decision** | Choosing between options | Single canvas with comparison |
|
|
59
|
+
| **Runbook** | User must complete manual setup or provide a secret | Instructions → secure handoff → command → result |
|
|
59
60
|
|
|
60
61
|
### Local secret handoff
|
|
61
62
|
|
|
@@ -72,27 +73,51 @@ bunx agent-canvas exec \
|
|
|
72
73
|
|
|
73
74
|
Secret resolution requires a daemon capability available to the CLI but not to canvas JavaScript. A new canvas revision, daemon shutdown, or session cleanup clears the value. The target command must not print its environment or credentials.
|
|
74
75
|
|
|
76
|
+
### Review mode
|
|
77
|
+
|
|
78
|
+
Review mode puts the same comment loop on a running web application: a prototype, a mock or any dev server. Open the
|
|
79
|
+
application through the daemon's review proxy:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bunx agent-canvas review open http://localhost:5173/ --project my-app
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The daemon serves the application on a port of its own with the widget added to every HTML page (hot reload passes
|
|
86
|
+
through) and opens it in the browser. To keep the application's own address, add the widget's tag to its page in
|
|
87
|
+
development instead: `bunx agent-canvas review snippet --project my-app`.
|
|
88
|
+
|
|
89
|
+
The widget mounts a toolbar in its own shadow root. The reviewer clicks **Comment**, picks an element or selects text,
|
|
90
|
+
and saves the comment as a note or sends it to the agent. The agent waits with `bunx agent-canvas review watch` (`--follow` keeps waiting),
|
|
91
|
+
receives each thread with its page URL, the element's role and accessible name, a CSS path, the React components that
|
|
92
|
+
own it with their source positions (resolved through the dev server's source maps), the trimmed markup and, when the
|
|
93
|
+
reviewer attaches one, a screenshot of the tab, changes the code and answers with `bunx agent-canvas review reply <thread> "<text>" --resolve`. The answer and
|
|
94
|
+
the thread's state show up next to the element.
|
|
95
|
+
|
|
96
|
+
Threads are stored per project under the daemon's data directory. The daemon accepts widget requests only from loopback
|
|
97
|
+
origins (`localhost`, `127.0.0.1`, `*.localhost`) and agent requests only from clients that send no `Origin`, so a web
|
|
98
|
+
page cannot queue work for the agent.
|
|
99
|
+
|
|
75
100
|
## Theming
|
|
76
101
|
|
|
77
102
|
Agent Canvas supports light and dark themes, automatically matching your system preference.
|
|
78
103
|
|
|
79
104
|
**Dark mode**
|
|
80
105
|
|
|
81
|
-

|
|
82
107
|
|
|
83
108
|
## Development
|
|
84
109
|
|
|
85
110
|
### Reusable packages
|
|
86
111
|
|
|
87
|
-
| Package
|
|
88
|
-
|
|
|
89
|
-
| [`@fabrika/daemon-kit`](https://github.com/contember/fabrika-platform/tree/main/packages/daemon-kit)
|
|
90
|
-
| [`@fabrika/annotations`](https://github.com/contember/fabrika-platform/tree/main/packages/annotations)
|
|
91
|
-
| [`@fabrika/canvas-kernel`](https://github.com/contember/fabrika-platform/tree/main/packages/canvas-kernel) | JSX compiler, revisions, renderer and interactive canvas components
|
|
112
|
+
| Package | Use without the Agent Canvas shell |
|
|
113
|
+
| ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
114
|
+
| [`@fabrika/daemon-kit`](https://github.com/contember/fabrika-platform/tree/main/packages/daemon-kit) | Bun daemon lifecycle, HTTP routing, WebSocket channels and CLI waiters |
|
|
115
|
+
| [`@fabrika/annotations`](https://github.com/contember/fabrika-platform/tree/main/packages/annotations) | Annotation state, editors and text/block/region locators |
|
|
116
|
+
| [`@fabrika/canvas-kernel`](https://github.com/contember/fabrika-platform/tree/main/packages/canvas-kernel) | JSX compiler, revisions, renderer and interactive canvas components |
|
|
92
117
|
|
|
93
118
|
The kernel depends on the other two; neither depends on the kernel. All ship as
|
|
94
|
-
TypeScript sources
|
|
95
|
-
|
|
119
|
+
TypeScript sources and are released together with `agent-canvas` from the
|
|
120
|
+
[`fabrika-platform`](https://github.com/contember/fabrika-platform) repository.
|
|
96
121
|
|
|
97
122
|
### Local commands
|
|
98
123
|
|
package/bin/agent-canvas.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { execFileSync } = require(
|
|
4
|
-
const { join } = require(
|
|
3
|
+
const { execFileSync } = require('child_process')
|
|
4
|
+
const { join } = require('path')
|
|
5
5
|
|
|
6
|
-
const script = join(__dirname,
|
|
6
|
+
const script = join(__dirname, 'agent-canvas.ts')
|
|
7
7
|
|
|
8
8
|
try {
|
|
9
|
-
|
|
9
|
+
execFileSync('bun', [script, ...process.argv.slice(2)], { stdio: 'inherit' })
|
|
10
10
|
} catch (e) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
14
|
}
|
package/bin/agent-canvas.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
3
|
+
import { handleDaemon } from './lib/commands/daemon.ts'
|
|
4
|
+
import { handleExec } from './lib/commands/exec.ts'
|
|
5
|
+
import { handleFetch } from './lib/commands/fetch.ts'
|
|
6
|
+
import { handleInstall } from './lib/commands/install.ts'
|
|
7
|
+
import { handleInstructions } from './lib/commands/instructions.ts'
|
|
8
|
+
import { handlePush } from './lib/commands/push.ts'
|
|
9
|
+
import { handleReview } from './lib/commands/review.ts'
|
|
10
|
+
import { handleWatch } from './lib/commands/watch.ts'
|
|
11
|
+
import { VERSION } from './lib/config.ts'
|
|
11
12
|
|
|
12
13
|
function printUsage() {
|
|
13
|
-
|
|
14
|
+
console.log(`agent-canvas v${VERSION} — Interactive visual canvas for Claude Code
|
|
14
15
|
|
|
15
16
|
Commands:
|
|
16
17
|
agent-canvas install [local|global] Install skill for Claude Code
|
|
@@ -18,6 +19,7 @@ Commands:
|
|
|
18
19
|
agent-canvas push <directory> Push canvases, open browser
|
|
19
20
|
agent-canvas fetch [--session <id>] Check for feedback (returns immediately)
|
|
20
21
|
agent-canvas watch [--session <id>] Wait for user feedback (blocks until submitted)
|
|
22
|
+
agent-canvas review <command> Comment on a running application (see: agent-canvas review --help)
|
|
21
23
|
agent-canvas exec --secret ENV=id -- <command>
|
|
22
24
|
Run a command with an in-memory secret
|
|
23
25
|
agent-canvas daemon status Show daemon status
|
|
@@ -26,41 +28,50 @@ Commands:
|
|
|
26
28
|
agent-canvas daemon restart Restart the daemon
|
|
27
29
|
|
|
28
30
|
Environment:
|
|
29
|
-
CANVAS_PORT Daemon port (default: 19400)`)
|
|
31
|
+
CANVAS_PORT Daemon port (default: 19400)`)
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
async function main() {
|
|
33
|
-
|
|
35
|
+
const args = process.argv.slice(2)
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if (args[0] === '--version' || args[0] === '-v') {
|
|
38
|
+
console.log(VERSION)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
43
|
+
printUsage()
|
|
44
|
+
process.exit(args.length === 0 ? 1 : 0)
|
|
45
|
+
}
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
const command = args[0]
|
|
48
|
+
const rest = args.slice(1)
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
switch (command) {
|
|
51
|
+
case 'push':
|
|
52
|
+
return handlePush(rest)
|
|
53
|
+
case 'fetch':
|
|
54
|
+
return handleFetch(rest)
|
|
55
|
+
case 'watch':
|
|
56
|
+
return handleWatch(rest)
|
|
57
|
+
case 'review':
|
|
58
|
+
return handleReview(rest)
|
|
59
|
+
case 'exec':
|
|
60
|
+
return handleExec(rest)
|
|
61
|
+
case 'instructions':
|
|
62
|
+
return handleInstructions(rest)
|
|
63
|
+
case 'install':
|
|
64
|
+
return handleInstall(rest)
|
|
65
|
+
case 'daemon':
|
|
66
|
+
return handleDaemon(rest)
|
|
67
|
+
default:
|
|
68
|
+
console.error(`Unknown command: ${command}`)
|
|
69
|
+
printUsage()
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
main().catch((e) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
})
|
|
75
|
+
console.error(e)
|
|
76
|
+
process.exit(1)
|
|
77
|
+
})
|
|
@@ -1,48 +1,51 @@
|
|
|
1
|
-
import { parseArgs } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { parseArgs } from 'util'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { BASE_URL, DAEMON_PORT } from '../config.ts'
|
|
4
|
+
import { isDaemonRunning, startDaemon, stopDaemon } from '../daemon-lifecycle.ts'
|
|
5
|
+
|
|
6
|
+
const healthResponseSchema = z.object({ sessions: z.array(z.string()) })
|
|
4
7
|
|
|
5
8
|
export async function handleDaemon(args: string[]) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
const { positionals } = parseArgs({
|
|
10
|
+
args,
|
|
11
|
+
allowPositionals: true,
|
|
12
|
+
})
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
const subcommand = positionals[0]
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
16
|
+
switch (subcommand) {
|
|
17
|
+
case 'status': {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`${BASE_URL}/health`, { signal: AbortSignal.timeout(2000) })
|
|
20
|
+
const data = healthResponseSchema.parse(await res.json())
|
|
21
|
+
console.log(`Daemon: running on port ${DAEMON_PORT}`)
|
|
22
|
+
console.log(`Sessions: ${data.sessions.length > 0 ? data.sessions.join(', ') : 'none'}`)
|
|
23
|
+
} catch {
|
|
24
|
+
console.log('Daemon: not running')
|
|
25
|
+
}
|
|
26
|
+
break
|
|
27
|
+
}
|
|
28
|
+
case 'stop': {
|
|
29
|
+
await stopDaemon()
|
|
30
|
+
break
|
|
31
|
+
}
|
|
32
|
+
case 'start': {
|
|
33
|
+
if (await isDaemonRunning()) {
|
|
34
|
+
console.log('Daemon is already running.')
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
await startDaemon()
|
|
38
|
+
break
|
|
39
|
+
}
|
|
40
|
+
case 'restart': {
|
|
41
|
+
// stopDaemon() already waits for the daemon to go down.
|
|
42
|
+
await stopDaemon()
|
|
43
|
+
await startDaemon()
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
default: {
|
|
47
|
+
console.error('Usage: agent-canvas daemon [status|stop|start|restart]')
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
48
51
|
}
|
package/bin/lib/commands/exec.ts
CHANGED
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from
|
|
2
|
-
import { resolve } from
|
|
3
|
-
import { parseArgs } from
|
|
4
|
-
import { BASE_URL, CLI_AUTH_FILE } from
|
|
5
|
-
import { ensureDaemon } from
|
|
6
|
-
import { getSessionId } from
|
|
1
|
+
import { existsSync, readFileSync } from 'fs'
|
|
2
|
+
import { resolve } from 'path'
|
|
3
|
+
import { parseArgs } from 'util'
|
|
4
|
+
import { BASE_URL, CLI_AUTH_FILE } from '../config.ts'
|
|
5
|
+
import { ensureDaemon } from '../daemon-lifecycle.ts'
|
|
6
|
+
import { getSessionId } from '../helpers.ts'
|
|
7
7
|
|
|
8
|
-
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]
|
|
9
|
-
const FIELD_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}
|
|
8
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
9
|
+
const FIELD_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
10
10
|
|
|
11
11
|
export interface SecretAssignment {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
envName: string
|
|
13
|
+
fieldId: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
-
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function parseSecretAssignments(rawAssignments: string[]): SecretAssignment[] {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
if (rawAssignments.length === 0) {
|
|
22
|
+
throw new Error('At least one --secret ENV_NAME=field-id mapping is required')
|
|
23
|
+
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
25
|
+
const assignments: SecretAssignment[] = []
|
|
26
|
+
const environmentNames = new Set<string>()
|
|
27
|
+
for (const raw of rawAssignments) {
|
|
28
|
+
const separator = raw.indexOf('=')
|
|
29
|
+
const envName = separator === -1 ? '' : raw.slice(0, separator)
|
|
30
|
+
const fieldId = separator === -1 ? '' : raw.slice(separator + 1)
|
|
31
|
+
if (!ENV_NAME_RE.test(envName)) {
|
|
32
|
+
throw new Error(`Invalid environment variable name in --secret mapping: ${raw}`)
|
|
33
|
+
}
|
|
34
|
+
if (!FIELD_ID_RE.test(fieldId)) {
|
|
35
|
+
throw new Error(`Invalid secret field ID in --secret mapping: ${raw}`)
|
|
36
|
+
}
|
|
37
|
+
if (environmentNames.has(envName)) {
|
|
38
|
+
throw new Error(`Duplicate environment variable mapping: ${envName}`)
|
|
39
|
+
}
|
|
40
|
+
environmentNames.add(envName)
|
|
41
|
+
assignments.push({ envName, fieldId })
|
|
42
|
+
}
|
|
43
|
+
return assignments
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async function resolveSecretValues(sessionId: string, fieldIds: string[]): Promise<Map<string, string>> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
47
|
+
if (!existsSync(CLI_AUTH_FILE)) throw new Error('Canvas daemon CLI capability is unavailable')
|
|
48
|
+
const cliAuthToken = readFileSync(CLI_AUTH_FILE, 'utf-8').trim()
|
|
49
|
+
const response = await fetch(`${BASE_URL}/api/session/${encodeURIComponent(sessionId)}/secrets/resolve`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'X-Agent-Canvas-CLI-Token': cliAuthToken,
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({ fields: fieldIds }),
|
|
56
|
+
})
|
|
57
|
+
const raw: unknown = await response.json().catch(() => null)
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const message = isRecord(raw) && typeof raw.error === 'string'
|
|
60
|
+
? raw.error
|
|
61
|
+
: `Secret API request failed with status ${response.status}`
|
|
62
|
+
const missing = isRecord(raw) && Array.isArray(raw.missing)
|
|
63
|
+
? raw.missing.filter((field): field is string => typeof field === 'string')
|
|
64
|
+
: []
|
|
65
|
+
throw new Error(missing.length > 0 ? `${message}: ${missing.join(', ')}` : message)
|
|
66
|
+
}
|
|
67
|
+
if (!isRecord(raw) || !Array.isArray(raw.values)) {
|
|
68
|
+
throw new Error('Secret API returned an invalid response')
|
|
69
|
+
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
const values = new Map<string, string>()
|
|
72
|
+
for (const entry of raw.values) {
|
|
73
|
+
if (!isRecord(entry) || typeof entry.id !== 'string' || typeof entry.value !== 'string') {
|
|
74
|
+
throw new Error('Secret API returned an invalid value entry')
|
|
75
|
+
}
|
|
76
|
+
values.set(entry.id, entry.value)
|
|
77
|
+
}
|
|
78
|
+
return values
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function executableExists(command: string): boolean {
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
if (command.includes('/')) return existsSync(resolve(command))
|
|
83
|
+
return Bun.spawnSync(['which', command], { stdout: 'ignore', stderr: 'ignore' }).exitCode === 0
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export async function spawnWithSecretEnvironment(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
commandArgs: string[],
|
|
88
|
+
assignments: SecretAssignment[],
|
|
89
|
+
values: Map<string, string>,
|
|
90
90
|
): Promise<number> {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
const command = commandArgs[0]
|
|
92
|
+
if (!command) throw new Error('No command specified after --')
|
|
93
|
+
if (!executableExists(command)) throw new Error(`Executable not found: ${command}`)
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
const environment = { ...process.env }
|
|
96
|
+
for (const assignment of assignments) {
|
|
97
|
+
const value = values.get(assignment.fieldId)
|
|
98
|
+
if (value === undefined) throw new Error(`Secret is not ready: ${assignment.fieldId}`)
|
|
99
|
+
environment[assignment.envName] = value
|
|
100
|
+
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
const child = Bun.spawn(commandArgs, {
|
|
103
|
+
env: environment,
|
|
104
|
+
stdin: 'inherit',
|
|
105
|
+
stdout: 'inherit',
|
|
106
|
+
stderr: 'inherit',
|
|
107
|
+
})
|
|
108
|
+
return child.exited
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
export async function handleExec(args: string[]): Promise<void> {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
112
|
+
const { values, positionals } = parseArgs({
|
|
113
|
+
args,
|
|
114
|
+
options: {
|
|
115
|
+
session: { type: 'string' },
|
|
116
|
+
secret: { type: 'string', multiple: true },
|
|
117
|
+
},
|
|
118
|
+
allowPositionals: true,
|
|
119
|
+
})
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
const assignments = parseSecretAssignments(values.secret ?? [])
|
|
122
|
+
if (positionals.length === 0) throw new Error('No command specified. Put the command after --')
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
const sessionId = getSessionId(values.session)
|
|
125
|
+
await ensureDaemon()
|
|
126
|
+
const secretValues = await resolveSecretValues(sessionId, assignments.map((assignment) => assignment.fieldId))
|
|
127
|
+
const exitCode = await spawnWithSecretEnvironment(positionals, assignments, secretValues)
|
|
128
|
+
process.exit(exitCode)
|
|
129
129
|
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { parseArgs } from
|
|
2
|
-
import { ensureDaemon } from
|
|
3
|
-
import {
|
|
1
|
+
import { parseArgs } from 'util'
|
|
2
|
+
import { ensureDaemon } from '../daemon-lifecycle.ts'
|
|
3
|
+
import { consumeFeedback, getSessionId } from '../helpers.ts'
|
|
4
4
|
|
|
5
5
|
export async function handleFetch(args: string[]) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
const { values } = parseArgs({
|
|
7
|
+
args,
|
|
8
|
+
options: {
|
|
9
|
+
session: { type: 'string' },
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const sessionId = getSessionId(values.session)
|
|
14
|
+
await ensureDaemon()
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const feedback = await consumeFeedback(sessionId)
|
|
17
|
+
if (feedback) {
|
|
18
|
+
process.stdout.write(feedback)
|
|
19
|
+
}
|
|
20
20
|
}
|