@sylad/cadence 1.41.0 → 1.43.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/dist/archive.js +103 -6
- package/dist/audit.js +2 -2
- package/dist/clean.js +0 -4
- package/dist/cli/context.js +129 -0
- package/dist/cli/deliver-commands.js +114 -0
- package/dist/cli/gates.js +234 -0
- package/dist/cli/lead-commands.js +161 -0
- package/dist/cli/lot-commands.js +257 -0
- package/dist/cli/read-commands.js +227 -0
- package/dist/cli/session-commands.js +152 -0
- package/dist/cli/setup-commands.js +72 -0
- package/dist/cli/spec-commands.js +298 -0
- package/dist/cli/stats-commands.js +138 -0
- package/dist/cli-add.js +37 -0
- package/dist/cli.js +82 -1651
- package/dist/config.js +16 -103
- package/dist/corrections.js +28 -42
- package/dist/docsync.js +15 -3
- package/dist/lead.js +6 -24
- package/dist/orchestrate/attente.js +82 -0
- package/dist/orchestrate/command-args.js +138 -0
- package/dist/orchestrate/command.js +22 -1618
- package/dist/orchestrate/continue.js +2 -12
- package/dist/orchestrate/cycle.js +8 -12
- package/dist/orchestrate/draw.js +62 -0
- package/dist/orchestrate/dry-run.js +223 -0
- package/dist/orchestrate/execute.js +421 -0
- package/dist/orchestrate/integrate.js +58 -4
- package/dist/orchestrate/integration-vivante.js +138 -0
- package/dist/orchestrate/launch.js +6 -18
- package/dist/orchestrate/lock.js +3 -20
- package/dist/orchestrate/pool.js +1 -1
- package/dist/orchestrate/preflight.js +190 -0
- package/dist/orchestrate/registry.js +8 -1
- package/dist/orchestrate/resume.js +384 -0
- package/dist/orchestrate/schemas.js +5 -1
- package/dist/orchestrate/snapshot.js +19 -4
- package/dist/orchestrate/state.js +17 -0
- package/dist/orchestrate/status.js +264 -0
- package/dist/plan-extras.js +3 -2
- package/dist/plan-ids.js +95 -0
- package/dist/plan-read.js +2 -0
- package/dist/plan.js +29 -34
- package/dist/ref/command.js +2 -3
- package/dist/resources.js +3 -9
- package/dist/state.js +8 -8
- package/dist/verify-gate.js +33 -2
- package/dist/yaml-keys.js +19 -0
- package/docs/cadence-hud.md +1 -1
- package/docs/deliver.md +1 -1
- package/docs/lead.md +2 -2
- package/docs/news.md +2 -0
- package/docs/orchestrate.md +20 -4
- package/docs/qualite.md +6 -1
- package/docs/raf.md +22 -4
- package/docs/whats-new.md +9 -0
- package/package.json +3 -1
- package/scripts/qualite.d.mts +15 -0
- package/scripts/qualite.mjs +352 -0
- package/skills/lead/SKILL.md +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { gitRoot } from '../git.js';
|
|
3
|
+
import { localOverdueLines } from '../local-task.js';
|
|
4
|
+
import { RafError } from '../plan.js';
|
|
5
|
+
import { findProjects, isProject as isLeadProject } from '../lead.js';
|
|
6
|
+
import { freeLine, readyFree } from '../ready-free.js';
|
|
7
|
+
import { cadenceHome, expiredHolds, freeSlots, liveHolds, liveSlots, liveWaves } from './registry.js';
|
|
8
|
+
import { longDuration } from './guard.js';
|
|
9
|
+
import { keptLines } from './worktree.js';
|
|
10
|
+
import { lotFinished, lotKey, RunStore } from './state.js';
|
|
11
|
+
import { launchPrices, renderDetail, renderTable, resumeCommand } from './table.js';
|
|
12
|
+
import { waveReadsAnswerQueue, waveReadsControl } from './snapshot.js';
|
|
13
|
+
import { DEFAULT_MAX_SESSIONS, parseMaxSessions } from './command-args.js';
|
|
14
|
+
export function maxSessions(args, io) {
|
|
15
|
+
if (args.maxSessions !== undefined)
|
|
16
|
+
return args.maxSessions;
|
|
17
|
+
const env = io.env.CADENCE_MAX_SESSIONS;
|
|
18
|
+
return env ? parseMaxSessions(env, 'CADENCE_MAX_SESSIONS') : DEFAULT_MAX_SESSIONS;
|
|
19
|
+
}
|
|
20
|
+
/** Plafond à AFFICHER (`--status`, `lead tour`) : le réglage de l'humain, jamais 2 en dur. Un réglage invalide ne casse pas l'affichage (le lancement, lui, le refuse) : retour au défaut. */
|
|
21
|
+
export function configuredCap(env, flag) {
|
|
22
|
+
if (flag !== undefined)
|
|
23
|
+
return flag;
|
|
24
|
+
try {
|
|
25
|
+
return env.CADENCE_MAX_SESSIONS ? parseMaxSessions(env.CADENCE_MAX_SESSIONS, 'CADENCE_MAX_SESSIONS') : DEFAULT_MAX_SESSIONS;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return DEFAULT_MAX_SESSIONS;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function duration(ms) {
|
|
32
|
+
const s = Math.round(ms / 1000);
|
|
33
|
+
return s < 60 ? `${s} s` : `${Math.floor(s / 60)} min ${String(s % 60).padStart(2, '0')} s`;
|
|
34
|
+
}
|
|
35
|
+
/** La ligne des créneaux (L141, L255) et leur nombre libre : toujours, même sans vague ni session du lead (L416 : « libres : 3 sur 3 » se lit, il ne se déduit pas d'une absence de ligne). */
|
|
36
|
+
export function slotsLine(cap = DEFAULT_MAX_SESSIONS, now = new Date()) {
|
|
37
|
+
const home = cadenceHome();
|
|
38
|
+
const holds = liveHolds(home, now);
|
|
39
|
+
const free = freeSlots(liveWaves(home), cap, holds.length);
|
|
40
|
+
const lead = holds.length ? ` · sessions du lead : ${holds.length} (${holds.map((h) => h.label).join(', ')})` : '';
|
|
41
|
+
return { line: `vagues en cours : ${liveWaves(home).length} · sessions en cours : ${liveSlots(home).length}${lead} · créneaux libres : ${free} sur ${cap}`, free };
|
|
42
|
+
}
|
|
43
|
+
/** Vagues vivantes (toutes, quel que soit leur dossier de départ), sessions du lead (L255), créneaux libres et dépôts qu'elles tiennent. Rien sans vague ni session du lead. */
|
|
44
|
+
export function liveLines(cap = DEFAULT_MAX_SESSIONS, now = new Date()) {
|
|
45
|
+
const home = cadenceHome();
|
|
46
|
+
const waves = liveWaves(home);
|
|
47
|
+
if (waves.length === 0 && liveHolds(home, now).length === 0)
|
|
48
|
+
return [];
|
|
49
|
+
const lines = [slotsLine(cap, now).line];
|
|
50
|
+
for (const w of waves)
|
|
51
|
+
lines.push(` ${w.wave} (pid ${w.pid}, depuis ${w.started}) lancée depuis ${w.cwd} · plafond ${w.cap ?? '?'} · dépôts : ${w.repos.join(', ')}`);
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
/** Une ligne ⚠ par session du lead dont le TTL est échu et qui n'a pas été rendue (L278) : vide sans session échue. */
|
|
55
|
+
export function expiredHoldLines(now = new Date()) {
|
|
56
|
+
return expiredHolds(cadenceHome(), now).map((h) => {
|
|
57
|
+
const since = Date.parse(h.since);
|
|
58
|
+
const age = Number.isFinite(since) ? `inscrite depuis ${longDuration(now.getTime() - since)}, ` : '';
|
|
59
|
+
return `⚠ session du lead échue : ${h.label} (${age}échue depuis ${longDuration(now.getTime() - Date.parse(h.expires))}) — la reprendre (cadence lead hold ${h.label} --ttl <minutes>) ou l'arrêter (cadence lead release ${h.label})`;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** `--continue` (L252) : la ligne des lots prêts dont aucune ressource n'est prise par un lot en cours ni par une vague, sans les lots du premier tirage `drawn` ni les ressources qu'ils tiennent. Vide si aucun. */
|
|
63
|
+
export function freeLines(launch, drawn) {
|
|
64
|
+
const dirs = isLeadProject(launch) ? [launch] : findProjects(launch);
|
|
65
|
+
const free = readyFree(dirs, cadenceHome(), drawn);
|
|
66
|
+
const line = freeLine(dirs.filter((d) => free.has(d)).map((d) => ({ project: basename(d), free: free.get(d) })), 60);
|
|
67
|
+
return line ? [line] : [];
|
|
68
|
+
}
|
|
69
|
+
/** Efface l'écran et replace le curseur en haut : l'entrée d'un rendu de `--status --watch`. */
|
|
70
|
+
const CLEAR_SCREEN = '\x1b[H\x1b[2J';
|
|
71
|
+
const DEFAULT_WATCH_INTERVAL = 10;
|
|
72
|
+
/** Ce que `--status` montre à un instant : les vagues vivantes, puis le tableau (absent si seules les vagues vivantes comptent). */
|
|
73
|
+
function statusView(args, launch, cap, now) {
|
|
74
|
+
const id = statusWave(args);
|
|
75
|
+
const store = id !== undefined ? RunStore.find(launch, id) : RunStore.last(launch);
|
|
76
|
+
const live = [...liveLines(cap, now), ...expiredHoldLines(now), ...localOverdueLines((isLeadProject(launch) ? [launch] : findProjects(launch)).map((d) => gitRoot(d) ?? d), now)];
|
|
77
|
+
const waveLive = liveWaves(cadenceHome()).length > 0;
|
|
78
|
+
if (!store && live.length && id === undefined)
|
|
79
|
+
return { live, running: waveLive };
|
|
80
|
+
if (!store)
|
|
81
|
+
throw new RafError(id !== undefined ? `vague inconnue : ${id}` : 'aucune vague dans ce dossier');
|
|
82
|
+
// `running` : la vague suivie tourne — pour un <id> explicite, elle seule compte, pas les autres vagues de la machine
|
|
83
|
+
const running = id !== undefined ? liveWaves(cadenceHome()).some((w) => w.wave === store.readWave().id) : waveLive;
|
|
84
|
+
const lots = store.lots();
|
|
85
|
+
if (args.detail !== undefined) {
|
|
86
|
+
try {
|
|
87
|
+
return { live: [], table: renderDetail(lots, args.detail), running };
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
throw new RafError(e.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { live, table: [...renderTable(store.readWave(), lots, { compact: true, prices: launchPrices(launch, (w) => live.push(`⚠ ${w}`)) }), ...keptLines(lots.map((l) => l.repo), now.getTime())], running };
|
|
94
|
+
}
|
|
95
|
+
/** La vague que `--status` suit (L395) : `--status <id>` ou `--status --wave <id>` ; aucune : la dernière du dossier. Deux ids différents : refus. */
|
|
96
|
+
function statusWave(args) {
|
|
97
|
+
if (typeof args.status === 'string' && args.wave !== undefined && args.status !== args.wave)
|
|
98
|
+
throw new RafError(`--status ${args.status} et --wave ${args.wave} désignent deux vagues : une seule`);
|
|
99
|
+
return typeof args.status === 'string' ? args.status : args.status === true ? args.wave : undefined;
|
|
100
|
+
}
|
|
101
|
+
/** Refus d'une `--wave` déjà existante, identique en simulation et au vrai lancement. */
|
|
102
|
+
export function waveExists(wave, io) {
|
|
103
|
+
io.err(`orchestrate : --wave ${wave} : cette vague existe déjà`);
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
/** L382 : `claude auth status` peut durer jusqu'à 20 s ; en --continue, preflight() le relancerait pour chaque lot candidat de chaque tour. Une seule lecture par lancement (une vague). */
|
|
107
|
+
export function onceAuth(deps) {
|
|
108
|
+
const read = deps.claudeAuth;
|
|
109
|
+
if (!read)
|
|
110
|
+
return deps;
|
|
111
|
+
let cached;
|
|
112
|
+
return { ...deps, claudeAuth: () => (cached ??= { value: read() }).value };
|
|
113
|
+
}
|
|
114
|
+
/** `--status` (L395) : le tableau des vagues vivantes et de la vague suivie, une fois ou rafraîchi avec `--watch` jusqu'à ce que plus rien ne tourne. */
|
|
115
|
+
export async function statusCommand(args, io, deps, launch) {
|
|
116
|
+
maxSessions(args, io); // une valeur invalide est refusée ici aussi
|
|
117
|
+
const view = () => statusView(args, launch, configuredCap(io.env, args.maxSessions), io.now());
|
|
118
|
+
if (!args.watch) {
|
|
119
|
+
const { live, table } = view();
|
|
120
|
+
printStatus(io, live, table); // sans identifiant (pas de tableau) : les vagues vivantes suffisent
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
// --watch : le même tableau, rafraîchi ; il s'arrête seul quand plus aucune vague ne tourne (le dernier rendu reste affiché)
|
|
124
|
+
const sleep = deps.watchSleep ?? ((ms) => new Promise((res) => setTimeout(res, ms)));
|
|
125
|
+
const seconds = args.interval ?? DEFAULT_WATCH_INTERVAL;
|
|
126
|
+
for (;;) {
|
|
127
|
+
const { live, table, running } = view();
|
|
128
|
+
io.out(CLEAR_SCREEN);
|
|
129
|
+
printStatus(io, live, table);
|
|
130
|
+
if (!running)
|
|
131
|
+
return 0;
|
|
132
|
+
io.out('');
|
|
133
|
+
io.out(`rafraîchi toutes les ${seconds} s — Ctrl-C pour quitter`);
|
|
134
|
+
await sleep(seconds * 1000);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Les vagues vivantes, puis (s'il y en a un) le tableau de la vague suivie, séparés d'une ligne vide. */
|
|
138
|
+
function printStatus(io, live, table) {
|
|
139
|
+
for (const line of live)
|
|
140
|
+
io.out(line);
|
|
141
|
+
if (!table)
|
|
142
|
+
return;
|
|
143
|
+
if (live.length)
|
|
144
|
+
io.out('');
|
|
145
|
+
for (const line of table)
|
|
146
|
+
io.out(line);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* `--answer` sur une vague VIVANTE (L407) : la réponse est déposée dans la file de la vague (`answers.log`), qui la prend entre deux étapes et rejoue le lot dès qu'un créneau est libre.
|
|
150
|
+
* Sans cela le lead attendait la fin de la vague (15 min d'un lot bloqué le 28-10). Les lots visés sont contrôlés comme à la reprise : connus, en question.
|
|
151
|
+
*/
|
|
152
|
+
export function answerLive(args, io, store) {
|
|
153
|
+
const lots = store.lots();
|
|
154
|
+
const refusals = [];
|
|
155
|
+
const keys = [];
|
|
156
|
+
for (const a of args.answers) {
|
|
157
|
+
const matches = lots.filter((l) => l.lot === a.lot && (a.project ? l.project === a.project : true));
|
|
158
|
+
const where = `${a.project ? `${a.project}:` : ''}${a.lot}`;
|
|
159
|
+
if (matches.length !== 1)
|
|
160
|
+
refusals.push(matches.length ? `--answer ${where} : plusieurs projets portent ce lot, précisez projet:lot` : `--answer ${where} : lot inconnu dans la vague ${store.id}`);
|
|
161
|
+
else if (matches[0].status !== 'question')
|
|
162
|
+
refusals.push(`--answer ${lotKey(matches[0].project, matches[0].lot)} : le lot n'attend pas de réponse (${matches[0].status})`);
|
|
163
|
+
else
|
|
164
|
+
keys.push({ key: lotKey(matches[0].project, matches[0].lot), text: a.text });
|
|
165
|
+
}
|
|
166
|
+
if (!waveReadsAnswerQueue(store.dir))
|
|
167
|
+
refusals.push(`la vague ${store.id} ne lit pas la file de réponses (lancée avec une version de cadence d'avant L407) : attendre sa fin, puis ${resumeCommand(store.id, '--answer', '<projet>:<lot>', '"…"')}`);
|
|
168
|
+
if (refusals.length) {
|
|
169
|
+
for (const r of refusals)
|
|
170
|
+
io.err(`orchestrate : ${r}`);
|
|
171
|
+
return 2;
|
|
172
|
+
}
|
|
173
|
+
for (const { key, text } of keys) {
|
|
174
|
+
const id = store.requestAnswer(key, text);
|
|
175
|
+
// La vague a pu finir entre le contrôle et le dépôt : sa file ne sera plus lue, la réponse est retirée et le lead reprend à la main.
|
|
176
|
+
if (!liveWaves(cadenceHome()).some((w) => w.wave === store.id)) {
|
|
177
|
+
store.markAnswerTaken(id);
|
|
178
|
+
io.err(`orchestrate : ${key} : la vague ${store.id} vient de finir, réponse non prise : ${resumeCommand(store.id, '--answer', key, '"…"')}`);
|
|
179
|
+
return 2;
|
|
180
|
+
}
|
|
181
|
+
store.journal(`demande : réponse du lead pour ${key}`);
|
|
182
|
+
io.out(`${key} : réponse déposée — la vague ${store.id} la prend entre deux étapes et rejoue le lot dès qu'un créneau est libre`);
|
|
183
|
+
}
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
/** La vague vivante, pour `--answer` seul (sans --resume) : la même recherche que `--drop`. */
|
|
187
|
+
export function answerOnly(args, io) {
|
|
188
|
+
if (args.drop.length || args.stopAfterCurrent || args.budget !== undefined || args.dryRun || args.continue || args.lots.length)
|
|
189
|
+
throw new RafError("--answer ne se combine pas avec --drop, --stop-after-current, --budget, --dry-run, --continue ni des lots à lancer (sur une vague arrêtée : --resume --answer)");
|
|
190
|
+
const store = liveStore('--answer', args, io);
|
|
191
|
+
if (typeof store === 'number') {
|
|
192
|
+
io.err(`orchestrate : --answer : une vague arrêtée se reprend avec ${resumeCommand('<vague>', '--answer', '<projet>:<lot>', '"…"')}`);
|
|
193
|
+
return store;
|
|
194
|
+
}
|
|
195
|
+
return answerLive(args, io, store);
|
|
196
|
+
}
|
|
197
|
+
/** La vague vivante lancée depuis ce dossier que vise `--wave` (ou la seule) ; sinon le refus (code 2) déjà écrit. */
|
|
198
|
+
export function liveStore(flags, args, io) {
|
|
199
|
+
const live = liveWaves(cadenceHome()).filter((w) => w.cwd === io.cwd && (args.wave === undefined || w.wave === args.wave));
|
|
200
|
+
if (live.length === 0) {
|
|
201
|
+
io.err(`orchestrate : ${flags} : aucune vague en cours${args.wave ? ` (${args.wave})` : ''} lancée depuis ce dossier`);
|
|
202
|
+
return 2;
|
|
203
|
+
}
|
|
204
|
+
if (live.length > 1) {
|
|
205
|
+
io.err(`orchestrate : ${flags} : plusieurs vagues tournent depuis ce dossier (${live.map((w) => w.wave).join(', ')}) : précisez --wave`);
|
|
206
|
+
return 2;
|
|
207
|
+
}
|
|
208
|
+
const store = RunStore.find(io.cwd, live[0].wave);
|
|
209
|
+
if (!store) {
|
|
210
|
+
io.err(`orchestrate : ${flags} : dossier d'état de la vague ${live[0].wave} introuvable`);
|
|
211
|
+
return 2;
|
|
212
|
+
}
|
|
213
|
+
return store;
|
|
214
|
+
}
|
|
215
|
+
/** `--drop` / `--stop-after-current` (L79) : une demande écrite dans le dossier d'état de la vague vivante ; c'est elle qui la lit avant sa prochaine session. */
|
|
216
|
+
export function control(args, io) {
|
|
217
|
+
const flags = '--drop / --stop-after-current';
|
|
218
|
+
if (args.lots.length || args.continue || args.dryRun || args.budget !== undefined || args.answers.length)
|
|
219
|
+
throw new RafError(`${flags} ne se combinent pas avec des lots à lancer, --continue, --dry-run, --budget ou --answer`);
|
|
220
|
+
const store = liveStore(flags, args, io);
|
|
221
|
+
if (typeof store === 'number')
|
|
222
|
+
return store;
|
|
223
|
+
const lots = store.lots();
|
|
224
|
+
const refusals = [];
|
|
225
|
+
const keys = [];
|
|
226
|
+
for (const d of args.drop) {
|
|
227
|
+
const matches = lots.filter((l) => l.lot === d.lot && (d.project ? l.project === d.project : true));
|
|
228
|
+
const where = `${d.project ? `${d.project}:` : ''}${d.lot}`;
|
|
229
|
+
if (matches.length !== 1)
|
|
230
|
+
refusals.push(matches.length ? `--drop ${where} : plusieurs projets portent ce lot, précisez projet:lot` : `--drop ${where} : lot inconnu dans la vague ${store.id}`);
|
|
231
|
+
else if (lotFinished(matches[0]))
|
|
232
|
+
refusals.push(`--drop ${lotKey(matches[0].project, matches[0].lot)} : le lot est déjà fini (${matches[0].status})`);
|
|
233
|
+
else
|
|
234
|
+
keys.push(lotKey(matches[0].project, matches[0].lot));
|
|
235
|
+
}
|
|
236
|
+
if (refusals.length) {
|
|
237
|
+
for (const r of refusals)
|
|
238
|
+
io.err(`orchestrate : ${r}`);
|
|
239
|
+
return 2;
|
|
240
|
+
}
|
|
241
|
+
// La demande est écrite quoi qu'il arrive (le fichier est inoffensif) ; mais une vague d'avant L79 ne le lira jamais : on le dit.
|
|
242
|
+
if (!waveReadsControl(store.dir))
|
|
243
|
+
io.err(`orchestrate : ${flags} : la vague ${store.id} ne lit pas les demandes de contrôle (lancée avec une version de cadence d'avant L79) : la demande est écrite dans control.log mais cette vague ne la lira pas`);
|
|
244
|
+
for (const k of keys) {
|
|
245
|
+
store.requestDrop(k);
|
|
246
|
+
store.journal(`demande : retirer ${k} de la vague`);
|
|
247
|
+
io.out(`${k} : retrait demandé — la vague ${store.id} l'écarte avant sa prochaine session (la session en cours finit)`);
|
|
248
|
+
}
|
|
249
|
+
if (args.stopAfterCurrent) {
|
|
250
|
+
store.requestStopAfterCurrent();
|
|
251
|
+
store.journal('demande : arrêt après les sessions en cours');
|
|
252
|
+
io.out(`vague ${store.id} : arrêt demandé — les sessions en cours finissent, aucune autre ne part ; reprise par « ${resumeCommand(store.id)} »`);
|
|
253
|
+
}
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
/** La seule vague non terminée du dossier (L309) ; plusieurs : refus qui les liste, aucune : refus (jamais le choix silencieux de la plus récente). */
|
|
257
|
+
export function singleUnfinished(launch) {
|
|
258
|
+
const open = RunStore.unfinished(launch);
|
|
259
|
+
if (open.length === 0)
|
|
260
|
+
throw new RafError('aucune vague à reprendre dans ce dossier');
|
|
261
|
+
if (open.length > 1)
|
|
262
|
+
throw new RafError(`plusieurs vagues non terminées dans ce dossier, laquelle reprendre ?\n${open.map((s) => ` ${s.id} (${s.readWave()?.status ?? 'illisible'}, lancée ${s.readWave()?.created ?? '?'})`).join('\n')}\nreprise : ${resumeCommand('<vague>')}`);
|
|
263
|
+
return open[0];
|
|
264
|
+
}
|
package/dist/plan-extras.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { PLACE_KEY } from './archive.js';
|
|
1
2
|
import { FIELDS, RafError } from './plan.js';
|
|
2
3
|
const EXTRA_KEYS = ['enum', 'default'];
|
|
3
4
|
const EXTRA_NAME = /^[a-z][a-z0-9_]*$/;
|
|
4
|
-
/** Les clés que raf lit ou écrit sur un lot : un champ d'extension ne peut pas en prendre le nom. */
|
|
5
|
-
const LOT_KEYS = ['tasks', 'uses', 'local', 'ux', 'review', 'docs_ok', 'extras', 'problems', 'state', 'state_before'];
|
|
5
|
+
/** Les clés que raf lit ou écrit sur un lot (`place` : le rang d'un lot archivé sous `plan.idPattern`, L536) : un champ d'extension ne peut pas en prendre le nom. */
|
|
6
|
+
const LOT_KEYS = ['tasks', 'uses', 'local', 'ux', 'review', 'docs_ok', 'extras', 'problems', 'state', 'state_before', PLACE_KEY];
|
|
6
7
|
const nonEmptyText = (v) => typeof v === 'string' && v.trim() !== '';
|
|
7
8
|
/** Une déclaration `plan.extras.<nom>` lue, ou l'erreur (message sans préfixe) qui la refuse. */
|
|
8
9
|
function readDecl(name, decl) {
|
package/dist/plan-ids.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export function escapeRe(s) {
|
|
2
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Ce qui ne peut pas suivre un identifiant cité : un point suivi d'un caractère de mot — « L1.4 » n'est
|
|
6
|
+
* pas le lot L1, « NC2.4 » n'est pas NC2 — alors que le point qui finit une phrase (« voir L1. ») passe.
|
|
7
|
+
* Même garde pour le format de raf et pour les identifiants d'un plan en lecture seule.
|
|
8
|
+
*/
|
|
9
|
+
export const DOTTED = '\\.\\w';
|
|
10
|
+
/** Matches `L3` or `L3/t1` as whole words. Group 1 = lot id, group 2 = task id. */
|
|
11
|
+
function refPattern(prefix) {
|
|
12
|
+
return new RegExp(`(?<![\\w/])(${escapeRe(prefix)}\\d+)(?:/(t\\d+))?(?!\\w|${DOTTED})`, 'g');
|
|
13
|
+
}
|
|
14
|
+
export function extractRefs(text, prefix) {
|
|
15
|
+
return [...text.matchAll(refPattern(prefix))].map((m) => ({ lot: m[1], task: m[2] }));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Le motif des références d'un plan natif qui déclare `plan.idPattern` : `<préfixe>\d+` ou le motif du projet, puis un sous-id `/slug` facultatif
|
|
19
|
+
* (`X7/t1-fusion`). Garde des `<préfixe>\d+` : celle du format de raf (pas collés à un mot ni à un chemin, pas suivis d'un point et d'un mot ; « feat(L3-L5) » cite L3 et L5) ; les ids du motif du projet, plus strictes, refusent aussi un `-` ou un `.` avant et un `-` après (« AB-1-x »). Le point de fin de phrase passe.
|
|
20
|
+
* Seul le groupe nommé `sub` est capturé, après le motif du projet : ses groupes numérotés (`([A-Z])\1\d+`) gardent leur numéro. L'id est le reste de la correspondance.
|
|
21
|
+
*/
|
|
22
|
+
function idRefPattern(prefix, idPattern) {
|
|
23
|
+
const own = `(?<![\\w/])${escapeRe(prefix)}\\d+`;
|
|
24
|
+
const free = `(?<![\\w/.-])(?:${idPattern})(?=/|(?!-))`;
|
|
25
|
+
return new RegExp(`(?:${own}|${free})(?:/(?<sub>[\\w-]+(?:\\.[\\w-]+)*)(?![\\w-]|${DOTTED})|(?!\\w|${DOTTED}))`, 'g');
|
|
26
|
+
}
|
|
27
|
+
/** Références d'un message dans un plan natif qui déclare `plan.idPattern`. Un sous-id `t<n>` est une sous-tâche du lot ; tout autre (`t1-fusion`) n'est qu'une précision sur le lot cité. */
|
|
28
|
+
export function idRefs(text, prefix, idPattern) {
|
|
29
|
+
return [...text.matchAll(idRefPattern(prefix, idPattern))].map((m) => {
|
|
30
|
+
const sub = m.groups.sub;
|
|
31
|
+
return { lot: sub === undefined ? m[0] : m[0].slice(0, -(sub.length + 1)), ...(/^t\d+$/.test(sub ?? '') ? { task: sub } : {}) };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Ce que le motif `plan.idPattern` a de refusable (ancres, groupe nommé `id` ou `sub` réservé, id vide), null s'il est utilisable. Vérifié à la lecture de cadence.yaml. */
|
|
35
|
+
function idPatternProblem(idPattern) {
|
|
36
|
+
if (idPattern.trim() === '')
|
|
37
|
+
return 'est vide';
|
|
38
|
+
if (/^\^|(?<!\\)\$$/.test(idPattern))
|
|
39
|
+
return "ne prend pas d'ancre (^ ou $) : le motif est ancré par raf";
|
|
40
|
+
if (/\(\?<(?:id|sub)>/.test(idPattern))
|
|
41
|
+
return 'ne peut pas nommer un groupe « id » ou « sub » (noms réservés à raf)';
|
|
42
|
+
try {
|
|
43
|
+
idRefPattern('L', idPattern);
|
|
44
|
+
if (new RegExp(`^(?:${idPattern})$`).test(''))
|
|
45
|
+
return "accepte l'identifiant vide";
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
return `n'est pas une expression régulière utilisable (${e.message.split('\n')[0]})`;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/** Pourquoi `id` ne peut pas être donné à `raf add --id` : sans `plan.idPattern`, avec un « / » (réservé aux sous-tâches), hors du motif, ou déjà pris (archivés compris). Null s'il est libre. */
|
|
53
|
+
function freeIdProblem(id, idPattern, known) {
|
|
54
|
+
if (idPattern === undefined)
|
|
55
|
+
return '--id exige plan.idPattern dans cadence.yaml : sans motif déclaré, les identifiants sont <préfixe><n> générés par raf';
|
|
56
|
+
if (id.includes('/'))
|
|
57
|
+
return `identifiant invalide : ${id} (« / » est réservé aux sous-tâches)`;
|
|
58
|
+
if (!new RegExp(`^(?:${idPattern})$`).test(id))
|
|
59
|
+
return `identifiant invalide : ${id === '' ? '(vide)' : id} ne suit pas plan.idPattern (${idPattern})`;
|
|
60
|
+
return known.has(id) ? `identifiant déjà pris : ${id}` : null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* L'identifiant d'un lot ajouté : celui que `raf add --id` donne (libre, voir `freeIdProblem`), sinon `<préfixe><n+1>` ; `under`, le lot parent
|
|
64
|
+
* d'un sous-lot, doit exister. `known` : tous les lots du plan, archivés compris — un identifiant n'est jamais réutilisé.
|
|
65
|
+
*/
|
|
66
|
+
export function pickId(prefix, given, under, idPattern, known) {
|
|
67
|
+
if (under !== undefined && !known.has(under))
|
|
68
|
+
return { problem: `lot parent inconnu : ${under}` };
|
|
69
|
+
if (given !== undefined) {
|
|
70
|
+
const problem = freeIdProblem(given, idPattern, known);
|
|
71
|
+
return problem ? { problem } : { id: given };
|
|
72
|
+
}
|
|
73
|
+
const re = new RegExp(`^${escapeRe(prefix)}(\\d+)$`);
|
|
74
|
+
return { id: `${prefix}${Math.max(0, ...[...known].map((id) => Number(re.exec(id)?.[1] ?? 0))) + 1}` };
|
|
75
|
+
}
|
|
76
|
+
/** Le `parent:` d'un lot à plat : un identifiant de lot ; toute autre forme est un problème du lot et ne désigne aucun parent. */
|
|
77
|
+
export function readParent(raw, problems) {
|
|
78
|
+
if (raw == null)
|
|
79
|
+
return {};
|
|
80
|
+
if (typeof raw === 'string' && raw.trim() !== '')
|
|
81
|
+
return { parent: raw.trim() };
|
|
82
|
+
problems.push(`parent : un identifiant de lot attendu (reçu « ${String(raw)} »)`);
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
/** `plan.idPattern` lu dans cadence.yaml : `{ idPattern }` (rien sans la clé), ou l'erreur que `bad` fabrique (qui nomme la clé). */
|
|
86
|
+
export function readIdPattern(raw, bad) {
|
|
87
|
+
if (raw == null)
|
|
88
|
+
return {};
|
|
89
|
+
if (typeof raw !== 'string')
|
|
90
|
+
throw bad('idPattern doit être une expression régulière (texte)');
|
|
91
|
+
const problem = idPatternProblem(raw);
|
|
92
|
+
if (problem)
|
|
93
|
+
throw bad(`idPattern ${problem}`);
|
|
94
|
+
return { idPattern: raw };
|
|
95
|
+
}
|
package/dist/plan-read.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isDay } from './dates.js';
|
|
2
2
|
import { RESOURCE_NAME, RESOURCES_BAD_LIST, SCREENS, STATUSES } from './plan.js';
|
|
3
|
+
import { readParent } from './plan-ids.js';
|
|
3
4
|
const GATE_INVALID = 'gate invalide : count: entier ≥ 1 et un critère spec: true ou screen: page attendus';
|
|
4
5
|
/** Lit `gate: { count: N, spec: true }` ou `{ count: N, screen: page }` ; toute autre forme est un problème du lot et une condition jamais remplie. */
|
|
5
6
|
function readGate(raw, problems) {
|
|
@@ -68,6 +69,7 @@ export function normalizeLot(raw, problems = []) {
|
|
|
68
69
|
const docsOk = okRaw && typeof okRaw === 'object' && typeof okRaw.reason === 'string' && okRaw.reason.trim() !== '' ? { date: String(okRaw.date ?? ''), reason: okRaw.reason.trim() } : undefined;
|
|
69
70
|
return {
|
|
70
71
|
id: String(raw.id),
|
|
72
|
+
...readParent(raw.parent, problems),
|
|
71
73
|
title: String(raw.title ?? ''),
|
|
72
74
|
status,
|
|
73
75
|
...(typeof raw.state === 'string' && raw.state.trim() !== '' ? { state: raw.state.trim() } : {}),
|
package/dist/plan.js
CHANGED
|
@@ -8,7 +8,8 @@ import { normalizeLot } from './plan-read.js';
|
|
|
8
8
|
import { currentState, defaultState, rebase, stateProblems, transitionProblem } from './plan-states.js';
|
|
9
9
|
import { DOCS_OK_FILE, readDocsOkFile } from './docs-ok-file.js';
|
|
10
10
|
import { checkedExtras, withExtras } from './plan-extras.js';
|
|
11
|
-
import {
|
|
11
|
+
import { DOTTED, escapeRe, extractRefs, idRefs, pickId } from './plan-ids.js';
|
|
12
|
+
import { ARCHIVE_DIR, PLACE_KEY, archiveFiles, archiveLots, archiveYear, mergeArchived, newArchiveDoc, readArchiveDoc, reopenArchived, selectArchivable } from './archive.js';
|
|
12
13
|
/** Délai de la commande `plan.onWrite` : une republication du plan, pas un build. */
|
|
13
14
|
const ON_WRITE_TIMEOUT_MS = 120_000;
|
|
14
15
|
/**
|
|
@@ -42,28 +43,13 @@ function cleanUses(names) {
|
|
|
42
43
|
return [...new Set(names)];
|
|
43
44
|
}
|
|
44
45
|
const trueFlags = (flags) => Object.fromEntries(Object.entries(flags).filter(([, on]) => on).map(([k]) => [k, true])); // les drapeaux vrais, chacun à `true` : les absents et les faux ne s'écrivent pas
|
|
46
|
+
export { escapeRe, extractRefs, readIdPattern } from './plan-ids.js';
|
|
45
47
|
export const FIELDS = ['id', 'title', 'status', 'estimate', 'quickwin', 'visible', 'screen', 'spec', 'public', 'every', 'last', 'after', 'created', 'started', 'finished', 'notes', 'repos', 'parent', 'gate', 'goal'];
|
|
46
48
|
/** Nature de l'écran d'un lot visible : nouvelle page, nouveau composant, parcours modifié. */
|
|
47
49
|
export const SCREENS = ['page', 'component', 'journey'];
|
|
48
50
|
export function isOpen(status) {
|
|
49
51
|
return status === 'todo' || status === 'doing';
|
|
50
52
|
}
|
|
51
|
-
export function escapeRe(s) {
|
|
52
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Ce qui ne peut pas suivre un identifiant cité : un point suivi d'un caractère de mot — « L1.4 » n'est
|
|
56
|
-
* pas le lot L1, « NC2.4 » n'est pas NC2 — alors que le point qui finit une phrase (« voir L1. ») passe.
|
|
57
|
-
* Même garde pour le format de raf et pour les identifiants d'un plan en lecture seule.
|
|
58
|
-
*/
|
|
59
|
-
const DOTTED = '\\.\\w';
|
|
60
|
-
/** Matches `L3` or `L3/t1` as whole words. Group 1 = lot id, group 2 = task id. */
|
|
61
|
-
function refPattern(prefix) {
|
|
62
|
-
return new RegExp(`(?<![\\w/])(${escapeRe(prefix)}\\d+)(?:/(t\\d+))?(?!\\w|${DOTTED})`, 'g');
|
|
63
|
-
}
|
|
64
|
-
export function extractRefs(text, prefix) {
|
|
65
|
-
return [...text.matchAll(refPattern(prefix))].map((m) => ({ lot: m[1], task: m[2] }));
|
|
66
|
-
}
|
|
67
53
|
function initialContent(project, prefix, since) {
|
|
68
54
|
return `# Plan « reste à faire » tenu par raf (https://github.com/Sylad/cadence).
|
|
69
55
|
# Édité par le CLI, mais les modifications à la main et les commentaires sont préservés.
|
|
@@ -305,7 +291,7 @@ export class Plan {
|
|
|
305
291
|
*/
|
|
306
292
|
refs = (text) => {
|
|
307
293
|
if (!this.settings.format)
|
|
308
|
-
return extractRefs(text, this.prefix);
|
|
294
|
+
return this.settings.idPattern === undefined ? extractRefs(text, this.prefix) : idRefs(text, this.prefix, this.settings.idPattern);
|
|
309
295
|
const { refs, tasks, ids } = this.read();
|
|
310
296
|
if (!refs)
|
|
311
297
|
return [];
|
|
@@ -395,7 +381,8 @@ export class Plan {
|
|
|
395
381
|
// L396 : l'archive est un détail de stockage — les lots archivés reprennent leur place parmi ceux du fichier actif, comme un seul plan.
|
|
396
382
|
const archived = this.loadArchives().flatMap((a) => archiveLots(a.doc).toJSON());
|
|
397
383
|
const read = (r) => withExtras(normalizeLot(r), r, this.settings.extras);
|
|
398
|
-
const
|
|
384
|
+
const places = new Map(archived.flatMap((r) => (typeof r[PLACE_KEY] === 'string' || r[PLACE_KEY] === null ? [[String(r.id), r[PLACE_KEY] ?? '']] : [])));
|
|
385
|
+
const lots = mergeArchived(archived.map(read), this.activeRaw().map(read), places);
|
|
399
386
|
const model = this.settings.states;
|
|
400
387
|
return model ? lots.map((l) => ({ ...l, problems: [...l.problems, ...stateProblems(model, l)] })) : lots;
|
|
401
388
|
}
|
|
@@ -472,20 +459,11 @@ export class Plan {
|
|
|
472
459
|
}
|
|
473
460
|
/**
|
|
474
461
|
* Un lot archivé qui n'est plus clos (`raf todo` sur un lot done archivé) revient à la fin du fichier du plan à l'écriture :
|
|
475
|
-
* l'archive ne porte que des lots done ou dropped, et le plan actif reste le seul fichier à porter du travail ouvert.
|
|
462
|
+
* l'archive ne porte que des lots done ou dropped, et le plan actif reste le seul fichier à porter du travail ouvert. Seul lui bouge :
|
|
463
|
+
* les lots archivés dont le `place:` le nommait (L536) sont re-pointés sur le lot qui le précédait, et gardent leur place dans le plan présenté.
|
|
476
464
|
*/
|
|
477
465
|
reopened() {
|
|
478
|
-
|
|
479
|
-
for (const a of this.archives ?? []) {
|
|
480
|
-
const seq = archiveLots(a.doc);
|
|
481
|
-
const back = seq.items.filter((n) => isMap(n) && !['done', 'dropped'].includes(String(n.get('status'))));
|
|
482
|
-
if (back.length === 0)
|
|
483
|
-
continue;
|
|
484
|
-
seq.items = seq.items.filter((n) => !back.includes(n));
|
|
485
|
-
for (const n of back)
|
|
486
|
-
active.add(n);
|
|
487
|
-
a.dirty = true;
|
|
488
|
-
}
|
|
466
|
+
reopenArchived(this.archives ?? [], this.doc.get('lots'), () => this.lots().map((l) => l.id));
|
|
489
467
|
}
|
|
490
468
|
/**
|
|
491
469
|
* `raf archive` : déplace les lots done et dropped du fichier du plan (terminés avant `before` s'il est donné) vers le fichier de leur année
|
|
@@ -506,8 +484,13 @@ export class Plan {
|
|
|
506
484
|
// De quoi revenir en arrière en mémoire : les items du plan, ceux de chaque archive déjà chargée, et le nombre d'archives (les nouvelles s'ajoutent à la fin).
|
|
507
485
|
const archives = this.loadArchives();
|
|
508
486
|
const snapshot = { items: [...seq.items], archives: archives.map((a) => ({ a, items: [...archiveLots(a.doc).items], dirty: a.dirty })) };
|
|
487
|
+
// Sous plan.idPattern, le nombre final d'un id ne dit rien de l'ordre : chaque lot archivé garde le lot qui le précède dans le plan
|
|
488
|
+
// PRÉSENTÉ (archivés compris, ordre d'avant ce passage), pas dans le fichier seul — deux passages successifs gardent ainsi l'ordre du plan.
|
|
489
|
+
const merged = this.settings.idPattern === undefined ? [] : this.lots().map((l) => l.id);
|
|
509
490
|
for (const n of nodes) {
|
|
510
491
|
const lot = move.find((l) => l.id === String(n.get('id')));
|
|
492
|
+
if (this.settings.idPattern !== undefined)
|
|
493
|
+
n.set(PLACE_KEY, merged[merged.indexOf(lot.id) - 1] ?? '');
|
|
511
494
|
const target = this.archiveFor(archiveYear(lot, today));
|
|
512
495
|
archiveLots(target.doc).add(n);
|
|
513
496
|
target.dirty = true;
|
|
@@ -563,15 +546,18 @@ export class Plan {
|
|
|
563
546
|
for (const dep of opts.after ?? [])
|
|
564
547
|
if (!known.has(dep))
|
|
565
548
|
throw new RafError(`dépendance inconnue : ${dep}`);
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
549
|
+
const picked = pickId(this.prefix, opts.id, opts.under, this.settings.idPattern, known);
|
|
550
|
+
if ('problem' in picked)
|
|
551
|
+
throw new RafError(picked.problem);
|
|
552
|
+
const { id } = picked;
|
|
569
553
|
const pub = opts.public === undefined ? undefined : cleanPublic(opts.public);
|
|
570
554
|
if (opts.every !== undefined && !(Number.isInteger(opts.every) && opts.every > 0)) {
|
|
571
555
|
throw new RafError(`périodicité invalide : ${opts.every} (un nombre entier de jours, 1 au moins)`);
|
|
572
556
|
}
|
|
573
557
|
const extras = checkedExtras(this.settings.extras, opts.extras);
|
|
574
558
|
const entry = { id, title };
|
|
559
|
+
if (opts.under !== undefined)
|
|
560
|
+
entry.parent = opts.under;
|
|
575
561
|
if (pub !== undefined)
|
|
576
562
|
entry.public = pub;
|
|
577
563
|
Object.assign(entry, { status: 'todo', estimate: opts.estimate ?? 1 }, trueFlags({ quickwin: opts.quickwin, visible: opts.visible }));
|
|
@@ -595,6 +581,15 @@ export class Plan {
|
|
|
595
581
|
this.doc.get('lots').add(node);
|
|
596
582
|
return id;
|
|
597
583
|
}
|
|
584
|
+
/** Remplace l'estimation (en jours) d'un lot de tout statut, archivé compris ; jamais sur une sous-tâche. */
|
|
585
|
+
setEstimate(ref, days) {
|
|
586
|
+
this.writable();
|
|
587
|
+
if (ref.includes('/'))
|
|
588
|
+
throw new RafError(`l'estimation se pose sur un lot, pas une sous-tâche : ${ref}`);
|
|
589
|
+
if (!(Number.isFinite(days) && days > 0))
|
|
590
|
+
throw new RafError(`estimation invalide : ${days} (un nombre de jours > 0)`);
|
|
591
|
+
this.lotNode(ref).set('estimate', days);
|
|
592
|
+
}
|
|
598
593
|
/** Déclare les dépôts voisins d'un lot (clé `repos:`) : un chemin seul, ou `{ path, cite }` quand le voisin est partagé. */
|
|
599
594
|
setRepos(lotId, repos) {
|
|
600
595
|
this.writable();
|
package/dist/ref/command.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { join, resolve } from 'node:path';
|
|
5
4
|
import { RafError } from '../plan.js';
|
|
6
5
|
import { AGENTS_DIR } from '../skills.js';
|
|
7
|
-
import { realClaude, readAgents } from '../orchestrate/launch.js';
|
|
6
|
+
import { claudeConfigDir, realClaude, readAgents } from '../orchestrate/launch.js';
|
|
8
7
|
import { add } from './add.js';
|
|
9
8
|
import { analyse } from './analyse.js';
|
|
10
9
|
import { checkInventaire, readInventaire, summaryInventaire } from './check.js';
|
|
@@ -33,7 +32,7 @@ export function refCommand(rest, values, root, io) {
|
|
|
33
32
|
}
|
|
34
33
|
if (verb === 'analyse' && args.length === 1) {
|
|
35
34
|
const claude = realClaude(io.env.CADENCE_CLAUDE_BIN || 'claude', io.env);
|
|
36
|
-
return analyse({ document: args[0], slug: values.slug, sections: values.sections, model: values.model, modelB: values['model-b'], double: values.double === true, seuil: values.seuil, env: io.env, cwd: io.cwd, refsDir, configPath, claudeHome: io.env
|
|
35
|
+
return analyse({ document: args[0], slug: values.slug, sections: values.sections, model: values.model, modelB: values['model-b'], double: values.double === true, seuil: values.seuil, env: io.env, cwd: io.cwd, refsDir, configPath, claudeHome: claudeConfigDir(io.env), now: io.now(), out: io.out, err: io.err, claude, agents: readAgents(AGENTS_DIR) });
|
|
37
36
|
}
|
|
38
37
|
if (verb === 'measure' && args.length === 3) {
|
|
39
38
|
measure({ slug: args[0], page: args[1], zone: args[2], quoi: values.quoi, largeur: values.largeur, env: io.env, refsDir, json: values.json === true, out: io.out });
|
package/dist/resources.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { readYamlFile } from './yaml-keys.js';
|
|
4
4
|
import { isOpen, RafError, RESOURCE_NAME, RESOURCES_BAD_LIST } from './plan.js';
|
|
5
5
|
/**
|
|
6
6
|
* Clé `resources:` du cadence.yaml le plus proche en remontant depuis le dossier parent de `root` : le premier fichier qui porte la clé
|
|
@@ -12,13 +12,7 @@ export function readResources(root) {
|
|
|
12
12
|
for (let dir = dirname(root);; dir = dirname(dir)) {
|
|
13
13
|
const file = join(dir, 'cadence.yaml');
|
|
14
14
|
if (existsSync(file) && statSync(file).isFile()) {
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
18
|
-
}
|
|
19
|
-
catch (e) {
|
|
20
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
21
|
-
}
|
|
15
|
+
const raw = readYamlFile(file);
|
|
22
16
|
if (raw != null && typeof raw === 'object' && Object.hasOwn(raw, 'resources')) {
|
|
23
17
|
const list = raw.resources ?? [];
|
|
24
18
|
if (!Array.isArray(list) || !list.every((n) => typeof n === 'string' && RESOURCE_NAME.test(n)))
|
package/dist/state.js
CHANGED
|
@@ -52,7 +52,11 @@ export function writeSessionMark(dir, mark) {
|
|
|
52
52
|
export function lockPath(dir) {
|
|
53
53
|
return join(dir, 'deliver.lock');
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Lit un fichier de verrou : null s'il est absent ou illisible du système de fichiers ; sinon son contenu mis en forme par `fromRaw` et l'âge du fichier,
|
|
57
|
+
* ou `broken` (marqué `unreadable`) quand ce n'est pas du JSON. Commun au verrou de livraison et à celui de l'orchestration.
|
|
58
|
+
*/
|
|
59
|
+
export function readLockJson(file, fromRaw, broken) {
|
|
56
60
|
let text;
|
|
57
61
|
let ageMs;
|
|
58
62
|
try {
|
|
@@ -63,13 +67,13 @@ function readLockFile(file) {
|
|
|
63
67
|
return null;
|
|
64
68
|
}
|
|
65
69
|
try {
|
|
66
|
-
|
|
67
|
-
return { pid: Number(raw.pid), sha: String(raw.sha), started: String(raw.started), ageMs };
|
|
70
|
+
return { ...fromRaw(JSON.parse(text)), ageMs };
|
|
68
71
|
}
|
|
69
72
|
catch {
|
|
70
|
-
return {
|
|
73
|
+
return { ...broken, unreadable: true, ageMs };
|
|
71
74
|
}
|
|
72
75
|
}
|
|
76
|
+
const readLockFile = (file) => readLockJson(file, (raw) => ({ pid: Number(raw.pid), sha: String(raw.sha), started: String(raw.started) }), { pid: 0, sha: '?', started: '?' });
|
|
73
77
|
export function readLock(dir) {
|
|
74
78
|
return readLockFile(lockPath(dir));
|
|
75
79
|
}
|
|
@@ -282,7 +286,3 @@ export function readFailedEntries(dir) {
|
|
|
282
286
|
return [];
|
|
283
287
|
}
|
|
284
288
|
}
|
|
285
|
-
/** Les jours de `deliveries-failed.log` (une ligne par livraison rouge), dans l'ordre du fichier. */
|
|
286
|
-
export function readFailedDeliveries(dir) {
|
|
287
|
-
return readFailedEntries(dir).map((e) => e.day);
|
|
288
|
-
}
|