@ogygie/cli 0.1.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.
@@ -0,0 +1,42 @@
1
+ # Governed by Ogygie (spec ogygie-standalone-repo B2): overwritten as-is by
2
+ # `ogygie upgrade`, never merged. Product-specific CI lives in OTHER workflow
3
+ # files, never in this one.
4
+ name: ogygie-gates
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ pull_request:
9
+
10
+ # Least-privilege token: gitleaks-action reads the PR's commit list on
11
+ # pull_request events; PR comments stay disabled so no write scope is needed.
12
+ permissions:
13
+ contents: read
14
+ pull-requests: read
15
+
16
+ jobs:
17
+ verifier:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ with: { fetch-depth: 0 }
22
+ - uses: pnpm/action-setup@v4
23
+ - uses: actions/setup-node@v4
24
+ with: { node-version: 22, cache: pnpm }
25
+ - run: pnpm install --frozen-lockfile
26
+ # Deliberate order: Ogygie gates BEFORE the tests —
27
+ # a lying manifest must break faster than a red test.
28
+ - run: pnpm exec ogygie check # lockstep versions + governed-file checksums (B5)
29
+ - run: pnpm ogygie:verify-manifest # manifest ↔ actual code (routes, folders, tables)
30
+ - run: pnpm ogygie:verify-specs # frontmatter + sections + spec lifecycle + decision refs
31
+ - run: pnpm ogygie:verify-contracts # transitive purity of every schemas.ts
32
+ - run: pnpm typecheck
33
+ - run: pnpm lint # any, ts-ignore, raw-db, dangerouslySetInnerHTML
34
+ - run: pnpm test
35
+ # Security
36
+ - uses: gitleaks/gitleaks-action@v2 # committed secrets
37
+ env:
38
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
39
+ GITLEAKS_ENABLE_COMMENTS: "false"
40
+ - uses: semgrep/semgrep-action@v1 # dangerous patterns (SAST)
41
+ with: { config: "p/typescript p/owasp-top-ten" }
42
+ - run: pnpm audit --audit-level=high # dependency CVEs
@@ -0,0 +1,28 @@
1
+ # Catalogue des patterns UI
2
+
3
+ > Les patterns sont les SEULES briques d'écran autorisées (AGENTS.md §Front) :
4
+ > un écran est une COMPOSITION de patterns, jamais de `<table>`, `<form>` ni
5
+ > fetch nus. Le starter n'a pas encore d'app front produit : à l'ouverture du
6
+ > premier front, chaque pattern retenu est COPIÉ dans `apps/web/src/patterns/`
7
+ > et sa fiche ci-dessous devient son contrat. Un pattern manquant se crée ICI
8
+ > d'abord (fiche + décision), jamais en one-off dans un écran.
9
+ >
10
+ > Ce catalogue est une source de vérité du brief de design
11
+ > (`pnpm ogygie:design-brief <feature>`) : chaque fiche `## <Name>` devient
12
+ > une entrée du vocabulaire de composition. Noms en anglais (machine-lisible),
13
+ > fiches en français (document humain).
14
+
15
+ ## DataTable
16
+ Tableau de données (TanStack Table, seul usage autorisé). États : squelette de chargement (`DataTable.Skeleton`), vide (déléguer à EmptyState), nominal trié/paginé, erreur (déléguer à ErrorBanner). Chiffres en `tabular-nums` alignés à droite, unité explicite.
17
+
18
+ ## EmptyState
19
+ État vide intentionnel : un titre, une explication courte, UNE action de création. Jamais de tableau ou de liste vides nus — l'écran vide est le premier écran que voit chaque nouvel utilisateur.
20
+
21
+ ## ErrorBanner
22
+ Bandeau d'erreur qui affiche une OgygieError : message traduit via `userMessageKey` (clé ICU), code stable et `suggestedFix` repliables. Tout état d'erreur d'écran passe par lui — jamais de message d'erreur improvisé.
23
+
24
+ ## ContractForm
25
+ Formulaire DÉRIVÉ d'un contrat Zod (react-hook-form, seul usage autorisé) : les contraintes du schéma (min/max/regex, obligatoire) sont les validations des champs — rien n'est redéclaré à la main. États : saisie, soumission en cours (bouton désactivé), erreurs de champ, erreur serveur (ErrorBanner).
26
+
27
+ ## VarianceChart
28
+ Graphique d'écart (Recharts, seul usage autorisé) : compare une valeur constatée à une référence. États : chargement, vide, nominal, erreur. Montants en `tabular-nums`, axe et unité toujours étiquetés.
@@ -0,0 +1,50 @@
1
+ {
2
+ "$intent": "Placeholder design tokens of the starter. A product REWRITES these values when it opens (AGENTS.product.md); the structure stays. The design tool composes ONLY from these tokens — a missing color or spacing is an amendment of this file (explicit decision), never a hardcoded value (AGENTS.md §Front).",
3
+ "color": {
4
+ "surface": "#f6f6f4",
5
+ "surface-raised": "#ffffff",
6
+ "border": "#e2e1dc",
7
+ "text": "#22241f",
8
+ "text-muted": "#5f6259",
9
+ "accent": "#3b5b92",
10
+ "accent-contrast": "#ffffff",
11
+ "positive": "#2f6b4f",
12
+ "warning": "#a96a1f",
13
+ "danger": "#b3423a",
14
+ "focus": "#7aa2ff"
15
+ },
16
+ "typography": {
17
+ "font-sans": "system-ui, -apple-system, 'Segoe UI', sans-serif",
18
+ "font-mono": "ui-monospace, 'SF Mono', 'Cascadia Mono', monospace",
19
+ "size": {
20
+ "xs": "0.75rem",
21
+ "sm": "0.875rem",
22
+ "base": "1rem",
23
+ "lg": "1.125rem",
24
+ "xl": "1.375rem",
25
+ "2xl": "1.75rem"
26
+ },
27
+ "leading": {
28
+ "tight": "1.25",
29
+ "normal": "1.5"
30
+ }
31
+ },
32
+ "spacing": {
33
+ "1": "0.25rem",
34
+ "2": "0.5rem",
35
+ "3": "0.75rem",
36
+ "4": "1rem",
37
+ "6": "1.5rem",
38
+ "8": "2rem",
39
+ "12": "3rem"
40
+ },
41
+ "radius": {
42
+ "sm": "0.25rem",
43
+ "md": "0.5rem",
44
+ "lg": "0.75rem",
45
+ "full": "9999px"
46
+ },
47
+ "shadow": {
48
+ "raised": "0 1px 2px rgb(0 0 0 / 0.06), 0 2px 8px rgb(0 0 0 / 0.04)"
49
+ }
50
+ }
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ *.log
5
+ .DS_Store
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 1,
3
+ "product": {
4
+ "name": "__PRODUCT_NAME__",
5
+ "description": "Produit Ogygie __PRODUCT_NAME__ — décrire le job-to-be-done ici (checklist de démarrage)."
6
+ },
7
+ "ogygieVersion": "__OGYGIE_VERSION__",
8
+ "apps": [],
9
+ "features": [],
10
+ "conventions": {
11
+ "agentsMd": "AGENTS.md",
12
+ "failuresMd": "FAILURES.md",
13
+ "specsFolder": "specs/"
14
+ }
15
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "__PRODUCT_NAME__",
3
+ "private": true,
4
+ "type": "module",
5
+ "packageManager": "pnpm@11.3.0",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "scripts": {
10
+ "test": "vitest run",
11
+ "typecheck": "tsc -p tsconfig.json && pnpm -r --if-present run typecheck",
12
+ "lint": "tsx scripts/lint-interdits.ts",
13
+ "ogygie:verify-manifest": "tsx scripts/verify-manifest.ts",
14
+ "ogygie:verify-specs": "tsx scripts/verify-specs.ts",
15
+ "ogygie:verify-contracts": "tsx scripts/verify-contracts.ts",
16
+ "ogygie:impact": "tsx scripts/ogygie-impact.ts",
17
+ "ogygie:design-brief": "tsx scripts/ogygie-design-brief.ts",
18
+ "verify": "pnpm ogygie:verify-manifest && pnpm ogygie:verify-specs && pnpm ogygie:verify-contracts && pnpm typecheck && pnpm lint && pnpm test"
19
+ },
20
+ "dependencies": {
21
+ "@ogygie/core": "__OGYGIE_VERSION__"
22
+ },
23
+ "devDependencies": {
24
+ "@ogygie/cli": "__OGYGIE_VERSION__",
25
+ "@types/node": "^24.0.0",
26
+ "tsx": "^4.20.0",
27
+ "typescript": "^5.8.0",
28
+ "vitest": "^3.2.0",
29
+ "zod": "^4.0.0"
30
+ }
31
+ }
@@ -0,0 +1,8 @@
1
+ packages:
2
+ - "apps/*"
3
+ - "packages/*"
4
+
5
+ # Build scripts are opt-in with pnpm 11; esbuild postinstall fetches its binary
6
+ # (needed by tsx and vitest).
7
+ allowBuilds:
8
+ esbuild: true
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @intent CI gate `pnpm lint`: greps the source for the absolute bans of
3
+ * AGENTS.md §Interdits. Deliberately dependency-free — a real ESLint setup
4
+ * (incl. the `ogygie/no-raw-db` rule) is tracked as a follow-up. Type-level
5
+ * bans are only checked on code (comments stripped); the ts-ignore ban is
6
+ * checked everywhere since it lives in comments by nature.
7
+ *
8
+ * The raw-db ban is no longer a hand-kept list: it is DERIVED from
9
+ * ogygie.manifest.json through @ogygie/core/shared-tables (mayUseRawDb), so a
10
+ * feature earns the right to touch the database directly only by declaring, in
11
+ * the manifest, that every table it owns is shared (AGENTS.md §Multi-tenant
12
+ * rule 4). The policy itself is unit-tested with its mutations (D22) — this
13
+ * script only reports.
14
+ */
15
+ import { readdirSync, readFileSync, statSync } from "node:fs";
16
+ import { join, relative } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { OgygieManifestSchema } from "@ogygie/core/manifest";
19
+ import { mayUseRawDb } from "@ogygie/core/shared-tables";
20
+
21
+ const ROOT = fileURLToPath(new URL("..", import.meta.url));
22
+ const SELF = fileURLToPath(import.meta.url);
23
+ const SCANNED_DIRS = ["apps", "packages", "scripts"];
24
+ const IGNORED_DIRS = new Set(["node_modules", "dist", ".git"]);
25
+ const EXTENSIONS = [".ts", ".tsx", ".mts", ".mjs"];
26
+
27
+ const TS_IGNORE_BAN = {
28
+ pattern: new RegExp(`@ts-${"ignore"}`),
29
+ rule: "ts-ignore interdit — @ts-expect-error avec justification si inévitable",
30
+ };
31
+
32
+ // Closed-list socle exception (AGENTS.md §Multi-tenant): the tenancy feature
33
+ // BUILDS the contexts, so it predates them. Every other feature goes through
34
+ // tenantDb — unless the manifest declares that every table it owns is SHARED,
35
+ // which have no tenant column to filter on. Interim guard until the real
36
+ // ESLint `ogygie/no-raw-db` rule lands.
37
+ const FEATURES_PREFIX = "apps/api/src/features/";
38
+ // TODO(product): the ONE tenancy socle feature allowed to touch the raw
39
+ // database (it BUILDS the tenant contexts) — declare it in AGENTS.product.md
40
+ // and here. Empty until the socle exists.
41
+ const RAW_DB_SOCLE_ALLOWLIST = new Set<string>([]);
42
+ const RAW_DB_BAN = {
43
+ pattern: /\bctx\.db\b/,
44
+ rule: "ctx.db interdit hors du socle et des features à tables partagées — passer par tenantDb (AGENTS.md §Multi-tenant)",
45
+ };
46
+ const CODE_BANS = [
47
+ { pattern: /as unknown as/, rule: "as unknown as interdit" },
48
+ { pattern: /(:\s*any\b)|(<any[,>])|(\bas any\b)|(\bany\[\])/, rule: "type any interdit" },
49
+ { pattern: /dangerouslySetInnerHTML/, rule: "dangerouslySetInnerHTML interdit" },
50
+ ];
51
+
52
+ const manifest = OgygieManifestSchema.parse(
53
+ JSON.parse(readFileSync(join(ROOT, "ogygie.manifest.json"), "utf8")),
54
+ );
55
+ /** Feature folder names allowed to touch the raw database, derived — not held. */
56
+ const rawDbAllowedFolders = new Set(
57
+ manifest.features
58
+ .filter((feature) => mayUseRawDb(feature, RAW_DB_SOCLE_ALLOWLIST))
59
+ .map((feature) => feature.folder.split("/").at(-1) ?? feature.name),
60
+ );
61
+
62
+ const violations: string[] = [];
63
+
64
+ function stripComments(line: string): string {
65
+ const trimmed = line.trim();
66
+ if (trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed.startsWith("//")) {
67
+ return "";
68
+ }
69
+ return line.replace(/\/\/.*$/, "");
70
+ }
71
+
72
+ function scan(dir: string): void {
73
+ for (const entry of readdirSync(dir)) {
74
+ if (IGNORED_DIRS.has(entry)) continue;
75
+ const entryPath = join(dir, entry);
76
+ const stats = statSync(entryPath);
77
+ if (stats.isDirectory()) {
78
+ scan(entryPath);
79
+ continue;
80
+ }
81
+ if (entryPath === SELF) continue;
82
+ if (!EXTENSIONS.some((extension) => entry.endsWith(extension))) continue;
83
+ const lines = readFileSync(entryPath, "utf8").split("\n");
84
+ lines.forEach((line, index) => {
85
+ const location = `${relative(ROOT, entryPath)}:${index + 1}`;
86
+ if (TS_IGNORE_BAN.pattern.test(line)) {
87
+ violations.push(`${location} — ${TS_IGNORE_BAN.rule}`);
88
+ }
89
+ const code = stripComments(line);
90
+ for (const { pattern, rule } of CODE_BANS) {
91
+ if (pattern.test(code)) {
92
+ violations.push(`${location} — ${rule}`);
93
+ }
94
+ }
95
+ const relPath = relative(ROOT, entryPath);
96
+ if (relPath.startsWith(FEATURES_PREFIX)) {
97
+ const featureFolder = relPath.slice(FEATURES_PREFIX.length).split("/")[0] ?? "";
98
+ if (!rawDbAllowedFolders.has(featureFolder) && RAW_DB_BAN.pattern.test(code)) {
99
+ violations.push(`${location} — ${RAW_DB_BAN.rule}`);
100
+ }
101
+ }
102
+ });
103
+ }
104
+ }
105
+
106
+ for (const dir of SCANNED_DIRS) {
107
+ try {
108
+ scan(join(ROOT, dir));
109
+ } catch {
110
+ // folder may not exist yet in a fresh checkout — nothing to scan
111
+ }
112
+ }
113
+
114
+ if (violations.length > 0) {
115
+ for (const violation of violations) console.error(violation);
116
+ console.error(`\nlint — ${violations.length} violation(s) des interdits AGENTS.md.`);
117
+ process.exit(1);
118
+ }
119
+ console.log("lint — OK, aucun interdit détecté.");
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @intent Session tool `pnpm ogygie:design-brief <feature> [--stdout]`
3
+ * (specs/ogygie-design-brief.spec.md): writes the feature's design brief to
4
+ * design/briefs/<feature>.md, DERIVED from the repo's sources of truth —
5
+ * manifest routes, Zod contracts (via z.toJSONSchema), the feature spec,
6
+ * design/tokens.json, design/patterns.md, the front apps' pattern modules,
7
+ * and test-fixtures/ counts. Same family as ogygie-impact and verify-*:
8
+ * fs + manifest in, findings as OgygieError, no new dependency.
9
+ *
10
+ * This file only GATHERS: every decision (formats, warnings, empty-section
11
+ * signaling, rendering) lives in @ogygie/core/design-brief with its spec
12
+ * tests. Missing sources are passed as null — signaled downstream, never
13
+ * guessed here.
14
+ *
15
+ * Output contract (spec §États UI): Markdown file + console summary; --stdout
16
+ * additionally prints the document. Exit 0, or 1 when the feature is unknown.
17
+ */
18
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { pathToFileURL } from "node:url";
21
+ import { z } from "zod";
22
+ import {
23
+ buildDesignBrief,
24
+ renderDesignBriefMarkdown,
25
+ type DesignBriefSources,
26
+ type JsonSchemaNode,
27
+ } from "@ogygie/core/design-brief";
28
+ import { OgygieManifestSchema } from "@ogygie/core/manifest";
29
+ import { dbTableNamesOf } from "@ogygie/core/shared-tables";
30
+ import { ogygieError } from "@ogygie/core/errors";
31
+
32
+ const ROOT = new URL("..", import.meta.url).pathname;
33
+ const MANIFEST_PATH = "ogygie.manifest.json";
34
+ const BRIEFS_FOLDER = join("design", "briefs");
35
+
36
+ // ---------------------------------------------------------------- helpers
37
+ function readTextIfExists(relativePath: string): string | null {
38
+ const fullPath = join(ROOT, relativePath);
39
+ return existsSync(fullPath) ? readFileSync(fullPath, "utf8") : null;
40
+ }
41
+
42
+ /** First `@intent` line of a module, the pattern's one-line contract. */
43
+ function intentLineOf(content: string): string | null {
44
+ const match = /@intent\s+(.+)/.exec(content);
45
+ return match?.[1] === undefined ? null : match[1].trim();
46
+ }
47
+
48
+ /** Production .ts/.tsx files of a folder, tests excluded, stable order. */
49
+ function productionSourcesOf(folder: string): Array<{ file: string; content: string }> {
50
+ const fullFolder = join(ROOT, folder);
51
+ if (!existsSync(fullFolder)) return [];
52
+ const sources: Array<{ file: string; content: string }> = [];
53
+ for (const entry of readdirSync(fullFolder, { withFileTypes: true }).sort((a, b) =>
54
+ a.name.localeCompare(b.name, "en"),
55
+ )) {
56
+ const relativePath = join(folder, entry.name);
57
+ if (entry.isDirectory()) {
58
+ sources.push(...productionSourcesOf(relativePath));
59
+ continue;
60
+ }
61
+ if (!/\.(ts|tsx)$/.test(entry.name) || /\.test\.(ts|tsx)$/.test(entry.name)) continue;
62
+ sources.push({ file: relativePath, content: readFileSync(join(ROOT, relativePath), "utf8") });
63
+ }
64
+ return sources;
65
+ }
66
+
67
+ /** Rows of test-fixtures/<table>.json — null when absent or not an array. */
68
+ function fixtureRowCountOf(table: string): number | null {
69
+ const raw = readTextIfExists(join("test-fixtures", `${table}.json`));
70
+ if (raw === null) return null;
71
+ try {
72
+ const parsed: unknown = JSON.parse(raw);
73
+ return Array.isArray(parsed) ? parsed.length : null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /** A named export of schemas.ts turned into JSON Schema — null, never a guess. */
80
+ function jsonSchemaOfExport(
81
+ schemasModule: Record<string, unknown> | null,
82
+ exportName: string,
83
+ ): JsonSchemaNode | null {
84
+ const candidate = schemasModule?.[exportName];
85
+ if (candidate === undefined || candidate === null) return null;
86
+ try {
87
+ return z.toJSONSchema(candidate as z.ZodType) as JsonSchemaNode;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ // ---------------------------------------------------------------- CLI args
94
+ const cliArguments = process.argv.slice(2);
95
+ const printToStdout = cliArguments.includes("--stdout");
96
+ const featureArgument = cliArguments.find((argument) => !argument.startsWith("--"));
97
+
98
+ if (featureArgument === undefined) {
99
+ const failure = ogygieError({
100
+ code: "DESIGN_BRIEF_UNKNOWN_FEATURE",
101
+ message: "No feature name given. Usage: pnpm ogygie:design-brief <feature> [--stdout].",
102
+ file: null,
103
+ cause: "process.argv carries no non-flag argument",
104
+ suggestedFix:
105
+ "Pass a feature name from ogygie.manifest.json, e.g. `pnpm ogygie:design-brief notes`.",
106
+ });
107
+ console.error(JSON.stringify(failure, null, 2));
108
+ process.exit(1);
109
+ }
110
+ const featureName = featureArgument;
111
+
112
+ // ---------------------------------------------------------------- gathering
113
+ const manifest = OgygieManifestSchema.parse(
114
+ JSON.parse(readFileSync(join(ROOT, MANIFEST_PATH), "utf8")),
115
+ );
116
+ const apiApp = manifest.apps.find((app) => app.type === "api");
117
+ const feature = manifest.features.find((candidate) => candidate.name === featureName);
118
+ const owningApp =
119
+ feature?.app === undefined
120
+ ? apiApp
121
+ : manifest.apps.find((app) => app.name === feature.app) ?? apiApp;
122
+
123
+ const patternModules: DesignBriefSources["patternModules"] = [];
124
+ for (const app of manifest.apps.filter((candidate) => candidate.type === "front")) {
125
+ const patternsFolder = join(app.path, "src", "patterns");
126
+ for (const source of productionSourcesOf(patternsFolder)) {
127
+ const name = source.file.split("/").pop()?.replace(/\.(ts|tsx)$/, "");
128
+ if (name !== undefined) patternModules.push({ name, intent: intentLineOf(source.content) });
129
+ }
130
+ }
131
+
132
+ let sources: DesignBriefSources = {
133
+ featureName,
134
+ validFeatureNames: manifest.features.map((candidate) => candidate.name),
135
+ visualTokensJson: readTextIfExists(join("design", "tokens.json")),
136
+ patternsCatalogDoc: readTextIfExists(join("design", "patterns.md")),
137
+ patternModules,
138
+ routes: [],
139
+ specContent: null,
140
+ featureSources: [],
141
+ fixtureCounts: [],
142
+ };
143
+
144
+ if (feature !== undefined && owningApp !== undefined) {
145
+ const featureFolder = join(owningApp.path, feature.folder);
146
+ const schemasPath = join(ROOT, featureFolder, "schemas.ts");
147
+ let schemasModule: Record<string, unknown> | null = null;
148
+ if (existsSync(schemasPath)) {
149
+ schemasModule = (await import(pathToFileURL(schemasPath).href)) as Record<string, unknown>;
150
+ }
151
+ sources = {
152
+ ...sources,
153
+ routes: feature.routes.map((route) => ({
154
+ method: route.method,
155
+ path: route.path,
156
+ description: route.description,
157
+ inputSchemaName: route.schemaInput,
158
+ outputSchemaName: route.schemaOutput,
159
+ inputJsonSchema: jsonSchemaOfExport(schemasModule, route.schemaInput),
160
+ outputJsonSchema: jsonSchemaOfExport(schemasModule, route.schemaOutput),
161
+ })),
162
+ specContent: feature.spec == null ? null : readTextIfExists(feature.spec),
163
+ featureSources: productionSourcesOf(featureFolder),
164
+ fixtureCounts: dbTableNamesOf(feature).map((table) => ({
165
+ table,
166
+ count: fixtureRowCountOf(table),
167
+ })),
168
+ };
169
+ }
170
+
171
+ // ---------------------------------------------------------------- build
172
+ const result = buildDesignBrief(sources);
173
+ if (result.failure !== null) {
174
+ console.error(JSON.stringify(result.failure, null, 2));
175
+ console.error("\nogygie:design-brief — feature inconnue.");
176
+ process.exit(1);
177
+ }
178
+
179
+ const markdown = renderDesignBriefMarkdown(result.brief);
180
+ mkdirSync(join(ROOT, BRIEFS_FOLDER), { recursive: true });
181
+ const briefPath = join(BRIEFS_FOLDER, `${featureName}.md`);
182
+ writeFileSync(join(ROOT, briefPath), markdown);
183
+
184
+ if (printToStdout) console.log(markdown);
185
+ console.log(`── ogygie:design-brief ${featureName} ──`);
186
+ console.log(`Brief écrit : ${briefPath} (${markdown.split("\n").length - 1} lignes)`);
187
+ console.log(
188
+ result.brief.warnings.length === 0
189
+ ? "Avertissements : aucun."
190
+ : `Avertissements : ${result.brief.warnings.length}\n${result.brief.warnings.map((warning) => ` ⚠ ${warning}`).join("\n")}`,
191
+ );
192
+ process.exit(0);