@erclx/canon 5.53.0 → 5.55.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 +18 -0
- package/README.md +10 -9
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +5 -5
- package/claude/skills/create-skill/SKILL.md +1 -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/document-health/SKILL.md +6 -6
- package/claude/skills/document-repair/SKILL.md +5 -5
- package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
- package/claude/skills/draw-figure/SKILL.md +1 -1
- package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
- 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/target-check/SKILL.md +6 -8
- package/claude/skills/target-check/references/domains.md +1 -4
- package/claude/skills/target-setup/SKILL.md +8 -8
- package/claude/skills/target-update/SKILL.md +5 -5
- package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
- package/claude/skills/teach-craft/SKILL.md +32 -1
- package/claude/skills/teach-craft/references/adopted.md +113 -0
- package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
- package/claude/skills/teach-craft/references/pedagogy.md +29 -2
- package/docs/agents/install-and-sync.md +2 -10
- package/docs/agents/output-shape.md +11 -10
- package/docs/target/migrations.md +8 -3
- package/docs/target/projects.md +1 -1
- package/docs/target/sync.md +1 -1
- package/docs/workflow/skill-map.md +0 -1
- 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 -5
- package/scripts/core/install-check.sh +1 -1
- package/scripts/lib/ui.sh +12 -65
- package/src/audits/run.ts +1 -1
- package/src/claude/cases/misc.ts +0 -5
- package/src/claude/routing.ts +1 -1
- package/src/claude/skills-audit.ts +1 -0
- package/src/cli/help.ts +0 -3
- package/src/cli/run.ts +5 -6
- package/src/cli/ui.ts +87 -28
- package/src/cli.ts +6 -2
- package/src/commands/claude.ts +2 -3
- package/src/commands/design.ts +20 -324
- package/src/commands/gate.ts +5 -3
- package/src/commands/gov.ts +11 -21
- package/src/commands/labels.ts +1 -1
- package/src/commands/pr.ts +1 -1
- package/src/commands/sync.ts +5 -6
- package/src/commands/tooling.ts +5 -8
- package/src/commands/transcripts.ts +6 -11
- package/src/design/regen.ts +5 -32
- package/src/design/render.ts +1 -1
- package/src/design/tokens.ts +3 -2
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +4 -20
- package/src/gov/audit.ts +1 -6
- package/src/hooks/post-merge.ts +2 -5
- package/src/markdown/scope.ts +2 -4
- package/src/process/harness.ts +3 -4
- package/src/sandbox/equivalence.ts +0 -1
- package/src/sandbox/provision.ts +8 -13
- package/src/sandbox/scenario.ts +6 -12
- package/src/shipped/references.ts +1 -2
- package/src/sync/check.ts +4 -29
- package/src/sync/engine.ts +3 -8
- package/src/sync/stamp.ts +6 -6
- package/src/sync/target.ts +1 -4
- package/src/sync/workflow.ts +3 -5
- package/src/tooling/verify-stack.ts +1 -1
- package/tooling/base/configs/.husky/post-merge +6 -10
- 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/governance/rules/ui/canvas.md +0 -13
- package/scripts/core/check-color-source.sh +0 -51
- 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/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/commands/design.ts
CHANGED
|
@@ -1,90 +1,60 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join, relative } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
|
-
import {
|
|
5
|
-
createDesignAdapter,
|
|
6
|
-
DESIGN_INSTALL_DIR,
|
|
7
|
-
DESIGN_PROJECT_SUBDIR,
|
|
8
|
-
} from '@/design/adapter'
|
|
9
4
|
import { buildDesignCss } from '@/design/css'
|
|
10
5
|
import { HAND_DRAWN_FONT_FACES } from '@/design/fonts'
|
|
11
|
-
import {
|
|
12
|
-
import { renderDesignDoc } from '@/design/render'
|
|
13
|
-
import { DESIGN_BASE_CSS, DESIGN_DOCUMENT, regenDesign } from '@/design/regen'
|
|
6
|
+
import { DESIGN_DOCUMENT, regenDesign } from '@/design/regen'
|
|
14
7
|
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
|
|
15
|
-
import {
|
|
16
|
-
import { surfaceDir } from '@/roots/surface'
|
|
17
|
-
import { recordStamp, runDomainSync } from '@/sync/engine'
|
|
18
|
-
import { isDirectory, resolveTarget } from '@/targets/validate'
|
|
19
|
-
import {
|
|
20
|
-
intro,
|
|
21
|
-
logAdd,
|
|
22
|
-
logError,
|
|
23
|
-
logInfo,
|
|
24
|
-
logWarn,
|
|
25
|
-
outro,
|
|
26
|
-
palette,
|
|
27
|
-
} from '@/cli/ui'
|
|
8
|
+
import { intro, logError, logInfo, logWarn, outro } from '@/cli/ui'
|
|
28
9
|
|
|
29
10
|
export function register(program: Command): void {
|
|
30
11
|
const design = program
|
|
31
12
|
.command('design')
|
|
32
13
|
.helpOption('-h, --help', 'Show this help message')
|
|
33
|
-
.description(
|
|
34
|
-
'Design system commands (regen, css, render, install, sync, fonts)',
|
|
35
|
-
)
|
|
14
|
+
.description('Design system commands (regen, css)')
|
|
36
15
|
|
|
37
16
|
design
|
|
38
17
|
.command('regen')
|
|
39
18
|
.helpOption('-h, --help', 'Show this help message')
|
|
40
|
-
.description(
|
|
41
|
-
'Rewrite canon/DESIGN.md and the base stylesheet from src/design/tokens.ts',
|
|
42
|
-
)
|
|
19
|
+
.description('Rewrite canon/DESIGN.md from src/design/tokens.ts')
|
|
43
20
|
.addHelpText(
|
|
44
21
|
'after',
|
|
45
22
|
[
|
|
46
23
|
'',
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
24
|
+
'This runs against the toolkit checkout rather than a target. It',
|
|
25
|
+
'rewrites the record from the token module and refuses where the record',
|
|
26
|
+
'is absent, which is what the CLI installed into a project as a',
|
|
27
|
+
'dependency looks like.',
|
|
51
28
|
'',
|
|
52
29
|
].join('\n'),
|
|
53
30
|
)
|
|
54
31
|
.action(() => {
|
|
55
|
-
const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
|
|
56
|
-
|
|
57
32
|
// The guard below catches a `PROJECT_ROOT` with no record at all, which
|
|
58
33
|
// is an installed package. It cannot catch a second real checkout that
|
|
59
34
|
// has one, so both paths carry the warning. It is a frame-interior line,
|
|
60
35
|
// so each emits it after its own opener rather than ahead of the branch.
|
|
61
36
|
const mismatch = checkoutMismatchWarning(process.cwd())
|
|
62
37
|
|
|
63
|
-
//
|
|
38
|
+
// The output resolves from `PROJECT_ROOT`, which is the installed
|
|
64
39
|
// package directory when the CLI runs out of a target's `node_modules`.
|
|
65
|
-
// The record is
|
|
66
|
-
//
|
|
40
|
+
// The record is already committed here and ships with no package, so
|
|
41
|
+
// its absence is what separates the two.
|
|
67
42
|
if (!existsSync(join(PROJECT_ROOT, DESIGN_DOCUMENT))) {
|
|
68
|
-
|
|
43
|
+
intro('Regenerate design source')
|
|
69
44
|
if (mismatch !== undefined) logWarn(mismatch)
|
|
70
|
-
|
|
71
|
-
|
|
45
|
+
logError(
|
|
46
|
+
`No ${DESIGN_DOCUMENT} at ${PROJECT_ROOT}. Regen runs in the toolkit checkout, not against a target.`,
|
|
72
47
|
)
|
|
48
|
+
outro()
|
|
73
49
|
process.exitCode = 1
|
|
74
50
|
return
|
|
75
51
|
}
|
|
76
52
|
|
|
77
|
-
|
|
78
|
-
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}Regenerate design source${NC}\n`,
|
|
79
|
-
)
|
|
53
|
+
intro('Regenerate design source')
|
|
80
54
|
if (mismatch !== undefined) logWarn(mismatch)
|
|
81
55
|
const result = regenDesign(PROJECT_ROOT)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
`${GREY}│${NC} ${GREEN}✓${NC} ${relative(PROJECT_ROOT, path)}\n`,
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
process.stderr.write(`${GREY}└${NC}\n`)
|
|
56
|
+
logInfo(relative(PROJECT_ROOT, result.documentPath))
|
|
57
|
+
outro()
|
|
88
58
|
})
|
|
89
59
|
|
|
90
60
|
design
|
|
@@ -107,278 +77,4 @@ export function register(program: Command): void {
|
|
|
107
77
|
}),
|
|
108
78
|
)
|
|
109
79
|
})
|
|
110
|
-
|
|
111
|
-
design
|
|
112
|
-
.command('render')
|
|
113
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
114
|
-
.description('Render DESIGN.md tokens to HTML and CSS preview')
|
|
115
|
-
.option(
|
|
116
|
-
'-s, --source <path>',
|
|
117
|
-
'Source DESIGN.md path',
|
|
118
|
-
relative(process.cwd(), surfaceDir(process.cwd(), 'DESIGN.md')),
|
|
119
|
-
)
|
|
120
|
-
.option(
|
|
121
|
-
'-o, --out <path>',
|
|
122
|
-
'Output directory',
|
|
123
|
-
creationRel(process.cwd(), SCRATCH, 'render', 'design'),
|
|
124
|
-
)
|
|
125
|
-
.option(
|
|
126
|
-
'--embed-fonts',
|
|
127
|
-
'Declare each vendored face the typography table names, and set the page body in the body stack',
|
|
128
|
-
)
|
|
129
|
-
.action((opts: { source: string; out: string; embedFonts?: boolean }) => {
|
|
130
|
-
const sourcePath = resolve(process.cwd(), opts.source)
|
|
131
|
-
const outDir = resolve(process.cwd(), opts.out)
|
|
132
|
-
const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
|
|
133
|
-
if (!existsSync(sourcePath)) {
|
|
134
|
-
process.stderr.write(
|
|
135
|
-
`${GREY}┌${NC}\n${GREY}│${NC} ${RED}✗${NC} ${opts.source} not found\n${GREY}└${NC}\n`,
|
|
136
|
-
)
|
|
137
|
-
process.exitCode = 1
|
|
138
|
-
return
|
|
139
|
-
}
|
|
140
|
-
process.stderr.write(
|
|
141
|
-
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}Render design tokens${NC}\n`,
|
|
142
|
-
)
|
|
143
|
-
const result = renderDesignDoc(sourcePath, outDir, {
|
|
144
|
-
embedFonts: opts.embedFonts,
|
|
145
|
-
})
|
|
146
|
-
process.stderr.write(
|
|
147
|
-
`${GREY}│${NC} ${GREEN}✓${NC} ${result.htmlPath}\n${GREY}│${NC} ${GREEN}✓${NC} ${result.cssPath}\n${GREY}└${NC}\n`,
|
|
148
|
-
)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
design
|
|
152
|
-
.command('install')
|
|
153
|
-
.description('Install the base stylesheet into a project')
|
|
154
|
-
.argument('[target]', 'Target directory', '.')
|
|
155
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
156
|
-
.addHelpText(
|
|
157
|
-
'after',
|
|
158
|
-
[
|
|
159
|
-
'',
|
|
160
|
-
'Installs one toolkit-owned file and creates no override. A project',
|
|
161
|
-
`overrides a value by writing ${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/`,
|
|
162
|
-
'itself, which sync never touches. Nothing arrives on a project that',
|
|
163
|
-
'has not run this, so a design value reaches a target on an install',
|
|
164
|
-
'rather than on the next sync. A base stylesheet already installed is',
|
|
165
|
-
'replaced.',
|
|
166
|
-
'',
|
|
167
|
-
'Exit codes:',
|
|
168
|
-
' 0 the stylesheet was installed',
|
|
169
|
-
' 1 the target is missing or is the toolkit root, or the toolkit',
|
|
170
|
-
' source is absent',
|
|
171
|
-
'',
|
|
172
|
-
].join('\n'),
|
|
173
|
-
)
|
|
174
|
-
.action(async (target: string) => {
|
|
175
|
-
process.exitCode = await runInstall(target)
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
design
|
|
179
|
-
.command('sync')
|
|
180
|
-
.description(
|
|
181
|
-
'Update the base stylesheet already installed under .claude/design/',
|
|
182
|
-
)
|
|
183
|
-
.argument('[target]', 'Target directory', '.')
|
|
184
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
185
|
-
.addHelpText(
|
|
186
|
-
'after',
|
|
187
|
-
[
|
|
188
|
-
'',
|
|
189
|
-
'Replaces the shipped design files under .claude/design/ with the',
|
|
190
|
-
'toolkit copy, edited or not, and leaves',
|
|
191
|
-
`${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/ alone. It deletes nothing it finds no`,
|
|
192
|
-
'source for.',
|
|
193
|
-
'',
|
|
194
|
-
'Exit codes:',
|
|
195
|
-
' 0 synced, already up to date, or the prompt was cancelled',
|
|
196
|
-
' 1 the target is missing or is the toolkit root',
|
|
197
|
-
'',
|
|
198
|
-
].join('\n'),
|
|
199
|
-
)
|
|
200
|
-
.action(async (target: string) => {
|
|
201
|
-
process.exitCode = await runDomainSync(
|
|
202
|
-
createDesignAdapter(PROJECT_ROOT),
|
|
203
|
-
target,
|
|
204
|
-
{ protectedRoot: PROJECT_ROOT },
|
|
205
|
-
)
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
registerFonts(design)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const FONTS_DIR = join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR, 'fonts')
|
|
212
|
-
|
|
213
|
-
interface FontsRefused {
|
|
214
|
-
readonly ok: false
|
|
215
|
-
readonly reason: string
|
|
216
|
-
readonly detail: string
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function writeJson(record: unknown): void {
|
|
220
|
-
process.stdout.write(`${JSON.stringify(record)}\n`)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function refuseFonts(
|
|
224
|
-
title: string,
|
|
225
|
-
refusal: FontsRefused,
|
|
226
|
-
emitJson: boolean,
|
|
227
|
-
): number {
|
|
228
|
-
if (emitJson) writeJson(refusal)
|
|
229
|
-
intro(title)
|
|
230
|
-
logError(refusal.detail)
|
|
231
|
-
outro()
|
|
232
|
-
return 1
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** A target that is a directory, or the refusal naming the path given. */
|
|
236
|
-
function fontsTarget(target: string): string | FontsRefused {
|
|
237
|
-
const resolved = resolve(target)
|
|
238
|
-
if (!isDirectory(resolved)) {
|
|
239
|
-
return {
|
|
240
|
-
ok: false,
|
|
241
|
-
reason: 'no-target',
|
|
242
|
-
detail: `Target directory not found: ${target}`,
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return resolved
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function registerFonts(design: Command): void {
|
|
249
|
-
const fonts = design
|
|
250
|
-
.command('fonts')
|
|
251
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
252
|
-
.description(`Read and add the project's own faces under ${FONTS_DIR}/`)
|
|
253
|
-
.addHelpText(
|
|
254
|
-
'after',
|
|
255
|
-
[
|
|
256
|
-
'',
|
|
257
|
-
`Faces are declared as @font-face rules in ${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/fonts.css,`,
|
|
258
|
-
'which the canvas embeds into every frame. Sync never writes the folder.',
|
|
259
|
-
'',
|
|
260
|
-
].join('\n'),
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
fonts
|
|
264
|
-
.command('list')
|
|
265
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
266
|
-
.description('List each face the project override sheets declare')
|
|
267
|
-
.argument('[target]', 'Target directory', '.')
|
|
268
|
-
.option('--json', 'Emit the record on stdout')
|
|
269
|
-
.action((target: string, opts: { json?: boolean }) => {
|
|
270
|
-
const title = 'canon design fonts list'
|
|
271
|
-
const resolved = fontsTarget(target)
|
|
272
|
-
if (typeof resolved !== 'string') {
|
|
273
|
-
process.exitCode = refuseFonts(title, resolved, Boolean(opts.json))
|
|
274
|
-
return
|
|
275
|
-
}
|
|
276
|
-
const faces = listProjectFaces(resolved)
|
|
277
|
-
if (opts.json) writeJson({ ok: true, faces })
|
|
278
|
-
intro(title)
|
|
279
|
-
if (faces.length === 0) logInfo(`No faces declared under ${FONTS_DIR}/`)
|
|
280
|
-
for (const face of faces) {
|
|
281
|
-
const line = `${face.family} ${face.weight} ${face.style}: ${face.file} in ${face.sheet}`
|
|
282
|
-
if (face.present) logInfo(line)
|
|
283
|
-
else logWarn(`${line} (${face.problem ?? 'missing'})`)
|
|
284
|
-
}
|
|
285
|
-
outro()
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
fonts
|
|
289
|
-
.command('add')
|
|
290
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
291
|
-
.description(
|
|
292
|
-
`Copy a woff2, woff, ttf, or otf face into ${FONTS_DIR}/ and declare it`,
|
|
293
|
-
)
|
|
294
|
-
.argument('<file>', 'Font file to copy')
|
|
295
|
-
.argument('[target]', 'Target directory', '.')
|
|
296
|
-
.requiredOption('--family <name>', 'Family name the face declares')
|
|
297
|
-
.option('--weight <n>', "Weight, or a range such as '100 900'", '400')
|
|
298
|
-
.option('--style <style>', 'normal or italic', 'normal')
|
|
299
|
-
.option('--json', 'Emit the record on stdout')
|
|
300
|
-
.action(
|
|
301
|
-
(
|
|
302
|
-
file: string,
|
|
303
|
-
target: string,
|
|
304
|
-
opts: { family: string; weight: string; style: string; json?: boolean },
|
|
305
|
-
) => {
|
|
306
|
-
const title = 'canon design fonts add'
|
|
307
|
-
const emitJson = Boolean(opts.json)
|
|
308
|
-
const resolved = fontsTarget(target)
|
|
309
|
-
if (typeof resolved !== 'string') {
|
|
310
|
-
process.exitCode = refuseFonts(title, resolved, emitJson)
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
if (resolved === PROJECT_ROOT) {
|
|
314
|
-
process.exitCode = refuseFonts(
|
|
315
|
-
title,
|
|
316
|
-
{
|
|
317
|
-
ok: false,
|
|
318
|
-
reason: 'toolkit-root',
|
|
319
|
-
detail:
|
|
320
|
-
'Cannot run against toolkit root. Files here are the source of truth.',
|
|
321
|
-
},
|
|
322
|
-
emitJson,
|
|
323
|
-
)
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
const outcome = addProjectFace(resolved, resolve(file), {
|
|
327
|
-
family: opts.family,
|
|
328
|
-
weight: opts.weight,
|
|
329
|
-
style: opts.style,
|
|
330
|
-
})
|
|
331
|
-
if (!outcome.ok) {
|
|
332
|
-
process.exitCode = refuseFonts(title, outcome, emitJson)
|
|
333
|
-
return
|
|
334
|
-
}
|
|
335
|
-
if (emitJson) writeJson(outcome)
|
|
336
|
-
intro(title)
|
|
337
|
-
logAdd(join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR, outcome.file))
|
|
338
|
-
logInfo(
|
|
339
|
-
`Declared ${outcome.family} ${outcome.weight} ${outcome.style} in ${outcome.sheet}`,
|
|
340
|
-
)
|
|
341
|
-
outro()
|
|
342
|
-
},
|
|
343
|
-
)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Copies the base and stops. The override folder is deliberately not created:
|
|
348
|
-
* an empty override is a file the project did not ask for and did not write,
|
|
349
|
-
* the three-way merge already handles a missing side, and an empty file invites
|
|
350
|
-
* a target to fill it before it has an opinion.
|
|
351
|
-
*/
|
|
352
|
-
async function runInstall(target: string): Promise<number> {
|
|
353
|
-
intro('canon design install')
|
|
354
|
-
|
|
355
|
-
const resolved = resolveTarget(target, PROJECT_ROOT)
|
|
356
|
-
if (typeof resolved === 'number') {
|
|
357
|
-
outro()
|
|
358
|
-
return resolved
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const source = join(PROJECT_ROOT, DESIGN_BASE_CSS)
|
|
362
|
-
if (!existsSync(source)) {
|
|
363
|
-
logError(
|
|
364
|
-
`No base stylesheet at ${DESIGN_BASE_CSS}. Run canon design regen.`,
|
|
365
|
-
)
|
|
366
|
-
outro()
|
|
367
|
-
return 1
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const dir = join(resolved, DESIGN_INSTALL_DIR)
|
|
371
|
-
mkdirSync(dir, { recursive: true })
|
|
372
|
-
|
|
373
|
-
const dest = join(dir, basename(source))
|
|
374
|
-
copyFileSync(source, dest)
|
|
375
|
-
logAdd(relative(resolved, dest))
|
|
376
|
-
|
|
377
|
-
await recordStamp(createDesignAdapter(PROJECT_ROOT), resolved, new Date())
|
|
378
|
-
|
|
379
|
-
logInfo(
|
|
380
|
-
`Override a value in ${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/, which sync leaves alone.`,
|
|
381
|
-
)
|
|
382
|
-
outro()
|
|
383
|
-
return 0
|
|
384
80
|
}
|
package/src/commands/gate.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
logStep,
|
|
20
20
|
logWarn,
|
|
21
21
|
outro,
|
|
22
|
+
outroSuccess,
|
|
22
23
|
palette,
|
|
23
24
|
pipeOutput,
|
|
24
25
|
plural,
|
|
@@ -216,20 +217,21 @@ function report(result: StageResult): void {
|
|
|
216
217
|
* against.
|
|
217
218
|
*/
|
|
218
219
|
function close(summary: Summary, code: number): void {
|
|
219
|
-
const {
|
|
220
|
-
outro()
|
|
220
|
+
const { NC, RED, YELLOW } = palette(process.stderr)
|
|
221
221
|
|
|
222
222
|
if (code !== 0) {
|
|
223
|
+
outro()
|
|
223
224
|
process.stderr.write(`${RED}✗ Verification failed${NC}\n\n`)
|
|
224
225
|
return
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
if (summary.unmeasured > 0) {
|
|
229
|
+
outro()
|
|
228
230
|
process.stderr.write(
|
|
229
231
|
`${YELLOW}! Verification passed, ${plural(summary.unmeasured, 'stage')} measured nothing${NC}\n\n`,
|
|
230
232
|
)
|
|
231
233
|
return
|
|
232
234
|
}
|
|
233
235
|
|
|
234
|
-
|
|
236
|
+
outroSuccess('Verification passed')
|
|
235
237
|
}
|
package/src/commands/gov.ts
CHANGED
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
logStep,
|
|
76
76
|
logWarn,
|
|
77
77
|
outro,
|
|
78
|
-
|
|
78
|
+
outroSuccess,
|
|
79
79
|
select,
|
|
80
80
|
} from '@/cli/ui'
|
|
81
81
|
|
|
@@ -184,9 +184,9 @@ export function register(program: Command): void {
|
|
|
184
184
|
'Copies each rule into .claude/rules/canon/, replacing a copy already',
|
|
185
185
|
'there. Rules under .claude/rules/project/ are never read.',
|
|
186
186
|
'',
|
|
187
|
-
'The install confirm needs a TTY. --yes answers it,
|
|
188
|
-
'
|
|
189
|
-
'
|
|
187
|
+
'The install confirm needs a TTY. --yes answers it, and a call with no',
|
|
188
|
+
'terminal on stdin takes Yes. It does not pick a stack, so a headless',
|
|
189
|
+
'call still names one.',
|
|
190
190
|
'',
|
|
191
191
|
'Exit codes:',
|
|
192
192
|
' 0 installed, the stack held no rules, or the prompt was cancelled',
|
|
@@ -1160,7 +1160,7 @@ function runTestOrder(opts: TestOrderOptions): number {
|
|
|
1160
1160
|
const report = readTestOrder(root, { base: opts.base })
|
|
1161
1161
|
const emitJson = opts.json ?? false
|
|
1162
1162
|
|
|
1163
|
-
// The
|
|
1163
|
+
// The refusal reaches stderr in both modes and the record goes to stdout
|
|
1164
1164
|
// alone, which is the split `docs/agents/output-shape.md` fixes. A consumer
|
|
1165
1165
|
// reading stdout sees pure data either way, and an operator reading the
|
|
1166
1166
|
// terminal sees the refusal rather than a command that appeared to do nothing.
|
|
@@ -1455,8 +1455,8 @@ function displayPath(target: string, rel: string): string {
|
|
|
1455
1455
|
}
|
|
1456
1456
|
|
|
1457
1457
|
/**
|
|
1458
|
-
* Refuses rather than picking. `select_option` returned `options[0]`
|
|
1459
|
-
*
|
|
1458
|
+
* Refuses rather than picking. `select_option` returned `options[0]` with no
|
|
1459
|
+
* terminal to answer it, so a headless `canon gov install` with no stack
|
|
1460
1460
|
* installed whichever stack sorted first, measured as 26 astro rules into an
|
|
1461
1461
|
* empty directory. Every documented agent path already passes the argument.
|
|
1462
1462
|
*/
|
|
@@ -1558,16 +1558,13 @@ async function runInstall(
|
|
|
1558
1558
|
now,
|
|
1559
1559
|
)
|
|
1560
1560
|
|
|
1561
|
-
|
|
1562
|
-
outro()
|
|
1563
|
-
process.stderr.write(`${GREEN}✓ Rules installed${NC}\n`)
|
|
1561
|
+
outroSuccess('Rules installed')
|
|
1564
1562
|
return 0
|
|
1565
1563
|
}
|
|
1566
1564
|
|
|
1567
1565
|
/**
|
|
1568
1566
|
* The install confirm. `--yes` answers it without a prompt, so the line it
|
|
1569
|
-
* would have printed is skipped rather than duplicated
|
|
1570
|
-
* `CANON_NON_INTERACTIVE=1`.
|
|
1567
|
+
* would have printed is skipped rather than duplicated on a headless call.
|
|
1571
1568
|
*/
|
|
1572
1569
|
async function confirmInstall(
|
|
1573
1570
|
count: number,
|
|
@@ -1582,7 +1579,6 @@ async function confirmInstall(
|
|
|
1582
1579
|
{ value: false, label: 'No' },
|
|
1583
1580
|
],
|
|
1584
1581
|
nonInteractiveDefault: true,
|
|
1585
|
-
ttyHint: 'Pass --yes',
|
|
1586
1582
|
})
|
|
1587
1583
|
}
|
|
1588
1584
|
|
|
@@ -1638,9 +1634,7 @@ async function installExtras(
|
|
|
1638
1634
|
|
|
1639
1635
|
await recordStamp(createGovAdapter(PROJECT_ROOT), resolved, new Date())
|
|
1640
1636
|
|
|
1641
|
-
|
|
1642
|
-
outro()
|
|
1643
|
-
process.stderr.write(`${GREEN}✓ Rules installed${NC}\n`)
|
|
1637
|
+
outroSuccess('Rules installed')
|
|
1644
1638
|
return 0
|
|
1645
1639
|
}
|
|
1646
1640
|
|
|
@@ -1689,10 +1683,6 @@ async function runBuild(target: string): Promise<number> {
|
|
|
1689
1683
|
await writeFile(output, buildRulesPayload(files))
|
|
1690
1684
|
logAdd(payload)
|
|
1691
1685
|
|
|
1692
|
-
|
|
1693
|
-
outro()
|
|
1694
|
-
process.stderr.write(
|
|
1695
|
-
`${GREEN}✓ Rules built (${files.length} rules → ${payload})${NC}\n`,
|
|
1696
|
-
)
|
|
1686
|
+
outroSuccess(`Rules built (${files.length} rules → ${payload})`)
|
|
1697
1687
|
return 0
|
|
1698
1688
|
}
|
package/src/commands/labels.ts
CHANGED
|
@@ -311,7 +311,7 @@ async function runAudit(paths: string[], opts: AuditOptions): Promise<number> {
|
|
|
311
311
|
|
|
312
312
|
intro('canon labels audit')
|
|
313
313
|
|
|
314
|
-
// The
|
|
314
|
+
// The refusal reaches stderr in both modes and the record goes to stdout
|
|
315
315
|
// alone, so an operator reading the terminal sees the refusal rather than a
|
|
316
316
|
// command that appeared to do nothing.
|
|
317
317
|
if (report.kind === 'refused') {
|
package/src/commands/pr.ts
CHANGED
package/src/commands/sync.ts
CHANGED
|
@@ -35,7 +35,6 @@ import { describeSkew } from '@/version/skew'
|
|
|
35
35
|
|
|
36
36
|
const SYNC_ARGS: Record<SyncDomain, readonly string[]> = {
|
|
37
37
|
governance: ['gov', 'sync'],
|
|
38
|
-
design: ['design', 'sync'],
|
|
39
38
|
claude: ['claude', 'sync'],
|
|
40
39
|
}
|
|
41
40
|
|
|
@@ -58,10 +57,10 @@ export function register(program: Command): void {
|
|
|
58
57
|
'after',
|
|
59
58
|
[
|
|
60
59
|
'',
|
|
61
|
-
'Runs `canon gov sync
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
60
|
+
'Runs `canon gov sync` and `canon claude sync` on each domain',
|
|
61
|
+
'installed, so each carries its own replace behavior: governance',
|
|
62
|
+
'replaces an edited rule and deletes a retired one, and claude writes',
|
|
63
|
+
'.gitignore alone.',
|
|
65
64
|
'Tooling is not synced. Refuses a dirty working tree.',
|
|
66
65
|
'',
|
|
67
66
|
'Exit codes:',
|
|
@@ -424,7 +423,7 @@ async function runSync(target: string): Promise<number> {
|
|
|
424
423
|
/**
|
|
425
424
|
* Each domain sync runs as its own process. They open their own frames and
|
|
426
425
|
* prompt for their own changes, so stdin stays inherited. Claude is the one
|
|
427
|
-
* exception:
|
|
426
|
+
* exception: its stdin is closed because the combined pull request preview is the
|
|
428
427
|
* single confirmation gate for the whole sync, and `canon claude sync` writes
|
|
429
428
|
* only `.gitignore`.
|
|
430
429
|
*/
|
package/src/commands/tooling.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
logStep,
|
|
37
37
|
logWarn,
|
|
38
38
|
outro,
|
|
39
|
-
|
|
39
|
+
outroSuccess,
|
|
40
40
|
select,
|
|
41
41
|
} from '@/cli/ui'
|
|
42
42
|
|
|
@@ -101,7 +101,7 @@ export function register(program: Command): void {
|
|
|
101
101
|
'Examples:',
|
|
102
102
|
' canon tooling sync base',
|
|
103
103
|
' canon tooling sync base --check',
|
|
104
|
-
'
|
|
104
|
+
' canon tooling sync base --write',
|
|
105
105
|
' canon tooling sync vite-react web --skip base',
|
|
106
106
|
'',
|
|
107
107
|
'A target below its git root syncs as a subfolder. It withholds',
|
|
@@ -412,7 +412,6 @@ async function runSync(
|
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
const mode = resolveWriteMode(opts)
|
|
415
|
-
const { GREEN, NC } = palette(process.stderr)
|
|
416
415
|
|
|
417
416
|
if (result.totalChanges === 0) {
|
|
418
417
|
// The stamp is a write like any other, so a run with no authority to write
|
|
@@ -421,8 +420,7 @@ async function runSync(
|
|
|
421
420
|
if (mode === 'apply' || mode === 'prompt') {
|
|
422
421
|
await stampChain(prepared.chain, prepared.target)
|
|
423
422
|
}
|
|
424
|
-
|
|
425
|
-
process.stderr.write(`${GREEN}✓ Everything up to date${NC}\n`)
|
|
423
|
+
outroSuccess('Everything up to date')
|
|
426
424
|
return 0
|
|
427
425
|
}
|
|
428
426
|
|
|
@@ -450,8 +448,7 @@ async function runSync(
|
|
|
450
448
|
|
|
451
449
|
await stampChain(prepared.chain, prepared.target)
|
|
452
450
|
|
|
453
|
-
|
|
454
|
-
process.stderr.write(`${GREEN}✓ Tooling sync complete${NC}\n`)
|
|
451
|
+
outroSuccess('Tooling sync complete')
|
|
455
452
|
return 0
|
|
456
453
|
}
|
|
457
454
|
|
|
@@ -607,7 +604,7 @@ async function promptForStack(): Promise<string | undefined> {
|
|
|
607
604
|
return select({
|
|
608
605
|
message: 'Select tooling stack:',
|
|
609
606
|
options: stacks.map((name) => ({ value: name, label: name })),
|
|
610
|
-
|
|
607
|
+
ttyHint: `Pass a stack, one of: ${stacks.join(', ')}`,
|
|
611
608
|
})
|
|
612
609
|
}
|
|
613
610
|
|
|
@@ -2,7 +2,7 @@ import { resolve } from 'node:path'
|
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
3
|
import { recordDir } from '@/roots/record'
|
|
4
4
|
import { ensureYtDlp, fetchOne } from '@/transcripts/fetch'
|
|
5
|
-
import {
|
|
5
|
+
import { intro, logError, logInfo, outro } from '@/cli/ui'
|
|
6
6
|
import { mainWorktreeRoot } from '@/git/worktree'
|
|
7
7
|
|
|
8
8
|
interface TranscriptOptions {
|
|
@@ -37,10 +37,7 @@ export function register(program: Command): void {
|
|
|
37
37
|
)
|
|
38
38
|
.action(async (url: string, opts: TranscriptOptions) => {
|
|
39
39
|
const outDir = await resolveOutDir(opts)
|
|
40
|
-
|
|
41
|
-
process.stderr.write(
|
|
42
|
-
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}canon transcripts${NC}\n`,
|
|
43
|
-
)
|
|
40
|
+
intro('canon transcripts')
|
|
44
41
|
try {
|
|
45
42
|
ensureYtDlp()
|
|
46
43
|
const target = await fetchOne(url, {
|
|
@@ -48,14 +45,12 @@ export function register(program: Command): void {
|
|
|
48
45
|
keepTimestamps: opts.keepTimestamps ?? false,
|
|
49
46
|
})
|
|
50
47
|
process.stdout.write(`${target}\n`)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
48
|
+
logInfo(target)
|
|
49
|
+
outro()
|
|
54
50
|
} catch (error) {
|
|
55
51
|
const message = error instanceof Error ? error.message : String(error)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
52
|
+
logError(message)
|
|
53
|
+
outro()
|
|
59
54
|
process.exitCode = 1
|
|
60
55
|
}
|
|
61
56
|
})
|