@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.js CHANGED
@@ -909,7 +909,7 @@ var PlanCatalogDoctorCheck = class {
909
909
  severity: "ok",
910
910
  message: `${plans.length} plan(s), ${this.catalog.features?.length ?? 0} feature(s) loaded.`,
911
911
  details: {
912
- projectKey: this.catalog.projectKey,
912
+ app: this.catalog.app.name,
913
913
  planIds: plans.map((p) => p.id)
914
914
  }
915
915
  };
@@ -1129,13 +1129,43 @@ function extractModelBlocks(fragment) {
1129
1129
  return extractBlocks(fragment, "model");
1130
1130
  }
1131
1131
  __name(extractModelBlocks, "extractModelBlocks");
1132
+ function extractEnumBlocks(fragment) {
1133
+ return extractBlocks(fragment, "enum");
1134
+ }
1135
+ __name(extractEnumBlocks, "extractEnumBlocks");
1136
+ function extractEnumNames(schema2) {
1137
+ return extractBlockNames(schema2, "enum");
1138
+ }
1139
+ __name(extractEnumNames, "extractEnumNames");
1140
+ function extractFragmentBlocks(fragment) {
1141
+ return {
1142
+ enums: extractEnumBlocks(fragment),
1143
+ models: extractModelBlocks(fragment)
1144
+ };
1145
+ }
1146
+ __name(extractFragmentBlocks, "extractFragmentBlocks");
1132
1147
  function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1133
- const existing = new Set(extractModelNames(schema2));
1148
+ const blocks = fragmentBlocks instanceof Map ? {
1149
+ enums: /* @__PURE__ */ new Map(),
1150
+ models: fragmentBlocks
1151
+ } : fragmentBlocks;
1152
+ const existingModels = new Set(extractModelNames(schema2));
1153
+ const existingEnums = new Set(extractEnumNames(schema2));
1134
1154
  const added = [];
1135
1155
  const skipped = [];
1156
+ const addedEnums = [];
1157
+ const skippedEnums = [];
1136
1158
  const additions = [];
1137
- for (const [name, block] of fragmentBlocks) {
1138
- if (existing.has(name)) {
1159
+ for (const [name, block] of blocks.enums) {
1160
+ if (existingEnums.has(name)) {
1161
+ skippedEnums.push(name);
1162
+ } else {
1163
+ addedEnums.push(name);
1164
+ additions.push(block);
1165
+ }
1166
+ }
1167
+ for (const [name, block] of blocks.models) {
1168
+ if (existingModels.has(name)) {
1139
1169
  skipped.push(name);
1140
1170
  } else {
1141
1171
  added.push(name);
@@ -1146,6 +1176,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1146
1176
  return {
1147
1177
  added,
1148
1178
  skipped,
1179
+ addedEnums,
1180
+ skippedEnums,
1149
1181
  schema: schema2
1150
1182
  };
1151
1183
  }
@@ -1162,6 +1194,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1162
1194
  return {
1163
1195
  added,
1164
1196
  skipped,
1197
+ addedEnums,
1198
+ skippedEnums,
1165
1199
  schema: trimmedSchema + header + additions.join("\n\n") + "\n"
1166
1200
  };
1167
1201
  }
@@ -1547,10 +1581,11 @@ function required(value, what) {
1547
1581
  return value;
1548
1582
  }
1549
1583
  __name(required, "required");
1550
- function projectKeyPattern() {
1551
- return new RegExp(required(schema.properties?.projectKey?.pattern, "pattern for projectKey"));
1584
+ var APP_KEY_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
1585
+ function appKeyPattern() {
1586
+ return APP_KEY_PATTERN;
1552
1587
  }
1553
- __name(projectKeyPattern, "projectKeyPattern");
1588
+ __name(appKeyPattern, "appKeyPattern");
1554
1589
  function quotaKeyPattern() {
1555
1590
  const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
1556
1591
  if (patterns.length !== 1) {
@@ -1563,12 +1598,12 @@ function minimumQuotasPerPlan() {
1563
1598
  return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
1564
1599
  }
1565
1600
  __name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
1566
- function assertValidProjectKey(projectKey) {
1567
- const pattern = projectKeyPattern();
1568
- if (pattern.test(projectKey)) return;
1569
- 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.`);
1601
+ function assertValidAppKey(appKey) {
1602
+ const pattern = appKeyPattern();
1603
+ if (pattern.test(appKey)) return;
1604
+ 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.`);
1570
1605
  }
1571
- __name(assertValidProjectKey, "assertValidProjectKey");
1606
+ __name(assertValidAppKey, "assertValidAppKey");
1572
1607
  function assertValidQuotaKey(quotaKey) {
1573
1608
  const pattern = quotaKeyPattern();
1574
1609
  if (pattern.test(quotaKey)) return;
@@ -1600,24 +1635,24 @@ function pascalCase(value) {
1600
1635
  __name(pascalCase, "pascalCase");
1601
1636
  var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
1602
1637
  function planInit(options) {
1603
- const projectKey = options.projectKey;
1604
- if (!projectKey) throw new Error("init needs a --project-key.");
1605
- assertValidProjectKey(projectKey);
1606
- const appLabel = options.appName ?? pascalCase(projectKey);
1638
+ const appKey = options.appKey;
1639
+ if (!appKey) throw new Error("init needs an --app-key.");
1640
+ assertValidAppKey(appKey);
1641
+ const appLabel = options.appName ?? pascalCase(appKey);
1607
1642
  const appName = pascalCase(appLabel);
1608
1643
  const apiBase = options.apiBase ?? "/api/v1/admin";
1609
1644
  const quotas = (options.quotas ?? []).map(parseQuota);
1610
1645
  assertEnoughQuotas(quotas);
1611
1646
  const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
1612
- const featureKey = `${projectKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
1647
+ const featureKey = `${appKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
1613
1648
  const shared = {
1614
- PROJECT_KEY: projectKey,
1649
+ APP_KEY: appKey,
1615
1650
  APP_NAME: appName,
1616
1651
  APP_LABEL: appLabel,
1617
1652
  API_BASE: apiBase,
1618
1653
  FEATURE_KEY: featureKey,
1619
- REGISTRY_CONST: `${constantCase(projectKey)}_FEATURE_UI_REGISTRY`,
1620
- MANIFEST_CONST: `${constantCase(projectKey)}_MANIFEST_CONTRIBUTION`,
1654
+ REGISTRY_CONST: `${constantCase(appKey)}_FEATURE_UI_REGISTRY`,
1655
+ MANIFEST_CONST: `${constantCase(appKey)}_MANIFEST_CONTRIBUTION`,
1621
1656
  ADMIN_MODULE_CLASS: `${appName}AdminModule`,
1622
1657
  HASHER_CLASS: hasherClass ?? "",
1623
1658
  HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
@@ -1716,6 +1751,63 @@ function patchOptionsFor(plan) {
1716
1751
  }
1717
1752
  __name(patchOptionsFor, "patchOptionsFor");
1718
1753
 
1754
+ // src/init/module-resolution.ts
1755
+ import { join } from "path";
1756
+ var RESOLVES_SUBPATHS = /* @__PURE__ */ new Set([
1757
+ "node16",
1758
+ "nodenext",
1759
+ "bundler"
1760
+ ]);
1761
+ function judgeModuleResolution(value) {
1762
+ if (value === null || RESOLVES_SUBPATHS.has(value)) {
1763
+ return {
1764
+ ok: true,
1765
+ value
1766
+ };
1767
+ }
1768
+ return {
1769
+ ok: false,
1770
+ value,
1771
+ 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.'
1772
+ };
1773
+ }
1774
+ __name(judgeModuleResolution, "judgeModuleResolution");
1775
+ function readEffectiveModuleResolution(root, ts) {
1776
+ const configPath = join(root, "tsconfig.json");
1777
+ const read = ts.readConfigFile(configPath, ts.sys.readFile);
1778
+ if (read.error || read.config === void 0) return null;
1779
+ const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root);
1780
+ const kind = parsed.options.moduleResolution;
1781
+ if (kind === void 0) return null;
1782
+ return ts.ModuleResolutionKind[kind].toLowerCase();
1783
+ }
1784
+ __name(readEffectiveModuleResolution, "readEffectiveModuleResolution");
1785
+
1786
+ // src/init/settings-written.ts
1787
+ import { loadPlanCatalogFromString } from "@saasicat/nest/billing";
1788
+ function flatten(prefix, value, into) {
1789
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1790
+ for (const [key, child] of Object.entries(value)) {
1791
+ flatten(`${prefix}.${key}`, child, into);
1792
+ }
1793
+ return;
1794
+ }
1795
+ into.push({
1796
+ key: prefix,
1797
+ value: JSON.stringify(value)
1798
+ });
1799
+ }
1800
+ __name(flatten, "flatten");
1801
+ function settingsWrittenTo(catalogYaml, source = "config/saas.yaml") {
1802
+ const catalog = loadPlanCatalogFromString(catalogYaml, {
1803
+ source
1804
+ });
1805
+ const settings = [];
1806
+ flatten("tenantBilling", catalog.tenantBilling, settings);
1807
+ return settings;
1808
+ }
1809
+ __name(settingsWrittenTo, "settingsWrittenTo");
1810
+
1719
1811
  // src/codemods/v1-imports.ts
1720
1812
  var PUBLIC_PREFIXES = [
1721
1813
  "ui/",
@@ -1979,6 +2071,327 @@ function rewriteManifest(text, table, options) {
1979
2071
  }
1980
2072
  __name(rewriteManifest, "rewriteManifest");
1981
2073
 
2074
+ // src/codemods/v1-project-key.ts
2075
+ function stripQueryParameter(text) {
2076
+ const NEEDLE = "projectKey=";
2077
+ let out = "";
2078
+ let index = 0;
2079
+ const taken = /* @__PURE__ */ new Set();
2080
+ for (; ; ) {
2081
+ const at = text.indexOf(NEEDLE, index);
2082
+ if (at < 0) break;
2083
+ const separator = at === 0 ? "" : text[at - 1];
2084
+ const value = separator === "?" || separator === "&" ? simpleValueEnd(text, at + NEEDLE.length) : null;
2085
+ if (value === null || !servesTheCatalogue(text, at)) {
2086
+ out += text.slice(index, at + NEEDLE.length);
2087
+ index = at + NEEDLE.length;
2088
+ continue;
2089
+ }
2090
+ out += text.slice(index, at - 1);
2091
+ if (separator === "?" && text[value] === "&") {
2092
+ out += "?";
2093
+ index = value + 1;
2094
+ } else {
2095
+ index = value;
2096
+ }
2097
+ taken.add(at);
2098
+ }
2099
+ return {
2100
+ text: out + text.slice(index),
2101
+ taken
2102
+ };
2103
+ }
2104
+ __name(stripQueryParameter, "stripQueryParameter");
2105
+ var HIDES_A_BRACE = /[`'"{}/\\]/;
2106
+ function simpleValueEnd(text, from) {
2107
+ let i = from;
2108
+ while (i < text.length) {
2109
+ const ch = text[i];
2110
+ if (ch === "&" || ch === "#" || ch === "'" || ch === '"' || ch === "`" || ch === "\n" || ch === " ") {
2111
+ return i;
2112
+ }
2113
+ if (ch !== "$" || text[i + 1] !== "{") {
2114
+ if (ch === "{" || ch === "}") return null;
2115
+ i += 1;
2116
+ continue;
2117
+ }
2118
+ const close = text.indexOf("}", i + 2);
2119
+ if (close < 0) return null;
2120
+ if (HIDES_A_BRACE.test(text.slice(i + 2, close))) return null;
2121
+ i = close + 1;
2122
+ }
2123
+ return null;
2124
+ }
2125
+ __name(simpleValueEnd, "simpleValueEnd");
2126
+ function servesTheCatalogue(text, at) {
2127
+ let start = at;
2128
+ while (start > 0) {
2129
+ const ch = text[start - 1];
2130
+ if (ch === "`" || ch === "'" || ch === '"' || ch === "\n") break;
2131
+ start -= 1;
2132
+ }
2133
+ const url = text.slice(start, at);
2134
+ const query = url.indexOf("?");
2135
+ return (query < 0 ? url : url.slice(0, query)).includes("/catalog/");
2136
+ }
2137
+ __name(servesTheCatalogue, "servesTheCatalogue");
2138
+ function isIdentifierChar(ch) {
2139
+ return ch !== void 0 && /[A-Za-z0-9_$]/.test(ch);
2140
+ }
2141
+ __name(isIdentifierChar, "isIdentifierChar");
2142
+ function lineAt(text, at) {
2143
+ let line = 1;
2144
+ for (let i = 0; i < at; i += 1) if (text[i] === "\n") line += 1;
2145
+ return line;
2146
+ }
2147
+ __name(lineAt, "lineAt");
2148
+ function removeProjectKey(text, kind = "source") {
2149
+ if (kind === "yaml") return removeFromYaml(text);
2150
+ const query = stripQueryParameter(text);
2151
+ const reported = /* @__PURE__ */ new Set();
2152
+ for (let at = text.indexOf("projectKey"); at >= 0; at = text.indexOf("projectKey", at + 1)) {
2153
+ if (query.taken.has(at)) continue;
2154
+ if (isIdentifierChar(text[at - 1])) continue;
2155
+ if (isIdentifierChar(text[at + "projectKey".length])) continue;
2156
+ reported.add(lineAt(text, at));
2157
+ }
2158
+ return {
2159
+ text: query.text,
2160
+ rewritten: query.taken.size,
2161
+ undecided: [
2162
+ ...reported
2163
+ ].sort((a, b) => a - b)
2164
+ };
2165
+ }
2166
+ __name(removeProjectKey, "removeProjectKey");
2167
+ function removeFromYaml(text) {
2168
+ const lines = text.split("\n");
2169
+ const kept = [];
2170
+ let rewritten = 0;
2171
+ for (const line of lines) {
2172
+ if (line.startsWith("projectKey:")) {
2173
+ rewritten += 1;
2174
+ continue;
2175
+ }
2176
+ kept.push(line);
2177
+ }
2178
+ return {
2179
+ text: kept.join("\n"),
2180
+ rewritten,
2181
+ undecided: []
2182
+ };
2183
+ }
2184
+ __name(removeFromYaml, "removeFromYaml");
2185
+
2186
+ // src/codemods/v1-moved-settings.ts
2187
+ import { planCatalogSchema as planCatalogSchema2 } from "@saasicat/spec";
2188
+ var SETTINGS_THAT_MOVED = Object.keys(planCatalogSchema2.properties?.tenantBilling?.properties ?? (() => {
2189
+ throw new Error("plan-catalog.schema.json declares no tenantBilling properties \u2014 @saasicat/spec and @saasicat/cli are out of step.");
2190
+ })());
2191
+ var isIdentifierChar2 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
2192
+ function lineAt2(text, index) {
2193
+ let line = 1;
2194
+ for (let at = 0; at < index; at += 1) {
2195
+ if (text[at] === "\n") line += 1;
2196
+ }
2197
+ return line;
2198
+ }
2199
+ __name(lineAt2, "lineAt");
2200
+ var SCANNED_FOR_MOVED_SETTINGS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue)$/;
2201
+ function findMovedSettings(text) {
2202
+ const occurrences = [];
2203
+ for (const setting of SETTINGS_THAT_MOVED) {
2204
+ for (let at = text.indexOf(setting); at >= 0; at = text.indexOf(setting, at + 1)) {
2205
+ if (isIdentifierChar2(text[at - 1])) continue;
2206
+ if (isIdentifierChar2(text[at + setting.length])) continue;
2207
+ occurrences.push({
2208
+ setting,
2209
+ line: lineAt2(text, at)
2210
+ });
2211
+ }
2212
+ }
2213
+ return {
2214
+ occurrences: occurrences.sort((a, b) => a.line - b.line || a.setting.localeCompare(b.setting))
2215
+ };
2216
+ }
2217
+ __name(findMovedSettings, "findMovedSettings");
2218
+ var WHERE_IT_GOES = {
2219
+ 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.",
2220
+ selfServiceBlockedPlans: 'config/saas.yaml \u2192 tenantBilling.selfServiceBlockedPlans \u2014 both `asTarget` and `asSource` are required, and `[]` is the way to say "nothing is blocked".'
2221
+ };
2222
+
2223
+ // src/codemods/v1-db-catalog.ts
2224
+ import { DB_CATALOG_MEMBERS } from "@saasicat/nest/platform";
2225
+ var SCANNED_FOR_DB_CATALOG = SCANNED_FOR_MOVED_SETTINGS;
2226
+ var PROPERTY = "dbCatalog";
2227
+ var TAKEN = new Set(DB_CATALOG_MEMBERS);
2228
+ var isIdentifierStart = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z_$]/.test(ch), "isIdentifierStart");
2229
+ var isIdentifierChar3 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
2230
+ var isBlank = /* @__PURE__ */ __name((ch) => ch === " " || ch === " " || ch === "\n" || ch === "\r", "isBlank");
2231
+ function lineAt3(text, index) {
2232
+ let line = 1;
2233
+ for (let at = 0; at < index; at += 1) {
2234
+ if (text[at] === "\n") line += 1;
2235
+ }
2236
+ return line;
2237
+ }
2238
+ __name(lineAt3, "lineAt");
2239
+ function skipBlanks(text, from) {
2240
+ let at = from;
2241
+ while (isBlank(text[at])) at += 1;
2242
+ return at;
2243
+ }
2244
+ __name(skipBlanks, "skipBlanks");
2245
+ function closingQuote(text, open) {
2246
+ const quote = text[open];
2247
+ for (let at = open + 1; at < text.length; at += 1) {
2248
+ if (text[at] === "\\") {
2249
+ at += 1;
2250
+ continue;
2251
+ }
2252
+ if (text[at] === quote) return at;
2253
+ }
2254
+ return text.length;
2255
+ }
2256
+ __name(closingQuote, "closingQuote");
2257
+ function skipCommentOrString(text, at) {
2258
+ const ch = text[at];
2259
+ if (ch === "/" && text[at + 1] === "/") {
2260
+ const end = text.indexOf("\n", at);
2261
+ return end === -1 ? text.length : end;
2262
+ }
2263
+ if (ch === "/" && text[at + 1] === "*") {
2264
+ const end = text.indexOf("*/", at + 2);
2265
+ return end === -1 ? text.length : end + 2;
2266
+ }
2267
+ if (ch === "'" || ch === '"' || ch === "`") return closingQuote(text, at) + 1;
2268
+ return at;
2269
+ }
2270
+ __name(skipCommentOrString, "skipCommentOrString");
2271
+ function objectLiteralAt(text, open) {
2272
+ const members = [];
2273
+ let depth = 0;
2274
+ let expectingKey = true;
2275
+ let at = open + 1;
2276
+ while (at < text.length) {
2277
+ const ch = text[at];
2278
+ if (ch === "'" || ch === '"' || ch === "`") {
2279
+ const end = closingQuote(text, at);
2280
+ if (depth === 0 && expectingKey && text[skipBlanks(text, end + 1)] === ":") {
2281
+ members.push(text.slice(at + 1, end));
2282
+ }
2283
+ at = end + 1;
2284
+ continue;
2285
+ }
2286
+ const skipped = skipCommentOrString(text, at);
2287
+ if (skipped > at) {
2288
+ at = skipped;
2289
+ continue;
2290
+ }
2291
+ if (ch === "{" || ch === "[" || ch === "(") {
2292
+ depth += 1;
2293
+ at += 1;
2294
+ continue;
2295
+ }
2296
+ if (ch === "}" || ch === "]" || ch === ")") {
2297
+ if (depth === 0) return {
2298
+ members,
2299
+ end: at + 1
2300
+ };
2301
+ depth -= 1;
2302
+ at += 1;
2303
+ continue;
2304
+ }
2305
+ if (depth === 0 && ch === ":") expectingKey = false;
2306
+ if (depth === 0 && ch === ",") expectingKey = true;
2307
+ if (depth === 0 && expectingKey && ch === "." && text.startsWith("...", at)) {
2308
+ let end = at + 3;
2309
+ while (isIdentifierChar3(text[end])) end += 1;
2310
+ members.push(text.slice(at, end));
2311
+ at = end;
2312
+ continue;
2313
+ }
2314
+ if (depth === 0 && expectingKey && isIdentifierStart(ch)) {
2315
+ let end = at;
2316
+ while (isIdentifierChar3(text[end])) end += 1;
2317
+ const next = text[skipBlanks(text, end)];
2318
+ if (next === ":" || next === "," || next === "}") members.push(text.slice(at, end));
2319
+ at = end;
2320
+ continue;
2321
+ }
2322
+ at += 1;
2323
+ }
2324
+ return {
2325
+ members,
2326
+ end: text.length
2327
+ };
2328
+ }
2329
+ __name(objectLiteralAt, "objectLiteralAt");
2330
+ function findDbCatalogBlocks(text) {
2331
+ const occurrences = [];
2332
+ let at = 0;
2333
+ while (at < text.length) {
2334
+ const skipped = skipCommentOrString(text, at);
2335
+ if (skipped > at) {
2336
+ at = skipped;
2337
+ continue;
2338
+ }
2339
+ if (!text.startsWith(PROPERTY, at) || isIdentifierChar3(text[at - 1])) {
2340
+ at += 1;
2341
+ continue;
2342
+ }
2343
+ const afterName = at + PROPERTY.length;
2344
+ if (isIdentifierChar3(text[afterName])) {
2345
+ at = afterName;
2346
+ continue;
2347
+ }
2348
+ const colon = skipBlanks(text, afterName);
2349
+ if (text[colon] !== ":") {
2350
+ at = afterName;
2351
+ continue;
2352
+ }
2353
+ const value = skipBlanks(text, colon + 1);
2354
+ const line = lineAt3(text, at);
2355
+ if (text[value] !== "{") {
2356
+ occurrences.push({
2357
+ line,
2358
+ shape: "reference",
2359
+ leftovers: []
2360
+ });
2361
+ at = value;
2362
+ continue;
2363
+ }
2364
+ const { members, end } = objectLiteralAt(text, value);
2365
+ const leftovers = members.filter((member) => !TAKEN.has(member));
2366
+ if (!members.includes("path")) {
2367
+ occurrences.push({
2368
+ line,
2369
+ shape: "values",
2370
+ leftovers
2371
+ });
2372
+ } else if (leftovers.length > 0) {
2373
+ occurrences.push({
2374
+ line,
2375
+ shape: "mixed",
2376
+ leftovers
2377
+ });
2378
+ }
2379
+ at = end;
2380
+ }
2381
+ return {
2382
+ occurrences
2383
+ };
2384
+ }
2385
+ __name(findDbCatalogBlocks, "findDbCatalogBlocks");
2386
+ function describeDbCatalogOccurrence(occurrence) {
2387
+ const { shape, leftovers } = occurrence;
2388
+ if (shape === "reference") return "carries something this cannot see into";
2389
+ if (shape === "mixed") return `names the path but still carries ${leftovers.join(", ")}`;
2390
+ return leftovers.length > 0 ? `carries the values: ${leftovers.join(", ")}` : "names no path";
2391
+ }
2392
+ __name(describeDbCatalogOccurrence, "describeDbCatalogOccurrence");
2393
+ 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.";
2394
+
1982
2395
  // src/init/patch-app-module.ts
1983
2396
  var MARKER = "SaaSiCatModule.forRoot";
1984
2397
  function patchAppModule(source, options) {
@@ -1999,7 +2412,7 @@ ${block}`;
1999
2412
  return {
2000
2413
  source,
2001
2414
  status: "declined",
2002
- reason: "no `@Module({ imports: [ ... ] })` was found in this file, so there is nowhere to add the platform without guessing at the structure",
2415
+ 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",
2003
2416
  manualBlock
2004
2417
  };
2005
2418
  }
@@ -2078,7 +2491,7 @@ function renderForRootBlock(options) {
2078
2491
  "SaaSiCatModule.forRoot(",
2079
2492
  " defineSaaSiCat({",
2080
2493
  " // Plans straight from the YAML. Apps that manage plans in the",
2081
- " // SuperAdmin UI pass `dbCatalog` instead.",
2494
+ " // SuperAdmin UI pass `dbCatalog: { path: 'config/saas.yaml' }` instead.",
2082
2495
  " planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),",
2083
2496
  " // Your authentication guard. This does NOT compile until you",
2084
2497
  " // name one, and that is deliberate: an empty array is how the",
@@ -2087,6 +2500,13 @@ function renderForRootBlock(options) {
2087
2500
  " // your whole capability inventory \u2014 and the manifest routes to",
2088
2501
  " // anyone who asks. Import your guard and put it in.",
2089
2502
  " controller: { guards: [YourAuthGuard] },",
2503
+ " // The modules whose providers the platform injects: the one",
2504
+ " // exporting PrismaService, and the one your guard depends on.",
2505
+ " // `prismaPersistence({ client: PrismaService })` is resolved",
2506
+ " // inside the platform module, which sees only what is listed",
2507
+ " // here or declared @Global. Same rule as the guard: this does",
2508
+ " // not compile until you name them.",
2509
+ " imports: [YourPrismaModule, YourAuthModule],",
2090
2510
  options.persistenceImport ? " persistence," : " // persistence: prismaPersistence({ client: PrismaService }),",
2091
2511
  " catalog: { featureUiRegistry: " + options.registry.constName + " },",
2092
2512
  " adminResources: true,",
@@ -3188,17 +3608,23 @@ export {
3188
3608
  MfaSetupFlow,
3189
3609
  PLATFORM_DOCTOR_CHECK_PROVIDERS,
3190
3610
  PlanCatalogDoctorCheck,
3611
+ SCANNED_FOR_DB_CATALOG,
3612
+ SCANNED_FOR_MOVED_SETTINGS,
3613
+ SETTINGS_THAT_MOVED,
3191
3614
  UI_VUE_SPECIFIER,
3192
3615
  USER_MANAGEMENT_PORT_TOKEN,
3193
3616
  USER_PORT_TOKEN,
3194
3617
  UserCommands,
3195
3618
  UserPortDoctorCheck,
3619
+ WHERE_DB_CATALOG_GOES,
3620
+ WHERE_IT_GOES,
3196
3621
  WhoAmIFlow,
3622
+ appKeyPattern,
3197
3623
  appendConstraints,
3198
3624
  applyFragmentBlocks,
3199
3625
  applyTokens,
3200
3626
  assertModelsExist,
3201
- assertValidProjectKey,
3627
+ assertValidAppKey,
3202
3628
  assertValidQuotaKey,
3203
3629
  blankStringLiterals,
3204
3630
  blockBodyLines,
@@ -3206,17 +3632,24 @@ export {
3206
3632
  buildImportMap,
3207
3633
  checkSchema,
3208
3634
  constraintsFor,
3635
+ describeDbCatalogOccurrence,
3209
3636
  enableFkPointers,
3210
3637
  extractBlockNames,
3211
3638
  extractBlocks,
3639
+ extractEnumBlocks,
3640
+ extractEnumNames,
3641
+ extractFragmentBlocks,
3212
3642
  extractModelBlocks,
3213
3643
  extractModelNames,
3644
+ findDbCatalogBlocks,
3214
3645
  findFkPointers,
3646
+ findMovedSettings,
3215
3647
  foreignKeyOf,
3216
3648
  hasBackRelation,
3217
3649
  hasConstraints,
3218
3650
  isNoLongerPublic,
3219
3651
  isOneToOne,
3652
+ judgeModuleResolution,
3220
3653
  kebabCase,
3221
3654
  migrationCreatedBy,
3222
3655
  minimumQuotasPerPlan,
@@ -3230,14 +3663,16 @@ export {
3230
3663
  patchAppModule,
3231
3664
  patchOptionsFor,
3232
3665
  planInit,
3233
- projectKeyPattern,
3234
3666
  quotaKeyPattern,
3667
+ readEffectiveModuleResolution,
3235
3668
  relationNameOf,
3669
+ removeProjectKey,
3236
3670
  reportConstraints,
3237
3671
  rewriteImports,
3238
3672
  rewriteManifest,
3239
3673
  rewriteNames,
3240
3674
  rewriteSubpath,
3675
+ settingsWrittenTo,
3241
3676
  stripLineComment,
3242
3677
  structuralOnly,
3243
3678
  tablesAddressedBy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/cli",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.11",
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",
@@ -16,7 +16,8 @@
16
16
  "types": "./dist/index.d.cts",
17
17
  "default": "./dist/index.cjs"
18
18
  }
19
- }
19
+ },
20
+ "./package.json": "./package.json"
20
21
  },
21
22
  "files": [
22
23
  "dist",
@@ -29,9 +30,9 @@
29
30
  },
30
31
  "dependencies": {
31
32
  "qrcode-terminal": "^0.12.0",
32
- "@saasicat/nest": "^1.0.0-rc.1",
33
- "@saasicat/spec": "^1.0.0-rc.1",
34
- "@saasicat/core": "^1.0.0-rc.1"
33
+ "@saasicat/nest": "^1.0.0-rc.11",
34
+ "@saasicat/spec": "^1.0.0-rc.11",
35
+ "@saasicat/core": "^1.0.0-rc.11"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "@nestjs/common": "^11.0.0",