rvlib-shipkit 0.14.0 → 0.17.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 +141 -0
- package/README.md +6 -22
- package/package.json +1 -1
- package/src/assets/kits/identity-kit.ts +1 -1
- package/src/assets/panels/geometry-rects.ts +3 -17
- package/src/assets/server/studio-server.ts +1 -1
- package/src/cli/cmd_Bump.ts +61 -0
- package/src/cli/cmd_Check.ts +2 -2
- package/src/cli/cmd_Ci.ts +10 -2
- package/src/cli/cmd_Commands.ts +126 -0
- package/src/cli/cmd_Hooks.ts +66 -0
- package/src/cli/cmd_Mv.ts +29 -6
- package/src/cli/cmd_Release.ts +41 -0
- package/src/cli/cmd_Releases.ts +42 -0
- package/src/cli/cmd_Rules.ts +20 -6
- package/src/cli/cmd_Start.ts +2 -2
- package/src/cli.ts +14 -1
- package/src/collections/posts.ts +1 -1
- package/src/collections/providers.ts +1 -1
- package/src/contracts/manifests/discord.contract.ts +1 -1
- package/src/contracts/manifests/itch.contract.ts +2 -4
- package/src/core/canonical-scripts.ts +9 -14
- package/src/core/check-suite.ts +1 -1
- package/src/core/discover-checks.ts +21 -6
- package/src/core/run-check.ts +69 -21
- package/src/core/run-ci.ts +78 -9
- package/src/core/run-fixers.ts +144 -0
- package/src/core/write-status.ts +14 -5
- package/src/hooks/commit-message.ts +42 -0
- package/src/hooks/leak-guard.ts +0 -0
- package/src/hooks/root-identity.ts +64 -0
- package/src/hooks/run-hooks.ts +149 -0
- package/src/init-repo.ts +10 -5
- package/src/reflect/prompt-corpus.ts +3 -4
- package/src/release/changelog.ts +70 -0
- package/src/release/git-facts.ts +133 -0
- package/src/release/run-bump.ts +436 -0
- package/src/release/run-release.ts +271 -0
- package/src/release/run-releases.ts +230 -0
- package/src/release/unit-gate.ts +67 -0
- package/src/release/units.ts +246 -0
- package/src/repo-config.ts +56 -2
- package/src/rules/SK001-package-json-shape.ts +39 -17
- package/src/rules/SK002-contracts-fulfillness.ts +1 -0
- package/src/rules/SK003-claude-md-shape.ts +2 -1
- package/src/rules/SK004-gitignore-policy.ts +4 -0
- package/src/rules/SK005-rvlib-versions.ts +22 -3
- package/src/rules/SK006-npm-publish-safety.ts +1 -0
- package/src/rules/SK007-github-visibility.ts +5 -1
- package/src/rules/SK008-reviewer-agent.ts +1 -0
- package/src/rules/SK009-commit-ai-tells.ts +66 -0
- package/src/rules/SK010-undeclared-release-unit.ts +125 -0
- package/src/rules/rule-registry.ts +26 -6
- package/src/rules/rule.ts +8 -1
- package/src/rules/run-fix.ts +15 -8
- package/src/serve/hub-app/col-card.tsx +2 -3
- package/src/serve/hub-app/hub-app.tsx +6 -8
- package/src/serve/hub-app/hub-st.ts +4 -7
- package/src/serve/hub-app/media-view.tsx +2 -2
- package/src/serve/run-serve.ts +4 -5
- package/src/serve/serve-hub.ts +2 -3
- package/src/shipkit.ts +9 -1
- package/src/tui/checks-panel.ts +1 -1
- package/src/tui/run-tui.ts +2 -3
- package/src/tui/tasks-panel.ts +10 -4
- package/src/tui/tui-store.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,146 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
**Correction to 0.16.0.** Its notes and its `--frozen` flag description claimed some combination of flags made `shipkit ci` write nothing. None does: STATUS.md is refreshed on every run unless a caller passes `status: false`. If you wired `ci --no-fix --frozen` into CI expecting a read-only run, it leaves STATUS.md behind, and `--json` is the only output that writes nothing. 0.16.0 also flipped rule SK005 from warning to BLOCKING without saying so: a `link:` dependency that no longer resolves, or a declared dependency that is not installed, now fails the gate.
|
|
6
|
+
|
|
7
|
+
- **`bump --all` takes a kind per unit.** Write `## Unreleased (minor)` on a unit's changelog heading and a batch release gives each unit the kind it actually needs, in ONE gate. A unit whose heading declares nothing refuses rather than being guessed into a version number. Passing a kind on the command line still applies it to the whole selection, and promoting strips the marker. `shipkit bump <unit>` with no kind works the same way.
|
|
8
|
+
- **`notReleasable` can no longer cover a declared release unit.** An entry naming a container that holds one excused every future package under it while the rule reported green. The two lists state opposite intents, so overlap fails.
|
|
9
|
+
- **A release gates fresh.** SK005 caches a clean pass for a day to rate-limit its registry call, but its blocking branches are local facts about whether the tree can import its own dependencies. Those are measured at release time now, not replayed.
|
|
10
|
+
- **The bump rehearsal reads `git fetch`.** Offline or with an expired credential it could compare against stale remote refs and still report a clean merge. It warns and closes inconclusive instead.
|
|
11
|
+
- New pre-commit gate **root-identity**: the name in the root `package.json` and `repo.config.ts`, index against HEAD, plus staged `.bak`/`.orig`/`.rej`. A fixture builder that escapes its temp dir overwrites those files and every other gate still passes, because a gutted repo lints fine. A deliberate rename passes with `SHIPKIT_ALLOW_IDENTITY_CHANGE=1`.
|
|
12
|
+
- `.rv-private/` joins the SK004 gitignore policy. It holds the leak guard's own keyword file, which was committable in every repo until now.
|
|
13
|
+
- `ci --frozen` exists as a flag, and no TUI panel writes STATUS.md any more, so opening the dashboard cannot overwrite a full gate's verdict with a tasks-only one.
|
|
14
|
+
|
|
15
|
+
## 0.16.0
|
|
16
|
+
|
|
17
|
+
Upgrading from 0.14.0 lands straight here: 0.15.0 was stamped and never
|
|
18
|
+
published, so everything under the `## 0.15.0` heading below ships in this
|
|
19
|
+
release too, its BREAKING lines included.
|
|
20
|
+
|
|
21
|
+
Releasing becomes part of the toolkit. `shipkit ci` is the one gate, `shipkit
|
|
22
|
+
bump` is the one release command, git hooks are owned by the binary instead of
|
|
23
|
+
by hand-written shell, and a repo can now ship more than its own root.
|
|
24
|
+
|
|
25
|
+
### One gate
|
|
26
|
+
|
|
27
|
+
- `shipkit ci` repairs before it judges. A mechanical fix pass runs first
|
|
28
|
+
(formatter, canonical imports, the fixable repo-shape rules), then the task
|
|
29
|
+
DAG, then every check. `--no-fix` skips the repair pass. For a run that
|
|
30
|
+
writes nothing, add `--frozen`. Under CI the
|
|
31
|
+
fixers still run and the gate FAILS on any file they rewrote, so a green CI
|
|
32
|
+
cannot hide an unformatted commit. `--fix-rules` covers the structural rule
|
|
33
|
+
autofixes and `--no-fix-rules` leaves repo config alone.
|
|
34
|
+
- **BREAKING: every check declares `blocking`.** `export const blocking = true`
|
|
35
|
+
means the failure is about this repo and stops you. `false` means the check
|
|
36
|
+
reports the WORLD (a credential nobody minted, a machine that is off), so it
|
|
37
|
+
prints as a warning and never blocks. A repo whose checks all probe the world
|
|
38
|
+
declares `checkDefaults: { blocking: false }` once instead of repeating it in
|
|
39
|
+
every file. A check that declares neither, in a repo with no default, now
|
|
40
|
+
fails the run: a check that silently does not block is the failure nobody
|
|
41
|
+
sees. A non-blocking failure is demoted where it is produced, so the printed
|
|
42
|
+
severity can never disagree with the exit code.
|
|
43
|
+
- A repo's own rules (`repo.config.ts rules: []`) run in the same engine as the
|
|
44
|
+
built-in `SK###` rules, so a repo rule can BLOCK rather than only warn in a
|
|
45
|
+
health report. `shipkit rules` marks every rule BLOCKS or warns.
|
|
46
|
+
- New `runCi` options: `frozen`, `failOnAdvisory`, `skipChecks`.
|
|
47
|
+
- `shipkit check` writes STATUS.md as `shipkit check`, and only the full gate
|
|
48
|
+
says "clear to ship".
|
|
49
|
+
|
|
50
|
+
### Release units
|
|
51
|
+
|
|
52
|
+
- A repo is N releasable units. The ROOT unit is mandatory and never declared.
|
|
53
|
+
Sub-units are optional and declared in `repo.config.ts units: [{ dir }]`.
|
|
54
|
+
Each unit owns its own CHANGELOG, its own version, its own tag and its own
|
|
55
|
+
targets. Tag shapes stay per unit: `v{version}` for the root,
|
|
56
|
+
`{name}@{version}` for a sub-unit by default, overridable with `tag`.
|
|
57
|
+
- **BREAKING: a sub-unit publishes to npm only when its entry declares
|
|
58
|
+
`npm: true`.** A package that merely forgot `private: true` is no longer a
|
|
59
|
+
publish target by inference. `npm: true` on a private package.json is a
|
|
60
|
+
resolution failure, not a silent skip.
|
|
61
|
+
- `notReleasable: [dir]` declares a package that is deliberately not a release
|
|
62
|
+
unit. New rule SK010 fails on any package that is neither declared nor
|
|
63
|
+
excluded, so a forgotten package cannot hide. It asks git for the package
|
|
64
|
+
list, so gitignored trees never trip it.
|
|
65
|
+
- Nothing is scanned into existence. Every surface that used to rediscover
|
|
66
|
+
packages on its own (drift probes, per-unit tests, per-unit typecheck, the
|
|
67
|
+
stranded-tag probe) reads the declared units.
|
|
68
|
+
|
|
69
|
+
### The release path
|
|
70
|
+
|
|
71
|
+
- `shipkit bump <major|minor|patch> [unit] [--all]` is the whole release from
|
|
72
|
+
one word. Run from a feature branch it merges into the release branch FIRST,
|
|
73
|
+
then computes every fact on the merged tree (next version, tag refusals,
|
|
74
|
+
per-unit test and typecheck), promotes the CHANGELOG `## Unreleased` section,
|
|
75
|
+
writes the version, runs the gate and every prepublish script, commits, tags,
|
|
76
|
+
pushes and publishes. Nothing is pushed until the sequence succeeds, so any
|
|
77
|
+
refusal before the version commit rolls the merge back and returns you to
|
|
78
|
+
your branch. A tag the remote already carries refuses before anything is
|
|
79
|
+
written. `--all` bumps every unit that has Unreleased notes, root last, in
|
|
80
|
+
one version commit. `--dry-run` rehearses the whole thing and writes nothing.
|
|
81
|
+
- `shipkit release [unit]` publishes the version already in package.json,
|
|
82
|
+
reading its notes from the matching `## <version>` section. It resumes a
|
|
83
|
+
partial release by comparing the tag's COMMIT rather than its name, so a tag
|
|
84
|
+
name taken on the remote reads as a burnt version instead of "fully
|
|
85
|
+
released".
|
|
86
|
+
- `shipkit releases` prints one row per unit: local version, npm version, tag
|
|
87
|
+
state, work landed since the tag, GitHub release state, then ONE next action
|
|
88
|
+
per unit. A version stamped without a release is called out: notes that stop
|
|
89
|
+
before the work did are how a package ships under someone else's changelog.
|
|
90
|
+
A unit that is in sync with npm and carries `## Unreleased` notes says so as
|
|
91
|
+
a note, so a release waiting on a decision is never invisible.
|
|
92
|
+
- npm publishing lives here now. A publish runs the unit's own test suite and
|
|
93
|
+
typecheck, refuses a dirty unit directory, and reads the npm token from
|
|
94
|
+
`release.npmTokenCommand` into a throwaway userconfig, so the token never
|
|
95
|
+
reaches argv, a log, or a tracked `.npmrc`. A private unit skips npm and
|
|
96
|
+
still gets its tag, its push and its GitHub release.
|
|
97
|
+
- The release path runs ONE gate. The separate health gate is gone and
|
|
98
|
+
`release.requireHealthy` works through `runCi` (non-blocking check failures
|
|
99
|
+
become fatal). The gate runs frozen, and the tree is re-checked for dirt
|
|
100
|
+
before anything publishes.
|
|
101
|
+
|
|
102
|
+
### Commit gates, owned by the binary
|
|
103
|
+
|
|
104
|
+
- `shipkit hooks install | list | pre-commit | commit-msg`. Built in: a leak
|
|
105
|
+
guard over a keyword file, and one commit-message predicate that rule SK009
|
|
106
|
+
also uses, replacing a copy kept in sync by hand. A repo adds its own gates
|
|
107
|
+
with `hooks: { preCommit: [{ id, cmd }] }` or `commitMsg`, and disables a
|
|
108
|
+
built-in by name. Hand-rolled hooks are invisible: a repo reports green while
|
|
109
|
+
its next commit is already doomed, and `shipkit hooks list` answers what
|
|
110
|
+
blocks a commit without anyone reading shell.
|
|
111
|
+
|
|
112
|
+
### Discoverability and scaffolding
|
|
113
|
+
|
|
114
|
+
- `shipkit commands [--json] [--out <file>] [--check]` prints every command the
|
|
115
|
+
binary registers, with every flag and a worked example. `--out` rewrites a
|
|
116
|
+
marked block inside a docs page and `--check` fails when that block is stale,
|
|
117
|
+
so a command added without a doc line cannot land.
|
|
118
|
+
- `shipkit mv` renames code as well as md pages. A non-md path routes to the
|
|
119
|
+
ts-tools rename, which rewrites every path reference. Knowing WHICH rename
|
|
120
|
+
tool applies is exactly the doubt the toolkit exists to remove.
|
|
121
|
+
- **BREAKING: a package.json script that only forwards to a shipkit verb is no
|
|
122
|
+
longer canon.** SK001 stops requiring `ci`, `check`, `contracts`, `tui`,
|
|
123
|
+
`mv`, `asks` and `assets:identity` scripts, and `shipkit init` no longer
|
|
124
|
+
scaffolds them. The verb is the vocabulary; an alias is one more name to keep
|
|
125
|
+
in sync. Only a body shipkit cannot express itself (`hub`, which needs watch
|
|
126
|
+
mode) stays a script.
|
|
127
|
+
- SK001 asks for a `test` script only once the repo has test files. `bun test`
|
|
128
|
+
exits non-zero when it finds none, and a fixer must never manufacture a
|
|
129
|
+
failure.
|
|
130
|
+
- SK007 (github visibility) blocks.
|
|
131
|
+
|
|
132
|
+
## 0.15.0 — 2026-07-28
|
|
133
|
+
|
|
134
|
+
- Hub: the `?needs=1` URL form is gone — the severity filter address is
|
|
135
|
+
`/?f=needs` · `/?f=red` only.
|
|
136
|
+
- Assets (BREAKING): the exported `migrateCircle` is removed, and
|
|
137
|
+
`cleanRect` no longer reads the legacy rect `scale` key — session files
|
|
138
|
+
must carry `size` (a one-shot `"scale"` → `"size"` rename migrates old
|
|
139
|
+
files).
|
|
140
|
+
- Comment cleanup across src (no behavior change).
|
|
141
|
+
- check discovery: the self-cached heuristic recognizes `PageValidator`
|
|
142
|
+
(replacing the old subclass name) alongside `CheckSuite`.
|
|
143
|
+
|
|
3
144
|
## 0.14.0 — 2026-07-27
|
|
4
145
|
|
|
5
146
|
New rule SK008 — the mandated reviewer agent type. A repo declares
|
package/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# rvlib-shipkit
|
|
2
2
|
|
|
3
|
-
Repo toolkit for shipping: one config file gives a repo a commit gate
|
|
4
|
-
(task DAG), release-contract fulfillness checks, a human ask queue,
|
|
5
|
-
markdown-wiki tooling, and the asset atelier (generate, curate, and ship
|
|
6
|
-
assets — favicon/logo/OG included).
|
|
3
|
+
Repo toolkit for shipping: one config file gives a repo a commit gate (task DAG), release-contract fulfillness checks, a human ask queue, markdown-wiki tooling, and the asset atelier (generate, curate, and ship assets — favicon/logo/OG included).
|
|
7
4
|
|
|
8
5
|
```sh
|
|
9
6
|
bun add -d rvlib-shipkit
|
|
@@ -22,8 +19,7 @@ export default defineRepo({
|
|
|
22
19
|
})
|
|
23
20
|
```
|
|
24
21
|
|
|
25
|
-
Health checks need no config at all: drop a `<name>.check.ts` file anywhere
|
|
26
|
-
and `shipkit check` discovers and runs it (wire output + exit code).
|
|
22
|
+
Health checks need no config at all: drop a `<name>.check.ts` file anywhere and `shipkit check` discovers and runs it (wire output + exit code).
|
|
27
23
|
|
|
28
24
|
## CLI
|
|
29
25
|
|
|
@@ -60,28 +56,16 @@ shipkit init scaffold config + scripts, print the resolved floor
|
|
|
60
56
|
shipkit start ensure <root>/<name> + its CLAUDE.md include; prints the dir
|
|
61
57
|
```
|
|
62
58
|
|
|
63
|
-
Output is a stable line protocol — `ok:` / `WARN` / `FAIL:` / `skip:` with
|
|
64
|
-
indented `fix:` pointers — exit 0/1. Greppable by humans, parseable by
|
|
65
|
-
agents, `--json` where structure helps.
|
|
59
|
+
Output is a stable line protocol — `ok:` / `WARN` / `FAIL:` / `skip:` with indented `fix:` pointers — exit 0/1. Greppable by humans, parseable by agents, `--json` where structure helps.
|
|
66
60
|
|
|
67
61
|
## Release contracts
|
|
68
62
|
|
|
69
|
-
A venue contract is data: required files, exact PNG dimensions (header-parsed,
|
|
70
|
-
no image dependency), glob minimums, required markdown sections. Enforcement
|
|
71
|
-
is both ways — a declared venue with an incomplete folder fails, and an
|
|
72
|
-
undeclared venue whose signals fire (deploy scripts, folders) fails too.
|
|
73
|
-
`exempt: { steam: "reason" }` skips visibly. Wire venue deploys as
|
|
74
|
-
`deps: ['contracts']` and an incomplete folder physically blocks the upload.
|
|
63
|
+
A venue contract is data: required files, exact PNG dimensions (header-parsed, no image dependency), glob minimums, required markdown sections. Enforcement is both ways — a declared venue with an incomplete folder fails, and an undeclared venue whose signals fire (deploy scripts, folders) fails too. `exempt: { steam: "reason" }` skips visibly. Wire venue deploys as `deps: ['contracts']` and an incomplete folder physically blocks the upload.
|
|
75
64
|
|
|
76
|
-
Ship your own contracts via `contracts: { extra: [defineContract({...})] }`
|
|
77
|
-
— see `rvlib-shipkit/contracts` for the rule vocabulary.
|
|
65
|
+
Ship your own contracts via `contracts: { extra: [defineContract({...})] }` — see `rvlib-shipkit/contracts` for the rule vocabulary.
|
|
78
66
|
|
|
79
67
|
## Library use
|
|
80
68
|
|
|
81
|
-
Everything the CLI does is importable: `runTasks`, `CheckSuite`,
|
|
82
|
-
`evaluateContracts`, the asks queue, `scanMdGraph`/`lintMd`/`serveMdGraph`,
|
|
83
|
-
and the assets surface (`defineKit`/`defineKind`, `identityKinds`,
|
|
84
|
-
`studioRoutes`, `saveEndpoint` via `rvlib-shipkit/vite`).
|
|
85
|
-
Ships TypeScript source; Bun ≥ 1.0.
|
|
69
|
+
Everything the CLI does is importable: `runTasks`, `CheckSuite`, `evaluateContracts`, the asks queue, `scanMdGraph`/`lintMd`/`serveMdGraph`, and the assets surface (`defineKit`/`defineKind`, `identityKinds`, `studioRoutes`, `saveEndpoint` via `rvlib-shipkit/vite`). Ships TypeScript source. Bun ≥ 1.0.
|
|
86
70
|
|
|
87
71
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@ export const OG_H = 630
|
|
|
37
37
|
export const LOGO_SIZE = 1024
|
|
38
38
|
|
|
39
39
|
/** the one next step when a comfy mark has no picked roll yet */
|
|
40
|
-
export const NO_PICK_FIX = 'roll + pick in the studio: bun run
|
|
40
|
+
export const NO_PICK_FIX = 'roll + pick in the studio: bun run hub → /assets/#identity-logo'
|
|
41
41
|
|
|
42
42
|
export function identityKinds(brand: Brand): Kind[] {
|
|
43
43
|
return [
|
|
@@ -20,11 +20,11 @@ export type GroundRect = {
|
|
|
20
20
|
const r2 = (n: number): number => Math.round(n * 100) / 100
|
|
21
21
|
const r3 = (n: number): number => Math.round(n * 1000) / 1000
|
|
22
22
|
|
|
23
|
-
/** Normalize a
|
|
24
|
-
*
|
|
23
|
+
/** Normalize a rect: round (x/y/w/h 2dp, size 3dp, legs 2dp), ratio-lock h
|
|
24
|
+
* preserving center-y. */
|
|
25
25
|
export function cleanRect(raw: Record<string, unknown>, ratio: number): GroundRect {
|
|
26
26
|
const num = (k: string, dflt: number): number => (typeof raw[k] === 'number' && Number.isFinite(raw[k]) ? (raw[k] as number) : dflt)
|
|
27
|
-
const size =
|
|
27
|
+
const size = num('size', 1)
|
|
28
28
|
const x = num('x', 0)
|
|
29
29
|
const y = num('y', 0)
|
|
30
30
|
const w = Math.max(1, num('w', 1))
|
|
@@ -68,17 +68,3 @@ export function defaultLegs(oval: GroundRect): LegRect[] {
|
|
|
68
68
|
return { x: r2(cx + dx * u - lw / 2), y: r2(footY - lh), w: r2(lw), h: r2(lh) }
|
|
69
69
|
})
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
/** Legacy circle → oval migration (run on session read). A circle is
|
|
73
|
-
* |h − w| ≤ 0.5. anchor: where the new oval's center-y comes from —
|
|
74
|
-
* 'bottom' (characters/scenery: feet at circle bottom) or 'center'
|
|
75
|
-
* (buildings). artNormalized folds art-height units into oval-width units:
|
|
76
|
-
* size *= w / art. Non-circles pass through untouched. */
|
|
77
|
-
export function migrateCircle(rect: GroundRect, opts: { ratio: number; anchor: 'bottom' | 'center'; artNormalized?: boolean }): GroundRect {
|
|
78
|
-
if (Math.abs(rect.h - rect.w) > 0.5) return rect
|
|
79
|
-
const cy = opts.anchor === 'bottom' ? rect.y + rect.h : rect.y + rect.h / 2
|
|
80
|
-
let size = rect.size
|
|
81
|
-
if (opts.artNormalized === true && rect.art != null && rect.art > 0) size = size * (rect.w / rect.art)
|
|
82
|
-
const h = rect.w * opts.ratio
|
|
83
|
-
return cleanRect({ ...rect, size, h, y: cy - h / 2 } as unknown as Record<string, unknown>, opts.ratio)
|
|
84
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The studio HTTP server — roll variants, compare, pick, geometry metadata,
|
|
2
2
|
// category grid data. 127.0.0.1 ONLY, forever. Watch-mode restarts drop the
|
|
3
|
-
// in-memory queue — accepted tradeoff
|
|
3
|
+
// in-memory queue — accepted tradeoff; `shipkit
|
|
4
4
|
// assets fill` is the recovery path.
|
|
5
5
|
//
|
|
6
6
|
// ids/names are validated against the kit + the roll-file regex — no path
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// shipkit bump, version, changelog, gate, prepublish, commit, release, from
|
|
2
|
+
// one word. Thin clipanion surface; logic in release/run-bump.ts.
|
|
3
|
+
import { Command, Option } from 'clipanion'
|
|
4
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
5
|
+
import { runBump, type BumpKind } from '../release/run-bump.ts'
|
|
6
|
+
|
|
7
|
+
export class BumpCommand extends Command {
|
|
8
|
+
static override paths = [['bump']]
|
|
9
|
+
|
|
10
|
+
static override usage = Command.Usage({
|
|
11
|
+
category: 'Gate',
|
|
12
|
+
description: 'ship a version in one word: bumps a release unit (default the repo root), run from a feature branch it merges into the release branch first, then promotes the CHANGELOG Unreleased section, bumps package.json, runs the gate and every prepublish script, commits, tags, pushes, and cuts the release. Any failure before the version commit rolls the merge back and returns you to your branch.',
|
|
13
|
+
examples: [
|
|
14
|
+
['a bugfix release of the repo itself', 'shipkit bump patch'],
|
|
15
|
+
['a feature release', 'shipkit bump minor'],
|
|
16
|
+
['a breaking release', 'shipkit bump major'],
|
|
17
|
+
['one sub-unit (ids: shipkit releases)', 'shipkit bump minor rvlib-shipkit'],
|
|
18
|
+
['a whole mixed batch in ONE gate, each unit at the kind its heading declares', 'shipkit bump --all'],
|
|
19
|
+
['every unit that has Unreleased notes, all at the same kind', 'shipkit bump patch --all'],
|
|
20
|
+
['one unit, kind from its own heading', 'shipkit bump rvlib-shipkit'],
|
|
21
|
+
['rehearse the whole thing', 'shipkit bump minor --dry-run'],
|
|
22
|
+
['bump and commit, release later', 'shipkit bump patch --no-release'],
|
|
23
|
+
['from a feature branch, refuse rather than merge', 'shipkit bump patch --no-merge'],
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// both positionals are optional so `bump --all` can read each unit's own
|
|
28
|
+
// kind. The kinds are a closed set of three words, so a first positional
|
|
29
|
+
// that is not one of them is a unit id, never an ambiguity
|
|
30
|
+
kind = Option.String({ required: false })
|
|
31
|
+
unit = Option.String({ required: false })
|
|
32
|
+
dryRun = Option.Boolean('--dry-run', false, { description: 'rehearse: run every preflight, the gate and the prepublish scripts, then stop before any version is written. the gate can rewrite files, and the merge is probed rather than performed, so the release branch never moves' })
|
|
33
|
+
noRelease = Option.Boolean('--no-release', false, { description: 'stop after the version commit, do not tag or publish' })
|
|
34
|
+
noMerge = Option.Boolean('--no-merge', false, { description: 'refuse on a feature branch instead of merging it into the release branch' })
|
|
35
|
+
all = Option.Boolean('--all', false, { description: 'bump every unit whose changelog carries Unreleased notes, in declared order, root last. Given a kind it applies to ALL of them. Given none, each unit takes the kind from its own "## Unreleased (minor)" heading, and a unit whose heading is silent refuses' })
|
|
36
|
+
|
|
37
|
+
async execute(): Promise<number> {
|
|
38
|
+
const KINDS = ['major', 'minor', 'patch']
|
|
39
|
+
// `bump rvlib-shipkit` names a unit and leaves the kind to its heading
|
|
40
|
+
const kind = this.kind != null && KINDS.includes(this.kind) ? (this.kind as BumpKind) : null
|
|
41
|
+
const unit = kind == null && this.kind != null ? this.kind : this.unit
|
|
42
|
+
if (kind == null && this.kind != null && this.unit != null) {
|
|
43
|
+
this.context.stderr.write(`FAIL: bump takes major, minor or patch (got '${this.kind}')\n`)
|
|
44
|
+
return 1
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const config = await loadRepoConfig(process.cwd())
|
|
48
|
+
return await runBump(process.cwd(), config, kind, {
|
|
49
|
+
dryRun: this.dryRun,
|
|
50
|
+
noRelease: this.noRelease,
|
|
51
|
+
noMerge: this.noMerge,
|
|
52
|
+
unit,
|
|
53
|
+
all: this.all,
|
|
54
|
+
print: (l) => this.context.stdout.write(`${l}\n`),
|
|
55
|
+
})
|
|
56
|
+
} catch (e) {
|
|
57
|
+
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
58
|
+
return 1
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/cli/cmd_Check.ts
CHANGED
|
@@ -6,11 +6,11 @@ import { runCheck } from '../core/run-check.ts'
|
|
|
6
6
|
import { runFix } from '../rules/run-fix.ts'
|
|
7
7
|
|
|
8
8
|
export class CheckCommand extends Command {
|
|
9
|
-
static override paths = [['check']]
|
|
9
|
+
static override paths = [['check'], ['doctor']]
|
|
10
10
|
|
|
11
11
|
static override usage = Command.Usage({
|
|
12
12
|
category: 'Health',
|
|
13
|
-
description: '
|
|
13
|
+
description: 'the checks alone, without the build tasks. `shipkit ci` runs these too and is what you normally want. --fix applies rule autofixes',
|
|
14
14
|
examples: [
|
|
15
15
|
['run (cached where valid)', 'shipkit check'],
|
|
16
16
|
['bust every cache layer', 'shipkit check --fresh'],
|
package/src/cli/cmd_Ci.ts
CHANGED
|
@@ -8,9 +8,11 @@ export class CiCommand extends Command {
|
|
|
8
8
|
|
|
9
9
|
static override usage = Command.Usage({
|
|
10
10
|
category: 'Gate',
|
|
11
|
-
description: '
|
|
11
|
+
description: 'THE command. Repairs what a machine can (formatter, imports, repo shape), runs every task, then every check. A check that declares blocking:false warns instead of failing. Run this instead of anything else.',
|
|
12
12
|
examples: [
|
|
13
|
-
['full gate', 'shipkit ci'],
|
|
13
|
+
['full gate, mechanical fixes applied', 'shipkit ci'],
|
|
14
|
+
['gate without the repair pass (checks can still write)', 'shipkit ci --no-fix'],
|
|
15
|
+
['leave repo config files alone', 'shipkit ci --no-fix-rules'],
|
|
14
16
|
['stop at the first failure', 'shipkit ci --bail'],
|
|
15
17
|
['one task + its dep closure', 'shipkit ci --only typecheck'],
|
|
16
18
|
['include an opt-in task', 'shipkit ci --with up:itch'],
|
|
@@ -19,6 +21,9 @@ export class CiCommand extends Command {
|
|
|
19
21
|
|
|
20
22
|
bail = Option.Boolean('--bail', false, { description: 'stop at the first failure' })
|
|
21
23
|
json = Option.Boolean('--json', false, { description: 'machine-readable result instead of wire lines' })
|
|
24
|
+
fix = Option.Boolean('--fix', true, { description: 'repair the mechanical slots before gating. Default on. --no-fix skips the repair pass, checks can still write. Under CI the fixers still run but the gate FAILS on any file they rewrite' })
|
|
25
|
+
fixRules = Option.Boolean('--fix-rules', true, { description: 'apply the structural rule autofixes too. Default on. --no-fix-rules leaves repo config alone' })
|
|
26
|
+
frozen = Option.Boolean('--frozen', false, { description: 'report-only mode for self-maintaining rules (SK005 never touches package.json). STATUS.md is still refreshed, so no combination of flags makes the run write nothing' })
|
|
22
27
|
skip = Option.Array('--skip', [], { description: 'skip a task group (repeatable)' })
|
|
23
28
|
withIds = Option.Array('--with', [], { description: 'include an opt-in task (repeatable)' })
|
|
24
29
|
only = Option.Array('--only', [], { description: 'run only this task + its dep closure (repeatable)' })
|
|
@@ -28,6 +33,9 @@ export class CiCommand extends Command {
|
|
|
28
33
|
return await runCi(process.cwd(), {
|
|
29
34
|
bail: this.bail,
|
|
30
35
|
json: this.json,
|
|
36
|
+
fix: this.fix,
|
|
37
|
+
fixRules: this.fixRules,
|
|
38
|
+
frozen: this.frozen,
|
|
31
39
|
skipGroups: this.skip,
|
|
32
40
|
withIds: this.withIds,
|
|
33
41
|
onlyIds: this.only,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// the binary describing itself, in markdown. a hand-copied command list
|
|
2
|
+
// drifts the moment a flag changes, and then teaches commands that do not
|
|
3
|
+
// exist. rendering the live registry makes the doc a build artifact.
|
|
4
|
+
import { Command, Option } from 'clipanion'
|
|
5
|
+
|
|
6
|
+
export type CommandOption = { definition: string; description: string }
|
|
7
|
+
export type CommandRow = {
|
|
8
|
+
path: string
|
|
9
|
+
category: string
|
|
10
|
+
description: string
|
|
11
|
+
usage: string
|
|
12
|
+
options: CommandOption[]
|
|
13
|
+
examples: [string, string][]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Definition = {
|
|
17
|
+
path: string
|
|
18
|
+
category?: string | null
|
|
19
|
+
description?: string | null
|
|
20
|
+
usage?: string | null
|
|
21
|
+
examples?: [string, string][] | null
|
|
22
|
+
options?: { definition?: string; description?: string | null }[] | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// clipanion renders a value placeholder as `#0`, which nobody can type
|
|
26
|
+
const clean = (s: string | null | undefined): string => (s ?? '').trim().replace(/\s+/g, ' ').replace(/ #\d+\b/g, ' <value>')
|
|
27
|
+
|
|
28
|
+
/** rows from the live registry, builtins dropped, sorted by category then path.
|
|
29
|
+
* clipanion splits what we need across two calls: the plain form carries the
|
|
30
|
+
* examples, the detailed form carries the flags. Merge by path, or the page
|
|
31
|
+
* ends up showing a command with no arguments, which is the state that sends
|
|
32
|
+
* an agent to `--help` before it can act. */
|
|
33
|
+
export function commandRows(plain: Definition[], detailed: Definition[] = []): CommandRow[] {
|
|
34
|
+
const flagsByPath = new Map(detailed.map((d) => [d.path, d.options ?? []]))
|
|
35
|
+
return plain
|
|
36
|
+
.filter((d) => !/^shipkit (--help|-h|--version|-v)$/.test(d.path))
|
|
37
|
+
.map((d) => ({
|
|
38
|
+
path: d.path.replace(/^shipkit /, ''),
|
|
39
|
+
category: clean(d.category) || 'Other',
|
|
40
|
+
// verbatim from the CLI so the doc cannot say something it does not
|
|
41
|
+
description: clean(d.description),
|
|
42
|
+
usage: clean(d.usage),
|
|
43
|
+
options: (flagsByPath.get(d.path) ?? [])
|
|
44
|
+
.map((o) => ({ definition: clean(o.definition), description: clean(o.description) }))
|
|
45
|
+
.filter((o) => o.definition !== ''),
|
|
46
|
+
examples: (d.examples ?? []).map(([label, cmd]) => [clean(label), clean(cmd)] as [string, string]),
|
|
47
|
+
}))
|
|
48
|
+
.sort((a, b) => a.category.localeCompare(b.category) || a.path.localeCompare(b.path))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** the markdown block: per command, the real invocation with its arguments and
|
|
52
|
+
* flags, then a worked example. A name and a sentence are not enough to act
|
|
53
|
+
* on: an agent that cannot see the arguments either guesses them or runs
|
|
54
|
+
* `--help` first, and both are the waste this page exists to remove. */
|
|
55
|
+
export function renderMarkdown(rows: CommandRow[]): string {
|
|
56
|
+
const out: string[] = []
|
|
57
|
+
let cat = ''
|
|
58
|
+
for (const r of rows) {
|
|
59
|
+
if (r.category !== cat) { cat = r.category; out.push(`\n### ${cat}\n`) }
|
|
60
|
+
const sig = r.usage.replace(/^\$0/, 'shipkit').trim() || `shipkit ${r.path}`
|
|
61
|
+
const flags = r.options.map((o) => `[${o.definition}]`).join(' ')
|
|
62
|
+
// one line per bullet: a description on its own indented line reads as
|
|
63
|
+
// hard-wrapped prose to the house lint
|
|
64
|
+
out.push(`- \`${sig}${flags === '' ? '' : ` ${flags}`}\` — ${r.description}`)
|
|
65
|
+
for (const o of r.options) if (o.description !== '') out.push(` - \`${o.definition}\` ${o.description}`)
|
|
66
|
+
for (const [label, cmd] of r.examples) out.push(` - ${label}: \`${cmd}\``)
|
|
67
|
+
}
|
|
68
|
+
return out.join('\n').trim()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const BEGIN = '<!-- shipkit:commands -->'
|
|
72
|
+
export const END = '<!-- /shipkit:commands -->'
|
|
73
|
+
|
|
74
|
+
/** replace the marked block in a host file. Returns null when the markers are
|
|
75
|
+
* missing, so a renamed or hand-mangled host fails loud instead of silently
|
|
76
|
+
* writing nothing. */
|
|
77
|
+
export function spliceBlock(host: string, block: string): string | null {
|
|
78
|
+
const a = host.indexOf(BEGIN)
|
|
79
|
+
const b = host.indexOf(END)
|
|
80
|
+
if (a === -1 || b === -1 || b < a) return null
|
|
81
|
+
return `${host.slice(0, a + BEGIN.length)}\n\n${block}\n\n${host.slice(b)}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class CommandsCommand extends Command {
|
|
85
|
+
static override paths = [['commands']]
|
|
86
|
+
|
|
87
|
+
static override usage = Command.Usage({
|
|
88
|
+
category: 'Health',
|
|
89
|
+
description: 'every command this binary registers, as markdown or json — the source a docs page is generated FROM, never hand-copied',
|
|
90
|
+
examples: [
|
|
91
|
+
['markdown index', 'shipkit commands'],
|
|
92
|
+
['machine-readable', 'shipkit commands --json'],
|
|
93
|
+
['write into a docs page between the markers', 'shipkit commands --out wiki/build/shipkit-cli.md'],
|
|
94
|
+
['fail if that page is stale (lint mode)', 'shipkit commands --out wiki/build/shipkit-cli.md --check'],
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
json = Option.Boolean('--json', false, { description: 'machine-readable [{path, category, description}]' })
|
|
99
|
+
out = Option.String('--out', { description: 'host file carrying the shipkit:commands markers. The block between them is rewritten' })
|
|
100
|
+
check = Option.Boolean('--check', false, { description: 'with --out: exit 1 if the block is stale, write nothing' })
|
|
101
|
+
|
|
102
|
+
async execute(): Promise<number> {
|
|
103
|
+
const rows = commandRows(
|
|
104
|
+
this.cli.definitions() as Definition[],
|
|
105
|
+
(this.cli.definitions as (o?: { detailed?: boolean }) => Definition[])({ detailed: true }),
|
|
106
|
+
)
|
|
107
|
+
if (this.out == null) {
|
|
108
|
+
this.context.stdout.write(this.json ? `${JSON.stringify(rows)}\n` : `${renderMarkdown(rows)}\n`)
|
|
109
|
+
return 0
|
|
110
|
+
}
|
|
111
|
+
const { existsSync, readFileSync, writeFileSync } = await import('fs')
|
|
112
|
+
if (!existsSync(this.out)) { this.context.stdout.write(`FAIL: ${this.out} does not exist\n`); return 1 }
|
|
113
|
+
const host = readFileSync(this.out, 'utf8')
|
|
114
|
+
const next = spliceBlock(host, renderMarkdown(rows))
|
|
115
|
+
if (next == null) { this.context.stdout.write(`FAIL: ${this.out} has no ${BEGIN} / ${END} markers\n`); return 1 }
|
|
116
|
+
if (next === host) { this.context.stdout.write(`ok: ${this.out} command index current (${rows.length} commands)\n`); return 0 }
|
|
117
|
+
if (this.check) {
|
|
118
|
+
this.context.stdout.write(`FAIL: ${this.out} command index is STALE — the binary registers commands the page does not list\n`)
|
|
119
|
+
this.context.stdout.write(` fix: shipkit commands --out ${this.out}\n`)
|
|
120
|
+
return 1
|
|
121
|
+
}
|
|
122
|
+
writeFileSync(this.out, next)
|
|
123
|
+
this.context.stdout.write(`ok: ${this.out} command index rewritten (${rows.length} commands)\n`)
|
|
124
|
+
return 0
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// shipkit hooks, install the git hooks, run their gates, or list what
|
|
2
|
+
// blocks a commit. Thin clipanion surface; logic in hooks/run-hooks.ts.
|
|
3
|
+
import { Command, Option } from 'clipanion'
|
|
4
|
+
import { installHooks, listGates, runCommitMsg, runPreCommit, type GateOutcome } from '../hooks/run-hooks.ts'
|
|
5
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
6
|
+
|
|
7
|
+
const report = (out: GateOutcome[], write: (s: string) => void): number => {
|
|
8
|
+
for (const g of out) for (const l of g.lines) write(`${l}\n`)
|
|
9
|
+
return out.every((g) => g.ok) ? 0 : 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class HooksInstallCommand extends Command {
|
|
13
|
+
static override paths = [['hooks', 'install']]
|
|
14
|
+
static override usage = Command.Usage({
|
|
15
|
+
category: 'Gate',
|
|
16
|
+
description: 'write hooks/pre-commit + hooks/commit-msg and point core.hooksPath at them (idempotent)',
|
|
17
|
+
examples: [['wire the hooks', 'shipkit hooks install']],
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
bin = Option.String('--bin', 'bunx rvlib-shipkit', { description: 'how the hook body invokes shipkit (in-repo consumers pass `bun <path to cli.ts>`)' })
|
|
21
|
+
|
|
22
|
+
async execute(): Promise<number> {
|
|
23
|
+
const r = installHooks(process.cwd(), this.bin)
|
|
24
|
+
for (const w of r.written) this.context.stdout.write(`ok: wrote ${w}\n`)
|
|
25
|
+
if (r.written.length === 0) this.context.stdout.write('ok: hook bodies already current\n')
|
|
26
|
+
this.context.stdout.write(`${r.note.startsWith('WARN') ? r.note : `ok: ${r.note}`}\n`)
|
|
27
|
+
return r.note.startsWith('WARN') ? 1 : 0
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class HooksPreCommitCommand extends Command {
|
|
32
|
+
static override paths = [['hooks', 'pre-commit']]
|
|
33
|
+
static override usage = Command.Usage({ category: 'Gate', description: 'run the pre-commit gate (called by the hook)' })
|
|
34
|
+
|
|
35
|
+
async execute(): Promise<number> {
|
|
36
|
+
const config = await loadRepoConfig(process.cwd())
|
|
37
|
+
return report(await runPreCommit(process.cwd(), config), (s) => this.context.stdout.write(s))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class HooksCommitMsgCommand extends Command {
|
|
42
|
+
static override paths = [['hooks', 'commit-msg']]
|
|
43
|
+
static override usage = Command.Usage({ category: 'Gate', description: 'run the commit-message gate (called by the hook)' })
|
|
44
|
+
|
|
45
|
+
file = Option.String({ required: true })
|
|
46
|
+
|
|
47
|
+
async execute(): Promise<number> {
|
|
48
|
+
const config = await loadRepoConfig(process.cwd())
|
|
49
|
+
return report(await runCommitMsg(process.cwd(), config, this.file), (s) => this.context.stdout.write(s))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class HooksListCommand extends Command {
|
|
54
|
+
static override paths = [['hooks', 'list']]
|
|
55
|
+
static override usage = Command.Usage({
|
|
56
|
+
category: 'Gate',
|
|
57
|
+
description: 'what blocks a commit in this repo, built-ins and repo-declared',
|
|
58
|
+
examples: [['list the gates', 'shipkit hooks list']],
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
async execute(): Promise<number> {
|
|
62
|
+
const config = await loadRepoConfig(process.cwd())
|
|
63
|
+
for (const l of listGates(config, process.cwd())) this.context.stdout.write(`${l}\n`)
|
|
64
|
+
return 0
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/cli/cmd_Mv.ts
CHANGED
|
@@ -1,27 +1,50 @@
|
|
|
1
|
-
// shipkit mv
|
|
2
|
-
//
|
|
1
|
+
// shipkit mv, ONE rename command for the whole repo. An md page routes to
|
|
2
|
+
// mv/mv-md.ts (page + sibling asset dir + check.ts + every tracked ref);
|
|
3
|
+
// anything else routes to rvlib-ts-tools' rename, which rewrites every path
|
|
4
|
+
// reference in the code. Renaming used to mean knowing WHICH tool applied,
|
|
5
|
+
// and knowing that is exactly the kind of doubt the toolkit exists to remove.
|
|
3
6
|
import { Command, Option } from 'clipanion'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
4
8
|
import { movePage } from '../mv/mv-md.ts'
|
|
9
|
+
import { spawnWithTimeout } from '../core/spawn-timeout.ts'
|
|
5
10
|
|
|
6
11
|
export class MvCommand extends Command {
|
|
7
12
|
static override paths = [['mv']]
|
|
8
13
|
|
|
9
14
|
static override usage = Command.Usage({
|
|
10
|
-
category: '
|
|
11
|
-
description: 'rename an md page
|
|
15
|
+
category: 'Refactor',
|
|
16
|
+
description: 'rename anything and sweep every reference: an md page (plus its sibling dir and check.ts) or a code file or folder. Dry run by default. Renames must be CHEAP',
|
|
12
17
|
examples: [
|
|
13
|
-
['dry run
|
|
18
|
+
['dry run, the default, shows what changes', 'shipkit mv docs/old-name.md docs/new-name.md'],
|
|
19
|
+
['apply it', 'shipkit mv docs/old-name.md docs/new-name.md --write'],
|
|
14
20
|
['apply + one conventional commit', 'shipkit mv docs/old-name.md docs/new-name.md --commit'],
|
|
21
|
+
['a code file, every import rewritten', 'shipkit mv src/old-mod.ts src/new-mod.ts --write'],
|
|
22
|
+
['a whole folder', 'shipkit mv src/old-dir src/new-dir --write'],
|
|
15
23
|
],
|
|
16
24
|
})
|
|
17
25
|
|
|
18
26
|
oldPath = Option.String()
|
|
19
27
|
newPath = Option.String()
|
|
20
28
|
write = Option.Boolean('--write', false, { description: 'apply (default is a dry run)' })
|
|
21
|
-
commit = Option.Boolean('--commit', false, { description: 'apply + git commit (pre-commit
|
|
29
|
+
commit = Option.Boolean('--commit', false, { description: 'apply + git commit, md pages only (the pre-commit gate still runs)' })
|
|
22
30
|
|
|
23
31
|
async execute(): Promise<number> {
|
|
32
|
+
const write = this.write || this.commit
|
|
24
33
|
try {
|
|
34
|
+
const isMd = this.oldPath.endsWith('.md') || this.newPath.endsWith('.md')
|
|
35
|
+
if (!isMd) {
|
|
36
|
+
if (this.commit) {
|
|
37
|
+
this.context.stderr.write('FAIL: --commit is md-only for now — run with --write, then commit\n')
|
|
38
|
+
return 1
|
|
39
|
+
}
|
|
40
|
+
let cli: string
|
|
41
|
+
try { cli = fileURLToPath(import.meta.resolve('rvlib-ts-tools/src/cli.ts')) }
|
|
42
|
+
catch { this.context.stderr.write('FAIL: rvlib-ts-tools is unresolvable — reinstall shipkit deps\n'); return 1 }
|
|
43
|
+
const r = await spawnWithTimeout(['bun', cli, 'rename', this.oldPath, this.newPath, ...(write ? ['--apply'] : [])], { cwd: process.cwd(), timeoutMs: 120_000 })
|
|
44
|
+
this.context.stdout.write(r.out)
|
|
45
|
+
if (!write && r.code === 0) this.context.stdout.write('note: dry run — pass --write to apply\n')
|
|
46
|
+
return r.code
|
|
47
|
+
}
|
|
25
48
|
return movePage(process.cwd(), this.oldPath, this.newPath, {
|
|
26
49
|
print: (l) => this.context.stdout.write(`${l}\n`),
|
|
27
50
|
printErr: (l) => this.context.stderr.write(`${l}\n`),
|