@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/create-skill/SKILL.md +1 -1
  4. package/claude/skills/document-health/SKILL.md +6 -6
  5. package/claude/skills/document-repair/SKILL.md +5 -5
  6. package/claude/skills/target-check/SKILL.md +6 -8
  7. package/claude/skills/target-check/references/domains.md +1 -4
  8. package/claude/skills/target-setup/SKILL.md +8 -8
  9. package/claude/skills/target-update/SKILL.md +5 -5
  10. package/docs/agents/install-and-sync.md +2 -2
  11. package/docs/agents/output-shape.md +11 -10
  12. package/docs/target/sync.md +1 -1
  13. package/package.json +1 -1
  14. package/scripts/core/install-check.sh +1 -1
  15. package/scripts/lib/ui.sh +12 -65
  16. package/src/audits/run.ts +1 -1
  17. package/src/claude/routing.ts +1 -1
  18. package/src/cli/run.ts +5 -6
  19. package/src/cli/ui.ts +87 -28
  20. package/src/cli.ts +6 -0
  21. package/src/commands/claude.ts +2 -3
  22. package/src/commands/design.ts +8 -13
  23. package/src/commands/gate.ts +5 -3
  24. package/src/commands/gov.ts +11 -21
  25. package/src/commands/labels.ts +1 -1
  26. package/src/commands/pr.ts +1 -1
  27. package/src/commands/sync.ts +1 -1
  28. package/src/commands/tooling.ts +5 -8
  29. package/src/commands/transcripts.ts +6 -11
  30. package/src/gate/sequencer.ts +1 -1
  31. package/src/gate/stages.ts +0 -10
  32. package/src/hooks/post-merge.ts +2 -5
  33. package/src/process/harness.ts +3 -4
  34. package/src/sandbox/equivalence.ts +0 -1
  35. package/src/sandbox/provision.ts +8 -13
  36. package/src/sandbox/scenario.ts +6 -12
  37. package/src/sync/engine.ts +3 -8
  38. package/src/sync/workflow.ts +3 -5
  39. package/src/tooling/verify-stack.ts +1 -1
  40. package/tooling/base/configs/.husky/post-merge +6 -10
  41. package/scripts/core/check-color-source.sh +0 -51
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.55.0](https://github.com/erclx/canon/compare/v5.54.0...v5.55.0) (2026-10-21)
4
+
5
+
6
+ ### Features
7
+
8
+ * **cli:** decide headless from a missing terminal on stdin ([#2600](https://github.com/erclx/canon/issues/2600)) ([63e9af6](https://github.com/erclx/canon/commit/63e9af6fcd2b23c1aa4abe45e0fb818519b0e314))
9
+ * **cli:** quiet stderr on --json runs and drop the frame off a terminal ([#2599](https://github.com/erclx/canon/issues/2599)) ([824861b](https://github.com/erclx/canon/commit/824861b55ce9a1c92227b3b8c96f541684d2da8b))
10
+
3
11
  ## [5.54.0](https://github.com/erclx/canon/compare/v5.53.0...v5.54.0) (2026-10-21)
4
12
 
5
13
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
4
- "version": "5.54.0",
4
+ "version": "5.55.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -42,7 +42,7 @@ Read these in the same pass:
42
42
  3. Draft the sibling `REQUIREMENT.md` from what the skill is for, in the shape the requirement standard states.
43
43
  - Write the gaps from the user's description rather than from the drafted body, since a requirement derived from the body records whatever the draft overfitted to.
44
44
  4. Confirm the skill name and both files with the user before writing, when a person can answer.
45
- - Skip the confirmation when `CANON_NON_INTERACTIVE=1` is set or another skill's run invoked this one, such as `auto-ship` or a dispatched worker.
45
+ - Skip the confirmation when another skill's run invoked this one, such as `auto-ship`, or when a dispatched worker runs it.
46
46
  - Then put the three Step 1 answers and the two drafted paths in the output and continue to Step 5, since a question nobody reads parks the chain.
47
47
 
48
48
  ### Write and check
@@ -15,7 +15,7 @@ Three axes carry the report. Two answer from a verb and the third answers from a
15
15
 
16
16
  - `canon` absent from the path: stop. `❌ canon is not on PATH, so neither measured axis can be read.`
17
17
  - `git ls-files '*.md'` listing nothing: stop. `❌ No tracked markdown here, so there is no corpus to read.`
18
- - Run every command under `CANON_NON_INTERACTIVE=1` and prompt at no point. A caller reaching this from a rollout wave has nobody present to answer.
18
+ - Prompt at no point, since a caller reaching this from a rollout wave has nobody present to answer. A call with no terminal on stdin already runs headless, so no command takes a prefix.
19
19
  - Branch on each record's own fields rather than on an exit status, since a shell profile may wrap `canon` in a function whose trailing command reports every refusal as success
20
20
  - Repair nothing, write no file in the project, and open no pull request. Whoever read the report decides what to fix.
21
21
 
@@ -30,8 +30,8 @@ Drop what nobody hand-authors before reporting a finding against it: a generated
30
30
  Run these from the project root, in parallel:
31
31
 
32
32
  ```bash
33
- CANON_NON_INTERACTIVE=1 canon markdown audit --json
34
- CANON_NON_INTERACTIVE=1 canon context audit --json
33
+ canon markdown audit --json
34
+ canon context audit --json
35
35
  ```
36
36
 
37
37
  The first answers length, carrying the bullet, paragraph, and depth checkpoints and the per-file weights read against them. The second answers placement for the folders it resolves, carrying entry length, reference form, and index drift.
@@ -39,8 +39,8 @@ The first answers length, carrying the bullet, paragraph, and depth checkpoints
39
39
  Then read which classifier would run, and deepen placement with the verdicts per section:
40
40
 
41
41
  ```bash
42
- CANON_NON_INTERACTIVE=1 canon context classifier show
43
- CANON_NON_INTERACTIVE=1 canon context classify sweep --json
42
+ canon context classifier show
43
+ canon context classify sweep --json
44
44
  ```
45
45
 
46
46
  The first names the backend, the model, and the source that decided both. Report which layer answered, since a project with the model layer off reads from the regex layer alone and the sweep says nothing about which one it used.
@@ -65,7 +65,7 @@ Mark every staleness finding as a reading. It is this session's judgment rather
65
65
  When the scope reaches `canon/REQUIREMENTS.md`, read it for identity drift as well as for stale paths, and take the review point from the verb:
66
66
 
67
67
  ```bash
68
- CANON_NON_INTERACTIVE=1 canon records stale canonical --json
68
+ canon records stale canonical --json
69
69
  ```
70
70
 
71
71
  The doc in the record carries `reviewed` and `releasesSince`. `${CLAUDE_SKILL_DIR}/references/axes.md` holds the identity claim shapes to test and how the record reads. Skip both the read and the verb when the scope excludes the doc.
@@ -16,14 +16,14 @@ The CLI holds every number. `canon markdown audit` measures, and `canon markdown
16
16
  - `canon` absent from the path: stop. `❌ canon is not on PATH, so nothing can be measured or gated.`
17
17
  - The audit refusing: stop and name its `reason`. A `malformed-config` refusal names the file to fix by hand, since the verb will not append to a file it cannot read.
18
18
  - `gated add` refusing with `canon-checkout`: stop. Canon's own checkout gates through a constant in its source, which a code change edits.
19
- - Run every command under `CANON_NON_INTERACTIVE=1`, and branch on each record's own fields rather than on an exit status a shell wrapper can flatten
19
+ - Branch on each record's own fields rather than on an exit status a shell wrapper can flatten. A call with no terminal on stdin already runs headless, so no command takes a prefix.
20
20
 
21
21
  ## Step 1: read the audit
22
22
 
23
23
  Run the audit over the whole tree and take the density and reference shape findings from its record:
24
24
 
25
25
  ```bash
26
- CANON_NON_INTERACTIVE=1 canon markdown audit --json
26
+ canon markdown audit --json
27
27
  ```
28
28
 
29
29
  Take the candidate folders from the order table in `${CLAUDE_SKILL_DIR}/references/batches.md` rather than from the findings, since a clean folder carries no finding and would never be gated. Drop every folder `canon/config/markdown.toml` already lists, since a listed folder already gates.
@@ -41,7 +41,7 @@ Load `markdown-craft` and `write-human`, then rewrite each failing block by its
41
41
  Re-run the audit on the folder alone after each file:
42
42
 
43
43
  ```bash
44
- CANON_NON_INTERACTIVE=1 canon markdown audit <prefix> --json
44
+ canon markdown audit <prefix> --json
45
45
  ```
46
46
 
47
47
  Read `${CLAUDE_SKILL_DIR}/references/batches.md` for what a rewrite may cut. A stated reason survives every rewrite, and the audit reaching zero cannot see one that went missing.
@@ -49,7 +49,7 @@ Read `${CLAUDE_SKILL_DIR}/references/batches.md` for what a rewrite may cut. A s
49
49
  ## Step 4: gate the folder
50
50
 
51
51
  ```bash
52
- CANON_NON_INTERACTIVE=1 canon markdown gated add <prefix> --json
52
+ canon markdown gated add <prefix> --json
53
53
  ```
54
54
 
55
55
  Pass the prefix exactly as the batch named it: a folder with its one trailing slash, such as `docs/`, and a file by its bare path, such as `README.md`. The verb refuses a doubled slash and a slash after a file, since either matches nothing and would gate nothing.
@@ -61,7 +61,7 @@ Branch on the record. `action: appended` or `present` means the folder gates now
61
61
  A skill's `REQUIREMENT.md` gates only on its own path, never through a folder prefix, so gating `.claude/skills/` leaves each requirement reporting. Gate each one that reaches zero by its path:
62
62
 
63
63
  ```bash
64
- CANON_NON_INTERACTIVE=1 canon markdown gated add .claude/skills/<name>/REQUIREMENT.md --json
64
+ canon markdown gated add .claude/skills/<name>/REQUIREMENT.md --json
65
65
  ```
66
66
 
67
67
  ## Output
@@ -17,7 +17,7 @@ Six domains carry the report. Five answer from a verb that already exists and th
17
17
  - No `.claude/` directory at the working root: stop. `❌ No .claude/ here, so nothing is installed to check. Run canon init first.`
18
18
  - A `claude/skills/` folder at the root, no leading dot: stop. `❌ This is the toolkit itself rather than a target. Ask the orchestrator for a worker per repository to take a change out to the targets.`
19
19
  - A target receives skills at `.claude/skills/` and authors none, so the spelling without the dot separates the source from what it installs into
20
- - Run every command under `CANON_NON_INTERACTIVE=1` and prompt at no point, since every caller is a dispatched worker with nobody to answer
20
+ - Prompt at no point, since every caller is a dispatched worker with nobody to answer. A call with no terminal on stdin already runs headless, so no command takes a prefix.
21
21
  - Branch on each record's own fields rather than on an exit status. A shell function wrapping `canon` can report every refusal as success.
22
22
 
23
23
  ## Step 1: read the reports
@@ -25,25 +25,23 @@ Six domains carry the report. Five answer from a verb that already exists and th
25
25
  Run these from the target root, in parallel:
26
26
 
27
27
  ```bash
28
- CANON_NON_INTERACTIVE=1 canon sync --check . --json
29
- CANON_NON_INTERACTIVE=1 canon context audit --json
28
+ canon sync --check . --json
29
+ canon context audit --json
30
30
  ```
31
31
 
32
32
  The first answers five domains from one read, being `skew`, `domains`, `seeds`, `tooling`, and the layout fields `unmigrated`, `superseded`, `unmovedSurfaces`, and `reverse`. The second deepens a domain that first read only counts.
33
33
 
34
- Then run `CANON_NON_INTERACTIVE=1 canon records validate <kind> --json` once per record folder the target carries, taking `<kind>` from what is present under the record root rather than from a list held here.
34
+ Then run `canon records validate <kind> --json` once per record folder the target carries, taking `<kind>` from what is present under the record root rather than from a list held here.
35
35
 
36
36
  ### The tooling read takes a stack, always
37
37
 
38
38
  The tooling comparison runs last, because it needs a value the first read returns. Take `<stack>` from the first name in the report's `tooling.chain`, which records the stack nearest the target, and pass it:
39
39
 
40
40
  ```bash
41
- CANON_NON_INTERACTIVE=1 canon tooling sync --check <stack>
41
+ canon tooling sync --check <stack>
42
42
  ```
43
43
 
44
- Refuse the domain and report it unread when `chain` names nothing, rather than running the command bare. A bare call reaches the stack prompt, which the non-interactive variable resolves to the catalog's first option instead of refusing.
45
-
46
- Every target would then be compared against whichever stack sorts first, on every run, and the drift reported would be an artifact of the wrong comparison rather than anything the target did.
44
+ Refuse the domain and report it unread when `chain` names nothing, rather than running the command bare. A bare call reaches the stack prompt, which refuses a headless caller, so it would report nothing about the domain.
47
45
 
48
46
  ### Domains come from the report
49
47
 
@@ -72,10 +72,7 @@ Read the `tooling` block off the report first, because the comparison command ne
72
72
 
73
73
  ### The comparison
74
74
 
75
- On a measured report, take `<stack>` from the first name in `chain` and run `canon tooling sync --check <stack>`. Never run it bare.
76
-
77
- - Omitting the stack reaches a prompt the non-interactive variable resolves to the catalog's first entry rather than refusing
78
- - That compares the target against a stack it does not use and reports the difference as drift
75
+ On a measured report, take `<stack>` from the first name in `chain` and run `canon tooling sync --check <stack>`. Never run it bare, since omitting the stack reaches a prompt that refuses a headless caller and compares nothing.
79
76
 
80
77
  ### Verdicts
81
78
 
@@ -33,9 +33,9 @@ rather than a step within it, so the full chain never runs it separately.
33
33
 
34
34
  ## Non-interactive path
35
35
 
36
- Every phase runs under `CANON_NON_INTERACTIVE=1` with no TTY. Three phases
37
- otherwise pause, and each takes an argument that answers its prompt ahead of
38
- time:
36
+ Every phase runs headless with no prefix, since a call with no terminal on
37
+ stdin is already headless. Three phases otherwise pause, and each takes an
38
+ argument that answers its prompt ahead of time:
39
39
 
40
40
  | Phase | Pauses to | Answered ahead by | With neither and no TTY |
41
41
  | ------------------- | ------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |
@@ -91,7 +91,7 @@ target project's current directory. Do not cd into the toolkit source tree. The
91
91
  Step 1: `canon init` installs base tooling, claude seeds, and governance rules.
92
92
 
93
93
  ```bash
94
- CANON_NON_INTERACTIVE=1 canon init \
94
+ canon init \
95
95
  --stack <stack> \
96
96
  --add <rules> \
97
97
  <target>
@@ -106,7 +106,7 @@ Step 2: `canon tooling sync <tooling-stack> --write` installs stack deps, script
106
106
  - A fresh target has nothing to lose, so pass `--write` directly rather than reading a report first
107
107
 
108
108
  ```bash
109
- CANON_NON_INTERACTIVE=1 canon tooling sync <tooling-stack> <target> --write
109
+ canon tooling sync <tooling-stack> <target> --write
110
110
  ```
111
111
 
112
112
  ### Monorepo with multiple language roots
@@ -114,8 +114,8 @@ CANON_NON_INTERACTIVE=1 canon tooling sync <tooling-stack> <target> --write
114
114
  Run `canon init` once at the repo root so `base` (husky, oxfmt, commitlint, CI) lands once, then sync each subtree with `--skip base`. Take the subtree commands from the preview's per-root rows rather than picking folders by hand.
115
115
 
116
116
  ```bash
117
- CANON_NON_INTERACTIVE=1 canon tooling sync vite-react ./frontend --skip base --write
118
- CANON_NON_INTERACTIVE=1 canon tooling sync python ./backend --skip base --write
117
+ canon tooling sync vite-react ./frontend --skip base --write
118
+ canon tooling sync python ./backend --skip base --write
119
119
  ```
120
120
 
121
121
  - Without `--skip base`, each subtree re-drops husky, and git honors only one `core.hooksPath`, so the extra hook dirs silently break
@@ -139,7 +139,7 @@ Resolve exactly as the detect reference states, confirm the proposed extras
139
139
  there, then run:
140
140
 
141
141
  ```bash
142
- CANON_NON_INTERACTIVE=1 canon gov install <stack> --add <extras> <target>
142
+ canon gov install <stack> --add <extras> <target>
143
143
  ```
144
144
 
145
145
  Report the rule count installed, the target path, and any rules that failed to
@@ -24,7 +24,7 @@ Stop with the line beside each state.
24
24
 
25
25
  Then hold two rules for every verb this body runs.
26
26
 
27
- - Run it under `CANON_NON_INTERACTIVE=1` from the target root
27
+ - Run it from the target root with no prefix, since a call with no terminal on stdin already runs headless
28
28
  - Branch on its record's fields rather than its exit, which a shell function wrapping `canon` can flatten
29
29
 
30
30
  ## Step 1: bring the binary and plugin forward
@@ -38,12 +38,12 @@ Both write to the machine rather than the target, and the preview needs the verb
38
38
  Run these in parallel from the target root:
39
39
 
40
40
  ```bash
41
- CANON_NON_INTERACTIVE=1 canon changelog --json
42
- CANON_NON_INTERACTIVE=1 canon migrate status --json
43
- CANON_NON_INTERACTIVE=1 canon sync --check . --json
41
+ canon changelog --json
42
+ canon migrate status --json
43
+ canon sync --check . --json
44
44
  ```
45
45
 
46
- Then take `<stack>` from the first name in the report's `tooling.chain` and run `canon tooling diff <stack> . --json`. Never run it bare, since the stack prompt resolves to the catalog's first entry under the non-interactive variable. Report tooling unmeasured when `tooling.measured` is false or `chain` names nothing.
46
+ Then take `<stack>` from the first name in the report's `tooling.chain` and run `canon tooling diff <stack> . --json`. Never run it bare, since the stack prompt refuses a headless caller. Report tooling unmeasured when `tooling.measured` is false or `chain` names nothing.
47
47
 
48
48
  ### When a read refuses
49
49
 
@@ -15,8 +15,8 @@ Any flag skips the confirmation prompt, but the `base` default of `--stack` does
15
15
 
16
16
  ## Governance install
17
17
 
18
- - `canon gov install <stack> [target]` needs the stack under `CANON_NON_INTERACTIVE=1`, else lists the valid names on stderr and exits 1
19
- - `--yes` answers the install confirm with no TTY and no env var. Without it, a headless call refuses and the message names `--yes`. It does not pick a stack.
18
+ - `canon gov install <stack> [target]` needs the stack on a call with no terminal, else lists the valid names on stderr and exits 1
19
+ - A headless call takes Yes at the install confirm, and `--yes` answers it at a terminal too. Neither picks a stack.
20
20
  - It refuses the toolkit root and a missing path
21
21
  - `--add <rules>` with no stack installs into the one positional, or the current directory, keeping the recorded chain. Pass `./node` for a target named like a stack.
22
22
 
@@ -22,18 +22,19 @@ Data goes to stdout. UI and logs go to stderr. Help is the exception and prints
22
22
  - `└` closes the frame on stderr
23
23
  - An error renders as `│ ✗ message` inside the frame, never as a lone line
24
24
  - Help skips the banner and puts `Usage:` directly on `├`
25
+ - A stderr that is not a terminal gets plain lines instead, with the banner and section titles bare and each item behind its mark alone
25
26
 
26
27
  A help request naming an unknown subcommand, under a group taking no operand, writes its refusal to stderr and exits 1.
27
28
 
28
29
  ## JSON and names modes
29
30
 
30
- `--json` and `--names` keep stdout pure data. The frame still renders on stderr, and diagnostics reach stderr in every mode. A JSON refusal carries an action and a reason, so branch on the record rather than on stderr.
31
+ `--json` and `--names` keep stdout pure data. A `--json` run writes no progress to stderr, only its warnings and errors, since diagnostics reach stderr in every mode. A JSON refusal carries an action and a reason, so branch on the record rather than on stderr.
31
32
 
32
33
  A shell profile may wrap `canon` in a function that takes its status from a later command. Where a verb documents a record field, read the field rather than the exit.
33
34
 
34
35
  ## Color
35
36
 
36
- Escape sequences reach a destination that renders them and nowhere else, decided per stream. `NO_COLOR` set to any non-empty value turns color off, and so does a destination that is not a terminal. The frame glyphs and the `✓ ! + - ✗` marks survive both.
37
+ Escape sequences reach a destination that renders them and nowhere else, decided per stream. `NO_COLOR` set to any non-empty value turns color off, and so does a destination that is not a terminal. The `✓ ! + - ✗` marks survive both. The frame glyphs need a terminal.
37
38
 
38
39
  ## Exit discipline
39
40
 
@@ -59,16 +60,16 @@ Discover names at runtime rather than hardcoding them.
59
60
 
60
61
  ## Headless invocation
61
62
 
62
- Set `CANON_NON_INTERACTIVE=1` to refuse every prompt.
63
+ A call with no terminal on stdin is headless and needs no prefix. Each prompt takes its documented default or refuses and names the flag to pass.
63
64
 
64
65
  ```bash
65
- CANON_NON_INTERACTIVE=1 canon init --stack node --skip records /path/to/project
66
- CANON_NON_INTERACTIVE=1 canon sync /path/to/project
67
- CANON_NON_INTERACTIVE=1 canon gov install node --add astro,shadcn /path/to/project
68
- CANON_NON_INTERACTIVE=1 canon gov sync /path/to/project
69
- CANON_NON_INTERACTIVE=1 canon gov build /path/to/project
70
- CANON_NON_INTERACTIVE=1 canon tooling diff astro /path/to/project
71
- CANON_NON_INTERACTIVE=1 canon tooling sync astro /path/to/project --write
66
+ canon init --stack node --skip records /path/to/project
67
+ canon sync /path/to/project
68
+ canon gov install node --add astro,shadcn /path/to/project
69
+ canon gov sync /path/to/project
70
+ canon gov build /path/to/project
71
+ canon tooling diff astro /path/to/project
72
+ canon tooling sync astro /path/to/project --write
72
73
  canon tooling inject base /path/to/project --configs --seeds
73
74
  canon tooling prune-gitignore base /path/to/project
74
75
  canon tooling verify vite-react
@@ -51,4 +51,4 @@ Try a scenario from [sandbox](../../canon/context/development/sandbox.md) before
51
51
 
52
52
  ## Running sync from an agent session
53
53
 
54
- `canon sync .` applies every domain sync, then offers a commit and pull request that needs a terminal. Under `CANON_NON_INTERACTIVE=1` the syncs apply, the git step is refused, and the command exits 0. Commit yourself. Sync refuses a dirty working tree, and `canon sync --check .` does not.
54
+ `canon sync .` applies every domain sync, then offers a commit and pull request that needs a terminal. With no terminal on stdin the syncs apply, the git step is refused, and the command exits 0. Commit yourself. Sync refuses a dirty working tree, and `canon sync --check .` does not.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.54.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": {
@@ -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
 
@@ -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
 
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