@saasicat/cli 1.0.0-rc.5 → 1.0.0-rc.7

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/README.md CHANGED
@@ -43,7 +43,7 @@ import { PrismaUserPortAdapter } from './adapters/prisma-user-port';
43
43
  imports: [
44
44
  PrismaModule,
45
45
  PlanCatalogModule.forRoot({
46
- projectKey: 'myapp',
46
+ app: { name: 'MyApp' },
47
47
  currency: 'EUR',
48
48
  vatRate: 19,
49
49
  // The catalogue is read from the database, not from a file — the
package/bin/saasicat.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // `saasicat` — bootstrap CLI for the SaaSiCat framework.
3
3
  // naming-history: the codemod help below names the pre-1.0 spellings it rewrites.
4
+ // project-key-history: `codemod v1-project-key` is named after what it removes.
4
5
  //
5
6
  // Sub-commands:
6
7
  // schema apply [--prisma-schema=PATH] [--fragments=01,02,03]
@@ -16,10 +17,11 @@
16
17
  // apply --all, then `prisma migrate dev`, then append the constraints
17
18
  // Prisma's DSL cannot express to the migration it just wrote.
18
19
  //
19
- // init --project-key=X --quota=key:Model [--quota=…]... [--app-name=X] [--api-base=X]
20
+ // init --app-key=X --quota=key:Model [--quota=…]... [--app-name=X] [--api-base=X]
20
21
  // codemod v1-imports [--dir=X] [--dry-run]
21
22
  // codemod v1-rename [--dir=X] [--dry-run]
22
- // codemod v1 [--dir=X] [--dry-run] — both, in that order
23
+ // codemod v1-project-key [--dir=X] [--dry-run]
24
+ // codemod v1 [--dir=X] [--dry-run] — all three, in that order
23
25
  // [--skip-hasher] [--dry-run] [--dir=.]
24
26
  // Writes the platform wiring — config, persistence, manifest
25
27
  // contribution, admin module, one provider per quota — and adds
@@ -27,7 +29,7 @@
27
29
 
28
30
  import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
29
31
  import { existsSync } from 'node:fs';
30
- import { basename, dirname, join, resolve } from 'node:path';
32
+ import { basename, dirname, join, relative, resolve } from 'node:path';
31
33
  import { createRequire } from 'node:module';
32
34
  import { fileURLToPath } from 'node:url';
33
35
  import { spawn } from 'node:child_process';
@@ -46,7 +48,7 @@ import {
46
48
  extractFragmentBlocks,
47
49
  findFkPointers,
48
50
  hasConstraints,
49
- assertValidProjectKey,
51
+ assertValidAppKey,
50
52
  judgeModuleResolution,
51
53
  readEffectiveModuleResolution,
52
54
  buildImportMap,
@@ -54,6 +56,7 @@ import {
54
56
  rewriteImports,
55
57
  rewriteManifest,
56
58
  rewriteNames,
59
+ removeProjectKey,
57
60
  reportConstraints,
58
61
  patchAppModule,
59
62
  patchOptionsFor,
@@ -71,7 +74,7 @@ const require_ = createRequire(import.meta.url);
71
74
  // `pascalCase` and came back as `value.replace is not a function` with exit 99
72
75
  // — an internal error for what is an ordinary typo.
73
76
  const VALUE_FLAGS = new Set([
74
- 'project-key',
77
+ 'app-key',
75
78
  'app-name',
76
79
  'api-base',
77
80
  'quota',
@@ -678,11 +681,32 @@ function codemodTable(name) {
678
681
  const CODEMOD_SKIP = new Set(['node_modules', '.git', '.output', 'coverage']);
679
682
  const isBuildOutput = (name) => name === 'dist' || name.startsWith('dist-');
680
683
  const CODEMOD_EXTENSIONS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue|md)$/;
684
+ /**
685
+ * Widens the walk for `v1-project-key` alone — see `walkSources`.
686
+ *
687
+ * `.prisma` belongs here for the reason `.yaml` does: a consumer copies the
688
+ * platform's models into their own `schema.prisma`, which is the documented
689
+ * integration path, so that file carries `projectKey` fields and composite
690
+ * indexes of its own. Left out of the walk they were neither rewritten nor
691
+ * reported — and after the SQL migration drops the columns, a schema still
692
+ * declaring them generates a client that queries them, while the next
693
+ * `db push` tries to put them back.
694
+ *
695
+ * Reported rather than rewritten, like every other declaration: a `.prisma`
696
+ * model is a schema, and which of its fields a consumer still needs is theirs.
697
+ */
698
+ const CODEMOD_CONFIG_EXTENSIONS = /\.(yaml|yml|prisma)$/;
681
699
  /** Walked for the package renames alone; see `rewriteManifest`. */
682
700
  const CODEMOD_MANIFEST = 'package.json';
683
701
 
684
- /** Every source file under `root` a codemod may touch, with its text. */
685
- async function walkSources(root, visit) {
702
+ /**
703
+ * Every source file under `root` a codemod may touch, with its text.
704
+ *
705
+ * `extra` widens the set for one codemod. Only `v1-project-key` passes it, and
706
+ * only for `.yaml`: the other two rewrite identifiers and import specifiers,
707
+ * and letting them loose on a configuration file would corrupt it.
708
+ */
709
+ async function walkSources(root, visit, extra = null) {
686
710
  const walk = async (dir) => {
687
711
  for (const entry of await readdir(dir, { withFileTypes: true })) {
688
712
  if (CODEMOD_SKIP.has(entry.name) || isBuildOutput(entry.name)) continue;
@@ -691,13 +715,70 @@ async function walkSources(root, visit) {
691
715
  await walk(full);
692
716
  continue;
693
717
  }
694
- if (!CODEMOD_EXTENSIONS.test(entry.name) && entry.name !== CODEMOD_MANIFEST) continue;
718
+ const included =
719
+ CODEMOD_EXTENSIONS.test(entry.name) ||
720
+ entry.name === CODEMOD_MANIFEST ||
721
+ (extra !== null && extra.test(entry.name));
722
+ if (!included) continue;
695
723
  await visit(full, await readFile(full, 'utf8'));
696
724
  }
697
725
  };
698
726
  await walk(root);
699
727
  }
700
728
 
729
+ /**
730
+ * Takes `projectKey` out of a consumer's code, and names what it will not.
731
+ *
732
+ * A removal is not a rename: the word is an ordinary property name. Two forms
733
+ * need no grammar to decide — a `?projectKey=` on a `/catalog/` URL, and the
734
+ * top-level key of a `saas.yaml` — and those are rewritten. An object member is
735
+ * reported: an object literal and a type literal are lexically identical in
736
+ * TypeScript, so removing one would sometimes delete a member of the consumer's
737
+ * own type. The migration guide's table says what each shape becomes.
738
+ */
739
+ async function cmdCodemodV1ProjectKey(args) {
740
+ const root = resolve(args.dir ?? '.');
741
+ const dryRun = args['dry-run'] === true;
742
+
743
+ const undecided = [];
744
+ let rewritten = 0;
745
+ let touched = 0;
746
+ await walkSources(
747
+ root,
748
+ async (full, source) => {
749
+ if (basename(full) === CODEMOD_MANIFEST) return;
750
+ const result = removeProjectKey(source, isCatalogConfig(full) ? 'yaml' : 'source');
751
+ for (const line of result.undecided) undecided.push(`${relative(root, full)}:${line}`);
752
+ if (result.rewritten === 0) return;
753
+ if (!dryRun) await writeFile(full, result.text);
754
+ rewritten += result.rewritten;
755
+ touched += 1;
756
+ },
757
+ CODEMOD_CONFIG_EXTENSIONS,
758
+ );
759
+
760
+ console.log(
761
+ `${dryRun ? 'Would remove' : 'Removed'} ${rewritten} occurrence(s) in ${touched} file(s).`,
762
+ );
763
+ if (undecided.length === 0) return;
764
+
765
+ console.log('');
766
+ console.log(`${undecided.length} occurrence(s) are yours to look at:`);
767
+ for (const where of undecided) console.log(` ${where}`);
768
+ console.log('');
769
+ console.log(' Two kinds of file end up here. In TypeScript an object literal and a type');
770
+ console.log(' literal are the same tokens, so this cannot tell a payload member from one');
771
+ console.log(' of your own declarations without parsing. And a `.prisma` model is a schema:');
772
+ console.log(" which of its fields you still need is yours to say, not this tool's.");
773
+ console.log(' It reports rather than guesses — docs/guides/upgrade-to-1.0.md has a table');
774
+ console.log(' of what each shape becomes.');
775
+ }
776
+
777
+ /** A `saas.yaml`, whose top-level `projectKey:` needs no anchor. */
778
+ function isCatalogConfig(full) {
779
+ return basename(full) === 'saas.yaml' || basename(full) === 'saas.yml';
780
+ }
781
+
701
782
  /** Reads a repeated flag (`--quota=a --quota=b`) off argv. */
702
783
  function repeatedFlag(argv, name) {
703
784
  return argv
@@ -706,17 +787,17 @@ function repeatedFlag(argv, name) {
706
787
  }
707
788
 
708
789
  async function cmdInit(args, argv) {
709
- if (!args['project-key']) {
710
- console.error('✗ --project-key=<key> is required.');
711
- console.error(' It names the catalogue this app administers.');
790
+ if (!args['app-key']) {
791
+ console.error('✗ --app-key=<key> is required.');
792
+ console.error(' It is the slug of this application: npm package name, storage prefix.');
712
793
  process.exit(1);
713
794
  }
714
795
 
715
796
  // A usage error, so it exits 1 like every other one here rather than
716
- // through the top-level handler's 99. The rule itself comes from the
717
- // catalogue schema — see src/init/project-key.ts.
797
+ // through the top-level handler's 99. The rule itself is in
798
+ // src/init/catalog-keys.ts.
718
799
  try {
719
- assertValidProjectKey(args['project-key']);
800
+ assertValidAppKey(args['app-key']);
720
801
  } catch (err) {
721
802
  console.error(`✗ ${err.message}`);
722
803
  process.exit(1);
@@ -741,7 +822,7 @@ async function cmdInit(args, argv) {
741
822
  }
742
823
 
743
824
  const plan = planInit({
744
- projectKey: args['project-key'],
825
+ appKey: args['app-key'],
745
826
  appName: args['app-name'],
746
827
  apiBase: args['api-base'],
747
828
  quotas: repeatedFlag(argv, 'quota'),
@@ -881,11 +962,15 @@ async function main() {
881
962
  if (cmd === 'codemod' && sub === 'v1-rename') {
882
963
  return cmdCodemodV1Rename(parseArgs(rest));
883
964
  }
965
+ if (cmd === 'codemod' && sub === 'v1-project-key') {
966
+ return cmdCodemodV1ProjectKey(parseArgs(rest));
967
+ }
884
968
  if (cmd === 'codemod' && sub === 'v1') {
885
969
  // Imports first: the rename table keys its per-entry tokens by the
886
970
  // specifier they are imported from, which the import rewrite settles.
887
971
  await cmdCodemodV1Imports(parseArgs(rest));
888
- return cmdCodemodV1Rename(parseArgs(rest));
972
+ await cmdCodemodV1Rename(parseArgs(rest));
973
+ return cmdCodemodV1ProjectKey(parseArgs(rest));
889
974
  }
890
975
  if (cmd === 'init') {
891
976
  return cmdInit(parseArgs([sub, ...rest].filter(Boolean)), process.argv.slice(3));
@@ -914,13 +999,13 @@ async function main() {
914
999
  ' schema migrate --name=<name> apply --all + migrate dev + constraints',
915
1000
  );
916
1001
  console.log('');
917
- console.log(' init --project-key=<key> --quota=<key>:<Model>');
1002
+ console.log(' init --app-key=<key> --quota=<key>:<Model>');
918
1003
  console.log(' scaffold the platform wiring. At least one --quota:');
919
1004
  console.log(' every plan must declare one, or the catalogue does not load.');
920
- console.log(' init --project-key=myapp --quota=notes:Note --quota=seats:Seat');
1005
+ console.log(' init --app-key=myapp --quota=notes:Note --quota=seats:Seat');
921
1006
  console.log('');
922
1007
  console.log(' codemod v1 [--dir=.] [--dry-run]');
923
- console.log(' the whole 1.0 migration: v1-imports, then v1-rename');
1008
+ console.log(' the whole 1.0 migration: imports, names, then projectKey');
924
1009
  console.log(' codemod v1-imports [--dir=.] [--dry-run]');
925
1010
  console.log(' rewrite @saasicat/ui-vue imports to the 1.0 export map');
926
1011
  console.log(' codemod v1-rename [--dir=.] [--dry-run]');
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- e8c9a1b5bfe7882ded7efee3939be9243fb7879cc0b605afe9e29c1a77630c3e
1
+ e08122ad3833db9d96fed2edad527604182ab8047a8d8afefec4239ec29fe8f8
package/dist/index.cjs CHANGED
@@ -70,11 +70,12 @@ __export(index_exports, {
70
70
  UserCommands: () => UserCommands,
71
71
  UserPortDoctorCheck: () => UserPortDoctorCheck,
72
72
  WhoAmIFlow: () => WhoAmIFlow,
73
+ appKeyPattern: () => appKeyPattern,
73
74
  appendConstraints: () => appendConstraints,
74
75
  applyFragmentBlocks: () => applyFragmentBlocks,
75
76
  applyTokens: () => applyTokens,
76
77
  assertModelsExist: () => assertModelsExist,
77
- assertValidProjectKey: () => assertValidProjectKey,
78
+ assertValidAppKey: () => assertValidAppKey,
78
79
  assertValidQuotaKey: () => assertValidQuotaKey,
79
80
  blankStringLiterals: () => blankStringLiterals,
80
81
  blockBodyLines: () => blockBodyLines,
@@ -110,10 +111,10 @@ __export(index_exports, {
110
111
  patchAppModule: () => patchAppModule,
111
112
  patchOptionsFor: () => patchOptionsFor,
112
113
  planInit: () => planInit,
113
- projectKeyPattern: () => projectKeyPattern,
114
114
  quotaKeyPattern: () => quotaKeyPattern,
115
115
  readEffectiveModuleResolution: () => readEffectiveModuleResolution,
116
116
  relationNameOf: () => relationNameOf,
117
+ removeProjectKey: () => removeProjectKey,
117
118
  reportConstraints: () => reportConstraints,
118
119
  rewriteImports: () => rewriteImports,
119
120
  rewriteManifest: () => rewriteManifest,
@@ -1033,7 +1034,7 @@ var PlanCatalogDoctorCheck = class {
1033
1034
  severity: "ok",
1034
1035
  message: `${plans.length} plan(s), ${this.catalog.features?.length ?? 0} feature(s) loaded.`,
1035
1036
  details: {
1036
- projectKey: this.catalog.projectKey,
1037
+ app: this.catalog.app.name,
1037
1038
  planIds: plans.map((p) => p.id)
1038
1039
  }
1039
1040
  };
@@ -1705,10 +1706,11 @@ function required(value, what) {
1705
1706
  return value;
1706
1707
  }
1707
1708
  __name(required, "required");
1708
- function projectKeyPattern() {
1709
- return new RegExp(required(schema.properties?.projectKey?.pattern, "pattern for projectKey"));
1709
+ var APP_KEY_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
1710
+ function appKeyPattern() {
1711
+ return APP_KEY_PATTERN;
1710
1712
  }
1711
- __name(projectKeyPattern, "projectKeyPattern");
1713
+ __name(appKeyPattern, "appKeyPattern");
1712
1714
  function quotaKeyPattern() {
1713
1715
  const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
1714
1716
  if (patterns.length !== 1) {
@@ -1721,12 +1723,12 @@ function minimumQuotasPerPlan() {
1721
1723
  return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
1722
1724
  }
1723
1725
  __name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
1724
- function assertValidProjectKey(projectKey) {
1725
- const pattern = projectKeyPattern();
1726
- if (pattern.test(projectKey)) return;
1727
- 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.`);
1726
+ function assertValidAppKey(appKey) {
1727
+ const pattern = appKeyPattern();
1728
+ if (pattern.test(appKey)) return;
1729
+ 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.`);
1728
1730
  }
1729
- __name(assertValidProjectKey, "assertValidProjectKey");
1731
+ __name(assertValidAppKey, "assertValidAppKey");
1730
1732
  function assertValidQuotaKey(quotaKey) {
1731
1733
  const pattern = quotaKeyPattern();
1732
1734
  if (pattern.test(quotaKey)) return;
@@ -1758,24 +1760,24 @@ function pascalCase(value) {
1758
1760
  __name(pascalCase, "pascalCase");
1759
1761
  var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
1760
1762
  function planInit(options) {
1761
- const projectKey = options.projectKey;
1762
- if (!projectKey) throw new Error("init needs a --project-key.");
1763
- assertValidProjectKey(projectKey);
1764
- const appLabel = options.appName ?? pascalCase(projectKey);
1763
+ const appKey = options.appKey;
1764
+ if (!appKey) throw new Error("init needs an --app-key.");
1765
+ assertValidAppKey(appKey);
1766
+ const appLabel = options.appName ?? pascalCase(appKey);
1765
1767
  const appName = pascalCase(appLabel);
1766
1768
  const apiBase = options.apiBase ?? "/api/v1/admin";
1767
1769
  const quotas = (options.quotas ?? []).map(parseQuota);
1768
1770
  assertEnoughQuotas(quotas);
1769
1771
  const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
1770
- const featureKey = `${projectKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
1772
+ const featureKey = `${appKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
1771
1773
  const shared = {
1772
- PROJECT_KEY: projectKey,
1774
+ APP_KEY: appKey,
1773
1775
  APP_NAME: appName,
1774
1776
  APP_LABEL: appLabel,
1775
1777
  API_BASE: apiBase,
1776
1778
  FEATURE_KEY: featureKey,
1777
- REGISTRY_CONST: `${constantCase(projectKey)}_FEATURE_UI_REGISTRY`,
1778
- MANIFEST_CONST: `${constantCase(projectKey)}_MANIFEST_CONTRIBUTION`,
1779
+ REGISTRY_CONST: `${constantCase(appKey)}_FEATURE_UI_REGISTRY`,
1780
+ MANIFEST_CONST: `${constantCase(appKey)}_MANIFEST_CONTRIBUTION`,
1779
1781
  ADMIN_MODULE_CLASS: `${appName}AdminModule`,
1780
1782
  HASHER_CLASS: hasherClass ?? "",
1781
1783
  HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
@@ -2169,6 +2171,118 @@ function rewriteManifest(text, table, options) {
2169
2171
  }
2170
2172
  __name(rewriteManifest, "rewriteManifest");
2171
2173
 
2174
+ // src/codemods/v1-project-key.ts
2175
+ function stripQueryParameter(text) {
2176
+ const NEEDLE = "projectKey=";
2177
+ let out = "";
2178
+ let index = 0;
2179
+ const taken = /* @__PURE__ */ new Set();
2180
+ for (; ; ) {
2181
+ const at = text.indexOf(NEEDLE, index);
2182
+ if (at < 0) break;
2183
+ const separator = at === 0 ? "" : text[at - 1];
2184
+ const value = separator === "?" || separator === "&" ? simpleValueEnd(text, at + NEEDLE.length) : null;
2185
+ if (value === null || !servesTheCatalogue(text, at)) {
2186
+ out += text.slice(index, at + NEEDLE.length);
2187
+ index = at + NEEDLE.length;
2188
+ continue;
2189
+ }
2190
+ out += text.slice(index, at - 1);
2191
+ if (separator === "?" && text[value] === "&") {
2192
+ out += "?";
2193
+ index = value + 1;
2194
+ } else {
2195
+ index = value;
2196
+ }
2197
+ taken.add(at);
2198
+ }
2199
+ return {
2200
+ text: out + text.slice(index),
2201
+ taken
2202
+ };
2203
+ }
2204
+ __name(stripQueryParameter, "stripQueryParameter");
2205
+ var HIDES_A_BRACE = /[`'"{}/\\]/;
2206
+ function simpleValueEnd(text, from) {
2207
+ let i = from;
2208
+ while (i < text.length) {
2209
+ const ch = text[i];
2210
+ if (ch === "&" || ch === "#" || ch === "'" || ch === '"' || ch === "`" || ch === "\n" || ch === " ") {
2211
+ return i;
2212
+ }
2213
+ if (ch !== "$" || text[i + 1] !== "{") {
2214
+ if (ch === "{" || ch === "}") return null;
2215
+ i += 1;
2216
+ continue;
2217
+ }
2218
+ const close = text.indexOf("}", i + 2);
2219
+ if (close < 0) return null;
2220
+ if (HIDES_A_BRACE.test(text.slice(i + 2, close))) return null;
2221
+ i = close + 1;
2222
+ }
2223
+ return null;
2224
+ }
2225
+ __name(simpleValueEnd, "simpleValueEnd");
2226
+ function servesTheCatalogue(text, at) {
2227
+ let start = at;
2228
+ while (start > 0) {
2229
+ const ch = text[start - 1];
2230
+ if (ch === "`" || ch === "'" || ch === '"' || ch === "\n") break;
2231
+ start -= 1;
2232
+ }
2233
+ const url = text.slice(start, at);
2234
+ const query = url.indexOf("?");
2235
+ return (query < 0 ? url : url.slice(0, query)).includes("/catalog/");
2236
+ }
2237
+ __name(servesTheCatalogue, "servesTheCatalogue");
2238
+ function isIdentifierChar(ch) {
2239
+ return ch !== void 0 && /[A-Za-z0-9_$]/.test(ch);
2240
+ }
2241
+ __name(isIdentifierChar, "isIdentifierChar");
2242
+ function lineAt(text, at) {
2243
+ let line = 1;
2244
+ for (let i = 0; i < at; i += 1) if (text[i] === "\n") line += 1;
2245
+ return line;
2246
+ }
2247
+ __name(lineAt, "lineAt");
2248
+ function removeProjectKey(text, kind = "source") {
2249
+ if (kind === "yaml") return removeFromYaml(text);
2250
+ const query = stripQueryParameter(text);
2251
+ const reported = /* @__PURE__ */ new Set();
2252
+ for (let at = text.indexOf("projectKey"); at >= 0; at = text.indexOf("projectKey", at + 1)) {
2253
+ if (query.taken.has(at)) continue;
2254
+ if (isIdentifierChar(text[at - 1])) continue;
2255
+ if (isIdentifierChar(text[at + "projectKey".length])) continue;
2256
+ reported.add(lineAt(text, at));
2257
+ }
2258
+ return {
2259
+ text: query.text,
2260
+ rewritten: query.taken.size,
2261
+ undecided: [
2262
+ ...reported
2263
+ ].sort((a, b) => a - b)
2264
+ };
2265
+ }
2266
+ __name(removeProjectKey, "removeProjectKey");
2267
+ function removeFromYaml(text) {
2268
+ const lines = text.split("\n");
2269
+ const kept = [];
2270
+ let rewritten = 0;
2271
+ for (const line of lines) {
2272
+ if (line.startsWith("projectKey:")) {
2273
+ rewritten += 1;
2274
+ continue;
2275
+ }
2276
+ kept.push(line);
2277
+ }
2278
+ return {
2279
+ text: kept.join("\n"),
2280
+ rewritten,
2281
+ undecided: []
2282
+ };
2283
+ }
2284
+ __name(removeFromYaml, "removeFromYaml");
2285
+
2172
2286
  // src/init/patch-app-module.ts
2173
2287
  var MARKER = "SaaSiCatModule.forRoot";
2174
2288
  function patchAppModule(source, options) {
@@ -3392,11 +3506,12 @@ UserCommands = _ts_decorate14([
3392
3506
  UserCommands,
3393
3507
  UserPortDoctorCheck,
3394
3508
  WhoAmIFlow,
3509
+ appKeyPattern,
3395
3510
  appendConstraints,
3396
3511
  applyFragmentBlocks,
3397
3512
  applyTokens,
3398
3513
  assertModelsExist,
3399
- assertValidProjectKey,
3514
+ assertValidAppKey,
3400
3515
  assertValidQuotaKey,
3401
3516
  blankStringLiterals,
3402
3517
  blockBodyLines,
@@ -3432,10 +3547,10 @@ UserCommands = _ts_decorate14([
3432
3547
  patchAppModule,
3433
3548
  patchOptionsFor,
3434
3549
  planInit,
3435
- projectKeyPattern,
3436
3550
  quotaKeyPattern,
3437
3551
  readEffectiveModuleResolution,
3438
3552
  relationNameOf,
3553
+ removeProjectKey,
3439
3554
  reportConstraints,
3440
3555
  rewriteImports,
3441
3556
  rewriteManifest,
package/dist/index.d.cts CHANGED
@@ -719,9 +719,9 @@ declare function assertModelsExist(declaredModels: readonly string[], models: Fk
719
719
 
720
720
  /** What the caller asked for. */
721
721
  interface InitOptions {
722
- /** The catalogue this app administers. Also the storage-key prefix. */
723
- projectKey: string;
724
- /** Human name in the manifest and the YAML. Defaults to `projectKey`. */
722
+ /** Slug of the application: npm package name, storage-key prefix, id prefix. */
723
+ appKey: string;
724
+ /** Human name in the manifest and the YAML. Defaults to `appKey`. */
725
725
  appName?: string;
726
726
  /** Admin API prefix, e.g. `/api/v1/admin`. */
727
727
  apiBase?: string;
@@ -854,8 +854,7 @@ declare function judgeModuleResolution(value: string | null): ModuleResolutionVe
854
854
  */
855
855
  declare function readEffectiveModuleResolution(root: string, ts: typeof TypeScript): string | null;
856
856
 
857
- /** The pattern `plan-catalog.schema.json` puts on `projectKey`. */
858
- declare function projectKeyPattern(): RegExp;
857
+ declare function appKeyPattern(): RegExp;
859
858
  /** The pattern it puts on the keys inside a plan's `quotas` object. */
860
859
  declare function quotaKeyPattern(): RegExp;
861
860
  /** How many quotas a plan must declare — 1 today, and read rather than assumed. */
@@ -866,7 +865,7 @@ declare function minimumQuotasPerPlan(): number;
866
865
  * The message carries the pattern rather than a prose paraphrase, because the
867
866
  * paraphrase is what goes stale.
868
867
  */
869
- declare function assertValidProjectKey(projectKey: string): void;
868
+ declare function assertValidAppKey(appKey: string): void;
870
869
  /** The same, for a quota key. `additionalProperties: false` makes it a hard rule. */
871
870
  declare function assertValidQuotaKey(quotaKey: string): void;
872
871
 
@@ -981,6 +980,26 @@ interface ManifestRewriteOptions {
981
980
  }
982
981
  declare function rewriteManifest(text: string, table: RenameTable, options: ManifestRewriteOptions): RenameResult;
983
982
 
983
+ interface ProjectKeyResult {
984
+ readonly text: string;
985
+ /** How many occurrences were taken out. */
986
+ readonly rewritten: number;
987
+ /**
988
+ * 1-based line numbers of the occurrences left in place.
989
+ *
990
+ * Reported rather than removed: the codemod could not tell them from a
991
+ * consumer's own field, and a wrong deletion is worse than a named one.
992
+ */
993
+ readonly undecided: readonly number[];
994
+ }
995
+ /**
996
+ * Rewrites one source file.
997
+ *
998
+ * `yaml` switches to the config form: there the field is a top-level key in a
999
+ * file the platform owns the schema of, so it is decidable without an anchor.
1000
+ */
1001
+ declare function removeProjectKey(text: string, kind?: 'source' | 'yaml'): ProjectKeyResult;
1002
+
984
1003
  interface PatchAppModuleOptions {
985
1004
  /** Import specifier for the persistence bundle, or null when not generated. */
986
1005
  persistenceImport: string | null;
@@ -1224,4 +1243,4 @@ declare class UserCommands extends CommandRunner {
1224
1243
  parsePassword(val: string): string;
1225
1244
  }
1226
1245
 
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 };
1246
+ 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 ProjectKeyResult, 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, appKeyPattern, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidAppKey, 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, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, removeProjectKey, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, stripLineComment, structuralOnly, tablesAddressedBy };
package/dist/index.d.ts CHANGED
@@ -719,9 +719,9 @@ declare function assertModelsExist(declaredModels: readonly string[], models: Fk
719
719
 
720
720
  /** What the caller asked for. */
721
721
  interface InitOptions {
722
- /** The catalogue this app administers. Also the storage-key prefix. */
723
- projectKey: string;
724
- /** Human name in the manifest and the YAML. Defaults to `projectKey`. */
722
+ /** Slug of the application: npm package name, storage-key prefix, id prefix. */
723
+ appKey: string;
724
+ /** Human name in the manifest and the YAML. Defaults to `appKey`. */
725
725
  appName?: string;
726
726
  /** Admin API prefix, e.g. `/api/v1/admin`. */
727
727
  apiBase?: string;
@@ -854,8 +854,7 @@ declare function judgeModuleResolution(value: string | null): ModuleResolutionVe
854
854
  */
855
855
  declare function readEffectiveModuleResolution(root: string, ts: typeof TypeScript): string | null;
856
856
 
857
- /** The pattern `plan-catalog.schema.json` puts on `projectKey`. */
858
- declare function projectKeyPattern(): RegExp;
857
+ declare function appKeyPattern(): RegExp;
859
858
  /** The pattern it puts on the keys inside a plan's `quotas` object. */
860
859
  declare function quotaKeyPattern(): RegExp;
861
860
  /** How many quotas a plan must declare — 1 today, and read rather than assumed. */
@@ -866,7 +865,7 @@ declare function minimumQuotasPerPlan(): number;
866
865
  * The message carries the pattern rather than a prose paraphrase, because the
867
866
  * paraphrase is what goes stale.
868
867
  */
869
- declare function assertValidProjectKey(projectKey: string): void;
868
+ declare function assertValidAppKey(appKey: string): void;
870
869
  /** The same, for a quota key. `additionalProperties: false` makes it a hard rule. */
871
870
  declare function assertValidQuotaKey(quotaKey: string): void;
872
871
 
@@ -981,6 +980,26 @@ interface ManifestRewriteOptions {
981
980
  }
982
981
  declare function rewriteManifest(text: string, table: RenameTable, options: ManifestRewriteOptions): RenameResult;
983
982
 
983
+ interface ProjectKeyResult {
984
+ readonly text: string;
985
+ /** How many occurrences were taken out. */
986
+ readonly rewritten: number;
987
+ /**
988
+ * 1-based line numbers of the occurrences left in place.
989
+ *
990
+ * Reported rather than removed: the codemod could not tell them from a
991
+ * consumer's own field, and a wrong deletion is worse than a named one.
992
+ */
993
+ readonly undecided: readonly number[];
994
+ }
995
+ /**
996
+ * Rewrites one source file.
997
+ *
998
+ * `yaml` switches to the config form: there the field is a top-level key in a
999
+ * file the platform owns the schema of, so it is decidable without an anchor.
1000
+ */
1001
+ declare function removeProjectKey(text: string, kind?: 'source' | 'yaml'): ProjectKeyResult;
1002
+
984
1003
  interface PatchAppModuleOptions {
985
1004
  /** Import specifier for the persistence bundle, or null when not generated. */
986
1005
  persistenceImport: string | null;
@@ -1224,4 +1243,4 @@ declare class UserCommands extends CommandRunner {
1224
1243
  parsePassword(val: string): string;
1225
1244
  }
1226
1245
 
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 };
1246
+ 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 ProjectKeyResult, 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, appKeyPattern, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidAppKey, 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, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, removeProjectKey, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, stripLineComment, structuralOnly, tablesAddressedBy };
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
  };
@@ -1581,10 +1581,11 @@ function required(value, what) {
1581
1581
  return value;
1582
1582
  }
1583
1583
  __name(required, "required");
1584
- function projectKeyPattern() {
1585
- 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;
1586
1587
  }
1587
- __name(projectKeyPattern, "projectKeyPattern");
1588
+ __name(appKeyPattern, "appKeyPattern");
1588
1589
  function quotaKeyPattern() {
1589
1590
  const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
1590
1591
  if (patterns.length !== 1) {
@@ -1597,12 +1598,12 @@ function minimumQuotasPerPlan() {
1597
1598
  return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
1598
1599
  }
1599
1600
  __name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
1600
- function assertValidProjectKey(projectKey) {
1601
- const pattern = projectKeyPattern();
1602
- if (pattern.test(projectKey)) return;
1603
- 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.`);
1604
1605
  }
1605
- __name(assertValidProjectKey, "assertValidProjectKey");
1606
+ __name(assertValidAppKey, "assertValidAppKey");
1606
1607
  function assertValidQuotaKey(quotaKey) {
1607
1608
  const pattern = quotaKeyPattern();
1608
1609
  if (pattern.test(quotaKey)) return;
@@ -1634,24 +1635,24 @@ function pascalCase(value) {
1634
1635
  __name(pascalCase, "pascalCase");
1635
1636
  var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
1636
1637
  function planInit(options) {
1637
- const projectKey = options.projectKey;
1638
- if (!projectKey) throw new Error("init needs a --project-key.");
1639
- assertValidProjectKey(projectKey);
1640
- 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);
1641
1642
  const appName = pascalCase(appLabel);
1642
1643
  const apiBase = options.apiBase ?? "/api/v1/admin";
1643
1644
  const quotas = (options.quotas ?? []).map(parseQuota);
1644
1645
  assertEnoughQuotas(quotas);
1645
1646
  const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
1646
- const featureKey = `${projectKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
1647
+ const featureKey = `${appKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
1647
1648
  const shared = {
1648
- PROJECT_KEY: projectKey,
1649
+ APP_KEY: appKey,
1649
1650
  APP_NAME: appName,
1650
1651
  APP_LABEL: appLabel,
1651
1652
  API_BASE: apiBase,
1652
1653
  FEATURE_KEY: featureKey,
1653
- REGISTRY_CONST: `${constantCase(projectKey)}_FEATURE_UI_REGISTRY`,
1654
- MANIFEST_CONST: `${constantCase(projectKey)}_MANIFEST_CONTRIBUTION`,
1654
+ REGISTRY_CONST: `${constantCase(appKey)}_FEATURE_UI_REGISTRY`,
1655
+ MANIFEST_CONST: `${constantCase(appKey)}_MANIFEST_CONTRIBUTION`,
1655
1656
  ADMIN_MODULE_CLASS: `${appName}AdminModule`,
1656
1657
  HASHER_CLASS: hasherClass ?? "",
1657
1658
  HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
@@ -2045,6 +2046,118 @@ function rewriteManifest(text, table, options) {
2045
2046
  }
2046
2047
  __name(rewriteManifest, "rewriteManifest");
2047
2048
 
2049
+ // src/codemods/v1-project-key.ts
2050
+ function stripQueryParameter(text) {
2051
+ const NEEDLE = "projectKey=";
2052
+ let out = "";
2053
+ let index = 0;
2054
+ const taken = /* @__PURE__ */ new Set();
2055
+ for (; ; ) {
2056
+ const at = text.indexOf(NEEDLE, index);
2057
+ if (at < 0) break;
2058
+ const separator = at === 0 ? "" : text[at - 1];
2059
+ const value = separator === "?" || separator === "&" ? simpleValueEnd(text, at + NEEDLE.length) : null;
2060
+ if (value === null || !servesTheCatalogue(text, at)) {
2061
+ out += text.slice(index, at + NEEDLE.length);
2062
+ index = at + NEEDLE.length;
2063
+ continue;
2064
+ }
2065
+ out += text.slice(index, at - 1);
2066
+ if (separator === "?" && text[value] === "&") {
2067
+ out += "?";
2068
+ index = value + 1;
2069
+ } else {
2070
+ index = value;
2071
+ }
2072
+ taken.add(at);
2073
+ }
2074
+ return {
2075
+ text: out + text.slice(index),
2076
+ taken
2077
+ };
2078
+ }
2079
+ __name(stripQueryParameter, "stripQueryParameter");
2080
+ var HIDES_A_BRACE = /[`'"{}/\\]/;
2081
+ function simpleValueEnd(text, from) {
2082
+ let i = from;
2083
+ while (i < text.length) {
2084
+ const ch = text[i];
2085
+ if (ch === "&" || ch === "#" || ch === "'" || ch === '"' || ch === "`" || ch === "\n" || ch === " ") {
2086
+ return i;
2087
+ }
2088
+ if (ch !== "$" || text[i + 1] !== "{") {
2089
+ if (ch === "{" || ch === "}") return null;
2090
+ i += 1;
2091
+ continue;
2092
+ }
2093
+ const close = text.indexOf("}", i + 2);
2094
+ if (close < 0) return null;
2095
+ if (HIDES_A_BRACE.test(text.slice(i + 2, close))) return null;
2096
+ i = close + 1;
2097
+ }
2098
+ return null;
2099
+ }
2100
+ __name(simpleValueEnd, "simpleValueEnd");
2101
+ function servesTheCatalogue(text, at) {
2102
+ let start = at;
2103
+ while (start > 0) {
2104
+ const ch = text[start - 1];
2105
+ if (ch === "`" || ch === "'" || ch === '"' || ch === "\n") break;
2106
+ start -= 1;
2107
+ }
2108
+ const url = text.slice(start, at);
2109
+ const query = url.indexOf("?");
2110
+ return (query < 0 ? url : url.slice(0, query)).includes("/catalog/");
2111
+ }
2112
+ __name(servesTheCatalogue, "servesTheCatalogue");
2113
+ function isIdentifierChar(ch) {
2114
+ return ch !== void 0 && /[A-Za-z0-9_$]/.test(ch);
2115
+ }
2116
+ __name(isIdentifierChar, "isIdentifierChar");
2117
+ function lineAt(text, at) {
2118
+ let line = 1;
2119
+ for (let i = 0; i < at; i += 1) if (text[i] === "\n") line += 1;
2120
+ return line;
2121
+ }
2122
+ __name(lineAt, "lineAt");
2123
+ function removeProjectKey(text, kind = "source") {
2124
+ if (kind === "yaml") return removeFromYaml(text);
2125
+ const query = stripQueryParameter(text);
2126
+ const reported = /* @__PURE__ */ new Set();
2127
+ for (let at = text.indexOf("projectKey"); at >= 0; at = text.indexOf("projectKey", at + 1)) {
2128
+ if (query.taken.has(at)) continue;
2129
+ if (isIdentifierChar(text[at - 1])) continue;
2130
+ if (isIdentifierChar(text[at + "projectKey".length])) continue;
2131
+ reported.add(lineAt(text, at));
2132
+ }
2133
+ return {
2134
+ text: query.text,
2135
+ rewritten: query.taken.size,
2136
+ undecided: [
2137
+ ...reported
2138
+ ].sort((a, b) => a - b)
2139
+ };
2140
+ }
2141
+ __name(removeProjectKey, "removeProjectKey");
2142
+ function removeFromYaml(text) {
2143
+ const lines = text.split("\n");
2144
+ const kept = [];
2145
+ let rewritten = 0;
2146
+ for (const line of lines) {
2147
+ if (line.startsWith("projectKey:")) {
2148
+ rewritten += 1;
2149
+ continue;
2150
+ }
2151
+ kept.push(line);
2152
+ }
2153
+ return {
2154
+ text: kept.join("\n"),
2155
+ rewritten,
2156
+ undecided: []
2157
+ };
2158
+ }
2159
+ __name(removeFromYaml, "removeFromYaml");
2160
+
2048
2161
  // src/init/patch-app-module.ts
2049
2162
  var MARKER = "SaaSiCatModule.forRoot";
2050
2163
  function patchAppModule(source, options) {
@@ -3267,11 +3380,12 @@ export {
3267
3380
  UserCommands,
3268
3381
  UserPortDoctorCheck,
3269
3382
  WhoAmIFlow,
3383
+ appKeyPattern,
3270
3384
  appendConstraints,
3271
3385
  applyFragmentBlocks,
3272
3386
  applyTokens,
3273
3387
  assertModelsExist,
3274
- assertValidProjectKey,
3388
+ assertValidAppKey,
3275
3389
  assertValidQuotaKey,
3276
3390
  blankStringLiterals,
3277
3391
  blockBodyLines,
@@ -3307,10 +3421,10 @@ export {
3307
3421
  patchAppModule,
3308
3422
  patchOptionsFor,
3309
3423
  planInit,
3310
- projectKeyPattern,
3311
3424
  quotaKeyPattern,
3312
3425
  readEffectiveModuleResolution,
3313
3426
  relationNameOf,
3427
+ removeProjectKey,
3314
3428
  reportConstraints,
3315
3429
  rewriteImports,
3316
3430
  rewriteManifest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/cli",
3
- "version": "1.0.0-rc.5",
3
+ "version": "1.0.0-rc.7",
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",
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "qrcode-terminal": "^0.12.0",
33
- "@saasicat/nest": "^1.0.0-rc.5",
34
- "@saasicat/spec": "^1.0.0-rc.5",
35
- "@saasicat/core": "^1.0.0-rc.5"
33
+ "@saasicat/nest": "^1.0.0-rc.7",
34
+ "@saasicat/spec": "^1.0.0-rc.7",
35
+ "@saasicat/core": "^1.0.0-rc.7"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@nestjs/common": "^11.0.0",
@@ -1,5 +1,4 @@
1
1
  schemaVersion: 1
2
- projectKey: __PROJECT_KEY__
3
2
 
4
3
  app:
5
4
  name: __APP_LABEL__
@@ -19,7 +19,7 @@ export const __MANIFEST_CONST__: ManifestContribution = {
19
19
  dashboard: {
20
20
  kpiCards: [
21
21
  {
22
- id: '__PROJECT_KEY__.tenants',
22
+ id: '__APP_KEY__.tenants',
23
23
  label: 'Tenants',
24
24
  // Served by your own controller — the platform does not know
25
25
  // what your KPIs count.