@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.
Files changed (201) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +11 -13
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +5 -8
  5. package/claude/skills/context-fold/REQUIREMENT.md +0 -1
  6. package/claude/skills/design-identity/REQUIREMENT.md +2 -2
  7. package/claude/skills/design-identity/SKILL.md +6 -6
  8. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  9. package/claude/skills/design-record/SKILL.md +4 -4
  10. package/claude/skills/design-record/references/folder.md +1 -1
  11. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  12. package/claude/skills/design-taste/references/kinds.md +0 -1
  13. package/claude/skills/draw-figure/REQUIREMENT.md +3 -4
  14. package/claude/skills/draw-figure/SKILL.md +5 -6
  15. package/claude/skills/draw-figure/references/figures.md +0 -1
  16. package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
  17. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -5
  18. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  19. package/claude/skills/pick-by-eye/references/board.md +42 -0
  20. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  21. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  22. package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
  23. package/claude/skills/teach-craft/SKILL.md +70 -0
  24. package/claude/skills/teach-craft/references/adopted.md +113 -0
  25. package/claude/skills/teach-craft/references/lesson-craft.md +60 -0
  26. package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +41 -6
  27. package/docs/agents/audits.md +0 -1
  28. package/docs/agents/install-and-sync.md +0 -8
  29. package/docs/target/migrations.md +16 -2
  30. package/docs/target/projects.md +1 -1
  31. package/docs/workflow/parallel-features.md +1 -1
  32. package/docs/workflow/skill-map-on-request.md +10 -22
  33. package/docs/workflow/skill-map.md +11 -14
  34. package/docs/workflow/visual-design-workflow.md +1 -4
  35. package/governance/detect.toml +1 -6
  36. package/governance/renames.toml +0 -1
  37. package/package.json +2 -8
  38. package/scripts/core/regen-web-previews.ts +2 -67
  39. package/src/audits/catalog.ts +0 -1
  40. package/src/browser/engine.ts +3 -6
  41. package/src/capture/render.ts +1 -1
  42. package/src/claude/cases/authoring.ts +0 -12
  43. package/src/claude/cases/misc.ts +0 -22
  44. package/src/claude/cases/workflow.ts +2 -38
  45. package/src/claude/skills-audit.ts +1 -1
  46. package/src/cli/help.ts +0 -10
  47. package/src/cli.ts +0 -8
  48. package/src/commands/design.ts +16 -315
  49. package/src/commands/records.ts +0 -1
  50. package/src/commands/serve.ts +2 -2
  51. package/src/commands/sync.ts +4 -5
  52. package/src/design/components.ts +8 -1514
  53. package/src/design/css.ts +5 -6
  54. package/src/design/fonts.ts +3 -5
  55. package/src/design/regen.ts +5 -32
  56. package/src/design/render.ts +1 -1
  57. package/src/design/tokens.ts +3 -2
  58. package/src/driver/drive.ts +3 -4
  59. package/src/driver/steps.ts +2 -5
  60. package/src/gate/stages.ts +4 -10
  61. package/src/gov/audit.ts +1 -6
  62. package/src/inventory/walk.ts +1 -1
  63. package/src/markdown/scope.ts +6 -35
  64. package/src/records/validate.ts +4 -176
  65. package/src/sandbox/equivalence.ts +0 -5
  66. package/src/shipped/references.ts +1 -2
  67. package/src/sync/check.ts +4 -29
  68. package/src/sync/stamp.ts +6 -6
  69. package/src/sync/target.ts +1 -4
  70. package/tooling/claude/reference.md +1 -1
  71. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  72. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  73. package/claude/skills/canvas-board/SKILL.md +0 -143
  74. package/claude/skills/canvas-board/references/canvas.md +0 -170
  75. package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
  76. package/claude/skills/draft-and-pick/SKILL.md +0 -11
  77. package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
  78. package/claude/skills/draft-doc/SKILL.md +0 -11
  79. package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
  80. package/claude/skills/draft-figure/SKILL.md +0 -11
  81. package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
  82. package/claude/skills/draft-identity/SKILL.md +0 -11
  83. package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
  84. package/claude/skills/draft-ready/SKILL.md +0 -11
  85. package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
  86. package/claude/skills/draft-screencast/SKILL.md +0 -11
  87. package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
  88. package/claude/skills/draft-slides/SKILL.md +0 -11
  89. package/claude/skills/make-deck/REQUIREMENT.md +0 -36
  90. package/claude/skills/make-deck/SKILL.md +0 -80
  91. package/claude/skills/make-deck/references/deck.md +0 -183
  92. package/claude/skills/read-frames/REQUIREMENT.md +0 -30
  93. package/claude/skills/read-frames/SKILL.md +0 -55
  94. package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
  95. package/claude/skills/record-screencast/SKILL.md +0 -99
  96. package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
  97. package/claude/skills/script-screencast/SKILL.md +0 -70
  98. package/claude/skills/script-screencast/references/beats-template.md +0 -119
  99. package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
  100. package/claude/skills/teach-workspace/SKILL.md +0 -196
  101. package/claude/skills/teach-workspace/references/glossary.md +0 -90
  102. package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
  103. package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
  104. package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
  105. package/claude/skills/teach-workspace/references/promotion.md +0 -116
  106. package/claude/skills/teach-workspace/references/revisit.md +0 -28
  107. package/claude/skills/teach-workspace/references/teach.md +0 -211
  108. package/claude/skills/video-craft/REQUIREMENT.md +0 -35
  109. package/claude/skills/video-craft/SKILL.md +0 -96
  110. package/claude/skills/video-craft/references/adopted.md +0 -110
  111. package/governance/rules/ui/canvas.md +0 -13
  112. package/src/canvas/address.ts +0 -227
  113. package/src/canvas/arrange.ts +0 -66
  114. package/src/canvas/capture.ts +0 -391
  115. package/src/canvas/client/app.tsx +0 -188
  116. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  117. package/src/canvas/client/inspector/color-field.tsx +0 -398
  118. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  119. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  120. package/src/canvas/client/inspector/color-space.ts +0 -240
  121. package/src/canvas/client/inspector/field.tsx +0 -263
  122. package/src/canvas/client/inspector/fields.css +0 -763
  123. package/src/canvas/client/inspector/section.tsx +0 -27
  124. package/src/canvas/client/inspector/size-field.tsx +0 -69
  125. package/src/canvas/client/inspector/token-select.tsx +0 -109
  126. package/src/canvas/client/inspector/values.ts +0 -297
  127. package/src/canvas/client/inspector.tsx +0 -692
  128. package/src/canvas/client/keys.ts +0 -71
  129. package/src/canvas/client/layers.tsx +0 -118
  130. package/src/canvas/client/main.tsx +0 -71
  131. package/src/canvas/client/pages-panel.tsx +0 -212
  132. package/src/canvas/client/panel-handle.tsx +0 -90
  133. package/src/canvas/client/selection.tsx +0 -199
  134. package/src/canvas/client/shell.css +0 -762
  135. package/src/canvas/client/state.ts +0 -792
  136. package/src/canvas/client/surface.tsx +0 -803
  137. package/src/canvas/client/theme-panel.tsx +0 -55
  138. package/src/canvas/client/tools.tsx +0 -153
  139. package/src/canvas/content.ts +0 -781
  140. package/src/canvas/edit.ts +0 -575
  141. package/src/canvas/editing.ts +0 -203
  142. package/src/canvas/fit.ts +0 -238
  143. package/src/canvas/global.d.ts +0 -15
  144. package/src/canvas/history.ts +0 -116
  145. package/src/canvas/index.html +0 -20
  146. package/src/canvas/server.ts +0 -766
  147. package/src/canvas/tokens.ts +0 -221
  148. package/src/commands/canvas.ts +0 -1097
  149. package/src/commands/demo.ts +0 -542
  150. package/src/commands/slides.ts +0 -184
  151. package/src/commands/teach.ts +0 -1401
  152. package/src/demo/beats.ts +0 -135
  153. package/src/demo/compile.ts +0 -350
  154. package/src/demo/container.ts +0 -219
  155. package/src/demo/cursors.ts +0 -55
  156. package/src/demo/drive.ts +0 -442
  157. package/src/demo/pointer.ts +0 -178
  158. package/src/demo/theme.ts +0 -112
  159. package/src/demo/timeline.ts +0 -64
  160. package/src/design/adapter.ts +0 -59
  161. package/src/design/base.css +0 -203
  162. package/src/design/brand-favicon.ts +0 -18
  163. package/src/design/neutral.ts +0 -127
  164. package/src/design/project-fonts.ts +0 -392
  165. package/src/slides/convert/chart.ts +0 -165
  166. package/src/slides/convert/deck.ts +0 -244
  167. package/src/slides/convert/export.ts +0 -567
  168. package/src/slides/convert/master.ts +0 -198
  169. package/src/slides/convert/shapes.ts +0 -804
  170. package/src/slides/convert/svg.ts +0 -38
  171. package/src/slides/convert/walk.ts +0 -671
  172. package/src/slides/decks.ts +0 -110
  173. package/src/slides/open.ts +0 -18
  174. package/src/slides/package/fonts.ts +0 -301
  175. package/src/slides/package/motion.ts +0 -316
  176. package/src/slides/package/test-face.ts +0 -113
  177. package/src/teach/browser/compile.ts +0 -44
  178. package/src/teach/browser/dismiss.ts +0 -76
  179. package/src/teach/browser/focus-line.ts +0 -19
  180. package/src/teach/browser/glossary-filter.ts +0 -44
  181. package/src/teach/browser/head.ts +0 -30
  182. package/src/teach/browser/quiz.ts +0 -19
  183. package/src/teach/browser/reload.ts +0 -9
  184. package/src/teach/browser/sidebar.ts +0 -233
  185. package/src/teach/browser/theme.ts +0 -18
  186. package/src/teach/components/code.tsx +0 -24
  187. package/src/teach/components/heading.tsx +0 -11
  188. package/src/teach/components/list.tsx +0 -17
  189. package/src/teach/components/paragraph.tsx +0 -29
  190. package/src/teach/components/refs.tsx +0 -28
  191. package/src/teach/cross-references.ts +0 -78
  192. package/src/teach/fonts.ts +0 -16
  193. package/src/teach/highlight.ts +0 -71
  194. package/src/teach/html/jsx-dev-runtime.ts +0 -13
  195. package/src/teach/html/jsx-runtime.ts +0 -113
  196. package/src/teach/html/raw.ts +0 -28
  197. package/src/teach/lesson.ts +0 -178
  198. package/src/teach/nav.ts +0 -1337
  199. package/src/teach/render.ts +0 -240
  200. package/src/teach/workspace.ts +0 -1164
  201. package/src/teach/write-if-changed.ts +0 -17
@@ -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
- }