@saasicat/cli 1.0.0-rc.0 → 1.0.0-rc.10

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,13 +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,
128
+ rewriteManifest: () => rewriteManifest,
114
129
  rewriteNames: () => rewriteNames,
115
130
  rewriteSubpath: () => rewriteSubpath,
131
+ settingsWrittenTo: () => settingsWrittenTo,
116
132
  stripLineComment: () => stripLineComment,
117
133
  structuralOnly: () => structuralOnly,
118
134
  tablesAddressedBy: () => tablesAddressedBy
@@ -1027,7 +1043,7 @@ var PlanCatalogDoctorCheck = class {
1027
1043
  severity: "ok",
1028
1044
  message: `${plans.length} plan(s), ${this.catalog.features?.length ?? 0} feature(s) loaded.`,
1029
1045
  details: {
1030
- projectKey: this.catalog.projectKey,
1046
+ app: this.catalog.app.name,
1031
1047
  planIds: plans.map((p) => p.id)
1032
1048
  }
1033
1049
  };
@@ -1247,13 +1263,43 @@ function extractModelBlocks(fragment) {
1247
1263
  return extractBlocks(fragment, "model");
1248
1264
  }
1249
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");
1250
1281
  function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1251
- 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));
1252
1288
  const added = [];
1253
1289
  const skipped = [];
1290
+ const addedEnums = [];
1291
+ const skippedEnums = [];
1254
1292
  const additions = [];
1255
- for (const [name, block] of fragmentBlocks) {
1256
- 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)) {
1257
1303
  skipped.push(name);
1258
1304
  } else {
1259
1305
  added.push(name);
@@ -1264,6 +1310,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1264
1310
  return {
1265
1311
  added,
1266
1312
  skipped,
1313
+ addedEnums,
1314
+ skippedEnums,
1267
1315
  schema: schema2
1268
1316
  };
1269
1317
  }
@@ -1280,6 +1328,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1280
1328
  return {
1281
1329
  added,
1282
1330
  skipped,
1331
+ addedEnums,
1332
+ skippedEnums,
1283
1333
  schema: trimmedSchema + header + additions.join("\n\n") + "\n"
1284
1334
  };
1285
1335
  }
@@ -1665,10 +1715,11 @@ function required(value, what) {
1665
1715
  return value;
1666
1716
  }
1667
1717
  __name(required, "required");
1668
- function projectKeyPattern() {
1669
- 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;
1670
1721
  }
1671
- __name(projectKeyPattern, "projectKeyPattern");
1722
+ __name(appKeyPattern, "appKeyPattern");
1672
1723
  function quotaKeyPattern() {
1673
1724
  const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
1674
1725
  if (patterns.length !== 1) {
@@ -1681,12 +1732,12 @@ function minimumQuotasPerPlan() {
1681
1732
  return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
1682
1733
  }
1683
1734
  __name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
1684
- function assertValidProjectKey(projectKey) {
1685
- const pattern = projectKeyPattern();
1686
- if (pattern.test(projectKey)) return;
1687
- 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.`);
1688
1739
  }
1689
- __name(assertValidProjectKey, "assertValidProjectKey");
1740
+ __name(assertValidAppKey, "assertValidAppKey");
1690
1741
  function assertValidQuotaKey(quotaKey) {
1691
1742
  const pattern = quotaKeyPattern();
1692
1743
  if (pattern.test(quotaKey)) return;
@@ -1718,24 +1769,24 @@ function pascalCase(value) {
1718
1769
  __name(pascalCase, "pascalCase");
1719
1770
  var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
1720
1771
  function planInit(options) {
1721
- const projectKey = options.projectKey;
1722
- if (!projectKey) throw new Error("init needs a --project-key.");
1723
- assertValidProjectKey(projectKey);
1724
- 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);
1725
1776
  const appName = pascalCase(appLabel);
1726
1777
  const apiBase = options.apiBase ?? "/api/v1/admin";
1727
1778
  const quotas = (options.quotas ?? []).map(parseQuota);
1728
1779
  assertEnoughQuotas(quotas);
1729
1780
  const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
1730
- const featureKey = `${projectKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
1781
+ const featureKey = `${appKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
1731
1782
  const shared = {
1732
- PROJECT_KEY: projectKey,
1783
+ APP_KEY: appKey,
1733
1784
  APP_NAME: appName,
1734
1785
  APP_LABEL: appLabel,
1735
1786
  API_BASE: apiBase,
1736
1787
  FEATURE_KEY: featureKey,
1737
- REGISTRY_CONST: `${constantCase(projectKey)}_FEATURE_UI_REGISTRY`,
1738
- MANIFEST_CONST: `${constantCase(projectKey)}_MANIFEST_CONTRIBUTION`,
1788
+ REGISTRY_CONST: `${constantCase(appKey)}_FEATURE_UI_REGISTRY`,
1789
+ MANIFEST_CONST: `${constantCase(appKey)}_MANIFEST_CONTRIBUTION`,
1739
1790
  ADMIN_MODULE_CLASS: `${appName}AdminModule`,
1740
1791
  HASHER_CLASS: hasherClass ?? "",
1741
1792
  HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
@@ -1834,6 +1885,63 @@ function patchOptionsFor(plan) {
1834
1885
  }
1835
1886
  __name(patchOptionsFor, "patchOptionsFor");
1836
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
+
1837
1945
  // src/codemods/v1-imports.ts
1838
1946
  var PUBLIC_PREFIXES = [
1839
1947
  "ui/",
@@ -1999,6 +2107,13 @@ function rewriteNames(text, table) {
1999
2107
  return `${quote}${to}`;
2000
2108
  });
2001
2109
  }
2110
+ for (const [from, to] of Object.entries(table.packages ?? {})) {
2111
+ if (from === "_") continue;
2112
+ next = next.replace(new RegExp(`${escape(from)}(?=['"\`/])`, "g"), () => {
2113
+ rewritten += 1;
2114
+ return to;
2115
+ });
2116
+ }
2002
2117
  for (const [from, to] of Object.entries(table.subpaths)) {
2003
2118
  next = next.replace(new RegExp(escape(from), "g"), () => {
2004
2119
  rewritten += 1;
@@ -2014,6 +2129,402 @@ function rewriteNames(text, table) {
2014
2129
  };
2015
2130
  }
2016
2131
  __name(rewriteNames, "rewriteNames");
2132
+ var DEPENDENCY_FIELDS = [
2133
+ "dependencies",
2134
+ "devDependencies",
2135
+ "peerDependencies",
2136
+ "optionalDependencies"
2137
+ ];
2138
+ var UNTRANSLATABLE_RANGE = /^(workspace:|file:|link:|npm:|git\+|https?:)/;
2139
+ function rewriteManifest(text, table, options) {
2140
+ const renames = Object.entries(table.packages ?? {}).filter(([from]) => from !== "_");
2141
+ const ambiguous = [];
2142
+ let manifest;
2143
+ try {
2144
+ manifest = JSON.parse(text);
2145
+ } catch {
2146
+ return {
2147
+ text,
2148
+ rewritten: 0,
2149
+ ambiguous
2150
+ };
2151
+ }
2152
+ let rewritten = 0;
2153
+ for (const field of DEPENDENCY_FIELDS) {
2154
+ const deps = manifest[field];
2155
+ if (!deps || typeof deps !== "object") continue;
2156
+ const entries = Object.entries(deps);
2157
+ const renamed = entries.map(([name, range]) => {
2158
+ const to = renames.find(([from]) => from === name)?.[1];
2159
+ if (!to) return [
2160
+ name,
2161
+ range
2162
+ ];
2163
+ if (UNTRANSLATABLE_RANGE.test(range)) {
2164
+ ambiguous.push(`${name} in ${field} (${range})`);
2165
+ return [
2166
+ name,
2167
+ range
2168
+ ];
2169
+ }
2170
+ rewritten += 1;
2171
+ return [
2172
+ to,
2173
+ options.targetRange
2174
+ ];
2175
+ });
2176
+ manifest[field] = Object.fromEntries(renamed);
2177
+ }
2178
+ const meta = manifest.peerDependenciesMeta;
2179
+ if (meta && typeof meta === "object") {
2180
+ manifest.peerDependenciesMeta = Object.fromEntries(Object.entries(meta).map(([name, flags]) => {
2181
+ const to = renames.find(([from]) => from === name)?.[1];
2182
+ if (!to) return [
2183
+ name,
2184
+ flags
2185
+ ];
2186
+ rewritten += 1;
2187
+ return [
2188
+ to,
2189
+ flags
2190
+ ];
2191
+ }));
2192
+ }
2193
+ if (rewritten === 0) return {
2194
+ text,
2195
+ rewritten: 0,
2196
+ ambiguous
2197
+ };
2198
+ const indent = /^[ \t]+/m.exec(text)?.[0] ?? " ";
2199
+ const trailing = text.endsWith("\n") ? "\n" : "";
2200
+ return {
2201
+ text: JSON.stringify(manifest, null, indent) + trailing,
2202
+ rewritten,
2203
+ ambiguous
2204
+ };
2205
+ }
2206
+ __name(rewriteManifest, "rewriteManifest");
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.";
2017
2528
 
2018
2529
  // src/init/patch-app-module.ts
2019
2530
  var MARKER = "SaaSiCatModule.forRoot";
@@ -2035,7 +2546,7 @@ ${block}`;
2035
2546
  return {
2036
2547
  source,
2037
2548
  status: "declined",
2038
- 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",
2039
2550
  manualBlock
2040
2551
  };
2041
2552
  }
@@ -2114,7 +2625,7 @@ function renderForRootBlock(options) {
2114
2625
  "SaaSiCatModule.forRoot(",
2115
2626
  " defineSaaSiCat({",
2116
2627
  " // Plans straight from the YAML. Apps that manage plans in the",
2117
- " // SuperAdmin UI pass `dbCatalog` instead.",
2628
+ " // SuperAdmin UI pass `dbCatalog: { path: 'config/saas.yaml' }` instead.",
2118
2629
  " planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),",
2119
2630
  " // Your authentication guard. This does NOT compile until you",
2120
2631
  " // name one, and that is deliberate: an empty array is how the",
@@ -2123,6 +2634,13 @@ function renderForRootBlock(options) {
2123
2634
  " // your whole capability inventory \u2014 and the manifest routes to",
2124
2635
  " // anyone who asks. Import your guard and put it in.",
2125
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],",
2126
2644
  options.persistenceImport ? " persistence," : " // persistence: prismaPersistence({ client: PrismaService }),",
2127
2645
  " catalog: { featureUiRegistry: " + options.registry.constName + " },",
2128
2646
  " adminResources: true,",
@@ -2769,7 +3287,7 @@ DoctorCommands = _ts_decorate12([
2769
3287
 
2770
3288
  // src/discovery.command.ts
2771
3289
  var import_node_fs = require("fs");
2772
- var import_node_path = require("path");
3290
+ var import_node_path2 = require("path");
2773
3291
  var import_common13 = require("@nestjs/common");
2774
3292
  var import_nest_commander5 = require("nest-commander");
2775
3293
  var import_nest6 = require("@saasicat/nest");
@@ -2807,8 +3325,8 @@ var DiscoveryScanCommand = class extends import_nest_commander5.CommandRunner {
2807
3325
  try {
2808
3326
  const snapshot = this.scanner.rebuildSnapshot();
2809
3327
  if (flags.out) {
2810
- const outPath = (0, import_node_path.resolve)(flags.out);
2811
- (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), {
2812
3330
  recursive: true
2813
3331
  });
2814
3332
  (0, import_node_fs.writeFileSync)(outPath, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
@@ -2817,7 +3335,7 @@ var DiscoveryScanCommand = class extends import_nest_commander5.CommandRunner {
2817
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
2818
3336
  `);
2819
3337
  if (target) {
2820
- process.stdout.write(`Snapshot persisted: ${(0, import_node_path.resolve)(target)}
3338
+ process.stdout.write(`Snapshot persisted: ${(0, import_node_path2.resolve)(target)}
2821
3339
  `);
2822
3340
  } else {
2823
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");
@@ -3225,17 +3743,23 @@ UserCommands = _ts_decorate14([
3225
3743
  MfaSetupFlow,
3226
3744
  PLATFORM_DOCTOR_CHECK_PROVIDERS,
3227
3745
  PlanCatalogDoctorCheck,
3746
+ SCANNED_FOR_DB_CATALOG,
3747
+ SCANNED_FOR_MOVED_SETTINGS,
3748
+ SETTINGS_THAT_MOVED,
3228
3749
  UI_VUE_SPECIFIER,
3229
3750
  USER_MANAGEMENT_PORT_TOKEN,
3230
3751
  USER_PORT_TOKEN,
3231
3752
  UserCommands,
3232
3753
  UserPortDoctorCheck,
3754
+ WHERE_DB_CATALOG_GOES,
3755
+ WHERE_IT_GOES,
3233
3756
  WhoAmIFlow,
3757
+ appKeyPattern,
3234
3758
  appendConstraints,
3235
3759
  applyFragmentBlocks,
3236
3760
  applyTokens,
3237
3761
  assertModelsExist,
3238
- assertValidProjectKey,
3762
+ assertValidAppKey,
3239
3763
  assertValidQuotaKey,
3240
3764
  blankStringLiterals,
3241
3765
  blockBodyLines,
@@ -3243,17 +3767,24 @@ UserCommands = _ts_decorate14([
3243
3767
  buildImportMap,
3244
3768
  checkSchema,
3245
3769
  constraintsFor,
3770
+ describeDbCatalogOccurrence,
3246
3771
  enableFkPointers,
3247
3772
  extractBlockNames,
3248
3773
  extractBlocks,
3774
+ extractEnumBlocks,
3775
+ extractEnumNames,
3776
+ extractFragmentBlocks,
3249
3777
  extractModelBlocks,
3250
3778
  extractModelNames,
3779
+ findDbCatalogBlocks,
3251
3780
  findFkPointers,
3781
+ findMovedSettings,
3252
3782
  foreignKeyOf,
3253
3783
  hasBackRelation,
3254
3784
  hasConstraints,
3255
3785
  isNoLongerPublic,
3256
3786
  isOneToOne,
3787
+ judgeModuleResolution,
3257
3788
  kebabCase,
3258
3789
  migrationCreatedBy,
3259
3790
  minimumQuotasPerPlan,
@@ -3267,13 +3798,16 @@ UserCommands = _ts_decorate14([
3267
3798
  patchAppModule,
3268
3799
  patchOptionsFor,
3269
3800
  planInit,
3270
- projectKeyPattern,
3271
3801
  quotaKeyPattern,
3802
+ readEffectiveModuleResolution,
3272
3803
  relationNameOf,
3804
+ removeProjectKey,
3273
3805
  reportConstraints,
3274
3806
  rewriteImports,
3807
+ rewriteManifest,
3275
3808
  rewriteNames,
3276
3809
  rewriteSubpath,
3810
+ settingsWrittenTo,
3277
3811
  stripLineComment,
3278
3812
  structuralOnly,
3279
3813
  tablesAddressedBy