@ogygie/cli 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js
CHANGED
|
@@ -15200,6 +15200,50 @@ function checkProject(deps) {
|
|
|
15200
15200
|
return problems;
|
|
15201
15201
|
}
|
|
15202
15202
|
|
|
15203
|
+
// src/commands/generate.ts
|
|
15204
|
+
import { createRequire } from "node:module";
|
|
15205
|
+
import { join as join5 } from "node:path";
|
|
15206
|
+
import { pathToFileURL } from "node:url";
|
|
15207
|
+
var GENERATE_CLI_ENTRY = "@ogygie/generate/cli";
|
|
15208
|
+
var GenerateNotResolved = class extends Error {
|
|
15209
|
+
};
|
|
15210
|
+
async function loadGenerateFrom(projectRoot) {
|
|
15211
|
+
const resolveFromProject = createRequire(join5(projectRoot, "package.json"));
|
|
15212
|
+
let entryPoint;
|
|
15213
|
+
try {
|
|
15214
|
+
entryPoint = resolveFromProject.resolve(GENERATE_CLI_ENTRY);
|
|
15215
|
+
} catch (thrown) {
|
|
15216
|
+
throw new GenerateNotResolved(thrown instanceof Error ? thrown.message : String(thrown));
|
|
15217
|
+
}
|
|
15218
|
+
return await import(pathToFileURL(entryPoint).href);
|
|
15219
|
+
}
|
|
15220
|
+
async function runGenerate(deps) {
|
|
15221
|
+
const importGenerate = deps.importGenerate ?? (() => loadGenerateFrom(deps.projectRoot));
|
|
15222
|
+
let generator;
|
|
15223
|
+
try {
|
|
15224
|
+
generator = await importGenerate();
|
|
15225
|
+
} catch (thrown) {
|
|
15226
|
+
const reason = thrown instanceof Error ? thrown.message : String(thrown);
|
|
15227
|
+
if (thrown instanceof GenerateNotResolved) {
|
|
15228
|
+
throwOgygieError({
|
|
15229
|
+
code: "GENERATE_NOT_INSTALLED",
|
|
15230
|
+
message: "The Ogygie generator is not installed in this project.",
|
|
15231
|
+
file: "package.json",
|
|
15232
|
+
cause: `${GENERATE_CLI_ENTRY} cannot be resolved: ${reason}`,
|
|
15233
|
+
suggestedFix: `The generator is experimental (spec ogygie-generate): until its promotion it lives in the framework repo as packages/ogygie-generate \u2014 add it to the workspace as @ogygie/generate (workspace:*) there, or wait for the published package at the framework's version (${deps.cliVersion}, lockstep D2).`
|
|
15234
|
+
});
|
|
15235
|
+
}
|
|
15236
|
+
throwOgygieError({
|
|
15237
|
+
code: "GENERATE_LOAD_FAILED",
|
|
15238
|
+
message: "The Ogygie generator is installed but could not be loaded.",
|
|
15239
|
+
file: "package.json",
|
|
15240
|
+
cause: reason,
|
|
15241
|
+
suggestedFix: "Reinstalling will not help. The package ships as TypeScript source until its promotion: run the command through a TypeScript host (`pnpm ogygie:generate`, `pnpm ogygie:verify-generation`, or `pnpm exec tsx` on the CLI entry), and check that @ogygie/generate matches @ogygie/cli (lockstep D2)."
|
|
15242
|
+
});
|
|
15243
|
+
}
|
|
15244
|
+
return generator.runGenerateCommand({ projectRoot: deps.projectRoot, argv: deps.argv, log: deps.log, error: deps.error });
|
|
15245
|
+
}
|
|
15246
|
+
|
|
15203
15247
|
// src/commands/init.ts
|
|
15204
15248
|
import {
|
|
15205
15249
|
existsSync as existsSync5,
|
|
@@ -15209,16 +15253,16 @@ import {
|
|
|
15209
15253
|
statSync,
|
|
15210
15254
|
writeFileSync as writeFileSync3
|
|
15211
15255
|
} from "node:fs";
|
|
15212
|
-
import { dirname as dirname2, join as
|
|
15256
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
15213
15257
|
var PRODUCT_NAME_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
15214
15258
|
var RENAMED_TEMPLATE_FILES = {
|
|
15215
15259
|
gitignore: ".gitignore"
|
|
15216
15260
|
};
|
|
15217
15261
|
function listTemplateFiles(root, folder = "") {
|
|
15218
15262
|
const files = [];
|
|
15219
|
-
for (const entry of readdirSync(
|
|
15263
|
+
for (const entry of readdirSync(join6(root, folder)).sort()) {
|
|
15220
15264
|
const relativePath = folder === "" ? entry : `${folder}/${entry}`;
|
|
15221
|
-
if (statSync(
|
|
15265
|
+
if (statSync(join6(root, relativePath)).isDirectory()) {
|
|
15222
15266
|
files.push(...listTemplateFiles(root, relativePath));
|
|
15223
15267
|
} else {
|
|
15224
15268
|
files.push(relativePath);
|
|
@@ -15243,12 +15287,12 @@ function initProject(deps, productName2) {
|
|
|
15243
15287
|
suggestedFix: "Pick a kebab-case name, e.g. `ogygie init order-desk`."
|
|
15244
15288
|
});
|
|
15245
15289
|
}
|
|
15246
|
-
if (existsSync5(
|
|
15290
|
+
if (existsSync5(join6(deps.targetDir, MANIFEST_FILE_NAME))) {
|
|
15247
15291
|
throwOgygieError({
|
|
15248
15292
|
code: "OGYGIE_INIT_TARGET_NOT_EMPTY",
|
|
15249
15293
|
message: `The target directory already hosts an Ogygie project (${MANIFEST_FILE_NAME} found).`,
|
|
15250
15294
|
file: MANIFEST_FILE_NAME,
|
|
15251
|
-
cause: `${
|
|
15295
|
+
cause: `${join6(deps.targetDir, MANIFEST_FILE_NAME)} exists`,
|
|
15252
15296
|
suggestedFix: "Run `pnpm ogygie upgrade` in that project instead of init."
|
|
15253
15297
|
});
|
|
15254
15298
|
}
|
|
@@ -15262,12 +15306,12 @@ function initProject(deps, productName2) {
|
|
|
15262
15306
|
suggestedFix: "Start from an empty directory (only a .git folder is tolerated)."
|
|
15263
15307
|
});
|
|
15264
15308
|
}
|
|
15265
|
-
const projectTemplates =
|
|
15309
|
+
const projectTemplates = join6(deps.templatesDir, "project");
|
|
15266
15310
|
const writtenFiles = [];
|
|
15267
15311
|
for (const templateRelativePath of listTemplateFiles(projectTemplates)) {
|
|
15268
|
-
const content = readFileSync5(
|
|
15312
|
+
const content = readFileSync5(join6(projectTemplates, templateRelativePath), "utf8").replaceAll("__PRODUCT_NAME__", productName2).replaceAll("__OGYGIE_VERSION__", deps.cliVersion);
|
|
15269
15313
|
const targetRelativePath = targetPathOf(templateRelativePath);
|
|
15270
|
-
const absoluteTarget =
|
|
15314
|
+
const absoluteTarget = join6(deps.targetDir, targetRelativePath);
|
|
15271
15315
|
mkdirSync2(dirname2(absoluteTarget), { recursive: true });
|
|
15272
15316
|
writeFileSync3(absoluteTarget, content);
|
|
15273
15317
|
writtenFiles.push(targetRelativePath);
|
|
@@ -15284,14 +15328,14 @@ function initProject(deps, productName2) {
|
|
|
15284
15328
|
}
|
|
15285
15329
|
|
|
15286
15330
|
// src/commands/studio.ts
|
|
15287
|
-
import { createRequire } from "node:module";
|
|
15288
|
-
import { join as
|
|
15289
|
-
import { pathToFileURL } from "node:url";
|
|
15331
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
15332
|
+
import { join as join7 } from "node:path";
|
|
15333
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
15290
15334
|
var PUBLISHED_STUDIO_SERVER = "@ogygie/studio/server";
|
|
15291
15335
|
async function loadStudioFrom(projectRoot) {
|
|
15292
|
-
const resolveFromProject =
|
|
15336
|
+
const resolveFromProject = createRequire2(join7(projectRoot, "package.json"));
|
|
15293
15337
|
const entryPoint = resolveFromProject.resolve(PUBLISHED_STUDIO_SERVER);
|
|
15294
|
-
return await import(
|
|
15338
|
+
return await import(pathToFileURL2(entryPoint).href);
|
|
15295
15339
|
}
|
|
15296
15340
|
function isModuleNotFound(thrown) {
|
|
15297
15341
|
const code = thrown?.code;
|
|
@@ -15329,14 +15373,14 @@ async function runStudio(deps) {
|
|
|
15329
15373
|
|
|
15330
15374
|
// src/commands/mcp.ts
|
|
15331
15375
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
15332
|
-
import { createRequire as
|
|
15333
|
-
import { join as
|
|
15334
|
-
import { pathToFileURL as
|
|
15376
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
15377
|
+
import { join as join8 } from "node:path";
|
|
15378
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
15335
15379
|
var PUBLISHED_MCP_SERVER = "@ogygie/mcp/server";
|
|
15336
15380
|
async function loadMcpFrom(projectRoot) {
|
|
15337
|
-
const resolveFromProject =
|
|
15381
|
+
const resolveFromProject = createRequire3(join8(projectRoot, "package.json"));
|
|
15338
15382
|
const entryPoint = resolveFromProject.resolve(PUBLISHED_MCP_SERVER);
|
|
15339
|
-
return await import(
|
|
15383
|
+
return await import(pathToFileURL3(entryPoint).href);
|
|
15340
15384
|
}
|
|
15341
15385
|
function isModuleNotFound2(thrown) {
|
|
15342
15386
|
const code = thrown?.code;
|
|
@@ -15377,7 +15421,7 @@ async function runMcp(deps) {
|
|
|
15377
15421
|
|
|
15378
15422
|
// src/commands/upgrade.ts
|
|
15379
15423
|
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
15380
|
-
import { dirname as dirname3, join as
|
|
15424
|
+
import { dirname as dirname3, join as join9 } from "node:path";
|
|
15381
15425
|
|
|
15382
15426
|
// src/version.ts
|
|
15383
15427
|
function compareVersions(a, b) {
|
|
@@ -15425,7 +15469,7 @@ async function upgradeProject(deps) {
|
|
|
15425
15469
|
for (const governed of governedFiles) {
|
|
15426
15470
|
const expected = installedChecksums[governed.targetPath];
|
|
15427
15471
|
if (expected === void 0) continue;
|
|
15428
|
-
const absolutePath =
|
|
15472
|
+
const absolutePath = join9(deps.projectRoot, governed.targetPath);
|
|
15429
15473
|
const actual = existsSync6(absolutePath) ? sha256Hex(readFileSync7(absolutePath, "utf8")) : "<absent>";
|
|
15430
15474
|
if (actual !== expected) {
|
|
15431
15475
|
throwOgygieError({
|
|
@@ -15441,10 +15485,10 @@ async function upgradeProject(deps) {
|
|
|
15441
15485
|
const diffBlocks = [];
|
|
15442
15486
|
for (const governed of governedFiles) {
|
|
15443
15487
|
const content = readFileSync7(
|
|
15444
|
-
|
|
15488
|
+
join9(deps.templatesDir, governed.templatePath),
|
|
15445
15489
|
"utf8"
|
|
15446
15490
|
);
|
|
15447
|
-
const absolutePath =
|
|
15491
|
+
const absolutePath = join9(deps.projectRoot, governed.targetPath);
|
|
15448
15492
|
const current = existsSync6(absolutePath) ? readFileSync7(absolutePath, "utf8") : "";
|
|
15449
15493
|
const diff = diffLines(current, content);
|
|
15450
15494
|
if (diff !== "") {
|
|
@@ -15465,7 +15509,7 @@ ${diff}`);
|
|
|
15465
15509
|
};
|
|
15466
15510
|
}
|
|
15467
15511
|
for (const write of plannedWrites) {
|
|
15468
|
-
const absolutePath =
|
|
15512
|
+
const absolutePath = join9(deps.projectRoot, write.targetPath);
|
|
15469
15513
|
mkdirSync3(dirname3(absolutePath), { recursive: true });
|
|
15470
15514
|
writeFileSync4(absolutePath, write.content);
|
|
15471
15515
|
}
|
|
@@ -15487,7 +15531,7 @@ ${diff}`);
|
|
|
15487
15531
|
|
|
15488
15532
|
// src/commands/verify-vocabulary.ts
|
|
15489
15533
|
import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
15490
|
-
import { join as
|
|
15534
|
+
import { join as join10, relative, sep } from "node:path";
|
|
15491
15535
|
|
|
15492
15536
|
// ../ogygie-core/src/vocabulary.ts
|
|
15493
15537
|
var TOKEN_PATTERN = new RegExp("\\p{L}[\\p{L}\\p{N}]*(?:[-_$][\\p{L}\\p{N}]+)*", "gu");
|
|
@@ -15632,7 +15676,7 @@ function isScannableDirectory(name) {
|
|
|
15632
15676
|
// src/commands/verify-vocabulary.ts
|
|
15633
15677
|
var CODE_EXTENSIONS = [".ts", ".tsx", ".mts", ".mjs", ".js", ".jsx"];
|
|
15634
15678
|
function readManifest(projectRoot) {
|
|
15635
|
-
const manifestPath =
|
|
15679
|
+
const manifestPath = join10(projectRoot, MANIFEST_FILE_NAME);
|
|
15636
15680
|
if (!existsSync7(manifestPath)) {
|
|
15637
15681
|
throwOgygieError({
|
|
15638
15682
|
code: "OGYGIE_CLI_USAGE",
|
|
@@ -15657,13 +15701,13 @@ function readManifest(projectRoot) {
|
|
|
15657
15701
|
return parsed.data;
|
|
15658
15702
|
}
|
|
15659
15703
|
function collectCodeFiles(projectRoot, folder) {
|
|
15660
|
-
const absoluteFolder =
|
|
15704
|
+
const absoluteFolder = join10(projectRoot, folder);
|
|
15661
15705
|
if (!existsSync7(absoluteFolder)) return [];
|
|
15662
15706
|
const files = [];
|
|
15663
15707
|
const walk = (directory) => {
|
|
15664
15708
|
for (const entry of readdirSync2(directory)) {
|
|
15665
15709
|
if (!isScannableDirectory(entry)) continue;
|
|
15666
|
-
const entryPath =
|
|
15710
|
+
const entryPath = join10(directory, entry);
|
|
15667
15711
|
if (statSync2(entryPath).isDirectory()) {
|
|
15668
15712
|
walk(entryPath);
|
|
15669
15713
|
continue;
|
|
@@ -15706,9 +15750,9 @@ function verifyVocabulary(deps) {
|
|
|
15706
15750
|
const sourceFiles = productCodeFoldersOf(manifest).flatMap(
|
|
15707
15751
|
(folder) => collectCodeFiles(deps.projectRoot, folder)
|
|
15708
15752
|
);
|
|
15709
|
-
const governed = governedFiles.filter((file2) => existsSync7(
|
|
15753
|
+
const governed = governedFiles.filter((file2) => existsSync7(join10(deps.projectRoot, file2.targetPath))).map((file2) => ({
|
|
15710
15754
|
path: file2.targetPath,
|
|
15711
|
-
content: readFileSync8(
|
|
15755
|
+
content: readFileSync8(join10(deps.projectRoot, file2.targetPath), "utf8")
|
|
15712
15756
|
}));
|
|
15713
15757
|
return auditProductVocabulary({
|
|
15714
15758
|
vocabulary: manifest.vocabulary,
|
|
@@ -15884,6 +15928,30 @@ var governedChecksumsByVersion = {
|
|
|
15884
15928
|
"scripts/verify-specs.ts": "03d58baa11822ad7ae67080f990f7a82f4f94d223b12777728b8b9ddd7ebd843",
|
|
15885
15929
|
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15886
15930
|
},
|
|
15931
|
+
"0.16.0": {
|
|
15932
|
+
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15933
|
+
"AGENTS.md": "74725a71e531502d4d4898518bad68e610a370bd410058b58e11220532dc327f",
|
|
15934
|
+
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15935
|
+
"scripts/lint-interdits.ts": "4644e1aaabc1d19ed42a0bdf8e2e55b9451f789d87d59754bce3432de7c357e7",
|
|
15936
|
+
"scripts/ogygie-design-brief.ts": "aba1585717b30808dd0bc8930a37a1ad2d83ee6869f969dbd06ca595e37a4a89",
|
|
15937
|
+
"scripts/ogygie-impact.ts": "f06cc8b4a46ddfbcfb32ba747c2454548ca49918123d04ab13d210b4a89c1fb9",
|
|
15938
|
+
"scripts/verify-contracts.ts": "9cd64c88dcc3b54b65bf1e1b7a68eb00353ee16f2a9ce8bca3c49ba063c2a08b",
|
|
15939
|
+
"scripts/verify-manifest.ts": "6c448fa3a19b87666f686ca57c6ca834d9e169b1380e4fe6e4c19b995bf94f3c",
|
|
15940
|
+
"scripts/verify-specs.ts": "03d58baa11822ad7ae67080f990f7a82f4f94d223b12777728b8b9ddd7ebd843",
|
|
15941
|
+
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15942
|
+
},
|
|
15943
|
+
"0.17.0": {
|
|
15944
|
+
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15945
|
+
"AGENTS.md": "e058a54ff4750abf19f7c4b026e8be661d8ce25419596d22c20de2c387469370",
|
|
15946
|
+
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15947
|
+
"scripts/lint-interdits.ts": "4644e1aaabc1d19ed42a0bdf8e2e55b9451f789d87d59754bce3432de7c357e7",
|
|
15948
|
+
"scripts/ogygie-design-brief.ts": "aba1585717b30808dd0bc8930a37a1ad2d83ee6869f969dbd06ca595e37a4a89",
|
|
15949
|
+
"scripts/ogygie-impact.ts": "f06cc8b4a46ddfbcfb32ba747c2454548ca49918123d04ab13d210b4a89c1fb9",
|
|
15950
|
+
"scripts/verify-contracts.ts": "9cd64c88dcc3b54b65bf1e1b7a68eb00353ee16f2a9ce8bca3c49ba063c2a08b",
|
|
15951
|
+
"scripts/verify-manifest.ts": "6c448fa3a19b87666f686ca57c6ca834d9e169b1380e4fe6e4c19b995bf94f3c",
|
|
15952
|
+
"scripts/verify-specs.ts": "03d58baa11822ad7ae67080f990f7a82f4f94d223b12777728b8b9ddd7ebd843",
|
|
15953
|
+
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15954
|
+
},
|
|
15887
15955
|
"0.2.0": {
|
|
15888
15956
|
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15889
15957
|
"AGENTS.md": "ca73ab619d8c55fac11858d815bf5fbab6537ecebe7ae4123cbdeb3e3e36bf1a",
|
|
@@ -15978,22 +16046,22 @@ var governedChecksumsByVersion = {
|
|
|
15978
16046
|
|
|
15979
16047
|
// src/paths.ts
|
|
15980
16048
|
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
15981
|
-
import { dirname as dirname4, join as
|
|
16049
|
+
import { dirname as dirname4, join as join11 } from "node:path";
|
|
15982
16050
|
import { fileURLToPath } from "node:url";
|
|
15983
|
-
var PACKAGE_ROOT =
|
|
16051
|
+
var PACKAGE_ROOT = join11(dirname4(fileURLToPath(import.meta.url)), "..");
|
|
15984
16052
|
var PackageVersionSchema2 = external_exports.object({ version: external_exports.string() });
|
|
15985
16053
|
function cliVersion() {
|
|
15986
16054
|
return PackageVersionSchema2.parse(
|
|
15987
|
-
JSON.parse(readFileSync9(
|
|
16055
|
+
JSON.parse(readFileSync9(join11(PACKAGE_ROOT, "package.json"), "utf8"))
|
|
15988
16056
|
).version;
|
|
15989
16057
|
}
|
|
15990
16058
|
function templatesDir() {
|
|
15991
|
-
const embedded =
|
|
16059
|
+
const embedded = join11(PACKAGE_ROOT, "templates");
|
|
15992
16060
|
if (existsSync8(embedded)) return embedded;
|
|
15993
16061
|
return repoTemplatesDir();
|
|
15994
16062
|
}
|
|
15995
16063
|
function repoTemplatesDir() {
|
|
15996
|
-
return
|
|
16064
|
+
return join11(PACKAGE_ROOT, "..", "..", "templates");
|
|
15997
16065
|
}
|
|
15998
16066
|
|
|
15999
16067
|
// src/cli.ts
|
|
@@ -16086,14 +16154,23 @@ ogygie verify-vocabulary \u2014 ${problems.length} probl\xE8me(s).`);
|
|
|
16086
16154
|
cliVersion: cliVersion(),
|
|
16087
16155
|
argv: process.argv.slice(3)
|
|
16088
16156
|
});
|
|
16157
|
+
} else if (command === "generate") {
|
|
16158
|
+
const exitCode = await runGenerate({
|
|
16159
|
+
projectRoot: process.cwd(),
|
|
16160
|
+
cliVersion: cliVersion(),
|
|
16161
|
+
argv: process.argv.slice(3),
|
|
16162
|
+
log: (line) => console.log(line),
|
|
16163
|
+
error: (line) => console.error(line)
|
|
16164
|
+
});
|
|
16165
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
16089
16166
|
} else {
|
|
16090
16167
|
printError(
|
|
16091
16168
|
ogygieError({
|
|
16092
16169
|
code: "OGYGIE_CLI_USAGE",
|
|
16093
16170
|
message: `Unknown command ${JSON.stringify(command ?? "")}.`,
|
|
16094
16171
|
file: null,
|
|
16095
|
-
cause: `expected one of: init <product-name>, adopt, upgrade, check, verify-vocabulary, studio, mcp (flags: --json, --yes)`,
|
|
16096
|
-
suggestedFix: "Run `ogygie init <product-name>`, `ogygie adopt`, `ogygie upgrade`, `ogygie check`, `ogygie verify-vocabulary`, `ogygie studio` or `ogygie
|
|
16172
|
+
cause: `expected one of: init <product-name>, adopt, upgrade, check, verify-vocabulary, studio, mcp, generate <check|generate|verify> (flags: --json, --yes)`,
|
|
16173
|
+
suggestedFix: "Run `ogygie init <product-name>`, `ogygie adopt`, `ogygie upgrade`, `ogygie check`, `ogygie verify-vocabulary`, `ogygie studio`, `ogygie mcp` or `ogygie generate <check|generate|verify>`."
|
|
16097
16174
|
})
|
|
16098
16175
|
);
|
|
16099
16176
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogygie/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Ogygie framework CLI: deterministic scaffolding (init), governed-file upgrades (upgrade) and lockstep verification (check) for Ogygie products.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@ogygie/core": "0.
|
|
20
|
+
"@ogygie/core": "0.17.0",
|
|
21
21
|
"@types/node": "^24.0.0",
|
|
22
22
|
"esbuild": "^0.28.0",
|
|
23
23
|
"tsx": "^4.20.0",
|
|
@@ -295,12 +295,19 @@ dérogation qui s'oublie. Elle ne couvre JAMAIS le code du produit lui-même.
|
|
|
295
295
|
### Stack imposée (toute autre lib = amendement DECISIONS.md d'abord)
|
|
296
296
|
Vite + React 19 + TanStack Router (routing EN CODE) + TanStack Query (état serveur) ;
|
|
297
297
|
PAS de lib d'état client (useState suffit) ; Tailwind v4 thémé depuis `design/tokens.json`
|
|
298
|
-
(valeurs arbitraires `p-[13px]` interdites par lint) ;
|
|
299
|
-
`
|
|
300
|
-
|
|
301
|
-
|
|
298
|
+
(valeurs arbitraires `p-[13px]` interdites par lint) ; le socle de patterns vient du
|
|
299
|
+
paquet `@ogygie/ui` — `ScreenState`, `EmptyState`, `ErrorBanner`, `EditableText`,
|
|
300
|
+
`fetchContract`, et derrière leur propre point d'entrée `DataTable`
|
|
301
|
+
(`@ogygie/ui/data-table`), `ContractForm` (`@ogygie/ui/contract-form`),
|
|
302
|
+
`VarianceChart` (`@ogygie/ui/variance-chart`) — IMPORTÉ, jamais recopié ; les
|
|
303
|
+
patterns PRODUIT vivent dans `apps/web/src/patterns/` (jamais consommés directement
|
|
304
|
+
depuis node_modules par une feature) ; react-hook-form UNIQUEMENT via `ContractForm` ;
|
|
305
|
+
TanStack Table UNIQUEMENT via `DataTable` ; Recharts UNIQUEMENT via `VarianceChart` ;
|
|
302
306
|
date-fns via les helpers `@ogygie/core` (jamais de `new Date()` nu) ; i18n Lingui
|
|
303
|
-
(ICU, extraction vérifiée en CI)
|
|
307
|
+
(ICU, extraction vérifiée en CI) — `@ogygie/ui` ne porte AUCUNE chaîne : tout texte
|
|
308
|
+
lui arrive en prop, traduit par la feature. Noms de patterns en anglais. La feuille
|
|
309
|
+
Tailwind du produit déclare le paquet comme source
|
|
310
|
+
(`@source "../node_modules/@ogygie/ui/src";`), sinon ses classes ne sont pas générées.
|
|
304
311
|
|
|
305
312
|
### Structure d'une feature front (miroir de l'API)
|
|
306
313
|
```
|
|
@@ -319,7 +326,7 @@ apps/web/features/orders/
|
|
|
319
326
|
/** @intent Data access of the orders feature. Re-validates contracts in dev. */
|
|
320
327
|
import { useQuery } from "@tanstack/react-query";
|
|
321
328
|
import { ListOrdersOutputSchema } from "@product/api/features/orders/schemas";
|
|
322
|
-
import { fetchContract } from "
|
|
329
|
+
import { fetchContract } from "@ogygie/ui"; // Zod parse of the answer; an OgygieFailure on a refused call or a broken contract
|
|
323
330
|
|
|
324
331
|
export function useOrders() {
|
|
325
332
|
return useQuery({
|
|
@@ -334,18 +341,25 @@ export function useOrders() {
|
|
|
334
341
|
// apps/web/features/orders/OrdersList.tsx
|
|
335
342
|
/** @intent Orders list screen: composition of patterns, zero business logic. */
|
|
336
343
|
import { Trans } from "@lingui/react/macro";
|
|
337
|
-
import {
|
|
338
|
-
import {
|
|
339
|
-
import { ErrorBanner } from "../../patterns/ErrorBanner";
|
|
344
|
+
import { ScreenState } from "@ogygie/ui";
|
|
345
|
+
import { DataTable } from "@ogygie/ui/data-table";
|
|
340
346
|
import { useOrders } from "./queries";
|
|
341
347
|
|
|
342
348
|
export function OrdersList() {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
+
return (
|
|
350
|
+
<ScreenState
|
|
351
|
+
query={useOrders()}
|
|
352
|
+
errorTitle={<Trans>Cette vue n'a pas pu être chargée</Trans>}
|
|
353
|
+
loading={<DataTable.Skeleton />}
|
|
354
|
+
isEmpty={(data) => data.orders.length === 0}
|
|
355
|
+
emptyState={{
|
|
356
|
+
title: <Trans>Ajoutez votre première commande</Trans>,
|
|
357
|
+
explanation: <Trans>Les commandes enregistrées apparaissent ici.</Trans>,
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
{(data) => <DataTable rows={data.orders} columns={orderColumns} rowKey={(order) => order.id} />}
|
|
361
|
+
</ScreenState>
|
|
362
|
+
);
|
|
349
363
|
}
|
|
350
364
|
```
|
|
351
365
|
|
|
@@ -353,6 +367,9 @@ export function OrdersList() {
|
|
|
353
367
|
- Un écran est une COMPOSITION de patterns : jamais de `<table>`, `<form>` ou fetch nus
|
|
354
368
|
dans une feature. Si un pattern manque, on le crée dans `patterns/` (et on met à jour
|
|
355
369
|
`design/patterns.md`), on ne bricole pas dans la feature.
|
|
370
|
+
- Les états d'un écran — accès refusé, erreur, chargement, vide — passent par
|
|
371
|
+
`ScreenState`, jamais par une cascade de `if` propre à l'écran : le corps ne reçoit
|
|
372
|
+
que des données servies.
|
|
356
373
|
- Chaque état UI listé dans la spec (vide/chargement/nominal/erreur) a son test.
|
|
357
374
|
- Toute chaîne visible passe par Lingui (`<Trans>`/`t`) — la CI casse sinon.
|
|
358
375
|
- Chiffres financiers : `tabular-nums`, alignés à droite, unité explicite.
|
|
@@ -2,27 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
> Les patterns sont les SEULES briques d'écran autorisées (AGENTS.md §Front) :
|
|
4
4
|
> un écran est une COMPOSITION de patterns, jamais de `<table>`, `<form>` ni
|
|
5
|
-
> fetch nus.
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
5
|
+
> fetch nus. Les patterns GÉNÉRIQUES ci-dessous sont LIVRÉS par le paquet
|
|
6
|
+
> `@ogygie/ui` : un produit les importe, il ne les recopie pas, et leur fiche
|
|
7
|
+
> ci-dessous est leur contrat. Un pattern PRODUIT — propre au domaine — naît
|
|
8
|
+
> dans `apps/web/src/patterns/` avec sa fiche ICI d'abord, jamais en one-off
|
|
9
|
+
> dans un écran. Tout texte lu par un humain arrive à un pattern générique en
|
|
10
|
+
> prop, traduit par la feature (Lingui) : le paquet ne porte aucune chaîne.
|
|
9
11
|
>
|
|
10
12
|
> Ce catalogue est une source de vérité du brief de design
|
|
11
13
|
> (`pnpm ogygie:design-brief <feature>`) : chaque fiche `## <Name>` devient
|
|
12
14
|
> une entrée du vocabulaire de composition. Noms en anglais (machine-lisible),
|
|
13
15
|
> fiches en français (document humain).
|
|
14
16
|
|
|
17
|
+
## ScreenState
|
|
18
|
+
Le composant d'état UNIQUE d'un écran (`@ogygie/ui`). Reçoit la lecture (`QueryLike`, sous-ensemble d'un résultat TanStack Query) et le corps en fonction de rendu, et décide dans cet ordre : accès refusé (`denied` de l'appelant, ou une réponse 401/403 quand une copie `accessDenied` est donnée), erreur (`ErrorBanner`, titré par `errorTitle`), chargement (`loading`, à défaut `Skeleton`), vide (`isEmpty` + `emptyState`, un `EmptyState` avec son action), puis le corps avec des données servies. Un corps ne branche jamais sur `undefined`.
|
|
19
|
+
|
|
15
20
|
## DataTable
|
|
16
|
-
Tableau de données (TanStack Table
|
|
21
|
+
Tableau de données (`@ogygie/ui/data-table`, TanStack Table — seul usage autorisé). Colonnes typées `{ id, header, cell, sortValue?, align? }` ; tri par clic d'en-tête (`aria-sort`), chiffres `align: "end"` en `tabular-nums` alignés à droite, unité explicite dans la cellule. États : squelette de chargement (`DataTable.Skeleton`), vide DÉLÉGUÉ (`emptyState` : un `EmptyState` qui dit pourquoi), nominal trié ; l'erreur appartient à `ScreenState`. La liste de colonnes reste stable (module ou `useMemo`).
|
|
17
22
|
|
|
18
23
|
## EmptyState
|
|
19
|
-
État vide intentionnel : un titre, une explication
|
|
24
|
+
État vide intentionnel (`@ogygie/ui`) : un titre qui nomme la CAUSE, une explication qui dit ce qui le remplirait, UNE action de création au plus. Jamais de tableau ou de liste vides nus — l'écran vide est le premier écran que voit chaque nouvel utilisateur. Deux causes, deux formulations : rien d'enregistré, ou un filtre qui ne trouve rien.
|
|
20
25
|
|
|
21
26
|
## ErrorBanner
|
|
22
|
-
Bandeau d'erreur
|
|
27
|
+
Bandeau d'erreur (`@ogygie/ui`) : affiche une `OgygieFailure` en entier — le titre du produit, le message, le code stable avec le statut et la cause, la correction suggérée — et dégrade toute autre erreur à son message. `kind: "save"` marque une écriture refusée (le produit titre autrement) ; `retry` ajoute le bouton que le produit libelle. Tout état d'erreur d'écran passe par lui — jamais de message improvisé.
|
|
23
28
|
|
|
24
29
|
## ContractForm
|
|
25
|
-
Formulaire DÉRIVÉ d'un contrat Zod (react-hook-form
|
|
30
|
+
Formulaire DÉRIVÉ d'un contrat Zod (`@ogygie/ui/contract-form`, react-hook-form — seul usage autorisé) : les champs viennent de la forme du schéma, leurs contrôles de leur genre (texte, nombre, date, sélection d'enum, case à cocher), leurs contraintes des bornes du schéma — rien n'est redéclaré à la main — et le schéma valide chaque soumission (`safeParse`). Libellés, aides, libellés d'options, `submitLabel` et `cancel` sont des props traduites ; `renderIssue` traduit la phrase d'un refus. États : saisie, soumission en cours (bouton inactif, `pending`), erreur de champ (une phrase sous le champ), erreur de formulaire (un `refine`), erreur serveur (`error` : un `ErrorBanner` save au-dessus des actions). `onSubmit` reçoit la SORTIE parsée du contrat.
|
|
26
31
|
|
|
27
32
|
## VarianceChart
|
|
28
|
-
Graphique d'écart (Recharts
|
|
33
|
+
Graphique d'écart (`@ogygie/ui/variance-chart`, Recharts — seul usage autorisé) : une valeur constatée (`measured`) contre une référence (`reference`), par libellé, en deux barres groupées ; l'écart signé se lit dans l'infobulle ; une ligne zéro quand les valeurs la traversent. Couleurs tirées des tokens du produit par variables CSS, noms de séries et unité en props traduites. États : vide DÉLÉGUÉ (`emptyState`), squelette (`VarianceChart.Skeleton`) ; chargement et erreur appartiennent à `ScreenState`. Montants en `tabular-nums`, axe et unité toujours étiquetés.
|
|
34
|
+
|
|
35
|
+
## EditableText
|
|
36
|
+
Texte qui s'écrit là où il se lit (`@ogygie/ui`) : un clic le transforme en champ, quitter le champ ou Entrée enregistre, Échap abandonne. Un lecteur sans droit voit le texte nu, jamais un contrôle inactif. Un brouillon blanc est refusé sans écriture (sauf `allowEmpty`) ; une borne (`maxLength`) arrête le champ et affiche le compteur ; `pending` rend le contrôle inactif le temps d'une écriture. `placeholder` et `ariaLabel` sont traduits par la feature.
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* `url: "/api/..."` found in a feature's routes.ts and every `pgTable("...")`
|
|
17
17
|
* found in a feature's db.schema.ts must be declared in the manifest
|
|
18
18
|
* (MANIFEST_ROUTE_UNDECLARED / MANIFEST_TABLE_UNDECLARED).
|
|
19
|
+
*
|
|
20
|
+
* A GENERATED feature (Ogygie generator, spec ogygie-generate) carries the
|
|
21
|
+
* generator's twin of a canonical file — `routes.gen.ts` where a hand-written
|
|
22
|
+
* feature has `routes.ts` — and this gate reads whichever nature is on disk,
|
|
23
|
+
* in both directions. `index.ts` keeps its canonical name by exception and
|
|
24
|
+
* `schemas.ts` has no twin: it is the human gesture the generator starts from.
|
|
19
25
|
*/
|
|
20
26
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
21
27
|
import { join } from "node:path";
|
|
@@ -148,6 +154,36 @@ for (const productPackage of manifest.packages ?? []) {
|
|
|
148
154
|
const apiApp = manifest.apps.find((app) => app.type === "api");
|
|
149
155
|
const featureNames = new Set(manifest.features.map((feature) => feature.name));
|
|
150
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Canonical file → its generated twin (`.gen` infix, Ogygie generator). A
|
|
159
|
+
* feature carries one nature or the other, never both by hand; when both
|
|
160
|
+
* exist, both are read, so a route or a table never hides in the file the gate
|
|
161
|
+
* did not open.
|
|
162
|
+
*/
|
|
163
|
+
const GENERATED_TWIN: Readonly<Record<string, string>> = {
|
|
164
|
+
"routes.ts": "routes.gen.ts",
|
|
165
|
+
"service.ts": "service.gen.ts",
|
|
166
|
+
"repository.ts": "repository.gen.ts",
|
|
167
|
+
"db.schema.ts": "db.schema.gen.ts",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
function naturesOf(fileName: string): string[] {
|
|
171
|
+
const twin = GENERATED_TWIN[fileName];
|
|
172
|
+
return twin === undefined ? [fileName] : [fileName, twin];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** `routes.ts` → `routes.ts or routes.gen.ts`, for the messages. */
|
|
176
|
+
function eitherNature(fileName: string): string {
|
|
177
|
+
return naturesOf(fileName).join(" or ");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The canonical file and/or its generated twin, as they exist in the folder. */
|
|
181
|
+
function readNatures(dir: string, fileName: string): Array<{ name: string; content: string }> {
|
|
182
|
+
return naturesOf(fileName)
|
|
183
|
+
.filter((name) => existsSync(join(dir, name)))
|
|
184
|
+
.map((name) => ({ name, content: readFileSync(join(dir, name), "utf8") }));
|
|
185
|
+
}
|
|
186
|
+
|
|
151
187
|
for (const feature of manifest.features) {
|
|
152
188
|
const manifestRef = `${MANIFEST_PATH}#features.${feature.name}`;
|
|
153
189
|
|
|
@@ -229,21 +265,22 @@ for (const feature of manifest.features) {
|
|
|
229
265
|
|
|
230
266
|
const canonicalFiles = ["index.ts", "routes.ts", "schemas.ts", "service.ts", "repository.ts"];
|
|
231
267
|
for (const fileName of canonicalFiles) {
|
|
232
|
-
if (!existsSync(join(featureDir,
|
|
268
|
+
if (!naturesOf(fileName).some((name) => existsSync(join(featureDir, name)))) {
|
|
233
269
|
report({
|
|
234
270
|
code: "FEATURE_CANONICAL_FILE_MISSING",
|
|
235
|
-
message: `Feature "${feature.name}" is missing canonical file ${fileName}.`,
|
|
271
|
+
message: `Feature "${feature.name}" is missing canonical file ${eitherNature(fileName)}.`,
|
|
236
272
|
file: join(apiApp.path, feature.folder, fileName),
|
|
237
|
-
cause: `${fileName} absent (AGENTS.md §Structure d'une feature)`,
|
|
238
|
-
suggestedFix: "Create the file following the canonical template in AGENTS.md.",
|
|
273
|
+
cause: `${eitherNature(fileName)} absent (AGENTS.md §Structure d'une feature; the generated twin counts)`,
|
|
274
|
+
suggestedFix: "Create the file following the canonical template in AGENTS.md, or generate the feature (spec ogygie-generate).",
|
|
239
275
|
});
|
|
240
276
|
}
|
|
241
277
|
}
|
|
242
278
|
|
|
243
279
|
const schemasPath = join(featureDir, "schemas.ts");
|
|
244
|
-
const routesPath = join(featureDir, "routes.ts");
|
|
245
280
|
const schemasContent = existsSync(schemasPath) ? readFileSync(schemasPath, "utf8") : "";
|
|
246
|
-
const
|
|
281
|
+
const routeNatures = readNatures(featureDir, "routes.ts");
|
|
282
|
+
const routesFile = routeNatures[0]?.name ?? "routes.ts";
|
|
283
|
+
const routesContent = routeNatures.map((nature) => nature.content).join("\n");
|
|
247
284
|
|
|
248
285
|
for (const route of feature.routes) {
|
|
249
286
|
for (const schemaName of [route.schemaInput, route.schemaOutput]) {
|
|
@@ -261,26 +298,27 @@ for (const feature of manifest.features) {
|
|
|
261
298
|
if (!routesContent.includes(`"${route.path}"`)) {
|
|
262
299
|
report({
|
|
263
300
|
code: "MANIFEST_ROUTE_NOT_FOUND",
|
|
264
|
-
message: `Route ${route.method} ${route.path} is declared in the manifest but absent from
|
|
265
|
-
file: join(apiApp.path, feature.folder,
|
|
266
|
-
cause: `"${route.path}" not found in routes.ts`,
|
|
267
|
-
suggestedFix: "Register the route in routes.ts or remove it from the manifest.",
|
|
301
|
+
message: `Route ${route.method} ${route.path} is declared in the manifest but absent from ${routesFile}.`,
|
|
302
|
+
file: join(apiApp.path, feature.folder, routesFile),
|
|
303
|
+
cause: `"${route.path}" not found in ${eitherNature("routes.ts")}`,
|
|
304
|
+
suggestedFix: "Register the route in routes.ts (or regenerate routes.gen.ts from the spec) or remove it from the manifest.",
|
|
268
305
|
});
|
|
269
306
|
}
|
|
270
307
|
}
|
|
271
308
|
|
|
272
309
|
if (feature.dbTables.length > 0) {
|
|
273
|
-
const
|
|
274
|
-
const
|
|
310
|
+
const dbSchemaNatures = readNatures(featureDir, "db.schema.ts");
|
|
311
|
+
const dbSchemaFile = dbSchemaNatures[0]?.name ?? "db.schema.ts";
|
|
312
|
+
const dbSchemaContent = dbSchemaNatures.map((nature) => nature.content).join("\n");
|
|
275
313
|
for (const table of dbTableNamesOf(feature)) {
|
|
276
314
|
const tableDefinition = new RegExp(`pgTable\\(\\s*"${table}"`);
|
|
277
315
|
if (!tableDefinition.test(dbSchemaContent)) {
|
|
278
316
|
report({
|
|
279
317
|
code: "MANIFEST_TABLE_NOT_FOUND",
|
|
280
|
-
message: `Feature "${feature.name}" declares table "${table}" but
|
|
281
|
-
file: join(apiApp.path, feature.folder,
|
|
282
|
-
cause: `pgTable("${table}") not found`,
|
|
283
|
-
suggestedFix: "Define the table in db.schema.ts or remove it from dbTables.",
|
|
318
|
+
message: `Feature "${feature.name}" declares table "${table}" but ${dbSchemaFile} does not define it.`,
|
|
319
|
+
file: join(apiApp.path, feature.folder, dbSchemaFile),
|
|
320
|
+
cause: `pgTable("${table}") not found in ${eitherNature("db.schema.ts")}`,
|
|
321
|
+
suggestedFix: "Define the table in db.schema.ts (or regenerate db.schema.gen.ts from the spec) or remove it from dbTables.",
|
|
284
322
|
});
|
|
285
323
|
}
|
|
286
324
|
}
|
|
@@ -297,7 +335,7 @@ for (const feature of manifest.features) {
|
|
|
297
335
|
report({
|
|
298
336
|
code: "SHARED_TABLE_WITH_TENANT_COLUMN",
|
|
299
337
|
message: `Table "${finding.table}" is declared shared in the manifest but its Drizzle definition still carries "${finding.tenantColumn}".`,
|
|
300
|
-
file: join(apiApp.path, feature.folder,
|
|
338
|
+
file: join(apiApp.path, feature.folder, dbSchemaFile),
|
|
301
339
|
cause: `pgTable("${finding.table}") declares column "${finding.tenantColumn}" while the manifest says shared: true`,
|
|
302
340
|
suggestedFix:
|
|
303
341
|
"Either drop the tenant column (the table is a shared referential: no tenant owns it, no RLS tenant policy), or remove `shared: true` and go back through tenantDb.",
|
|
@@ -333,16 +371,14 @@ if (apiApp !== undefined) {
|
|
|
333
371
|
? ` (no manifest entry has folder=${relativeFolder})`
|
|
334
372
|
: "";
|
|
335
373
|
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
const routesContent = readFileSync(routesPath, "utf8");
|
|
339
|
-
for (const match of routesContent.matchAll(/url:\s*"(\/api\/[^"]*)"/g)) {
|
|
374
|
+
for (const nature of readNatures(join(featuresRoot, dirName), "routes.ts")) {
|
|
375
|
+
for (const match of nature.content.matchAll(/url:\s*"(\/api\/[^"]*)"/g)) {
|
|
340
376
|
const routePath = match[1];
|
|
341
377
|
if (routePath !== undefined && !declaredPaths.has(routePath)) {
|
|
342
378
|
report({
|
|
343
379
|
code: "MANIFEST_ROUTE_UNDECLARED",
|
|
344
|
-
message: `Route ${routePath} is registered in ${relativeFolder}
|
|
345
|
-
file: join(apiApp.path, relativeFolder,
|
|
380
|
+
message: `Route ${routePath} is registered in ${relativeFolder}/${nature.name} but not declared in the manifest.`,
|
|
381
|
+
file: join(apiApp.path, relativeFolder, nature.name),
|
|
346
382
|
cause: `url: "${routePath}" found in code, absent from the manifest${missingEntryCause}`,
|
|
347
383
|
suggestedFix:
|
|
348
384
|
"Declare the route (method, path, schemas) in ogygie.manifest.json — the manifest must never lie by omission.",
|
|
@@ -351,16 +387,14 @@ if (apiApp !== undefined) {
|
|
|
351
387
|
}
|
|
352
388
|
}
|
|
353
389
|
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
const dbSchemaContent = readFileSync(dbSchemaPath, "utf8");
|
|
357
|
-
for (const match of dbSchemaContent.matchAll(/pgTable\(\s*"([^"]+)"/g)) {
|
|
390
|
+
for (const nature of readNatures(join(featuresRoot, dirName), "db.schema.ts")) {
|
|
391
|
+
for (const match of nature.content.matchAll(/pgTable\(\s*"([^"]+)"/g)) {
|
|
358
392
|
const tableName = match[1];
|
|
359
393
|
if (tableName !== undefined && !declaredTables.has(tableName)) {
|
|
360
394
|
report({
|
|
361
395
|
code: "MANIFEST_TABLE_UNDECLARED",
|
|
362
|
-
message: `Table "${tableName}" is defined in ${relativeFolder}
|
|
363
|
-
file: join(apiApp.path, relativeFolder,
|
|
396
|
+
message: `Table "${tableName}" is defined in ${relativeFolder}/${nature.name} but not declared in the manifest.`,
|
|
397
|
+
file: join(apiApp.path, relativeFolder, nature.name),
|
|
364
398
|
cause: `pgTable("${tableName}") found in code, absent from dbTables${missingEntryCause}`,
|
|
365
399
|
suggestedFix:
|
|
366
400
|
"Add the table to the feature's dbTables in ogygie.manifest.json (one table = one owner).",
|