@erclx/canon 5.52.0 → 5.54.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/CHANGELOG.md +17 -0
- package/README.md +11 -13
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +5 -8
- package/claude/skills/context-fold/REQUIREMENT.md +0 -1
- package/claude/skills/design-identity/REQUIREMENT.md +2 -2
- package/claude/skills/design-identity/SKILL.md +6 -6
- package/claude/skills/design-record/REQUIREMENT.md +2 -2
- package/claude/skills/design-record/SKILL.md +4 -4
- package/claude/skills/design-record/references/folder.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/references/kinds.md +0 -1
- package/claude/skills/draw-figure/REQUIREMENT.md +3 -4
- package/claude/skills/draw-figure/SKILL.md +5 -6
- package/claude/skills/draw-figure/references/figures.md +0 -1
- package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
- package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -5
- package/claude/skills/pick-by-eye/SKILL.md +34 -41
- package/claude/skills/pick-by-eye/references/board.md +42 -0
- package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
- package/claude/skills/teach-craft/SKILL.md +70 -0
- package/claude/skills/teach-craft/references/adopted.md +113 -0
- package/claude/skills/teach-craft/references/lesson-craft.md +60 -0
- package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +41 -6
- package/docs/agents/audits.md +0 -1
- package/docs/agents/install-and-sync.md +0 -8
- package/docs/target/migrations.md +16 -2
- package/docs/target/projects.md +1 -1
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +10 -22
- package/docs/workflow/skill-map.md +11 -14
- package/docs/workflow/visual-design-workflow.md +1 -4
- package/governance/detect.toml +1 -6
- package/governance/renames.toml +0 -1
- package/package.json +2 -8
- package/scripts/core/regen-web-previews.ts +2 -67
- package/src/audits/catalog.ts +0 -1
- package/src/browser/engine.ts +3 -6
- package/src/capture/render.ts +1 -1
- package/src/claude/cases/authoring.ts +0 -12
- package/src/claude/cases/misc.ts +0 -22
- package/src/claude/cases/workflow.ts +2 -38
- package/src/claude/skills-audit.ts +1 -1
- package/src/cli/help.ts +0 -10
- package/src/cli.ts +0 -8
- package/src/commands/design.ts +16 -315
- package/src/commands/records.ts +0 -1
- package/src/commands/serve.ts +2 -2
- package/src/commands/sync.ts +4 -5
- package/src/design/components.ts +8 -1514
- package/src/design/css.ts +5 -6
- package/src/design/fonts.ts +3 -5
- package/src/design/regen.ts +5 -32
- package/src/design/render.ts +1 -1
- package/src/design/tokens.ts +3 -2
- package/src/driver/drive.ts +3 -4
- package/src/driver/steps.ts +2 -5
- package/src/gate/stages.ts +4 -10
- package/src/gov/audit.ts +1 -6
- package/src/inventory/walk.ts +1 -1
- package/src/markdown/scope.ts +6 -35
- package/src/records/validate.ts +4 -176
- package/src/sandbox/equivalence.ts +0 -5
- package/src/shipped/references.ts +1 -2
- package/src/sync/check.ts +4 -29
- package/src/sync/stamp.ts +6 -6
- package/src/sync/target.ts +1 -4
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/canon/DESIGN.md +1 -1
- package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
- package/claude/skills/canvas-board/SKILL.md +0 -143
- package/claude/skills/canvas-board/references/canvas.md +0 -170
- package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
- package/claude/skills/draft-and-pick/SKILL.md +0 -11
- package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
- package/claude/skills/draft-doc/SKILL.md +0 -11
- package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
- package/claude/skills/draft-figure/SKILL.md +0 -11
- package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
- package/claude/skills/draft-identity/SKILL.md +0 -11
- package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
- package/claude/skills/draft-ready/SKILL.md +0 -11
- package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
- package/claude/skills/draft-screencast/SKILL.md +0 -11
- package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
- package/claude/skills/draft-slides/SKILL.md +0 -11
- package/claude/skills/make-deck/REQUIREMENT.md +0 -36
- package/claude/skills/make-deck/SKILL.md +0 -80
- package/claude/skills/make-deck/references/deck.md +0 -183
- package/claude/skills/read-frames/REQUIREMENT.md +0 -30
- package/claude/skills/read-frames/SKILL.md +0 -55
- package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
- package/claude/skills/record-screencast/SKILL.md +0 -99
- package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
- package/claude/skills/script-screencast/SKILL.md +0 -70
- package/claude/skills/script-screencast/references/beats-template.md +0 -119
- package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
- package/claude/skills/teach-workspace/SKILL.md +0 -196
- package/claude/skills/teach-workspace/references/glossary.md +0 -90
- package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
- package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
- package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
- package/claude/skills/teach-workspace/references/promotion.md +0 -116
- package/claude/skills/teach-workspace/references/revisit.md +0 -28
- package/claude/skills/teach-workspace/references/teach.md +0 -211
- package/claude/skills/video-craft/REQUIREMENT.md +0 -35
- package/claude/skills/video-craft/SKILL.md +0 -96
- package/claude/skills/video-craft/references/adopted.md +0 -110
- package/governance/rules/ui/canvas.md +0 -13
- package/src/canvas/address.ts +0 -227
- package/src/canvas/arrange.ts +0 -66
- package/src/canvas/capture.ts +0 -391
- package/src/canvas/client/app.tsx +0 -188
- package/src/canvas/client/inspector/align-grid.tsx +0 -162
- package/src/canvas/client/inspector/color-field.tsx +0 -398
- package/src/canvas/client/inspector/color-inputs.tsx +0 -139
- package/src/canvas/client/inspector/color-picker.tsx +0 -668
- package/src/canvas/client/inspector/color-space.ts +0 -240
- package/src/canvas/client/inspector/field.tsx +0 -263
- package/src/canvas/client/inspector/fields.css +0 -763
- package/src/canvas/client/inspector/section.tsx +0 -27
- package/src/canvas/client/inspector/size-field.tsx +0 -69
- package/src/canvas/client/inspector/token-select.tsx +0 -109
- package/src/canvas/client/inspector/values.ts +0 -297
- package/src/canvas/client/inspector.tsx +0 -692
- package/src/canvas/client/keys.ts +0 -71
- package/src/canvas/client/layers.tsx +0 -118
- package/src/canvas/client/main.tsx +0 -71
- package/src/canvas/client/pages-panel.tsx +0 -212
- package/src/canvas/client/panel-handle.tsx +0 -90
- package/src/canvas/client/selection.tsx +0 -199
- package/src/canvas/client/shell.css +0 -762
- package/src/canvas/client/state.ts +0 -792
- package/src/canvas/client/surface.tsx +0 -803
- package/src/canvas/client/theme-panel.tsx +0 -55
- package/src/canvas/client/tools.tsx +0 -153
- package/src/canvas/content.ts +0 -781
- package/src/canvas/edit.ts +0 -575
- package/src/canvas/editing.ts +0 -203
- package/src/canvas/fit.ts +0 -238
- package/src/canvas/global.d.ts +0 -15
- package/src/canvas/history.ts +0 -116
- package/src/canvas/index.html +0 -20
- package/src/canvas/server.ts +0 -766
- package/src/canvas/tokens.ts +0 -221
- package/src/commands/canvas.ts +0 -1097
- package/src/commands/demo.ts +0 -542
- package/src/commands/slides.ts +0 -184
- package/src/commands/teach.ts +0 -1401
- package/src/demo/beats.ts +0 -135
- package/src/demo/compile.ts +0 -350
- package/src/demo/container.ts +0 -219
- package/src/demo/cursors.ts +0 -55
- package/src/demo/drive.ts +0 -442
- package/src/demo/pointer.ts +0 -178
- package/src/demo/theme.ts +0 -112
- package/src/demo/timeline.ts +0 -64
- package/src/design/adapter.ts +0 -59
- package/src/design/base.css +0 -203
- package/src/design/brand-favicon.ts +0 -18
- package/src/design/neutral.ts +0 -127
- package/src/design/project-fonts.ts +0 -392
- package/src/slides/convert/chart.ts +0 -165
- package/src/slides/convert/deck.ts +0 -244
- package/src/slides/convert/export.ts +0 -567
- package/src/slides/convert/master.ts +0 -198
- package/src/slides/convert/shapes.ts +0 -804
- package/src/slides/convert/svg.ts +0 -38
- package/src/slides/convert/walk.ts +0 -671
- package/src/slides/decks.ts +0 -110
- package/src/slides/open.ts +0 -18
- package/src/slides/package/fonts.ts +0 -301
- package/src/slides/package/motion.ts +0 -316
- package/src/slides/package/test-face.ts +0 -113
- package/src/teach/browser/compile.ts +0 -44
- package/src/teach/browser/dismiss.ts +0 -76
- package/src/teach/browser/focus-line.ts +0 -19
- package/src/teach/browser/glossary-filter.ts +0 -44
- package/src/teach/browser/head.ts +0 -30
- package/src/teach/browser/quiz.ts +0 -19
- package/src/teach/browser/reload.ts +0 -9
- package/src/teach/browser/sidebar.ts +0 -233
- package/src/teach/browser/theme.ts +0 -18
- package/src/teach/components/code.tsx +0 -24
- package/src/teach/components/heading.tsx +0 -11
- package/src/teach/components/list.tsx +0 -17
- package/src/teach/components/paragraph.tsx +0 -29
- package/src/teach/components/refs.tsx +0 -28
- package/src/teach/cross-references.ts +0 -78
- package/src/teach/fonts.ts +0 -16
- package/src/teach/highlight.ts +0 -71
- package/src/teach/html/jsx-dev-runtime.ts +0 -13
- package/src/teach/html/jsx-runtime.ts +0 -113
- package/src/teach/html/raw.ts +0 -28
- package/src/teach/lesson.ts +0 -178
- package/src/teach/nav.ts +0 -1337
- package/src/teach/render.ts +0 -240
- package/src/teach/workspace.ts +0 -1164
- package/src/teach/write-if-changed.ts +0 -17
package/src/commands/demo.ts
DELETED
|
@@ -1,542 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { basename, dirname, extname, join, relative, resolve } from 'node:path'
|
|
3
|
-
import type { Command } from 'commander'
|
|
4
|
-
import { INSTALL_BROWSER, isEngineMissing } from '@/browser/engine'
|
|
5
|
-
import { parseDraft } from '@/demo/beats'
|
|
6
|
-
import { compilePlan, parsePlan, unresolved } from '@/demo/compile'
|
|
7
|
-
import {
|
|
8
|
-
convertToGif,
|
|
9
|
-
convertToMp4,
|
|
10
|
-
extractFrames,
|
|
11
|
-
INSTALL_CONVERTER,
|
|
12
|
-
} from '@/demo/container'
|
|
13
|
-
import { DEFAULT_CURSORS } from '@/demo/cursors'
|
|
14
|
-
import { loadCursorTheme } from '@/demo/theme'
|
|
15
|
-
import {
|
|
16
|
-
intro,
|
|
17
|
-
logError,
|
|
18
|
-
logInfo,
|
|
19
|
-
logStep,
|
|
20
|
-
logWarn,
|
|
21
|
-
outro,
|
|
22
|
-
plural,
|
|
23
|
-
} from '@/cli/ui'
|
|
24
|
-
|
|
25
|
-
const DEFAULT_OUT = 'demos'
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Holds wiring only. Every browser reference sits behind `loadDriver`, because
|
|
29
|
-
* `src/cli.ts` imports this module at startup and resolving the engine there
|
|
30
|
-
* would put a browser launch in front of every other command.
|
|
31
|
-
*/
|
|
32
|
-
type Driver = typeof import('@/demo/drive')
|
|
33
|
-
|
|
34
|
-
interface CompileOptions {
|
|
35
|
-
readonly out: string
|
|
36
|
-
readonly slug?: string
|
|
37
|
-
readonly force?: boolean
|
|
38
|
-
readonly json?: boolean
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface RunOptions {
|
|
42
|
-
readonly out?: string
|
|
43
|
-
readonly cursor?: string
|
|
44
|
-
readonly video: boolean
|
|
45
|
-
readonly still: boolean
|
|
46
|
-
readonly gif?: boolean
|
|
47
|
-
readonly json?: boolean
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface FramesOptions {
|
|
51
|
-
readonly out?: string
|
|
52
|
-
readonly fps: string
|
|
53
|
-
readonly json?: boolean
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function register(program: Command): void {
|
|
57
|
-
const demo = program
|
|
58
|
-
.command('demo')
|
|
59
|
-
.description('Drive a running application and record what it did')
|
|
60
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
61
|
-
|
|
62
|
-
demo
|
|
63
|
-
.command('compile')
|
|
64
|
-
.description('Turn a screencast draft into a plan a run can drive')
|
|
65
|
-
.argument('<draft>', 'Screencast draft written by script-screencast')
|
|
66
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
67
|
-
.option('-o, --out <dir>', 'Directory the plan is written to', DEFAULT_OUT)
|
|
68
|
-
.option('-s, --slug <slug>', 'Plan name, defaulting to the draft filename')
|
|
69
|
-
.option('--force', 'Overwrite a plan that already exists')
|
|
70
|
-
.option('--json', 'Add a machine-readable record on stdout')
|
|
71
|
-
.addHelpText(
|
|
72
|
-
'after',
|
|
73
|
-
[
|
|
74
|
-
'',
|
|
75
|
-
'The plan is committed rather than scratch. It carries the target, the',
|
|
76
|
-
'wait condition, and the timing a beat lacks, and the timing is a',
|
|
77
|
-
'starting point you tune, which is why a recompile refuses to overwrite.',
|
|
78
|
-
'',
|
|
79
|
-
'Reasons:',
|
|
80
|
-
' draft-missing no file at the draft path',
|
|
81
|
-
' draft-unreadable no beat sheet in the draft, or a sheet with no beats',
|
|
82
|
-
' plan-exists a plan is already there and --force was not passed',
|
|
83
|
-
' plan-unresolved a URL or a target is still empty, named field by field',
|
|
84
|
-
'',
|
|
85
|
-
'Exit codes:',
|
|
86
|
-
' 0 a plan was written',
|
|
87
|
-
' 1 refused, with the reason on stderr',
|
|
88
|
-
'',
|
|
89
|
-
'Examples:',
|
|
90
|
-
' canon demo compile demos/inline-edit/beats.md --slug inline-edit',
|
|
91
|
-
' canon demo compile draft.md --out demos --force',
|
|
92
|
-
'',
|
|
93
|
-
].join('\n'),
|
|
94
|
-
)
|
|
95
|
-
.action(async (draft: string, opts: CompileOptions) => {
|
|
96
|
-
process.exitCode = runCompile(draft, opts)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
demo
|
|
100
|
-
.command('run')
|
|
101
|
-
.description('Drive the application a plan names and write the recording')
|
|
102
|
-
.argument('<plan>', 'Plan written by canon demo compile')
|
|
103
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
104
|
-
.option('-o, --out <dir>', 'Directory to write into, overriding the plan')
|
|
105
|
-
.option(
|
|
106
|
-
'-c, --cursor <dir>',
|
|
107
|
-
'Cursor theme folder to draw the pointer from',
|
|
108
|
-
)
|
|
109
|
-
.option('--no-video', 'Skip the recording and write only the still')
|
|
110
|
-
.option('--no-still', 'Skip the still and write only the recording')
|
|
111
|
-
.option('--gif', 'Also write a gif, for a host that strips video')
|
|
112
|
-
.option('--json', 'Add a machine-readable record on stdout')
|
|
113
|
-
.addHelpText(
|
|
114
|
-
'after',
|
|
115
|
-
[
|
|
116
|
-
'',
|
|
117
|
-
'Needs a browser binary. Install it with:',
|
|
118
|
-
` ${INSTALL_BROWSER}`,
|
|
119
|
-
'',
|
|
120
|
-
'Writes mp4 beside the webm when ffmpeg is on PATH, and skips it',
|
|
121
|
-
`otherwise without failing the run. Install it with: ${INSTALL_CONVERTER}`,
|
|
122
|
-
'',
|
|
123
|
-
'Beside the webm it writes <video>.timeline.json, an entries array with',
|
|
124
|
-
'one entry per step carrying index, kind, and startMs and endMs from the',
|
|
125
|
-
'first frame. A click, fill, or hover entry adds box, the target in',
|
|
126
|
-
'viewport pixels. The still is the hero beat, or the last beat.',
|
|
127
|
-
'',
|
|
128
|
-
'A gif is opt-in behind --gif, since it is far larger than the webm',
|
|
129
|
-
'and only a host that strips video needs one. GitHub is that host.',
|
|
130
|
-
'',
|
|
131
|
-
'Reasons:',
|
|
132
|
-
' no-output-requested --no-video with --no-still, which would write nothing',
|
|
133
|
-
' plan-unreadable the plan is not JSON, or a step names a kind nothing drives',
|
|
134
|
-
' browser-missing the browser binary is not installed',
|
|
135
|
-
' engine-missing the browser package did not resolve',
|
|
136
|
-
'',
|
|
137
|
-
'Exit codes:',
|
|
138
|
-
' 0 the recording and the still were written',
|
|
139
|
-
' 1 refused, with the reason on stderr',
|
|
140
|
-
'',
|
|
141
|
-
'Examples:',
|
|
142
|
-
' canon demo run demos/inline-edit/plan.json',
|
|
143
|
-
' canon demo run demos/inline-edit/plan.json --cursor ~/cursors/theme',
|
|
144
|
-
'',
|
|
145
|
-
].join('\n'),
|
|
146
|
-
)
|
|
147
|
-
.action(async (plan: string, opts: RunOptions) => {
|
|
148
|
-
process.exitCode = await runDrive(plan, opts)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
demo
|
|
152
|
-
.command('frames')
|
|
153
|
-
.description('Pull numbered still frames from a recorded video')
|
|
154
|
-
.argument('<video>', 'Video written by canon demo run')
|
|
155
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
156
|
-
.option(
|
|
157
|
-
'-o, --out <dir>',
|
|
158
|
-
'Directory to write frames into, default beside the video',
|
|
159
|
-
)
|
|
160
|
-
.option('--fps <n>', 'Frames extracted per second of video', '1')
|
|
161
|
-
.option('--json', 'Add a machine-readable record on stdout')
|
|
162
|
-
.addHelpText(
|
|
163
|
-
'after',
|
|
164
|
-
[
|
|
165
|
-
'',
|
|
166
|
-
'Needs ffmpeg on PATH, the same binary demo run already shells to for',
|
|
167
|
-
`mp4 and gif conversion. Install it with: ${INSTALL_CONVERTER}`,
|
|
168
|
-
'',
|
|
169
|
-
'Reasons:',
|
|
170
|
-
' video-missing no file at the video path',
|
|
171
|
-
' converter-missing ffmpeg is not on PATH, so nothing was extracted',
|
|
172
|
-
' extraction-failed ffmpeg exited non-zero, with its stderr in the message',
|
|
173
|
-
'',
|
|
174
|
-
'Exit codes:',
|
|
175
|
-
' 0 frames were written',
|
|
176
|
-
' 1 refused, with the reason on stderr',
|
|
177
|
-
'',
|
|
178
|
-
'Examples:',
|
|
179
|
-
' canon demo frames demos/inline-edit/take/inline-edit.webm',
|
|
180
|
-
' canon demo frames demos/inline-edit/take/inline-edit.webm --fps 2 --out frames',
|
|
181
|
-
'',
|
|
182
|
-
].join('\n'),
|
|
183
|
-
)
|
|
184
|
-
.action(async (video: string, opts: FramesOptions) => {
|
|
185
|
-
process.exitCode = await runFrames(video, opts)
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function runCompile(draftPath: string, opts: CompileOptions): number {
|
|
190
|
-
intro('canon demo compile')
|
|
191
|
-
|
|
192
|
-
const source = resolve(process.cwd(), draftPath)
|
|
193
|
-
if (!existsSync(source)) {
|
|
194
|
-
logStep('Draft')
|
|
195
|
-
logError(`${draftPath} not found`)
|
|
196
|
-
outro()
|
|
197
|
-
emit(opts.json, { draft: source, reason: 'draft-missing' })
|
|
198
|
-
return 1
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const parsed = parseDraft(readFileSync(source, 'utf8'))
|
|
202
|
-
if (parsed.status === 'failed') {
|
|
203
|
-
logStep('Draft')
|
|
204
|
-
logError(`${display(source)}: ${parsed.reason}`)
|
|
205
|
-
outro()
|
|
206
|
-
emit(opts.json, {
|
|
207
|
-
draft: source,
|
|
208
|
-
reason: 'draft-unreadable',
|
|
209
|
-
message: parsed.reason,
|
|
210
|
-
})
|
|
211
|
-
return 1
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const slug = opts.slug ?? basename(source, extname(source))
|
|
215
|
-
const target = resolve(process.cwd(), opts.out, slug, 'plan.json')
|
|
216
|
-
|
|
217
|
-
if (existsSync(target) && !opts.force) {
|
|
218
|
-
logStep('Plan')
|
|
219
|
-
logError(`${display(target)} already exists`)
|
|
220
|
-
// Stated rather than implied, because the value at risk is whatever the
|
|
221
|
-
// operator set by hand, such as timing tuned by watching a recording or a
|
|
222
|
-
// color scheme, and the draft can reproduce neither.
|
|
223
|
-
logWarn('Pass --force to overwrite it, losing any field set by hand.')
|
|
224
|
-
outro()
|
|
225
|
-
emit(opts.json, { plan: target, reason: 'plan-exists' })
|
|
226
|
-
return 1
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const plan = compilePlan(parsed.draft, { slug, outDir: opts.out })
|
|
230
|
-
mkdirSync(dirname(target), { recursive: true })
|
|
231
|
-
writeFileSync(target, `${JSON.stringify(plan, null, 2)}\n`)
|
|
232
|
-
|
|
233
|
-
logStep('Draft')
|
|
234
|
-
logInfo(`${display(source)} ${plural(parsed.draft.beats.length, 'beat')}`)
|
|
235
|
-
|
|
236
|
-
logStep('Plan')
|
|
237
|
-
logInfo(display(target))
|
|
238
|
-
|
|
239
|
-
const outstanding = unresolved(plan)
|
|
240
|
-
logStep('Outstanding')
|
|
241
|
-
if (outstanding.length === 0) {
|
|
242
|
-
logInfo('Nothing to fill, so the plan runs as written.')
|
|
243
|
-
} else {
|
|
244
|
-
logWarn(
|
|
245
|
-
`${plural(outstanding.length, 'field')} the draft could not supply:`,
|
|
246
|
-
)
|
|
247
|
-
for (const field of outstanding) logWarn(` ${field}`)
|
|
248
|
-
}
|
|
249
|
-
outro()
|
|
250
|
-
|
|
251
|
-
emit(opts.json, {
|
|
252
|
-
draft: source,
|
|
253
|
-
plan: target,
|
|
254
|
-
beats: parsed.draft.beats.length,
|
|
255
|
-
unresolved: outstanding,
|
|
256
|
-
})
|
|
257
|
-
return 0
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function runDrive(planPath: string, opts: RunOptions): Promise<number> {
|
|
261
|
-
intro('canon demo run')
|
|
262
|
-
|
|
263
|
-
const source = resolve(process.cwd(), planPath)
|
|
264
|
-
if (!existsSync(source)) {
|
|
265
|
-
logStep('Plan')
|
|
266
|
-
logError(`${planPath} not found`)
|
|
267
|
-
outro()
|
|
268
|
-
emit(opts.json, { plan: source, reason: 'plan-missing' })
|
|
269
|
-
return 1
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const parsed = parsePlan(readFileSync(source, 'utf8'))
|
|
273
|
-
if (parsed.status === 'failed') {
|
|
274
|
-
logStep('Plan')
|
|
275
|
-
logError(`${display(source)}: ${parsed.reason}`)
|
|
276
|
-
outro()
|
|
277
|
-
emit(opts.json, {
|
|
278
|
-
plan: source,
|
|
279
|
-
reason: 'plan-unreadable',
|
|
280
|
-
message: parsed.reason,
|
|
281
|
-
})
|
|
282
|
-
return 1
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (!opts.video && !opts.still) {
|
|
286
|
-
logStep('Output')
|
|
287
|
-
logError('--no-video and --no-still together leave nothing to write')
|
|
288
|
-
outro()
|
|
289
|
-
emit(opts.json, { plan: source, reason: 'no-output-requested' })
|
|
290
|
-
return 1
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const outstanding = unresolved(parsed.plan)
|
|
294
|
-
if (outstanding.length) {
|
|
295
|
-
logStep('Plan')
|
|
296
|
-
logError(
|
|
297
|
-
`${display(source)} has ${plural(outstanding.length, 'field')} to fill`,
|
|
298
|
-
)
|
|
299
|
-
for (const field of outstanding) logWarn(` ${field}`)
|
|
300
|
-
outro()
|
|
301
|
-
emit(opts.json, {
|
|
302
|
-
plan: source,
|
|
303
|
-
reason: 'plan-unresolved',
|
|
304
|
-
unresolved: outstanding,
|
|
305
|
-
})
|
|
306
|
-
return 1
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
logStep('Plan')
|
|
310
|
-
logInfo(`${display(source)} ${plural(parsed.plan.steps.length, 'step')}`)
|
|
311
|
-
|
|
312
|
-
const cursors = resolveCursors(opts.cursor)
|
|
313
|
-
if (cursors.status === 'failed') {
|
|
314
|
-
logError(cursors.reason)
|
|
315
|
-
outro()
|
|
316
|
-
emit(opts.json, { plan: source, reason: 'cursor-unreadable' })
|
|
317
|
-
return 1
|
|
318
|
-
}
|
|
319
|
-
logInfo(cursors.label)
|
|
320
|
-
|
|
321
|
-
const driver = await loadDriver()
|
|
322
|
-
if (!driver) {
|
|
323
|
-
logStep('Browser')
|
|
324
|
-
logError('the browser engine is not installed in this project')
|
|
325
|
-
logWarn(`Install it with: ${INSTALL_BROWSER}`)
|
|
326
|
-
outro()
|
|
327
|
-
emit(opts.json, {
|
|
328
|
-
plan: source,
|
|
329
|
-
reason: 'engine-missing',
|
|
330
|
-
install: INSTALL_BROWSER,
|
|
331
|
-
})
|
|
332
|
-
return 1
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
logStep('Recording')
|
|
336
|
-
const result = await driver.drive({
|
|
337
|
-
plan: parsed.plan,
|
|
338
|
-
cursors: cursors.value,
|
|
339
|
-
...(opts.video
|
|
340
|
-
? { videoPath: outputPath(parsed.plan.output.video, opts.out) }
|
|
341
|
-
: {}),
|
|
342
|
-
...(opts.still
|
|
343
|
-
? { stillPath: outputPath(parsed.plan.output.still, opts.out) }
|
|
344
|
-
: {}),
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
if (result.status === 'failed') {
|
|
348
|
-
logError(result.message.split('\n')[0] ?? 'the run failed')
|
|
349
|
-
if (result.reason === 'browser-missing') {
|
|
350
|
-
logWarn(`Install the browser binary with: ${INSTALL_BROWSER}`)
|
|
351
|
-
}
|
|
352
|
-
outro()
|
|
353
|
-
emit(opts.json, {
|
|
354
|
-
plan: source,
|
|
355
|
-
reason: result.reason,
|
|
356
|
-
message: result.message,
|
|
357
|
-
...(result.reason === 'browser-missing'
|
|
358
|
-
? { install: INSTALL_BROWSER }
|
|
359
|
-
: {}),
|
|
360
|
-
})
|
|
361
|
-
return 1
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (result.videoPath) logInfo(display(result.videoPath))
|
|
365
|
-
if (result.stillPath) logInfo(`${display(result.stillPath)} still`)
|
|
366
|
-
if (result.timelinePath) logInfo(`${display(result.timelinePath)} timeline`)
|
|
367
|
-
logInfo(
|
|
368
|
-
`${result.steps} steps in ${Math.round(result.durationMs / 100) / 10}s`,
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
let mp4Path: string | undefined
|
|
372
|
-
let mp4Reason: string | undefined
|
|
373
|
-
if (result.videoPath) {
|
|
374
|
-
logStep('Container')
|
|
375
|
-
const converted = await convertToMp4(result.videoPath)
|
|
376
|
-
if (converted.status === 'converted') {
|
|
377
|
-
mp4Path = converted.mp4Path
|
|
378
|
-
logInfo(display(mp4Path))
|
|
379
|
-
} else if (converted.status === 'skipped') {
|
|
380
|
-
mp4Reason = converted.reason
|
|
381
|
-
logWarn('ffmpeg is not installed, so no mp4 was written.')
|
|
382
|
-
logWarn(`Install it with: ${INSTALL_CONVERTER}`)
|
|
383
|
-
} else {
|
|
384
|
-
mp4Reason = converted.reason
|
|
385
|
-
logWarn(`mp4 conversion failed: ${converted.reason}`)
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
let gifPath: string | undefined
|
|
390
|
-
let gifReason: string | undefined
|
|
391
|
-
if (result.videoPath && opts.gif) {
|
|
392
|
-
const converted = await convertToGif(result.videoPath)
|
|
393
|
-
if (converted.status === 'converted') {
|
|
394
|
-
gifPath = converted.gifPath
|
|
395
|
-
logInfo(display(gifPath))
|
|
396
|
-
} else if (converted.status === 'skipped') {
|
|
397
|
-
gifReason = converted.reason
|
|
398
|
-
logWarn('ffmpeg is not installed, so no gif was written.')
|
|
399
|
-
logWarn(`Install it with: ${INSTALL_CONVERTER}`)
|
|
400
|
-
} else {
|
|
401
|
-
gifReason = converted.reason
|
|
402
|
-
logWarn(`gif conversion failed: ${converted.reason}`)
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
outro()
|
|
407
|
-
|
|
408
|
-
emit(opts.json, {
|
|
409
|
-
plan: source,
|
|
410
|
-
video: result.videoPath ?? null,
|
|
411
|
-
mp4: mp4Path ?? null,
|
|
412
|
-
gif: gifPath ?? null,
|
|
413
|
-
still: result.stillPath ?? null,
|
|
414
|
-
timeline: result.timelinePath ?? null,
|
|
415
|
-
steps: result.steps,
|
|
416
|
-
durationMs: result.durationMs,
|
|
417
|
-
...(mp4Reason ? { mp4Reason } : {}),
|
|
418
|
-
...(gifReason ? { gifReason } : {}),
|
|
419
|
-
})
|
|
420
|
-
return 0
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async function runFrames(
|
|
424
|
-
videoPath: string,
|
|
425
|
-
opts: FramesOptions,
|
|
426
|
-
): Promise<number> {
|
|
427
|
-
intro('canon demo frames')
|
|
428
|
-
|
|
429
|
-
const source = resolve(process.cwd(), videoPath)
|
|
430
|
-
if (!existsSync(source)) {
|
|
431
|
-
logStep('Video')
|
|
432
|
-
logError(`${videoPath} not found`)
|
|
433
|
-
outro()
|
|
434
|
-
emit(opts.json, { video: source, reason: 'video-missing' })
|
|
435
|
-
return 1
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
logStep('Video')
|
|
439
|
-
logInfo(display(source))
|
|
440
|
-
|
|
441
|
-
const outDir = opts.out ? resolve(process.cwd(), opts.out) : undefined
|
|
442
|
-
const fps = Number(opts.fps)
|
|
443
|
-
|
|
444
|
-
logStep('Frames')
|
|
445
|
-
const extracted = await extractFrames(source, outDir, { fps })
|
|
446
|
-
if (extracted.status === 'skipped') {
|
|
447
|
-
logError('ffmpeg is not installed, so no frames were written.')
|
|
448
|
-
logWarn(`Install it with: ${INSTALL_CONVERTER}`)
|
|
449
|
-
outro()
|
|
450
|
-
emit(opts.json, { video: source, reason: extracted.reason })
|
|
451
|
-
return 1
|
|
452
|
-
}
|
|
453
|
-
if (extracted.status === 'failed') {
|
|
454
|
-
logError(`frame extraction failed: ${extracted.reason}`)
|
|
455
|
-
outro()
|
|
456
|
-
emit(opts.json, {
|
|
457
|
-
video: source,
|
|
458
|
-
reason: 'extraction-failed',
|
|
459
|
-
message: extracted.reason,
|
|
460
|
-
})
|
|
461
|
-
return 1
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
for (const framePath of extracted.framePaths) logInfo(display(framePath))
|
|
465
|
-
logInfo(`${plural(extracted.framePaths.length, 'frame')} at ${fps} fps`)
|
|
466
|
-
outro()
|
|
467
|
-
|
|
468
|
-
emit(opts.json, {
|
|
469
|
-
video: source,
|
|
470
|
-
frames: extracted.framePaths,
|
|
471
|
-
fps,
|
|
472
|
-
})
|
|
473
|
-
return 0
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
type CursorChoice =
|
|
477
|
-
| { status: 'ready'; value: typeof DEFAULT_CURSORS; label: string }
|
|
478
|
-
| { status: 'failed'; reason: string }
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* A theme contributes per state rather than per folder, so a folder carrying an
|
|
482
|
-
* arrow and no hand still supplies its arrow and the bundled artwork covers the
|
|
483
|
-
* rest.
|
|
484
|
-
*/
|
|
485
|
-
function resolveCursors(dir: string | undefined): CursorChoice {
|
|
486
|
-
if (!dir) {
|
|
487
|
-
return {
|
|
488
|
-
status: 'ready',
|
|
489
|
-
value: DEFAULT_CURSORS,
|
|
490
|
-
label: 'pointer drawn from the bundled artwork',
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const loaded = loadCursorTheme(resolve(process.cwd(), dir), DEFAULT_CURSORS)
|
|
495
|
-
if (loaded.status === 'failed')
|
|
496
|
-
return { status: 'failed', reason: loaded.reason }
|
|
497
|
-
|
|
498
|
-
return {
|
|
499
|
-
status: 'ready',
|
|
500
|
-
value: loaded.cursors,
|
|
501
|
-
label: `pointer drawn from ${display(resolve(process.cwd(), dir))} for ${loaded.states.join(', ')}`,
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* Reports absence only when the module or its engine cannot be resolved, which
|
|
507
|
-
* is the case a target hits before installing the browser package. Any other
|
|
508
|
-
* import failure is a defect inside the driver and propagates, rather than
|
|
509
|
-
* being reported as a missing dependency.
|
|
510
|
-
*/
|
|
511
|
-
async function loadDriver(): Promise<Driver | undefined> {
|
|
512
|
-
try {
|
|
513
|
-
return await import('@/demo/drive')
|
|
514
|
-
} catch (error) {
|
|
515
|
-
if (isEngineMissing(error)) return undefined
|
|
516
|
-
throw error
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Resolves where one artifact lands. `--out` replaces the directory the plan
|
|
522
|
-
* names rather than acting as a root the plan's own directory hangs off, which
|
|
523
|
-
* would nest the output path inside itself on every run that passes both.
|
|
524
|
-
*/
|
|
525
|
-
function outputPath(planned: string, out: string | undefined): string {
|
|
526
|
-
const relativePath = out ? join(out, basename(planned)) : planned
|
|
527
|
-
return resolve(process.cwd(), relativePath)
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function emit(json: boolean | undefined, record: unknown): void {
|
|
531
|
-
if (json) process.stdout.write(`${JSON.stringify(record)}\n`)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Keeps a path clickable in the operator's terminal. A path outside the project
|
|
536
|
-
* reports absolute, since a relative path to it is a run of `..` segments no
|
|
537
|
-
* editor resolves.
|
|
538
|
-
*/
|
|
539
|
-
function display(path: string): string {
|
|
540
|
-
const fromCwd = relative(process.cwd(), path)
|
|
541
|
-
return fromCwd.startsWith('..') ? path : fromCwd
|
|
542
|
-
}
|