@erclx/canon 5.24.0 → 5.25.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 (80) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
  3. package/claude/skills/backlog-triage/SKILL.md +19 -6
  4. package/claude/skills/canvas/REQUIREMENT.md +49 -0
  5. package/claude/skills/canvas/SKILL.md +106 -0
  6. package/claude/skills/design-extract/REQUIREMENT.md +1 -1
  7. package/claude/skills/draft-doc/REQUIREMENT.md +1 -1
  8. package/claude/skills/draft-figure/REQUIREMENT.md +14 -3
  9. package/claude/skills/draft-figure/SKILL.md +24 -51
  10. package/claude/skills/draft-figure/references/architecture-set.md +35 -0
  11. package/claude/skills/draft-figure/references/hand-drawn.md +31 -0
  12. package/claude/skills/draft-figure/references/mermaid.md +55 -0
  13. package/claude/skills/git-followup/SKILL.md +17 -6
  14. package/claude/skills/git-pr/references/evidence.md +4 -2
  15. package/claude/skills/git-split/SKILL.md +1 -1
  16. package/claude/skills/index-lookup/SKILL.md +2 -2
  17. package/claude/skills/review-address/SKILL.md +6 -13
  18. package/claude/skills/review-address/references/rest-reads.md +43 -0
  19. package/claude/skills/review-craft/references/ui.md +7 -1
  20. package/claude/skills/review-pr/REQUIREMENT.md +1 -0
  21. package/claude/skills/review-pr/SKILL.md +9 -3
  22. package/claude/skills/review-pr/references/close-out.md +2 -2
  23. package/claude/skills/review-pr/references/post.md +17 -5
  24. package/claude/skills/review-pr/references/scope.md +10 -8
  25. package/claude/skills/review-ui/SKILL.md +12 -4
  26. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +30 -1
  27. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +4 -0
  28. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +4 -4
  29. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +6 -0
  30. package/claude/skills/role-orchestrator/scripts/poll.ts +90 -27
  31. package/claude/skills/role-orchestrator/scripts/repo.ts +3 -5
  32. package/claude/skills/role-orchestrator/scripts/watch.ts +4 -7
  33. package/docs/agents/canvas.md +1 -1
  34. package/docs/agents/commands.md +45 -45
  35. package/docs/agents/context-audit-checks.md +2 -2
  36. package/docs/agents/index.md +0 -1
  37. package/docs/agents/key-changes.md +3 -3
  38. package/docs/agents/pr-evidence.md +11 -2
  39. package/docs/agents/pr-reads.md +5 -4
  40. package/docs/target/projects.md +4 -4
  41. package/docs/workflow/ai-workflow.md +0 -1
  42. package/docs/workflow/skill-map.md +3 -3
  43. package/docs/workflow/visual-design-workflow.md +1 -0
  44. package/governance/renames.toml +0 -4
  45. package/governance/rules/standards/markdown.md +1 -0
  46. package/package.json +1 -2
  47. package/scripts/core/install-check.sh +1 -1
  48. package/src/audits/catalog.ts +2 -2
  49. package/src/canvas/client/inspector/field.tsx +200 -0
  50. package/src/canvas/client/inspector/fields.css +151 -0
  51. package/src/canvas/client/inspector/section.tsx +27 -0
  52. package/src/canvas/client/inspector/values.ts +60 -0
  53. package/src/canvas/client/inspector.tsx +154 -115
  54. package/src/canvas/client/shell.css +0 -101
  55. package/src/claude/cases/misc.ts +5 -0
  56. package/src/claude/cases/workflow.ts +1 -1
  57. package/src/claude/seeds.ts +0 -1
  58. package/src/commands/design.ts +2 -64
  59. package/src/commands/pr.ts +219 -202
  60. package/src/context/audit.ts +7 -6
  61. package/src/help.ts +2 -2
  62. package/src/migrate/skill-names.ts +7 -1
  63. package/src/pr/evidence.ts +25 -5
  64. package/src/pr/rest.ts +137 -0
  65. package/standards/canvas.md +112 -0
  66. package/standards/context.md +2 -1
  67. package/standards/figures.md +0 -1
  68. package/standards/index.md +1 -1
  69. package/standards/markdown.md +1 -0
  70. package/standards/mermaid.md +1 -7
  71. package/tooling/claude/reference.md +0 -5
  72. package/claude/skills/draft-diagram/REQUIREMENT.md +0 -45
  73. package/claude/skills/draft-diagram/SKILL.md +0 -177
  74. package/docs/agents/design-board.md +0 -33
  75. package/governance/rules/standards/diagrams.md +0 -14
  76. package/governance/rules/standards/mermaid.md +0 -15
  77. package/scripts/core/check-gallery-exclusion.sh +0 -50
  78. package/src/design/board.ts +0 -576
  79. package/standards/diagrams.md +0 -132
  80. package/tooling/claude/seeds/.claude/diagrams/index.md +0 -8
@@ -1,6 +1,17 @@
1
1
  /** @jsxImportSource preact */
2
2
  import type { JSX } from 'preact'
3
3
  import { addressOf, elementAt, excerpt, isRawText } from '@/canvas/address'
4
+ import {
5
+ Field,
6
+ ReadOnlyField,
7
+ type Scrub,
8
+ } from '@/canvas/client/inspector/field'
9
+ import { Section } from '@/canvas/client/inspector/section'
10
+ import {
11
+ clampScrub,
12
+ displayValue,
13
+ toCssValue,
14
+ } from '@/canvas/client/inspector/values'
4
15
  import {
5
16
  currentPage,
6
17
  editElement,
@@ -16,8 +27,7 @@ import {
16
27
  tokens,
17
28
  writeError,
18
29
  } from '@/canvas/client/state'
19
-
20
- const FIELDS = ['x', 'y', 'width', 'height'] as const
30
+ import '@/canvas/client/inspector/fields.css'
21
31
 
22
32
  /** Elements the parser never gives content, so they hold no text to edit. */
23
33
  const VOID = new Set([
@@ -37,44 +47,43 @@ const VOID = new Set([
37
47
  ])
38
48
 
39
49
  interface StyleField {
50
+ /** The full word, which names the field. */
40
51
  readonly label: string
52
+ readonly glyph: string
41
53
  readonly property: string
42
- readonly isColor?: boolean
54
+ readonly isWide?: boolean
55
+ readonly placeholder?: string
43
56
  }
44
57
 
45
58
  /**
46
- * The basic set, in the order the panel lists it. The writer refuses any
47
- * property outside it, so this list and the writer's have to agree.
59
+ * The basic set, grouped as the panel lists it. The writer refuses any
60
+ * property outside it, so these lists and the writer's have to agree.
48
61
  */
49
- const STYLE_FIELDS: readonly StyleField[] = [
50
- { label: 'color', property: 'color', isColor: true },
51
- { label: 'background', property: 'background-color', isColor: true },
52
- { label: 'size', property: 'font-size' },
53
- { label: 'weight', property: 'font-weight' },
54
- { label: 'width', property: 'width' },
55
- { label: 'height', property: 'height' },
56
- { label: 'padding', property: 'padding' },
57
- { label: 'gap', property: 'gap' },
58
- { label: 'direction', property: 'flex-direction' },
62
+ const SIZE_FIELDS: readonly StyleField[] = [
63
+ { label: 'width', glyph: 'W', property: 'width' },
64
+ { label: 'height', glyph: 'H', property: 'height' },
59
65
  ]
60
66
 
61
- interface Row {
62
- readonly name: string
63
- readonly value: string
64
- }
67
+ const FLEX_FIELDS: readonly StyleField[] = [
68
+ { label: 'direction', glyph: 'Dir', property: 'flex-direction' },
69
+ { label: 'gap', glyph: 'Gap', property: 'gap', placeholder: '0' },
70
+ { label: 'padding', glyph: 'Pad', property: 'padding', isWide: true },
71
+ ]
65
72
 
66
- /** Where the element sits in its frame, which the panel shows and never edits. */
67
- function positionRows(element: Element): Row[] {
68
- const rect = element.getBoundingClientRect()
69
- const font = element.ownerDocument.defaultView
70
- ?.getComputedStyle(element)
71
- .getPropertyValue('font-family')
72
- return [
73
- { name: 'x', value: String(Math.round(rect.left)) },
74
- { name: 'y', value: String(Math.round(rect.top)) },
75
- ...(font ? [{ name: 'font', value: font }] : []),
76
- ]
77
- }
73
+ const TYPE_FIELDS: readonly StyleField[] = [
74
+ { label: 'size', glyph: 'Size', property: 'font-size' },
75
+ { label: 'weight', glyph: 'Wt', property: 'font-weight' },
76
+ ]
77
+
78
+ const FILL_FIELDS: readonly StyleField[] = [
79
+ { label: 'color', glyph: 'Fg', property: 'color' },
80
+ {
81
+ label: 'background',
82
+ glyph: 'Bg',
83
+ property: 'background-color',
84
+ placeholder: 'None',
85
+ },
86
+ ]
78
87
 
79
88
  function inlineValue(element: Element, property: string): string {
80
89
  return 'style' in element
@@ -137,44 +146,19 @@ function elementName(element: Element): string {
137
146
  return [tag, ...element.classList].join('.')
138
147
  }
139
148
 
140
- interface FieldProps {
149
+ interface TokenPickerProps {
141
150
  readonly label: string
142
151
  readonly initial: string
143
152
  readonly isBusy: boolean
144
153
  readonly onCommit: (value: string) => void
145
154
  }
146
155
 
147
- /**
148
- * Commits on change, which a text input fires on Enter or on leaving it, and
149
- * sends nothing when the value is what it started at. Escape puts it back.
150
- */
151
- function Field({ label, initial, isBusy, onCommit }: FieldProps): JSX.Element {
152
- return (
153
- <input
154
- class="field"
155
- type="text"
156
- aria-label={label}
157
- title={initial}
158
- defaultValue={initial}
159
- disabled={isBusy}
160
- spellcheck={false}
161
- onChange={(event) => {
162
- const value = event.currentTarget.value.trim()
163
- if (value !== initial.trim()) onCommit(value)
164
- }}
165
- onKeyDown={(event) => {
166
- if (event.key === 'Escape') event.currentTarget.value = initial
167
- }}
168
- />
169
- )
170
- }
171
-
172
156
  function TokenPicker({
173
157
  label,
174
158
  initial,
175
159
  isBusy,
176
160
  onCommit,
177
- }: FieldProps): JSX.Element | null {
161
+ }: TokenPickerProps): JSX.Element | null {
178
162
  const colors =
179
163
  tokens.value?.groups?.find((group) => group.kind === 'color')?.tokens ?? []
180
164
  if (colors.length === 0) return null
@@ -219,45 +203,80 @@ function ElementFields({
219
203
  const commit = (property: string) => (value: string) => {
220
204
  if (address) void editElement(frameRef, key, address, property, value)
221
205
  }
222
- const isTextOnly = holdsTextAlone(node)
206
+ const rect = node.getBoundingClientRect()
207
+ const font = currentValue(node, 'font-family')
223
208
  const text = node.textContent ?? ''
224
209
 
210
+ /**
211
+ * Previews into the frame's own inline style and writes through the same
212
+ * edit as typing. A reload mid-drag replaces the document, so a release
213
+ * against one no longer on screen posts nothing.
214
+ */
215
+ const scrubOf = (property: string): Scrub => ({
216
+ clamp: (value) => clampScrub(property, value),
217
+ begin: () => {
218
+ const original = inlineValue(node, property)
219
+ const style = (node as HTMLElement).style
220
+ return {
221
+ preview: (shown) =>
222
+ style.setProperty(property, toCssValue(property, shown)),
223
+ restore: () =>
224
+ original
225
+ ? style.setProperty(property, original)
226
+ : style.removeProperty(property),
227
+ commit: (shown) => {
228
+ if (frameDocuments.value.get(key) !== doc || !node.isConnected) return
229
+ commit(property)(toCssValue(property, shown))
230
+ },
231
+ }
232
+ },
233
+ })
234
+
235
+ const styleField = (field: StyleField): JSX.Element => (
236
+ <Field
237
+ key={field.property}
238
+ label={field.label}
239
+ glyph={field.glyph}
240
+ initial={displayValue(field.property, currentValue(node, field.property))}
241
+ isBusy={isBusy}
242
+ isWide={field.isWide}
243
+ placeholder={field.placeholder}
244
+ onCommit={(typed) =>
245
+ commit(field.property)(toCssValue(field.property, typed))
246
+ }
247
+ scrub={'style' in node ? scrubOf(field.property) : undefined}
248
+ />
249
+ )
250
+
225
251
  return (
226
- <dl class="box styles">
227
- {positionRows(node).map((row) => (
228
- <div key={row.name} class="box-row">
229
- <dt>{row.name}</dt>
230
- <dd title={row.value}>{row.value}</dd>
231
- </div>
232
- ))}
233
- <div class="box-row">
234
- <dt>text</dt>
235
- <dd title={excerpt(text)}>
236
- {isTextOnly ? (
237
- <Field
238
- label="text"
239
- initial={text}
240
- isBusy={isBusy}
241
- onCommit={commit('text')}
242
- />
243
- ) : (
244
- excerpt(text)
245
- )}
246
- </dd>
247
- </div>
248
- {STYLE_FIELDS.map((field) => {
249
- const initial = currentValue(node, field.property)
250
- return (
251
- <div key={field.property} class="box-row">
252
- <dt>{field.label}</dt>
253
- <dd class="field-cell">
254
- <Field
255
- label={field.label}
256
- initial={initial}
257
- isBusy={isBusy}
258
- onCommit={commit(field.property)}
259
- />
260
- {field.isColor && isRawInline(node, field.property) ? (
252
+ <>
253
+ <Section title="Layout">
254
+ <ReadOnlyField
255
+ label="x"
256
+ glyph="X"
257
+ value={String(Math.round(rect.left))}
258
+ />
259
+ <ReadOnlyField
260
+ label="y"
261
+ glyph="Y"
262
+ value={String(Math.round(rect.top))}
263
+ />
264
+ {SIZE_FIELDS.map(styleField)}
265
+ </Section>
266
+ <Section title="Flex">{FLEX_FIELDS.map(styleField)}</Section>
267
+ <Section title="Typography">
268
+ {TYPE_FIELDS.map(styleField)}
269
+ {font ? (
270
+ <ReadOnlyField label="font" glyph="Font" value={font} isWide />
271
+ ) : null}
272
+ </Section>
273
+ <Section title="Fill">
274
+ {FILL_FIELDS.map((field) => {
275
+ const initial = currentValue(node, field.property)
276
+ return (
277
+ <div key={field.property} class="fill-row is-wide">
278
+ {styleField(field)}
279
+ {isRawInline(node, field.property) ? (
261
280
  <span
262
281
  class="raw"
263
282
  title="Set as a raw value, so it will not follow the theme. Pick a token to fix it"
@@ -265,19 +284,33 @@ function ElementFields({
265
284
  raw
266
285
  </span>
267
286
  ) : null}
268
- {field.isColor ? (
269
- <TokenPicker
270
- label={field.label}
271
- initial={initial}
272
- isBusy={isBusy}
273
- onCommit={commit(field.property)}
274
- />
275
- ) : null}
276
- </dd>
277
- </div>
278
- )
279
- })}
280
- </dl>
287
+ <TokenPicker
288
+ label={field.label}
289
+ initial={initial}
290
+ isBusy={isBusy}
291
+ onCommit={commit(field.property)}
292
+ />
293
+ </div>
294
+ )
295
+ })}
296
+ </Section>
297
+ <Section title="Text">
298
+ {holdsTextAlone(node) ? (
299
+ <Field
300
+ label="text"
301
+ glyph="T"
302
+ initial={text}
303
+ isBusy={isBusy}
304
+ isWide
305
+ onCommit={commit('text')}
306
+ />
307
+ ) : (
308
+ <p class="text-excerpt is-wide" title={excerpt(text)}>
309
+ {excerpt(text)}
310
+ </p>
311
+ )}
312
+ </Section>
313
+ </>
281
314
  )
282
315
  }
283
316
 
@@ -342,14 +375,20 @@ export function Inspector(): JSX.Element {
342
375
  <p class="detail" title={frame.name}>
343
376
  {frame.name}
344
377
  </p>
345
- <dl class="box">
346
- {FIELDS.map((field) => (
347
- <div key={field} class="box-row">
348
- <dt>{field}</dt>
349
- <dd>{frame[field]}</dd>
350
- </div>
351
- ))}
352
- </dl>
378
+ <div class="field-grid">
379
+ <ReadOnlyField label="x" glyph="X" value={String(frame.x)} />
380
+ <ReadOnlyField label="y" glyph="Y" value={String(frame.y)} />
381
+ <ReadOnlyField
382
+ label="width"
383
+ glyph="W"
384
+ value={String(frame.width)}
385
+ />
386
+ <ReadOnlyField
387
+ label="height"
388
+ glyph="H"
389
+ value={String(frame.height)}
390
+ />
391
+ </div>
353
392
  </>
354
393
  ) : (
355
394
  <p class="empty">Select a frame to see its box</p>
@@ -320,29 +320,6 @@ code {
320
320
  outline-offset: 6px;
321
321
  }
322
322
 
323
- .box {
324
- display: grid;
325
- grid-template-columns: 1fr 1fr;
326
- gap: var(--space-xs) var(--space-md);
327
- margin: var(--space-xs) 0 0;
328
- }
329
-
330
- .box-row {
331
- display: flex;
332
- justify-content: space-between;
333
- gap: var(--space-sm);
334
- }
335
-
336
- .box dt {
337
- color: var(--color-muted);
338
- }
339
-
340
- .box dd {
341
- margin: 0;
342
- color: var(--color-text);
343
- font-variant-numeric: tabular-nums;
344
- }
345
-
346
323
  .toolbar {
347
324
  position: absolute;
348
325
  right: var(--space-md);
@@ -436,84 +413,6 @@ code {
436
413
  white-space: nowrap;
437
414
  }
438
415
 
439
- /* One property a row, since a computed color or font stack outgrows a half. */
440
- .styles {
441
- grid-template-columns: 1fr;
442
- }
443
-
444
- .styles dd {
445
- min-width: 0;
446
- overflow: hidden;
447
- text-overflow: ellipsis;
448
- white-space: nowrap;
449
- }
450
-
451
- .styles .box-row {
452
- align-items: center;
453
- }
454
-
455
- .styles dt {
456
- flex: none;
457
- width: 4.5rem;
458
- }
459
-
460
- .styles dd {
461
- flex: 1;
462
- }
463
-
464
- .field-cell {
465
- display: flex;
466
- gap: var(--space-xs);
467
- }
468
-
469
- .field,
470
- .token-picker {
471
- min-width: 0;
472
- min-height: 24px;
473
- padding: 2px var(--space-xs);
474
- border: 1px solid var(--color-border);
475
- border-radius: var(--radius-action);
476
- background: var(--color-surface);
477
- color: var(--color-text);
478
- font: inherit;
479
- font-variant-numeric: tabular-nums;
480
- }
481
-
482
- .field {
483
- flex: 1;
484
- width: 100%;
485
- }
486
-
487
- .token-picker {
488
- flex: none;
489
- width: 4.5rem;
490
- cursor: pointer;
491
- }
492
-
493
- .raw {
494
- flex: none;
495
- align-self: center;
496
- color: var(--color-text-secondary);
497
- cursor: help;
498
- }
499
-
500
- .field:hover,
501
- .token-picker:hover {
502
- border-color: var(--color-muted);
503
- }
504
-
505
- .field:disabled,
506
- .token-picker:disabled {
507
- opacity: 0.5;
508
- cursor: not-allowed;
509
- }
510
-
511
- .saved {
512
- margin-left: var(--space-sm);
513
- color: var(--color-success);
514
- font-weight: 400;
515
- }
516
-
517
416
  .tabs {
518
417
  display: flex;
519
418
  gap: 2px;
@@ -27,6 +27,11 @@ export const MISC_CASES: readonly SkillCase[] = [
27
27
  "I found two sites whose look I want us to draw from. Put them side by side and let's settle which one the design should follow before anyone writes a token.",
28
28
  expect: 'sketch-design',
29
29
  },
30
+ {
31
+ prompt:
32
+ 'Draw the signup screen as a frame on the canvas, then restyle the element I just selected there.',
33
+ expect: 'canvas',
34
+ },
30
35
  {
31
36
  prompt: 'Fire up the dev server the way this project documents it.',
32
37
  expect: 'project-commands',
@@ -26,7 +26,7 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
26
26
  },
27
27
  {
28
28
  prompt: 'Draw me a diagram of how the pieces of this system connect.',
29
- expect: 'draft-diagram',
29
+ expect: 'draft-figure',
30
30
  },
31
31
  {
32
32
  prompt:
@@ -20,7 +20,6 @@ const HOOKS = 'hooks'
20
20
  export const SUBDIRS: readonly string[] = [
21
21
  HOOKS,
22
22
  'context',
23
- 'diagrams',
24
23
  'memory',
25
24
  'tasks',
26
25
  'wireframes',
@@ -6,30 +6,22 @@ import {
6
6
  DESIGN_INSTALL_DIR,
7
7
  DESIGN_PROJECT_SUBDIR,
8
8
  } from '@/design/adapter'
9
- import { generateBoard } from '@/design/board'
10
9
  import { buildDesignCss } from '@/design/css'
11
10
  import { HAND_DRAWN_FONT_FACES } from '@/design/fonts'
12
11
  import { renderDesignDoc } from '@/design/render'
13
12
  import { DESIGN_BASE_CSS, DESIGN_DOCUMENT, regenDesign } from '@/design/regen'
14
- import {
15
- checkoutMismatchWarning,
16
- isOwnCheckout,
17
- PROJECT_ROOT,
18
- } from '@/project-root'
13
+ import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
19
14
  import { creationRel, SCRATCH } from '@/record-root'
20
15
  import { surfaceDir } from '@/surface-root'
21
16
  import { recordStamp, runDomainSync } from '@/sync/engine'
22
17
  import { resolveTarget } from '@/target'
23
18
  import { intro, logAdd, logError, logInfo, logWarn, outro, palette } from '@/ui'
24
- import { mainWorktreeRoot } from '@/worktree'
25
19
 
26
20
  export function register(program: Command): void {
27
21
  const design = program
28
22
  .command('design')
29
23
  .helpOption('-h, --help', 'Show this help message')
30
- .description(
31
- 'Design system commands (regen, css, render, board, install, sync)',
32
- )
24
+ .description('Design system commands (regen, css, render, install, sync)')
33
25
 
34
26
  design
35
27
  .command('regen')
@@ -145,60 +137,6 @@ export function register(program: Command): void {
145
137
  )
146
138
  })
147
139
 
148
- design
149
- .command('board')
150
- .helpOption('-h, --help', 'Show this help message')
151
- .description(
152
- 'Generate the design board, an index over a project’s design surfaces',
153
- )
154
- .option(
155
- '-o, --out <path>',
156
- 'Output directory',
157
- creationRel(process.cwd(), SCRATCH, 'render', 'board'),
158
- )
159
- .option('--root <path>', 'Project root, defaulting to the main worktree')
160
- .addHelpText(
161
- 'after',
162
- [
163
- '',
164
- 'Reads its sources from --root, defaulting to the main worktree of',
165
- 'whatever project the caller stands in, and writes a static page',
166
- 'set, never installed or synced. The surfaces panel’s landing-page',
167
- 'half and the whole components panel render only when that root is',
168
- 'this toolkit’s own checkout. Open the result with canon serve <out>.',
169
- '',
170
- ].join('\n'),
171
- )
172
- .action(async (opts: { out: string; root?: string }) => {
173
- const outDir = resolve(process.cwd(), opts.out)
174
- const root = opts.root
175
- ? resolve(process.cwd(), opts.root)
176
- : await mainWorktreeRoot()
177
- const isToolkitCheckout = isOwnCheckout(root)
178
- const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
179
- const mismatch = checkoutMismatchWarning(process.cwd())
180
- process.stderr.write(
181
- `${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}Generate design board${NC}\n`,
182
- )
183
- if (mismatch !== undefined) logWarn(mismatch)
184
- const result = generateBoard(
185
- root,
186
- outDir,
187
- process.cwd(),
188
- isToolkitCheckout,
189
- )
190
- if (!result.ok) {
191
- process.stderr.write(
192
- `${GREY}│${NC} ${RED}✗${NC} ${result.detail}\n${GREY}└${NC}\n`,
193
- )
194
- process.exitCode = 1
195
- return
196
- }
197
- process.stderr.write(
198
- `${GREY}│${NC} ${GREEN}✓${NC} ${relative(process.cwd(), result.indexPath)}\n${GREY}│${NC} Open with: canon serve ${relative(process.cwd(), result.outDir)}\n${GREY}└${NC}\n`,
199
- )
200
- })
201
-
202
140
  design
203
141
  .command('install')
204
142
  .description('Install the base stylesheet into a project')