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

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/bin/saasicat.js CHANGED
@@ -27,7 +27,7 @@
27
27
 
28
28
  import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
29
29
  import { existsSync } from 'node:fs';
30
- import { dirname, join, resolve } from 'node:path';
30
+ import { basename, dirname, join, resolve } from 'node:path';
31
31
  import { createRequire } from 'node:module';
32
32
  import { fileURLToPath } from 'node:url';
33
33
  import { spawn } from 'node:child_process';
@@ -43,13 +43,16 @@ import {
43
43
  applyTokens,
44
44
  constraintsFor,
45
45
  checkSchema,
46
- extractModelBlocks,
46
+ extractFragmentBlocks,
47
47
  findFkPointers,
48
48
  hasConstraints,
49
49
  assertValidProjectKey,
50
+ judgeModuleResolution,
51
+ readEffectiveModuleResolution,
50
52
  buildImportMap,
51
53
  migrationCreatedBy,
52
54
  rewriteImports,
55
+ rewriteManifest,
53
56
  rewriteNames,
54
57
  reportConstraints,
55
58
  patchAppModule,
@@ -132,14 +135,15 @@ async function selectFragmentFiles(dir, filter) {
132
135
 
133
136
  async function loadFragments(dir, filter) {
134
137
  const selected = await selectFragmentFiles(dir, filter);
135
- const blocks = new Map();
138
+ const blocks = { enums: new Map(), models: new Map() };
136
139
  for (const file of selected) {
137
140
  const content = await readFile(join(dir, file), 'utf8');
138
- const fileBlocks = extractModelBlocks(content);
139
- for (const [name, body] of fileBlocks) {
140
- if (!blocks.has(name)) {
141
- blocks.set(name, body);
142
- }
141
+ const fileBlocks = extractFragmentBlocks(content);
142
+ for (const [name, body] of fileBlocks.enums) {
143
+ if (!blocks.enums.has(name)) blocks.enums.set(name, body);
144
+ }
145
+ for (const [name, body] of fileBlocks.models) {
146
+ if (!blocks.models.has(name)) blocks.models.set(name, body);
143
147
  }
144
148
  }
145
149
  return { files: selected, blocks };
@@ -171,7 +175,7 @@ async function cmdSchemaApply(args) {
171
175
  }
172
176
 
173
177
  const { files, blocks } = await loadFragments(fragmentsDir, filter);
174
- if (blocks.size === 0) {
178
+ if (blocks.models.size === 0) {
175
179
  console.error('✗ No models found in the selected fragments.');
176
180
  process.exit(1);
177
181
  }
@@ -185,13 +189,16 @@ async function cmdSchemaApply(args) {
185
189
  // one where the manual step is most likely to have been forgotten.
186
190
  const fk = resolveFkPointers(result.schema, args);
187
191
 
188
- if (result.added.length === 0 && fk.enabled.length === 0) {
192
+ if (result.added.length === 0 && result.addedEnums.length === 0 && fk.enabled.length === 0) {
189
193
  console.log(`→ Nothing to do. Models already present: ${result.skipped.join(', ')}`);
190
194
  reportFkPointers(fk, args);
191
195
  return;
192
196
  }
193
197
 
194
198
  if (args['dry-run']) {
199
+ if (result.addedEnums.length) {
200
+ console.log(`(--dry-run) Would append enums: ${result.addedEnums.join(', ')}`);
201
+ }
195
202
  if (result.added.length) {
196
203
  console.log(`(--dry-run) Would append: ${result.added.join(', ')}`);
197
204
  }
@@ -209,12 +216,18 @@ async function cmdSchemaApply(args) {
209
216
  for (const { line } of fk.enabled) {
210
217
  console.log(` ${line + 1}: ${fk.schema.split('\n')[line]?.trim() ?? ''}`);
211
218
  }
212
- if (fk.enabled.length > 0 && result.added.length > 0) console.log('');
213
- if (result.added.length > 0) console.log(result.schema.slice(schema.length));
219
+ const appended = result.added.length > 0 || result.addedEnums.length > 0;
220
+ if (fk.enabled.length > 0 && appended) console.log('');
221
+ if (appended) console.log(result.schema.slice(schema.length));
214
222
  return;
215
223
  }
216
224
 
217
225
  await writeFile(schemaPath, fk.schema, 'utf8');
226
+ if (result.addedEnums.length) {
227
+ console.log(
228
+ `✓ Appended ${result.addedEnums.length} enum(s): ${result.addedEnums.join(', ')}`,
229
+ );
230
+ }
218
231
  console.log(`✓ Appended ${result.added.length} model(s): ${result.added.join(', ')}`);
219
232
  if (result.skipped.length) {
220
233
  console.log(`→ Skipped (already present): ${result.skipped.join(', ')}`);
@@ -548,6 +561,7 @@ async function cmdCodemodV1Imports(args) {
548
561
  let rewritten = 0;
549
562
  let touched = 0;
550
563
  await walkSources(root, async (full, source) => {
564
+ if (basename(full) === CODEMOD_MANIFEST) return;
551
565
  const result = rewriteImports(source, map);
552
566
  for (const [subpath, n] of result.unmapped) {
553
567
  unmapped.set(subpath, (unmapped.get(subpath) ?? 0) + n);
@@ -592,8 +606,15 @@ async function cmdCodemodV1Rename(args) {
592
606
  const ambiguous = new Map();
593
607
  let rewritten = 0;
594
608
  let touched = 0;
609
+ let manifestsTouched = 0;
595
610
  await walkSources(root, async (full, source) => {
596
- const result = rewriteNames(source, table);
611
+ // A manifest takes the package renames in its dependency fields; a
612
+ // source file takes everything. Under pnpm an import a manifest does
613
+ // not declare fails to resolve, so the two travel together.
614
+ const result =
615
+ basename(full) === CODEMOD_MANIFEST
616
+ ? rewriteManifest(source, table, { targetRange: `^${OWN_VERSION}` })
617
+ : rewriteNames(source, table);
597
618
  for (const name of result.ambiguous) {
598
619
  ambiguous.set(name, (ambiguous.get(name) ?? 0) + 1);
599
620
  }
@@ -601,11 +622,26 @@ async function cmdCodemodV1Rename(args) {
601
622
  if (!dryRun) await writeFile(full, result.text);
602
623
  rewritten += result.rewritten;
603
624
  touched += 1;
625
+ if (basename(full) === CODEMOD_MANIFEST) manifestsTouched += 1;
604
626
  });
605
627
 
606
628
  console.log(
607
629
  `${dryRun ? 'Would rewrite' : 'Rewrote'} ${rewritten} name(s) in ${touched} file(s).`,
608
630
  );
631
+ if (manifestsTouched > 0) {
632
+ // The lockfile is not rewritten: its shape is the package manager's,
633
+ // and a wrong guess at it is worse than an honest instruction. A CI
634
+ // that installs with a frozen lockfile refuses the migrated checkout
635
+ // until it is regenerated.
636
+ console.log('');
637
+ console.log(
638
+ `${manifestsTouched} package.json ${manifestsTouched === 1 ? 'file' : 'files'} changed — ` +
639
+ 'regenerate the lockfile before committing:',
640
+ );
641
+ console.log(
642
+ ' pnpm install (or npm install / yarn install, whichever owns your lockfile)',
643
+ );
644
+ }
609
645
  if (ambiguous.size === 0) return;
610
646
 
611
647
  console.log('');
@@ -617,8 +653,20 @@ async function cmdCodemodV1Rename(args) {
617
653
  console.log(' FEATURE_UI_REGISTRY_TOKEN meant one registry in `@saasicat/nest/billing`');
618
654
  console.log(' and another in `@saasicat/nest/catalog`. Import it from the entry you');
619
655
  console.log(' mean — BILLING_FEATURE_UI_REGISTRY_TOKEN or CATALOG_FEATURE_UI_REGISTRY_TOKEN.');
656
+ console.log(' A dependency listed "in <field> (<range>)" points at a workspace or a path;');
657
+ console.log(' rename it by hand to @saasicat/core at the location you keep it.');
620
658
  }
621
659
 
660
+ /**
661
+ * The version this CLI was released as — and therefore the line a consumer
662
+ * running its codemod is migrating to. The manifest rewrite sets the renamed
663
+ * dependency to `^<this>`, because the old range (`^0.27.0`) names a line
664
+ * the renamed package was never published on.
665
+ */
666
+ const OWN_VERSION = JSON.parse(
667
+ await readFile(join(dirname(require_.resolve('@saasicat/cli')), '..', 'package.json'), 'utf8'),
668
+ ).version;
669
+
622
670
  /** Where a shipped codemod table lives, resolved through the package itself. */
623
671
  function codemodTable(name) {
624
672
  return join(dirname(require_.resolve('@saasicat/cli')), '..', 'codemods', name);
@@ -630,6 +678,8 @@ function codemodTable(name) {
630
678
  const CODEMOD_SKIP = new Set(['node_modules', '.git', '.output', 'coverage']);
631
679
  const isBuildOutput = (name) => name === 'dist' || name.startsWith('dist-');
632
680
  const CODEMOD_EXTENSIONS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue|md)$/;
681
+ /** Walked for the package renames alone; see `rewriteManifest`. */
682
+ const CODEMOD_MANIFEST = 'package.json';
633
683
 
634
684
  /** Every source file under `root` a codemod may touch, with its text. */
635
685
  async function walkSources(root, visit) {
@@ -641,7 +691,7 @@ async function walkSources(root, visit) {
641
691
  await walk(full);
642
692
  continue;
643
693
  }
644
- if (!CODEMOD_EXTENSIONS.test(entry.name)) continue;
694
+ if (!CODEMOD_EXTENSIONS.test(entry.name) && entry.name !== CODEMOD_MANIFEST) continue;
645
695
  await visit(full, await readFile(full, 'utf8'));
646
696
  }
647
697
  };
@@ -673,6 +723,23 @@ async function cmdInit(args, argv) {
673
723
  }
674
724
 
675
725
  const root = resolve(args.dir ?? '.');
726
+
727
+ // Before anything is written: the files below import subpath exports,
728
+ // and under `moduleResolution: node` none of them resolves. Found by
729
+ // running the quickstart against an app that predates `nodenext`.
730
+ if (existsSync(join(root, 'tsconfig.json'))) {
731
+ const ts = loadTypeScript(root);
732
+ if (ts === null) {
733
+ console.log('! tsconfig.json not checked: no TypeScript resolvable from this project.');
734
+ } else {
735
+ const verdict = judgeModuleResolution(readEffectiveModuleResolution(root, ts));
736
+ if (!verdict.ok) {
737
+ console.error(`✗ ${verdict.reason}`);
738
+ process.exit(1);
739
+ }
740
+ }
741
+ }
742
+
676
743
  const plan = planInit({
677
744
  projectKey: args['project-key'],
678
745
  appName: args['app-name'],
@@ -734,12 +801,33 @@ async function cmdInit(args, argv) {
734
801
  console.log(' — the generated app does NOT compile until you do. An empty');
735
802
  console.log(' array means "deliberately auth-free" to the platform, and');
736
803
  console.log(' would publish GET /admin/discovery to anyone who asks.');
804
+ console.log(' 2. Name the modules in `imports: [YourPrismaModule, YourAuthModule]`');
805
+ console.log(' — the one exporting PrismaService and the one your guard needs.');
806
+ console.log(' The platform module resolves its providers from that list;');
807
+ console.log(' without it the first boot stops at "Nest can\'t resolve');
808
+ console.log(' dependencies of … (PrismaService)".');
737
809
  if (plan.quotaProviders.length > 0) {
738
- console.log(' 2. Check each quota provider counts the right thing');
739
- console.log(' 3. saasicat schema migrate --name=add_saasicat');
810
+ console.log(' 3. Check each quota provider counts the right thing');
811
+ console.log(' 4. saasicat schema migrate --name=add_saasicat');
740
812
  } else {
741
- console.log(' 2. saasicat schema migrate --name=add_saasicat');
813
+ console.log(' 3. saasicat schema migrate --name=add_saasicat');
814
+ }
815
+ }
816
+
817
+ /**
818
+ * The TypeScript that will compile the project: the consumer's own, resolved
819
+ * from the project root, so the tsconfig is read the way the build reads it.
820
+ * Falls back to the one this CLI was installed with, then to null.
821
+ */
822
+ function loadTypeScript(root) {
823
+ for (const from of [join(root, 'package.json'), import.meta.url]) {
824
+ try {
825
+ return createRequire(from)('typescript');
826
+ } catch {
827
+ // Not resolvable from here — try the next origin.
828
+ }
742
829
  }
830
+ return null;
743
831
  }
744
832
 
745
833
  /** Adds the platform to `src/app.module.ts`, or says exactly what to paste. */
@@ -15,6 +15,8 @@
15
15
  "consumer that spelled a key themselves gets the same symbol the platform",
16
16
  "now registers; without this their injection would resolve to nothing.",
17
17
  "",
18
+ "`packages` renames a whole package — in specifiers and in package.json.",
19
+ "",
18
20
  "`entryTokens` is keyed by the import specifier, because the old name",
19
21
  "meant different registries in different entries. An import of it from",
20
22
  "anywhere else is reported, not guessed."
@@ -41,5 +43,9 @@
41
43
  },
42
44
  "subpaths": {
43
45
  "@saasicat/ui-vue/testing-e2e/": "@saasicat/ui-vue/testing/"
46
+ },
47
+ "packages": {
48
+ "_": "A package that was renamed. Applied to import specifiers AND to the dependency fields of every package.json the walk meets: an import rewritten without its manifest does not resolve under pnpm.",
49
+ "@saasicat/types": "@saasicat/core"
44
50
  }
45
51
  }
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- 87fdb737df2d686fe3d296add0d87c2a3765371380b486e2ff20fda896b3db65
1
+ d16106f714ce74c061cc0211a2be4ebe01a1cb80b8e4b5da45ff296c861cff71
package/dist/index.cjs CHANGED
@@ -85,6 +85,9 @@ __export(index_exports, {
85
85
  enableFkPointers: () => enableFkPointers,
86
86
  extractBlockNames: () => extractBlockNames,
87
87
  extractBlocks: () => extractBlocks,
88
+ extractEnumBlocks: () => extractEnumBlocks,
89
+ extractEnumNames: () => extractEnumNames,
90
+ extractFragmentBlocks: () => extractFragmentBlocks,
88
91
  extractModelBlocks: () => extractModelBlocks,
89
92
  extractModelNames: () => extractModelNames,
90
93
  findFkPointers: () => findFkPointers,
@@ -93,6 +96,7 @@ __export(index_exports, {
93
96
  hasConstraints: () => hasConstraints,
94
97
  isNoLongerPublic: () => isNoLongerPublic,
95
98
  isOneToOne: () => isOneToOne,
99
+ judgeModuleResolution: () => judgeModuleResolution,
96
100
  kebabCase: () => kebabCase,
97
101
  migrationCreatedBy: () => migrationCreatedBy,
98
102
  minimumQuotasPerPlan: () => minimumQuotasPerPlan,
@@ -108,9 +112,11 @@ __export(index_exports, {
108
112
  planInit: () => planInit,
109
113
  projectKeyPattern: () => projectKeyPattern,
110
114
  quotaKeyPattern: () => quotaKeyPattern,
115
+ readEffectiveModuleResolution: () => readEffectiveModuleResolution,
111
116
  relationNameOf: () => relationNameOf,
112
117
  reportConstraints: () => reportConstraints,
113
118
  rewriteImports: () => rewriteImports,
119
+ rewriteManifest: () => rewriteManifest,
114
120
  rewriteNames: () => rewriteNames,
115
121
  rewriteSubpath: () => rewriteSubpath,
116
122
  stripLineComment: () => stripLineComment,
@@ -1247,13 +1253,43 @@ function extractModelBlocks(fragment) {
1247
1253
  return extractBlocks(fragment, "model");
1248
1254
  }
1249
1255
  __name(extractModelBlocks, "extractModelBlocks");
1256
+ function extractEnumBlocks(fragment) {
1257
+ return extractBlocks(fragment, "enum");
1258
+ }
1259
+ __name(extractEnumBlocks, "extractEnumBlocks");
1260
+ function extractEnumNames(schema2) {
1261
+ return extractBlockNames(schema2, "enum");
1262
+ }
1263
+ __name(extractEnumNames, "extractEnumNames");
1264
+ function extractFragmentBlocks(fragment) {
1265
+ return {
1266
+ enums: extractEnumBlocks(fragment),
1267
+ models: extractModelBlocks(fragment)
1268
+ };
1269
+ }
1270
+ __name(extractFragmentBlocks, "extractFragmentBlocks");
1250
1271
  function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1251
- const existing = new Set(extractModelNames(schema2));
1272
+ const blocks = fragmentBlocks instanceof Map ? {
1273
+ enums: /* @__PURE__ */ new Map(),
1274
+ models: fragmentBlocks
1275
+ } : fragmentBlocks;
1276
+ const existingModels = new Set(extractModelNames(schema2));
1277
+ const existingEnums = new Set(extractEnumNames(schema2));
1252
1278
  const added = [];
1253
1279
  const skipped = [];
1280
+ const addedEnums = [];
1281
+ const skippedEnums = [];
1254
1282
  const additions = [];
1255
- for (const [name, block] of fragmentBlocks) {
1256
- if (existing.has(name)) {
1283
+ for (const [name, block] of blocks.enums) {
1284
+ if (existingEnums.has(name)) {
1285
+ skippedEnums.push(name);
1286
+ } else {
1287
+ addedEnums.push(name);
1288
+ additions.push(block);
1289
+ }
1290
+ }
1291
+ for (const [name, block] of blocks.models) {
1292
+ if (existingModels.has(name)) {
1257
1293
  skipped.push(name);
1258
1294
  } else {
1259
1295
  added.push(name);
@@ -1264,6 +1300,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1264
1300
  return {
1265
1301
  added,
1266
1302
  skipped,
1303
+ addedEnums,
1304
+ skippedEnums,
1267
1305
  schema: schema2
1268
1306
  };
1269
1307
  }
@@ -1280,6 +1318,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1280
1318
  return {
1281
1319
  added,
1282
1320
  skipped,
1321
+ addedEnums,
1322
+ skippedEnums,
1283
1323
  schema: trimmedSchema + header + additions.join("\n\n") + "\n"
1284
1324
  };
1285
1325
  }
@@ -1834,6 +1874,38 @@ function patchOptionsFor(plan) {
1834
1874
  }
1835
1875
  __name(patchOptionsFor, "patchOptionsFor");
1836
1876
 
1877
+ // src/init/module-resolution.ts
1878
+ var import_node_path = require("path");
1879
+ var RESOLVES_SUBPATHS = /* @__PURE__ */ new Set([
1880
+ "node16",
1881
+ "nodenext",
1882
+ "bundler"
1883
+ ]);
1884
+ function judgeModuleResolution(value) {
1885
+ if (value === null || RESOLVES_SUBPATHS.has(value)) {
1886
+ return {
1887
+ ok: true,
1888
+ value
1889
+ };
1890
+ }
1891
+ return {
1892
+ ok: false,
1893
+ value,
1894
+ 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.'
1895
+ };
1896
+ }
1897
+ __name(judgeModuleResolution, "judgeModuleResolution");
1898
+ function readEffectiveModuleResolution(root, ts) {
1899
+ const configPath = (0, import_node_path.join)(root, "tsconfig.json");
1900
+ const read = ts.readConfigFile(configPath, ts.sys.readFile);
1901
+ if (read.error || read.config === void 0) return null;
1902
+ const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root);
1903
+ const kind = parsed.options.moduleResolution;
1904
+ if (kind === void 0) return null;
1905
+ return ts.ModuleResolutionKind[kind].toLowerCase();
1906
+ }
1907
+ __name(readEffectiveModuleResolution, "readEffectiveModuleResolution");
1908
+
1837
1909
  // src/codemods/v1-imports.ts
1838
1910
  var PUBLIC_PREFIXES = [
1839
1911
  "ui/",
@@ -1999,6 +2071,13 @@ function rewriteNames(text, table) {
1999
2071
  return `${quote}${to}`;
2000
2072
  });
2001
2073
  }
2074
+ for (const [from, to] of Object.entries(table.packages ?? {})) {
2075
+ if (from === "_") continue;
2076
+ next = next.replace(new RegExp(`${escape(from)}(?=['"\`/])`, "g"), () => {
2077
+ rewritten += 1;
2078
+ return to;
2079
+ });
2080
+ }
2002
2081
  for (const [from, to] of Object.entries(table.subpaths)) {
2003
2082
  next = next.replace(new RegExp(escape(from), "g"), () => {
2004
2083
  rewritten += 1;
@@ -2014,6 +2093,81 @@ function rewriteNames(text, table) {
2014
2093
  };
2015
2094
  }
2016
2095
  __name(rewriteNames, "rewriteNames");
2096
+ var DEPENDENCY_FIELDS = [
2097
+ "dependencies",
2098
+ "devDependencies",
2099
+ "peerDependencies",
2100
+ "optionalDependencies"
2101
+ ];
2102
+ var UNTRANSLATABLE_RANGE = /^(workspace:|file:|link:|npm:|git\+|https?:)/;
2103
+ function rewriteManifest(text, table, options) {
2104
+ const renames = Object.entries(table.packages ?? {}).filter(([from]) => from !== "_");
2105
+ const ambiguous = [];
2106
+ let manifest;
2107
+ try {
2108
+ manifest = JSON.parse(text);
2109
+ } catch {
2110
+ return {
2111
+ text,
2112
+ rewritten: 0,
2113
+ ambiguous
2114
+ };
2115
+ }
2116
+ let rewritten = 0;
2117
+ for (const field of DEPENDENCY_FIELDS) {
2118
+ const deps = manifest[field];
2119
+ if (!deps || typeof deps !== "object") continue;
2120
+ const entries = Object.entries(deps);
2121
+ const renamed = entries.map(([name, range]) => {
2122
+ const to = renames.find(([from]) => from === name)?.[1];
2123
+ if (!to) return [
2124
+ name,
2125
+ range
2126
+ ];
2127
+ if (UNTRANSLATABLE_RANGE.test(range)) {
2128
+ ambiguous.push(`${name} in ${field} (${range})`);
2129
+ return [
2130
+ name,
2131
+ range
2132
+ ];
2133
+ }
2134
+ rewritten += 1;
2135
+ return [
2136
+ to,
2137
+ options.targetRange
2138
+ ];
2139
+ });
2140
+ manifest[field] = Object.fromEntries(renamed);
2141
+ }
2142
+ const meta = manifest.peerDependenciesMeta;
2143
+ if (meta && typeof meta === "object") {
2144
+ manifest.peerDependenciesMeta = Object.fromEntries(Object.entries(meta).map(([name, flags]) => {
2145
+ const to = renames.find(([from]) => from === name)?.[1];
2146
+ if (!to) return [
2147
+ name,
2148
+ flags
2149
+ ];
2150
+ rewritten += 1;
2151
+ return [
2152
+ to,
2153
+ flags
2154
+ ];
2155
+ }));
2156
+ }
2157
+ if (rewritten === 0) return {
2158
+ text,
2159
+ rewritten: 0,
2160
+ ambiguous
2161
+ };
2162
+ const indent = /^[ \t]+/m.exec(text)?.[0] ?? " ";
2163
+ const trailing = text.endsWith("\n") ? "\n" : "";
2164
+ return {
2165
+ text: JSON.stringify(manifest, null, indent) + trailing,
2166
+ rewritten,
2167
+ ambiguous
2168
+ };
2169
+ }
2170
+ __name(rewriteManifest, "rewriteManifest");
2017
2171
 
2018
2172
  // src/init/patch-app-module.ts
2019
2173
  var MARKER = "SaaSiCatModule.forRoot";
@@ -2035,7 +2189,7 @@ ${block}`;
2035
2189
  return {
2036
2190
  source,
2037
2191
  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",
2192
+ 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
2193
  manualBlock
2040
2194
  };
2041
2195
  }
@@ -2123,6 +2277,13 @@ function renderForRootBlock(options) {
2123
2277
  " // your whole capability inventory \u2014 and the manifest routes to",
2124
2278
  " // anyone who asks. Import your guard and put it in.",
2125
2279
  " controller: { guards: [YourAuthGuard] },",
2280
+ " // The modules whose providers the platform injects: the one",
2281
+ " // exporting PrismaService, and the one your guard depends on.",
2282
+ " // `prismaPersistence({ client: PrismaService })` is resolved",
2283
+ " // inside the platform module, which sees only what is listed",
2284
+ " // here or declared @Global. Same rule as the guard: this does",
2285
+ " // not compile until you name them.",
2286
+ " imports: [YourPrismaModule, YourAuthModule],",
2126
2287
  options.persistenceImport ? " persistence," : " // persistence: prismaPersistence({ client: PrismaService }),",
2127
2288
  " catalog: { featureUiRegistry: " + options.registry.constName + " },",
2128
2289
  " adminResources: true,",
@@ -2769,7 +2930,7 @@ DoctorCommands = _ts_decorate12([
2769
2930
 
2770
2931
  // src/discovery.command.ts
2771
2932
  var import_node_fs = require("fs");
2772
- var import_node_path = require("path");
2933
+ var import_node_path2 = require("path");
2773
2934
  var import_common13 = require("@nestjs/common");
2774
2935
  var import_nest_commander5 = require("nest-commander");
2775
2936
  var import_nest6 = require("@saasicat/nest");
@@ -2807,8 +2968,8 @@ var DiscoveryScanCommand = class extends import_nest_commander5.CommandRunner {
2807
2968
  try {
2808
2969
  const snapshot = this.scanner.rebuildSnapshot();
2809
2970
  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), {
2971
+ const outPath = (0, import_node_path2.resolve)(flags.out);
2972
+ (0, import_node_fs.mkdirSync)((0, import_node_path2.dirname)(outPath), {
2812
2973
  recursive: true
2813
2974
  });
2814
2975
  (0, import_node_fs.writeFileSync)(outPath, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
@@ -2817,7 +2978,7 @@ var DiscoveryScanCommand = class extends import_nest_commander5.CommandRunner {
2817
2978
  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
2979
  `);
2819
2980
  if (target) {
2820
- process.stdout.write(`Snapshot persisted: ${(0, import_node_path.resolve)(target)}
2981
+ process.stdout.write(`Snapshot persisted: ${(0, import_node_path2.resolve)(target)}
2821
2982
  `);
2822
2983
  } else {
2823
2984
  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");
@@ -3246,6 +3407,9 @@ UserCommands = _ts_decorate14([
3246
3407
  enableFkPointers,
3247
3408
  extractBlockNames,
3248
3409
  extractBlocks,
3410
+ extractEnumBlocks,
3411
+ extractEnumNames,
3412
+ extractFragmentBlocks,
3249
3413
  extractModelBlocks,
3250
3414
  extractModelNames,
3251
3415
  findFkPointers,
@@ -3254,6 +3418,7 @@ UserCommands = _ts_decorate14([
3254
3418
  hasConstraints,
3255
3419
  isNoLongerPublic,
3256
3420
  isOneToOne,
3421
+ judgeModuleResolution,
3257
3422
  kebabCase,
3258
3423
  migrationCreatedBy,
3259
3424
  minimumQuotasPerPlan,
@@ -3269,9 +3434,11 @@ UserCommands = _ts_decorate14([
3269
3434
  planInit,
3270
3435
  projectKeyPattern,
3271
3436
  quotaKeyPattern,
3437
+ readEffectiveModuleResolution,
3272
3438
  relationNameOf,
3273
3439
  reportConstraints,
3274
3440
  rewriteImports,
3441
+ rewriteManifest,
3275
3442
  rewriteNames,
3276
3443
  rewriteSubpath,
3277
3444
  stripLineComment,
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { MfaService, AdminAuditService, AdminManifestService, DiscoverySnapshot, ProviderSpec, DiscoveryScanner } from '@saasicat/nest';
2
- import { UserPort, PlatformUserDto, AuditQueryPort, AuditEntry, AdminManifest, ManifestAccessPort, PlanCatalog, UserManagementPort } from '@saasicat/types';
2
+ import { UserPort, PlatformUserDto, AuditQueryPort, AuditEntry, AdminManifest, ManifestAccessPort, PlanCatalog, UserManagementPort } from '@saasicat/core';
3
3
  import { Type, DynamicModule } from '@nestjs/common';
4
+ import * as TypeScript from 'typescript';
4
5
  import { CommandRunner } from 'nest-commander';
5
6
 
6
7
  declare const CLI_CONTEXT_CONFIG_TOKEN: unique symbol;
@@ -381,19 +382,41 @@ declare function extractModelNames(schema: string): string[];
381
382
  * `name -> complete block text incl. opening/closing braces`.
382
383
  */
383
384
  declare function extractModelBlocks(fragment: string): Map<string, string>;
385
+ /** Returns all `enum X { ... }` blocks from a fragment, keyed by name. */
386
+ declare function extractEnumBlocks(fragment: string): Map<string, string>;
387
+ /** Names of all top-level `enum X { ... }` blocks in the schema. */
388
+ declare function extractEnumNames(schema: string): string[];
389
+ /**
390
+ * What a fragment contributes: its enums and its models. Both travel
391
+ * together, because a model's `BillingCycle` field is a validation error in
392
+ * a schema that has the model and not the enum — which is what `apply` used
393
+ * to produce for every consumer whose schema did not already carry the enums
394
+ * by hand, and what the example app masked by carrying them.
395
+ */
396
+ interface FragmentBlocks {
397
+ enums: Map<string, string>;
398
+ models: Map<string, string>;
399
+ }
400
+ declare function extractFragmentBlocks(fragment: string): FragmentBlocks;
384
401
  interface ApplyResult {
385
- /** Number of models that were added. */
402
+ /** Models that were added. */
386
403
  added: string[];
387
- /** Number of models that were already present (no write). */
404
+ /** Models that were already present (no write). */
388
405
  skipped: string[];
389
- /** Resulting schema text. When `added.length === 0` identical to the input. */
406
+ /** Enums that were added, before the models that use them. */
407
+ addedEnums: string[];
408
+ /** Enums that were already present (no write). */
409
+ skippedEnums: string[];
410
+ /** Resulting schema text. When nothing was added, identical to the input. */
390
411
  schema: string;
391
412
  }
392
413
  /**
393
- * Appends missing models from `fragmentBlocks` to the end of `schema`. Existing
394
- * models (same name) stay unchanged and are listed in `skipped`.
414
+ * Appends missing enums and models from `fragmentBlocks` to the end of
415
+ * `schema`. Existing blocks (same name) stay unchanged and are listed as
416
+ * skipped. A plain map of models is accepted for the callers that only ever
417
+ * had models; the enums are then `[]`, which is what they were before.
395
418
  */
396
- declare function applyFragmentBlocks(schema: string, fragmentBlocks: Map<string, string>, options?: {
419
+ declare function applyFragmentBlocks(schema: string, fragmentBlocks: FragmentBlocks | Map<string, string>, options?: {
397
420
  fragmentLabel?: string;
398
421
  }): ApplyResult;
399
422
 
@@ -804,6 +827,33 @@ interface PatchOptionsFromPlan {
804
827
  };
805
828
  }
806
829
 
830
+ interface ModuleResolutionVerdict {
831
+ readonly ok: boolean;
832
+ /** The effective setting, lower-cased as TypeScript names it, or null when unset. */
833
+ readonly value: string | null;
834
+ readonly reason?: string;
835
+ }
836
+ /**
837
+ * Judges an effective `moduleResolution`, as `readEffectiveModuleResolution`
838
+ * reports it.
839
+ *
840
+ * Unset is `ok`: TypeScript then derives it from `module`, and a `module`
841
+ * that implies the old resolution is a setup this cannot see from here —
842
+ * the next build says so, with TypeScript's own message.
843
+ */
844
+ declare function judgeModuleResolution(value: string | null): ModuleResolutionVerdict;
845
+ /**
846
+ * The `moduleResolution` a project's `tsconfig.json` resolves to, after
847
+ * `extends`, lower-cased as TypeScript names the kind (`node10`, `node16`,
848
+ * `nodenext`, `bundler`, `classic`) — or null when there is no config, the
849
+ * config cannot be parsed, or the option is not set anywhere in the chain.
850
+ *
851
+ * `ts` is whichever TypeScript will compile the project: the consumer's own
852
+ * when it can be resolved from the project root, so the reading matches the
853
+ * build that follows.
854
+ */
855
+ declare function readEffectiveModuleResolution(root: string, ts: typeof TypeScript): string | null;
856
+
807
857
  /** The pattern `plan-catalog.schema.json` puts on `projectKey`. */
808
858
  declare function projectKeyPattern(): RegExp;
809
859
  /** The pattern it puts on the keys inside a plan's `quotas` object. */
@@ -878,6 +928,13 @@ interface RenameTable {
878
928
  readonly entryTokens: Readonly<Record<string, Readonly<Record<string, string>>>>;
879
929
  /** A module specifier prefix and its replacement. */
880
930
  readonly subpaths: Readonly<Record<string, string>>;
931
+ /**
932
+ * A package that was renamed. Rewritten in specifiers by `rewriteNames`
933
+ * and in `package.json` dependency fields by `rewriteManifest` — both,
934
+ * because an import a manifest does not declare fails to resolve under
935
+ * pnpm's isolated `node_modules`.
936
+ */
937
+ readonly packages?: Readonly<Record<string, string>>;
881
938
  }
882
939
  interface RenameResult {
883
940
  readonly text: string;
@@ -903,6 +960,26 @@ declare function namedImports(text: string): Array<{
903
960
  }>;
904
961
  /** Applies the table to one file's text. Idempotent: a second run changes nothing. */
905
962
  declare function rewriteNames(text: string, table: RenameTable): RenameResult;
963
+ /**
964
+ * Applies the package renames to a `package.json` text.
965
+ *
966
+ * Parsed and re-serialised rather than string-replaced, so a rename lands in a
967
+ * dependency field and nowhere else — not in `name`, not in a description.
968
+ * The file's indentation is kept; a consumer's formatter must not see a diff
969
+ * it did not cause. Returns the text unchanged when nothing applied.
970
+ */
971
+ interface ManifestRewriteOptions {
972
+ /**
973
+ * The range the renamed dependency gets — `^<the version this CLI was
974
+ * released as>`. The old range cannot be carried over: a 0.x consumer
975
+ * declares `"@saasicat/types": "^0.27.0"`, and `@saasicat/core` has no
976
+ * 0.27 — the rename starts on the 1.0 line. The caller passes the CLI's
977
+ * own version because that IS the line the consumer is migrating to;
978
+ * the codemod ships with it.
979
+ */
980
+ readonly targetRange: string;
981
+ }
982
+ declare function rewriteManifest(text: string, table: RenameTable, options: ManifestRewriteOptions): RenameResult;
906
983
 
907
984
  interface PatchAppModuleOptions {
908
985
  /** Import specifier for the persistence bundle, or null when not generated. */
@@ -1147,4 +1224,4 @@ declare class UserCommands extends CommandRunner {
1147
1224
  parsePassword(val: string): string;
1148
1225
  }
1149
1226
 
1150
- export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, 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 MissingBlockAttribute, type MissingEnumValue, type MissingField, type MoveTable, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidProjectKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, enableFkPointers, extractBlockNames, extractBlocks, extractModelBlocks, extractModelNames, findFkPointers, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, projectKeyPattern, quotaKeyPattern, relationNameOf, reportConstraints, rewriteImports, rewriteNames, rewriteSubpath, stripLineComment, structuralOnly, tablesAddressedBy };
1227
+ export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidProjectKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findFkPointers, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, projectKeyPattern, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, stripLineComment, structuralOnly, tablesAddressedBy };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { MfaService, AdminAuditService, AdminManifestService, DiscoverySnapshot, ProviderSpec, DiscoveryScanner } from '@saasicat/nest';
2
- import { UserPort, PlatformUserDto, AuditQueryPort, AuditEntry, AdminManifest, ManifestAccessPort, PlanCatalog, UserManagementPort } from '@saasicat/types';
2
+ import { UserPort, PlatformUserDto, AuditQueryPort, AuditEntry, AdminManifest, ManifestAccessPort, PlanCatalog, UserManagementPort } from '@saasicat/core';
3
3
  import { Type, DynamicModule } from '@nestjs/common';
4
+ import * as TypeScript from 'typescript';
4
5
  import { CommandRunner } from 'nest-commander';
5
6
 
6
7
  declare const CLI_CONTEXT_CONFIG_TOKEN: unique symbol;
@@ -381,19 +382,41 @@ declare function extractModelNames(schema: string): string[];
381
382
  * `name -> complete block text incl. opening/closing braces`.
382
383
  */
383
384
  declare function extractModelBlocks(fragment: string): Map<string, string>;
385
+ /** Returns all `enum X { ... }` blocks from a fragment, keyed by name. */
386
+ declare function extractEnumBlocks(fragment: string): Map<string, string>;
387
+ /** Names of all top-level `enum X { ... }` blocks in the schema. */
388
+ declare function extractEnumNames(schema: string): string[];
389
+ /**
390
+ * What a fragment contributes: its enums and its models. Both travel
391
+ * together, because a model's `BillingCycle` field is a validation error in
392
+ * a schema that has the model and not the enum — which is what `apply` used
393
+ * to produce for every consumer whose schema did not already carry the enums
394
+ * by hand, and what the example app masked by carrying them.
395
+ */
396
+ interface FragmentBlocks {
397
+ enums: Map<string, string>;
398
+ models: Map<string, string>;
399
+ }
400
+ declare function extractFragmentBlocks(fragment: string): FragmentBlocks;
384
401
  interface ApplyResult {
385
- /** Number of models that were added. */
402
+ /** Models that were added. */
386
403
  added: string[];
387
- /** Number of models that were already present (no write). */
404
+ /** Models that were already present (no write). */
388
405
  skipped: string[];
389
- /** Resulting schema text. When `added.length === 0` identical to the input. */
406
+ /** Enums that were added, before the models that use them. */
407
+ addedEnums: string[];
408
+ /** Enums that were already present (no write). */
409
+ skippedEnums: string[];
410
+ /** Resulting schema text. When nothing was added, identical to the input. */
390
411
  schema: string;
391
412
  }
392
413
  /**
393
- * Appends missing models from `fragmentBlocks` to the end of `schema`. Existing
394
- * models (same name) stay unchanged and are listed in `skipped`.
414
+ * Appends missing enums and models from `fragmentBlocks` to the end of
415
+ * `schema`. Existing blocks (same name) stay unchanged and are listed as
416
+ * skipped. A plain map of models is accepted for the callers that only ever
417
+ * had models; the enums are then `[]`, which is what they were before.
395
418
  */
396
- declare function applyFragmentBlocks(schema: string, fragmentBlocks: Map<string, string>, options?: {
419
+ declare function applyFragmentBlocks(schema: string, fragmentBlocks: FragmentBlocks | Map<string, string>, options?: {
397
420
  fragmentLabel?: string;
398
421
  }): ApplyResult;
399
422
 
@@ -804,6 +827,33 @@ interface PatchOptionsFromPlan {
804
827
  };
805
828
  }
806
829
 
830
+ interface ModuleResolutionVerdict {
831
+ readonly ok: boolean;
832
+ /** The effective setting, lower-cased as TypeScript names it, or null when unset. */
833
+ readonly value: string | null;
834
+ readonly reason?: string;
835
+ }
836
+ /**
837
+ * Judges an effective `moduleResolution`, as `readEffectiveModuleResolution`
838
+ * reports it.
839
+ *
840
+ * Unset is `ok`: TypeScript then derives it from `module`, and a `module`
841
+ * that implies the old resolution is a setup this cannot see from here —
842
+ * the next build says so, with TypeScript's own message.
843
+ */
844
+ declare function judgeModuleResolution(value: string | null): ModuleResolutionVerdict;
845
+ /**
846
+ * The `moduleResolution` a project's `tsconfig.json` resolves to, after
847
+ * `extends`, lower-cased as TypeScript names the kind (`node10`, `node16`,
848
+ * `nodenext`, `bundler`, `classic`) — or null when there is no config, the
849
+ * config cannot be parsed, or the option is not set anywhere in the chain.
850
+ *
851
+ * `ts` is whichever TypeScript will compile the project: the consumer's own
852
+ * when it can be resolved from the project root, so the reading matches the
853
+ * build that follows.
854
+ */
855
+ declare function readEffectiveModuleResolution(root: string, ts: typeof TypeScript): string | null;
856
+
807
857
  /** The pattern `plan-catalog.schema.json` puts on `projectKey`. */
808
858
  declare function projectKeyPattern(): RegExp;
809
859
  /** The pattern it puts on the keys inside a plan's `quotas` object. */
@@ -878,6 +928,13 @@ interface RenameTable {
878
928
  readonly entryTokens: Readonly<Record<string, Readonly<Record<string, string>>>>;
879
929
  /** A module specifier prefix and its replacement. */
880
930
  readonly subpaths: Readonly<Record<string, string>>;
931
+ /**
932
+ * A package that was renamed. Rewritten in specifiers by `rewriteNames`
933
+ * and in `package.json` dependency fields by `rewriteManifest` — both,
934
+ * because an import a manifest does not declare fails to resolve under
935
+ * pnpm's isolated `node_modules`.
936
+ */
937
+ readonly packages?: Readonly<Record<string, string>>;
881
938
  }
882
939
  interface RenameResult {
883
940
  readonly text: string;
@@ -903,6 +960,26 @@ declare function namedImports(text: string): Array<{
903
960
  }>;
904
961
  /** Applies the table to one file's text. Idempotent: a second run changes nothing. */
905
962
  declare function rewriteNames(text: string, table: RenameTable): RenameResult;
963
+ /**
964
+ * Applies the package renames to a `package.json` text.
965
+ *
966
+ * Parsed and re-serialised rather than string-replaced, so a rename lands in a
967
+ * dependency field and nowhere else — not in `name`, not in a description.
968
+ * The file's indentation is kept; a consumer's formatter must not see a diff
969
+ * it did not cause. Returns the text unchanged when nothing applied.
970
+ */
971
+ interface ManifestRewriteOptions {
972
+ /**
973
+ * The range the renamed dependency gets — `^<the version this CLI was
974
+ * released as>`. The old range cannot be carried over: a 0.x consumer
975
+ * declares `"@saasicat/types": "^0.27.0"`, and `@saasicat/core` has no
976
+ * 0.27 — the rename starts on the 1.0 line. The caller passes the CLI's
977
+ * own version because that IS the line the consumer is migrating to;
978
+ * the codemod ships with it.
979
+ */
980
+ readonly targetRange: string;
981
+ }
982
+ declare function rewriteManifest(text: string, table: RenameTable, options: ManifestRewriteOptions): RenameResult;
906
983
 
907
984
  interface PatchAppModuleOptions {
908
985
  /** Import specifier for the persistence bundle, or null when not generated. */
@@ -1147,4 +1224,4 @@ declare class UserCommands extends CommandRunner {
1147
1224
  parsePassword(val: string): string;
1148
1225
  }
1149
1226
 
1150
- export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, 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 MissingBlockAttribute, type MissingEnumValue, type MissingField, type MoveTable, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidProjectKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, enableFkPointers, extractBlockNames, extractBlocks, extractModelBlocks, extractModelNames, findFkPointers, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, projectKeyPattern, quotaKeyPattern, relationNameOf, reportConstraints, rewriteImports, rewriteNames, rewriteSubpath, stripLineComment, structuralOnly, tablesAddressedBy };
1227
+ export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidProjectKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findFkPointers, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, projectKeyPattern, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, stripLineComment, structuralOnly, tablesAddressedBy };
package/dist/index.js CHANGED
@@ -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
  }
@@ -1716,6 +1750,38 @@ function patchOptionsFor(plan) {
1716
1750
  }
1717
1751
  __name(patchOptionsFor, "patchOptionsFor");
1718
1752
 
1753
+ // src/init/module-resolution.ts
1754
+ import { join } from "path";
1755
+ var RESOLVES_SUBPATHS = /* @__PURE__ */ new Set([
1756
+ "node16",
1757
+ "nodenext",
1758
+ "bundler"
1759
+ ]);
1760
+ function judgeModuleResolution(value) {
1761
+ if (value === null || RESOLVES_SUBPATHS.has(value)) {
1762
+ return {
1763
+ ok: true,
1764
+ value
1765
+ };
1766
+ }
1767
+ return {
1768
+ ok: false,
1769
+ value,
1770
+ 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.'
1771
+ };
1772
+ }
1773
+ __name(judgeModuleResolution, "judgeModuleResolution");
1774
+ function readEffectiveModuleResolution(root, ts) {
1775
+ const configPath = join(root, "tsconfig.json");
1776
+ const read = ts.readConfigFile(configPath, ts.sys.readFile);
1777
+ if (read.error || read.config === void 0) return null;
1778
+ const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root);
1779
+ const kind = parsed.options.moduleResolution;
1780
+ if (kind === void 0) return null;
1781
+ return ts.ModuleResolutionKind[kind].toLowerCase();
1782
+ }
1783
+ __name(readEffectiveModuleResolution, "readEffectiveModuleResolution");
1784
+
1719
1785
  // src/codemods/v1-imports.ts
1720
1786
  var PUBLIC_PREFIXES = [
1721
1787
  "ui/",
@@ -1881,6 +1947,13 @@ function rewriteNames(text, table) {
1881
1947
  return `${quote}${to}`;
1882
1948
  });
1883
1949
  }
1950
+ for (const [from, to] of Object.entries(table.packages ?? {})) {
1951
+ if (from === "_") continue;
1952
+ next = next.replace(new RegExp(`${escape(from)}(?=['"\`/])`, "g"), () => {
1953
+ rewritten += 1;
1954
+ return to;
1955
+ });
1956
+ }
1884
1957
  for (const [from, to] of Object.entries(table.subpaths)) {
1885
1958
  next = next.replace(new RegExp(escape(from), "g"), () => {
1886
1959
  rewritten += 1;
@@ -1896,6 +1969,81 @@ function rewriteNames(text, table) {
1896
1969
  };
1897
1970
  }
1898
1971
  __name(rewriteNames, "rewriteNames");
1972
+ var DEPENDENCY_FIELDS = [
1973
+ "dependencies",
1974
+ "devDependencies",
1975
+ "peerDependencies",
1976
+ "optionalDependencies"
1977
+ ];
1978
+ var UNTRANSLATABLE_RANGE = /^(workspace:|file:|link:|npm:|git\+|https?:)/;
1979
+ function rewriteManifest(text, table, options) {
1980
+ const renames = Object.entries(table.packages ?? {}).filter(([from]) => from !== "_");
1981
+ const ambiguous = [];
1982
+ let manifest;
1983
+ try {
1984
+ manifest = JSON.parse(text);
1985
+ } catch {
1986
+ return {
1987
+ text,
1988
+ rewritten: 0,
1989
+ ambiguous
1990
+ };
1991
+ }
1992
+ let rewritten = 0;
1993
+ for (const field of DEPENDENCY_FIELDS) {
1994
+ const deps = manifest[field];
1995
+ if (!deps || typeof deps !== "object") continue;
1996
+ const entries = Object.entries(deps);
1997
+ const renamed = entries.map(([name, range]) => {
1998
+ const to = renames.find(([from]) => from === name)?.[1];
1999
+ if (!to) return [
2000
+ name,
2001
+ range
2002
+ ];
2003
+ if (UNTRANSLATABLE_RANGE.test(range)) {
2004
+ ambiguous.push(`${name} in ${field} (${range})`);
2005
+ return [
2006
+ name,
2007
+ range
2008
+ ];
2009
+ }
2010
+ rewritten += 1;
2011
+ return [
2012
+ to,
2013
+ options.targetRange
2014
+ ];
2015
+ });
2016
+ manifest[field] = Object.fromEntries(renamed);
2017
+ }
2018
+ const meta = manifest.peerDependenciesMeta;
2019
+ if (meta && typeof meta === "object") {
2020
+ manifest.peerDependenciesMeta = Object.fromEntries(Object.entries(meta).map(([name, flags]) => {
2021
+ const to = renames.find(([from]) => from === name)?.[1];
2022
+ if (!to) return [
2023
+ name,
2024
+ flags
2025
+ ];
2026
+ rewritten += 1;
2027
+ return [
2028
+ to,
2029
+ flags
2030
+ ];
2031
+ }));
2032
+ }
2033
+ if (rewritten === 0) return {
2034
+ text,
2035
+ rewritten: 0,
2036
+ ambiguous
2037
+ };
2038
+ const indent = /^[ \t]+/m.exec(text)?.[0] ?? " ";
2039
+ const trailing = text.endsWith("\n") ? "\n" : "";
2040
+ return {
2041
+ text: JSON.stringify(manifest, null, indent) + trailing,
2042
+ rewritten,
2043
+ ambiguous
2044
+ };
2045
+ }
2046
+ __name(rewriteManifest, "rewriteManifest");
1899
2047
 
1900
2048
  // src/init/patch-app-module.ts
1901
2049
  var MARKER = "SaaSiCatModule.forRoot";
@@ -1917,7 +2065,7 @@ ${block}`;
1917
2065
  return {
1918
2066
  source,
1919
2067
  status: "declined",
1920
- reason: "no `@Module({ imports: [ ... ] })` was found in this file, so there is nowhere to add the platform without guessing at the structure",
2068
+ 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",
1921
2069
  manualBlock
1922
2070
  };
1923
2071
  }
@@ -2005,6 +2153,13 @@ function renderForRootBlock(options) {
2005
2153
  " // your whole capability inventory \u2014 and the manifest routes to",
2006
2154
  " // anyone who asks. Import your guard and put it in.",
2007
2155
  " controller: { guards: [YourAuthGuard] },",
2156
+ " // The modules whose providers the platform injects: the one",
2157
+ " // exporting PrismaService, and the one your guard depends on.",
2158
+ " // `prismaPersistence({ client: PrismaService })` is resolved",
2159
+ " // inside the platform module, which sees only what is listed",
2160
+ " // here or declared @Global. Same rule as the guard: this does",
2161
+ " // not compile until you name them.",
2162
+ " imports: [YourPrismaModule, YourAuthModule],",
2008
2163
  options.persistenceImport ? " persistence," : " // persistence: prismaPersistence({ client: PrismaService }),",
2009
2164
  " catalog: { featureUiRegistry: " + options.registry.constName + " },",
2010
2165
  " adminResources: true,",
@@ -3127,6 +3282,9 @@ export {
3127
3282
  enableFkPointers,
3128
3283
  extractBlockNames,
3129
3284
  extractBlocks,
3285
+ extractEnumBlocks,
3286
+ extractEnumNames,
3287
+ extractFragmentBlocks,
3130
3288
  extractModelBlocks,
3131
3289
  extractModelNames,
3132
3290
  findFkPointers,
@@ -3135,6 +3293,7 @@ export {
3135
3293
  hasConstraints,
3136
3294
  isNoLongerPublic,
3137
3295
  isOneToOne,
3296
+ judgeModuleResolution,
3138
3297
  kebabCase,
3139
3298
  migrationCreatedBy,
3140
3299
  minimumQuotasPerPlan,
@@ -3150,9 +3309,11 @@ export {
3150
3309
  planInit,
3151
3310
  projectKeyPattern,
3152
3311
  quotaKeyPattern,
3312
+ readEffectiveModuleResolution,
3153
3313
  relationNameOf,
3154
3314
  reportConstraints,
3155
3315
  rewriteImports,
3316
+ rewriteManifest,
3156
3317
  rewriteNames,
3157
3318
  rewriteSubpath,
3158
3319
  stripLineComment,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/cli",
3
- "version": "1.0.0-rc.0",
3
+ "version": "1.0.0-rc.2",
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.0",
33
- "@saasicat/spec": "^1.0.0-rc.0",
34
- "@saasicat/types": "^1.0.0-rc.0"
33
+ "@saasicat/nest": "^1.0.0-rc.2",
34
+ "@saasicat/spec": "^1.0.0-rc.2",
35
+ "@saasicat/core": "^1.0.0-rc.2"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "@nestjs/common": "^11.0.0",
@@ -59,7 +60,7 @@
59
60
  "access": "public"
60
61
  },
61
62
  "scripts": {
62
- "build": "node ../../scripts/build-and-prune.mjs \"tsup src/index.ts --format esm,cjs --dts --external @saasicat/types --external @saasicat/nest --external @nestjs/common --external nest-commander --external qrcode-terminal\"",
63
+ "build": "node ../../scripts/build-and-prune.mjs \"tsup src/index.ts --format esm,cjs --dts --external @saasicat/core --external @saasicat/nest --external @nestjs/common --external nest-commander --external qrcode-terminal\"",
63
64
  "pretest": "pnpm run build",
64
65
  "test": "node --test 'tests/*.test.js'"
65
66
  }
@@ -1,6 +1,6 @@
1
1
  import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
2
2
  import { Injectable } from '@nestjs/common';
3
- import type { PasswordHasher } from '@saasicat/types';
3
+ import type { PasswordHasher } from '@saasicat/core';
4
4
 
5
5
  const KEY_LENGTH = 64;
6
6
 
@@ -1,4 +1,4 @@
1
- import type { ManifestContribution } from '@saasicat/types';
1
+ import type { ManifestContribution } from '@saasicat/core';
2
2
 
3
3
  /**
4
4
  * What this app adds to the SuperAdmin UI.
@@ -1,4 +1,4 @@
1
- import type { FeatureUiRegistry } from '@saasicat/types';
1
+ import type { FeatureUiRegistry } from '@saasicat/core';
2
2
 
3
3
  /**
4
4
  * Labels and icons for the feature and quota keys discovery finds in your code.
@@ -1,6 +1,6 @@
1
1
  import { Injectable } from '@nestjs/common';
2
2
  import { DefinesQuota } from '@saasicat/nest/discovery';
3
- import type { QuotaProvider } from '@saasicat/types';
3
+ import type { QuotaProvider } from '@saasicat/core';
4
4
 
5
5
  import { PrismaService } from '../prisma/prisma.service';
6
6