@sylad/cadence 0.14.0 → 0.16.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +100 -5
- package/bin/cadence.js +1 -0
- package/dist/audit.js +1 -1
- package/dist/cli.js +132 -14
- package/dist/config.js +27 -0
- package/dist/git.js +38 -0
- package/dist/hook.js +99 -4
- package/dist/lead.js +36 -21
- package/dist/orchestrate/briefs.js +21 -1
- package/dist/orchestrate/command.js +31 -18
- package/dist/orchestrate/cycle.js +76 -44
- package/dist/orchestrate/pool.js +20 -6
- package/dist/orchestrate/state.js +4 -0
- package/dist/plan.js +26 -3
- package/dist/repos.js +72 -0
- package/package.json +1 -1
- package/skills/session-close/SKILL.md +6 -5
- package/skills/session-start/SKILL.md +5 -4
- package/templates/orchestrate/fix-minors.md +1 -0
- package/templates/orchestrate/fix.md +1 -0
- package/templates/orchestrate/implement.md +1 -0
- package/templates/orchestrate/precheck.md +1 -0
- package/templates/orchestrate/review-recheck.md +1 -0
- package/templates/orchestrate/review-small.md +1 -0
- package/templates/orchestrate/review.md +1 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "cadence",
|
|
10
10
|
"description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.16.0",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Sylvain Ladoire"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cadence",
|
|
3
3
|
"description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"author": { "name": "Sylvain Ladoire" },
|
|
6
6
|
"homepage": "https://github.com/Sylad/cadence",
|
|
7
7
|
"repository": "https://github.com/Sylad/cadence",
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Code prompt that shows the session's context, quota, cost and the orchestrate wa
|
|
|
24
24
|
|
|
25
25
|
## What's new
|
|
26
26
|
|
|
27
|
-
**0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
|
|
27
|
+
**0.16.0**: a lot can declare **neighbouring repositories** (`repos:` key of the lot, `{ path, cite }` for a repository shared between projects): `cadence orchestrate` refuses, locks and guards them, gives them to every session as `--add-dir`, and `raf commits`, `raf show` and the review verdict read the commits that cite the lot there too. **0.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
|
|
28
28
|
stops with the wave) and the lead skill that names the wave and follows its journal; lot ids with `/`
|
|
29
29
|
(sub-tasks of a read-only plan) accepted by `orchestrate`; `cadence-hud`'s first line measured in
|
|
30
30
|
terminal cells and kept to ASCII; `publish.yml` on the v5 actions. **0.12.0**: the `cadence-hud` plugin (a status band above the Claude Code prompt),
|
|
@@ -91,15 +91,38 @@ raf gantt # docs/plan/gantt.html
|
|
|
91
91
|
| `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
|
|
92
92
|
| `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`) |
|
|
93
93
|
| `raf note <id> "text"` | dated note — keep decisions next to the work |
|
|
94
|
-
| `raf show <id> [--notes]` | one lot: status, dates, `after`, public title, dated notes in order, then the counted commits (as `raf commits
|
|
95
|
-
| `raf commits <id>` | the commits counted for a lot (the set the code review gate uses), one `<sha> <subject>` per line, oldest first |
|
|
94
|
+
| `raf show <id> [--notes]` | one lot: status, dates, `after`, public title, dated notes in order, then the counted commits (as `raf commits`, neighbouring repositories included); `--notes` prints the notes alone |
|
|
95
|
+
| `raf commits <id>` | the commits counted for a lot (the set the code review gate uses), one `<sha> <subject>` per line, oldest first; a lot that declares `repos:` ([neighbouring repositories](#a-lot-whose-work-is-in-a-neighbouring-repository)) gets, after them, one `dépôt <path> :` section per neighbour with the commits there that cite the lot and fall within the lot's period (`started` to `finished`) and, when the lot declares a `cite:` for that neighbour, contain that string (`aucun commit du lot`, or `introuvable` for a path that is not there) |
|
|
96
96
|
| `raf now` | what to do next |
|
|
97
97
|
| `raf list [--status s]` | flat list |
|
|
98
98
|
| `raf ignore <sha> \| "exact subject" [--reason text]` | acknowledge a commit without a lot (tooling chore, a plan commit citing an unknown id) without rewriting history: a dated, reasoned line in the plan's `acknowledged:` section; a sha is exact, a subject covers every commit carrying it |
|
|
99
99
|
| `raf check --ignored` | list the acknowledged commits with their date and reason |
|
|
100
100
|
| `raf check [--since date] [--idle 7]` | since the plan's adoption date by default (a visible lot without a public title is only a `⚠` warning: it never changes the exit code): commits without a lot (commits touching only plan files are exempt), unknown ids, `todo` lots that already have commits, idle lots, `done` lots with open sub-tasks, bad or circular dependencies |
|
|
101
101
|
| `raf gantt [-o file]` | standalone Gantt page |
|
|
102
|
-
| `raf hook install` | add the
|
|
102
|
+
| `raf hook install` | add the post-commit hook (read-only, never blocks) and the pre-commit hook (does nothing unless `hook.autostart` says so, see below) |
|
|
103
|
+
|
|
104
|
+
#### A commit on a lot that is still `todo`
|
|
105
|
+
|
|
106
|
+
By default the post-commit hook only warns (`L1 est encore todo — raf start L1`). The `hook.autostart`
|
|
107
|
+
key of `cadence.yaml` changes what the **pre-commit** hook does when the message cites a `todo` lot
|
|
108
|
+
(a recurring lot, a lot already `doing`, a commit without a lot and a plan-upkeep commit are never concerned):
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
hook:
|
|
112
|
+
autostart: refuse # warn (default) | refuse | start
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- `warn`: today's behaviour, the commit goes through, the post-commit hook warns.
|
|
116
|
+
- `refuse`: the commit fails, loudly, with `raf start <id>` in the message (`--no-verify` still bypasses it).
|
|
117
|
+
- `start`: the hook runs `raf start <id>` and stages the plan, so the plan change is **in the same commit** —
|
|
118
|
+
no amend afterwards, no dirty plan left behind. Only when `docs/plan/raf.yaml` is identical to `HEAD` (no
|
|
119
|
+
uncommitted change to the plan, staged or not): otherwise nothing is started, the hook says why
|
|
120
|
+
(`plan modifié non commité : démarrage automatique sauté, raf start <id> à la main`) and behaves like `warn`.
|
|
121
|
+
|
|
122
|
+
The pre-commit hook reads the message from the command line of the `git commit` that runs it (`-m`, `-am`,
|
|
123
|
+
`--message`, `-F`; Linux, through `/proc`) or from `CADENCE_COMMIT_MESSAGE`. When the message is not known yet
|
|
124
|
+
(editor, `--amend`) it does nothing and the post-commit warning stays. Run `raf hook install` again to add the
|
|
125
|
+
pre-commit block to an existing repository. A read-only plan is never started by the hook. Only a refusal (exit code 3 of `raf hook pre-commit`) stops a commit: an older or broken `raf` lets it through.
|
|
103
126
|
|
|
104
127
|
Global options: `--file path` or `RAF_FILE`; `RAF_TODAY=YYYY-MM-DD` to freeze
|
|
105
128
|
the date. `cadence raf …` is the same as `raf …`.
|
|
@@ -172,6 +195,7 @@ plan:
|
|
|
172
195
|
started: demarre_le
|
|
173
196
|
finished: [livre_le, ferme_le]
|
|
174
197
|
notes: note
|
|
198
|
+
repos: depots # the lot's neighbouring repositories (a list of paths and/or { path, cite }, or one of them)
|
|
175
199
|
parent: parent
|
|
176
200
|
statuses: # raf status: their states
|
|
177
201
|
todo: [prevu, specifie]
|
|
@@ -182,7 +206,7 @@ plan:
|
|
|
182
206
|
```
|
|
183
207
|
|
|
184
208
|
- Fields: `id`, `title`, `status`, `estimate`, `quickwin`, `visible`, `public`, `every`, `last`, `after`, `created`, `started`,
|
|
185
|
-
`finished`, `notes`, `parent`; one left out is read under its own name. A timestamp counts for
|
|
209
|
+
`finished`, `notes`, `repos`, `parent`; one left out is read under its own name. A timestamp counts for
|
|
186
210
|
its day; a note written as plain text is one note.
|
|
187
211
|
- `parent`: an entry `B33/t1-fusion` whose parent is `B33` becomes the sub-task `t1-fusion` of `B33`.
|
|
188
212
|
- Ids need no prefix: a commit belongs to a lot when its message cites one of the plan's ids as a
|
|
@@ -311,6 +335,12 @@ written by hand without a `commit` field is not checked for staleness. An empty
|
|
|
311
335
|
verdict is refused, and one left empty or blank by hand in the YAML counts as no
|
|
312
336
|
review. Plans without `reviewSince` are not affected.
|
|
313
337
|
|
|
338
|
+
A lot that works in neighbouring repositories (`repos:`, see [orchestrate](#a-lot-whose-work-is-in-a-neighbouring-repository))
|
|
339
|
+
has its verdict tied to each of them: `raf review` also stores, under `repos`, the sha of the latest counted commit of the
|
|
340
|
+
lot in each neighbour (the one `raf commits` lists last: it cites the lot, falls within the lot's period and contains the `cite:` string if any) (`review: { date, verdict, commit, repos: { ../aetherwx-gitops: <sha> } }`, `null` when a
|
|
341
|
+
neighbour has none), and refuses to record when a listed repository cannot be read. `raf check` does not audit the
|
|
342
|
+
neighbours: their staleness is not checked, only the project's own commits are.
|
|
343
|
+
|
|
314
344
|
### QA review
|
|
315
345
|
|
|
316
346
|
No gate and no command here: the QA review comes **after** a delivery, and
|
|
@@ -387,10 +417,21 @@ cadence session start --since "3 days ago" --idle 2
|
|
|
387
417
|
cadence session close # today's commits by lot, commits without a lot, lots in progress
|
|
388
418
|
# with no commit today, drift, uncommitted / unpushed work
|
|
389
419
|
# exit 1 while something is still open
|
|
420
|
+
cadence session start --all # from a folder that is not a repository: one "## project" section per
|
|
421
|
+
cadence session close --all --depth 2 # sub-folder holding a plan (--depth n: n levels down, 1 by default);
|
|
422
|
+
# exit = the worst code of the projects (2 error, 1 open, 0 closed)
|
|
390
423
|
cadence session next "finish L3" "review L4" # shown by the next session start; replaces the previous notes
|
|
391
424
|
cadence session next --clear # erase those notes, on purpose
|
|
392
425
|
```
|
|
393
426
|
|
|
427
|
+
`--all` runs the same command inside each project found under the current folder —
|
|
428
|
+
`docs/plan/raf.yaml`, or a `cadence.yaml` with `plan:` — and never enters a project
|
|
429
|
+
to look for another. `--since` and `--idle` go to every project; an error in one
|
|
430
|
+
project is printed in its section and the others still run. It replaces the shell
|
|
431
|
+
loop over the sub-folders. A project that is not the root of its own git repository gets a `✗` line
|
|
432
|
+
(and exit 2) instead of the parent repository's plan. `--all` and `--depth` are refused outside `session`,
|
|
433
|
+
and `--all` refuses `--file`, `--config` and `RAF_FILE`: each project reads its own plan. For a one-line-per-project overview, `cadence lead tour`.
|
|
434
|
+
|
|
394
435
|
`cadence session next` without a line refuses (exit 2) and leaves the notes of the
|
|
395
436
|
last close as they are — it used to erase them silently; erasing is `--clear`.
|
|
396
437
|
|
|
@@ -784,6 +825,60 @@ The sessions of the wave also get `.cadence/runs/<wave>/tool/bin` (the `raf` and
|
|
|
784
825
|
`PATH`**, ahead of the per-project Node below and of the usual `PATH`: the `raf` a reviewer runs (`raf commits`…) is the
|
|
785
826
|
snapshot's, not the installed one. A wave without a snapshot leaves the `PATH` as it is.
|
|
786
827
|
|
|
828
|
+
### A lot whose work is in a neighbouring repository
|
|
829
|
+
|
|
830
|
+
Some lots are done in another repository that is not a cadence project (B64 of `maritime-atlas` lives in
|
|
831
|
+
`aetherwx-gitops`, a Helm chart repository). The lot declares it with the **lot key `repos:`** — a list of paths relative to
|
|
832
|
+
the project's repository, one path being enough as a string. There is no project-wide key: a neighbour belongs to a lot.
|
|
833
|
+
|
|
834
|
+
```yaml
|
|
835
|
+
lots:
|
|
836
|
+
- id: B64
|
|
837
|
+
title: …
|
|
838
|
+
repos: [../aetherwx-gitops]
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
**Which commits count in a neighbour.** Nothing is inferred from the neighbour or from the project's name. A commit counts
|
|
842
|
+
when it cites the lot's id (as in the project: `feat(B64): …`, `maritime: … (B64)`) **and** is dated within the lot's period,
|
|
843
|
+
`started` to `finished` (an open end is open). That is all, whatever the prefix or scope of the subject says and whatever
|
|
844
|
+
the commit mentions. When a neighbour is shared between projects whose lots share an id prefix (`developpeur-gitops` holds
|
|
845
|
+
the charts of `finance-tracker`, `warhammer40k` and `ol-companion`, all `L…`), the lot says so with an **explicit key**: an
|
|
846
|
+
entry of `repos:` can be an object, and the list can mix both forms.
|
|
847
|
+
|
|
848
|
+
```yaml
|
|
849
|
+
repos:
|
|
850
|
+
- ../aetherwx-gitops # id + period
|
|
851
|
+
- { path: ../developpeur-gitops, cite: ol-companion } # id + period + this string
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
With `cite`, a commit must also contain that string (a substring, case-insensitive, searched in the subject and the body);
|
|
855
|
+
a commit of another app that cites the same id is dropped from `raf commits`, from the briefs, from the sha a review
|
|
856
|
+
records and from the orchestrator's « already has work » test. Without `cite`, in a shared neighbour, the commits of the
|
|
857
|
+
other apps that cite the same id **do** count: that is the default, so declare `cite` for such a neighbour. `cite` applies
|
|
858
|
+
to the neighbour it is written on, not to the project's own repository. The commits of the lot in the neighbour must carry the
|
|
859
|
+
string too (the briefs say so); one that does not is not counted.
|
|
860
|
+
|
|
861
|
+
A read-only plan declares it in `cadence.yaml` with `plan.fields.repos: <the file's key>` (a left-out field is read under
|
|
862
|
+
the name `repos`). The effect, for `cadence orchestrate`:
|
|
863
|
+
|
|
864
|
+
- **Preconditions** (refused before acting, exit code 2, like the project's own repository): every listed path exists and
|
|
865
|
+
is inside a git repository, has no tracked file modified, has no `pre-push` hook of its own, and is not held by another
|
|
866
|
+
live orchestration. `--dry-run` applies the same refusals and prints `dépôts voisins : <paths>`. The project's own
|
|
867
|
+
repository and a duplicate listed twice are counted once.
|
|
868
|
+
- **Lock and guard**: the neighbour gets the repository lock (`orchestrate.lock`, so `cadence deliver` refuses it too, and no
|
|
869
|
+
other wave can hold it), the temporary `pre-push` hook, and the same after-session checks (push, tampered hook, a review
|
|
870
|
+
that changed it). Two lots that share a repository, as project or as neighbour, run one after the other, never two
|
|
871
|
+
sessions in one repository; `--status` lists the neighbours among the repositories a wave holds.
|
|
872
|
+
- **Sessions**: every session gets each neighbour as `--add-dir`, and the briefs name them: the implementation and the
|
|
873
|
+
corrections must commit there too, with a message that cites the lot (`feat(B64): …`), and never push; the reviewers read
|
|
874
|
+
the commits that cite the lot in the project **and** in each neighbour (`raf commits <lot>` lists them all; in a neighbour only the commits that cite the lot and are dated within the lot's period count, and also contain the `cite:` string when the lot declares one for it — a `fix(B64): …` outside the period, or one without the `cite:` string, is not listed.) A lot whose
|
|
875
|
+
whole work is in the neighbour is not « without commit »; a commit there that does not cite the lot is a warning; a
|
|
876
|
+
neighbour left with a tracked file modified hands the lot back.
|
|
877
|
+
- **Verdict**: `raf review` as recorded by the orchestrator stores the sha read in the project and in each neighbour
|
|
878
|
+
(`review.repos`), and the verdict text ends with `dépôts relus : <path>@<sha>`. A commit made in a neighbour after the
|
|
879
|
+
review hands the lot back, like one in the project.
|
|
880
|
+
- **Out of scope**: `raf check` does not audit the neighbours, and their delivery (push, CI, deploy) stays with the lead.
|
|
881
|
+
|
|
787
882
|
**What stays with you**: choosing the lots, the questions raised (`--resume --answer`), re-verifying
|
|
788
883
|
after the wave (`git log`, tests, `raf check`), `raf done`, **`raf ux`** (the orchestrator reports the UX
|
|
789
884
|
verdict and screenshots, it does not record it), the push and the deliveries, one project at a time.
|
package/bin/cadence.js
CHANGED
|
@@ -26,6 +26,7 @@ if (tool === 'raf') {
|
|
|
26
26
|
cadence session start [--since "24 hours ago"] [--idle 2]
|
|
27
27
|
faits de reprise : notes de la veille, en cours, fait depuis, écarts, propositions
|
|
28
28
|
cadence session close [--since …] faits de clôture ; code 1 tant que ce n'est pas fermé
|
|
29
|
+
cadence session start|close --all [--depth 1] depuis un dossier non dépôt : une section par projet des sous-dossiers
|
|
29
30
|
cadence session next "ligne" … notes pour la prochaine session (remplacent les précédentes)
|
|
30
31
|
cadence session next --clear efface ces notes ; sans ligne ni --clear, la commande refuse
|
|
31
32
|
cadence deliver [--dry-run] [--sha rév] [--config cadence.yaml] [-- arguments du script du projet]
|
package/dist/audit.js
CHANGED
|
@@ -13,7 +13,7 @@ function configRel(plan, root) {
|
|
|
13
13
|
return plan.configFile ? relative(root, plan.configFile) : 'cadence.yaml';
|
|
14
14
|
}
|
|
15
15
|
/** Le plan, sa page Gantt, la configuration lue (cadence.yaml) et les fichiers tenus avec lui (plan.files). */
|
|
16
|
-
function ownFiles(plan, root) {
|
|
16
|
+
export function ownFiles(plan, root) {
|
|
17
17
|
return new Set([relative(root, plan.path), relative(root, join(dirname(plan.path), 'gantt.html')), configRel(plan, root), ...plan.maintained]);
|
|
18
18
|
}
|
|
19
19
|
function withoutPlanKey(text) {
|
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import { readNewsConfig, readPlanConfig, readSessionConfig } from './config.js';
|
|
5
|
-
import {
|
|
4
|
+
import { readHookConfig, readNewsConfig, readPlanConfig, readSessionConfig } from './config.js';
|
|
5
|
+
import { repoSections, repoShas, resolveLotRepos } from './repos.js';
|
|
6
|
+
import { audit, exemptPlanOnly, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork } from './audit.js';
|
|
6
7
|
import { short } from './check.js';
|
|
7
8
|
import { isDay, toDay } from './dates.js';
|
|
8
9
|
import { deliver, parseDeliverConfig, realDeps } from './deliver.js';
|
|
9
10
|
import { defaultTarget, effectLines, realCheckDeps, verifyCommand } from './verify.js';
|
|
10
11
|
import { ganttData, renderGantt } from './gantt.js';
|
|
11
|
-
import { gitRoot, readCommits, resolveCommit } from './git.js';
|
|
12
|
-
import { installHook } from './hook.js';
|
|
12
|
+
import { gitRoot, readCommits, matchesHead, resolveCommit, stage, stagedFiles, syncIndexWithHead } from './git.js';
|
|
13
|
+
import { installHook, pendingCommitMessage, REFUSED } from './hook.js';
|
|
13
14
|
import { citedRefs, linkCommits } from './link.js';
|
|
14
15
|
import { buildNews, loadEntries, newEntry, newsData, newsIssues, publicTitleTooLong, stampEntries } from './news.js';
|
|
15
16
|
import { Plan, RafError, STATUSES } from './plan.js';
|
|
16
17
|
import { dueLine, isRecurring, recurringByDue } from './recurring.js';
|
|
17
18
|
import { schedule } from './schedule.js';
|
|
18
19
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
19
|
-
import { leadTour, tourLine } from './lead.js';
|
|
20
|
+
import { findProjects, leadTour, tourLine } from './lead.js';
|
|
20
21
|
import { orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
21
22
|
import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
|
|
22
23
|
import { sessionClose, sessionStart } from './session.js';
|
|
@@ -40,7 +41,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
40
41
|
raf check [--since date] [--idle 7] (défaut : date « since » du plan) code 1 s'il y a des écarts
|
|
41
42
|
raf ignore <sha> | "<sujet exact>" [--reason texte] acquitte un commit sans lot (ou citant un id inconnu) sans réécrire l'historique ; raf check --ignored les liste
|
|
42
43
|
raf gantt [-o docs/plan/gantt.html]
|
|
43
|
-
raf hook install
|
|
44
|
+
raf hook install (pre-commit et post-commit ; cadence.yaml hook.autostart: warn|refuse|start)
|
|
44
45
|
cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--dry-run] [--wave nom] une session claude neuve par étape ; --status [vague] [--watch [--interval s]] ; --resume [vague] [--budget 1M] [--answer projet:lot "réponse"]
|
|
45
46
|
cadence lead tour [dossier] [--idle 3] [--json] le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan (en cours, dérive, notes de clôture, prochain lot prêt, dépôt)
|
|
46
47
|
cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
|
|
@@ -114,6 +115,8 @@ function dispatch(argv, io) {
|
|
|
114
115
|
status: { type: 'string' },
|
|
115
116
|
since: { type: 'string' },
|
|
116
117
|
idle: { type: 'string' },
|
|
118
|
+
all: { type: 'boolean' },
|
|
119
|
+
depth: { type: 'string' },
|
|
117
120
|
output: { type: 'string', short: 'o' },
|
|
118
121
|
config: { type: 'string' },
|
|
119
122
|
'dry-run': { type: 'boolean' },
|
|
@@ -131,6 +134,8 @@ function dispatch(argv, io) {
|
|
|
131
134
|
io.out(HELP);
|
|
132
135
|
return 0;
|
|
133
136
|
}
|
|
137
|
+
if (command !== 'session' && (values.all || values.depth !== undefined))
|
|
138
|
+
throw new RafError(`${values.all ? '--all' : '--depth'} ne va qu'avec session start ou session close`);
|
|
134
139
|
const today = io.env.RAF_TODAY ?? toDay(io.now());
|
|
135
140
|
if (!isDay(today))
|
|
136
141
|
throw new RafError(`RAF_TODAY invalide : ${today} (attendu AAAA-MM-JJ)`);
|
|
@@ -155,6 +160,9 @@ function dispatch(argv, io) {
|
|
|
155
160
|
io.out(tourLine(r));
|
|
156
161
|
return 0;
|
|
157
162
|
}
|
|
163
|
+
// --all balaie les sous-dossiers : il ne lit ni le plan ni le cadence.yaml du dossier courant.
|
|
164
|
+
if (command === 'session' && (values.all || values.depth !== undefined))
|
|
165
|
+
return sessionAll(rest, values, io);
|
|
158
166
|
// cadence.yaml peut dire où est le plan et, s'il est tenu par un autre outil, comment le lire.
|
|
159
167
|
const configPath = resolve(io.cwd, values.config ?? join(root, 'cadence.yaml'));
|
|
160
168
|
// Installer les skills ou le hook ne lit pas le plan : un cadence.yaml fautif ne doit pas l'empêcher.
|
|
@@ -283,6 +291,8 @@ function dispatch(argv, io) {
|
|
|
283
291
|
// Une lecture : le même ensemble que « raf done » et « raf check », plan en lecture seule compris.
|
|
284
292
|
for (const c of lotWork(plan, root, id).reverse())
|
|
285
293
|
io.out(short(c));
|
|
294
|
+
for (const line of repoSections(plan, root, plan.lot(id)))
|
|
295
|
+
io.out(line);
|
|
286
296
|
return 0;
|
|
287
297
|
}
|
|
288
298
|
case 'show': {
|
|
@@ -313,6 +323,8 @@ function dispatch(argv, io) {
|
|
|
313
323
|
io.out(`commits (${commits.length}) :`);
|
|
314
324
|
for (const c of commits)
|
|
315
325
|
io.out(` ${short(c)}`);
|
|
326
|
+
for (const line of repoSections(plan, root, lot, ' '))
|
|
327
|
+
io.out(line);
|
|
316
328
|
return 0;
|
|
317
329
|
}
|
|
318
330
|
case 'list': {
|
|
@@ -383,8 +395,10 @@ function dispatch(argv, io) {
|
|
|
383
395
|
return 0;
|
|
384
396
|
}
|
|
385
397
|
if (rest[0] === 'post-commit')
|
|
386
|
-
return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io);
|
|
387
|
-
|
|
398
|
+
return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io, readHookConfig(configPath).autostart);
|
|
399
|
+
if (rest[0] === 'pre-commit')
|
|
400
|
+
return preCommit(existsSync(planPath) ? loadPlan : null, readHookConfig(configPath).autostart, root, today, io);
|
|
401
|
+
throw new RafError('usage : raf hook install|pre-commit|post-commit');
|
|
388
402
|
}
|
|
389
403
|
case 'session': {
|
|
390
404
|
if (!gitRoot(io.cwd))
|
|
@@ -476,8 +490,13 @@ function gate(kind, rest, loadPlan, root, today, io) {
|
|
|
476
490
|
if (kind === 'ux')
|
|
477
491
|
plan.recordUx(rest[0], verdict, today);
|
|
478
492
|
// Le verdict vaut jusqu'au dernier commit compté du lot ; un plan en lecture seule refuse sans lire git.
|
|
479
|
-
else
|
|
480
|
-
|
|
493
|
+
else {
|
|
494
|
+
// Les dépôts voisins du lot : un dépôt qu'on ne peut pas lire ferait enregistrer un verdict sur un sha non lu.
|
|
495
|
+
const neighbours = plan.readonly || rest[0].includes('/') ? undefined : resolveLotRepos(root, plan.lots().find((l) => l.id === rest[0]) ?? {});
|
|
496
|
+
if (neighbours?.problems.length)
|
|
497
|
+
throw new RafError(`${neighbours.problems.join(' ; ')} — la revue de code relit aussi les dépôts du lot`);
|
|
498
|
+
plan.recordReview(rest[0], verdict, today, plan.readonly ? null : (lotWork(plan, root, rest[0])[0]?.sha ?? null), neighbours?.repos.length ? repoShas(plan, neighbours.repos, rest[0]) : undefined);
|
|
499
|
+
}
|
|
481
500
|
plan.save();
|
|
482
501
|
return 0;
|
|
483
502
|
}
|
|
@@ -535,11 +554,66 @@ function now(plan, root, newsDir, today, io) {
|
|
|
535
554
|
io.out(`\n${issues.length} écart(s) entre le plan et l'historique — raf check`);
|
|
536
555
|
return 0;
|
|
537
556
|
}
|
|
538
|
-
|
|
557
|
+
/**
|
|
558
|
+
* Pre-commit : un commit qui cite un lot encore todo. `warn` (défaut) : rien ici, le post-commit avertit ; `refuse` :
|
|
559
|
+
* échoue (REFUSED) avec « raf start <id> » ; `start` : démarre le lot et met le plan dans l'index, donc dans CE commit,
|
|
560
|
+
* mais seulement si le plan est identique à HEAD : sinon rien n'est démarré (message, puis avertissement du post-commit).
|
|
561
|
+
* Ne rend REFUSED que pour un refus : toute autre panne laisse le commit passer. Sans message lisible (éditeur), rien.
|
|
562
|
+
*/
|
|
563
|
+
function preCommit(load, mode, root, today, io) {
|
|
564
|
+
if (!load || !mode || mode === 'warn')
|
|
565
|
+
return 0;
|
|
566
|
+
try {
|
|
567
|
+
const message = pendingCommitMessage(io.env);
|
|
568
|
+
if (message === null)
|
|
569
|
+
return 0;
|
|
570
|
+
const plan = load();
|
|
571
|
+
const [subject = '', ...body] = message.split('\n');
|
|
572
|
+
const refs = citedRefs({ sha: '', day: today, subject, body: body.join('\n').trim() }, plan.refs);
|
|
573
|
+
const staged = stagedFiles(root);
|
|
574
|
+
const own = ownFiles(plan, root);
|
|
575
|
+
// Entretien du plan : tous les fichiers du commit sont ceux du plan (la configuration seule ne compte pas).
|
|
576
|
+
if (staged.length > 0 && staged.every((f) => own.has(f) && f !== relative(root, plan.configFile ?? join(root, 'cadence.yaml'))))
|
|
577
|
+
return 0;
|
|
578
|
+
const todo = [...new Set(refs.map((r) => r.lot))].filter((id) => {
|
|
579
|
+
const lot = plan.lots().find((l) => l.id === id);
|
|
580
|
+
return lot?.status === 'todo' && !isRecurring(lot);
|
|
581
|
+
});
|
|
582
|
+
if (todo.length === 0)
|
|
583
|
+
return 0;
|
|
584
|
+
if (mode === 'refuse') {
|
|
585
|
+
for (const id of todo)
|
|
586
|
+
io.err(`raf: ${id} est encore todo — commit refusé (hook.autostart: refuse)${plan.readonly ? '' : ` : raf start ${id}`} (--no-verify pour passer outre)`);
|
|
587
|
+
return REFUSED;
|
|
588
|
+
}
|
|
589
|
+
if (plan.readonly)
|
|
590
|
+
return 0;
|
|
591
|
+
const file = relative(root, plan.path);
|
|
592
|
+
// Un plan déjà modifié (stagé ou non) : le restager emporterait ces modifications dans ce commit, ou, sur un
|
|
593
|
+
// commit partiel, les perdrait. On ne démarre rien ; le post-commit avertit comme en mode warn.
|
|
594
|
+
if (!matchesHead(root, file)) {
|
|
595
|
+
io.err(`raf: plan modifié non commité : démarrage automatique sauté (hook.autostart: start), raf start ${todo.join(' ')} à la main`);
|
|
596
|
+
return 0;
|
|
597
|
+
}
|
|
598
|
+
for (const id of todo) {
|
|
599
|
+
plan.setStatus(id, 'doing', today, {});
|
|
600
|
+
io.err(`raf: ${id} était todo — raf start ${id} fait par le hook, plan inclus dans ce commit`);
|
|
601
|
+
}
|
|
602
|
+
plan.save();
|
|
603
|
+
stage(root, file);
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// Un hook ne doit jamais gêner un commit, hors refus.
|
|
607
|
+
}
|
|
608
|
+
return 0;
|
|
609
|
+
}
|
|
610
|
+
function postCommit(load, newsDir, root, today, io, mode) {
|
|
539
611
|
if (!load)
|
|
540
612
|
return 0;
|
|
541
613
|
try {
|
|
542
614
|
const plan = load();
|
|
615
|
+
if (mode === 'start' && !plan.readonly)
|
|
616
|
+
syncIndexWithHead(root, relative(root, plan.path));
|
|
543
617
|
const head = readCommits(root, { range: '-1' })[0];
|
|
544
618
|
if (!head)
|
|
545
619
|
return 0;
|
|
@@ -646,6 +720,50 @@ function morningEffects(configPath, root) {
|
|
|
646
720
|
return [`✗ ${e.message}`];
|
|
647
721
|
}
|
|
648
722
|
}
|
|
723
|
+
/** `session start|close --all [--depth n]` : la commande jouée dans chaque projet sous le dossier courant, une section par projet. */
|
|
724
|
+
async function sessionAll(rest, values, io) {
|
|
725
|
+
const sub = rest[0];
|
|
726
|
+
if (!values.all)
|
|
727
|
+
throw new RafError('--depth demande --all');
|
|
728
|
+
// Chaque projet lit son propre plan : un plan ou une config imposés se retrouveraient dans toutes les sections.
|
|
729
|
+
const imposed = values.file !== undefined ? '--file' : values.config !== undefined ? '--config' : io.env.RAF_FILE !== undefined ? 'RAF_FILE' : null;
|
|
730
|
+
if (imposed)
|
|
731
|
+
throw new RafError(`${imposed} ne va pas avec --all : chaque projet lit son propre plan (lancer la commande dans le projet visé)`);
|
|
732
|
+
if (sub !== 'start' && sub !== 'close')
|
|
733
|
+
throw new RafError('--all ne va qu\'avec session start ou session close');
|
|
734
|
+
const depth = values.depth === undefined ? 1 : Number(values.depth);
|
|
735
|
+
if (!Number.isInteger(depth) || depth < 1)
|
|
736
|
+
throw new RafError(`--depth invalide : ${values.depth} (un entier ≥ 1)`);
|
|
737
|
+
if (values.idle !== undefined && !(Number.isInteger(Number(values.idle)) && Number(values.idle) >= 0))
|
|
738
|
+
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
739
|
+
const projects = findProjects(io.cwd, depth);
|
|
740
|
+
if (projects.length === 0) {
|
|
741
|
+
io.out(`aucun projet sous ${io.cwd} (docs/plan/raf.yaml ou cadence.yaml avec plan:)`);
|
|
742
|
+
return 0;
|
|
743
|
+
}
|
|
744
|
+
const argv = ['session', sub, ...(values.since === undefined ? [] : [`--since=${values.since}`]), ...(values.idle === undefined ? [] : ['--idle', values.idle])];
|
|
745
|
+
let worst = 0;
|
|
746
|
+
for (const dir of projects) {
|
|
747
|
+
io.out(`${dir === projects[0] ? '' : '\n'}## ${relative(io.cwd, dir)}`);
|
|
748
|
+
let code;
|
|
749
|
+
const top = gitRoot(dir);
|
|
750
|
+
if (top && realpathSync(top) !== realpathSync(dir)) {
|
|
751
|
+
// run() lirait le plan du dépôt parent : mieux vaut le dire que montrer le plan d'un autre projet.
|
|
752
|
+
io.out(`✗ ${relative(io.cwd, dir)} n'a pas son propre dépôt git (dépôt parent : ${top}) — lancer cadence dans ce dossier`);
|
|
753
|
+
worst = 2;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
code = await run(argv, { ...io, cwd: dir, err: (l) => io.out(`✗ ${l}`) });
|
|
758
|
+
}
|
|
759
|
+
catch (e) {
|
|
760
|
+
io.out(`✗ ${e instanceof Error ? e.message : String(e)}`);
|
|
761
|
+
code = 2;
|
|
762
|
+
}
|
|
763
|
+
worst = Math.max(worst, code);
|
|
764
|
+
}
|
|
765
|
+
return worst;
|
|
766
|
+
}
|
|
649
767
|
function session([sub, ...args], ctx, values) {
|
|
650
768
|
switch (sub) {
|
|
651
769
|
case 'start': {
|
|
@@ -676,6 +794,6 @@ function session([sub, ...args], ctx, values) {
|
|
|
676
794
|
return 0;
|
|
677
795
|
}
|
|
678
796
|
default:
|
|
679
|
-
throw new RafError('usage : cadence session start [--since …] [--idle 2] | close [--since …] | next "ligne" … | next --clear');
|
|
797
|
+
throw new RafError('usage : cadence session start [--since …] [--idle 2] | close [--since …] | start|close --all [--depth n] | next "ligne" … | next --clear');
|
|
680
798
|
}
|
|
681
799
|
}
|
package/dist/config.js
CHANGED
|
@@ -141,6 +141,33 @@ export function readNewsConfig(file) {
|
|
|
141
141
|
throw new RafError(`${file} : news.publicTitleMax : nombre entier de caractères (≥ 1) attendu`);
|
|
142
142
|
return { publicTitleMax: max };
|
|
143
143
|
}
|
|
144
|
+
const AUTOSTART = ['warn', 'refuse', 'start'];
|
|
145
|
+
/** Clé `hook:` de cadence.yaml. */
|
|
146
|
+
export function readHookConfig(file) {
|
|
147
|
+
if (!existsSync(file))
|
|
148
|
+
return {};
|
|
149
|
+
let raw;
|
|
150
|
+
try {
|
|
151
|
+
raw = parse(readFileSync(file, 'utf8'));
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
155
|
+
}
|
|
156
|
+
const h = raw?.hook;
|
|
157
|
+
if (h == null)
|
|
158
|
+
return {};
|
|
159
|
+
if (!isObject(h))
|
|
160
|
+
throw new RafError(`${file} : hook doit être un objet { autostart }`);
|
|
161
|
+
for (const k of Object.keys(h))
|
|
162
|
+
if (k !== 'autostart')
|
|
163
|
+
throw new RafError(`${file} : hook.${k} inconnu (attendu : autostart)`);
|
|
164
|
+
const a = h.autostart;
|
|
165
|
+
if (a == null)
|
|
166
|
+
return {};
|
|
167
|
+
if (typeof a !== 'string' || !AUTOSTART.includes(a))
|
|
168
|
+
throw new RafError(`${file} : hook.autostart : ${AUTOSTART.join(', ')} attendu`);
|
|
169
|
+
return { autostart: a };
|
|
170
|
+
}
|
|
144
171
|
/**
|
|
145
172
|
* Clé `session:` de cadence.yaml : les faits propres au projet (son script de reprise, de clôture),
|
|
146
173
|
* joués par cadence au lieu d'être remplacés.
|
package/dist/git.js
CHANGED
|
@@ -45,6 +45,44 @@ export function hooksDir(cwd) {
|
|
|
45
45
|
const dir = resolve(cwd, git(cwd, ['rev-parse', '--git-path', 'hooks']).trim());
|
|
46
46
|
return basename(dir) === '_' && basename(dirname(dir)) === '.husky' ? dirname(dir) : dir;
|
|
47
47
|
}
|
|
48
|
+
/** Fichiers de l'index (ce que le commit en préparation contient), relatifs à la racine du dépôt. */
|
|
49
|
+
export function stagedFiles(cwd) {
|
|
50
|
+
return git(cwd, ['-c', 'core.quotepath=off', 'diff', '--cached', '--name-only', '-z']).split('\0').filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
/** Ajoute un fichier à l'index. */
|
|
53
|
+
export function stage(cwd, file) {
|
|
54
|
+
git(cwd, ['add', '--', file]);
|
|
55
|
+
}
|
|
56
|
+
/** Le fichier de travail est-il identique à sa version de HEAD (ni modification stagée, ni non stagée) ? Faux s'il n'est pas dans HEAD. */
|
|
57
|
+
export function matchesHead(cwd, file) {
|
|
58
|
+
try {
|
|
59
|
+
git(cwd, ['cat-file', '-e', `HEAD:${file}`]);
|
|
60
|
+
git(cwd, ['diff', '--quiet', 'HEAD', '--', file]);
|
|
61
|
+
git(cwd, ['diff', '--quiet', '--cached', 'HEAD', '--', file]);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Aligne l'index sur HEAD pour `file` quand le fichier de travail est déjà celui de HEAD : un commit partiel
|
|
70
|
+
* (`git commit <chemins>`) ne recopie dans l'index réel que ces chemins, et y laisse l'ancienne version du fichier
|
|
71
|
+
* stagé par un pre-commit. Sans effet si le fichier a d'autres modifications.
|
|
72
|
+
*/
|
|
73
|
+
export function syncIndexWithHead(cwd, file) {
|
|
74
|
+
const same = (args) => {
|
|
75
|
+
try {
|
|
76
|
+
git(cwd, ['diff', '--quiet', ...args, '--', file]);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
if (same(['HEAD']) && !same(['--cached']))
|
|
84
|
+
stage(cwd, file);
|
|
85
|
+
}
|
|
48
86
|
/** Fichiers modifiés par un commit, relatifs à la racine du dépôt (noms accentués ou à espaces compris). */
|
|
49
87
|
export function changedFiles(cwd, sha) {
|
|
50
88
|
// -z : noms séparés par NUL, jamais échappés ; quotepath=off en plus pour les sorties qui citeraient quand même.
|
package/dist/hook.js
CHANGED
|
@@ -4,17 +4,29 @@ import { hooksDir } from './git.js';
|
|
|
4
4
|
import { RafError } from './plan.js';
|
|
5
5
|
const BEGIN = '# >>> raf (cadence)';
|
|
6
6
|
const END = '# <<< raf (cadence)';
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/** Code de sortie de `raf hook pre-commit` qui refuse le commit. */
|
|
8
|
+
export const REFUSED = 3;
|
|
9
|
+
const BLOCKS = {
|
|
10
|
+
'post-commit': `${BEGIN}\ncommand -v raf >/dev/null 2>&1 && raf hook post-commit || true\n${END}\n`,
|
|
11
|
+
// Seul un refus (hook.autostart: refuse, code 3) arrête le commit : un raf plus ancien ou en panne le laisse passer.
|
|
12
|
+
'pre-commit': `${BEGIN}\nif command -v raf >/dev/null 2>&1; then raf hook pre-commit; [ $? -ne ${REFUSED} ] || exit 1; fi\n${END}\n`,
|
|
13
|
+
};
|
|
14
|
+
/** Adds the raf blocks to post-commit and pre-commit without touching anything else already there. */
|
|
9
15
|
export function installHook(cwd) {
|
|
16
|
+
const post = installBlock(cwd, 'post-commit');
|
|
17
|
+
const pre = installBlock(cwd, 'pre-commit');
|
|
18
|
+
return { path: post.path, changed: post.changed || pre.changed };
|
|
19
|
+
}
|
|
20
|
+
function installBlock(cwd, name) {
|
|
21
|
+
const BLOCK = BLOCKS[name];
|
|
10
22
|
const dir = hooksDir(cwd);
|
|
11
23
|
mkdirSync(dir, { recursive: true });
|
|
12
|
-
const path = join(dir,
|
|
24
|
+
const path = join(dir, name);
|
|
13
25
|
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
14
26
|
if (current.includes(BEGIN))
|
|
15
27
|
return { path, changed: false };
|
|
16
28
|
if (current !== '' && !/^#!.*\b(sh|bash|zsh|dash)\b/.test(current)) {
|
|
17
|
-
throw new RafError(`${path} n'est pas un script shell : y ajouter à la main « raf hook
|
|
29
|
+
throw new RafError(`${path} n'est pas un script shell : y ajouter à la main « raf hook ${name} »`);
|
|
18
30
|
}
|
|
19
31
|
let next;
|
|
20
32
|
if (current === '') {
|
|
@@ -34,3 +46,86 @@ export function installHook(cwd) {
|
|
|
34
46
|
chmodSync(path, 0o755);
|
|
35
47
|
return { path, changed: true };
|
|
36
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Message d'un `git commit` d'après ses arguments (-m, -am, --message, plusieurs -m, -F) ; null sans message
|
|
51
|
+
* en ligne de commande (éditeur, --amend, -C…) ou si ce n'est pas un commit.
|
|
52
|
+
*/
|
|
53
|
+
export function messageFromArgv(argv) {
|
|
54
|
+
const at = argv.indexOf('commit');
|
|
55
|
+
if (at < 0)
|
|
56
|
+
return null;
|
|
57
|
+
const parts = [];
|
|
58
|
+
let file = null;
|
|
59
|
+
for (let i = at + 1; i < argv.length; i++) {
|
|
60
|
+
const a = argv[i];
|
|
61
|
+
if (a === '--')
|
|
62
|
+
break;
|
|
63
|
+
if (a.startsWith('--message='))
|
|
64
|
+
parts.push(a.slice('--message='.length));
|
|
65
|
+
else if (a === '--message')
|
|
66
|
+
parts.push(argv[++i] ?? '');
|
|
67
|
+
else if (a.startsWith('--file='))
|
|
68
|
+
file = a.slice('--file='.length);
|
|
69
|
+
else if (a === '--file')
|
|
70
|
+
file = argv[++i] ?? null;
|
|
71
|
+
else if (/^-[A-Za-z]+$/.test(a) && /[mF]$/.test(a)) {
|
|
72
|
+
// « -am », « -qm » : la dernière lettre prend l'argument suivant.
|
|
73
|
+
const value = argv[++i] ?? '';
|
|
74
|
+
if (a.endsWith('m'))
|
|
75
|
+
parts.push(value);
|
|
76
|
+
else
|
|
77
|
+
file = value;
|
|
78
|
+
}
|
|
79
|
+
else if (/^-[A-Za-z]*m./.test(a) && !a.startsWith('--'))
|
|
80
|
+
parts.push(a.slice(a.indexOf('m') + 1));
|
|
81
|
+
else if (/^-F./.test(a))
|
|
82
|
+
file = a.slice(2);
|
|
83
|
+
}
|
|
84
|
+
if (parts.length > 0)
|
|
85
|
+
return parts.join('\n\n');
|
|
86
|
+
if (file !== null) {
|
|
87
|
+
try {
|
|
88
|
+
return readFileSync(file === '-' ? 0 : file, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/** Arguments du processus `pid` (Linux : /proc) ; null si illisibles. */
|
|
97
|
+
function argvOf(pid) {
|
|
98
|
+
try {
|
|
99
|
+
return readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').slice(0, -1);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function parentOf(pid) {
|
|
106
|
+
try {
|
|
107
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
108
|
+
const ppid = Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1]);
|
|
109
|
+
return Number.isInteger(ppid) && ppid > 1 ? ppid : null;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Message du commit en cours depuis un pre-commit, quand git ne le donne pas encore : celui de la ligne de
|
|
117
|
+
* commande du `git commit` qui nous a lancés (ancêtres du hook), ou CADENCE_COMMIT_MESSAGE. Null sinon.
|
|
118
|
+
*/
|
|
119
|
+
export function pendingCommitMessage(env, pid = process.ppid) {
|
|
120
|
+
if (env.CADENCE_COMMIT_MESSAGE)
|
|
121
|
+
return env.CADENCE_COMMIT_MESSAGE;
|
|
122
|
+
let cur = pid;
|
|
123
|
+
for (let depth = 0; cur !== null && depth < 6; depth++) {
|
|
124
|
+
const argv = argvOf(cur);
|
|
125
|
+
const msg = argv && /(^|\/)git$/.test(argv[0] ?? '') ? messageFromArgv(argv) : null;
|
|
126
|
+
if (msg !== null)
|
|
127
|
+
return msg;
|
|
128
|
+
cur = parentOf(cur);
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|