@erclx/canon 5.14.1 → 5.16.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 +3 -2
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/draft-screencast/REQUIREMENT.md +2 -1
- package/claude/skills/draft-screencast/SKILL.md +14 -9
- package/claude/skills/draft-wiki/SKILL.md +1 -1
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/read-frames/SKILL.md +3 -2
- package/claude/skills/record-screencast/REQUIREMENT.md +3 -0
- package/claude/skills/record-screencast/SKILL.md +28 -5
- package/claude/skills/search-craft/REQUIREMENT.md +57 -0
- package/claude/skills/search-craft/SKILL.md +83 -0
- package/docs/agents/canvas.md +36 -0
- package/docs/agents/commands.md +3 -0
- package/docs/agents/demo.md +34 -7
- package/docs/agents/index.md +1 -0
- package/docs/agents/migrate.md +1 -1
- package/docs/agents/sandbox.md +25 -0
- package/docs/target/migrations.md +1 -1
- package/docs/workflow/ai-workflow.md +2 -3
- package/docs/workflow/skill-map.md +2 -1
- package/governance/renames.toml +7 -5
- package/governance/rules/canon/630-requirements.md +1 -1
- package/package.json +5 -2
- package/scripts/lib/sandbox-git.sh +2 -2
- package/src/canvas/address.ts +184 -0
- package/src/canvas/capture.ts +172 -0
- package/src/canvas/client/app.tsx +87 -0
- package/src/canvas/client/inspector.tsx +121 -0
- package/src/canvas/client/layers.tsx +118 -0
- package/src/canvas/client/main.tsx +62 -0
- package/src/canvas/client/pages-panel.tsx +157 -0
- package/src/canvas/client/shell.css +449 -0
- package/src/canvas/client/state.ts +372 -0
- package/src/canvas/client/surface.tsx +465 -0
- package/src/canvas/content.ts +617 -0
- package/src/canvas/index.html +14 -0
- package/src/canvas/server.ts +471 -0
- package/src/canvas/tokens.ts +71 -0
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/seeds.ts +0 -1
- package/src/cli.ts +2 -0
- package/src/commands/canvas.ts +522 -0
- package/src/commands/claude.ts +9 -14
- package/src/commands/demo.ts +6 -6
- package/src/commands/migrate.ts +1 -1
- package/src/commands/sandbox.ts +108 -0
- package/src/context/classify/prompts.ts +1 -1
- package/src/context/folders.ts +0 -1
- package/src/demo/compile.ts +8 -4
- package/src/demo/container.ts +1 -1
- package/src/gov/renames.ts +3 -2
- package/src/help.ts +1 -0
- package/src/migrate/rename.ts +5 -4
- package/src/record-root.ts +1 -0
- package/src/sandbox/equivalence.ts +847 -0
- package/src/serve/static.ts +25 -11
- package/src/surface-root.ts +0 -1
- package/src/teach/browser/compile.ts +44 -0
- package/src/teach/browser/dismiss.ts +76 -0
- package/src/teach/browser/focus-line.ts +19 -0
- package/src/teach/browser/glossary-filter.ts +44 -0
- package/src/teach/browser/head.ts +30 -0
- package/src/teach/browser/quiz.ts +19 -0
- package/src/teach/browser/sidebar.ts +233 -0
- package/src/teach/browser/theme.ts +18 -0
- package/src/teach/nav.ts +19 -374
- package/standards/architecture.md +1 -1
- package/standards/context.md +1 -1
- package/standards/design.md +1 -1
- package/standards/index.md +0 -1
- package/standards/requirements.md +4 -6
- package/standards/wireframes.md +2 -2
- package/tooling/claude/reference.md +0 -1
- package/tooling/claude/seeds/canon/REQUIREMENTS.md +0 -2
- package/governance/rules/canon/645-decisions.md +0 -12
- package/standards/decisions.md +0 -105
- package/tooling/claude/seeds/canon/decisions/index.md +0 -8
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { statSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import type { Command } from 'commander'
|
|
4
|
+
import {
|
|
5
|
+
addFrame,
|
|
6
|
+
addPage,
|
|
7
|
+
type ContentRefused,
|
|
8
|
+
canvasDir,
|
|
9
|
+
DEFAULT_FRAME,
|
|
10
|
+
listPages,
|
|
11
|
+
moveFrame,
|
|
12
|
+
readPage,
|
|
13
|
+
readSelection,
|
|
14
|
+
renamePage,
|
|
15
|
+
} from '@/canvas/content'
|
|
16
|
+
import { captureCanvas } from '@/canvas/capture'
|
|
17
|
+
import { CANVAS_PORT, startCanvas } from '@/canvas/server'
|
|
18
|
+
import { resolveFrameTokens } from '@/canvas/tokens'
|
|
19
|
+
import { displayPath, parsePort, waitForInterrupt } from '@/serve/report'
|
|
20
|
+
import {
|
|
21
|
+
intro,
|
|
22
|
+
logAdd,
|
|
23
|
+
logError,
|
|
24
|
+
logInfo,
|
|
25
|
+
logStep,
|
|
26
|
+
logWarn,
|
|
27
|
+
outro,
|
|
28
|
+
plural,
|
|
29
|
+
} from '@/ui'
|
|
30
|
+
import { mainWorktreeRoot } from '@/worktree'
|
|
31
|
+
|
|
32
|
+
const BANNER = 'canon canvas'
|
|
33
|
+
|
|
34
|
+
interface RootOptions {
|
|
35
|
+
readonly root?: string
|
|
36
|
+
readonly json?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Refused {
|
|
40
|
+
readonly ok: false
|
|
41
|
+
readonly reason:
|
|
42
|
+
| ContentRefused['reason']
|
|
43
|
+
| 'no-root'
|
|
44
|
+
| 'no-server'
|
|
45
|
+
| 'capture-failed'
|
|
46
|
+
readonly detail: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type RootOutcome = { readonly ok: true; readonly root: string } | Refused
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Refuses a root that is not a directory rather than letting the first write
|
|
53
|
+
* scaffold `.canon/canvas/` under a mistyped path, the way `canon serve`
|
|
54
|
+
* refuses a directory it cannot find.
|
|
55
|
+
*/
|
|
56
|
+
async function resolveRoot(opts: RootOptions): Promise<RootOutcome> {
|
|
57
|
+
const root = opts.root
|
|
58
|
+
? resolve(process.cwd(), opts.root)
|
|
59
|
+
: await mainWorktreeRoot()
|
|
60
|
+
if (!statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
reason: 'no-root',
|
|
64
|
+
detail: `${root} is not a directory`,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { ok: true, root }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeJson(record: unknown): void {
|
|
71
|
+
process.stdout.write(`${JSON.stringify(record)}\n`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Every verb's refusal, framed on stderr and mirrored on stdout under --json. */
|
|
75
|
+
function refuse(refusal: Refused, emitJson: boolean): number {
|
|
76
|
+
if (emitJson) writeJson(refusal)
|
|
77
|
+
intro(BANNER)
|
|
78
|
+
logError(refusal.detail)
|
|
79
|
+
outro()
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parses a size flag. NaN for a value that is not a positive integer, which
|
|
85
|
+
* the content module then refuses as an invalid size.
|
|
86
|
+
*/
|
|
87
|
+
function parseSize(raw: string | undefined, fallback: number): number {
|
|
88
|
+
if (raw === undefined) return fallback
|
|
89
|
+
const value = Number(raw)
|
|
90
|
+
return Number.isInteger(value) && value > 0 ? value : Number.NaN
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function withRoot(command: Command): Command {
|
|
94
|
+
return command
|
|
95
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
96
|
+
.option('--root <path>', 'Project root, defaulting to the main worktree')
|
|
97
|
+
.option('--json', 'Emit a machine-readable record on stdout')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function register(program: Command): void {
|
|
101
|
+
const canvas = program
|
|
102
|
+
.command('canvas')
|
|
103
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
104
|
+
.description(
|
|
105
|
+
'Local canvas of pages and HTML frames (serve, list, page, frame, selection, capture)',
|
|
106
|
+
)
|
|
107
|
+
.addHelpText(
|
|
108
|
+
'after',
|
|
109
|
+
[
|
|
110
|
+
'',
|
|
111
|
+
'A page is a folder under .canon/canvas/, a frame is one HTML file in',
|
|
112
|
+
'it, and layout.json beside them holds each frame box. Edit a frame by',
|
|
113
|
+
'editing its file, and an open canvas reloads that frame.',
|
|
114
|
+
'',
|
|
115
|
+
'Examples:',
|
|
116
|
+
' canon canvas serve',
|
|
117
|
+
' canon canvas page add drafts',
|
|
118
|
+
' canon canvas frame add drafts hero --width 1440 --height 900',
|
|
119
|
+
' canon canvas frame move drafts hero --x 200 --y 120',
|
|
120
|
+
' canon canvas selection --json',
|
|
121
|
+
' canon canvas capture drafts/hero',
|
|
122
|
+
' canon canvas list --json',
|
|
123
|
+
'',
|
|
124
|
+
].join('\n'),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
withRoot(
|
|
128
|
+
canvas
|
|
129
|
+
.command('serve')
|
|
130
|
+
.description('Serve the canvas on localhost and print the link')
|
|
131
|
+
.option('--port <number>', `Port to try first, default ${CANVAS_PORT}`),
|
|
132
|
+
)
|
|
133
|
+
.addHelpText(
|
|
134
|
+
'after',
|
|
135
|
+
[
|
|
136
|
+
'',
|
|
137
|
+
'Exit codes:',
|
|
138
|
+
' 0 the server stopped after running',
|
|
139
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
140
|
+
'',
|
|
141
|
+
'The server binds 127.0.0.1 and nothing else. It runs until',
|
|
142
|
+
'interrupted, so a session wanting the link starts it in the',
|
|
143
|
+
'background and reads the record off stdout.',
|
|
144
|
+
'',
|
|
145
|
+
].join('\n'),
|
|
146
|
+
)
|
|
147
|
+
.action(async (opts: RootOptions & { port?: string }) => {
|
|
148
|
+
process.exitCode = await runServe(opts)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
withRoot(
|
|
152
|
+
canvas.command('list').description('List pages and the frames on each'),
|
|
153
|
+
).action(async (opts: RootOptions) => {
|
|
154
|
+
const resolved = await resolveRoot(opts)
|
|
155
|
+
if (!resolved.ok) {
|
|
156
|
+
process.exitCode = refuse(resolved, opts.json ?? false)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const { root } = resolved
|
|
160
|
+
const pages = listPages(root)
|
|
161
|
+
if (opts.json) {
|
|
162
|
+
writeJson({ ok: true, root, content: canvasDir(root), pages })
|
|
163
|
+
}
|
|
164
|
+
intro(BANNER)
|
|
165
|
+
if (pages.length === 0) {
|
|
166
|
+
logStep('No pages')
|
|
167
|
+
logInfo('canon canvas page add <name> adds one')
|
|
168
|
+
}
|
|
169
|
+
for (const page of pages) {
|
|
170
|
+
logStep(`${page.name} (${plural(page.frames.length, 'frame')})`)
|
|
171
|
+
for (const frame of page.frames) {
|
|
172
|
+
logInfo(`${frame.name} ${frame.width} × ${frame.height}`)
|
|
173
|
+
}
|
|
174
|
+
if (page.layoutIssue === 'malformed') {
|
|
175
|
+
logWarn('layout.json does not parse, so frames sit in a default row')
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
outro()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const page = canvas
|
|
182
|
+
.command('page')
|
|
183
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
184
|
+
.description('Add or rename a page')
|
|
185
|
+
|
|
186
|
+
withRoot(
|
|
187
|
+
page
|
|
188
|
+
.command('add')
|
|
189
|
+
.description('Add a page')
|
|
190
|
+
.argument('<name>', 'Page name, one path segment'),
|
|
191
|
+
).action(async (name: string, opts: RootOptions) => {
|
|
192
|
+
const resolved = await resolveRoot(opts)
|
|
193
|
+
const outcome = resolved.ok ? addPage(resolved.root, name) : resolved
|
|
194
|
+
if (!outcome.ok) {
|
|
195
|
+
process.exitCode = refuse(outcome, opts.json ?? false)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
if (opts.json) writeJson(outcome)
|
|
199
|
+
intro(BANNER)
|
|
200
|
+
logAdd(`page ${outcome.page}`)
|
|
201
|
+
outro()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
withRoot(
|
|
205
|
+
page
|
|
206
|
+
.command('rename')
|
|
207
|
+
.description('Rename a page, keeping its frames and layout')
|
|
208
|
+
.argument('<from>', 'Current page name')
|
|
209
|
+
.argument('<to>', 'New page name'),
|
|
210
|
+
).action(async (from: string, to: string, opts: RootOptions) => {
|
|
211
|
+
const resolved = await resolveRoot(opts)
|
|
212
|
+
const outcome = resolved.ok ? renamePage(resolved.root, from, to) : resolved
|
|
213
|
+
if (!outcome.ok) {
|
|
214
|
+
process.exitCode = refuse(outcome, opts.json ?? false)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
if (opts.json) writeJson({ ...outcome, from })
|
|
218
|
+
intro(BANNER)
|
|
219
|
+
logAdd(`page ${from} → ${outcome.page}`)
|
|
220
|
+
outro()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const frame = canvas
|
|
224
|
+
.command('frame')
|
|
225
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
226
|
+
.description('Add a frame to a page, or move one')
|
|
227
|
+
|
|
228
|
+
withRoot(
|
|
229
|
+
frame
|
|
230
|
+
.command('add')
|
|
231
|
+
.description('Add a frame as a new HTML file, placed after the others')
|
|
232
|
+
.argument('<page>', 'Page to add the frame to')
|
|
233
|
+
.argument('<name>', 'Frame name, which becomes <name>.html')
|
|
234
|
+
.option('--width <px>', `Frame width, default ${DEFAULT_FRAME.width}`)
|
|
235
|
+
.option('--height <px>', `Frame height, default ${DEFAULT_FRAME.height}`),
|
|
236
|
+
).action(
|
|
237
|
+
async (
|
|
238
|
+
pageName: string,
|
|
239
|
+
name: string,
|
|
240
|
+
opts: RootOptions & { width?: string; height?: string },
|
|
241
|
+
) => {
|
|
242
|
+
const resolved = await resolveRoot(opts)
|
|
243
|
+
if (!resolved.ok) {
|
|
244
|
+
process.exitCode = refuse(resolved, opts.json ?? false)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
const { root } = resolved
|
|
248
|
+
const outcome = addFrame(root, pageName, name, {
|
|
249
|
+
width: parseSize(opts.width, DEFAULT_FRAME.width),
|
|
250
|
+
height: parseSize(opts.height, DEFAULT_FRAME.height),
|
|
251
|
+
})
|
|
252
|
+
if (!outcome.ok) {
|
|
253
|
+
process.exitCode = refuse(outcome, opts.json ?? false)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
if (opts.json) {
|
|
257
|
+
writeJson({
|
|
258
|
+
...outcome,
|
|
259
|
+
path: resolve(canvasDir(root), outcome.page, outcome.file),
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
intro(BANNER)
|
|
263
|
+
logAdd(`${outcome.page}/${outcome.file}`)
|
|
264
|
+
logInfo(
|
|
265
|
+
`${outcome.box.width} × ${outcome.box.height} at ${outcome.box.x}, ${outcome.box.y}`,
|
|
266
|
+
)
|
|
267
|
+
outro()
|
|
268
|
+
},
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
withRoot(
|
|
272
|
+
frame
|
|
273
|
+
.command('move')
|
|
274
|
+
.description('Move a frame, keeping its size')
|
|
275
|
+
.argument('<page>', 'Page the frame is on')
|
|
276
|
+
.argument('<name>', 'Frame name')
|
|
277
|
+
.option('--x <px>', 'New x, defaulting to where the frame is')
|
|
278
|
+
.option('--y <px>', 'New y, defaulting to where the frame is'),
|
|
279
|
+
).action(
|
|
280
|
+
async (
|
|
281
|
+
pageName: string,
|
|
282
|
+
name: string,
|
|
283
|
+
opts: RootOptions & { x?: string; y?: string },
|
|
284
|
+
) => {
|
|
285
|
+
const resolved = await resolveRoot(opts)
|
|
286
|
+
if (!resolved.ok) {
|
|
287
|
+
process.exitCode = refuse(resolved, opts.json ?? false)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
const { root } = resolved
|
|
291
|
+
const current = readPage(root, pageName)?.frames.find(
|
|
292
|
+
(candidate) => candidate.name === name,
|
|
293
|
+
)
|
|
294
|
+
const outcome = moveFrame(root, pageName, name, {
|
|
295
|
+
x: parsePosition(opts.x, current?.x ?? 0),
|
|
296
|
+
y: parsePosition(opts.y, current?.y ?? 0),
|
|
297
|
+
})
|
|
298
|
+
if (!outcome.ok) {
|
|
299
|
+
process.exitCode = refuse(outcome, opts.json ?? false)
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
if (opts.json) writeJson(outcome)
|
|
303
|
+
intro(BANNER)
|
|
304
|
+
logAdd(`${outcome.page}/${outcome.file}`)
|
|
305
|
+
logInfo(`at ${outcome.box.x}, ${outcome.box.y}`)
|
|
306
|
+
outro()
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
withRoot(
|
|
311
|
+
canvas
|
|
312
|
+
.command('selection')
|
|
313
|
+
.description(
|
|
314
|
+
'Report the frame, or the element in it, the operator selected',
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
.addHelpText(
|
|
318
|
+
'after',
|
|
319
|
+
[
|
|
320
|
+
'',
|
|
321
|
+
'Reports none when nothing is selected or when the selected frame has',
|
|
322
|
+
'since been removed. Read this when the operator says "this one".',
|
|
323
|
+
'',
|
|
324
|
+
'An element is named by its index in document order, with the tag,',
|
|
325
|
+
'classes, and text it had when picked. stale is true once the frame',
|
|
326
|
+
'file changed since, since the index may then name another element.',
|
|
327
|
+
'',
|
|
328
|
+
].join('\n'),
|
|
329
|
+
)
|
|
330
|
+
.action(async (opts: RootOptions) => {
|
|
331
|
+
const resolved = await resolveRoot(opts)
|
|
332
|
+
if (!resolved.ok) {
|
|
333
|
+
process.exitCode = refuse(resolved, opts.json ?? false)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
const { root } = resolved
|
|
337
|
+
const selected = readSelection(root)
|
|
338
|
+
if (opts.json) {
|
|
339
|
+
writeJson({
|
|
340
|
+
ok: true,
|
|
341
|
+
selection: selected
|
|
342
|
+
? {
|
|
343
|
+
...selected,
|
|
344
|
+
path: resolve(canvasDir(root), selected.page, selected.file),
|
|
345
|
+
}
|
|
346
|
+
: null,
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
intro(BANNER)
|
|
350
|
+
if (!selected) {
|
|
351
|
+
logStep('Nothing selected')
|
|
352
|
+
} else {
|
|
353
|
+
logStep(`${selected.page}/${selected.frame}`)
|
|
354
|
+
const { box, element } = selected
|
|
355
|
+
logInfo(`${box.width} × ${box.height} at ${box.x}, ${box.y}`)
|
|
356
|
+
if (element) {
|
|
357
|
+
const classes = element.classes.map((name) => `.${name}`).join('')
|
|
358
|
+
logInfo(
|
|
359
|
+
`<${element.tag}${classes}> at index ${element.index}: ${element.text}`,
|
|
360
|
+
)
|
|
361
|
+
if (element.stale) {
|
|
362
|
+
logWarn('The frame changed since, so the index may have moved')
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
outro()
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
withRoot(
|
|
370
|
+
canvas
|
|
371
|
+
.command('capture')
|
|
372
|
+
.description('Capture a frame, or every frame of a page, as a PNG')
|
|
373
|
+
.argument('<target>', '<page>/<frame> for one frame, <page> for all')
|
|
374
|
+
.option(
|
|
375
|
+
'-o, --out <path>',
|
|
376
|
+
'The PNG for a frame, or the folder for a page; defaults to session scratch',
|
|
377
|
+
),
|
|
378
|
+
)
|
|
379
|
+
.addHelpText(
|
|
380
|
+
'after',
|
|
381
|
+
[
|
|
382
|
+
'',
|
|
383
|
+
'Serves the canvas for the call and captures through that address, so',
|
|
384
|
+
'the injected tokens are in the PNG and each frame renders at its own',
|
|
385
|
+
'width. The raw file on disk is never captured.',
|
|
386
|
+
'',
|
|
387
|
+
'Exit codes:',
|
|
388
|
+
' 0 every frame rendered',
|
|
389
|
+
' 1 refused or a frame failed, with the reason on stderr or in the JSON record',
|
|
390
|
+
'',
|
|
391
|
+
].join('\n'),
|
|
392
|
+
)
|
|
393
|
+
.action(async (target: string, opts: RootOptions & { out?: string }) => {
|
|
394
|
+
const resolved = await resolveRoot(opts)
|
|
395
|
+
if (!resolved.ok) {
|
|
396
|
+
process.exitCode = refuse(resolved, opts.json ?? false)
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
let outcome: Awaited<ReturnType<typeof captureCanvas>>
|
|
400
|
+
try {
|
|
401
|
+
outcome = await captureCanvas(
|
|
402
|
+
resolved.root,
|
|
403
|
+
target,
|
|
404
|
+
opts.out ? resolve(process.cwd(), opts.out) : undefined,
|
|
405
|
+
)
|
|
406
|
+
} catch (error) {
|
|
407
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
408
|
+
process.exitCode = refuse(
|
|
409
|
+
{ ok: false, reason: 'capture-failed', detail },
|
|
410
|
+
opts.json ?? false,
|
|
411
|
+
)
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
if (!outcome.ok) {
|
|
415
|
+
process.exitCode = refuse(outcome, opts.json ?? false)
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const rows = outcome.captures.map(({ target: item, result }) =>
|
|
420
|
+
result.status === 'rendered'
|
|
421
|
+
? {
|
|
422
|
+
page: item.page,
|
|
423
|
+
frame: item.frame,
|
|
424
|
+
status: result.status,
|
|
425
|
+
path: result.pngPath,
|
|
426
|
+
width: result.width,
|
|
427
|
+
height: result.height,
|
|
428
|
+
}
|
|
429
|
+
: {
|
|
430
|
+
page: item.page,
|
|
431
|
+
frame: item.frame,
|
|
432
|
+
status: result.status,
|
|
433
|
+
reason: result.reason,
|
|
434
|
+
},
|
|
435
|
+
)
|
|
436
|
+
const failed = rows.some((row) => row.status === 'failed')
|
|
437
|
+
if (opts.json) writeJson({ ok: !failed, captures: rows })
|
|
438
|
+
intro(BANNER)
|
|
439
|
+
for (const row of rows) {
|
|
440
|
+
if (row.status === 'rendered') {
|
|
441
|
+
logAdd(`${row.page}/${row.frame}`)
|
|
442
|
+
logInfo(`${displayPath(row.path)} ${row.width}x${row.height}`)
|
|
443
|
+
} else {
|
|
444
|
+
logError(`${row.page}/${row.frame}: ${row.reason}`)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
outro()
|
|
448
|
+
if (failed) process.exitCode = 1
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** NaN for a value that is not a number, which the content module refuses. */
|
|
453
|
+
function parsePosition(raw: string | undefined, fallback: number): number {
|
|
454
|
+
if (raw === undefined) return fallback
|
|
455
|
+
return raw.trim() === '' ? Number.NaN : Number(raw)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function runServe(
|
|
459
|
+
opts: RootOptions & { port?: string },
|
|
460
|
+
): Promise<number> {
|
|
461
|
+
const emitJson = opts.json ?? false
|
|
462
|
+
const port = opts.port === undefined ? CANVAS_PORT : parsePort(opts.port)
|
|
463
|
+
if (port === undefined) {
|
|
464
|
+
return refuseServe('no-port', `${opts.port} is not a port`, emitJson)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const resolved = await resolveRoot(opts)
|
|
468
|
+
if (!resolved.ok) {
|
|
469
|
+
return refuseServe(resolved.reason, resolved.detail, emitJson)
|
|
470
|
+
}
|
|
471
|
+
const { root } = resolved
|
|
472
|
+
/*
|
|
473
|
+
* Imported here rather than at the top, so a command module the test runner
|
|
474
|
+
* loads never asks it for an HTML module it cannot parse.
|
|
475
|
+
*/
|
|
476
|
+
const { default: shell } = await import('@/canvas/index.html')
|
|
477
|
+
const outcome = startCanvas(root, { port, shell })
|
|
478
|
+
if (!outcome.ok) {
|
|
479
|
+
return refuseServe(outcome.reason, outcome.detail, emitJson)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const tokens = resolveFrameTokens(root)
|
|
483
|
+
const { css: _css, ...tokenSource } = tokens
|
|
484
|
+
if (emitJson) {
|
|
485
|
+
writeJson({
|
|
486
|
+
ok: true,
|
|
487
|
+
root: outcome.root,
|
|
488
|
+
content: outcome.content,
|
|
489
|
+
host: outcome.host,
|
|
490
|
+
port: outcome.port,
|
|
491
|
+
url: outcome.url,
|
|
492
|
+
tokens: tokenSource,
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
intro(BANNER)
|
|
496
|
+
logStep('Serving')
|
|
497
|
+
logInfo(displayPath(outcome.content))
|
|
498
|
+
logStep('Open')
|
|
499
|
+
logInfo(outcome.url)
|
|
500
|
+
logStep('Tokens')
|
|
501
|
+
if (tokens.source === 'none') logWarn(tokens.notice)
|
|
502
|
+
else if (tokens.source === 'installed') {
|
|
503
|
+
for (const file of tokens.files) logInfo(file)
|
|
504
|
+
} else logInfo('this toolkit’s own token module')
|
|
505
|
+
logStep('Stop')
|
|
506
|
+
logInfo('Ctrl-C')
|
|
507
|
+
|
|
508
|
+
await waitForInterrupt(outcome.stop)
|
|
509
|
+
return 0
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function refuseServe(
|
|
513
|
+
reason: string,
|
|
514
|
+
detail: string,
|
|
515
|
+
emitJson: boolean,
|
|
516
|
+
): number {
|
|
517
|
+
if (emitJson) writeJson({ ok: false, reason, detail })
|
|
518
|
+
intro(BANNER)
|
|
519
|
+
logError(detail)
|
|
520
|
+
outro()
|
|
521
|
+
return 1
|
|
522
|
+
}
|
package/src/commands/claude.ts
CHANGED
|
@@ -153,21 +153,16 @@ const SEEDED_FILES: readonly string[] = [
|
|
|
153
153
|
'REQUIREMENTS.md',
|
|
154
154
|
'DESIGN.md',
|
|
155
155
|
]
|
|
156
|
-
const SEEDED_DIRS: readonly string[] = [
|
|
157
|
-
'decisions',
|
|
158
|
-
'memory',
|
|
159
|
-
'tasks',
|
|
160
|
-
'wireframes',
|
|
161
|
-
]
|
|
156
|
+
const SEEDED_DIRS: readonly string[] = ['memory', 'tasks', 'wireframes']
|
|
162
157
|
|
|
163
158
|
/**
|
|
164
159
|
* Where a `SEEDED_DIRS` entry resolves for the sync presence check.
|
|
165
160
|
*
|
|
166
|
-
* `wireframes`
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
161
|
+
* `wireframes` is a tracked surface entry, resolved under `canon/` ahead of
|
|
162
|
+
* `.claude/`, while `memory` and `tasks` are session records resolved under
|
|
163
|
+
* `.canon/` ahead of `.claude/`. Routing every name through `recordDir`
|
|
164
|
+
* reported a migrated target's `canon/wireframes/` as missing, since that
|
|
165
|
+
* resolver never checks `canon/`.
|
|
171
166
|
*/
|
|
172
167
|
export function seededDirPath(resolved: string, name: string): string {
|
|
173
168
|
return SURFACE_ENTRIES.includes(name)
|
|
@@ -605,9 +600,9 @@ async function runSync(target: string): Promise<number> {
|
|
|
605
600
|
|
|
606
601
|
// A record folder resolves at either root, so a migrated target is reported as
|
|
607
602
|
// seeded rather than sent to `canon claude init` to re-create records it
|
|
608
|
-
// already holds. `wireframes`
|
|
609
|
-
//
|
|
610
|
-
//
|
|
603
|
+
// already holds. `wireframes` is a tracked surface entry instead, resolved
|
|
604
|
+
// through `surfaceDir`, since a migrated target holds it at `canon/` rather
|
|
605
|
+
// than under either record root.
|
|
611
606
|
logStep('Seeded')
|
|
612
607
|
for (const name of SEEDED_FILES) {
|
|
613
608
|
if (existsSync(join(resolved, '.claude', name))) logInfo(name)
|
package/src/commands/demo.ts
CHANGED
|
@@ -73,7 +73,7 @@ export function register(program: Command): void {
|
|
|
73
73
|
' 1 refused, with the reason on stderr',
|
|
74
74
|
'',
|
|
75
75
|
'Examples:',
|
|
76
|
-
' canon demo compile
|
|
76
|
+
' canon demo compile demos/inline-edit/beats.md --slug inline-edit',
|
|
77
77
|
' canon demo compile draft.md --out demos --force',
|
|
78
78
|
'',
|
|
79
79
|
].join('\n'),
|
|
@@ -114,8 +114,8 @@ export function register(program: Command): void {
|
|
|
114
114
|
' 1 refused, with the reason on stderr',
|
|
115
115
|
'',
|
|
116
116
|
'Examples:',
|
|
117
|
-
' canon demo run demos/inline-edit.json',
|
|
118
|
-
' canon demo run demos/inline-edit.json --cursor ~/cursors/theme',
|
|
117
|
+
' canon demo run demos/inline-edit/plan.json',
|
|
118
|
+
' canon demo run demos/inline-edit/plan.json --cursor ~/cursors/theme',
|
|
119
119
|
'',
|
|
120
120
|
].join('\n'),
|
|
121
121
|
)
|
|
@@ -146,8 +146,8 @@ export function register(program: Command): void {
|
|
|
146
146
|
' 1 refused, with the reason on stderr',
|
|
147
147
|
'',
|
|
148
148
|
'Examples:',
|
|
149
|
-
' canon demo frames demos/inline-edit.webm',
|
|
150
|
-
' canon demo frames demos/inline-edit.webm --fps 2 --out frames',
|
|
149
|
+
' canon demo frames demos/inline-edit/take/inline-edit.webm',
|
|
150
|
+
' canon demo frames demos/inline-edit/take/inline-edit.webm --fps 2 --out frames',
|
|
151
151
|
'',
|
|
152
152
|
].join('\n'),
|
|
153
153
|
)
|
|
@@ -182,7 +182,7 @@ function runCompile(draftPath: string, opts: CompileOptions): number {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
const slug = opts.slug ?? basename(source, extname(source))
|
|
185
|
-
const target = resolve(process.cwd(), opts.out,
|
|
185
|
+
const target = resolve(process.cwd(), opts.out, slug, 'plan.json')
|
|
186
186
|
|
|
187
187
|
if (existsSync(target) && !opts.force) {
|
|
188
188
|
logStep('Plan')
|
package/src/commands/migrate.ts
CHANGED
|
@@ -1222,7 +1222,7 @@ export function register(program: Command): void {
|
|
|
1222
1222
|
'',
|
|
1223
1223
|
'The changelog is never rewritten. Its entries record what shipped',
|
|
1224
1224
|
'under the old name, and GitHub redirects the links they carry.',
|
|
1225
|
-
'aitk-sandbox
|
|
1225
|
+
'aitk-sandbox, the old name of erclx/canon-sandbox, is left alone.',
|
|
1226
1226
|
'',
|
|
1227
1227
|
'Examples:',
|
|
1228
1228
|
' canon migrate rename',
|