@erclx/canon 5.52.0 → 5.53.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 (127) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -4
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +0 -3
  5. package/claude/skills/context-fold/REQUIREMENT.md +0 -1
  6. package/claude/skills/design-taste/references/kinds.md +0 -1
  7. package/claude/skills/draw-figure/REQUIREMENT.md +2 -3
  8. package/claude/skills/draw-figure/SKILL.md +4 -5
  9. package/claude/skills/draw-figure/references/figures.md +0 -1
  10. package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
  11. package/claude/skills/pick-by-eye/REQUIREMENT.md +0 -1
  12. package/claude/skills/pick-by-eye/SKILL.md +1 -1
  13. package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
  14. package/claude/skills/teach-craft/SKILL.md +39 -0
  15. package/claude/skills/teach-craft/references/lesson-craft.md +52 -0
  16. package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +12 -4
  17. package/docs/agents/audits.md +0 -1
  18. package/docs/target/migrations.md +10 -1
  19. package/docs/workflow/parallel-features.md +1 -1
  20. package/docs/workflow/skill-map-on-request.md +10 -22
  21. package/docs/workflow/skill-map.md +12 -14
  22. package/package.json +1 -4
  23. package/scripts/core/regen-web-previews.ts +2 -67
  24. package/src/audits/catalog.ts +0 -1
  25. package/src/browser/engine.ts +3 -6
  26. package/src/capture/render.ts +1 -1
  27. package/src/claude/cases/authoring.ts +0 -12
  28. package/src/claude/cases/misc.ts +0 -17
  29. package/src/claude/cases/workflow.ts +2 -38
  30. package/src/claude/skills-audit.ts +0 -1
  31. package/src/cli/help.ts +0 -7
  32. package/src/cli.ts +0 -6
  33. package/src/commands/records.ts +0 -1
  34. package/src/commands/serve.ts +2 -2
  35. package/src/design/components.ts +8 -1514
  36. package/src/design/css.ts +5 -6
  37. package/src/design/fonts.ts +3 -5
  38. package/src/driver/drive.ts +3 -4
  39. package/src/driver/steps.ts +2 -5
  40. package/src/inventory/walk.ts +1 -1
  41. package/src/markdown/scope.ts +4 -31
  42. package/src/records/validate.ts +4 -176
  43. package/src/sandbox/equivalence.ts +0 -5
  44. package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
  45. package/claude/skills/draft-and-pick/SKILL.md +0 -11
  46. package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
  47. package/claude/skills/draft-doc/SKILL.md +0 -11
  48. package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
  49. package/claude/skills/draft-figure/SKILL.md +0 -11
  50. package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
  51. package/claude/skills/draft-identity/SKILL.md +0 -11
  52. package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
  53. package/claude/skills/draft-ready/SKILL.md +0 -11
  54. package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
  55. package/claude/skills/draft-screencast/SKILL.md +0 -11
  56. package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
  57. package/claude/skills/draft-slides/SKILL.md +0 -11
  58. package/claude/skills/make-deck/REQUIREMENT.md +0 -36
  59. package/claude/skills/make-deck/SKILL.md +0 -80
  60. package/claude/skills/make-deck/references/deck.md +0 -183
  61. package/claude/skills/read-frames/REQUIREMENT.md +0 -30
  62. package/claude/skills/read-frames/SKILL.md +0 -55
  63. package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
  64. package/claude/skills/record-screencast/SKILL.md +0 -99
  65. package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
  66. package/claude/skills/script-screencast/SKILL.md +0 -70
  67. package/claude/skills/script-screencast/references/beats-template.md +0 -119
  68. package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
  69. package/claude/skills/teach-workspace/SKILL.md +0 -196
  70. package/claude/skills/teach-workspace/references/glossary.md +0 -90
  71. package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
  72. package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
  73. package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
  74. package/claude/skills/teach-workspace/references/promotion.md +0 -116
  75. package/claude/skills/teach-workspace/references/revisit.md +0 -28
  76. package/claude/skills/teach-workspace/references/teach.md +0 -211
  77. package/claude/skills/video-craft/REQUIREMENT.md +0 -35
  78. package/claude/skills/video-craft/SKILL.md +0 -96
  79. package/claude/skills/video-craft/references/adopted.md +0 -110
  80. package/src/commands/demo.ts +0 -542
  81. package/src/commands/slides.ts +0 -184
  82. package/src/commands/teach.ts +0 -1401
  83. package/src/demo/beats.ts +0 -135
  84. package/src/demo/compile.ts +0 -350
  85. package/src/demo/container.ts +0 -219
  86. package/src/demo/cursors.ts +0 -55
  87. package/src/demo/drive.ts +0 -442
  88. package/src/demo/pointer.ts +0 -178
  89. package/src/demo/theme.ts +0 -112
  90. package/src/demo/timeline.ts +0 -64
  91. package/src/slides/convert/chart.ts +0 -165
  92. package/src/slides/convert/deck.ts +0 -244
  93. package/src/slides/convert/export.ts +0 -567
  94. package/src/slides/convert/master.ts +0 -198
  95. package/src/slides/convert/shapes.ts +0 -804
  96. package/src/slides/convert/svg.ts +0 -38
  97. package/src/slides/convert/walk.ts +0 -671
  98. package/src/slides/decks.ts +0 -110
  99. package/src/slides/open.ts +0 -18
  100. package/src/slides/package/fonts.ts +0 -301
  101. package/src/slides/package/motion.ts +0 -316
  102. package/src/slides/package/test-face.ts +0 -113
  103. package/src/teach/browser/compile.ts +0 -44
  104. package/src/teach/browser/dismiss.ts +0 -76
  105. package/src/teach/browser/focus-line.ts +0 -19
  106. package/src/teach/browser/glossary-filter.ts +0 -44
  107. package/src/teach/browser/head.ts +0 -30
  108. package/src/teach/browser/quiz.ts +0 -19
  109. package/src/teach/browser/reload.ts +0 -9
  110. package/src/teach/browser/sidebar.ts +0 -233
  111. package/src/teach/browser/theme.ts +0 -18
  112. package/src/teach/components/code.tsx +0 -24
  113. package/src/teach/components/heading.tsx +0 -11
  114. package/src/teach/components/list.tsx +0 -17
  115. package/src/teach/components/paragraph.tsx +0 -29
  116. package/src/teach/components/refs.tsx +0 -28
  117. package/src/teach/cross-references.ts +0 -78
  118. package/src/teach/fonts.ts +0 -16
  119. package/src/teach/highlight.ts +0 -71
  120. package/src/teach/html/jsx-dev-runtime.ts +0 -13
  121. package/src/teach/html/jsx-runtime.ts +0 -113
  122. package/src/teach/html/raw.ts +0 -28
  123. package/src/teach/lesson.ts +0 -178
  124. package/src/teach/nav.ts +0 -1337
  125. package/src/teach/render.ts +0 -240
  126. package/src/teach/workspace.ts +0 -1164
  127. 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
- }