@roomi-fields/grillhouse 0.0.0-stage → 0.23.1
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/plugin.json +9 -0
- package/METACADRE.md +41 -0
- package/agents/developpeur.md +10 -0
- package/agents/explorateur.md +9 -0
- package/agents/questionneur.md +11 -0
- package/agents/referent-technique.md +9 -0
- package/agents/regulateur.md +11 -0
- package/agents/relecteur.md +9 -0
- package/agents/testwriter.md +10 -0
- package/bin/grillhouse.mjs +125 -0
- package/cadre/templates/projet/.claude/settings.json +30 -0
- package/cadre/templates/projet/.editorconfig +47 -0
- package/cadre/templates/projet/.env.example +57 -0
- package/cadre/templates/projet/.eslintrc.cjs +56 -0
- package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
- package/cadre/templates/projet/.mcp.json +13 -0
- package/cadre/templates/projet/.prettierrc +11 -0
- package/cadre/templates/projet/CHANGELOG.md +61 -0
- package/cadre/templates/projet/CLAUDE.md +46 -0
- package/cadre/templates/projet/CONTRIBUTING.md +243 -0
- package/cadre/templates/projet/CREDITS.md +41 -0
- package/cadre/templates/projet/README.md +152 -0
- package/cadre/templates/projet/_gitignore +91 -0
- package/cadre/templates/projet/_npmignore +46 -0
- package/cadre/templates/projet/deployment/README.md +98 -0
- package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
- package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
- package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
- package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
- package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
- package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
- package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
- package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
- package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
- package/cadre/templates/projet/ecosystem.config.cjs +73 -0
- package/cadre/templates/projet/package.json +90 -0
- package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
- package/cadre/templates/projet/src/config.ts +68 -0
- package/cadre/templates/projet/src/errors.ts +102 -0
- package/cadre/templates/projet/src/index.ts +46 -0
- package/cadre/templates/projet/src/services/.gitkeep +8 -0
- package/cadre/templates/projet/src/types/index.ts +24 -0
- package/cadre/templates/projet/src/utils/logger.ts +106 -0
- package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
- package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
- package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
- package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
- package/cadre/templates/projet/tsconfig.eslint.json +9 -0
- package/cadre/templates/projet/tsconfig.json +41 -0
- package/cadre/templates/projet/tsdoc.json +6 -0
- package/cadre/templates/projet/vitest.config.ts +44 -0
- package/cadre/templates/turbo/paquets.json +29 -0
- package/cadre/templates/turbo/sans-paquets.json +28 -0
- package/charte.md +107 -0
- package/docs/ARCHITECTURE.md +90 -0
- package/hooks/crochet.mjs +33 -0
- package/hooks/hooks.json +58 -0
- package/hooks/register.tsx +558 -0
- package/package.json +75 -4
- package/scripts/avancement.mjs +244 -0
- package/scripts/bilan.mjs +523 -0
- package/scripts/branche-de-nuit.mjs +59 -0
- package/scripts/cadre.mjs +109 -0
- package/scripts/circuit.mjs +4375 -0
- package/scripts/code-mort.mjs +371 -0
- package/scripts/composants-affectes.mjs +598 -0
- package/scripts/consommateurs-arbitres.mjs +192 -0
- package/scripts/consommateurs.mjs +310 -0
- package/scripts/crochet.mjs +24 -0
- package/scripts/crochets.mjs +135 -0
- package/scripts/dernier-vert.mjs +208 -0
- package/scripts/dette.mjs +181 -0
- package/scripts/droits.mjs +354 -0
- package/scripts/enveloppe/codes.mjs +17 -0
- package/scripts/enveloppe/composant.mjs +11 -0
- package/scripts/enveloppe/enveloppe.mjs +628 -0
- package/scripts/enveloppe/lancer.sh +311 -0
- package/scripts/etapes.mjs +323 -0
- package/scripts/etat-du-circuit.mjs +104 -0
- package/scripts/frontieres.mjs +456 -0
- package/scripts/gardes-du-cadre.mjs +72 -0
- package/scripts/greffons.mjs +338 -0
- package/scripts/grillhouse-maj.mjs +514 -0
- package/scripts/index-interfaces.mjs +293 -0
- package/scripts/init.mjs +117 -0
- package/scripts/integration/file-attente.mjs +61 -0
- package/scripts/integration/file.mjs +494 -0
- package/scripts/integration/integrer.mjs +381 -0
- package/scripts/integration/rejeu.mjs +139 -0
- package/scripts/integration/verdict.mjs +13 -0
- package/scripts/interfaces-contre-code.mjs +547 -0
- package/scripts/ligne-etat/defaut.sh +110 -0
- package/scripts/ligne-etat/ligne-etat.mjs +93 -0
- package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
- package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
- package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
- package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
- package/scripts/migrations.mjs +43 -0
- package/scripts/nuit.sh +269 -0
- package/scripts/paquets.mjs +79 -0
- package/scripts/poussee-cles.mjs +46 -0
- package/scripts/preparation.mjs +84 -0
- package/scripts/references-par-les-types.mjs +420 -0
- package/scripts/registre.mjs +336 -0
- package/scripts/resultats.mjs +580 -0
- package/scripts/resumes.mjs +57 -0
- package/scripts/retirer-copie.sh +127 -0
- package/scripts/session-start.sh +203 -0
- package/scripts/sortie.mjs +155 -0
- package/scripts/sous-modules.sh +316 -0
- package/scripts/structure-guard.sh +27 -0
- package/scripts/superviseur.mjs +102 -0
- package/scripts/tableau.mjs +1886 -0
- package/scripts/tests.mjs +236 -0
- package/scripts/ticket.sh +23 -0
- package/scripts/turbo-des-paquets.mjs +899 -0
- package/scripts/un-commit-par-ticket.mjs +212 -0
- package/scripts/verifier.mjs +469 -0
- package/scripts/verrou-de-fichier.mjs +130 -0
- package/scripts/verrou-de-machine.mjs +211 -0
- package/scripts/verrous/commande.mjs +180 -0
- package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
- package/scripts/verrous/verrou.mjs +165 -0
- package/scripts/vitest-config/avancement.mjs +39 -0
- package/scripts/vitest-config/avancement.reporter.mjs +56 -0
- package/scripts/vitest-config/base-de-passe.mjs +128 -0
- package/scripts/vitest-config/base.d.ts +6 -0
- package/scripts/vitest-config/base.mjs +87 -0
- package/scripts/vitest-config/changements.mjs +173 -0
- package/scripts/vitest-config/cibles.mjs +207 -0
- package/scripts/vitest-config/duree.reporter.mjs +98 -0
- package/scripts/vitest-config/machine.setup.mjs +14 -0
- package/scripts/vitest-config/mesure.d.ts +6 -0
- package/scripts/vitest-config/mesure.mjs +13 -0
- package/scripts/vitest-config/mesure.setup.mjs +12 -0
- package/scripts/vitest-config/pression.mjs +90 -0
- package/scripts/vitest-config/resultats.mjs +23 -0
- package/scripts/vitest-config/resultats.setup.mjs +19 -0
- package/scripts/vitest.nuit.config.mjs +26 -0
- package/skills/developpeur/SKILL.md +175 -0
- package/skills/explorateur/SKILL.md +84 -0
- package/skills/grill/SKILL.md +238 -0
- package/skills/grill/references/releve-structurel.md +38 -0
- package/skills/mesure/SKILL.md +168 -0
- package/skills/pitmaster/SKILL.md +382 -0
- package/skills/pitmaster/references/cadre.md +119 -0
- package/skills/pitmaster/references/consigne-agent.md +308 -0
- package/skills/pitmaster/references/erreurs.md +25 -0
- package/skills/pitmaster/references/tour.md +124 -0
- package/skills/questionneur/SKILL.md +64 -0
- package/skills/redacteur/SKILL.md +84 -0
- package/skills/redacteur/references/documents-d-un-composant.md +105 -0
- package/skills/referent-technique/SKILL.md +145 -0
- package/skills/regulateur/SKILL.md +220 -0
- package/skills/release/SKILL.md +134 -0
- package/skills/release/scripts/update-version.cjs +92 -0
- package/skills/relecteur/SKILL.md +152 -0
- package/skills/relecteur/references/LICENSE-MIT +30 -0
- package/skills/relecteur/references/SOURCE.md +8 -0
- package/skills/relecteur/references/claims-check.md +14 -0
- package/skills/relecteur/references/deletion-check.md +14 -0
- package/skills/relecteur/references/edge-case-hunter.md +80 -0
- package/skills/relecteur/references/verification-gap.md +113 -0
- package/skills/testwriter/SKILL.md +187 -0
- package/types/index.d.ts +56 -0
- package/README.md +0 -3
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The written interface of each component against its code.
|
|
3
|
+
//
|
|
4
|
+
// - The public API of a component is reported by API Extractor from its built declarations:
|
|
5
|
+
// `docs/INTERFACE.api.md`, kept in the repository. The check refuses an API that changed
|
|
6
|
+
// without its report. The interface is compared to the built API (the documentation model
|
|
7
|
+
// API Extractor builds from the code), not to the recorded report. A package's entry is its main
|
|
8
|
+
// entry (`types`, or `exports["."].types`); its other entries (test material) are not its API. A
|
|
9
|
+
// module of `src/` has `<types directory>/<module>/index.d.ts`. Types of a neighbour stay
|
|
10
|
+
// references to it.
|
|
11
|
+
// - The contract of each export lives in its TSDoc comment, in the code: a summary, one `@param`
|
|
12
|
+
// per parameter (a destructured parameter by its position, under any name), `@returns` for a
|
|
13
|
+
// function that returns a value (not void, undefined, never or Promise<void>), and one
|
|
14
|
+
// `@faute CODE` per fault it returns, the code being one of the « ## Les fautes » catalogue. The
|
|
15
|
+
// frame's `tsdoc.json` (repository root) declares `@faute` and is read for every component; an
|
|
16
|
+
// unknown or malformed TSDoc tag is refused. Every export is judged on its own rubrics, each
|
|
17
|
+
// overload signature included; the members of a class or interface are not.
|
|
18
|
+
// - `docs/INTERFACE.md` has two parts: the hand-written one, never touched, and the generated one
|
|
19
|
+
// between the marks `<!-- généré: début -->` and `<!-- généré: fin -->`, one entry per export
|
|
20
|
+
// (title, signature, summary, parameters, return, faults; an interface or a class lists its
|
|
21
|
+
// members). The check refuses a generated part that differs from what `--ecrire` writes, and
|
|
22
|
+
// marks that are missing, alone, doubled or out of order. `--ecrire` adds the marks at the end
|
|
23
|
+
// of an interface that has none, writes the part even when a rubric is missing, and then
|
|
24
|
+
// returns the refusals of the check.
|
|
25
|
+
// - The root interface `docs/INTERFACE.md` is not a component's: it needs no marks.
|
|
26
|
+
// - A package whose `exports` has subpaths only (`./test-fixtures`, the shared test material) and
|
|
27
|
+
// no `.` entry has no public API, nor has a package without `exports`, `main`, `types` or
|
|
28
|
+
// `typings` (a package of tools, its commands in `bin`): no built entry, no report, no generated
|
|
29
|
+
// part nor marks are asked of it, and nothing is reported; its « ## Les fautes », if any, is
|
|
30
|
+
// checked against its code.
|
|
31
|
+
// - When the interface has a « ## Les fautes » section, its codes (`PARSE_EXPECTED`…) are exactly
|
|
32
|
+
// the codes with the same prefixes found in the component's production code, and each is named
|
|
33
|
+
// by an `@faute` of the component.
|
|
34
|
+
//
|
|
35
|
+
// npx grillhouse interfaces-contre-code checks every component (run after the build)
|
|
36
|
+
// npm run api writes the API reports and the generated parts
|
|
37
|
+
// (`--ecrire`; run after the build)
|
|
38
|
+
//
|
|
39
|
+
// A missing report, or an API changed without its report, is refused with `npm run api`, the
|
|
40
|
+
// command that writes it; API Extractor's own warning on that report is not relayed (the refusal
|
|
41
|
+
// says what to do).
|
|
42
|
+
//
|
|
43
|
+
// A refusal inscribed in the project's debt (`grillhouse-dette.json`, see `dette.mjs`) passes, keyed
|
|
44
|
+
// by the component's interface and the rubric (`resume`, `param`, `param-orphelin`, `returns`,
|
|
45
|
+
// `faute-mal-formee`, `faute-hors-catalogue`, `faute-non-nommee`, `faute-non-produite`,
|
|
46
|
+
// `faute-non-listee`), or by the declaration file and TSDoc's message id for a TSDoc message.
|
|
47
|
+
//
|
|
48
|
+
// In the envelope of a component (`GRILLHOUSE_ENVELOPPE`, scripts/enveloppe/composant.mjs), the
|
|
49
|
+
// check and `npm run api` read and write that component alone: its neighbours' source is hidden,
|
|
50
|
+
// their published parts read-only, and the integration checks every component on the whole
|
|
51
|
+
// repository. The component's build comes first, as outside (`npm test -- <its files>` builds it).
|
|
52
|
+
//
|
|
53
|
+
// A component without `docs/INTERFACE.md` is reported and skipped. A project that prefers a
|
|
54
|
+
// bench per component calls `verifierComposant(racine, composant)` from its test.
|
|
55
|
+
import { createRequire } from 'node:module';
|
|
56
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
57
|
+
import { tmpdir } from 'node:os';
|
|
58
|
+
import path from 'node:path';
|
|
59
|
+
import { pathToFileURL } from 'node:url';
|
|
60
|
+
import { composants, lireSiPresent, parcourir } from './consommateurs.mjs';
|
|
61
|
+
import { composantDeLEnveloppe } from './enveloppe/composant.mjs';
|
|
62
|
+
import { trancher } from './dette.mjs';
|
|
63
|
+
|
|
64
|
+
const require = createRequire(import.meta.url);
|
|
65
|
+
const SOURCE = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|svelte|vue)$/;
|
|
66
|
+
const isTest = rel =>
|
|
67
|
+
/(^|\/)(test|tests|__tests__)\//.test(rel) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(rel);
|
|
68
|
+
const CODE = /\b[A-Z][A-Z0-9]*_[A-Z0-9_]*[A-Z0-9]\b/g;
|
|
69
|
+
|
|
70
|
+
const lireJson = p => JSON.parse(readFileSync(p, 'utf8'));
|
|
71
|
+
|
|
72
|
+
// Whether a component has no public API: its package exports subpaths only (`./test-fixtures`…),
|
|
73
|
+
// no `.` entry, or it declares no entry at all, neither `exports`, `main`, `types` nor `typings`
|
|
74
|
+
// (a package of tools, its commands in `bin`). API Extractor reads one entry point; such a package
|
|
75
|
+
// has none.
|
|
76
|
+
export function sansApi(c) {
|
|
77
|
+
const pj = path.join(c.dir, 'package.json');
|
|
78
|
+
if (!existsSync(pj)) return false;
|
|
79
|
+
const p = lireJson(pj);
|
|
80
|
+
const exp = p.exports;
|
|
81
|
+
if (exp === undefined) return [p.main, p.types, p.typings].every(e => e === undefined);
|
|
82
|
+
if (!exp || typeof exp !== 'object' || Array.isArray(exp)) return false;
|
|
83
|
+
const cles = Object.keys(exp);
|
|
84
|
+
return cles.length > 0 && cles.every(k => k.startsWith('.')) && !cles.includes('.');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The built entry of a component's public API, or null.
|
|
88
|
+
export function entree(racine, c) {
|
|
89
|
+
const pj = path.join(c.dir, 'package.json');
|
|
90
|
+
if (existsSync(pj)) {
|
|
91
|
+
const p = lireJson(pj);
|
|
92
|
+
const point = p.exports?.['.'];
|
|
93
|
+
const types =
|
|
94
|
+
(typeof point === 'object' && (point.types ?? point.import?.types)) || p.types || p.typings;
|
|
95
|
+
return types ? path.join(c.dir, types) : null;
|
|
96
|
+
}
|
|
97
|
+
const racinePj = path.join(racine, 'package.json');
|
|
98
|
+
const types = existsSync(racinePj) ? lireJson(racinePj).types : null;
|
|
99
|
+
if (!types) return null;
|
|
100
|
+
return path.join(racine, path.dirname(types), path.basename(c.dir), 'index.d.ts');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The fault codes an interface text declares in « ## Les fautes » of its hand-written part (null
|
|
104
|
+
// without the section); the generated part and code blocks are not read.
|
|
105
|
+
export function declare(texte) {
|
|
106
|
+
let fautes = null;
|
|
107
|
+
let section = null;
|
|
108
|
+
let bloc = false;
|
|
109
|
+
for (const l of ecrite(texte)) {
|
|
110
|
+
if (/^\s*(```|~~~)/.test(l)) bloc = !bloc;
|
|
111
|
+
const titre = !bloc && /^(#{2,3}) (.+)$/.exec(l);
|
|
112
|
+
if (titre) {
|
|
113
|
+
section = titre[1] === '##' ? titre[2].trim() : section;
|
|
114
|
+
if (section === 'Les fautes' && fautes === null) fautes = new Set();
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (section === 'Les fautes') for (const m of l.matchAll(CODE)) fautes.add(m[0]);
|
|
118
|
+
}
|
|
119
|
+
return { fautes };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The codes with one of <prefixes> found in the production code under <dir>, as `parcourir`
|
|
123
|
+
// walks it.
|
|
124
|
+
function codesDuCode(dir, racine, prefixes) {
|
|
125
|
+
const trouves = new Set();
|
|
126
|
+
const nom = p => path.basename(p);
|
|
127
|
+
const sources = parcourir(
|
|
128
|
+
dir,
|
|
129
|
+
p => ['node_modules', 'dist', 'docs'].includes(nom(p)) || nom(p).startsWith('.')
|
|
130
|
+
).filter(p => SOURCE.test(nom(p)) && !isTest(path.relative(racine, p)));
|
|
131
|
+
for (const p of sources) {
|
|
132
|
+
for (const m of (lireSiPresent(p) ?? '').matchAll(CODE)) {
|
|
133
|
+
if (prefixes.some(pr => m[0].startsWith(pr))) trouves.add(m[0]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return trouves;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Runs API Extractor on one component: whether the repository's report is up to date, the exports
|
|
140
|
+
// of the documentation model built from the code (whether or not the report exists) and the TSDoc
|
|
141
|
+
// messages of its comments.
|
|
142
|
+
function rapportApi(racine, c, entreeDts, ecrire, tsdocConfigFile) {
|
|
143
|
+
const { Extractor, ExtractorConfig } = require('@microsoft/api-extractor');
|
|
144
|
+
const { ApiModel } = require('@microsoft/api-extractor-model');
|
|
145
|
+
const pj = existsSync(path.join(c.dir, 'package.json'))
|
|
146
|
+
? path.join(c.dir, 'package.json')
|
|
147
|
+
: path.join(racine, 'package.json');
|
|
148
|
+
const tsconfig = [path.join(c.dir, 'tsconfig.json'), path.join(racine, 'tsconfig.json')].find(
|
|
149
|
+
existsSync
|
|
150
|
+
);
|
|
151
|
+
const tmp = mkdtempSync(path.join(tmpdir(), 'api-'));
|
|
152
|
+
const messages = [];
|
|
153
|
+
const tsdoc = new Map();
|
|
154
|
+
try {
|
|
155
|
+
const modele = path.join(tmp, 'modele.api.json');
|
|
156
|
+
const config = ExtractorConfig.prepare({
|
|
157
|
+
configObject: {
|
|
158
|
+
projectFolder: path.dirname(pj),
|
|
159
|
+
mainEntryPointFilePath: entreeDts,
|
|
160
|
+
compiler: tsconfig ? { tsconfigFilePath: tsconfig } : undefined,
|
|
161
|
+
apiReport: {
|
|
162
|
+
enabled: true,
|
|
163
|
+
reportFolder: path.join(c.dir, 'docs'),
|
|
164
|
+
reportTempFolder: tmp,
|
|
165
|
+
reportFileName: 'INTERFACE',
|
|
166
|
+
},
|
|
167
|
+
docModel: { enabled: true, apiJsonFilePath: modele },
|
|
168
|
+
dtsRollup: { enabled: false },
|
|
169
|
+
tsdocMetadata: { enabled: false },
|
|
170
|
+
messages: {
|
|
171
|
+
extractorMessageReporting: { 'ae-missing-release-tag': { logLevel: 'none' } },
|
|
172
|
+
tsdocMessageReporting: { default: { logLevel: 'warning' } },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
packageJsonFullPath: pj,
|
|
176
|
+
tsdocConfigFile,
|
|
177
|
+
});
|
|
178
|
+
const resultat = Extractor.invoke(config, {
|
|
179
|
+
localBuild: ecrire,
|
|
180
|
+
messageCallback: m => {
|
|
181
|
+
const duRapport = String(m.messageId).startsWith('console-api-report');
|
|
182
|
+
if (m.category === 'TSDoc') {
|
|
183
|
+
const fichier = m.sourceFilePath ? path.relative(racine, m.sourceFilePath) : null;
|
|
184
|
+
const lieu = fichier ? `${fichier}:${m.sourceFileLine ?? 1}: ` : '';
|
|
185
|
+
tsdoc.set(`${lieu}${m.text}`, {
|
|
186
|
+
fichier,
|
|
187
|
+
...(fichier ? { ligne: m.sourceFileLine ?? 1 } : {}),
|
|
188
|
+
regle: m.messageId,
|
|
189
|
+
});
|
|
190
|
+
m.logLevel = 'none';
|
|
191
|
+
} else if ((m.logLevel === 'error' || m.logLevel === 'warning') && (ecrire || !duRapport)) {
|
|
192
|
+
messages.push(m.text);
|
|
193
|
+
}
|
|
194
|
+
m.handled = true;
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
const paquet = new ApiModel().loadPackage(modele);
|
|
198
|
+
return {
|
|
199
|
+
ajour: resultat.succeeded && !resultat.apiReportChanged,
|
|
200
|
+
messages,
|
|
201
|
+
tsdoc: [...tsdoc].map(([texte, lieu]) => ({ texte, ...lieu })),
|
|
202
|
+
exports: paquet.entryPoints.flatMap(e => [...e.members]),
|
|
203
|
+
};
|
|
204
|
+
} finally {
|
|
205
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---- The TSDoc contract of an export ----
|
|
210
|
+
|
|
211
|
+
const RETOUR_VIDE = new Set(['void', 'undefined', 'never', 'Promise<void>']);
|
|
212
|
+
const CODE_SEUL = new RegExp(`^${CODE.source}$`);
|
|
213
|
+
|
|
214
|
+
// The text of a TSDoc node, its paragraphs apart by a blank line, each on one line.
|
|
215
|
+
function texteDoc(noeud) {
|
|
216
|
+
if (!noeud) return '';
|
|
217
|
+
const feuilles = n =>
|
|
218
|
+
n.kind === 'Excerpt' ? n.content.toString() : n.getChildNodes().map(feuilles).join('');
|
|
219
|
+
const paragraphes = noeud.kind === 'Section' ? noeud.getChildNodes() : [noeud];
|
|
220
|
+
return paragraphes
|
|
221
|
+
.map(p => feuilles(p).replace(/\s+/g, ' ').trim())
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.join('\n\n');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// The parameters of a function as the model gives them. API Extractor lists a destructuring pattern
|
|
227
|
+
// (`{ a }`, `[a]`) as a parameter without type, then the parameter itself under the name `input`,
|
|
228
|
+
// `input2`…; the pattern entry is dropped and the parameter that follows it is marked destructured
|
|
229
|
+
// (documented by its position, under any name). A parameter named `input` after no pattern is a
|
|
230
|
+
// named parameter.
|
|
231
|
+
function parametresDe(item) {
|
|
232
|
+
const sortie = [];
|
|
233
|
+
let motif = false;
|
|
234
|
+
for (const p of item.parameters) {
|
|
235
|
+
if (p.parameterTypeExcerpt.isEmpty && /^[{[]/.test(p.name)) {
|
|
236
|
+
motif = true;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
sortie.push({ name: p.name, destructure: motif });
|
|
240
|
+
motif = false;
|
|
241
|
+
}
|
|
242
|
+
return sortie;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// A function declared with `function` (API Extractor also models a function-typed constant as a
|
|
246
|
+
// function; a constant is a variable and takes its summary only).
|
|
247
|
+
const estFonction = item =>
|
|
248
|
+
item.kind === 'Function' &&
|
|
249
|
+
/^(?:(?:export|declare|default|async)\s+)*function\b/.test(item.getExcerptWithModifiers().trim());
|
|
250
|
+
|
|
251
|
+
// The rubrics of an export: its summary, its parameters, its return and its faults.
|
|
252
|
+
function contratDe(item) {
|
|
253
|
+
const doc = item.tsdocComment;
|
|
254
|
+
const blocs = doc?.params.blocks ?? [];
|
|
255
|
+
return {
|
|
256
|
+
resume: texteDoc(doc?.summarySection),
|
|
257
|
+
params: blocs.map(b => ({ nom: b.parameterName, texte: texteDoc(b.content) })),
|
|
258
|
+
retour: doc?.returnsBlock ? texteDoc(doc.returnsBlock.content) : null,
|
|
259
|
+
fautes: (doc?.customBlocks ?? [])
|
|
260
|
+
.filter(b => b.blockTag.tagName === '@faute')
|
|
261
|
+
.map(b => texteDoc(b.content)),
|
|
262
|
+
parametres: estFonction(item) ? parametresDe(item) : [],
|
|
263
|
+
rendUneValeur:
|
|
264
|
+
item.kind === 'Function' && !RETOUR_VIDE.has(item.returnTypeExcerpt.text.replace(/\s+/g, '')),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// The code an `@faute` text starts with, or null when its first word is not a code.
|
|
269
|
+
const codeDeFaute = texte => {
|
|
270
|
+
const mot = texte.split(/\s+/)[0];
|
|
271
|
+
return CODE_SEUL.test(mot) ? mot : null;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// The `@faute` texts <textes> of one element judged against the catalogue <fautes>: refusals
|
|
275
|
+
// (`{ regle, texte }`) are pushed on <refus>, the codes named on <codes>.
|
|
276
|
+
function fautesJugees(nom, textes, fautes, refus, codes) {
|
|
277
|
+
for (const f of textes) {
|
|
278
|
+
const code = codeDeFaute(f);
|
|
279
|
+
if (!code) {
|
|
280
|
+
refus.push({
|
|
281
|
+
regle: 'faute-mal-formee',
|
|
282
|
+
texte: `${nom} : un @faute est mal formé (il commence par un code, ex. @faute CODE).`,
|
|
283
|
+
});
|
|
284
|
+
} else if (!fautes?.has(code)) {
|
|
285
|
+
refus.push({
|
|
286
|
+
regle: 'faute-hors-catalogue',
|
|
287
|
+
texte: `${nom} : le @faute ${code} n'est pas au catalogue « ## Les fautes ».`,
|
|
288
|
+
});
|
|
289
|
+
} else codes.add(code);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// The refusals for one export against its contract (`{ regle, texte }`), with the fault codes it
|
|
294
|
+
// and its members name.
|
|
295
|
+
function jugerExport(item, fautes) {
|
|
296
|
+
const refus = [];
|
|
297
|
+
const nom = `\`${item.displayName}\``;
|
|
298
|
+
const k = contratDe(item);
|
|
299
|
+
if (!k.resume) refus.push({ regle: 'resume', texte: `${nom} n'a pas de résumé TSDoc.` });
|
|
300
|
+
const codes = new Set();
|
|
301
|
+
if (estFonction(item)) {
|
|
302
|
+
const noms = new Set(k.parametres.filter(p => !p.destructure).map(p => p.name));
|
|
303
|
+
const prises = new Set();
|
|
304
|
+
k.parametres.forEach((p, i) => {
|
|
305
|
+
const parNom = p.destructure ? -1 : k.params.findIndex(b => b.nom === p.name);
|
|
306
|
+
if (parNom >= 0) prises.add(parNom);
|
|
307
|
+
else if (!p.destructure)
|
|
308
|
+
refus.push({
|
|
309
|
+
regle: 'param',
|
|
310
|
+
texte: `${nom} : le paramètre « ${p.name} » n'a pas de @param.`,
|
|
311
|
+
});
|
|
312
|
+
else if (!k.params[i] || noms.has(k.params[i].nom))
|
|
313
|
+
refus.push({
|
|
314
|
+
regle: 'param',
|
|
315
|
+
texte: `${nom} : le paramètre déstructuré en position ${i + 1} n'a pas son @param à cette position.`,
|
|
316
|
+
});
|
|
317
|
+
else prises.add(i);
|
|
318
|
+
});
|
|
319
|
+
k.params.forEach((b, i) => {
|
|
320
|
+
if (!prises.has(i))
|
|
321
|
+
refus.push({
|
|
322
|
+
regle: 'param-orphelin',
|
|
323
|
+
texte: `${nom} : le @param « ${b.nom} » ne nomme aucun paramètre.`,
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
if (k.rendUneValeur && k.retour === null)
|
|
327
|
+
refus.push({ regle: 'returns', texte: `${nom} rend une valeur mais n'a pas de @returns.` });
|
|
328
|
+
}
|
|
329
|
+
fautesJugees(nom, k.fautes, fautes, refus, codes);
|
|
330
|
+
for (const m of item.members ?? []) {
|
|
331
|
+
const km = contratDe(m);
|
|
332
|
+
fautesJugees(`\`${m.displayName}\``, km.fautes, fautes, refus, codes);
|
|
333
|
+
}
|
|
334
|
+
return { refus, codes };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---- The generated part of docs/INTERFACE.md ----
|
|
338
|
+
|
|
339
|
+
export const DEBUT = '<!-- généré: début -->';
|
|
340
|
+
export const FIN = '<!-- généré: fin -->';
|
|
341
|
+
|
|
342
|
+
// The positions of the two marks in an interface text: { etat: 'ok' | 'absentes' | 'invalides', … }.
|
|
343
|
+
// A mark written in a code block is an example, not a mark.
|
|
344
|
+
export function marques(texte) {
|
|
345
|
+
const lignes = texte.split('\n');
|
|
346
|
+
const debuts = [];
|
|
347
|
+
const fins = [];
|
|
348
|
+
let cloture = null;
|
|
349
|
+
lignes.forEach((l, i) => {
|
|
350
|
+
const f = /^\s*(```|~~~)/.exec(l);
|
|
351
|
+
if (cloture) {
|
|
352
|
+
if (f && f[1] === cloture) cloture = null;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (f) {
|
|
356
|
+
cloture = f[1];
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (l.trim() === DEBUT) debuts.push(i);
|
|
360
|
+
if (l.trim() === FIN) fins.push(i);
|
|
361
|
+
});
|
|
362
|
+
if (!debuts.length && !fins.length) return { etat: 'absentes', lignes };
|
|
363
|
+
if (debuts.length === 1 && fins.length === 1 && debuts[0] < fins[0])
|
|
364
|
+
return { etat: 'ok', lignes, debut: debuts[0], fin: fins[0] };
|
|
365
|
+
return { etat: 'invalides', lignes };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// The lines of an interface text outside its generated part.
|
|
369
|
+
function ecrite(texte) {
|
|
370
|
+
const m = marques(texte);
|
|
371
|
+
return m.etat === 'ok' ? [...m.lignes.slice(0, m.debut), ...m.lignes.slice(m.fin + 1)] : m.lignes;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// The signature of an element on one line; a constant keeps its declaration (`const`, `let`).
|
|
375
|
+
const signature = item => {
|
|
376
|
+
const texte = item.getExcerptWithModifiers().replace(/\s+/g, ' ').replace(/;$/, '').trim();
|
|
377
|
+
if (
|
|
378
|
+
/^(?:(?:export|declare|default|async)\s+)*(?:function|class|interface|type|enum|namespace|const|let|var)\b/.test(
|
|
379
|
+
texte
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
return texte;
|
|
383
|
+
if (item.kind === 'Variable')
|
|
384
|
+
return `export declare ${item.isReadonly ? 'const' : 'let'} ${texte}`;
|
|
385
|
+
return item.kind === 'Function' ? `const ${texte}` : texte;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// The generated entry of one export: title, summary, signature, parameters, return, faults and,
|
|
389
|
+
// for an interface or a class, its members.
|
|
390
|
+
function entreeDe(item) {
|
|
391
|
+
const k = contratDe(item);
|
|
392
|
+
const out = [`### \`${item.displayName}\``, ''];
|
|
393
|
+
if (k.resume) out.push(k.resume, '');
|
|
394
|
+
out.push('```ts', `${signature(item)};`, '```');
|
|
395
|
+
const params = k.params.filter(b => b.texte);
|
|
396
|
+
if (params.length)
|
|
397
|
+
out.push('', '**Paramètres**', '', ...params.map(b => `- \`${b.nom}\` — ${b.texte}`));
|
|
398
|
+
if (k.retour) out.push('', `**Retour** — ${k.retour}`);
|
|
399
|
+
const codes = [...new Set(k.fautes.map(f => f.replace(/\s+/g, ' ')))];
|
|
400
|
+
if (codes.length)
|
|
401
|
+
out.push('', '**Fautes**', '', ...codes.map(f => `- ${f.replace(/^(\S+)/, '`$1`')}`));
|
|
402
|
+
const membres = [...(item.members ?? [])];
|
|
403
|
+
if (membres.length) {
|
|
404
|
+
out.push('', '**Membres**', '');
|
|
405
|
+
for (const m of membres) {
|
|
406
|
+
const resume = texteDoc(m.tsdocComment?.summarySection);
|
|
407
|
+
out.push(`- \`${signature(m)}\`${resume ? ` — ${resume}` : ''}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return out.join('\n');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The text between the marks for <exports>: empty without export.
|
|
414
|
+
const partieGeneree = exports =>
|
|
415
|
+
exports.length ? `\n${exports.map(entreeDe).join('\n\n')}\n\n` : '\n';
|
|
416
|
+
|
|
417
|
+
// The text of an interface with its generated part replaced; the marks are added at the end of a
|
|
418
|
+
// text that has none.
|
|
419
|
+
function avecPartie(m, texte, partie) {
|
|
420
|
+
if (m.etat === 'absentes')
|
|
421
|
+
return `${texte}${texte.endsWith('\n') ? '' : '\n'}\n${DEBUT}${partie}${FIN}\n`;
|
|
422
|
+
const avant = m.lignes.slice(0, m.debut).join('\n');
|
|
423
|
+
const apres = m.lignes.slice(m.fin + 1).join('\n');
|
|
424
|
+
return `${avant}${m.debut ? '\n' : ''}${DEBUT}${partie}${FIN}\n${apres}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// The refusals and reports for one component. A refusal is a finding `{ fichier, regle, message }`
|
|
428
|
+
// the debt (`dette.mjs`) judges, keyed by the component's interface (by the declaration file for a
|
|
429
|
+
// TSDoc message, its rule TSDoc's message id); a refusal that a command or a build repairs (a
|
|
430
|
+
// generated part, an API report, a built entry, the marks, the TSDoc configuration) has no key and
|
|
431
|
+
// is never inscribed.
|
|
432
|
+
export function verifierComposant(racine, c, { ecrire = false } = {}) {
|
|
433
|
+
const refus = [];
|
|
434
|
+
const signaux = [];
|
|
435
|
+
const rel = path.relative(racine, c.dir);
|
|
436
|
+
const iface = path.join(c.dir, 'docs', 'INTERFACE.md');
|
|
437
|
+
const relIface = path.relative(racine, iface);
|
|
438
|
+
// A refusal of the component: inscribable when <regle> is given, keyed by <fichier>.
|
|
439
|
+
const refuser = (texte, regle = null, fichier = relIface, ligne = null) =>
|
|
440
|
+
refus.push(
|
|
441
|
+
regle
|
|
442
|
+
? { fichier, ...(ligne ? { ligne } : {}), regle, message: `${rel} : ${texte}` }
|
|
443
|
+
: { message: `${rel} : ${texte}` }
|
|
444
|
+
);
|
|
445
|
+
if (!existsSync(iface)) {
|
|
446
|
+
signaux.push(`${rel} n'a pas d'interface (docs/INTERFACE.md) : rien n'est vérifié.`);
|
|
447
|
+
return { refus, signaux };
|
|
448
|
+
}
|
|
449
|
+
const texte = readFileSync(iface, 'utf8');
|
|
450
|
+
const { fautes } = declare(texte);
|
|
451
|
+
const m = marques(texte);
|
|
452
|
+
if (sansApi(c)) {
|
|
453
|
+
if (fautes) codesContreCatalogue(c, racine, fautes, refuser);
|
|
454
|
+
return { refus, signaux };
|
|
455
|
+
}
|
|
456
|
+
if (m.etat === 'invalides') {
|
|
457
|
+
refuser(
|
|
458
|
+
`docs/INTERFACE.md doit porter la marque « ${DEBUT} » puis la marque « ${FIN} », chacune une fois ; corrige les marques à la main (npm run api n'écrit rien).`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
const fichierTsdoc = path.join(racine, 'tsdoc.json');
|
|
462
|
+
const dts = entree(racine, c);
|
|
463
|
+
if (!existsSync(fichierTsdoc)) {
|
|
464
|
+
refuser('la configuration TSDoc du cadre (tsdoc.json à la racine) manque.');
|
|
465
|
+
} else if (!dts || !existsSync(dts)) {
|
|
466
|
+
refuser(`son entrée construite (${dts ?? 'types absent'}) manque ; construis d'abord.`);
|
|
467
|
+
} else {
|
|
468
|
+
const { TSDocConfigFile } = require('@microsoft/tsdoc-config');
|
|
469
|
+
const config = TSDocConfigFile.loadFile(fichierTsdoc);
|
|
470
|
+
const api = rapportApi(racine, c, dts, ecrire, config);
|
|
471
|
+
for (const msg of api.messages) signaux.push(`${rel} : ${msg}`);
|
|
472
|
+
for (const t of api.tsdoc) {
|
|
473
|
+
refuser(`TSDoc : ${t.texte}`, t.regle, t.fichier ?? relIface, t.ligne);
|
|
474
|
+
}
|
|
475
|
+
const manque = !ecrire && !existsSync(path.join(c.dir, 'docs', 'INTERFACE.api.md'));
|
|
476
|
+
if (manque) {
|
|
477
|
+
refuser(
|
|
478
|
+
"son rapport docs/INTERFACE.api.md manque ; npm run api l'écrit, après la construction."
|
|
479
|
+
);
|
|
480
|
+
} else if (!api.ajour && !ecrire) {
|
|
481
|
+
refuser(
|
|
482
|
+
"l'API a changé sans son rapport docs/INTERFACE.api.md ; npm run api le régénère, après la construction."
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
const nommees = new Set();
|
|
486
|
+
for (const item of api.exports) {
|
|
487
|
+
const j = jugerExport(item, fautes);
|
|
488
|
+
for (const r of j.refus) refuser(r.texte, r.regle);
|
|
489
|
+
for (const code of j.codes) nommees.add(code);
|
|
490
|
+
}
|
|
491
|
+
for (const f of fautes ?? []) {
|
|
492
|
+
if (!nommees.has(f))
|
|
493
|
+
refuser(`la faute ${f} du catalogue n'est nommée par aucun @faute.`, 'faute-non-nommee');
|
|
494
|
+
}
|
|
495
|
+
if (m.etat !== 'invalides') {
|
|
496
|
+
const attendu = avecPartie(m, texte, partieGeneree(api.exports));
|
|
497
|
+
if (ecrire) {
|
|
498
|
+
if (attendu !== texte) writeFileSync(iface, attendu);
|
|
499
|
+
} else if (attendu !== texte) {
|
|
500
|
+
refuser(
|
|
501
|
+
`la part générée de docs/INTERFACE.md (entre ${DEBUT} et ${FIN}) n'est pas celle du code ; npm run api la refait.`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (fautes) codesContreCatalogue(c, racine, fautes, refuser);
|
|
507
|
+
return { refus, signaux };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// The catalogue <fautes> against the codes of the component's production code, refused through
|
|
511
|
+
// <refuser>.
|
|
512
|
+
function codesContreCatalogue(c, racine, fautes, refuser) {
|
|
513
|
+
const prefixes = [...new Set([...fautes].map(f => f.slice(0, f.indexOf('_') + 1)))];
|
|
514
|
+
const dansLeCode = codesDuCode(c.dir, racine, prefixes);
|
|
515
|
+
for (const f of fautes) {
|
|
516
|
+
if (!dansLeCode.has(f))
|
|
517
|
+
refuser(`la faute ${f} est listée, mais le code ne la produit pas.`, 'faute-non-produite');
|
|
518
|
+
}
|
|
519
|
+
for (const f of dansLeCode) {
|
|
520
|
+
if (!fautes.has(f))
|
|
521
|
+
refuser(`le code produit ${f}, que « Les fautes » ne liste pas.`, 'faute-non-listee');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// The components a run reads: every one of <racine>; in the envelope of a component, that one
|
|
526
|
+
// alone, its neighbours read by their published form and never written.
|
|
527
|
+
export function aVerifier(racine, env = process.env) {
|
|
528
|
+
const tous = composants(racine);
|
|
529
|
+
const propre = composantDeLEnveloppe(env);
|
|
530
|
+
const sien = propre && tous.filter(c => path.resolve(c.dir) === path.resolve(racine, propre));
|
|
531
|
+
return sien?.length ? sien : tous;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
535
|
+
const racine = process.cwd();
|
|
536
|
+
const ecrire = process.argv.includes('--ecrire');
|
|
537
|
+
const constats = [];
|
|
538
|
+
for (const c of aVerifier(racine)) {
|
|
539
|
+
const { refus, signaux } = verifierComposant(racine, c, { ecrire });
|
|
540
|
+
for (const s of signaux) process.stderr.write(`⚠️ ${s}\n`);
|
|
541
|
+
constats.push(...refus);
|
|
542
|
+
}
|
|
543
|
+
const { refus, lignes } = trancher(racine, 'interfaces-contre-code', constats);
|
|
544
|
+
for (const r of refus) process.stderr.write(`⛔ ${r.message}\n`);
|
|
545
|
+
for (const l of lignes) process.stderr.write(`${l}\n`);
|
|
546
|
+
process.exitCode = refus.length ? 1 : 0;
|
|
547
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# The default left part of the Grillhouse status line, for a person without a status line command
|
|
3
|
+
# of their own: the model, the context used, the OpenRouter credit (cache 30 s) or the Anthropic
|
|
4
|
+
# rate limits over 5 h and 7 days. Reads Claude Code's status line input on stdin.
|
|
5
|
+
|
|
6
|
+
input=$(cat)
|
|
7
|
+
|
|
8
|
+
model=$(echo "$input" | jq -r '.model.display_name // "?"')
|
|
9
|
+
used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
|
|
10
|
+
five_hour=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
|
|
11
|
+
five_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
|
|
12
|
+
seven_day=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
|
|
13
|
+
seven_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
|
|
14
|
+
|
|
15
|
+
# --- Model ---
|
|
16
|
+
printf "\033[1;36m%s\033[0m" "$model"
|
|
17
|
+
|
|
18
|
+
# --- Context ---
|
|
19
|
+
if [ -n "$used_pct" ]; then
|
|
20
|
+
ctx_int=$(printf '%.0f' "$used_pct")
|
|
21
|
+
if [ "$ctx_int" -ge 80 ]; then
|
|
22
|
+
c="\033[1;31m"
|
|
23
|
+
elif [ "$ctx_int" -ge 60 ]; then
|
|
24
|
+
c="\033[1;33m"
|
|
25
|
+
else
|
|
26
|
+
c="\033[1;32m"
|
|
27
|
+
fi
|
|
28
|
+
printf " | ctx: ${c}%s%%\033[0m" "$ctx_int"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# --- OpenRouter: crédit restant (cache 30s) ---
|
|
32
|
+
if [ "${ANTHROPIC_BASE_URL:-}" = "https://openrouter.ai/api" ]; then
|
|
33
|
+
CACHE="/tmp/claude-or-credit.json"
|
|
34
|
+
NOW=$(date +%s)
|
|
35
|
+
if [ ! -f "$CACHE" ] || [ $(( NOW - $(jq -r '.ts // 0' "$CACHE" 2>/dev/null || echo 0) )) -ge 30 ]; then
|
|
36
|
+
KEY="${ANTHROPIC_AUTH_TOKEN:-}"
|
|
37
|
+
if [ -n "$KEY" ]; then
|
|
38
|
+
DATA=$(curl -s --max-time 3 "https://openrouter.ai/api/v1/auth/key" \
|
|
39
|
+
-H "Authorization: Bearer $KEY" 2>/dev/null)
|
|
40
|
+
if [ -n "$DATA" ]; then
|
|
41
|
+
USAGE=$(echo "$DATA" | jq -r '.data.usage // 0' 2>/dev/null)
|
|
42
|
+
LIMIT=$(echo "$DATA" | jq -r '.data.limit // 0' 2>/dev/null)
|
|
43
|
+
REMAIN=$(echo "$DATA" | jq -r '.data.limit_remaining // 0' 2>/dev/null)
|
|
44
|
+
printf '{"ts":%s,"usage":%s,"limit":%s,"remaining":%s}\n' \
|
|
45
|
+
"$NOW" "${USAGE:-0}" "${LIMIT:-0}" "${REMAIN:-0}" > "$CACHE" 2>/dev/null
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
if [ -f "$CACHE" ]; then
|
|
50
|
+
USAGE=$(jq -r '.usage // 0' "$CACHE" 2>/dev/null)
|
|
51
|
+
LIMIT=$(jq -r '.limit // 0' "$CACHE" 2>/dev/null)
|
|
52
|
+
REMAINING=$(jq -r '.remaining // 0' "$CACHE" 2>/dev/null)
|
|
53
|
+
# Si pas de limite configurée côté OpenRouter, fallback sur fichier budget local
|
|
54
|
+
if [ "$LIMIT" = "0" ] || [ "$LIMIT" = "null" ]; then
|
|
55
|
+
FALLBACK=$(cat ~/.claude/openrouter-budget 2>/dev/null || echo 0)
|
|
56
|
+
LIMIT=${OPENROUTER_BUDGET:-$FALLBACK}
|
|
57
|
+
REMAINING=$(LC_NUMERIC=C awk "BEGIN {printf \"%.2f\", ${LIMIT} - ${USAGE}}" 2>/dev/null || echo "?")
|
|
58
|
+
fi
|
|
59
|
+
LIMIT=${LIMIT:-0}
|
|
60
|
+
if [ "$LIMIT" != "0" ] && [ "$LIMIT" != "null" ]; then
|
|
61
|
+
PCT=$(LC_NUMERIC=C awk "BEGIN {printf \"%.0f\", (${USAGE}/${LIMIT})*100}" 2>/dev/null || echo "?")
|
|
62
|
+
if [ "$PCT" != "?" ] && [ "$PCT" -ge 80 ]; then
|
|
63
|
+
or_c="\033[1;31m"
|
|
64
|
+
elif [ "$PCT" != "?" ] && [ "$PCT" -ge 50 ]; then
|
|
65
|
+
or_c="\033[1;33m"
|
|
66
|
+
else
|
|
67
|
+
or_c="\033[1;32m"
|
|
68
|
+
fi
|
|
69
|
+
printf " | OR: ${or_c}\$%s/%s\$ (%s%%)\033[0m" "$(LC_NUMERIC=C awk "BEGIN {printf \"%.2f\", ${USAGE}}" 2>/dev/null)" "$(LC_NUMERIC=C awk "BEGIN {printf \"%.0f\", ${LIMIT}}" 2>/dev/null)" "$PCT"
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# --- 5h rate limit + reset (Anthropic) ---
|
|
75
|
+
if [ -n "$five_hour" ]; then
|
|
76
|
+
pct5=$(printf '%.0f' "$five_hour")
|
|
77
|
+
if [ -n "$five_reset" ] && [ "$five_reset" != "null" ]; then
|
|
78
|
+
reset5=$(date -d "@$five_reset" +"%Hh%M" 2>/dev/null || date -r "$five_reset" +"%Hh%M" 2>/dev/null || echo "?")
|
|
79
|
+
else
|
|
80
|
+
reset5="?"
|
|
81
|
+
fi
|
|
82
|
+
if [ "$pct5" -ge 80 ]; then
|
|
83
|
+
c5="\033[1;31m"
|
|
84
|
+
elif [ "$pct5" -ge 50 ]; then
|
|
85
|
+
c5="\033[1;33m"
|
|
86
|
+
else
|
|
87
|
+
c5="\033[1;32m"
|
|
88
|
+
fi
|
|
89
|
+
printf " | 5h: ${c5}%s%%\033[0m \033[0;90m->%s\033[0m" "$pct5" "$reset5"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# --- 7d rate limit + reset (Anthropic) ---
|
|
93
|
+
if [ -n "$seven_day" ]; then
|
|
94
|
+
pct7=$(printf '%.0f' "$seven_day")
|
|
95
|
+
if [ -n "$seven_reset" ] && [ "$seven_reset" != "null" ]; then
|
|
96
|
+
reset7=$(date -d "@$seven_reset" +"%a %Hh%M" 2>/dev/null || date -r "$seven_reset" +"%a %Hh%M" 2>/dev/null || echo "?")
|
|
97
|
+
else
|
|
98
|
+
reset7="?"
|
|
99
|
+
fi
|
|
100
|
+
if [ "$pct7" -ge 80 ]; then
|
|
101
|
+
c7="\033[1;31m"
|
|
102
|
+
elif [ "$pct7" -ge 50 ]; then
|
|
103
|
+
c7="\033[1;33m"
|
|
104
|
+
else
|
|
105
|
+
c7="\033[1;32m"
|
|
106
|
+
fi
|
|
107
|
+
printf " | 7d: ${c7}%s%%\033[0m \033[0;90m->%s\033[0m" "$pct7" "$reset7"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
printf "\n"
|