@erclx/canon 5.57.0 → 5.59.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 (40) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/context-fold/references/context.md +1 -1
  5. package/claude/skills/document-health/REQUIREMENT.md +2 -2
  6. package/claude/skills/document-health/SKILL.md +14 -0
  7. package/claude/skills/document-health/references/axes.md +12 -0
  8. package/claude/skills/document-repair/REQUIREMENT.md +6 -1
  9. package/claude/skills/document-repair/SKILL.md +21 -0
  10. package/claude/skills/plan-feature/SKILL.md +1 -1
  11. package/claude/skills/target-update/SKILL.md +5 -1
  12. package/docs/agents/context-audit.md +1 -1
  13. package/docs/agents/install-and-sync.md +1 -1
  14. package/docs/agents/migrate.md +6 -0
  15. package/docs/target/sync.md +3 -1
  16. package/docs/workflow/operating-model.md +1 -1
  17. package/governance/rules/code/routing.md +5 -1
  18. package/governance/rules/ui/design-taste.md +4 -0
  19. package/package.json +3 -3
  20. package/scripts/core/bootstrap.ts +120 -0
  21. package/scripts/core/install-check.ts +193 -0
  22. package/src/cli/retired.ts +63 -0
  23. package/src/cli/ui.ts +3 -5
  24. package/src/cli.ts +8 -2
  25. package/src/commands/gov.ts +8 -2
  26. package/src/commands/migrate.ts +7 -1
  27. package/src/commands/pr.ts +10 -1
  28. package/src/context/audit.ts +26 -1
  29. package/src/gov/adapter.ts +2 -0
  30. package/src/gov/coverage.ts +62 -0
  31. package/src/migrate/callers.ts +192 -0
  32. package/src/migrate/status.ts +11 -3
  33. package/src/pr/merge.ts +77 -1
  34. package/src/pr/rest.ts +1 -0
  35. package/src/sandbox/expect.ts +1 -1
  36. package/src/sync/engine.ts +84 -0
  37. package/src/sync/stamp.ts +51 -1
  38. package/scripts/core/bootstrap.sh +0 -86
  39. package/scripts/core/install-check.sh +0 -113
  40. package/scripts/lib/ui.sh +0 -45
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.59.0](https://github.com/erclx/canon/compare/v5.58.0...v5.59.0) (2026-10-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * **cli:** name the release that retired a verb and find its callers ([#2662](https://github.com/erclx/canon/issues/2662)) ([32fddb5](https://github.com/erclx/canon/commit/32fddb5f8c363441fc18ffb4243c08a65a863a67))
9
+ * **gov:** report rule coverage after gov sync ([#2660](https://github.com/erclx/canon/issues/2660)) ([c087965](https://github.com/erclx/canon/commit/c087965acd64249f8dfbe9ca1d98a7857621d380))
10
+ * **pr:** compose bot pull request merge messages ([#2659](https://github.com/erclx/canon/issues/2659)) ([698991c](https://github.com/erclx/canon/commit/698991c71b6423279756f023c86cd2537ae46017))
11
+ * **sandbox:** add a canon-feedback-triage anchor scenario ([#2656](https://github.com/erclx/canon/issues/2656)) ([ae45573](https://github.com/erclx/canon/commit/ae45573e2b385b97cdfbf300b4d1f9bb0f3db839))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **sync:** write the stamp in the formatter shape ([#2655](https://github.com/erclx/canon/issues/2655)) ([a34c5fb](https://github.com/erclx/canon/commit/a34c5fb2d18314ec31d83640be387057b68638a7))
17
+
18
+ ## [5.58.0](https://github.com/erclx/canon/compare/v5.57.0...v5.58.0) (2026-10-24)
19
+
20
+
21
+ ### Features
22
+
23
+ * **governance:** widen design-taste paths to component files ([#2645](https://github.com/erclx/canon/issues/2645)) ([01b1445](https://github.com/erclx/canon/commit/01b144514eced1150c90b18acc78f0be33990a23))
24
+ * **rules:** route interface changes to api-design ([#2647](https://github.com/erclx/canon/issues/2647)) ([09a073c](https://github.com/erclx/canon/commit/09a073ca38324a399de5c2c2e702fbcede6bb38c))
25
+ * **skills:** offer a restructure before document-repair rewrites ([#2648](https://github.com/erclx/canon/issues/2648)) ([7c2776a](https://github.com/erclx/canon/commit/7c2776a95b86a6626c44e399056b9eaa22418d65))
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * **context:** count table rows toward no folder paragraph ([#2646](https://github.com/erclx/canon/issues/2646)) ([a99b020](https://github.com/erclx/canon/commit/a99b02064105b81bc3a4f6e4820c73198942785e))
31
+
3
32
  ## [5.57.0](https://github.com/erclx/canon/compare/v5.56.0...v5.57.0) (2026-10-24)
4
33
 
5
34
 
package/README.md CHANGED
@@ -140,7 +140,7 @@ Bun, Git, and a shell are required, and the GitHub CLI is optional.
140
140
  - [Bun](https://bun.sh) for the CLI runtime and scripts
141
141
  - [Git](https://git-scm.com) with worktree support
142
142
  - [GitHub CLI](https://cli.github.com) (optional) for ship flows
143
- - Shell: `zsh` or bash 4+ (`brew install bash` on macOS).
143
+ - Shell: `zsh`, since bootstrap appends its aliases to `~/.zshrc`.
144
144
 
145
145
  Clone the repo, then run the bootstrap script. It installs dependencies, links the CLI globally, and appends a marked block of Claude Code shell aliases to your `~/.zshrc`.
146
146
 
@@ -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.57.0",
4
+ "version": "5.59.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -71,7 +71,7 @@ An entry carries three H2 sections and no other, being `Overview`, `Layout`, and
71
71
 
72
72
  Layout gives each folder doing real work its own H3, titled with the folder path in a code span, such as ``### `src/design/` ``.
73
73
 
74
- - Open the H3 with a one-paragraph lead stating what the folder does, then at most five bullets on what it does now
74
+ - Open the H3 with a one-paragraph lead stating what the folder does, then at most five bullets on what it does now. A table after the lead counts toward neither, with one sentence to lead the bullets
75
75
  - Put every trivial folder under `### Other folders` as a one-line bullet in owns-X form
76
76
  - Name folders only, never files, and write no decisions and no history under any H3
77
77
  - Give each folder under the code roots one H3 or `Other folders` line, since a second entry may cite it but never claims it
@@ -13,8 +13,8 @@ Length, placement, and staleness answer to different evidence, and one list hand
13
13
 
14
14
  ## Must
15
15
 
16
- - Report per document across the length, placement, and staleness axes, marking each finding with its evidence class
17
- - Read length and placement through the verb owning each measurement, and take every checkpoint from the record the verb returns
16
+ - Report per document across the length, placement, and staleness axes, marking each finding with its evidence class, read through the verb owning each measurement and its record's checkpoints
17
+ - Propose a split or an owner as a structure line for each overgrown entry and doubled or unnamed coverage finding, with the headings and the reason
18
18
  - Ship the staleness axis as a reading rather than cutting it for lacking a verb
19
19
  - Report a refusal, an absent key, or a missing subcommand as an unread axis naming the cause, and say which layer answered the classifier sweep
20
20
  - Run standalone, read the requirements record for identity drift when the scope reaches it, and print the `canon records stale canonical` reminder
@@ -47,6 +47,12 @@ The first names the backend, the model, and the source that decided both. Report
47
47
 
48
48
  Take the checkpoints from the record each verb returns rather than from a number written here. A checkpoint the CLI moves reaches this report with no edit, and a number held in this body goes stale on its own cadence.
49
49
 
50
+ ### The structure line
51
+
52
+ The context audit record already carries what a restructure needs, so the placement axis gains a structure line per area folder and runs no further verb. `${CLAUDE_SKILL_DIR}/references/axes.md` holds how to read `length` and `coverage` into a proposed split or owner.
53
+
54
+ Emit the line only for a folder with an entry past the line checkpoint whose Layout headings group into two or more domains, or with a doubled or unnamed coverage finding. A folder with neither gets no line, and `document-repair` goes straight to its rewrite.
55
+
50
56
  ### An unread axis is not a healthy one
51
57
 
52
58
  A refusal, an absent key, and a subcommand the project's binary does not carry each leave an axis unread. Report it as unread against the documents it would have covered, name the read that could not be taken, and name the cause.
@@ -106,4 +112,12 @@ When Step 3 read the canonical records, add one reminder line per doc the verb r
106
112
 
107
113
  Write `reviewed value is not a date: <value>` for a doc carrying `invalidReviewed`, and `no release tags to count` in place of the count when `tagged` is false. Name the unread cause instead when the verb refused or the binary carries no `canonical` kind.
108
114
 
115
+ Under the table, add one structure line per area folder that earned one, naming the entry and the split, or the finding and its owner:
116
+
117
+ ```markdown
118
+ Structure `canon/context/surfaces/`: `cli.md` (300 lines) splits into `cli.md` taking <headings> and `cli-<domain>.md` taking <headings>, because <reason>.
119
+ ```
120
+
121
+ The line is a proposal for the operator, with the headings each entry takes and why. Health applies nothing, and `document-repair` offers the proposal before it rewrites.
122
+
109
123
  Keep the evidence line under the table on every run. A table read without it flattens a judgment into a measurement, which is the one shape the three classes exist to prevent.
@@ -54,6 +54,18 @@ The sweep classifies each section as keep, replace or rewrite, history, or move.
54
54
  - A low-confidence `move` is a prompt to look rather than an instruction to cut
55
55
  - **Routes to:** the document the verdict names as the owner. Where the move crosses a tier rather than a file, the project's own content-ownership rules decide the destination.
56
56
 
57
+ ### The structure line
58
+
59
+ The audit record's `length` array lists each entry past the line checkpoint, and `coverage.findings` lists the `doubled` and `unnamed` ones. The structure line reads both, so it carries the `measured` class for the finding and `read` for the proposal built on it.
60
+
61
+ - **Overgrown entry:** read its `causes` in `length` first, since `history: yes` points at cutting provenance and one domain at a trim, so neither earns a split
62
+ - **Split:** group the Layout headings by domain, one new entry per group beyond the first, naming each entry's headings and one reason
63
+ - **Domain boundaries:** no verb measures one, so state the grouping as a judgment.
64
+ - **Doubled coverage:** two entries describe one source area. Propose the one that owns it, from which entry already holds the area's traps, and the other points at it.
65
+ - **Unnamed coverage:** no entry names the area. Propose an owner among the existing entries before a new one, since a new entry is the larger change.
66
+
67
+ A proposal names headings and a reason, so an operator judges a concrete split. A split nobody can act on names none of those.
68
+
57
69
  ### When a verb resolves nothing
58
70
 
59
71
  Both verbs resolve their folders by surface root rather than by a path written into them, so a project laid out differently resolves nothing. The two degrade differently.
@@ -15,14 +15,19 @@ A folder repaired by hand also drifts back, since a target gates nothing until a
15
15
 
16
16
  - Each batch takes one folder and leaves it at zero findings or reports it ungated
17
17
  - A folder gates only through `canon markdown gated add`, which refuses one with a finding left
18
- - A rewrite keeps every stated reason, command, and path the original carried
18
+ - A rewrite keeps every stated reason, command, and path the original carried, and leaves a context entry no more shape findings than it started with
19
19
  - The body names the verbs and states no limit number
20
20
  - A requirement gates by its own path, never through a folder prefix
21
21
 
22
+ ## Restructure first
23
+
24
+ An overgrown context folder is offered the restructure `document-health` proposes before any rewrite, and the restructure applies only on the operator's word.
25
+
22
26
  ## Must not
23
27
 
24
28
  - Edit `canon/config/markdown.toml` by hand
25
29
  - Report a folder gated that the verb refused
30
+ - Reach zero density findings by growing the context shape findings
26
31
  - Change what a document claims to pass the audit
27
32
  - Be reachable only by its name, since `document-health` names it for every length finding
28
33
 
@@ -18,6 +18,15 @@ The CLI holds every number. `canon markdown audit` measures, and `canon markdown
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
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
+ ## Step 0: offer the restructure
22
+
23
+ Restructure an overgrown context folder before rewriting it, so the effort is spent once. Run this step only when `canon/context/` is a candidate, meaning it is not yet listed in `canon/config/markdown.toml`, and skip it silently otherwise.
24
+
25
+ - Take the structure line `document-health` reports for each area folder, or read it from `canon context audit --json` the way that skill does
26
+ - A folder with no overgrown entry and no doubled or unnamed coverage finding has no line, so go straight to Step 1 with no prompt
27
+ - Show the operator each proposal and ask before applying it, since a split moves files other entries cite. Re-run the audit afterward to confirm the citations still resolve.
28
+ - Take a declined proposal as a decision and rewrite the entry as it stands
29
+
21
30
  ## Step 1: read the audit
22
31
 
23
32
  Run the audit over the whole tree and take the density and reference shape findings from its record:
@@ -26,6 +35,12 @@ Run the audit over the whole tree and take the density and reference shape findi
26
35
  canon markdown audit --json
27
36
  ```
28
37
 
38
+ When `canon/context/` is a candidate, read the context shape beside it and keep each entry's `shapeFindings` issues as the baseline Step 3 holds the rewrite to:
39
+
40
+ ```bash
41
+ canon context audit --json
42
+ ```
43
+
29
44
  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.
30
45
 
31
46
  Sort what is left in two. A candidate the findings never name needs no rewrite, so gate it through Step 4 before the first batch. Group the findings under the rest, which Steps 2 and 3 rewrite.
@@ -46,6 +61,12 @@ canon markdown audit <prefix> --json
46
61
 
47
62
  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.
48
63
 
64
+ ### Hold the context shape
65
+
66
+ For a file under a folder the context audit resolves, also re-run `canon context audit --json` and compare the entry's `shapeFindings` issues with the baseline. A density fix that splits a Layout paragraph into several can grow `layout-paragraphs`.
67
+
68
+ A rewrite leaving the entry with more shape issues than it started with is not done. Undo it, or fold the split into a table or a linked page, and report the entry as stopped when neither reaches the baseline. Zero density findings reached by growing shape findings only moves the failure.
69
+
49
70
  ## Step 4: gate the folder
50
71
 
51
72
  ```bash
@@ -84,7 +84,7 @@ What this skill adds on top of the standard:
84
84
  Every `- Suggested:` line takes senior judgment, picking the best option in one line with its reason or main tradeoff, and naming alternatives only when they change the pick.
85
85
 
86
86
  - Suggest a real default when best practice, the codebase, or prior context points to one
87
- - Load `canon:codebase-layout` before `## Files to touch` when an entry names a path that does not exist yet, and give each new path its placement reason. Report it if it does not resolve.
87
+ - Before `## Files to touch`, load `canon:codebase-layout` for a new path (its reason) and `canon:api-design` for a changed verb, flag, output, record shape, or endpoint. Report either if missing.
88
88
  - Fill `## Design references` with the `canon/design/<surface>/` folder for each surface a UI file in `## Files to touch` renders, and write `None identified.` in a project with no folder
89
89
  - Write `## Verification` as one bullet per outcome naming the test or command that proves it
90
90
  - Write `## Review focus` as the inputs or states that would break the change, carrying the risk you understood to the executor and the reviewer
@@ -64,6 +64,8 @@ Target update: `<path>`, canon `<installed>` to `<running>`.
64
64
 
65
65
  **Migrations, in run order:** <each pending or refused entry from migrate status, with its verb>
66
66
 
67
+ **Retired calls:** <each `retiredCallers` entry from migrate status, with its file, line, the release that retired the verb, and where the work went, or omitted when the list is empty>
68
+
67
69
  **Drift:** <each non-empty cause from sync --check, with its count and paths>
68
70
 
69
71
  **Where it lands:** <the linked worktree this run is already in, or the worktree `.claude/worktrees/<name>/` it will enter, or the branch it will create in place when the operator declines a worktree>
@@ -73,11 +75,13 @@ Target update: `<path>`, canon `<installed>` to `<running>`.
73
75
  **Commands, in order:** <the exact commands Steps 4 to 6 will run>
74
76
  ```
75
77
 
78
+ A retired call is no migration, so no step here fixes it. Name it in the preview and leave the edit to the operator, who owns the script.
79
+
76
80
  Summarize the changelog for this project, not for the toolkit. Drop an entry that touches nothing the target holds, and lead with anything a migration or a sync will change.
77
81
 
78
82
  ### Nothing pending
79
83
 
80
- When the changelog range is empty, `migrate status` lists nothing pending or refused, and the drift lines hold nothing a step here would take, write nothing. Invoke `canon:target-check` and report the target current with its table.
84
+ When the changelog range is empty, `migrate status` lists nothing pending or refused and no `retiredCallers`, and the drift lines hold nothing a step here would take, write nothing. Invoke `canon:target-check` and report the target current with its table.
81
85
 
82
86
  ### Take a branch after the go-ahead
83
87
 
@@ -46,7 +46,7 @@ The shape kinds are `retired-section`, `gotcha-count`, `gotcha-heading`, `gotcha
46
46
 
47
47
  - A `## Decisions` H2, or a `Gotchas` section past five bullets or holding a heading or paragraph
48
48
  - A `Layout` H3 titled neither a code-spanned path nor `Other folders`, or a heading below H3 under `Layout`
49
- - A folder H3 past five bullets or its one-paragraph lead
49
+ - A folder H3 past five bullets or its one-paragraph lead, where table rows and the sentence leading the bullets after a table count toward neither
50
50
 
51
51
  The development entry may carry a `Scripts` H3 exempt from both folder caps. `SHAPE_GATED_FOLDERS` lives in `src/context/shape-gated.ts`.
52
52
 
@@ -24,7 +24,7 @@ A flat installed-rule layout makes `gov sync` report "No governance surfaces fou
24
24
 
25
25
  ## Governance sync
26
26
 
27
- `canon gov sync [target]` updates what `.claude/rules/canon/` holds and adds nothing. `.claude/rules/project/` is never read, so put project rules there.
27
+ `canon gov sync [target]` updates what `.claude/rules/canon/` holds and adds nothing. `.claude/rules/project/` is never read, so put project rules there. After an apply it lists each copied rule with its globs and matching file count, and `--json` puts `{ "coverage": [...] }` on stdout.
28
28
 
29
29
  - A rule the toolkit no longer ships is deleted, edited or not
30
30
  - A rule renamed in `governance/renames.toml` moves, and its local edits are dropped
@@ -38,6 +38,12 @@ kind: reference
38
38
 
39
39
  `status` runs each verb as a dry run and exits 2 while any is pending, with a refusal outranking that. Each entry's `verb` carries the flags its dry run used, so append `--write` to it. A clean entry can turn pending once an earlier one applies, so run it again after each `--write`.
40
40
 
41
+ `status --json` also carries `retiredCallers`, the project calls to a verb a release retired.
42
+
43
+ - Each entry has `file`, `line`, `verb`, `release`, and `movedTo`
44
+ - The scan reads the workflow folder, the `package.json` scripts, `scripts/`, and the files those name, matching a command line or an argument list and never prose
45
+ - A caller changes no exit code, since no `--write` fixes one, so edit the script by hand
46
+
41
47
  Three markers keep an old spelling on a line. Each also covers the nearest non-blank line, below it for `canon-keep-record-root` and above it for `canon-keep-surface-root`.
42
48
 
43
49
  | Marker | Keeps |
@@ -43,10 +43,12 @@ Add `--json` for a machine-readable report and `--exit-code` to fail CI when a t
43
43
  ## Targeted
44
44
 
45
45
  - Seed docs such as `CLAUDE.md` and `canon/REQUIREMENTS.md`: merge by hand, keeping the project's customizations
46
- - Installed governance rules: `canon gov sync <path>`, which adds no new rules
46
+ - Installed governance rules: `canon gov sync <path>`, which adds no new rules and reports coverage afterward
47
47
  - Tooling configs and seeds: read differences with `canon tooling diff <stack> <path>`, then `canon tooling sync <stack> <path> --write`, which overwrites golden configs and copies each seed once
48
48
  - Index regeneration: `canon indexes regen`
49
49
 
50
+ After `canon gov sync` applies, it lists each rule it copied with its globs and how many project files match. A `ui/` rule adds a pointer to `/canon:ux-audit`. `--json` writes `{ "coverage": [{ "rule", "paths", "files", "ui" }] }` to stdout, empty when nothing was copied, with `files` null outside git. The report never changes the exit code.
51
+
50
52
  Try a scenario from [sandbox](../../canon/context/development/sandbox.md) before syncing a real project.
51
53
 
52
54
  ## Running sync from an agent session
@@ -36,7 +36,7 @@ Each role keeps to its own writes, and five rules cover how the roles meet.
36
36
  7. A pass posts `## Review` while anything is open and `## Review closed` when nothing is. The reviewer then runs `canon pr lift`.
37
37
  8. The human merges with `canon pr merge <number>`. The orchestrator tells any trailing worker whose files the merge touched to run `review-address`, which rebases.
38
38
 
39
- Each worker halts at the PR, and the merge stays a human gate. `canon pr merge` squashes with the body's Summary, Key changes, and Technical context as the message, and the seeded deny rule covers the forms an agent writes by default. The merge button stays for a bot's pull request, such as a dependency bump or a release.
39
+ Each worker halts at the PR, and the merge stays a human gate. `canon pr merge` squashes with the body's Summary, Key changes, and Technical context as the message, and the seeded deny rule covers the forms an agent writes by default. A release-please or Dependabot pull request merges the same way, from the bot's own body.
40
40
 
41
41
  ## The review layer
42
42
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Routes code placement and shape decisions to the skills that carry them, and library calls to the official docs
2
+ description: Routes code placement, shape, and interface decisions to the skills that carry them, and library calls to the official docs
3
3
  ---
4
4
 
5
5
  # Code standards
@@ -12,6 +12,10 @@ description: Routes code placement and shape decisions to the skills that carry
12
12
 
13
13
  - Load the `canon:code-craft` skill before deciding the shape of a new function, class, or module, and report it rather than proceeding silently when it does not resolve.
14
14
 
15
+ ## Interfaces
16
+
17
+ - Load `canon:api-design` before shaping a public function, flag or output, record format, endpoint, or caller-handled error, and report it rather than proceeding silently when it does not resolve.
18
+
15
19
  ## Libraries
16
20
 
17
21
  - Before calling a library API the codebase does not already use, fetch the official docs for the installed version and match the call to them, since recall of an API goes stale.
@@ -1,6 +1,10 @@
1
1
  ---
2
2
  description: Route edits that decide how a surface looks to the design-taste skill for the layer ordering, the coherence locks, and the defaults a model reaches for
3
3
  paths:
4
+ - '**/*.tsx'
5
+ - '**/*.jsx'
6
+ - '**/*.astro'
7
+ - '**/*.html'
4
8
  - '**/*.css'
5
9
  - '**/*.scss'
6
10
  - 'canon/DESIGN.md'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.57.0",
4
+ "version": "5.59.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": {
@@ -30,14 +30,14 @@
30
30
  "check:shell": "find scripts tooling claude .claude/hooks -name '*.sh' -exec shellcheck --severity=warning {} + && find .husky -maxdepth 1 -type f -exec shellcheck --severity=warning --shell=sh {} +",
31
31
  "check:lint": "oxlint --disable-nested-config --max-warnings 0",
32
32
  "check:types": "tsc --noEmit",
33
- "check:install": "./scripts/core/install-check.sh",
33
+ "check:install": "bun scripts/core/install-check.ts",
34
34
  "test": "bun --bun vitest run",
35
35
  "prepare": "husky",
36
36
  "check": "bun src/cli.ts gate run",
37
37
  "check:ci": "bun src/cli.ts gate run --all",
38
38
  "update": "bun update --interactive && bun run check",
39
39
  "clean": "rm -rf node_modules && bun pm cache rm && bun install",
40
- "bootstrap": "./scripts/core/bootstrap.sh",
40
+ "bootstrap": "bun scripts/core/bootstrap.ts",
41
41
  "canon:sandbox": "canon sandbox",
42
42
  "canon:sandbox:reset": "canon sandbox reset",
43
43
  "web:tokens": "bun src/cli.ts design css --no-components > web/src/styles/tokens.css.new && (echo '/* Generated by `canon design css --no-components`. Regenerate with `bun run web:tokens`. Do not hand-edit. */'; cat web/src/styles/tokens.css.new) > web/src/styles/tokens.css && rm web/src/styles/tokens.css.new",
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Installs dependencies, links the canon CLI globally, and appends the marked
3
+ * alias block to the user's `.zshrc`, the only file this writes outside the
4
+ * repository. Runs on Bun alone, so a stock macOS bash 3.2 never enters it.
5
+ */
6
+ import { appendFileSync, existsSync, readFileSync } from 'node:fs'
7
+ import { homedir } from 'node:os'
8
+ import { join } from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ import {
12
+ intro,
13
+ logAdd,
14
+ logError,
15
+ logInfo,
16
+ logStep,
17
+ logWarn,
18
+ outro,
19
+ } from '../../src/cli/ui'
20
+
21
+ const root = fileURLToPath(new URL('../..', import.meta.url)).replace(/\/$/, '')
22
+
23
+ const MARKER_OPEN = '# >>> canon aliases >>>'
24
+ const MARKER_CLOSE = '# <<< canon aliases <<<'
25
+
26
+ function zshrcPath(): string {
27
+ const dir = process.env.ZDOTDIR
28
+ return join(dir !== undefined && dir !== '' ? dir : homedir(), '.zshrc')
29
+ }
30
+
31
+ /** `$TOOLKIT` stays a literal in the block, so the shell expands it on load. */
32
+ function aliasBlock(): string {
33
+ return `${MARKER_OPEN}
34
+ TOOLKIT="${root}"
35
+ export CLAUDE_CODE_ENABLE_TODO_TOOLS=1
36
+
37
+ alias cl='claude'
38
+ alias clr='cl -r'
39
+ alias clc='cl -c'
40
+ alias clw='cl -w'
41
+ alias cls='cl --model sonnet'
42
+
43
+ alias clp='claude --plugin-dir $TOOLKIT/claude'
44
+ alias clpc='clp -c'
45
+ alias clps='clp --model sonnet'
46
+ alias clpa='clp agents'
47
+ alias clpac='clpa --cwd .'
48
+ ${MARKER_CLOSE}`
49
+ }
50
+
51
+ function run(step: string, cmd: string[]): void {
52
+ const result = Bun.spawnSync(cmd, {
53
+ cwd: root,
54
+ stdout: 'ignore',
55
+ stderr: 'pipe',
56
+ })
57
+ if (result.exitCode !== 0) {
58
+ throw new Error(`${step} failed: ${result.stderr.toString().trim()}`)
59
+ }
60
+ }
61
+
62
+ function installCli(): void {
63
+ logStep('Installing the canon CLI')
64
+ run('bun install', ['bun', 'install', '--cwd', root])
65
+ run('bun link', ['bun', 'link', '--cwd', root])
66
+ logInfo('Dependencies installed and canon linked')
67
+ }
68
+
69
+ function installAliases(): void {
70
+ const zshrc = zshrcPath()
71
+ logStep('Installing Claude Code shell aliases')
72
+ logInfo(
73
+ `This step manages a marked alias block in ${zshrc}, the only file outside the repo this script writes.`,
74
+ )
75
+ logInfo(
76
+ 'Delete the block and its two marker comments to remove the aliases. Re-running never adds a second copy.',
77
+ )
78
+
79
+ const existing = existsSync(zshrc) ? readFileSync(zshrc, 'utf8') : ''
80
+ if (existing.includes(MARKER_OPEN)) {
81
+ logInfo(`Aliases already present in ${zshrc}, skipping`)
82
+ return
83
+ }
84
+ if (/^alias clp=/m.test(existing)) {
85
+ logWarn(
86
+ `An unmanaged clp alias is already in ${zshrc}. Leaving it untouched.`,
87
+ )
88
+ logWarn('Remove it and re-run to switch to the managed block.')
89
+ return
90
+ }
91
+
92
+ appendFileSync(zshrc, `\n${aliasBlock()}\n`)
93
+ logAdd(`Appended canon alias block to ${zshrc}`)
94
+ logInfo(`Run 'source ${zshrc}' or open a new shell to load them`)
95
+ }
96
+
97
+ function verifyCli(): void {
98
+ logStep('Verifying the install')
99
+ if (Bun.which('canon') !== null) {
100
+ logInfo('canon resolves on PATH')
101
+ } else {
102
+ logWarn(
103
+ 'canon not on PATH yet. Ensure $BUN_INSTALL/bin is exported, then re-open your shell.',
104
+ )
105
+ }
106
+ }
107
+
108
+ intro('canon bootstrap')
109
+ let failed = false
110
+ try {
111
+ installCli()
112
+ installAliases()
113
+ verifyCli()
114
+ } catch (error) {
115
+ failed = true
116
+ logError(error instanceof Error ? error.message : String(error))
117
+ } finally {
118
+ outro()
119
+ }
120
+ process.exit(failed ? 1 : 0)