@saasicat/cli 0.10.1 → 0.11.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // `saas-platform` — bootstrap CLI for the SaaS platform.
2
+ // `saasicat` — bootstrap CLI for the SaaSiCat framework.
3
3
  //
4
4
  // Sub-commands:
5
5
  // schema apply [--prisma-schema=PATH] [--fragments=01,02,03]
@@ -7,6 +7,10 @@
7
7
  // Inserts missing platform Prisma models into your schema.prisma.
8
8
  // Idempotent; existing models are left untouched.
9
9
  //
10
+ // schema check [--prisma-schema=PATH] [--fragments=01,02,03]
11
+ // Reports what your schema is missing relative to the canonical
12
+ // fragments. Read-only; exits 1 on drift so CI can gate on it.
13
+ //
10
14
  // Spec: handoff/superadmin/QUICKSTART_SIMPLIFICATIONS.md §P5.
11
15
 
12
16
  import { readFile, writeFile, readdir } from 'node:fs/promises';
@@ -15,7 +19,7 @@ import { dirname, join, resolve } from 'node:path';
15
19
  import { createRequire } from 'node:module';
16
20
  import { spawn } from 'node:child_process';
17
21
 
18
- import { applyFragmentBlocks, extractModelBlocks } from '../dist/index.js';
22
+ import { applyFragmentBlocks, checkSchema, extractModelBlocks } from '../dist/index.js';
19
23
 
20
24
  const require_ = createRequire(import.meta.url);
21
25
 
@@ -46,14 +50,14 @@ function resolveFragmentsDir() {
46
50
  return candidate;
47
51
  }
48
52
 
49
- async function loadFragments(dir, filter) {
53
+ async function selectFragmentFiles(dir, filter) {
50
54
  const files = (await readdir(dir)).filter((f) => f.endsWith('.prisma')).sort();
51
- const selected = filter
52
- ? files.filter((f) => {
53
- const prefix = f.split('-')[0];
54
- return filter.includes(prefix);
55
- })
56
- : files;
55
+ if (!filter) return files;
56
+ return files.filter((f) => filter.includes(f.split('-')[0]));
57
+ }
58
+
59
+ async function loadFragments(dir, filter) {
60
+ const selected = await selectFragmentFiles(dir, filter);
57
61
  const blocks = new Map();
58
62
  for (const file of selected) {
59
63
  const content = await readFile(join(dir, file), 'utf8');
@@ -67,17 +71,24 @@ async function loadFragments(dir, filter) {
67
71
  return { files: selected, blocks };
68
72
  }
69
73
 
70
- async function cmdSchemaApply(args) {
74
+ function parseFragmentFilter(raw) {
75
+ return raw ? raw.split(',').map((s) => s.padStart(2, '0')) : null;
76
+ }
77
+
78
+ async function readSchemaOrExit(args) {
71
79
  const schemaPath = resolve(args['prisma-schema'] ?? 'prisma/schema.prisma');
72
80
  if (!existsSync(schemaPath)) {
73
81
  console.error(`✗ schema.prisma nicht gefunden: ${schemaPath}`);
74
82
  process.exit(1);
75
83
  }
84
+ return { schemaPath, schema: await readFile(schemaPath, 'utf8') };
85
+ }
86
+
87
+ async function cmdSchemaApply(args) {
88
+ const { schemaPath, schema } = await readSchemaOrExit(args);
76
89
 
77
90
  const fragmentsDir = resolveFragmentsDir();
78
- const filter = args.fragments
79
- ? args.fragments.split(',').map((s) => s.padStart(2, '0'))
80
- : null;
91
+ const filter = parseFragmentFilter(args.fragments);
81
92
  if (!filter && !args.all) {
82
93
  console.error(
83
94
  '✗ Entweder --fragments=01,02,03 oder --all übergeben. ' +
@@ -94,7 +105,6 @@ async function cmdSchemaApply(args) {
94
105
  process.exit(1);
95
106
  }
96
107
 
97
- const schema = await readFile(schemaPath, 'utf8');
98
108
  const result = applyFragmentBlocks(schema, blocks, {
99
109
  fragmentLabel: files.join(', '),
100
110
  });
@@ -125,6 +135,82 @@ async function cmdSchemaApply(args) {
125
135
  console.log(' 2. pnpm prisma migrate dev --name add_saas_platform');
126
136
  }
127
137
 
138
+ const MISMATCH_LABELS = {
139
+ type: 'Typ',
140
+ optionality: 'Optionalität',
141
+ list: 'Liste',
142
+ };
143
+
144
+ function printCheckReport(report) {
145
+ if (report.missingFields.length > 0) {
146
+ console.log(`✗ Fehlende Felder (${report.missingFields.length}):`);
147
+ for (const { model, field, type } of report.missingFields) {
148
+ console.log(` ${`${model}.${field}`.padEnd(44)} ${type}`);
149
+ }
150
+ console.log('');
151
+ }
152
+
153
+ if (report.missingEnumValues.length > 0) {
154
+ console.log(`✗ Fehlende Enum-Werte (${report.missingEnumValues.length}):`);
155
+ for (const entry of report.missingEnumValues) {
156
+ console.log(` ${entry.enum}.${entry.value}`);
157
+ }
158
+ console.log('');
159
+ }
160
+
161
+ if (report.fieldMismatches.length > 0) {
162
+ console.log(`✗ Abweichende Felder (${report.fieldMismatches.length}):`);
163
+ for (const { model, field, reason, expected, actual } of report.fieldMismatches) {
164
+ const location = `${model}.${field}`.padEnd(44);
165
+ console.log(
166
+ ` ${location} [${MISMATCH_LABELS[reason]}] erwartet ${expected}, vorhanden ${actual}`,
167
+ );
168
+ }
169
+ console.log('');
170
+ }
171
+
172
+ const absent = [...report.absentModels, ...report.absentEnums];
173
+ if (absent.length > 0) {
174
+ console.log(`→ Nicht übernommen (${absent.length}): ${absent.join(', ')}`);
175
+ console.log(' Kein Fehler — diese Fragmente nutzt die App nicht.');
176
+ console.log('');
177
+ }
178
+ }
179
+
180
+ async function cmdSchemaCheck(args) {
181
+ const { schemaPath, schema } = await readSchemaOrExit(args);
182
+ const fragmentsDir = resolveFragmentsDir();
183
+ const filter = parseFragmentFilter(args.fragments);
184
+ const files = await selectFragmentFiles(fragmentsDir, filter);
185
+
186
+ if (files.length === 0) {
187
+ console.error('✗ Keine Fragmente ausgewählt.');
188
+ process.exit(1);
189
+ }
190
+
191
+ const fragments = [];
192
+ for (const file of files) {
193
+ fragments.push(await readFile(join(fragmentsDir, file), 'utf8'));
194
+ }
195
+
196
+ console.log(`→ Prüfe ${schemaPath} gegen ${files.length} Fragment(e) aus @saasicat/spec`);
197
+ console.log('');
198
+
199
+ const report = checkSchema(fragments.join('\n'), schema);
200
+ printCheckReport(report);
201
+
202
+ const checked = `${report.checkedModelCount} Model(s), ${report.checkedEnumCount} Enum(s)`;
203
+ if (report.ok) {
204
+ console.log(`✓ Kein Drift. ${checked} geprüft.`);
205
+ return;
206
+ }
207
+
208
+ console.log(`✗ Drift gefunden (${checked} geprüft).`);
209
+ console.log(' Fehlende Felder und Enum-Werte ergänzen, dann migrieren.');
210
+ console.log(' Abweichende Felder prüfen: Plattform-Code liest sie mit dem Spec-Typ.');
211
+ process.exit(1);
212
+ }
213
+
128
214
  function runChild(cmd, args, opts = {}) {
129
215
  return new Promise((resolve_, reject) => {
130
216
  const proc = spawn(cmd, args, { stdio: 'inherit', ...opts });
@@ -142,7 +228,7 @@ async function cmdSchemaMigrate(args) {
142
228
  process.exit(1);
143
229
  }
144
230
 
145
- console.log(`→ Schritt 1/2: saas-platform schema apply ${args['fragments'] ? `--fragments=${args['fragments']}` : '--all'}`);
231
+ console.log(`→ Schritt 1/2: saasicat schema apply ${args['fragments'] ? `--fragments=${args['fragments']}` : '--all'}`);
146
232
  await cmdSchemaApply({
147
233
  ...args,
148
234
  all: args['fragments'] ? undefined : true,
@@ -159,16 +245,21 @@ async function main() {
159
245
  if (cmd === 'schema' && sub === 'apply') {
160
246
  return cmdSchemaApply(parseArgs(rest));
161
247
  }
248
+ if (cmd === 'schema' && sub === 'check') {
249
+ return cmdSchemaCheck(parseArgs(rest));
250
+ }
162
251
  if (cmd === 'schema' && sub === 'migrate') {
163
252
  return cmdSchemaMigrate(parseArgs(rest));
164
253
  }
165
254
  if (cmd === '--help' || cmd === '-h' || !cmd) {
166
- console.log('Usage: saas-platform <command> [...args]');
255
+ console.log('Usage: saasicat <command> [...args]');
167
256
  console.log('');
168
257
  console.log('Commands:');
169
258
  console.log(' schema apply --all alle Plattform-Models einfügen');
170
259
  console.log(' schema apply --fragments=01,02 nur diese Fragmente einfügen');
171
260
  console.log(' schema apply --dry-run nur Diff ausgeben');
261
+ console.log(' schema check Drift gegen @saasicat/spec melden');
262
+ console.log(' schema check --fragments=01,02 nur diese Fragmente prüfen');
172
263
  console.log(' schema migrate --name=<name> apply --all + prisma migrate dev');
173
264
  console.log('');
174
265
  console.log('Optional --prisma-schema=PATH (default prisma/schema.prisma).');
package/dist/index.cjs CHANGED
@@ -67,8 +67,16 @@ __export(index_exports, {
67
67
  UserPortDoctorCheck: () => UserPortDoctorCheck,
68
68
  WhoAmIFlow: () => WhoAmIFlow,
69
69
  applyFragmentBlocks: () => applyFragmentBlocks,
70
+ blockBodyLines: () => blockBodyLines,
71
+ checkSchema: () => checkSchema,
72
+ extractBlockNames: () => extractBlockNames,
73
+ extractBlocks: () => extractBlocks,
70
74
  extractModelBlocks: () => extractModelBlocks,
71
- extractModelNames: () => extractModelNames
75
+ extractModelNames: () => extractModelNames,
76
+ parseEnumValues: () => parseEnumValues,
77
+ parseFields: () => parseFields,
78
+ parseSchema: () => parseSchema,
79
+ stripLineComment: () => stripLineComment
72
80
  });
73
81
  module.exports = __toCommonJS(index_exports);
74
82
 
@@ -1099,51 +1107,48 @@ var PLATFORM_DOCTOR_CHECK_PROVIDERS = [
1099
1107
  AdminManifestDoctorCheck
1100
1108
  ];
1101
1109
 
1102
- // src/schema-apply.ts
1110
+ // src/prisma-blocks.ts
1103
1111
  function stripLineComment(line) {
1104
1112
  const commentStart = line.indexOf("//");
1105
1113
  return commentStart === -1 ? line : line.slice(0, commentStart);
1106
1114
  }
1107
1115
  __name(stripLineComment, "stripLineComment");
1108
- function extractModelNames(schema) {
1116
+ function declarationPattern(keyword) {
1117
+ return new RegExp(`^\\s*${keyword}\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\{`);
1118
+ }
1119
+ __name(declarationPattern, "declarationPattern");
1120
+ function extractBlockNames(schema, keyword) {
1121
+ const pattern = declarationPattern(keyword);
1109
1122
  const names = [];
1110
- const lines = schema.split("\n");
1111
- for (const line of lines) {
1112
- const stripped = stripLineComment(line).trim();
1113
- const match = stripped.match(/^model\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/);
1123
+ for (const line of schema.split("\n")) {
1124
+ const match = stripLineComment(line).match(pattern);
1114
1125
  if (match) names.push(match[1]);
1115
1126
  }
1116
1127
  return names;
1117
1128
  }
1118
- __name(extractModelNames, "extractModelNames");
1119
- function extractModelBlocks(fragment) {
1129
+ __name(extractBlockNames, "extractBlockNames");
1130
+ function extractBlocks(schema, keyword) {
1131
+ const pattern = declarationPattern(keyword);
1120
1132
  const blocks = /* @__PURE__ */ new Map();
1121
- const lines = fragment.split("\n");
1122
1133
  let current = null;
1123
- for (const rawLine of lines) {
1134
+ for (const rawLine of schema.split("\n")) {
1124
1135
  const stripped = stripLineComment(rawLine);
1136
+ const openCount = (stripped.match(/\{/g) ?? []).length;
1137
+ const closeCount = (stripped.match(/\}/g) ?? []).length;
1125
1138
  if (!current) {
1126
- const match = rawLine.match(/^\s*model\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/);
1127
- if (match) {
1128
- const openCount = (stripped.match(/\{/g) ?? []).length;
1129
- const closeCount = (stripped.match(/\}/g) ?? []).length;
1130
- current = {
1131
- name: match[1],
1132
- lines: [
1133
- rawLine
1134
- ],
1135
- depth: openCount - closeCount
1136
- };
1137
- if (current.depth === 0) {
1138
- blocks.set(current.name, current.lines.join("\n"));
1139
- current = null;
1140
- }
1141
- }
1142
- continue;
1139
+ const match = stripped.match(pattern);
1140
+ if (!match) continue;
1141
+ current = {
1142
+ name: match[1],
1143
+ lines: [
1144
+ rawLine
1145
+ ],
1146
+ depth: openCount - closeCount
1147
+ };
1148
+ } else {
1149
+ current.lines.push(rawLine);
1150
+ current.depth += openCount - closeCount;
1143
1151
  }
1144
- current.lines.push(rawLine);
1145
- current.depth += (stripped.match(/\{/g) ?? []).length;
1146
- current.depth -= (stripped.match(/\}/g) ?? []).length;
1147
1152
  if (current.depth <= 0) {
1148
1153
  blocks.set(current.name, current.lines.join("\n"));
1149
1154
  current = null;
@@ -1151,6 +1156,23 @@ function extractModelBlocks(fragment) {
1151
1156
  }
1152
1157
  return blocks;
1153
1158
  }
1159
+ __name(extractBlocks, "extractBlocks");
1160
+ function blockBodyLines(block) {
1161
+ const bodyStart = block.indexOf("{");
1162
+ const bodyEnd = block.lastIndexOf("}");
1163
+ if (bodyStart === -1 || bodyEnd <= bodyStart) return [];
1164
+ return block.slice(bodyStart + 1, bodyEnd).split("\n").map((line) => stripLineComment(line).trim()).filter((line) => line.length > 0 && !line.startsWith("@@"));
1165
+ }
1166
+ __name(blockBodyLines, "blockBodyLines");
1167
+
1168
+ // src/schema-apply.ts
1169
+ function extractModelNames(schema) {
1170
+ return extractBlockNames(schema, "model");
1171
+ }
1172
+ __name(extractModelNames, "extractModelNames");
1173
+ function extractModelBlocks(fragment) {
1174
+ return extractBlocks(fragment, "model");
1175
+ }
1154
1176
  __name(extractModelBlocks, "extractModelBlocks");
1155
1177
  function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
1156
1178
  const existing = new Set(extractModelNames(schema));
@@ -1175,11 +1197,11 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
1175
1197
  const header = options.fragmentLabel ? `
1176
1198
 
1177
1199
  // ============================================================
1178
- // Eingef\xFCgt durch \`saas-platform schema apply\` aus ${options.fragmentLabel}
1200
+ // Eingef\xFCgt durch \`saasicat schema apply\` aus ${options.fragmentLabel}
1179
1201
  // ============================================================
1180
1202
  ` : `
1181
1203
 
1182
- // Eingef\xFCgt durch \`saas-platform schema apply\`
1204
+ // Eingef\xFCgt durch \`saasicat schema apply\`
1183
1205
  `;
1184
1206
  const trimmedSchema = schema.endsWith("\n") ? schema : schema + "\n";
1185
1207
  return {
@@ -1190,6 +1212,130 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
1190
1212
  }
1191
1213
  __name(applyFragmentBlocks, "applyFragmentBlocks");
1192
1214
 
1215
+ // src/schema-check.ts
1216
+ function renderType(signature) {
1217
+ return `${signature.type}${signature.list ? "[]" : ""}${signature.optional ? "?" : ""}`;
1218
+ }
1219
+ __name(renderType, "renderType");
1220
+ function parseFields(block) {
1221
+ const fields = /* @__PURE__ */ new Map();
1222
+ for (const line of blockBodyLines(block)) {
1223
+ const [name, rawType] = line.split(/\s+/);
1224
+ if (!rawType || name.startsWith("@")) continue;
1225
+ const list = rawType.includes("[]");
1226
+ const optional = rawType.endsWith("?");
1227
+ fields.set(name, {
1228
+ name,
1229
+ type: rawType.replace("[]", "").replace("?", ""),
1230
+ optional,
1231
+ list
1232
+ });
1233
+ }
1234
+ return fields;
1235
+ }
1236
+ __name(parseFields, "parseFields");
1237
+ function parseEnumValues(block) {
1238
+ const values = [];
1239
+ for (const line of blockBodyLines(block)) {
1240
+ for (const token of line.split(/\s+/)) {
1241
+ if (token.startsWith("@")) break;
1242
+ if (token.length > 0) values.push(token);
1243
+ }
1244
+ }
1245
+ return values;
1246
+ }
1247
+ __name(parseEnumValues, "parseEnumValues");
1248
+ function parseSchema(schema) {
1249
+ const models = /* @__PURE__ */ new Map();
1250
+ for (const [name, block] of extractBlocks(schema, "model")) {
1251
+ models.set(name, parseFields(block));
1252
+ }
1253
+ const enums = /* @__PURE__ */ new Map();
1254
+ for (const [name, block] of extractBlocks(schema, "enum")) {
1255
+ enums.set(name, parseEnumValues(block));
1256
+ }
1257
+ return {
1258
+ models,
1259
+ enums
1260
+ };
1261
+ }
1262
+ __name(parseSchema, "parseSchema");
1263
+ function isDocumentedTypeSubstitution(spec, app, appEnums) {
1264
+ return spec.type === "String" && appEnums.has(app.type);
1265
+ }
1266
+ __name(isDocumentedTypeSubstitution, "isDocumentedTypeSubstitution");
1267
+ function compareFields(model, specFields, appFields, appEnums, missingFields, fieldMismatches) {
1268
+ for (const [name, spec] of specFields) {
1269
+ const app = appFields.get(name);
1270
+ if (!app) {
1271
+ missingFields.push({
1272
+ model,
1273
+ field: name,
1274
+ type: renderType(spec)
1275
+ });
1276
+ continue;
1277
+ }
1278
+ const mismatch = /* @__PURE__ */ __name((reason) => ({
1279
+ model,
1280
+ field: name,
1281
+ reason,
1282
+ expected: renderType(spec),
1283
+ actual: renderType(app)
1284
+ }), "mismatch");
1285
+ if (spec.type !== app.type && !isDocumentedTypeSubstitution(spec, app, appEnums)) {
1286
+ fieldMismatches.push(mismatch("type"));
1287
+ } else if (spec.list !== app.list) {
1288
+ fieldMismatches.push(mismatch("list"));
1289
+ } else if (!spec.optional && app.optional) {
1290
+ fieldMismatches.push(mismatch("optionality"));
1291
+ }
1292
+ }
1293
+ }
1294
+ __name(compareFields, "compareFields");
1295
+ function checkSchema(specSchema, appSchema) {
1296
+ const spec = parseSchema(specSchema);
1297
+ const app = parseSchema(appSchema);
1298
+ const absentModels = [];
1299
+ const missingFields = [];
1300
+ const fieldMismatches = [];
1301
+ for (const [model, specFields] of spec.models) {
1302
+ const appFields = app.models.get(model);
1303
+ if (!appFields) {
1304
+ absentModels.push(model);
1305
+ continue;
1306
+ }
1307
+ compareFields(model, specFields, appFields, app.enums, missingFields, fieldMismatches);
1308
+ }
1309
+ const absentEnums = [];
1310
+ const missingEnumValues = [];
1311
+ for (const [name, specValues] of spec.enums) {
1312
+ const appValues = app.enums.get(name);
1313
+ if (!appValues) {
1314
+ absentEnums.push(name);
1315
+ continue;
1316
+ }
1317
+ for (const value of specValues) {
1318
+ if (!appValues.includes(value)) {
1319
+ missingEnumValues.push({
1320
+ enum: name,
1321
+ value
1322
+ });
1323
+ }
1324
+ }
1325
+ }
1326
+ return {
1327
+ absentModels,
1328
+ absentEnums,
1329
+ missingFields,
1330
+ missingEnumValues,
1331
+ fieldMismatches,
1332
+ checkedModelCount: spec.models.size - absentModels.length,
1333
+ checkedEnumCount: spec.enums.size - absentEnums.length,
1334
+ ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0
1335
+ };
1336
+ }
1337
+ __name(checkSchema, "checkSchema");
1338
+
1193
1339
  // src/module.ts
1194
1340
  var import_common8 = require("@nestjs/common");
1195
1341
  var import_nest5 = require("@saasicat/nest");
@@ -2279,6 +2425,14 @@ UserCommands = _ts_decorate14([
2279
2425
  UserPortDoctorCheck,
2280
2426
  WhoAmIFlow,
2281
2427
  applyFragmentBlocks,
2428
+ blockBodyLines,
2429
+ checkSchema,
2430
+ extractBlockNames,
2431
+ extractBlocks,
2282
2432
  extractModelBlocks,
2283
- extractModelNames
2433
+ extractModelNames,
2434
+ parseEnumValues,
2435
+ parseFields,
2436
+ parseSchema,
2437
+ stripLineComment
2284
2438
  });
package/dist/index.d.cts CHANGED
@@ -326,17 +326,38 @@ declare class AdminManifestDoctorCheck implements DoctorCheck {
326
326
  */
327
327
  declare const PLATFORM_DOCTOR_CHECK_PROVIDERS: Array<Type<DoctorCheck>>;
328
328
 
329
+ /**
330
+ * Cuts off a trailing `//` comment. Deliberately index-based instead of
331
+ * `replace(/\/\/.*$/, '')`: the regex backtracks quadratically on lines with
332
+ * many single slashes.
333
+ */
334
+ declare function stripLineComment(line: string): string;
335
+ /** Returns the names of all top-level `<keyword> X { … }` blocks. */
336
+ declare function extractBlockNames(schema: string, keyword: string): string[];
337
+ /**
338
+ * Returns all top-level `<keyword> X { … }` blocks as a map
339
+ * `name -> complete block text incl. opening/closing braces`.
340
+ *
341
+ * Delimiting logic: we search for `^<keyword> X {` (start of line, possibly
342
+ * with leading whitespace) and close as soon as the brace depth drops back
343
+ * to 0. Braces inside comments are not counted.
344
+ */
345
+ declare function extractBlocks(schema: string, keyword: string): Map<string, string>;
346
+ /**
347
+ * Returns a block's body lines — comments stripped, blank lines, block-level
348
+ * attributes (`@@index`, `@@map`) and the enclosing braces removed.
349
+ *
350
+ * Cuts between the first `{` and the last `}` rather than dropping the first
351
+ * and last line, so single-line blocks (`model X { id String @id }`) yield
352
+ * their body too.
353
+ */
354
+ declare function blockBodyLines(block: string): string[];
355
+
329
356
  /** Returns the names of all top-level `model X { ... }` blocks in the schema. */
330
357
  declare function extractModelNames(schema: string): string[];
331
358
  /**
332
359
  * Returns all `model X { ... }` blocks from a fragment as a map
333
360
  * `name -> complete block text incl. opening/closing braces`.
334
- *
335
- * Delimiting logic: we search for `^model X {` (start of line, possibly with
336
- * leading whitespace) and close as soon as the brace depth drops back to
337
- * 0. Strings/comments inside the block are, in simplified terms, not
338
- * counted, which is robust for Prisma schemas (no curly braces
339
- * in strings, comments only `//`-style).
340
361
  */
341
362
  declare function extractModelBlocks(fragment: string): Map<string, string>;
342
363
  interface ApplyResult {
@@ -355,6 +376,65 @@ declare function applyFragmentBlocks(schema: string, fragmentBlocks: Map<string,
355
376
  fragmentLabel?: string;
356
377
  }): ApplyResult;
357
378
 
379
+ interface FieldSignature {
380
+ name: string;
381
+ /** Type without `?`/`[]` modifiers, e.g. `String`, `BillingCycle`. */
382
+ type: string;
383
+ optional: boolean;
384
+ list: boolean;
385
+ }
386
+ interface ParsedSchema {
387
+ models: Map<string, Map<string, FieldSignature>>;
388
+ enums: Map<string, string[]>;
389
+ }
390
+ interface MissingField {
391
+ model: string;
392
+ field: string;
393
+ /** Rendered spec type incl. modifiers, e.g. `DateTime?`. */
394
+ type: string;
395
+ }
396
+ interface MissingEnumValue {
397
+ enum: string;
398
+ value: string;
399
+ }
400
+ type FieldMismatchReason = 'type' | 'optionality' | 'list';
401
+ interface FieldMismatch {
402
+ model: string;
403
+ field: string;
404
+ reason: FieldMismatchReason;
405
+ expected: string;
406
+ actual: string;
407
+ }
408
+ interface SchemaCheckReport {
409
+ /** Platform models the consumer does not carry — informational. */
410
+ absentModels: string[];
411
+ /** Platform enums the consumer does not carry — informational. */
412
+ absentEnums: string[];
413
+ missingFields: MissingField[];
414
+ missingEnumValues: MissingEnumValue[];
415
+ fieldMismatches: FieldMismatch[];
416
+ /** Models present in both schemas, i.e. actually compared. */
417
+ checkedModelCount: number;
418
+ /** Enums present in both schemas, i.e. actually compared. */
419
+ checkedEnumCount: number;
420
+ /** True when nothing that breaks platform code was found. */
421
+ ok: boolean;
422
+ }
423
+ /** Parses the field lines of a `model` block into signatures, keyed by name. */
424
+ declare function parseFields(block: string): Map<string, FieldSignature>;
425
+ /**
426
+ * Parses the members of an `enum` block. Values may share a line
427
+ * (`enum Role { ADMIN USER }`); a trailing `@map(…)` attribute is not a value.
428
+ */
429
+ declare function parseEnumValues(block: string): string[];
430
+ /** Parses every `model` and `enum` declaration of a Prisma schema. */
431
+ declare function parseSchema(schema: string): ParsedSchema;
432
+ /**
433
+ * Compares a consumer schema against the canonical fragments. `specSchema` is
434
+ * the concatenation of the fragments the check should cover.
435
+ */
436
+ declare function checkSchema(specSchema: string, appSchema: string): SchemaCheckReport;
437
+
358
438
  interface CliContextModuleOptions {
359
439
  config: CliContextConfig;
360
440
  userPort: ProviderSpec<UserPort>;
@@ -545,4 +625,4 @@ declare class UserCommands extends CommandRunner {
545
625
  parsePassword(val: string): string;
546
626
  }
547
627
 
548
- export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, CLI_CONTEXT_CONFIG_TOKEN, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, PLATFORM_DOCTOR_CHECK_PROVIDERS, PlanCatalogDoctorCheck, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, applyFragmentBlocks, extractModelBlocks, extractModelNames };
628
+ export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, CLI_CONTEXT_CONFIG_TOKEN, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type FieldMismatch, type FieldMismatchReason, type FieldSignature, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingEnumValue, type MissingField, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, PlanCatalogDoctorCheck, type SchemaCheckReport, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, applyFragmentBlocks, blockBodyLines, checkSchema, extractBlockNames, extractBlocks, extractModelBlocks, extractModelNames, parseEnumValues, parseFields, parseSchema, stripLineComment };
package/dist/index.d.ts CHANGED
@@ -326,17 +326,38 @@ declare class AdminManifestDoctorCheck implements DoctorCheck {
326
326
  */
327
327
  declare const PLATFORM_DOCTOR_CHECK_PROVIDERS: Array<Type<DoctorCheck>>;
328
328
 
329
+ /**
330
+ * Cuts off a trailing `//` comment. Deliberately index-based instead of
331
+ * `replace(/\/\/.*$/, '')`: the regex backtracks quadratically on lines with
332
+ * many single slashes.
333
+ */
334
+ declare function stripLineComment(line: string): string;
335
+ /** Returns the names of all top-level `<keyword> X { … }` blocks. */
336
+ declare function extractBlockNames(schema: string, keyword: string): string[];
337
+ /**
338
+ * Returns all top-level `<keyword> X { … }` blocks as a map
339
+ * `name -> complete block text incl. opening/closing braces`.
340
+ *
341
+ * Delimiting logic: we search for `^<keyword> X {` (start of line, possibly
342
+ * with leading whitespace) and close as soon as the brace depth drops back
343
+ * to 0. Braces inside comments are not counted.
344
+ */
345
+ declare function extractBlocks(schema: string, keyword: string): Map<string, string>;
346
+ /**
347
+ * Returns a block's body lines — comments stripped, blank lines, block-level
348
+ * attributes (`@@index`, `@@map`) and the enclosing braces removed.
349
+ *
350
+ * Cuts between the first `{` and the last `}` rather than dropping the first
351
+ * and last line, so single-line blocks (`model X { id String @id }`) yield
352
+ * their body too.
353
+ */
354
+ declare function blockBodyLines(block: string): string[];
355
+
329
356
  /** Returns the names of all top-level `model X { ... }` blocks in the schema. */
330
357
  declare function extractModelNames(schema: string): string[];
331
358
  /**
332
359
  * Returns all `model X { ... }` blocks from a fragment as a map
333
360
  * `name -> complete block text incl. opening/closing braces`.
334
- *
335
- * Delimiting logic: we search for `^model X {` (start of line, possibly with
336
- * leading whitespace) and close as soon as the brace depth drops back to
337
- * 0. Strings/comments inside the block are, in simplified terms, not
338
- * counted, which is robust for Prisma schemas (no curly braces
339
- * in strings, comments only `//`-style).
340
361
  */
341
362
  declare function extractModelBlocks(fragment: string): Map<string, string>;
342
363
  interface ApplyResult {
@@ -355,6 +376,65 @@ declare function applyFragmentBlocks(schema: string, fragmentBlocks: Map<string,
355
376
  fragmentLabel?: string;
356
377
  }): ApplyResult;
357
378
 
379
+ interface FieldSignature {
380
+ name: string;
381
+ /** Type without `?`/`[]` modifiers, e.g. `String`, `BillingCycle`. */
382
+ type: string;
383
+ optional: boolean;
384
+ list: boolean;
385
+ }
386
+ interface ParsedSchema {
387
+ models: Map<string, Map<string, FieldSignature>>;
388
+ enums: Map<string, string[]>;
389
+ }
390
+ interface MissingField {
391
+ model: string;
392
+ field: string;
393
+ /** Rendered spec type incl. modifiers, e.g. `DateTime?`. */
394
+ type: string;
395
+ }
396
+ interface MissingEnumValue {
397
+ enum: string;
398
+ value: string;
399
+ }
400
+ type FieldMismatchReason = 'type' | 'optionality' | 'list';
401
+ interface FieldMismatch {
402
+ model: string;
403
+ field: string;
404
+ reason: FieldMismatchReason;
405
+ expected: string;
406
+ actual: string;
407
+ }
408
+ interface SchemaCheckReport {
409
+ /** Platform models the consumer does not carry — informational. */
410
+ absentModels: string[];
411
+ /** Platform enums the consumer does not carry — informational. */
412
+ absentEnums: string[];
413
+ missingFields: MissingField[];
414
+ missingEnumValues: MissingEnumValue[];
415
+ fieldMismatches: FieldMismatch[];
416
+ /** Models present in both schemas, i.e. actually compared. */
417
+ checkedModelCount: number;
418
+ /** Enums present in both schemas, i.e. actually compared. */
419
+ checkedEnumCount: number;
420
+ /** True when nothing that breaks platform code was found. */
421
+ ok: boolean;
422
+ }
423
+ /** Parses the field lines of a `model` block into signatures, keyed by name. */
424
+ declare function parseFields(block: string): Map<string, FieldSignature>;
425
+ /**
426
+ * Parses the members of an `enum` block. Values may share a line
427
+ * (`enum Role { ADMIN USER }`); a trailing `@map(…)` attribute is not a value.
428
+ */
429
+ declare function parseEnumValues(block: string): string[];
430
+ /** Parses every `model` and `enum` declaration of a Prisma schema. */
431
+ declare function parseSchema(schema: string): ParsedSchema;
432
+ /**
433
+ * Compares a consumer schema against the canonical fragments. `specSchema` is
434
+ * the concatenation of the fragments the check should cover.
435
+ */
436
+ declare function checkSchema(specSchema: string, appSchema: string): SchemaCheckReport;
437
+
358
438
  interface CliContextModuleOptions {
359
439
  config: CliContextConfig;
360
440
  userPort: ProviderSpec<UserPort>;
@@ -545,4 +625,4 @@ declare class UserCommands extends CommandRunner {
545
625
  parsePassword(val: string): string;
546
626
  }
547
627
 
548
- export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, CLI_CONTEXT_CONFIG_TOKEN, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, PLATFORM_DOCTOR_CHECK_PROVIDERS, PlanCatalogDoctorCheck, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, applyFragmentBlocks, extractModelBlocks, extractModelNames };
628
+ export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, CLI_CONTEXT_CONFIG_TOKEN, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type FieldMismatch, type FieldMismatchReason, type FieldSignature, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingEnumValue, type MissingField, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, PlanCatalogDoctorCheck, type SchemaCheckReport, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, applyFragmentBlocks, blockBodyLines, checkSchema, extractBlockNames, extractBlocks, extractModelBlocks, extractModelNames, parseEnumValues, parseFields, parseSchema, stripLineComment };
package/dist/index.js CHANGED
@@ -1028,51 +1028,48 @@ var PLATFORM_DOCTOR_CHECK_PROVIDERS = [
1028
1028
  AdminManifestDoctorCheck
1029
1029
  ];
1030
1030
 
1031
- // src/schema-apply.ts
1031
+ // src/prisma-blocks.ts
1032
1032
  function stripLineComment(line) {
1033
1033
  const commentStart = line.indexOf("//");
1034
1034
  return commentStart === -1 ? line : line.slice(0, commentStart);
1035
1035
  }
1036
1036
  __name(stripLineComment, "stripLineComment");
1037
- function extractModelNames(schema) {
1037
+ function declarationPattern(keyword) {
1038
+ return new RegExp(`^\\s*${keyword}\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\{`);
1039
+ }
1040
+ __name(declarationPattern, "declarationPattern");
1041
+ function extractBlockNames(schema, keyword) {
1042
+ const pattern = declarationPattern(keyword);
1038
1043
  const names = [];
1039
- const lines = schema.split("\n");
1040
- for (const line of lines) {
1041
- const stripped = stripLineComment(line).trim();
1042
- const match = stripped.match(/^model\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/);
1044
+ for (const line of schema.split("\n")) {
1045
+ const match = stripLineComment(line).match(pattern);
1043
1046
  if (match) names.push(match[1]);
1044
1047
  }
1045
1048
  return names;
1046
1049
  }
1047
- __name(extractModelNames, "extractModelNames");
1048
- function extractModelBlocks(fragment) {
1050
+ __name(extractBlockNames, "extractBlockNames");
1051
+ function extractBlocks(schema, keyword) {
1052
+ const pattern = declarationPattern(keyword);
1049
1053
  const blocks = /* @__PURE__ */ new Map();
1050
- const lines = fragment.split("\n");
1051
1054
  let current = null;
1052
- for (const rawLine of lines) {
1055
+ for (const rawLine of schema.split("\n")) {
1053
1056
  const stripped = stripLineComment(rawLine);
1057
+ const openCount = (stripped.match(/\{/g) ?? []).length;
1058
+ const closeCount = (stripped.match(/\}/g) ?? []).length;
1054
1059
  if (!current) {
1055
- const match = rawLine.match(/^\s*model\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/);
1056
- if (match) {
1057
- const openCount = (stripped.match(/\{/g) ?? []).length;
1058
- const closeCount = (stripped.match(/\}/g) ?? []).length;
1059
- current = {
1060
- name: match[1],
1061
- lines: [
1062
- rawLine
1063
- ],
1064
- depth: openCount - closeCount
1065
- };
1066
- if (current.depth === 0) {
1067
- blocks.set(current.name, current.lines.join("\n"));
1068
- current = null;
1069
- }
1070
- }
1071
- continue;
1060
+ const match = stripped.match(pattern);
1061
+ if (!match) continue;
1062
+ current = {
1063
+ name: match[1],
1064
+ lines: [
1065
+ rawLine
1066
+ ],
1067
+ depth: openCount - closeCount
1068
+ };
1069
+ } else {
1070
+ current.lines.push(rawLine);
1071
+ current.depth += openCount - closeCount;
1072
1072
  }
1073
- current.lines.push(rawLine);
1074
- current.depth += (stripped.match(/\{/g) ?? []).length;
1075
- current.depth -= (stripped.match(/\}/g) ?? []).length;
1076
1073
  if (current.depth <= 0) {
1077
1074
  blocks.set(current.name, current.lines.join("\n"));
1078
1075
  current = null;
@@ -1080,6 +1077,23 @@ function extractModelBlocks(fragment) {
1080
1077
  }
1081
1078
  return blocks;
1082
1079
  }
1080
+ __name(extractBlocks, "extractBlocks");
1081
+ function blockBodyLines(block) {
1082
+ const bodyStart = block.indexOf("{");
1083
+ const bodyEnd = block.lastIndexOf("}");
1084
+ if (bodyStart === -1 || bodyEnd <= bodyStart) return [];
1085
+ return block.slice(bodyStart + 1, bodyEnd).split("\n").map((line) => stripLineComment(line).trim()).filter((line) => line.length > 0 && !line.startsWith("@@"));
1086
+ }
1087
+ __name(blockBodyLines, "blockBodyLines");
1088
+
1089
+ // src/schema-apply.ts
1090
+ function extractModelNames(schema) {
1091
+ return extractBlockNames(schema, "model");
1092
+ }
1093
+ __name(extractModelNames, "extractModelNames");
1094
+ function extractModelBlocks(fragment) {
1095
+ return extractBlocks(fragment, "model");
1096
+ }
1083
1097
  __name(extractModelBlocks, "extractModelBlocks");
1084
1098
  function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
1085
1099
  const existing = new Set(extractModelNames(schema));
@@ -1104,11 +1118,11 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
1104
1118
  const header = options.fragmentLabel ? `
1105
1119
 
1106
1120
  // ============================================================
1107
- // Eingef\xFCgt durch \`saas-platform schema apply\` aus ${options.fragmentLabel}
1121
+ // Eingef\xFCgt durch \`saasicat schema apply\` aus ${options.fragmentLabel}
1108
1122
  // ============================================================
1109
1123
  ` : `
1110
1124
 
1111
- // Eingef\xFCgt durch \`saas-platform schema apply\`
1125
+ // Eingef\xFCgt durch \`saasicat schema apply\`
1112
1126
  `;
1113
1127
  const trimmedSchema = schema.endsWith("\n") ? schema : schema + "\n";
1114
1128
  return {
@@ -1119,6 +1133,130 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
1119
1133
  }
1120
1134
  __name(applyFragmentBlocks, "applyFragmentBlocks");
1121
1135
 
1136
+ // src/schema-check.ts
1137
+ function renderType(signature) {
1138
+ return `${signature.type}${signature.list ? "[]" : ""}${signature.optional ? "?" : ""}`;
1139
+ }
1140
+ __name(renderType, "renderType");
1141
+ function parseFields(block) {
1142
+ const fields = /* @__PURE__ */ new Map();
1143
+ for (const line of blockBodyLines(block)) {
1144
+ const [name, rawType] = line.split(/\s+/);
1145
+ if (!rawType || name.startsWith("@")) continue;
1146
+ const list = rawType.includes("[]");
1147
+ const optional = rawType.endsWith("?");
1148
+ fields.set(name, {
1149
+ name,
1150
+ type: rawType.replace("[]", "").replace("?", ""),
1151
+ optional,
1152
+ list
1153
+ });
1154
+ }
1155
+ return fields;
1156
+ }
1157
+ __name(parseFields, "parseFields");
1158
+ function parseEnumValues(block) {
1159
+ const values = [];
1160
+ for (const line of blockBodyLines(block)) {
1161
+ for (const token of line.split(/\s+/)) {
1162
+ if (token.startsWith("@")) break;
1163
+ if (token.length > 0) values.push(token);
1164
+ }
1165
+ }
1166
+ return values;
1167
+ }
1168
+ __name(parseEnumValues, "parseEnumValues");
1169
+ function parseSchema(schema) {
1170
+ const models = /* @__PURE__ */ new Map();
1171
+ for (const [name, block] of extractBlocks(schema, "model")) {
1172
+ models.set(name, parseFields(block));
1173
+ }
1174
+ const enums = /* @__PURE__ */ new Map();
1175
+ for (const [name, block] of extractBlocks(schema, "enum")) {
1176
+ enums.set(name, parseEnumValues(block));
1177
+ }
1178
+ return {
1179
+ models,
1180
+ enums
1181
+ };
1182
+ }
1183
+ __name(parseSchema, "parseSchema");
1184
+ function isDocumentedTypeSubstitution(spec, app, appEnums) {
1185
+ return spec.type === "String" && appEnums.has(app.type);
1186
+ }
1187
+ __name(isDocumentedTypeSubstitution, "isDocumentedTypeSubstitution");
1188
+ function compareFields(model, specFields, appFields, appEnums, missingFields, fieldMismatches) {
1189
+ for (const [name, spec] of specFields) {
1190
+ const app = appFields.get(name);
1191
+ if (!app) {
1192
+ missingFields.push({
1193
+ model,
1194
+ field: name,
1195
+ type: renderType(spec)
1196
+ });
1197
+ continue;
1198
+ }
1199
+ const mismatch = /* @__PURE__ */ __name((reason) => ({
1200
+ model,
1201
+ field: name,
1202
+ reason,
1203
+ expected: renderType(spec),
1204
+ actual: renderType(app)
1205
+ }), "mismatch");
1206
+ if (spec.type !== app.type && !isDocumentedTypeSubstitution(spec, app, appEnums)) {
1207
+ fieldMismatches.push(mismatch("type"));
1208
+ } else if (spec.list !== app.list) {
1209
+ fieldMismatches.push(mismatch("list"));
1210
+ } else if (!spec.optional && app.optional) {
1211
+ fieldMismatches.push(mismatch("optionality"));
1212
+ }
1213
+ }
1214
+ }
1215
+ __name(compareFields, "compareFields");
1216
+ function checkSchema(specSchema, appSchema) {
1217
+ const spec = parseSchema(specSchema);
1218
+ const app = parseSchema(appSchema);
1219
+ const absentModels = [];
1220
+ const missingFields = [];
1221
+ const fieldMismatches = [];
1222
+ for (const [model, specFields] of spec.models) {
1223
+ const appFields = app.models.get(model);
1224
+ if (!appFields) {
1225
+ absentModels.push(model);
1226
+ continue;
1227
+ }
1228
+ compareFields(model, specFields, appFields, app.enums, missingFields, fieldMismatches);
1229
+ }
1230
+ const absentEnums = [];
1231
+ const missingEnumValues = [];
1232
+ for (const [name, specValues] of spec.enums) {
1233
+ const appValues = app.enums.get(name);
1234
+ if (!appValues) {
1235
+ absentEnums.push(name);
1236
+ continue;
1237
+ }
1238
+ for (const value of specValues) {
1239
+ if (!appValues.includes(value)) {
1240
+ missingEnumValues.push({
1241
+ enum: name,
1242
+ value
1243
+ });
1244
+ }
1245
+ }
1246
+ }
1247
+ return {
1248
+ absentModels,
1249
+ absentEnums,
1250
+ missingFields,
1251
+ missingEnumValues,
1252
+ fieldMismatches,
1253
+ checkedModelCount: spec.models.size - absentModels.length,
1254
+ checkedEnumCount: spec.enums.size - absentEnums.length,
1255
+ ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0
1256
+ };
1257
+ }
1258
+ __name(checkSchema, "checkSchema");
1259
+
1122
1260
  // src/module.ts
1123
1261
  import { Module } from "@nestjs/common";
1124
1262
  import { asProvider } from "@saasicat/nest";
@@ -2207,6 +2345,14 @@ export {
2207
2345
  UserPortDoctorCheck,
2208
2346
  WhoAmIFlow,
2209
2347
  applyFragmentBlocks,
2348
+ blockBodyLines,
2349
+ checkSchema,
2350
+ extractBlockNames,
2351
+ extractBlocks,
2210
2352
  extractModelBlocks,
2211
- extractModelNames
2353
+ extractModelNames,
2354
+ parseEnumValues,
2355
+ parseFields,
2356
+ parseSchema,
2357
+ stripLineComment
2212
2358
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/cli",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "CLI helpers for SaaS platform consumers. Provides CliContextService (identity, MFA, production confirm, audit tag).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -23,13 +23,13 @@
23
23
  "bin"
24
24
  ],
25
25
  "bin": {
26
- "saas-platform": "./bin/saas-platform.js"
26
+ "saasicat": "./bin/saasicat.js"
27
27
  },
28
28
  "dependencies": {
29
29
  "qrcode-terminal": "^0.12.0",
30
- "@saasicat/nest": "^0.10.1",
31
- "@saasicat/spec": "^0.10.1",
32
- "@saasicat/types": "^0.10.1"
30
+ "@saasicat/nest": "^0.11.0",
31
+ "@saasicat/spec": "^0.11.0",
32
+ "@saasicat/types": "^0.11.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@nestjs/common": "^11.0.0",