@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.
Files changed (127) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +10 -9
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +5 -5
  5. package/claude/skills/create-skill/SKILL.md +1 -1
  6. package/claude/skills/design-identity/REQUIREMENT.md +2 -2
  7. package/claude/skills/design-identity/SKILL.md +6 -6
  8. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  9. package/claude/skills/design-record/SKILL.md +4 -4
  10. package/claude/skills/design-record/references/folder.md +1 -1
  11. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  12. package/claude/skills/document-health/SKILL.md +6 -6
  13. package/claude/skills/document-repair/SKILL.md +5 -5
  14. package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
  15. package/claude/skills/draw-figure/SKILL.md +1 -1
  16. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
  17. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  18. package/claude/skills/pick-by-eye/references/board.md +42 -0
  19. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  20. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  21. package/claude/skills/target-check/SKILL.md +6 -8
  22. package/claude/skills/target-check/references/domains.md +1 -4
  23. package/claude/skills/target-setup/SKILL.md +8 -8
  24. package/claude/skills/target-update/SKILL.md +5 -5
  25. package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
  26. package/claude/skills/teach-craft/SKILL.md +32 -1
  27. package/claude/skills/teach-craft/references/adopted.md +113 -0
  28. package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
  29. package/claude/skills/teach-craft/references/pedagogy.md +29 -2
  30. package/docs/agents/install-and-sync.md +2 -10
  31. package/docs/agents/output-shape.md +11 -10
  32. package/docs/target/migrations.md +8 -3
  33. package/docs/target/projects.md +1 -1
  34. package/docs/target/sync.md +1 -1
  35. package/docs/workflow/skill-map.md +0 -1
  36. package/docs/workflow/visual-design-workflow.md +1 -4
  37. package/governance/detect.toml +1 -6
  38. package/governance/renames.toml +0 -1
  39. package/package.json +2 -5
  40. package/scripts/core/install-check.sh +1 -1
  41. package/scripts/lib/ui.sh +12 -65
  42. package/src/audits/run.ts +1 -1
  43. package/src/claude/cases/misc.ts +0 -5
  44. package/src/claude/routing.ts +1 -1
  45. package/src/claude/skills-audit.ts +1 -0
  46. package/src/cli/help.ts +0 -3
  47. package/src/cli/run.ts +5 -6
  48. package/src/cli/ui.ts +87 -28
  49. package/src/cli.ts +6 -2
  50. package/src/commands/claude.ts +2 -3
  51. package/src/commands/design.ts +20 -324
  52. package/src/commands/gate.ts +5 -3
  53. package/src/commands/gov.ts +11 -21
  54. package/src/commands/labels.ts +1 -1
  55. package/src/commands/pr.ts +1 -1
  56. package/src/commands/sync.ts +5 -6
  57. package/src/commands/tooling.ts +5 -8
  58. package/src/commands/transcripts.ts +6 -11
  59. package/src/design/regen.ts +5 -32
  60. package/src/design/render.ts +1 -1
  61. package/src/design/tokens.ts +3 -2
  62. package/src/gate/sequencer.ts +1 -1
  63. package/src/gate/stages.ts +4 -20
  64. package/src/gov/audit.ts +1 -6
  65. package/src/hooks/post-merge.ts +2 -5
  66. package/src/markdown/scope.ts +2 -4
  67. package/src/process/harness.ts +3 -4
  68. package/src/sandbox/equivalence.ts +0 -1
  69. package/src/sandbox/provision.ts +8 -13
  70. package/src/sandbox/scenario.ts +6 -12
  71. package/src/shipped/references.ts +1 -2
  72. package/src/sync/check.ts +4 -29
  73. package/src/sync/engine.ts +3 -8
  74. package/src/sync/stamp.ts +6 -6
  75. package/src/sync/target.ts +1 -4
  76. package/src/sync/workflow.ts +3 -5
  77. package/src/tooling/verify-stack.ts +1 -1
  78. package/tooling/base/configs/.husky/post-merge +6 -10
  79. package/tooling/claude/reference.md +1 -1
  80. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  81. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  82. package/claude/skills/canvas-board/SKILL.md +0 -143
  83. package/claude/skills/canvas-board/references/canvas.md +0 -170
  84. package/governance/rules/ui/canvas.md +0 -13
  85. package/scripts/core/check-color-source.sh +0 -51
  86. package/src/canvas/address.ts +0 -227
  87. package/src/canvas/arrange.ts +0 -66
  88. package/src/canvas/capture.ts +0 -391
  89. package/src/canvas/client/app.tsx +0 -188
  90. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  91. package/src/canvas/client/inspector/color-field.tsx +0 -398
  92. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  93. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  94. package/src/canvas/client/inspector/color-space.ts +0 -240
  95. package/src/canvas/client/inspector/field.tsx +0 -263
  96. package/src/canvas/client/inspector/fields.css +0 -763
  97. package/src/canvas/client/inspector/section.tsx +0 -27
  98. package/src/canvas/client/inspector/size-field.tsx +0 -69
  99. package/src/canvas/client/inspector/token-select.tsx +0 -109
  100. package/src/canvas/client/inspector/values.ts +0 -297
  101. package/src/canvas/client/inspector.tsx +0 -692
  102. package/src/canvas/client/keys.ts +0 -71
  103. package/src/canvas/client/layers.tsx +0 -118
  104. package/src/canvas/client/main.tsx +0 -71
  105. package/src/canvas/client/pages-panel.tsx +0 -212
  106. package/src/canvas/client/panel-handle.tsx +0 -90
  107. package/src/canvas/client/selection.tsx +0 -199
  108. package/src/canvas/client/shell.css +0 -762
  109. package/src/canvas/client/state.ts +0 -792
  110. package/src/canvas/client/surface.tsx +0 -803
  111. package/src/canvas/client/theme-panel.tsx +0 -55
  112. package/src/canvas/client/tools.tsx +0 -153
  113. package/src/canvas/content.ts +0 -781
  114. package/src/canvas/edit.ts +0 -575
  115. package/src/canvas/editing.ts +0 -203
  116. package/src/canvas/fit.ts +0 -238
  117. package/src/canvas/global.d.ts +0 -15
  118. package/src/canvas/history.ts +0 -116
  119. package/src/canvas/index.html +0 -20
  120. package/src/canvas/server.ts +0 -766
  121. package/src/canvas/tokens.ts +0 -221
  122. package/src/commands/canvas.ts +0 -1097
  123. package/src/design/adapter.ts +0 -59
  124. package/src/design/base.css +0 -203
  125. package/src/design/brand-favicon.ts +0 -18
  126. package/src/design/neutral.ts +0 -127
  127. package/src/design/project-fonts.ts +0 -392
@@ -9,8 +9,7 @@
9
9
  # root and in every folder to depth two. `files` are globs matched against the
10
10
  # target's git-listed paths to depth two, and against each folder on those
11
11
  # paths, so `.github/workflows` matches a folder holding a workflow. A glob with
12
- # no wildcard is also tested on disk, which reaches a gitignored path such as
13
- # `.canon/canvas`.
12
+ # no wildcard is also tested on disk, which reaches a gitignored path.
14
13
  #
15
14
  # Every rule no stack reaches needs a table here, which the `unreferenced-rules`
16
15
  # gate stage checks, along with every key naming a live rule. A table for a rule
@@ -201,10 +200,6 @@ files = ["index.html", "*/index.html"]
201
200
  deps = ["react", "vue", "svelte", "astro", "next", "solid-js", "preact"]
202
201
  files = ["index.html", "*/index.html"]
203
202
 
204
- ["canvas"]
205
- deps = []
206
- files = [".canon/canvas"]
207
-
208
203
  ["design-reference"]
209
204
  deps = []
210
205
  files = ["canon/design/*/"]
@@ -73,4 +73,3 @@
73
73
  "460-design-taste" = "design-taste"
74
74
  "465-interface-casing" = "interface-casing"
75
75
  "470-motion" = "motion"
76
- "480-canvas" = "canvas"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.53.0",
4
+ "version": "5.55.0",
5
5
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -51,11 +51,9 @@
51
51
  "web:e2e": "cd web && playwright test"
52
52
  },
53
53
  "dependencies": {
54
- "@preact/signals": "^2.11.3",
55
54
  "commander": "^13.1.0",
56
55
  "execa": "^9.5.2",
57
- "playwright-core": "1.64.1",
58
- "preact": "^11.0.0"
56
+ "playwright-core": "1.64.1"
59
57
  },
60
58
  "devDependencies": {
61
59
  "@astrojs/check": "^0.9.8",
@@ -68,7 +66,6 @@
68
66
  "@types/bun": "^1.2.10",
69
67
  "@types/micromatch": "^4.0.10",
70
68
  "astro": "^6.1.9",
71
- "happy-dom": "^20.14.5",
72
69
  "husky": "^9.1.7",
73
70
  "micromatch": "^4.0.8",
74
71
  "oxfmt": "0.73.0",
@@ -88,7 +88,7 @@ mkdir -p "$TARGET_DIR"
88
88
  log_info "Initialized git in $TARGET_DIR"
89
89
 
90
90
  log_step "Run canon init"
91
- (cd "$TARGET_DIR" && CANON_NON_INTERACTIVE=1 bun run "$EXTRACT_DIR/src/cli.ts" init --stack base 2>&1 | pipe_output) || log_error "canon init failed"
91
+ (cd "$TARGET_DIR" && bun run "$EXTRACT_DIR/src/cli.ts" init --stack base </dev/null 2>&1 | pipe_output) || log_error "canon init failed"
92
92
  log_info "canon init completed"
93
93
 
94
94
  log_step "Assert scaffold"
package/scripts/lib/ui.sh CHANGED
@@ -6,93 +6,40 @@ if [ "${BASH_VERSINFO[0]:-0}" -lt 4 ]; then
6
6
  exit 1
7
7
  fi
8
8
 
9
- # `NO_COLOR` follows the published convention, where any non-empty value turns
10
- # color off whatever the value says.
11
- supports_color() {
12
- [ -z "${NO_COLOR:-}" ] && [ -t "$1" ]
13
- }
14
-
15
- # Assigns the six palette names for one file descriptor. The blank palette keeps
16
- # every frame character and drops only the escapes, so a captured run still
17
- # reads as one block.
18
- #
19
- # Bash returns a string and nothing richer, so the palette arrives through the
20
- # caller's own scope. A writer declaring all six `local` before calling this
21
- # keeps the assignment to itself, which is what leaves the source-time answer
22
- # below standing for anything that sources this file.
23
- set_palette() {
24
- if supports_color "$1"; then
25
- GREEN='\033[0;32m'
26
- RED='\033[0;31m'
27
- YELLOW='\033[0;33m'
28
- WHITE='\033[1;37m'
29
- GREY='\033[0;90m'
30
- NC='\033[0m'
31
- else
32
- GREEN=''
33
- RED=''
34
- YELLOW=''
35
- WHITE=''
36
- GREY=''
37
- NC=''
38
- fi
39
- }
40
-
41
- # Every writer below frames to stderr and asks about that stream at write time,
42
- # so no answer is frozen before a caller's redirect is in place. This one answers
43
- # for stdout instead, because it is read by a sourcing script writing its own
44
- # frame with a bare `echo`, and that is the stream a bare `echo` reaches.
45
- set_palette 1
9
+ # The frame writes no color. The scripts left on it run as setup and gate steps,
10
+ # where a pipe or a log reads the output more often than a terminal does.
46
11
 
47
12
  log_info() {
48
- local GREEN RED YELLOW WHITE GREY NC
49
- set_palette 2
50
- echo -e "${GREY}│${NC} ${GREEN}✓${NC} $1" >&2
13
+ echo -e "│ ✓ $1" >&2
51
14
  }
52
15
  log_warn() {
53
- local GREEN RED YELLOW WHITE GREY NC
54
- set_palette 2
55
- echo -e "${GREY}│${NC} ${YELLOW}!${NC} $1" >&2
16
+ echo -e "│ ! $1" >&2
56
17
  }
57
18
  log_error() {
58
- local GREEN RED YELLOW WHITE GREY NC
59
- set_palette 2
60
- echo -e "${GREY}│${NC} ${RED}✗${NC} $1" >&2
19
+ echo -e "│ ✗ $1" >&2
61
20
  exit 1
62
21
  }
63
22
  log_step() {
64
- local GREEN RED YELLOW WHITE GREY NC
65
- set_palette 2
66
- echo -e "${GREY}│${NC}\n${GREY}├${NC} ${WHITE}$1${NC}" >&2
23
+ echo -e "│\n├ $1" >&2
67
24
  }
68
25
  log_add() {
69
- local GREEN RED YELLOW WHITE GREY NC
70
- set_palette 2
71
- echo -e "${GREY}│${NC} ${GREEN}+${NC} $1" >&2
26
+ echo -e "│ + $1" >&2
72
27
  }
73
28
  log_rem() {
74
- local GREEN RED YELLOW WHITE GREY NC
75
- set_palette 2
76
- echo -e "${GREY}│${NC} ${RED}-${NC} $1" >&2
29
+ echo -e "│ - $1" >&2
77
30
  }
78
31
 
79
32
  pipe_output() {
80
- local GREEN RED YELLOW WHITE GREY NC
81
- set_palette 2
82
- while IFS= read -r line; do echo -e "${GREY}│${NC} $line" >&2; done
33
+ while IFS= read -r line; do echo -e "│ $line" >&2; done
83
34
  }
84
35
 
85
36
  open_timeline() {
86
- local GREEN RED YELLOW WHITE GREY NC
87
- set_palette 2
88
- echo -e "${GREY}┌${NC}" >&2
37
+ echo -e "┌" >&2
89
38
  if [ -n "${1:-}" ]; then
90
- echo -e "${GREY}│${NC} ${WHITE}$1${NC}" >&2
39
+ echo -e "│ $1" >&2
91
40
  fi
92
41
  }
93
42
 
94
43
  close_timeline() {
95
- local GREEN RED YELLOW WHITE GREY NC
96
- set_palette 2
97
- echo -e "${GREY}└${NC}" >&2
44
+ echo -e "└" >&2
98
45
  }
package/src/audits/run.ts CHANGED
@@ -74,7 +74,7 @@ export function spawnAudit(root: string): Spawn {
74
74
  // extending it. Spreading `process.env` alongside would put back the very
75
75
  // variables it strips, and a hook's `GIT_DIR` would then point every
76
76
  // history-reading verb at a repository nobody asked about.
77
- env: { ...gitEnv(), CANON_NON_INTERACTIVE: '1' },
77
+ env: gitEnv(),
78
78
  extendEnv: false,
79
79
  })
80
80
 
@@ -22,11 +22,6 @@ export const MISC_CASES: readonly SkillCase[] = [
22
22
  'This project has no logo yet. Draft one and give me a social card to go with it.',
23
23
  expect: 'design-identity',
24
24
  },
25
- {
26
- prompt:
27
- 'Draw the signup screen as a frame on the canvas, then restyle the element I just selected there.',
28
- expect: 'canvas-board',
29
- },
30
25
  {
31
26
  prompt:
32
27
  'The operator picked the new settings panel, so land it in canon/design with an index row for it.',
@@ -9,7 +9,7 @@ const TOKEN = /`([^`\s]+)`/g
9
9
 
10
10
  /**
11
11
  * A path a reader could open. Admits no `$`, `*`, or `=`, so an assignment
12
- * (`CANON_NON_INTERACTIVE=1`) and a variable are both excluded.
12
+ * (`NAME=value`) and a variable are both excluded.
13
13
  */
14
14
  const PATH_TOKEN = /^[.A-Za-z0-9_][A-Za-z0-9._/-]*$/
15
15
 
@@ -55,6 +55,7 @@ export const PRACTICE_SKILLS: readonly string[] = [
55
55
  join(SHIPPED_CORPUS, 'build-in-slices'),
56
56
  join(SHIPPED_CORPUS, 'deprecation-migration'),
57
57
  join(SHIPPED_CORPUS, 'search-craft'),
58
+ join(SHIPPED_CORPUS, 'teach-craft'),
58
59
  ]
59
60
 
60
61
  /**
package/src/cli/help.ts CHANGED
@@ -43,7 +43,6 @@ export const COMMAND_GROUPS: { title: string; rows: CommandRow[] }[] = [
43
43
  ['inventory [subj]', 'Report one computed property across every route'],
44
44
  ['drive <url> <run>', 'Walk a page through interactions, measure each'],
45
45
  ['transcripts <url>', 'Fetch a YouTube transcript with frontmatter'],
46
- ['canvas [cmd]', 'Local canvas of pages and frames'],
47
46
  ],
48
47
  },
49
48
  {
@@ -98,8 +97,6 @@ const EXAMPLES = [
98
97
  'canon indexes regen --dry-run --json',
99
98
  'canon docs list --json',
100
99
  'canon docs agents',
101
- 'canon design render',
102
- 'canon canvas list --json',
103
100
  'canon capture assets/captures/install.html --selector .window --out assets/frames',
104
101
  'canon inventory focus --json',
105
102
  'canon drive http://localhost:4173 run.json --json',
package/src/cli/run.ts CHANGED
@@ -2,7 +2,7 @@ import { join } from 'node:path'
2
2
  import { execa } from 'execa'
3
3
 
4
4
  export interface CliRunOptions {
5
- /** Sets `CANON_NON_INTERACTIVE=1` for the child. Defaults to false. */
5
+ /** Closes the child's stdin, which makes it headless. Defaults to false. */
6
6
  readonly nonInteractive?: boolean
7
7
  /** Defaults to `inherit`, so a child prompt still reaches the operator. */
8
8
  readonly stdin?: 'ignore' | 'inherit'
@@ -28,13 +28,12 @@ export function cliRun(
28
28
  options: CliRunOptions = {},
29
29
  ): () => Promise<boolean> {
30
30
  return async () => {
31
- const env = options.nonInteractive
32
- ? { ...process.env, CANON_NON_INTERACTIVE: '1' }
33
- : process.env
31
+ const stdin = options.nonInteractive
32
+ ? 'ignore'
33
+ : (options.stdin ?? 'inherit')
34
34
 
35
35
  const result = await execa('bun', [path, ...args], {
36
- stdio: [options.stdin ?? 'inherit', 'inherit', 'inherit'],
37
- env,
36
+ stdio: [stdin, 'inherit', 'inherit'],
38
37
  reject: false,
39
38
  })
40
39
 
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
- const { GREEN, GREY, NC } = palette(process.stderr)
68
- process.stderr.write(`${GREY}│${NC} ${GREEN}✓${NC} ${message}\n`)
98
+ if (quiet) return
99
+ process.stderr.write(markLine('GREEN', '✓', message))
69
100
  }
70
101
 
71
102
  export function logWarn(message: string): void {
72
- const { GREY, NC, YELLOW } = palette(process.stderr)
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
- const { GREEN, GREY, NC } = palette(process.stderr)
78
- process.stderr.write(`${GREY}│${NC} ${GREEN}+${NC} ${message}\n`)
107
+ if (quiet) return
108
+ process.stderr.write(markLine('GREEN', '+', message))
79
109
  }
80
110
 
81
111
  export function logRemove(message: string): void {
82
- const { GREY, NC, RED } = palette(process.stderr)
83
- process.stderr.write(`${GREY}│${NC} ${RED}-${NC} ${message}\n`)
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
- const { GREY, NC, RED } = palette(process.stderr)
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
- * The retired spelling is still honored, and permanently.
195
+ * A caller with no terminal on stdin is headless, since nobody can answer a
196
+ * prompt there.
140
197
  *
141
- * This variable is the documented way to drive any command headlessly, so
142
- * every skill body, CI job, and operator script already written sets the old
143
- * name. Those callers live in other repositories and in plugin caches this
144
- * release cannot reach, and the failure mode of dropping the fallback is a
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
- * under `CANON_NON_INTERACTIVE=1` instead of failing. Callers that would rather fail loudly than pick
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
- process.stderr.write(
175
- `${GREY}│${NC}\n${GREY}◇${NC} ${message} ${WHITE}${options[0].label}${NC}\n`,
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 (!process.stdin.isTTY) {
200
- process.stderr.write(
201
- `${GREY}│${NC} ${RED}✗${NC} ${message} requires a TTY. ${opts.ttyHint ?? 'Pass an argument'} or set CANON_NON_INTERACTIVE=1.\n${GREY}└${NC}\n`,
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
@@ -18,7 +18,6 @@ import { register as feedback } from '@/commands/feedback'
18
18
  import { register as transcripts } from '@/commands/transcripts'
19
19
  import { register as tasks } from '@/commands/tasks'
20
20
  import { register as intake } from '@/commands/intake'
21
- import { register as canvas } from '@/commands/canvas'
22
21
  import { register as comments } from '@/commands/comments'
23
22
  import { register as context } from '@/commands/context'
24
23
  import { register as markdown } from '@/commands/markdown'
@@ -41,6 +40,7 @@ import { register as changelog } from '@/commands/changelog'
41
40
  import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
42
41
  import { renderHelp } from '@/cli/help'
43
42
  import { renderAll, routeHelp } from '@/cli/help-all'
43
+ import { setQuiet } from '@/cli/ui'
44
44
 
45
45
  function showHelp(): void {
46
46
  console.log(renderHelp(process.stdout))
@@ -58,6 +58,11 @@ program.on('option:help', () => {
58
58
  process.exit(0)
59
59
  })
60
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
+ })
61
66
 
62
67
  init(program)
63
68
  sandbox(program)
@@ -76,7 +81,6 @@ feedback(program)
76
81
  transcripts(program)
77
82
  tasks(program)
78
83
  intake(program)
79
- canvas(program)
80
84
  comments(program)
81
85
  context(program)
82
86
  markdown(program)
@@ -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
- const { GREEN, NC } = palette(process.stderr)
608
- outro()
609
- process.stderr.write(`${GREEN}✓ ${message}${NC}\n`)
608
+ outroSuccess(message)
610
609
  return 0
611
610
  }
612
611