@erclx/canon 5.54.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 +8 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/create-skill/SKILL.md +1 -1
- package/claude/skills/document-health/SKILL.md +6 -6
- package/claude/skills/document-repair/SKILL.md +5 -5
- 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/docs/agents/install-and-sync.md +2 -2
- package/docs/agents/output-shape.md +11 -10
- package/docs/target/sync.md +1 -1
- package/package.json +1 -1
- 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/routing.ts +1 -1
- package/src/cli/run.ts +5 -6
- package/src/cli/ui.ts +87 -28
- package/src/cli.ts +6 -0
- package/src/commands/claude.ts +2 -3
- package/src/commands/design.ts +8 -13
- 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 +1 -1
- package/src/commands/tooling.ts +5 -8
- package/src/commands/transcripts.ts +6 -11
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +0 -10
- package/src/hooks/post-merge.ts +2 -5
- 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/sync/engine.ts +3 -8
- 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/scripts/core/check-color-source.sh +0 -51
package/src/cli/ui.ts
CHANGED
|
@@ -53,7 +53,38 @@ export function palette(stream: { isTTY?: boolean }): Palette {
|
|
|
53
53
|
return supportsColor(stream) ? COLOR : PLAIN
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
let quiet = false
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Silences the progress frame for a caller that asked for a record, since an
|
|
60
|
+
* agent reads both streams and the frame outweighed the data. Warnings and
|
|
61
|
+
* errors still write, since diagnostics reach stderr in every mode.
|
|
62
|
+
*/
|
|
63
|
+
export function setQuiet(on: boolean): void {
|
|
64
|
+
quiet = on
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The frame is drawn only for a terminal. A pipe or a file gets each line's
|
|
69
|
+
* mark alone, since a reader there gains nothing from `┌ │ ├ └` and an agent
|
|
70
|
+
* pays for every one of them.
|
|
71
|
+
*/
|
|
72
|
+
function framed(): boolean {
|
|
73
|
+
return process.stderr.isTTY === true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function markLine(color: keyof Palette, mark: string, message: string): string {
|
|
77
|
+
if (!framed()) return `${mark} ${message}\n`
|
|
78
|
+
const { GREY, NC, [color]: tint } = palette(process.stderr)
|
|
79
|
+
return `${GREY}│${NC} ${tint}${mark}${NC} ${message}\n`
|
|
80
|
+
}
|
|
81
|
+
|
|
56
82
|
export function intro(title: string): void {
|
|
83
|
+
if (quiet) return
|
|
84
|
+
if (!framed()) {
|
|
85
|
+
process.stderr.write(`${title}\n`)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
57
88
|
const { GREY, NC, WHITE } = palette(process.stderr)
|
|
58
89
|
process.stderr.write(`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}${title}${NC}\n`)
|
|
59
90
|
}
|
|
@@ -64,23 +95,22 @@ export function intro(title: string): void {
|
|
|
64
95
|
* leaving stdout clean for JSON and lists.
|
|
65
96
|
*/
|
|
66
97
|
export function logInfo(message: string): void {
|
|
67
|
-
|
|
68
|
-
process.stderr.write(
|
|
98
|
+
if (quiet) return
|
|
99
|
+
process.stderr.write(markLine('GREEN', '✓', message))
|
|
69
100
|
}
|
|
70
101
|
|
|
71
102
|
export function logWarn(message: string): void {
|
|
72
|
-
|
|
73
|
-
process.stderr.write(`${GREY}│${NC} ${YELLOW}!${NC} ${message}\n`)
|
|
103
|
+
process.stderr.write(markLine('YELLOW', '!', message))
|
|
74
104
|
}
|
|
75
105
|
|
|
76
106
|
export function logAdd(message: string): void {
|
|
77
|
-
|
|
78
|
-
process.stderr.write(
|
|
107
|
+
if (quiet) return
|
|
108
|
+
process.stderr.write(markLine('GREEN', '+', message))
|
|
79
109
|
}
|
|
80
110
|
|
|
81
111
|
export function logRemove(message: string): void {
|
|
82
|
-
|
|
83
|
-
process.stderr.write(
|
|
112
|
+
if (quiet) return
|
|
113
|
+
process.stderr.write(markLine('RED', '-', message))
|
|
84
114
|
}
|
|
85
115
|
|
|
86
116
|
/**
|
|
@@ -89,31 +119,48 @@ export function logRemove(message: string): void {
|
|
|
89
119
|
* exit code rather than terminating mid-write.
|
|
90
120
|
*/
|
|
91
121
|
export function logError(message: string): void {
|
|
92
|
-
|
|
93
|
-
process.stderr.write(`${GREY}│${NC} ${RED}✗${NC} ${message}\n`)
|
|
122
|
+
process.stderr.write(markLine('RED', '✗', message))
|
|
94
123
|
}
|
|
95
124
|
|
|
96
125
|
export function logStep(message: string): void {
|
|
126
|
+
if (quiet) return
|
|
127
|
+
if (!framed()) {
|
|
128
|
+
process.stderr.write(`${message}\n`)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
97
131
|
const { GREY, NC, WHITE } = palette(process.stderr)
|
|
98
132
|
process.stderr.write(`${GREY}│${NC}\n${GREY}├${NC} ${WHITE}${message}${NC}\n`)
|
|
99
133
|
}
|
|
100
134
|
|
|
101
135
|
export function outro(): void {
|
|
136
|
+
if (quiet || !framed()) return
|
|
102
137
|
const { GREY, NC } = palette(process.stderr)
|
|
103
138
|
process.stderr.write(`${GREY}└${NC}\n`)
|
|
104
139
|
}
|
|
105
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Closes the frame on the green line a command ends a success on. The line
|
|
143
|
+
* sits below the frame rather than inside it, and a quiet run writes neither.
|
|
144
|
+
*/
|
|
145
|
+
export function outroSuccess(message: string, detail?: string): void {
|
|
146
|
+
if (quiet) return
|
|
147
|
+
outro()
|
|
148
|
+
const { GREEN, GREY, NC } = palette(process.stderr)
|
|
149
|
+
const suffix = detail === undefined ? '' : ` ${GREY}${detail}${NC}`
|
|
150
|
+
process.stderr.write(`${GREEN}✓ ${message}${NC}${suffix}\n`)
|
|
151
|
+
}
|
|
152
|
+
|
|
106
153
|
/**
|
|
107
154
|
* Indents borrowed output inside an open frame, matching `pipe_output` in
|
|
108
155
|
* `scripts/lib/ui.sh`. Used for a document the command did not compose itself,
|
|
109
156
|
* such as a pull request body or the output of a git mutation.
|
|
110
157
|
*/
|
|
111
158
|
export function pipeOutput(text: string): void {
|
|
159
|
+
if (quiet) return
|
|
112
160
|
const { GREY, NC } = palette(process.stderr)
|
|
161
|
+
const bar = framed() ? `${GREY}│${NC}` : ''
|
|
113
162
|
const lines = text.replace(/\n$/, '').split('\n')
|
|
114
|
-
process.stderr.write(
|
|
115
|
-
`${lines.map((line) => `${GREY}│${NC} ${line}`).join('\n')}\n`,
|
|
116
|
-
)
|
|
163
|
+
process.stderr.write(`${lines.map((line) => `${bar} ${line}`).join('\n')}\n`)
|
|
117
164
|
}
|
|
118
165
|
|
|
119
166
|
/** Counts a noun for a report line, where the plural is the bare `s` form. */
|
|
@@ -122,6 +169,10 @@ export function plural(count: number, noun: string): string {
|
|
|
122
169
|
}
|
|
123
170
|
|
|
124
171
|
export function frameError(message: string): void {
|
|
172
|
+
if (!framed()) {
|
|
173
|
+
process.stderr.write(`✗ ${message}\n`)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
125
176
|
const { GREY, NC, RED } = palette(process.stderr)
|
|
126
177
|
process.stderr.write(
|
|
127
178
|
`${GREY}┌${NC}\n${GREY}│${NC} ${RED}✗${NC} ${message}\n${GREY}└${NC}\n`,
|
|
@@ -129,6 +180,11 @@ export function frameError(message: string): void {
|
|
|
129
180
|
}
|
|
130
181
|
|
|
131
182
|
export function frameSuccess(command: string, target: string): void {
|
|
183
|
+
if (quiet) return
|
|
184
|
+
if (!framed()) {
|
|
185
|
+
process.stderr.write(`${command}\n✓ ${target}\n`)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
132
188
|
const { GREEN, GREY, NC, WHITE } = palette(process.stderr)
|
|
133
189
|
process.stderr.write(
|
|
134
190
|
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}${command}${NC}\n${GREY}│${NC}\n${GREY}│${NC} ${GREEN}✓${NC} ${target}\n${GREY}└${NC}\n`,
|
|
@@ -136,17 +192,17 @@ export function frameSuccess(command: string, target: string): void {
|
|
|
136
192
|
}
|
|
137
193
|
|
|
138
194
|
/**
|
|
139
|
-
*
|
|
195
|
+
* A caller with no terminal on stdin is headless, since nobody can answer a
|
|
196
|
+
* prompt there.
|
|
140
197
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* command that quietly waits for a TTY nobody is watching rather than one that
|
|
146
|
-
* refuses and says why.
|
|
198
|
+
* Both variables are retired overrides that force headless on a terminal and
|
|
199
|
+
* stay readable permanently. Skill bodies, CI jobs, and operator scripts in
|
|
200
|
+
* other repositories and plugin caches this release cannot reach still set
|
|
201
|
+
* them, and dropping the read turns their call into a prompt nobody watches.
|
|
147
202
|
*/
|
|
148
203
|
export function isNonInteractive(): boolean {
|
|
149
204
|
return (
|
|
205
|
+
!process.stdin.isTTY ||
|
|
150
206
|
process.env.CANON_NON_INTERACTIVE === '1' ||
|
|
151
207
|
// canon-keep-retired
|
|
152
208
|
process.env.AITK_NON_INTERACTIVE === '1'
|
|
@@ -155,8 +211,8 @@ export function isNonInteractive(): boolean {
|
|
|
155
211
|
|
|
156
212
|
/**
|
|
157
213
|
* `nonInteractiveDefault` opts a prompt into resolving to its first option
|
|
158
|
-
*
|
|
159
|
-
* for the user leave it off.
|
|
214
|
+
* when the caller is headless instead of failing. Callers that would rather
|
|
215
|
+
* fail loudly than pick for the user leave it off.
|
|
160
216
|
*/
|
|
161
217
|
export async function select<Value>(opts: {
|
|
162
218
|
message: string
|
|
@@ -171,9 +227,11 @@ export async function select<Value>(opts: {
|
|
|
171
227
|
let cursor = 0
|
|
172
228
|
|
|
173
229
|
if (opts.nonInteractiveDefault && isNonInteractive()) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
230
|
+
if (!quiet) {
|
|
231
|
+
process.stderr.write(
|
|
232
|
+
markLine('GREY', '◇', `${message} ${options[0].label}`),
|
|
233
|
+
)
|
|
234
|
+
}
|
|
177
235
|
return options[0].value
|
|
178
236
|
}
|
|
179
237
|
|
|
@@ -196,10 +254,11 @@ export async function select<Value>(opts: {
|
|
|
196
254
|
}
|
|
197
255
|
|
|
198
256
|
return new Promise<Value>((resolve) => {
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
`${
|
|
257
|
+
if (isNonInteractive()) {
|
|
258
|
+
logError(
|
|
259
|
+
`${message} requires a TTY. ${opts.ttyHint ?? 'Pass an argument'}.`,
|
|
202
260
|
)
|
|
261
|
+
outro()
|
|
203
262
|
process.exit(1)
|
|
204
263
|
}
|
|
205
264
|
|
package/src/cli.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { register as changelog } from '@/commands/changelog'
|
|
|
40
40
|
import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
|
|
41
41
|
import { renderHelp } from '@/cli/help'
|
|
42
42
|
import { renderAll, routeHelp } from '@/cli/help-all'
|
|
43
|
+
import { setQuiet } from '@/cli/ui'
|
|
43
44
|
|
|
44
45
|
function showHelp(): void {
|
|
45
46
|
console.log(renderHelp(process.stdout))
|
|
@@ -57,6 +58,11 @@ program.on('option:help', () => {
|
|
|
57
58
|
process.exit(0)
|
|
58
59
|
})
|
|
59
60
|
program.option('-h, --help', 'Show help')
|
|
61
|
+
// A caller asking for a record reads it from stdout, so the frame on stderr is
|
|
62
|
+
// noise it pays for. Warnings and errors still write.
|
|
63
|
+
program.hook('preAction', (_program, action) => {
|
|
64
|
+
if (action.opts().json === true) setQuiet(true)
|
|
65
|
+
})
|
|
60
66
|
|
|
61
67
|
init(program)
|
|
62
68
|
sandbox(program)
|
package/src/commands/claude.ts
CHANGED
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
logStep,
|
|
92
92
|
logWarn,
|
|
93
93
|
outro,
|
|
94
|
+
outroSuccess,
|
|
94
95
|
palette,
|
|
95
96
|
pipeOutput,
|
|
96
97
|
plural,
|
|
@@ -604,9 +605,7 @@ export function register(program: Command): void {
|
|
|
604
605
|
}
|
|
605
606
|
|
|
606
607
|
function succeed(message: string): number {
|
|
607
|
-
|
|
608
|
-
outro()
|
|
609
|
-
process.stderr.write(`${GREEN}✓ ${message}${NC}\n`)
|
|
608
|
+
outroSuccess(message)
|
|
610
609
|
return 0
|
|
611
610
|
}
|
|
612
611
|
|
package/src/commands/design.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { buildDesignCss } from '@/design/css'
|
|
|
5
5
|
import { HAND_DRAWN_FONT_FACES } from '@/design/fonts'
|
|
6
6
|
import { DESIGN_DOCUMENT, regenDesign } from '@/design/regen'
|
|
7
7
|
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
|
|
8
|
-
import { logWarn,
|
|
8
|
+
import { intro, logError, logInfo, logWarn, outro } from '@/cli/ui'
|
|
9
9
|
|
|
10
10
|
export function register(program: Command): void {
|
|
11
11
|
const design = program
|
|
@@ -29,8 +29,6 @@ export function register(program: Command): void {
|
|
|
29
29
|
].join('\n'),
|
|
30
30
|
)
|
|
31
31
|
.action(() => {
|
|
32
|
-
const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
|
|
33
|
-
|
|
34
32
|
// The guard below catches a `PROJECT_ROOT` with no record at all, which
|
|
35
33
|
// is an installed package. It cannot catch a second real checkout that
|
|
36
34
|
// has one, so both paths carry the warning. It is a frame-interior line,
|
|
@@ -42,24 +40,21 @@ export function register(program: Command): void {
|
|
|
42
40
|
// The record is already committed here and ships with no package, so
|
|
43
41
|
// its absence is what separates the two.
|
|
44
42
|
if (!existsSync(join(PROJECT_ROOT, DESIGN_DOCUMENT))) {
|
|
45
|
-
|
|
43
|
+
intro('Regenerate design source')
|
|
46
44
|
if (mismatch !== undefined) logWarn(mismatch)
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
logError(
|
|
46
|
+
`No ${DESIGN_DOCUMENT} at ${PROJECT_ROOT}. Regen runs in the toolkit checkout, not against a target.`,
|
|
49
47
|
)
|
|
48
|
+
outro()
|
|
50
49
|
process.exitCode = 1
|
|
51
50
|
return
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}Regenerate design source${NC}\n`,
|
|
56
|
-
)
|
|
53
|
+
intro('Regenerate design source')
|
|
57
54
|
if (mismatch !== undefined) logWarn(mismatch)
|
|
58
55
|
const result = regenDesign(PROJECT_ROOT)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
process.stderr.write(`${GREY}└${NC}\n`)
|
|
56
|
+
logInfo(relative(PROJECT_ROOT, result.documentPath))
|
|
57
|
+
outro()
|
|
63
58
|
})
|
|
64
59
|
|
|
65
60
|
design
|
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
|
@@ -423,7 +423,7 @@ async function runSync(target: string): Promise<number> {
|
|
|
423
423
|
/**
|
|
424
424
|
* Each domain sync runs as its own process. They open their own frames and
|
|
425
425
|
* prompt for their own changes, so stdin stays inherited. Claude is the one
|
|
426
|
-
* exception:
|
|
426
|
+
* exception: its stdin is closed because the combined pull request preview is the
|
|
427
427
|
* single confirmation gate for the whole sync, and `canon claude sync` writes
|
|
428
428
|
* only `.gitignore`.
|
|
429
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
|
})
|
package/src/gate/sequencer.ts
CHANGED
package/src/gate/stages.ts
CHANGED
|
@@ -119,12 +119,6 @@ const TESTS_SCOPE = new RegExp(
|
|
|
119
119
|
].join('|'),
|
|
120
120
|
)
|
|
121
121
|
|
|
122
|
-
const script = (name: string, failure: string): Check => ({
|
|
123
|
-
kind: 'command',
|
|
124
|
-
argv: ['bash', `scripts/core/${name}`],
|
|
125
|
-
failure,
|
|
126
|
-
})
|
|
127
|
-
|
|
128
122
|
/**
|
|
129
123
|
* Every stage the gate runs, in the order it runs them.
|
|
130
124
|
*
|
|
@@ -530,10 +524,6 @@ export const STAGES: readonly Stage[] = [
|
|
|
530
524
|
argv: ['bun', 'run', 'check:shell'],
|
|
531
525
|
failure: 'Shell check failed',
|
|
532
526
|
},
|
|
533
|
-
script(
|
|
534
|
-
'check-color-source.sh',
|
|
535
|
-
'A color escape is defined outside scripts/lib/ui.sh.',
|
|
536
|
-
),
|
|
537
527
|
],
|
|
538
528
|
success: 'Shell check passed',
|
|
539
529
|
},
|
package/src/hooks/post-merge.ts
CHANGED
|
@@ -359,11 +359,8 @@ export async function runPostMerge(input: PostMergeInput): Promise<void> {
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
if (!isSkipped('CANON_SKIP_UPGRADE')) {
|
|
362
|
-
await step(
|
|
363
|
-
|
|
364
|
-
at([...STEP_VERBS.upgrade, '--json'], { CANON_NON_INTERACTIVE: '1' }),
|
|
365
|
-
(result, fields) =>
|
|
366
|
-
moveLines(result, fields, '⬆️ ', 'CLI not upgraded', []),
|
|
362
|
+
await step(input, at([...STEP_VERBS.upgrade, '--json']), (result, fields) =>
|
|
363
|
+
moveLines(result, fields, '⬆️ ', 'CLI not upgraded', []),
|
|
367
364
|
)
|
|
368
365
|
}
|
|
369
366
|
|
package/src/process/harness.ts
CHANGED
|
@@ -90,8 +90,7 @@ export function snapshotStateDir(): string {
|
|
|
90
90
|
*
|
|
91
91
|
* A git hook exports `GIT_DIR`, which would resolve a fixture's git-aware
|
|
92
92
|
* reads against this checkout instead of the temporary directory a case
|
|
93
|
-
* builds, so every spawn drops the `GIT_` prefix
|
|
94
|
-
* flag every case needs to avoid a picker blocking on stdin.
|
|
93
|
+
* builds, so every spawn drops the `GIT_` prefix.
|
|
95
94
|
*
|
|
96
95
|
* `CANON_STATE_DIR` and `CANON_SANDBOX_DIR` get the same treatment as `GIT_DIR`,
|
|
97
96
|
* each pointed at a folder under the case's own `cwd` rather than dropped,
|
|
@@ -99,7 +98,8 @@ export function snapshotStateDir(): string {
|
|
|
99
98
|
* `HOME` to this machine's real `~/.local/state/canon`. `stateDir()` and
|
|
100
99
|
* `sandboxTree()` resolve the same three ways and share that parent, so both
|
|
101
100
|
* overrides move together. A case explicitly passing its own value through
|
|
102
|
-
* `options.env` still wins
|
|
101
|
+
* `options.env` still wins. The child's stdin is a pipe rather than a
|
|
102
|
+
* terminal, so every case runs headless.
|
|
103
103
|
*
|
|
104
104
|
* The `stateDir()` snapshot before and after the spawn is what actually
|
|
105
105
|
* catches an escape past that redirection, since a default can be wrong in a
|
|
@@ -125,7 +125,6 @@ export function runCli(
|
|
|
125
125
|
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
126
126
|
env: {
|
|
127
127
|
...inherited,
|
|
128
|
-
CANON_NON_INTERACTIVE: '1',
|
|
129
128
|
CANON_STATE_DIR: join(options.cwd, '.canon-state'),
|
|
130
129
|
CANON_SANDBOX_DIR: join(options.cwd, '.canon-state', 'sandbox'),
|
|
131
130
|
...options.env,
|