@rwoll/canvas-kit 0.0.0-placeholder → 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/LICENSE +21 -0
- package/README.md +481 -1
- package/cli/canvas-kit.mjs +148 -0
- package/cli/lib.mjs +697 -0
- package/cli/release.json +4 -0
- package/dist/client.d.ts +72 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +156 -0
- package/dist/client.js.map +1 -0
- package/dist/define.d.ts +249 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +197 -0
- package/dist/define.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/errors.js.map +1 -0
- package/dist/extension.d.ts +25 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +70 -0
- package/dist/extension.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs.d.ts +56 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +33 -0
- package/dist/jobs.js.map +1 -0
- package/dist/playwright.d.ts +79 -0
- package/dist/playwright.d.ts.map +1 -0
- package/dist/playwright.js +124 -0
- package/dist/playwright.js.map +1 -0
- package/dist/react.d.ts +55 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +72 -0
- package/dist/react.js.map +1 -0
- package/dist/server/headless.d.ts +20 -0
- package/dist/server/headless.d.ts.map +1 -0
- package/dist/server/headless.js +35 -0
- package/dist/server/headless.js.map +1 -0
- package/dist/server/host.d.ts +146 -0
- package/dist/server/host.d.ts.map +1 -0
- package/dist/server/host.js +1078 -0
- package/dist/server/host.js.map +1 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +5 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/release.d.ts +30 -0
- package/dist/server/release.d.ts.map +1 -0
- package/dist/server/release.js +71 -0
- package/dist/server/release.js.map +1 -0
- package/dist/server/storage.d.ts +63 -0
- package/dist/server/storage.d.ts.map +1 -0
- package/dist/server/storage.js +253 -0
- package/dist/server/storage.js.map +1 -0
- package/dist/testing.d.ts +49 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +42 -0
- package/dist/testing.js.map +1 -0
- package/dist/vite.d.ts +15 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +213 -0
- package/dist/vite.js.map +1 -0
- package/package.json +111 -5
- package/styles.css +122 -0
- package/templates/canvas/README.md +19 -0
- package/templates/canvas/e2e/canvas.spec.ts +15 -0
- package/templates/canvas/index.html +13 -0
- package/templates/canvas/package.json +29 -0
- package/templates/canvas/playwright.config.ts +5 -0
- package/templates/canvas/server/canvas.test.ts +22 -0
- package/templates/canvas/server/canvas.ts +27 -0
- package/templates/canvas/src/App.css +21 -0
- package/templates/canvas/src/App.tsx +31 -0
- package/templates/canvas/src/index.css +3 -0
- package/templates/canvas/src/main.tsx +11 -0
- package/templates/canvas/tsconfig.app.json +1 -0
- package/templates/canvas/tsconfig.json +7 -0
- package/templates/canvas/tsconfig.node.json +1 -0
- package/templates/canvas/vite.config.ts +7 -0
- package/templates/ui/README.md +18 -0
- package/templates/ui/package.json +22 -0
- package/templates/ui/src/Checklist.tsx +42 -0
- package/templates/ui/src/Header.tsx +17 -0
- package/templates/ui/src/Markdown.tsx +140 -0
- package/templates/ui/src/TextButton.tsx +6 -0
- package/templates/ui/src/index.ts +4 -0
- package/templates/ui/theme.css +143 -0
- package/templates/ui/tsconfig.json +18 -0
- package/tsconfig/canvas.json +27 -0
- package/tsconfig/node.json +24 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ross A. Wollman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,483 @@
|
|
|
1
1
|
# @rwoll/canvas-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build Copilot canvas extensions with TypeScript and React. A canvas is an
|
|
4
|
+
interactive panel that the agent opens with `open_canvas` and drives with
|
|
5
|
+
`invoke_canvas_action`. A canvas package declares its state and actions once, and
|
|
6
|
+
the kit supplies everything else.
|
|
7
|
+
|
|
8
|
+
## Quickstart
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm create @rwoll/canvas my-marketplace -- --repository me/my-marketplace --canvas release-board
|
|
12
|
+
cd my-marketplace
|
|
13
|
+
pnpm install
|
|
14
|
+
pnpm --filter release-board dev # the canvas in a browser, with live actions
|
|
15
|
+
pnpm check # build, typecheck, unit tests
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Push it to GitHub and every push to `main` publishes the marketplace to the
|
|
19
|
+
`stable` branch. Install it with
|
|
20
|
+
`copilot plugin marketplace add me/my-marketplace#stable`.
|
|
21
|
+
|
|
22
|
+
Add more canvases with `pnpm new-canvas <name>` (`canvas-kit new`). You edit two
|
|
23
|
+
files per canvas: `server/canvas.ts` for state and actions, and `src/App.tsx` for
|
|
24
|
+
the UI.
|
|
25
|
+
|
|
26
|
+
## What the kit provides
|
|
27
|
+
|
|
28
|
+
- **Copilot SDK registration**: `joinSession`/`createCanvas` wiring, a built-in
|
|
29
|
+
`get_state` action, JSON Schemas generated from Zod, and `CanvasError` mapping.
|
|
30
|
+
- **Loopback server**: one `127.0.0.1` server per extension on an ephemeral port,
|
|
31
|
+
stopped when the last instance closes. Pages get a per-instance token embedded in
|
|
32
|
+
the HTML (never the URL), and the server checks `Host`, `Origin`, and the token.
|
|
33
|
+
- **Live state**: every `setState` is validated and pushed to open pages over
|
|
34
|
+
server-sent events. Actions on one document run one at a time; if an action
|
|
35
|
+
throws or its state cannot be saved, the state rolls back.
|
|
36
|
+
- **Persistence (optional)**: atomic, validated JSON files under
|
|
37
|
+
`<copilot-home>/extension-data/<canvas-id>/`, keyed by a domain identity. They
|
|
38
|
+
outlive the agent, and every chat that opens the same key shares one document:
|
|
39
|
+
changes apply one at a time under a cross-process lock, and open pages in other
|
|
40
|
+
chats update live.
|
|
41
|
+
- **Scheduled jobs (optional)**: `jobs` run on an interval while a chat has the
|
|
42
|
+
canvas open, once per interval across chats, and `canvas-kit run` runs them with
|
|
43
|
+
no chat open, from launchd or cron. Users and the agent can pause, retime, run,
|
|
44
|
+
and add jobs when the canvas allows it.
|
|
45
|
+
- **Build**: one `vite build` writes an installable plugin to `dist/plugin/`:
|
|
46
|
+
the React UI, a single bundled `extension.mjs` (dependencies included; only the
|
|
47
|
+
host-provided `@github/copilot-sdk` and Node.js built-ins stay external), the
|
|
48
|
+
plugin manifest generated from `package.json`, and `canvas.mjs`, the definition
|
|
49
|
+
without the SDK, for `canvas-kit run`.
|
|
50
|
+
- **Dev server**: `vite` serves the UI with the canvas definition running
|
|
51
|
+
in-process, so actions and live updates work in a normal browser. To preview
|
|
52
|
+
what only the agent can do, such as a tour, run an agent action with a
|
|
53
|
+
same-origin `POST /__canvas/dev/agent/<action>` and a JSON body. This route
|
|
54
|
+
exists only in the dev server.
|
|
55
|
+
- **Version footer**: every page has a small footer pinned to the bottom of the
|
|
56
|
+
viewport, such as
|
|
57
|
+
`changeset-review v0.3.0 · Published Oct 22, 2026, 7:41 PM`. The version comes from
|
|
58
|
+
`package.json`, and the publish time and source commit (in the tooltip) come from
|
|
59
|
+
the `release.json` stamp that `canvas-kit release` writes. Unpublished builds say
|
|
60
|
+
`Local build, not published`; the dev server says `Dev server`.
|
|
61
|
+
- **Tests**: `createTestCanvas` for unit tests and `canvasTest` fixtures for
|
|
62
|
+
Playwright browser tests against the built canvas.
|
|
63
|
+
- **Marketplace tooling**: the `canvas-kit` command scaffolds canvases, assembles a
|
|
64
|
+
plugin marketplace, and publishes it to a branch. A reusable GitHub Actions
|
|
65
|
+
workflow runs it on every push.
|
|
66
|
+
|
|
67
|
+
## Canvas package layout
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
canvases/<name>/
|
|
71
|
+
package.json name, version, and description become the plugin manifest
|
|
72
|
+
index.html Vite entry
|
|
73
|
+
vite.config.ts plugins: [canvasKit()]
|
|
74
|
+
server/canvas.ts export default defineCanvas({...}) (Node.js)
|
|
75
|
+
server/*.test.ts unit tests (Vitest)
|
|
76
|
+
src/ React UI; import only types from server/
|
|
77
|
+
e2e/*.spec.ts browser tests (Playwright)
|
|
78
|
+
playwright.config.ts export default defineCanvasConfig()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The folder name is the plugin name and extension folder name.
|
|
82
|
+
|
|
83
|
+
## Defining a canvas
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { defineCanvas, z } from '@rwoll/canvas-kit'
|
|
87
|
+
|
|
88
|
+
export default defineCanvas({
|
|
89
|
+
id: 'octo-release-board', // stable, distinctive, kebab-case
|
|
90
|
+
title: 'Release board',
|
|
91
|
+
description: 'Track release blockers for a repository.',
|
|
92
|
+
state: z.object({ repo: z.string(), blockers: z.array(z.string()) }),
|
|
93
|
+
input: z.object({ repo: z.string() }), // optional open_canvas input
|
|
94
|
+
init: ({ input }) => ({ repo: input.repo, blockers: [] }),
|
|
95
|
+
persist: { key: ({ input }) => input.repo }, // optional private persistence
|
|
96
|
+
actions: ({ action }) => ({
|
|
97
|
+
add_blocker: action({
|
|
98
|
+
description: 'Add a release blocker.',
|
|
99
|
+
input: z.object({ text: z.string().min(1).max(500) }),
|
|
100
|
+
run: (ctx, { text }) => ctx.setState(s => ({ ...s, blockers: [...s.blockers, text] })),
|
|
101
|
+
}),
|
|
102
|
+
clear: action({ description: 'Clear all blockers.', expose: 'agent', run: ctx => ctx.setState(s => ({ ...s, blockers: [] })) }),
|
|
103
|
+
}),
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `expose` is `'both'` (default), `'agent'`, or `'ui'`.
|
|
108
|
+
- `reopen({ instanceId, input, state })` runs when an open instance or a saved
|
|
109
|
+
document is opened again; return new state, or nothing to keep it. Opening an
|
|
110
|
+
instance with input that maps to a different `persist.key` switches it to that
|
|
111
|
+
document.
|
|
112
|
+
- `ctx.source` tells an action whether the agent or the UI called it; `ctx.log()`
|
|
113
|
+
writes to the session log. Never use `console.log` in extension code.
|
|
114
|
+
- `ctx.sendToAgent({ prompt, displayPrompt?, mode? })` messages the agent, for
|
|
115
|
+
example when the user submits feedback from the canvas. Keep the prompt short and
|
|
116
|
+
put bulky details in a file under `ctx.dataDir`, the canvas's private folder, so
|
|
117
|
+
the agent reads them only when needed. In the dev server, messages are printed to
|
|
118
|
+
the Vite log; in tests they are recorded in `harness.sentToAgent`.
|
|
119
|
+
- Throw `new CanvasKitError(code, message)` for expected failures.
|
|
120
|
+
- `get_state` is added automatically unless you define your own.
|
|
121
|
+
- Set `versionFooter: false` to hide the version footer. The same information is
|
|
122
|
+
available to the UI as `getCanvasClient().release` if you render your own.
|
|
123
|
+
- Pages get a strict Content-Security-Policy: `default-src 'none'`, with scripts and
|
|
124
|
+
connections limited to the canvas's own origin. A canvas that must reach a
|
|
125
|
+
local service can add `scheme://host[:port]` sources to `connect-src` with
|
|
126
|
+
`csp: { connectSrc: ['ws://127.0.0.1:*'] }`. The sources apply to that canvas only;
|
|
127
|
+
keywords, paths, and bare wildcards are rejected. The dev server sets no CSP, so
|
|
128
|
+
check such a canvas in a built plugin too.
|
|
129
|
+
- When `persist` is set, the saved file carries a format marker and version. A
|
|
130
|
+
corrupt file or one with a different `persist.version` is reported, never
|
|
131
|
+
overwritten. Bump the version when the state shape changes incompatibly.
|
|
132
|
+
- `ctx.now()` is the current time in milliseconds. Use it instead of `Date.now()`
|
|
133
|
+
so tests can control the clock.
|
|
134
|
+
|
|
135
|
+
## Shared documents and scheduled jobs
|
|
136
|
+
|
|
137
|
+
With `persist`, a document belongs to its key, not to a chat. It stays on disk
|
|
138
|
+
after the agent and the session end, and every chat that opens the same key works
|
|
139
|
+
on the same document:
|
|
140
|
+
|
|
141
|
+
- Each action, reopen, and job takes a lock on the document's file, reloads the
|
|
142
|
+
latest saved revision, runs, and saves the next revision. Changes from different
|
|
143
|
+
chats apply one at a time, so none is lost.
|
|
144
|
+
- Pages in every chat with the document open update live when another chat
|
|
145
|
+
changes it. The host watches the saved files, and also checks them every 2
|
|
146
|
+
seconds in case the operating system drops a change notification.
|
|
147
|
+
- The first open creates the document with `init()`, under the same lock, so
|
|
148
|
+
chats that open a new key at the same time start from the same state.
|
|
149
|
+
- A lock held by a process that exited is taken over at once; one that stops
|
|
150
|
+
being refreshed for 15 seconds is taken over as stale. A change that waits more
|
|
151
|
+
than 30 seconds for the lock fails with `busy`.
|
|
152
|
+
|
|
153
|
+
Jobs keep a document up to date on a schedule:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
export default defineCanvas({
|
|
157
|
+
// ...
|
|
158
|
+
persist: { key: ({ input }) => input.repo },
|
|
159
|
+
jobs: ({ job }) => ({
|
|
160
|
+
prune_triaged: job({
|
|
161
|
+
every: '1h', // 30s, 15m, 6h, 1d, or milliseconds
|
|
162
|
+
description: 'Remove tests triaged more than a week ago.',
|
|
163
|
+
run: (ctx) => {
|
|
164
|
+
const cutoff = ctx.now() - 7 * 24 * 60 * 60 * 1000
|
|
165
|
+
ctx.setState((s) => ({ ...s, tests: s.tests.filter((t) => !t.triagedAt || Date.parse(t.triagedAt) > cutoff) }))
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
nudge: job({
|
|
169
|
+
every: '1d',
|
|
170
|
+
run: async (ctx) => {
|
|
171
|
+
if (ctx.headless) return // no agent under `canvas-kit run`
|
|
172
|
+
await ctx.sendToAgent({ prompt: `${ctx.state.tests.length} flaky tests are open in ${ctx.key}` })
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
}),
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- While any chat has the canvas open, its due jobs run for each open document,
|
|
180
|
+
soon after opening and then on a timer. The last run time is saved in the
|
|
181
|
+
document and checked under the lock, so with several chats open a job still runs
|
|
182
|
+
once per interval, and an overdue job runs once to catch up.
|
|
183
|
+
- `ctx` has the state, `setState`, `sendToAgent`, `log`, `dataDir`, `now()`, the
|
|
184
|
+
document `key`, and `lastRunAt`. A job that throws has its changes rolled back
|
|
185
|
+
and its error saved with the run time; it runs again at the next interval.
|
|
186
|
+
- A job holds the document's lock while it runs, so actions in every chat wait
|
|
187
|
+
for it. Keep jobs short: fetch slow data before changing state, or do it in
|
|
188
|
+
several runs.
|
|
189
|
+
- Without `persist`, jobs run per instance while it is open.
|
|
190
|
+
- Job names are snake_case, and `every` is at least one second.
|
|
191
|
+
|
|
192
|
+
### Letting people control jobs
|
|
193
|
+
|
|
194
|
+
Jobs are code, so users cannot write new ones. With `controls`, the UI, the agent,
|
|
195
|
+
or both can pause, resume, retime, and run a job, and add jobs from a job that
|
|
196
|
+
takes `input`:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
jobs: ({ job }) => ({
|
|
200
|
+
prune_triaged: job({
|
|
201
|
+
every: '1h',
|
|
202
|
+
controls: 'both', // 'ui', 'agent', or 'both'; omit to leave it to code
|
|
203
|
+
minEvery: '15m', // the range users may pick from; defaults to every..30d
|
|
204
|
+
maxEvery: '7d',
|
|
205
|
+
run: (ctx) => { /* ... */ },
|
|
206
|
+
}),
|
|
207
|
+
recurring_check: job({ // with input, users add jobs from it; it never runs on its own
|
|
208
|
+
every: '7d', // the default for added jobs
|
|
209
|
+
controls: 'both',
|
|
210
|
+
input: z.object({ text: z.string().min(1).max(300) }),
|
|
211
|
+
maxJobs: 10, // per document
|
|
212
|
+
run: (ctx, { text }) => { /* ... */ },
|
|
213
|
+
}),
|
|
214
|
+
}),
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
const { useJobs } = createCanvasHooks<typeof canvas>()
|
|
219
|
+
|
|
220
|
+
function Schedule() {
|
|
221
|
+
const { jobs, definitions, update, run, add, remove, pending, error } = useJobs()
|
|
222
|
+
// jobs: [{ id: 'prune_triaged', every: 3600000, enabled: true, lastRunAt, nextRunAt, lastError, ... },
|
|
223
|
+
// { id: 'recurring_check-3f9a2c', job: 'recurring_check', input: { text }, added: true, ... }]
|
|
224
|
+
update('prune_triaged', { enabled: false }) // pause; { enabled: true } resumes
|
|
225
|
+
update('prune_triaged', { every: '6h' }) // within minEvery..maxEvery
|
|
226
|
+
run('prune_triaged') // now, even if paused
|
|
227
|
+
add('recurring_check', { text: 'Check security sign-off' }, { every: '1d' }) // typed input
|
|
228
|
+
remove('recurring_check-3f9a2c')
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- Settings and added jobs are saved in the document with the run history, so they
|
|
233
|
+
are shared live by every chat and used by `canvas-kit run`.
|
|
234
|
+
- A job's ID is its name, or `<name>-<hex>` for an added job. An added job runs
|
|
235
|
+
soon after it is added, then on its interval. Its input is checked against the
|
|
236
|
+
schema when it is added and before every run.
|
|
237
|
+
- The agent gets `list_jobs`, `update_job`, and `run_job` actions, plus `add_job`
|
|
238
|
+
and `remove_job` when a job takes input, covering only the jobs whose `controls`
|
|
239
|
+
include it. The UI calls the same actions through `useJobs()`.
|
|
240
|
+
- `formatInterval(ms)` turns an interval into text such as `6h`, and
|
|
241
|
+
`intervalMs('6h')` turns it back.
|
|
242
|
+
|
|
243
|
+
### Running jobs with no chat open
|
|
244
|
+
|
|
245
|
+
Nothing in the session runs after it ends. To run jobs with no chat open, run
|
|
246
|
+
`canvas-kit run` on a schedule. It runs every job that is due and not paused, for
|
|
247
|
+
every saved document, including jobs users added. Name a job or ID to run only
|
|
248
|
+
it, pass `--key` for one document, or `--force` to run even if not due or paused.
|
|
249
|
+
Jobs run with `ctx.headless` set, where `sendToAgent` fails. It uses the same lock,
|
|
250
|
+
so it is safe while chats have the canvas open.
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
# In a canvas package after `pnpm build`, or with --plugin for an installed plugin
|
|
254
|
+
npx canvas-kit run
|
|
255
|
+
npx canvas-kit run prune_triaged --plugin ~/.copilot/installed-plugins/my-marketplace/flaky-tests
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Run it at least as often as the shortest interval users may pick. For example,
|
|
259
|
+
every 15 minutes with cron (`crontab -e`):
|
|
260
|
+
|
|
261
|
+
```text
|
|
262
|
+
*/15 * * * * cd ~/canvases/flaky-tests && npx canvas-kit run >> /tmp/flaky-tests-jobs.log 2>&1
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Or with a launchd agent on macOS, in `~/Library/LaunchAgents/com.example.flaky-tests-jobs.plist`, loaded
|
|
266
|
+
with `launchctl load ~/Library/LaunchAgents/com.example.flaky-tests-jobs.plist`:
|
|
267
|
+
|
|
268
|
+
```xml
|
|
269
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
270
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
271
|
+
<plist version="1.0">
|
|
272
|
+
<dict>
|
|
273
|
+
<key>Label</key><string>com.example.flaky-tests-jobs</string>
|
|
274
|
+
<key>WorkingDirectory</key><string>/Users/me/canvases/flaky-tests</string>
|
|
275
|
+
<key>ProgramArguments</key>
|
|
276
|
+
<array><string>/opt/homebrew/bin/npx</string><string>canvas-kit</string><string>run</string></array>
|
|
277
|
+
<key>StartInterval</key><integer>900</integer>
|
|
278
|
+
</dict>
|
|
279
|
+
</plist>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The job runs on your machine because the documents are there; hosted CI such as
|
|
283
|
+
GitHub Actions cannot reach them. Work that needs the agent needs a live chat.
|
|
284
|
+
|
|
285
|
+
## Using it from React
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
import { createCanvasHooks } from '@rwoll/canvas-kit/react'
|
|
289
|
+
import type canvas from '../server/canvas.ts'
|
|
290
|
+
|
|
291
|
+
const { useCanvas, useAction } = createCanvasHooks<typeof canvas>()
|
|
292
|
+
|
|
293
|
+
function App() {
|
|
294
|
+
const { state, status } = useCanvas() // typed state, 'connecting' | 'live' | 'offline'
|
|
295
|
+
const [addBlocker, { pending, error }] = useAction('add_blocker')
|
|
296
|
+
// addBlocker({ text }) is type-checked against the action's Zod input
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`state` is available on the first render because the server embeds it in the
|
|
301
|
+
page. `useJobs()` shows and controls scheduled jobs; see
|
|
302
|
+
[Letting people control jobs](#letting-people-control-jobs). Import `@rwoll/canvas-kit/styles.css` for the shared light/dark theme.
|
|
303
|
+
|
|
304
|
+
UI code runs in the browser. The build fails if `src/` imports a Node.js module;
|
|
305
|
+
do that work in an action instead.
|
|
306
|
+
|
|
307
|
+
Keep canvas UIs flat and quiet, in the style of the Session summary canvas.
|
|
308
|
+
Separate sections with spacing or a single hairline instead of decorative
|
|
309
|
+
cards: no bordered or rounded containers, shadows, or outlined pills. Use plain
|
|
310
|
+
or muted text for metadata, and text links for secondary actions. A highlight
|
|
311
|
+
that helps people find the most important content, such as Session summary's
|
|
312
|
+
tinted executive summary, is the exception.
|
|
313
|
+
|
|
314
|
+
## Sharing components and a theme
|
|
315
|
+
|
|
316
|
+
A marketplace's `ui/` package holds the React components and the theme its canvases
|
|
317
|
+
share. `canvas-kit init` creates it (`@<marketplace>/ui`), `canvas-kit ui` adds it to
|
|
318
|
+
an existing marketplace, and `canvas-kit new` makes each new canvas depend on it,
|
|
319
|
+
import its `theme.css`, and use its `Header`.
|
|
320
|
+
|
|
321
|
+
```text
|
|
322
|
+
ui/package.json "name": "@my-canvases/ui", "exports": { ".": "./src/index.ts", "./theme.css": "./theme.css" }
|
|
323
|
+
ui/theme.css brand variables (--accent, --accent-contrast, ...) and component styles
|
|
324
|
+
ui/src/ Header, Markdown, Checklist, TextButton; add your own
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`theme.css` overrides the variables of `@rwoll/canvas-kit/styles.css`, so import it
|
|
328
|
+
second. It is plain TypeScript source in the workspace: each canvas's Vite build
|
|
329
|
+
bundles it, so the published plugins need nothing extra at run time.
|
|
330
|
+
|
|
331
|
+
## Testing
|
|
332
|
+
|
|
333
|
+
Unit tests run the canvas on a real local server with a temporary data folder:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { createTestCanvas } from '@rwoll/canvas-kit/testing'
|
|
337
|
+
|
|
338
|
+
const harness = await createTestCanvas(canvas)
|
|
339
|
+
await harness.open('test', { repo: 'octo/app' })
|
|
340
|
+
await harness.invoke('test', 'add_blocker', { text: 'CI red' }, 'ui')
|
|
341
|
+
expect(harness.state('test')?.blockers).toEqual(['CI red'])
|
|
342
|
+
expect(harness.sentToAgent).toEqual([]) // what ctx.sendToAgent() sent
|
|
343
|
+
await harness.runJob('prune_triaged') // run a job now; tickJobs() runs only due ones
|
|
344
|
+
await harness.invoke('test', 'update_job', { id: 'prune_triaged', enabled: false }, 'ui') // as useJobs() does
|
|
345
|
+
expect(harness.jobs('test')?.[0]?.enabled).toBe(false)
|
|
346
|
+
await harness.close()
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Browser tests use Playwright fixtures against the **built** canvas, so they run
|
|
350
|
+
with the same Content-Security-Policy and bundling as the installed plugin.
|
|
351
|
+
Build first (`pnpm build`) and install Chromium once (`pnpm -r --workspace-concurrency=1 run playwright:install`).
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
// e2e/board.spec.ts
|
|
355
|
+
import { canvasTest, expect } from '@rwoll/canvas-kit/playwright'
|
|
356
|
+
import canvas from '../server/canvas.ts'
|
|
357
|
+
|
|
358
|
+
const test = canvasTest(canvas)
|
|
359
|
+
|
|
360
|
+
test('the user and the agent share the board', async ({ openCanvas, agent, seed }) => {
|
|
361
|
+
await seed('octo/app', { repo: 'octo/app', blockers: [] }) // saved state, before opening
|
|
362
|
+
const { page, state, openAnotherPage } = await openCanvas({ repo: 'octo/app' })
|
|
363
|
+
|
|
364
|
+
await page.getByLabel('New blocker').fill('CI red')
|
|
365
|
+
await page.getByRole('button', { name: 'Add' }).click()
|
|
366
|
+
await agent.invoke('clear_resolved') // act as the agent
|
|
367
|
+
expect(agent.messages).toEqual([]) // what the canvas sent the agent
|
|
368
|
+
agent.failNextSend() // the next send fails
|
|
369
|
+
const other = await openAnotherPage() // a second view of the instance
|
|
370
|
+
})
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
| Fixture | What it does |
|
|
374
|
+
| --- | --- |
|
|
375
|
+
| `openCanvas(input?, { instanceId? })` | Opens an instance, points `page` at it, and waits until it is live (`html[data-canvas-status="live"]`). Returns `page`, `url`, `state()`, and `openAnotherPage()`. |
|
|
376
|
+
| `agent` | `invoke(action, input?)` as the agent, `messages` sent with `ctx.sendToAgent()`, and `failNextSend()`. |
|
|
377
|
+
| `seed(key, state)` | Saves state for a `persist` key before the canvas opens. |
|
|
378
|
+
| `runJob(id, { key?, force?, headless? })` | Runs a job now, by name or added job ID, for the latest instance or a saved document with `key`. |
|
|
379
|
+
| `canvasHarness` | The underlying `createTestCanvas` harness. |
|
|
380
|
+
|
|
381
|
+
Jobs run only through `runJob` and `harness.tickJobs()` in tests. To test
|
|
382
|
+
time-based jobs, pass a clock to both harnesses, `createTestCanvas(canvas, { now })`
|
|
383
|
+
and `canvasTest(canvas, { now })`, and move it forward between steps.
|
|
384
|
+
|
|
385
|
+
`defineCanvasConfig(overrides?)` in `playwright.config.ts` runs `e2e/` in light,
|
|
386
|
+
dark, and high-contrast Chromium. Canvas UIs show server state, so after a click,
|
|
387
|
+
assert with web-first assertions such as `toBeChecked()` rather than `check()`,
|
|
388
|
+
which expects the change at the moment of the click. Vitest skips `e2e/`.
|
|
389
|
+
|
|
390
|
+
## The canvas-kit command
|
|
391
|
+
|
|
392
|
+
A marketplace repository has `marketplace.config.json`, content-only plugins in
|
|
393
|
+
`plugins/<name>/` (skills, agents, MCP servers), and canvases in `canvases/<name>/`.
|
|
394
|
+
|
|
395
|
+
```json
|
|
396
|
+
{
|
|
397
|
+
"name": "my-marketplace",
|
|
398
|
+
"owner": { "name": "me" },
|
|
399
|
+
"repository": "me/my-marketplace",
|
|
400
|
+
"publishBranch": "stable",
|
|
401
|
+
"sourceBranch": "main"
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
| Command | What it does |
|
|
406
|
+
| --- | --- |
|
|
407
|
+
| `canvas-kit init <dir> --repository owner/name [--canvas name]` | Creates a marketplace repository with a shared `ui/` package (what `npm create @rwoll/canvas` runs). |
|
|
408
|
+
| `canvas-kit new <name> [--title] [--description] [--id] [--catalog] [--kit <range>]` | Adds a canvas package, wired to the shared UI package when the config names one. `--catalog` writes pnpm `catalog:` ranges. |
|
|
409
|
+
| `canvas-kit ui <package-name> [--catalog]` | Adds a shared UI package (`ui/`) to an existing marketplace and records it as `ui`. |
|
|
410
|
+
| `canvas-kit build` | Assembles `dist/marketplace` from `plugins/` and each canvas's `dist/plugin`, with a catalog generated from the plugin manifests. |
|
|
411
|
+
| `canvas-kit run [job] [--plugin] [--canvas] [--key] [--force] [--data-root]` | Runs a canvas's due jobs (or one job by name or ID) for its saved documents with no chat open, from a built plugin (`./dist/plugin` by default). Exits non-zero when a run fails. |
|
|
412
|
+
| `canvas-kit release [--dry-run] [--skip-checks]` | Builds the source branch in a clean worktree with a frozen-lockfile install, runs the `check` script (or `build`), and commits the output to the publish branch only when it changed. Each plugin gets a `release.json` stamp that the version footer shows. |
|
|
413
|
+
|
|
414
|
+
Optional config: `ui` (the shared UI package new canvases use), `pluginsDir`,
|
|
415
|
+
`canvasesDir`, `outDir`, `checkScript`, and
|
|
416
|
+
`installCommand`, `checkCommand`, or `buildCommand` to replace the detected
|
|
417
|
+
package manager commands. The library behind the command is
|
|
418
|
+
`@rwoll/canvas-kit/cli`.
|
|
419
|
+
|
|
420
|
+
## GitHub Actions
|
|
421
|
+
|
|
422
|
+
Publish on every push with the reusable workflow, pinned to a release commit.
|
|
423
|
+
`canvas-kit init` writes this for you with the commit your CLI was released from:
|
|
424
|
+
|
|
425
|
+
```yaml
|
|
426
|
+
# .github/workflows/publish.yml
|
|
427
|
+
on:
|
|
428
|
+
push:
|
|
429
|
+
branches: [main]
|
|
430
|
+
permissions: {}
|
|
431
|
+
jobs:
|
|
432
|
+
publish:
|
|
433
|
+
permissions:
|
|
434
|
+
contents: write
|
|
435
|
+
uses: rwoll/canvas-kit/.github/workflows/marketplace.yml@<commit> # v0.1.0
|
|
436
|
+
with:
|
|
437
|
+
playwright: true # when your check script runs browser tests
|
|
438
|
+
environment: stable # optional: approve each publish
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
It runs two jobs, so your dependencies never see a token that can write:
|
|
442
|
+
|
|
443
|
+
- **build** (read-only token) installs with the frozen lockfile, runs your
|
|
444
|
+
`check` script in a clean worktree, and prepares the next publish-branch commit
|
|
445
|
+
as a git bundle when the output changed.
|
|
446
|
+
- **push** (`contents: write`) runs no project code. It checks that the bundle is
|
|
447
|
+
one commit on top of the publish branch, built from this run's commit on your
|
|
448
|
+
source branch and newer than what is published, then fast-forwards the branch.
|
|
449
|
+
|
|
450
|
+
Inputs: `working-directory` (the folder with `marketplace.config.json`),
|
|
451
|
+
`node-version`, `playwright`, `dry-run`, `environment` (a GitHub environment for
|
|
452
|
+
the push job; give it required reviewers and limit it to your source branch),
|
|
453
|
+
`source-branch` (defaults to the repository's default branch), and
|
|
454
|
+
`publish-branch` (defaults to `stable`; the push job writes only that branch). Neither job
|
|
455
|
+
restores a dependency cache. Pin the workflow to a full commit SHA and let
|
|
456
|
+
Dependabot update it; `canvas-kit init` adds a `dependabot.yml` that does. The
|
|
457
|
+
workflow's own actions are locked in this repository's `actions.lock`, so it runs
|
|
458
|
+
under a **Require lockfile** policy.
|
|
459
|
+
|
|
460
|
+
Lock your marketplace's actions too: run
|
|
461
|
+
[`gh actions-lock`](https://github.com/github/gh-actions-lock) after `canvas-kit init`
|
|
462
|
+
and whenever you change a `uses:`, and commit `.github/workflows/actions.lock`. The
|
|
463
|
+
scaffolded `check.yml` verifies it on every pull request once it exists.
|
|
464
|
+
|
|
465
|
+
## Entry points
|
|
466
|
+
|
|
467
|
+
| Import | Runs in | Purpose |
|
|
468
|
+
| --- | --- | --- |
|
|
469
|
+
| `@rwoll/canvas-kit` | Node.js | `defineCanvas`, `z`, `CanvasKitError`, types |
|
|
470
|
+
| `@rwoll/canvas-kit/react` | Browser | `useCanvas`, `useAction`, `useJobs`, `createCanvasHooks`, `formatInterval` |
|
|
471
|
+
| `@rwoll/canvas-kit/client` | Browser | Framework-free `CanvasClient` |
|
|
472
|
+
| `@rwoll/canvas-kit/testing` | Node.js | `createTestCanvas` |
|
|
473
|
+
| `@rwoll/canvas-kit/playwright` | Playwright | `canvasTest`, `defineCanvasConfig`, `expect` |
|
|
474
|
+
| `@rwoll/canvas-kit/cli` | Node.js | `initMarketplace`, `newCanvas`, `buildMarketplace`, `release`, `runJob` |
|
|
475
|
+
| `@rwoll/canvas-kit/server` | Node.js | `CanvasHost`, `StateStore`, `runCanvasJob`, `copilotHome` |
|
|
476
|
+
| `@rwoll/canvas-kit/extension` | Node.js | SDK adapter used by the generated entry |
|
|
477
|
+
| `@rwoll/canvas-kit/vite` | Build | `canvasKit()` Vite plugin |
|
|
478
|
+
| `@rwoll/canvas-kit/styles.css` | Browser | Base theme |
|
|
479
|
+
| `@rwoll/canvas-kit/tsconfig/*.json` | Build | Shared TypeScript settings |
|
|
480
|
+
|
|
481
|
+
`react`, `vite`, `@playwright/test`, and `@github/copilot-sdk` are peer
|
|
482
|
+
dependencies; install the ones you use. The Copilot host supplies the SDK at run
|
|
483
|
+
time, so it is needed only for types.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { basename, resolve } from 'node:path';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { CliError, addUiPackage, buildMarketplace, initMarketplace, kitVersion, newCanvas, release, runJob } from './lib.mjs';
|
|
5
|
+
|
|
6
|
+
const USAGE = `Usage: canvas-kit <command> [options]
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
init <dir> --repository <owner/name> [--name <name>] [--owner <name>] [--canvas <name>]
|
|
10
|
+
Create a plugin marketplace repository with a shared ui/ package, optionally with a first canvas.
|
|
11
|
+
new <name> [--title <text>] [--description <text>] [--id <canvas-id>] [--catalog] [--kit <range>]
|
|
12
|
+
Add a canvas package under canvases/. --catalog writes pnpm catalog: ranges;
|
|
13
|
+
--kit sets the @rwoll/canvas-kit range, for example workspace:*. New canvases use the
|
|
14
|
+
marketplace's shared UI package when marketplace.config.json names one.
|
|
15
|
+
ui <package-name> [--catalog]
|
|
16
|
+
Add a shared package of React components and a theme (ui/) that new canvases use.
|
|
17
|
+
build
|
|
18
|
+
Assemble dist/marketplace from plugins/ and the built canvases.
|
|
19
|
+
release [--dry-run] [--skip-checks] [--footer <text>]... [--bundle <file> [--github-output]]
|
|
20
|
+
Build the source branch in a clean worktree and publish it to the publish branch.
|
|
21
|
+
With --bundle, write the prepared commit to a git bundle instead of pushing it;
|
|
22
|
+
--github-output also writes the commit and branch step outputs.
|
|
23
|
+
run [job] [--plugin <folder>] [--canvas <name>] [--key <key>] [--force] [--data-root <folder>]
|
|
24
|
+
Run a canvas's due jobs for its saved documents, with no chat open: every job,
|
|
25
|
+
or one job by name or ID. For launchd, cron, or Task Scheduler. --plugin
|
|
26
|
+
defaults to ./dist/plugin.
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--cwd <dir> Marketplace folder (default: current folder)
|
|
30
|
+
-h, --help Show this help
|
|
31
|
+
-v, --version Show the canvas-kit version
|
|
32
|
+
|
|
33
|
+
The marketplace folder holds marketplace.config.json.`;
|
|
34
|
+
|
|
35
|
+
async function main(argv) {
|
|
36
|
+
const { values, positionals } = parseArgs({
|
|
37
|
+
args: argv,
|
|
38
|
+
allowPositionals: true,
|
|
39
|
+
options: {
|
|
40
|
+
cwd: { type: 'string' },
|
|
41
|
+
help: { type: 'boolean', short: 'h' },
|
|
42
|
+
version: { type: 'boolean', short: 'v' },
|
|
43
|
+
title: { type: 'string' },
|
|
44
|
+
description: { type: 'string' },
|
|
45
|
+
id: { type: 'string' },
|
|
46
|
+
catalog: { type: 'boolean' },
|
|
47
|
+
kit: { type: 'string' },
|
|
48
|
+
name: { type: 'string' },
|
|
49
|
+
owner: { type: 'string' },
|
|
50
|
+
repository: { type: 'string' },
|
|
51
|
+
canvas: { type: 'string' },
|
|
52
|
+
'dry-run': { type: 'boolean' },
|
|
53
|
+
'skip-checks': { type: 'boolean' },
|
|
54
|
+
footer: { type: 'string', multiple: true },
|
|
55
|
+
bundle: { type: 'string' },
|
|
56
|
+
'github-output': { type: 'boolean' },
|
|
57
|
+
plugin: { type: 'string' },
|
|
58
|
+
key: { type: 'string' },
|
|
59
|
+
force: { type: 'boolean' },
|
|
60
|
+
'data-root': { type: 'string' },
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
const [command, ...rest] = positionals;
|
|
64
|
+
if (values.version) return console.log(await kitVersion());
|
|
65
|
+
if (values.help || !command) return console.log(USAGE);
|
|
66
|
+
const dir = resolve(values.cwd ?? '.');
|
|
67
|
+
|
|
68
|
+
switch (command) {
|
|
69
|
+
case 'init': {
|
|
70
|
+
const [target] = rest;
|
|
71
|
+
if (!target) throw new CliError('init needs a folder, for example `canvas-kit init my-marketplace --repository me/my-marketplace`');
|
|
72
|
+
if (!values.repository) throw new CliError('init needs --repository <owner/name>');
|
|
73
|
+
const folder = resolve(dir, target);
|
|
74
|
+
const name = values.name ?? basename(folder);
|
|
75
|
+
const owner = values.owner ?? values.repository.split('/')[0];
|
|
76
|
+
const { canvas } = await initMarketplace(folder, { name, owner, repository: values.repository, canvas: values.canvas });
|
|
77
|
+
console.log(`Created ${name} in ${folder}${canvas ? ` with the ${canvas.name} canvas` : ''}.\n\nNext:\n cd ${target}\n pnpm install\n pnpm check`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
case 'new': {
|
|
81
|
+
const [name] = rest;
|
|
82
|
+
if (!name) throw new CliError('new needs a canvas name, for example `canvas-kit new release-board`');
|
|
83
|
+
const canvas = await newCanvas(dir, name, {
|
|
84
|
+
title: values.title,
|
|
85
|
+
description: values.description,
|
|
86
|
+
id: values.id,
|
|
87
|
+
dependencies: values.catalog ? 'catalog' : 'ranges',
|
|
88
|
+
kitRange: values.kit,
|
|
89
|
+
});
|
|
90
|
+
console.log(`Created ${canvas.dir} (canvas id "${canvas.id}"). Run your package manager's install, then its dev script.`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
case 'ui': {
|
|
94
|
+
const [name] = rest;
|
|
95
|
+
if (!name) throw new CliError('ui needs a package name, for example `canvas-kit ui @acme/ui`');
|
|
96
|
+
const ui = await addUiPackage(dir, name, { dependencies: values.catalog ? 'catalog' : 'ranges' });
|
|
97
|
+
console.log(
|
|
98
|
+
`Created ${ui.dir} (${ui.name}) and recorded it in marketplace.config.json.` +
|
|
99
|
+
(ui.workspaceUpdated ? ' Added it to pnpm-workspace.yaml.' : ' Add its folder to your workspace packages.') +
|
|
100
|
+
`\nImport ${ui.name}/theme.css in each canvas's src/main.tsx; new canvases do this for you.`,
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
case 'build': {
|
|
105
|
+
const { catalog, outDir } = await buildMarketplace(dir);
|
|
106
|
+
console.log(`Built ${catalog.name} with ${catalog.plugins.length} plugins in ${outDir}:`);
|
|
107
|
+
for (const plugin of catalog.plugins) console.log(` ${plugin.name}@${plugin.version}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
case 'run': {
|
|
111
|
+
const [job] = rest;
|
|
112
|
+
const outcomes = await runJob(dir, job, {
|
|
113
|
+
plugin: values.plugin,
|
|
114
|
+
canvas: values.canvas,
|
|
115
|
+
key: values.key,
|
|
116
|
+
force: values.force,
|
|
117
|
+
dataRoot: values['data-root'],
|
|
118
|
+
log: (message, level) => console.error(`[${level}] ${message}`),
|
|
119
|
+
});
|
|
120
|
+
if (outcomes.length === 0) console.log('Nothing to run: no saved document has this job yet.');
|
|
121
|
+
for (const outcome of outcomes) {
|
|
122
|
+
const result = outcome.error ? `failed: ${outcome.error}` : outcome.ran ? 'ran' : outcome.paused ? 'paused' : 'not due';
|
|
123
|
+
console.log(`${outcome.key} ${outcome.id ?? outcome.job}: ${result}`);
|
|
124
|
+
}
|
|
125
|
+
if (outcomes.some(outcome => outcome.error)) process.exitCode = 1;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
case 'release':
|
|
129
|
+
await release(dir, {
|
|
130
|
+
dryRun: values['dry-run'],
|
|
131
|
+
skipChecks: values['skip-checks'],
|
|
132
|
+
footer: values.footer ?? [],
|
|
133
|
+
bundle: values.bundle,
|
|
134
|
+
githubOutput: values['github-output'],
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
default:
|
|
138
|
+
throw new CliError(`unknown command "${command}"\n\n${USAGE}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main(process.argv.slice(2)).catch(error => {
|
|
143
|
+
if (error instanceof CliError || error?.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
|
|
144
|
+
console.error(`canvas-kit: ${error.message}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
});
|