@saasicat/cli 1.0.0-rc.1 → 1.0.0-rc.11

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/index.cjs CHANGED
@@ -64,17 +64,23 @@ __export(index_exports, {
64
64
  MfaSetupFlow: () => MfaSetupFlow,
65
65
  PLATFORM_DOCTOR_CHECK_PROVIDERS: () => PLATFORM_DOCTOR_CHECK_PROVIDERS,
66
66
  PlanCatalogDoctorCheck: () => PlanCatalogDoctorCheck,
67
+ SCANNED_FOR_DB_CATALOG: () => SCANNED_FOR_DB_CATALOG,
68
+ SCANNED_FOR_MOVED_SETTINGS: () => SCANNED_FOR_MOVED_SETTINGS,
69
+ SETTINGS_THAT_MOVED: () => SETTINGS_THAT_MOVED,
67
70
  UI_VUE_SPECIFIER: () => UI_VUE_SPECIFIER,
68
71
  USER_MANAGEMENT_PORT_TOKEN: () => USER_MANAGEMENT_PORT_TOKEN,
69
72
  USER_PORT_TOKEN: () => USER_PORT_TOKEN,
70
73
  UserCommands: () => UserCommands,
71
74
  UserPortDoctorCheck: () => UserPortDoctorCheck,
75
+ WHERE_DB_CATALOG_GOES: () => WHERE_DB_CATALOG_GOES,
76
+ WHERE_IT_GOES: () => WHERE_IT_GOES,
72
77
  WhoAmIFlow: () => WhoAmIFlow,
78
+ appKeyPattern: () => appKeyPattern,
73
79
  appendConstraints: () => appendConstraints,
74
80
  applyFragmentBlocks: () => applyFragmentBlocks,
75
81
  applyTokens: () => applyTokens,
76
82
  assertModelsExist: () => assertModelsExist,
77
- assertValidProjectKey: () => assertValidProjectKey,
83
+ assertValidAppKey: () => assertValidAppKey,
78
84
  assertValidQuotaKey: () => assertValidQuotaKey,
79
85
  blankStringLiterals: () => blankStringLiterals,
80
86
  blockBodyLines: () => blockBodyLines,
@@ -82,17 +88,24 @@ __export(index_exports, {
82
88
  buildImportMap: () => buildImportMap,
83
89
  checkSchema: () => checkSchema,
84
90
  constraintsFor: () => constraintsFor,
91
+ describeDbCatalogOccurrence: () => describeDbCatalogOccurrence,
85
92
  enableFkPointers: () => enableFkPointers,
86
93
  extractBlockNames: () => extractBlockNames,
87
94
  extractBlocks: () => extractBlocks,
95
+ extractEnumBlocks: () => extractEnumBlocks,
96
+ extractEnumNames: () => extractEnumNames,
97
+ extractFragmentBlocks: () => extractFragmentBlocks,
88
98
  extractModelBlocks: () => extractModelBlocks,
89
99
  extractModelNames: () => extractModelNames,
100
+ findDbCatalogBlocks: () => findDbCatalogBlocks,
90
101
  findFkPointers: () => findFkPointers,
102
+ findMovedSettings: () => findMovedSettings,
91
103
  foreignKeyOf: () => foreignKeyOf,
92
104
  hasBackRelation: () => hasBackRelation,
93
105
  hasConstraints: () => hasConstraints,
94
106
  isNoLongerPublic: () => isNoLongerPublic,
95
107
  isOneToOne: () => isOneToOne,
108
+ judgeModuleResolution: () => judgeModuleResolution,
96
109
  kebabCase: () => kebabCase,
97
110
  migrationCreatedBy: () => migrationCreatedBy,
98
111
  minimumQuotasPerPlan: () => minimumQuotasPerPlan,
@@ -106,14 +119,16 @@ __export(index_exports, {
106
119
  patchAppModule: () => patchAppModule,
107
120
  patchOptionsFor: () => patchOptionsFor,
108
121
  planInit: () => planInit,
109
- projectKeyPattern: () => projectKeyPattern,
110
122
  quotaKeyPattern: () => quotaKeyPattern,
123
+ readEffectiveModuleResolution: () => readEffectiveModuleResolution,
111
124
  relationNameOf: () => relationNameOf,
125
+ removeProjectKey: () => removeProjectKey,
112
126
  reportConstraints: () => reportConstraints,
113
127
  rewriteImports: () => rewriteImports,
114
128
  rewriteManifest: () => rewriteManifest,
115
129
  rewriteNames: () => rewriteNames,
116
130
  rewriteSubpath: () => rewriteSubpath,
131
+ settingsWrittenTo: () => settingsWrittenTo,
117
132
  stripLineComment: () => stripLineComment,
118
133
  structuralOnly: () => structuralOnly,
119
134
  tablesAddressedBy: () => tablesAddressedBy
@@ -1028,7 +1043,7 @@ var PlanCatalogDoctorCheck = class {
1028
1043
  severity: "ok",
1029
1044
  message: `${plans.length} plan(s), ${this.catalog.features?.length ?? 0} feature(s) loaded.`,
1030
1045
  details: {
1031
- projectKey: this.catalog.projectKey,
1046
+ app: this.catalog.app.name,
1032
1047
  planIds: plans.map((p) => p.id)
1033
1048
  }
1034
1049
  };
@@ -1248,13 +1263,43 @@ function extractModelBlocks(fragment) {
1248
1263
  return extractBlocks(fragment, "model");
1249
1264
  }
1250
1265
  __name(extractModelBlocks, "extractModelBlocks");
1266
+ function extractEnumBlocks(fragment) {
1267
+ return extractBlocks(fragment, "enum");
1268
+ }
1269
+ __name(extractEnumBlocks, "extractEnumBlocks");
1270
+ function extractEnumNames(schema2) {
1271
+ return extractBlockNames(schema2, "enum");
1272
+ }
1273
+ __name(extractEnumNames, "extractEnumNames");
1274
+ function extractFragmentBlocks(fragment) {
1275
+ return {
1276
+ enums: extractEnumBlocks(fragment),
1277
+ models: extractModelBlocks(fragment)
1278
+ };
1279
+ }
1280
+ __name(extractFragmentBlocks, "extractFragmentBlocks");
1251
1281
  function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1252
- const existing = new Set(extractModelNames(schema2));
1282
+ const blocks = fragmentBlocks instanceof Map ? {
1283
+ enums: /* @__PURE__ */ new Map(),
1284
+ models: fragmentBlocks
1285
+ } : fragmentBlocks;
1286
+ const existingModels = new Set(extractModelNames(schema2));
1287
+ const existingEnums = new Set(extractEnumNames(schema2));
1253
1288
  const added = [];
1254
1289
  const skipped = [];
1290
+ const addedEnums = [];
1291
+ const skippedEnums = [];
1255
1292
  const additions = [];
1256
- for (const [name, block] of fragmentBlocks) {
1257
- if (existing.has(name)) {
1293
+ for (const [name, block] of blocks.enums) {
1294
+ if (existingEnums.has(name)) {
1295
+ skippedEnums.push(name);
1296
+ } else {
1297
+ addedEnums.push(name);
1298
+ additions.push(block);
1299
+ }
1300
+ }
1301
+ for (const [name, block] of blocks.models) {
1302
+ if (existingModels.has(name)) {
1258
1303
  skipped.push(name);
1259
1304
  } else {
1260
1305
  added.push(name);
@@ -1265,6 +1310,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1265
1310
  return {
1266
1311
  added,
1267
1312
  skipped,
1313
+ addedEnums,
1314
+ skippedEnums,
1268
1315
  schema: schema2
1269
1316
  };
1270
1317
  }
@@ -1281,6 +1328,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1281
1328
  return {
1282
1329
  added,
1283
1330
  skipped,
1331
+ addedEnums,
1332
+ skippedEnums,
1284
1333
  schema: trimmedSchema + header + additions.join("\n\n") + "\n"
1285
1334
  };
1286
1335
  }
@@ -1666,10 +1715,11 @@ function required(value, what) {
1666
1715
  return value;
1667
1716
  }
1668
1717
  __name(required, "required");
1669
- function projectKeyPattern() {
1670
- return new RegExp(required(schema.properties?.projectKey?.pattern, "pattern for projectKey"));
1718
+ var APP_KEY_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
1719
+ function appKeyPattern() {
1720
+ return APP_KEY_PATTERN;
1671
1721
  }
1672
- __name(projectKeyPattern, "projectKeyPattern");
1722
+ __name(appKeyPattern, "appKeyPattern");
1673
1723
  function quotaKeyPattern() {
1674
1724
  const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
1675
1725
  if (patterns.length !== 1) {
@@ -1682,12 +1732,12 @@ function minimumQuotasPerPlan() {
1682
1732
  return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
1683
1733
  }
1684
1734
  __name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
1685
- function assertValidProjectKey(projectKey) {
1686
- const pattern = projectKeyPattern();
1687
- if (pattern.test(projectKey)) return;
1688
- throw new Error(`--project-key=${projectKey} is not a valid project key. It has to match ${pattern.source} \u2014 lower case, starting with a letter, at least two characters. The platform validates config/saas.yaml against the same pattern at boot, so this would fail after every file was written.`);
1735
+ function assertValidAppKey(appKey) {
1736
+ const pattern = appKeyPattern();
1737
+ if (pattern.test(appKey)) return;
1738
+ throw new Error(`--app-key=${appKey} is not a valid app key. It has to match ${pattern.source} \u2014 lower case, starting with a letter, at least two characters. It becomes an npm package name and a browser storage prefix, so this would fail after every file was written.`);
1689
1739
  }
1690
- __name(assertValidProjectKey, "assertValidProjectKey");
1740
+ __name(assertValidAppKey, "assertValidAppKey");
1691
1741
  function assertValidQuotaKey(quotaKey) {
1692
1742
  const pattern = quotaKeyPattern();
1693
1743
  if (pattern.test(quotaKey)) return;
@@ -1719,24 +1769,24 @@ function pascalCase(value) {
1719
1769
  __name(pascalCase, "pascalCase");
1720
1770
  var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
1721
1771
  function planInit(options) {
1722
- const projectKey = options.projectKey;
1723
- if (!projectKey) throw new Error("init needs a --project-key.");
1724
- assertValidProjectKey(projectKey);
1725
- const appLabel = options.appName ?? pascalCase(projectKey);
1772
+ const appKey = options.appKey;
1773
+ if (!appKey) throw new Error("init needs an --app-key.");
1774
+ assertValidAppKey(appKey);
1775
+ const appLabel = options.appName ?? pascalCase(appKey);
1726
1776
  const appName = pascalCase(appLabel);
1727
1777
  const apiBase = options.apiBase ?? "/api/v1/admin";
1728
1778
  const quotas = (options.quotas ?? []).map(parseQuota);
1729
1779
  assertEnoughQuotas(quotas);
1730
1780
  const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
1731
- const featureKey = `${projectKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
1781
+ const featureKey = `${appKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
1732
1782
  const shared = {
1733
- PROJECT_KEY: projectKey,
1783
+ APP_KEY: appKey,
1734
1784
  APP_NAME: appName,
1735
1785
  APP_LABEL: appLabel,
1736
1786
  API_BASE: apiBase,
1737
1787
  FEATURE_KEY: featureKey,
1738
- REGISTRY_CONST: `${constantCase(projectKey)}_FEATURE_UI_REGISTRY`,
1739
- MANIFEST_CONST: `${constantCase(projectKey)}_MANIFEST_CONTRIBUTION`,
1788
+ REGISTRY_CONST: `${constantCase(appKey)}_FEATURE_UI_REGISTRY`,
1789
+ MANIFEST_CONST: `${constantCase(appKey)}_MANIFEST_CONTRIBUTION`,
1740
1790
  ADMIN_MODULE_CLASS: `${appName}AdminModule`,
1741
1791
  HASHER_CLASS: hasherClass ?? "",
1742
1792
  HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
@@ -1835,6 +1885,63 @@ function patchOptionsFor(plan) {
1835
1885
  }
1836
1886
  __name(patchOptionsFor, "patchOptionsFor");
1837
1887
 
1888
+ // src/init/module-resolution.ts
1889
+ var import_node_path = require("path");
1890
+ var RESOLVES_SUBPATHS = /* @__PURE__ */ new Set([
1891
+ "node16",
1892
+ "nodenext",
1893
+ "bundler"
1894
+ ]);
1895
+ function judgeModuleResolution(value) {
1896
+ if (value === null || RESOLVES_SUBPATHS.has(value)) {
1897
+ return {
1898
+ ok: true,
1899
+ value
1900
+ };
1901
+ }
1902
+ return {
1903
+ ok: false,
1904
+ value,
1905
+ reason: `tsconfig.json resolves modules with "moduleResolution": "${value}"` + (value === "node10" ? ' (what TypeScript calls the "node" setting)' : "") + '. The files init writes import subpath exports (`@saasicat/nest/platform`, `/billing`, `/discovery`), which TypeScript resolves only under "node16", "nodenext" or "bundler". Set one of those first \u2014 `nest new` has used "nodenext" since NestJS 10.'
1906
+ };
1907
+ }
1908
+ __name(judgeModuleResolution, "judgeModuleResolution");
1909
+ function readEffectiveModuleResolution(root, ts) {
1910
+ const configPath = (0, import_node_path.join)(root, "tsconfig.json");
1911
+ const read = ts.readConfigFile(configPath, ts.sys.readFile);
1912
+ if (read.error || read.config === void 0) return null;
1913
+ const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root);
1914
+ const kind = parsed.options.moduleResolution;
1915
+ if (kind === void 0) return null;
1916
+ return ts.ModuleResolutionKind[kind].toLowerCase();
1917
+ }
1918
+ __name(readEffectiveModuleResolution, "readEffectiveModuleResolution");
1919
+
1920
+ // src/init/settings-written.ts
1921
+ var import_billing = require("@saasicat/nest/billing");
1922
+ function flatten(prefix, value, into) {
1923
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1924
+ for (const [key, child] of Object.entries(value)) {
1925
+ flatten(`${prefix}.${key}`, child, into);
1926
+ }
1927
+ return;
1928
+ }
1929
+ into.push({
1930
+ key: prefix,
1931
+ value: JSON.stringify(value)
1932
+ });
1933
+ }
1934
+ __name(flatten, "flatten");
1935
+ function settingsWrittenTo(catalogYaml, source = "config/saas.yaml") {
1936
+ const catalog = (0, import_billing.loadPlanCatalogFromString)(catalogYaml, {
1937
+ source
1938
+ });
1939
+ const settings = [];
1940
+ flatten("tenantBilling", catalog.tenantBilling, settings);
1941
+ return settings;
1942
+ }
1943
+ __name(settingsWrittenTo, "settingsWrittenTo");
1944
+
1838
1945
  // src/codemods/v1-imports.ts
1839
1946
  var PUBLIC_PREFIXES = [
1840
1947
  "ui/",
@@ -2098,6 +2205,327 @@ function rewriteManifest(text, table, options) {
2098
2205
  }
2099
2206
  __name(rewriteManifest, "rewriteManifest");
2100
2207
 
2208
+ // src/codemods/v1-project-key.ts
2209
+ function stripQueryParameter(text) {
2210
+ const NEEDLE = "projectKey=";
2211
+ let out = "";
2212
+ let index = 0;
2213
+ const taken = /* @__PURE__ */ new Set();
2214
+ for (; ; ) {
2215
+ const at = text.indexOf(NEEDLE, index);
2216
+ if (at < 0) break;
2217
+ const separator = at === 0 ? "" : text[at - 1];
2218
+ const value = separator === "?" || separator === "&" ? simpleValueEnd(text, at + NEEDLE.length) : null;
2219
+ if (value === null || !servesTheCatalogue(text, at)) {
2220
+ out += text.slice(index, at + NEEDLE.length);
2221
+ index = at + NEEDLE.length;
2222
+ continue;
2223
+ }
2224
+ out += text.slice(index, at - 1);
2225
+ if (separator === "?" && text[value] === "&") {
2226
+ out += "?";
2227
+ index = value + 1;
2228
+ } else {
2229
+ index = value;
2230
+ }
2231
+ taken.add(at);
2232
+ }
2233
+ return {
2234
+ text: out + text.slice(index),
2235
+ taken
2236
+ };
2237
+ }
2238
+ __name(stripQueryParameter, "stripQueryParameter");
2239
+ var HIDES_A_BRACE = /[`'"{}/\\]/;
2240
+ function simpleValueEnd(text, from) {
2241
+ let i = from;
2242
+ while (i < text.length) {
2243
+ const ch = text[i];
2244
+ if (ch === "&" || ch === "#" || ch === "'" || ch === '"' || ch === "`" || ch === "\n" || ch === " ") {
2245
+ return i;
2246
+ }
2247
+ if (ch !== "$" || text[i + 1] !== "{") {
2248
+ if (ch === "{" || ch === "}") return null;
2249
+ i += 1;
2250
+ continue;
2251
+ }
2252
+ const close = text.indexOf("}", i + 2);
2253
+ if (close < 0) return null;
2254
+ if (HIDES_A_BRACE.test(text.slice(i + 2, close))) return null;
2255
+ i = close + 1;
2256
+ }
2257
+ return null;
2258
+ }
2259
+ __name(simpleValueEnd, "simpleValueEnd");
2260
+ function servesTheCatalogue(text, at) {
2261
+ let start = at;
2262
+ while (start > 0) {
2263
+ const ch = text[start - 1];
2264
+ if (ch === "`" || ch === "'" || ch === '"' || ch === "\n") break;
2265
+ start -= 1;
2266
+ }
2267
+ const url = text.slice(start, at);
2268
+ const query = url.indexOf("?");
2269
+ return (query < 0 ? url : url.slice(0, query)).includes("/catalog/");
2270
+ }
2271
+ __name(servesTheCatalogue, "servesTheCatalogue");
2272
+ function isIdentifierChar(ch) {
2273
+ return ch !== void 0 && /[A-Za-z0-9_$]/.test(ch);
2274
+ }
2275
+ __name(isIdentifierChar, "isIdentifierChar");
2276
+ function lineAt(text, at) {
2277
+ let line = 1;
2278
+ for (let i = 0; i < at; i += 1) if (text[i] === "\n") line += 1;
2279
+ return line;
2280
+ }
2281
+ __name(lineAt, "lineAt");
2282
+ function removeProjectKey(text, kind = "source") {
2283
+ if (kind === "yaml") return removeFromYaml(text);
2284
+ const query = stripQueryParameter(text);
2285
+ const reported = /* @__PURE__ */ new Set();
2286
+ for (let at = text.indexOf("projectKey"); at >= 0; at = text.indexOf("projectKey", at + 1)) {
2287
+ if (query.taken.has(at)) continue;
2288
+ if (isIdentifierChar(text[at - 1])) continue;
2289
+ if (isIdentifierChar(text[at + "projectKey".length])) continue;
2290
+ reported.add(lineAt(text, at));
2291
+ }
2292
+ return {
2293
+ text: query.text,
2294
+ rewritten: query.taken.size,
2295
+ undecided: [
2296
+ ...reported
2297
+ ].sort((a, b) => a - b)
2298
+ };
2299
+ }
2300
+ __name(removeProjectKey, "removeProjectKey");
2301
+ function removeFromYaml(text) {
2302
+ const lines = text.split("\n");
2303
+ const kept = [];
2304
+ let rewritten = 0;
2305
+ for (const line of lines) {
2306
+ if (line.startsWith("projectKey:")) {
2307
+ rewritten += 1;
2308
+ continue;
2309
+ }
2310
+ kept.push(line);
2311
+ }
2312
+ return {
2313
+ text: kept.join("\n"),
2314
+ rewritten,
2315
+ undecided: []
2316
+ };
2317
+ }
2318
+ __name(removeFromYaml, "removeFromYaml");
2319
+
2320
+ // src/codemods/v1-moved-settings.ts
2321
+ var import_spec2 = require("@saasicat/spec");
2322
+ var SETTINGS_THAT_MOVED = Object.keys(import_spec2.planCatalogSchema.properties?.tenantBilling?.properties ?? (() => {
2323
+ throw new Error("plan-catalog.schema.json declares no tenantBilling properties \u2014 @saasicat/spec and @saasicat/cli are out of step.");
2324
+ })());
2325
+ var isIdentifierChar2 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
2326
+ function lineAt2(text, index) {
2327
+ let line = 1;
2328
+ for (let at = 0; at < index; at += 1) {
2329
+ if (text[at] === "\n") line += 1;
2330
+ }
2331
+ return line;
2332
+ }
2333
+ __name(lineAt2, "lineAt");
2334
+ var SCANNED_FOR_MOVED_SETTINGS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue)$/;
2335
+ function findMovedSettings(text) {
2336
+ const occurrences = [];
2337
+ for (const setting of SETTINGS_THAT_MOVED) {
2338
+ for (let at = text.indexOf(setting); at >= 0; at = text.indexOf(setting, at + 1)) {
2339
+ if (isIdentifierChar2(text[at - 1])) continue;
2340
+ if (isIdentifierChar2(text[at + setting.length])) continue;
2341
+ occurrences.push({
2342
+ setting,
2343
+ line: lineAt2(text, at)
2344
+ });
2345
+ }
2346
+ }
2347
+ return {
2348
+ occurrences: occurrences.sort((a, b) => a.line - b.line || a.setting.localeCompare(b.setting))
2349
+ };
2350
+ }
2351
+ __name(findMovedSettings, "findMovedSettings");
2352
+ var WHERE_IT_GOES = {
2353
+ cancellationNoticeDays: "config/saas.yaml \u2192 tenantBilling.cancellationNoticeDays \u2014 both `monthly` and `yearly` are required, so write the number you are running today rather than leaving one out.",
2354
+ selfServiceBlockedPlans: 'config/saas.yaml \u2192 tenantBilling.selfServiceBlockedPlans \u2014 both `asTarget` and `asSource` are required, and `[]` is the way to say "nothing is blocked".'
2355
+ };
2356
+
2357
+ // src/codemods/v1-db-catalog.ts
2358
+ var import_platform = require("@saasicat/nest/platform");
2359
+ var SCANNED_FOR_DB_CATALOG = SCANNED_FOR_MOVED_SETTINGS;
2360
+ var PROPERTY = "dbCatalog";
2361
+ var TAKEN = new Set(import_platform.DB_CATALOG_MEMBERS);
2362
+ var isIdentifierStart = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z_$]/.test(ch), "isIdentifierStart");
2363
+ var isIdentifierChar3 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
2364
+ var isBlank = /* @__PURE__ */ __name((ch) => ch === " " || ch === " " || ch === "\n" || ch === "\r", "isBlank");
2365
+ function lineAt3(text, index) {
2366
+ let line = 1;
2367
+ for (let at = 0; at < index; at += 1) {
2368
+ if (text[at] === "\n") line += 1;
2369
+ }
2370
+ return line;
2371
+ }
2372
+ __name(lineAt3, "lineAt");
2373
+ function skipBlanks(text, from) {
2374
+ let at = from;
2375
+ while (isBlank(text[at])) at += 1;
2376
+ return at;
2377
+ }
2378
+ __name(skipBlanks, "skipBlanks");
2379
+ function closingQuote(text, open) {
2380
+ const quote = text[open];
2381
+ for (let at = open + 1; at < text.length; at += 1) {
2382
+ if (text[at] === "\\") {
2383
+ at += 1;
2384
+ continue;
2385
+ }
2386
+ if (text[at] === quote) return at;
2387
+ }
2388
+ return text.length;
2389
+ }
2390
+ __name(closingQuote, "closingQuote");
2391
+ function skipCommentOrString(text, at) {
2392
+ const ch = text[at];
2393
+ if (ch === "/" && text[at + 1] === "/") {
2394
+ const end = text.indexOf("\n", at);
2395
+ return end === -1 ? text.length : end;
2396
+ }
2397
+ if (ch === "/" && text[at + 1] === "*") {
2398
+ const end = text.indexOf("*/", at + 2);
2399
+ return end === -1 ? text.length : end + 2;
2400
+ }
2401
+ if (ch === "'" || ch === '"' || ch === "`") return closingQuote(text, at) + 1;
2402
+ return at;
2403
+ }
2404
+ __name(skipCommentOrString, "skipCommentOrString");
2405
+ function objectLiteralAt(text, open) {
2406
+ const members = [];
2407
+ let depth = 0;
2408
+ let expectingKey = true;
2409
+ let at = open + 1;
2410
+ while (at < text.length) {
2411
+ const ch = text[at];
2412
+ if (ch === "'" || ch === '"' || ch === "`") {
2413
+ const end = closingQuote(text, at);
2414
+ if (depth === 0 && expectingKey && text[skipBlanks(text, end + 1)] === ":") {
2415
+ members.push(text.slice(at + 1, end));
2416
+ }
2417
+ at = end + 1;
2418
+ continue;
2419
+ }
2420
+ const skipped = skipCommentOrString(text, at);
2421
+ if (skipped > at) {
2422
+ at = skipped;
2423
+ continue;
2424
+ }
2425
+ if (ch === "{" || ch === "[" || ch === "(") {
2426
+ depth += 1;
2427
+ at += 1;
2428
+ continue;
2429
+ }
2430
+ if (ch === "}" || ch === "]" || ch === ")") {
2431
+ if (depth === 0) return {
2432
+ members,
2433
+ end: at + 1
2434
+ };
2435
+ depth -= 1;
2436
+ at += 1;
2437
+ continue;
2438
+ }
2439
+ if (depth === 0 && ch === ":") expectingKey = false;
2440
+ if (depth === 0 && ch === ",") expectingKey = true;
2441
+ if (depth === 0 && expectingKey && ch === "." && text.startsWith("...", at)) {
2442
+ let end = at + 3;
2443
+ while (isIdentifierChar3(text[end])) end += 1;
2444
+ members.push(text.slice(at, end));
2445
+ at = end;
2446
+ continue;
2447
+ }
2448
+ if (depth === 0 && expectingKey && isIdentifierStart(ch)) {
2449
+ let end = at;
2450
+ while (isIdentifierChar3(text[end])) end += 1;
2451
+ const next = text[skipBlanks(text, end)];
2452
+ if (next === ":" || next === "," || next === "}") members.push(text.slice(at, end));
2453
+ at = end;
2454
+ continue;
2455
+ }
2456
+ at += 1;
2457
+ }
2458
+ return {
2459
+ members,
2460
+ end: text.length
2461
+ };
2462
+ }
2463
+ __name(objectLiteralAt, "objectLiteralAt");
2464
+ function findDbCatalogBlocks(text) {
2465
+ const occurrences = [];
2466
+ let at = 0;
2467
+ while (at < text.length) {
2468
+ const skipped = skipCommentOrString(text, at);
2469
+ if (skipped > at) {
2470
+ at = skipped;
2471
+ continue;
2472
+ }
2473
+ if (!text.startsWith(PROPERTY, at) || isIdentifierChar3(text[at - 1])) {
2474
+ at += 1;
2475
+ continue;
2476
+ }
2477
+ const afterName = at + PROPERTY.length;
2478
+ if (isIdentifierChar3(text[afterName])) {
2479
+ at = afterName;
2480
+ continue;
2481
+ }
2482
+ const colon = skipBlanks(text, afterName);
2483
+ if (text[colon] !== ":") {
2484
+ at = afterName;
2485
+ continue;
2486
+ }
2487
+ const value = skipBlanks(text, colon + 1);
2488
+ const line = lineAt3(text, at);
2489
+ if (text[value] !== "{") {
2490
+ occurrences.push({
2491
+ line,
2492
+ shape: "reference",
2493
+ leftovers: []
2494
+ });
2495
+ at = value;
2496
+ continue;
2497
+ }
2498
+ const { members, end } = objectLiteralAt(text, value);
2499
+ const leftovers = members.filter((member) => !TAKEN.has(member));
2500
+ if (!members.includes("path")) {
2501
+ occurrences.push({
2502
+ line,
2503
+ shape: "values",
2504
+ leftovers
2505
+ });
2506
+ } else if (leftovers.length > 0) {
2507
+ occurrences.push({
2508
+ line,
2509
+ shape: "mixed",
2510
+ leftovers
2511
+ });
2512
+ }
2513
+ at = end;
2514
+ }
2515
+ return {
2516
+ occurrences
2517
+ };
2518
+ }
2519
+ __name(findDbCatalogBlocks, "findDbCatalogBlocks");
2520
+ function describeDbCatalogOccurrence(occurrence) {
2521
+ const { shape, leftovers } = occurrence;
2522
+ if (shape === "reference") return "carries something this cannot see into";
2523
+ if (shape === "mixed") return `names the path but still carries ${leftovers.join(", ")}`;
2524
+ return leftovers.length > 0 ? `carries the values: ${leftovers.join(", ")}` : "names no path";
2525
+ }
2526
+ __name(describeDbCatalogOccurrence, "describeDbCatalogOccurrence");
2527
+ var WHERE_DB_CATALOG_GOES = "dbCatalog: { path: 'config/saas.yaml' } \u2014 the file the values were forwarded from. Delete the values; the platform reads app, currency, vatRate, tenantBilling, marketing and notifications from the file it names, and the plans from the read sink as before.";
2528
+
2101
2529
  // src/init/patch-app-module.ts
2102
2530
  var MARKER = "SaaSiCatModule.forRoot";
2103
2531
  function patchAppModule(source, options) {
@@ -2118,7 +2546,7 @@ ${block}`;
2118
2546
  return {
2119
2547
  source,
2120
2548
  status: "declined",
2121
- reason: "no `@Module({ imports: [ ... ] })` was found in this file, so there is nowhere to add the platform without guessing at the structure",
2549
+ reason: "this file has no `@Module({ ... })` with an `imports:` key on a line of its own \u2014 the shape `nest new` writes \u2014 so there is nowhere to add the platform without guessing at the structure",
2122
2550
  manualBlock
2123
2551
  };
2124
2552
  }
@@ -2197,7 +2625,7 @@ function renderForRootBlock(options) {
2197
2625
  "SaaSiCatModule.forRoot(",
2198
2626
  " defineSaaSiCat({",
2199
2627
  " // Plans straight from the YAML. Apps that manage plans in the",
2200
- " // SuperAdmin UI pass `dbCatalog` instead.",
2628
+ " // SuperAdmin UI pass `dbCatalog: { path: 'config/saas.yaml' }` instead.",
2201
2629
  " planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),",
2202
2630
  " // Your authentication guard. This does NOT compile until you",
2203
2631
  " // name one, and that is deliberate: an empty array is how the",
@@ -2206,6 +2634,13 @@ function renderForRootBlock(options) {
2206
2634
  " // your whole capability inventory \u2014 and the manifest routes to",
2207
2635
  " // anyone who asks. Import your guard and put it in.",
2208
2636
  " controller: { guards: [YourAuthGuard] },",
2637
+ " // The modules whose providers the platform injects: the one",
2638
+ " // exporting PrismaService, and the one your guard depends on.",
2639
+ " // `prismaPersistence({ client: PrismaService })` is resolved",
2640
+ " // inside the platform module, which sees only what is listed",
2641
+ " // here or declared @Global. Same rule as the guard: this does",
2642
+ " // not compile until you name them.",
2643
+ " imports: [YourPrismaModule, YourAuthModule],",
2209
2644
  options.persistenceImport ? " persistence," : " // persistence: prismaPersistence({ client: PrismaService }),",
2210
2645
  " catalog: { featureUiRegistry: " + options.registry.constName + " },",
2211
2646
  " adminResources: true,",
@@ -2852,7 +3287,7 @@ DoctorCommands = _ts_decorate12([
2852
3287
 
2853
3288
  // src/discovery.command.ts
2854
3289
  var import_node_fs = require("fs");
2855
- var import_node_path = require("path");
3290
+ var import_node_path2 = require("path");
2856
3291
  var import_common13 = require("@nestjs/common");
2857
3292
  var import_nest_commander5 = require("nest-commander");
2858
3293
  var import_nest6 = require("@saasicat/nest");
@@ -2890,8 +3325,8 @@ var DiscoveryScanCommand = class extends import_nest_commander5.CommandRunner {
2890
3325
  try {
2891
3326
  const snapshot = this.scanner.rebuildSnapshot();
2892
3327
  if (flags.out) {
2893
- const outPath = (0, import_node_path.resolve)(flags.out);
2894
- (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(outPath), {
3328
+ const outPath = (0, import_node_path2.resolve)(flags.out);
3329
+ (0, import_node_fs.mkdirSync)((0, import_node_path2.dirname)(outPath), {
2895
3330
  recursive: true
2896
3331
  });
2897
3332
  (0, import_node_fs.writeFileSync)(outPath, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
@@ -2900,7 +3335,7 @@ var DiscoveryScanCommand = class extends import_nest_commander5.CommandRunner {
2900
3335
  process.stdout.write(`Discovery-Scan (${snapshot.app.key} v${snapshot.app.version}): ${snapshot.capabilities.length} Capabilities \xB7 ${snapshot.features.length} Features \xB7 ${snapshot.quotas.length} Quotas \xB7 hash ${snapshot.hash.slice(0, 19)}\u2026
2901
3336
  `);
2902
3337
  if (target) {
2903
- process.stdout.write(`Snapshot persisted: ${(0, import_node_path.resolve)(target)}
3338
+ process.stdout.write(`Snapshot persisted: ${(0, import_node_path2.resolve)(target)}
2904
3339
  `);
2905
3340
  } else {
2906
3341
  process.stderr.write("WARNING: the snapshot was not persisted \u2014 neither snapshotPath (DiscoveryModule.forRoot) configured nor --out given. The seed gate will find no snapshot this way.\n");
@@ -3308,17 +3743,23 @@ UserCommands = _ts_decorate14([
3308
3743
  MfaSetupFlow,
3309
3744
  PLATFORM_DOCTOR_CHECK_PROVIDERS,
3310
3745
  PlanCatalogDoctorCheck,
3746
+ SCANNED_FOR_DB_CATALOG,
3747
+ SCANNED_FOR_MOVED_SETTINGS,
3748
+ SETTINGS_THAT_MOVED,
3311
3749
  UI_VUE_SPECIFIER,
3312
3750
  USER_MANAGEMENT_PORT_TOKEN,
3313
3751
  USER_PORT_TOKEN,
3314
3752
  UserCommands,
3315
3753
  UserPortDoctorCheck,
3754
+ WHERE_DB_CATALOG_GOES,
3755
+ WHERE_IT_GOES,
3316
3756
  WhoAmIFlow,
3757
+ appKeyPattern,
3317
3758
  appendConstraints,
3318
3759
  applyFragmentBlocks,
3319
3760
  applyTokens,
3320
3761
  assertModelsExist,
3321
- assertValidProjectKey,
3762
+ assertValidAppKey,
3322
3763
  assertValidQuotaKey,
3323
3764
  blankStringLiterals,
3324
3765
  blockBodyLines,
@@ -3326,17 +3767,24 @@ UserCommands = _ts_decorate14([
3326
3767
  buildImportMap,
3327
3768
  checkSchema,
3328
3769
  constraintsFor,
3770
+ describeDbCatalogOccurrence,
3329
3771
  enableFkPointers,
3330
3772
  extractBlockNames,
3331
3773
  extractBlocks,
3774
+ extractEnumBlocks,
3775
+ extractEnumNames,
3776
+ extractFragmentBlocks,
3332
3777
  extractModelBlocks,
3333
3778
  extractModelNames,
3779
+ findDbCatalogBlocks,
3334
3780
  findFkPointers,
3781
+ findMovedSettings,
3335
3782
  foreignKeyOf,
3336
3783
  hasBackRelation,
3337
3784
  hasConstraints,
3338
3785
  isNoLongerPublic,
3339
3786
  isOneToOne,
3787
+ judgeModuleResolution,
3340
3788
  kebabCase,
3341
3789
  migrationCreatedBy,
3342
3790
  minimumQuotasPerPlan,
@@ -3350,14 +3798,16 @@ UserCommands = _ts_decorate14([
3350
3798
  patchAppModule,
3351
3799
  patchOptionsFor,
3352
3800
  planInit,
3353
- projectKeyPattern,
3354
3801
  quotaKeyPattern,
3802
+ readEffectiveModuleResolution,
3355
3803
  relationNameOf,
3804
+ removeProjectKey,
3356
3805
  reportConstraints,
3357
3806
  rewriteImports,
3358
3807
  rewriteManifest,
3359
3808
  rewriteNames,
3360
3809
  rewriteSubpath,
3810
+ settingsWrittenTo,
3361
3811
  stripLineComment,
3362
3812
  structuralOnly,
3363
3813
  tablesAddressedBy