create-basalt 1.12.0 → 1.13.0

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
@@ -273,7 +273,11 @@ newest release **on the app's current major** (a newer major is listed as
273
273
  `minimumReleaseAge`, read from the project's `pnpm-workspace.yaml` when set) is
274
274
  left for later so the install cannot refuse it. When a framework release
275
275
  declares a peer range the app would not meet (say `zod ^5` while zod is held on
276
- 4), the plan warns before anything is written.
276
+ 4), the plan warns before anything is written. When a framework release
277
+ requires an `@basaltkit/*` peer the app does not list (`@basaltkit/testing` 3
278
+ takes `@basaltkit/mailer`), the plan adds it at its latest version (in
279
+ `dependencies` when a runtime dependency needs it, else `devDependencies`), or
280
+ warns when it cannot; optional peers are never added.
277
281
 
278
282
  Both `package.json` and `web/package.json` are updated, **in place**: only the
279
283
  version strings change — order, indentation and range style (`^`, `~`, exact)
@@ -466,7 +470,7 @@ Detects the manager that invoked the command from `npm_config_user_agent` (or th
466
470
  | --- | --- |
467
471
  | `runProjectCommand(argv, deps)` | Runs `update`/`add`/`doctor`/`info` and returns the exit code; every side effect (output, prompts, package-manager runs, registry fetch, codemods) is injected through `deps` |
468
472
  | `loadProject(dir, pm?)` | Reads an app: package.json (+ `web/`), manifest, detected features and package manager. Throws `NotABasaltAppError` |
469
- | `planUpdate(ctx, { major?, only?, tooling?, registry? })` | The update plan — entries, the full before/after of every file, tooling, warnings, framework majors. Writes nothing |
473
+ | `planUpdate(ctx, { major?, only?, tooling?, registry? })` | The update plan — entries, the full before/after of every file, tooling, warnings, framework majors, required peers it adds (`addedPeers`). Writes nothing |
470
474
  | `planAdd(ctx, feature, { force?, resolveLatest?, registry? })` | The add plan — created/merged/skipped files and manual steps. Writes nothing |
471
475
  | `runDoctor(ctx, options)` | Doctor findings (`{ level: 'ok' \| 'info' \| 'warn' \| 'error', area, message }`) |
472
476
  | `MANIFEST_PATH`, `readManifest(dir)`, `hashContent(text)` | The scaffold manifest |
@@ -114,6 +114,8 @@ export interface LatestInfo {
114
114
  latest?: string;
115
115
  /** `peerDependencies` declared by that latest version. */
116
116
  peers: Record<string, string>;
117
+ /** The peers its `peerDependenciesMeta` marks optional (absent: none). */
118
+ optionalPeers?: string[];
117
119
  /**
118
120
  * Release age of `latest` against the window: `mature` (provably older, or
119
121
  * not probed), `recent` (inside the window) or `unknown` (could not prove it).
@@ -153,6 +153,15 @@ function peersOf(body) {
153
153
  }
154
154
  return peers;
155
155
  }
156
+ /** The peers `peerDependenciesMeta` marks `optional: true`. */
157
+ function optionalPeersOf(body) {
158
+ const raw = body?.peerDependenciesMeta;
159
+ if (!raw || typeof raw !== 'object')
160
+ return [];
161
+ return Object.entries(raw)
162
+ .filter(([, meta]) => typeof meta === 'object' && meta !== null && meta.optional === true)
163
+ .map(([name]) => name);
164
+ }
156
165
  async function lookupLatest(fetchImpl, registry, name, signal) {
157
166
  try {
158
167
  const response = await fetchImpl(latestUrl(registry, name), {
@@ -165,7 +174,9 @@ async function lookupLatest(fetchImpl, registry, name, signal) {
165
174
  return 'miss';
166
175
  const body = (await response.json());
167
176
  const version = body?.version;
168
- return typeof version === 'string' && STABLE_SEMVER.test(version) ? { version, peers: peersOf(body) } : 'miss';
177
+ return typeof version === 'string' && STABLE_SEMVER.test(version)
178
+ ? { version, peers: peersOf(body), optionalPeers: optionalPeersOf(body) }
179
+ : 'miss';
169
180
  }
170
181
  catch {
171
182
  // Network error, per-request timeout, or malformed JSON.
@@ -223,7 +234,7 @@ export async function lookupLatestVersions(names, shouldProbe, options = {}) {
223
234
  const age = windowMs === 0 || !shouldProbe(name, found.version)
224
235
  ? 'mature'
225
236
  : await probeAge(fetchImpl, registry, name, windowMs, now, AbortSignal.any([AbortSignal.timeout(timeoutMs), overall.signal]));
226
- results.set(name, { latest: found.version, peers: found.peers, age });
237
+ results.set(name, { latest: found.version, peers: found.peers, optionalPeers: found.optionalPeers, age });
227
238
  }
228
239
  };
229
240
  const pool = Math.max(1, Math.min(options.concurrency ?? CONCURRENCY, names.length));
@@ -76,6 +76,17 @@ export interface UpdatePlan {
76
76
  to: string;
77
77
  changelog: string;
78
78
  }[];
79
+ /**
80
+ * Required `@basaltkit/*` peers of the framework releases the app ends up on
81
+ * that its package.json did not list (testing 3 takes mailer): added at their
82
+ * latest version, in `dependencies` when a runtime dependency needs them.
83
+ */
84
+ addedPeers: {
85
+ name: string;
86
+ range: string;
87
+ section: 'dependencies' | 'devDependencies';
88
+ requiredBy: string[];
89
+ }[];
79
90
  registry: string;
80
91
  }
81
92
  export interface UpdateOptions {
@@ -277,6 +277,50 @@ export async function planUpdate(ctx, options = {}) {
277
277
  }
278
278
  }
279
279
  }
280
+ // A required @basaltkit peer the app never declared: pnpm and npm may
281
+ // auto-install it, but yarn PnP, `auto-install-peers=false` or
282
+ // `--legacy-peer-deps` do not, and `doctor` checks installed versions, not
283
+ // the manifest. Add it at its latest version; when that is impossible
284
+ // (registry, release-age window, out of range), say so.
285
+ const declared = new Set(entries.filter((entry) => entry.file === 'package.json').map((entry) => entry.name));
286
+ const missing = new Map();
287
+ for (const entry of entries) {
288
+ if (!entry.framework || (entry.status !== 'update' && entry.status !== 'current') || entry.file !== 'package.json')
289
+ continue;
290
+ const info = results.get(entry.name);
291
+ for (const [peer, wanted] of Object.entries(info?.peers ?? {})) {
292
+ if (!isFramework(peer) || declared.has(peer) || info?.optionalPeers?.includes(peer))
293
+ continue;
294
+ const prior = missing.get(peer);
295
+ const section = entry.section === 'dependencies' || prior?.section === 'dependencies' ? 'dependencies' : 'devDependencies';
296
+ const requiredBy = [...(prior?.requiredBy ?? []), `${entry.name}@${info?.latest}`];
297
+ missing.set(peer, { wanted: prior?.wanted ?? wanted, requiredBy: [...new Set(requiredBy)], section });
298
+ }
299
+ }
300
+ const addedPeers = [];
301
+ if (missing.size > 0) {
302
+ const peerLookup = await lookupLatestVersions([...missing.keys()].sort(), (name) => !exempt(name), {
303
+ ...options.registry,
304
+ minimumReleaseAge: window,
305
+ });
306
+ const additions = { dependencies: {}, devDependencies: {} };
307
+ for (const [peer, need] of missing) {
308
+ const found = peerLookup.results.get(peer);
309
+ if (found?.latest !== undefined && found.age === 'mature' && satisfies(found.latest, need.wanted) === true) {
310
+ const range = `^${found.latest}`;
311
+ additions[need.section][peer] = range;
312
+ addedPeers.push({ name: peer, range, section: need.section, requiredBy: need.requiredBy });
313
+ }
314
+ else {
315
+ warnings.push(`${need.requiredBy.join(', ')} requires ${peer} ${need.wanted}, which package.json does not list — add it (${ctx.pm} add ${need.section === 'devDependencies' ? '-D ' : ''}${peer})`);
316
+ }
317
+ }
318
+ if (addedPeers.length > 0) {
319
+ const merged = mergePackageJson(files['package.json']?.after ?? ctx.packageJsonText, additions);
320
+ if (merged.changes.length > 0)
321
+ files['package.json'] = { before: ctx.packageJsonText, after: merged.text };
322
+ }
323
+ }
280
324
  const failed = entries.filter((entry) => entry.status === 'failed').map((entry) => entry.name);
281
325
  if (failed.length > 0)
282
326
  warnings.push(`The registry did not answer for ${[...new Set(failed)].join(', ')} — left unchanged.`);
@@ -289,7 +333,7 @@ export async function planUpdate(ctx, options = {}) {
289
333
  changelog: changelogUrl(entry.name),
290
334
  }))
291
335
  .filter((major, i, all) => all.findIndex((other) => other.name === major.name) === i);
292
- return { entries, updates, files, tooling: toolingNotes, manual, warnings, majors, registry };
336
+ return { entries, updates, files, tooling: toolingNotes, manual, warnings, majors, addedPeers, registry };
293
337
  }
294
338
  /** The plan as terminal lines: the update table, then notes, tooling, warnings and majors. */
295
339
  export function renderUpdatePlan(plan, colors) {
@@ -328,6 +372,12 @@ export function renderUpdatePlan(plan, colors) {
328
372
  lines.push(` ${major.name} ${major.from} → ${major.to}: ${major.changelog}`);
329
373
  lines.push(` Upgrade notes: ${DOCS_UPGRADING_URL}`);
330
374
  }
375
+ if (plan.addedPeers.length > 0) {
376
+ lines.push('', colors.bold('Required peers the app did not list (added):'));
377
+ for (const peer of plan.addedPeers) {
378
+ lines.push(` ${colors.cyan(peer.name)} ${peer.range} (${peer.section}) — required by ${peer.requiredBy.join(', ')}`);
379
+ }
380
+ }
331
381
  if (plan.tooling.length > 0) {
332
382
  lines.push('', colors.bold('Project tooling:'));
333
383
  for (const note of plan.tooling)
@@ -20,8 +20,8 @@ export { basaltBin } from './bin-template.js';
20
20
  export { SCAFFOLD_VERSIONS } from './versions.js';
21
21
  export { THIRD_PARTY_VERSIONS } from './latest-versions.js';
22
22
  /**
23
- * The dependency range for a @basaltkit/* package: the current release line
24
- * (`^<major>.<minor>.0`) of its workspace version, from the generated
23
+ * The dependency range for a @basaltkit/* package: its workspace version as
24
+ * the floor (`^<major>.<minor>.<patch>`), from the generated
25
25
  * {@link SCAFFOLD_VERSIONS} map. Throws for an unknown package rather than
26
26
  * guessing a range — a stale guess once pinned new apps to frozen 1.x lines.
27
27
  */
package/dist/templates.js CHANGED
@@ -5,8 +5,8 @@ export { basaltBin } from './bin-template.js';
5
5
  export { SCAFFOLD_VERSIONS } from './versions.js';
6
6
  export { THIRD_PARTY_VERSIONS } from './latest-versions.js';
7
7
  /**
8
- * The dependency range for a @basaltkit/* package: the current release line
9
- * (`^<major>.<minor>.0`) of its workspace version, from the generated
8
+ * The dependency range for a @basaltkit/* package: its workspace version as
9
+ * the floor (`^<major>.<minor>.<patch>`), from the generated
10
10
  * {@link SCAFFOLD_VERSIONS} map. Throws for an unknown package rather than
11
11
  * guessing a range — a stale guess once pinned new apps to frozen 1.x lines.
12
12
  */
@@ -104,6 +104,10 @@ export function packageJson(options) {
104
104
  }
105
105
  const devDependencies = {
106
106
  '@basaltkit/testing': versionOf('@basaltkit/testing'),
107
+ // @basaltkit/testing's peer: fakeMailer() binds the app's own MAILER token,
108
+ // so the copy must be the app's (a package manager that skips peers would
109
+ // otherwise leave `@basaltkit/testing` unable to load).
110
+ '@basaltkit/mailer': versionOf('@basaltkit/mailer'),
107
111
  // The project commands (\`pnpm basalt update | add | doctor | info\`) — dev
108
112
  // tooling with no dependencies of its own, never imported by the app.
109
113
  'create-basalt': versionOf('create-basalt'),
@@ -725,9 +729,17 @@ model AuthApiKey {
725
729
  expiresAt DateTime?
726
730
  lastUsedAt DateTime?
727
731
  revokedAt DateTime?
732
+ // 3.0: API key lifecycle (migration \`add_api_key_lifecycle\`).
733
+ serviceId String?
734
+ previousHash String? @unique
735
+ previousHashExpiresAt DateTime?
736
+ rotatedAt DateTime?
737
+ allowedIps String[] @default([])
738
+ meta Json?
728
739
 
729
740
  @@index([tenantId])
730
741
  @@index([userId])
742
+ @@index([serviceId])
731
743
  @@map("auth_api_keys")
732
744
  }
733
745
 
package/dist/versions.js CHANGED
@@ -1,29 +1,31 @@
1
1
  // GENERATED by scripts/sync-versions.mjs (runs before every build) — do not edit.
2
2
  // The dependency range a new app gets for each @basaltkit/* package: the
3
- // current release line (^<major>.<minor>.0) of the workspace version.
3
+ // workspace version as the floor (^<major>.<minor>.<patch>), so a new app never
4
+ // resolves an older patch of the same line.
4
5
  export const SCAFFOLD_VERSIONS = {
5
6
  '@basaltkit/admin': '^2.0.0',
6
- '@basaltkit/admin-shadcn': '^1.1.0',
7
+ '@basaltkit/admin-shadcn': '^1.1.1',
7
8
  '@basaltkit/ai-mcp': '^0.4.0',
8
- '@basaltkit/auth': '^4.2.0',
9
- '@basaltkit/auth-prisma': '^2.1.0',
10
- '@basaltkit/cli': '^1.4.0',
11
- '@basaltkit/config': '^1.1.0',
9
+ '@basaltkit/auth': '^5.0.0',
10
+ '@basaltkit/auth-prisma': '^3.0.0',
11
+ '@basaltkit/cli': '^1.4.1',
12
+ '@basaltkit/config': '^1.1.2',
12
13
  '@basaltkit/core': '^1.6.0',
13
- '@basaltkit/env': '^3.1.0',
14
+ '@basaltkit/env': '^3.1.1',
14
15
  '@basaltkit/events': '^1.3.0',
15
- '@basaltkit/fastify': '^2.6.0',
16
+ '@basaltkit/fastify': '^3.0.0',
16
17
  '@basaltkit/generator': '^1.6.0',
17
18
  '@basaltkit/logger': '^1.3.0',
18
- '@basaltkit/mcp': '^5.1.0',
19
- '@basaltkit/prisma': '^3.1.0',
19
+ '@basaltkit/mailer': '^2.1.0',
20
+ '@basaltkit/mcp': '^6.0.0',
21
+ '@basaltkit/prisma': '^4.0.0',
20
22
  '@basaltkit/sdk': '^2.1.0',
21
- '@basaltkit/subscriptions': '^5.0.0',
22
- '@basaltkit/subscriptions-prisma': '^3.0.0',
23
- '@basaltkit/teams': '^4.1.0',
23
+ '@basaltkit/subscriptions': '^6.0.0',
24
+ '@basaltkit/subscriptions-prisma': '^4.0.0',
25
+ '@basaltkit/teams': '^4.1.1',
24
26
  '@basaltkit/teams-prisma': '^1.1.0',
25
- '@basaltkit/tenancy': '^3.2.0',
26
- '@basaltkit/tenancy-prisma': '^2.1.0',
27
- '@basaltkit/testing': '^2.0.0',
28
- 'create-basalt': '^1.12.0',
27
+ '@basaltkit/tenancy': '^4.0.0',
28
+ '@basaltkit/tenancy-prisma': '^3.0.0',
29
+ '@basaltkit/testing': '^3.0.0',
30
+ 'create-basalt': '^1.13.0',
29
31
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-basalt",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "engines": {
5
5
  "node": ">=22.5.0"
6
6
  },
@@ -32,26 +32,26 @@
32
32
  "typescript": "^7.0.2",
33
33
  "vitest": "^5.0.1",
34
34
  "zod": "^4.6.5",
35
- "@basaltkit/auth": "^4.2.0",
36
- "@basaltkit/auth-prisma": "^2.1.0",
37
- "@basaltkit/cli": "^1.4.0",
35
+ "@basaltkit/auth-prisma": "^3.0.0",
36
+ "@basaltkit/cli": "^1.4.1",
37
+ "@basaltkit/config": "^1.1.2",
38
38
  "@basaltkit/core": "^1.6.0",
39
+ "@basaltkit/auth": "^5.0.0",
39
40
  "@basaltkit/events": "^1.3.0",
40
- "@basaltkit/fastify": "^2.6.0",
41
- "@basaltkit/config": "^1.1.2",
41
+ "@basaltkit/env": "^3.1.1",
42
+ "@basaltkit/fastify": "^3.0.0",
43
+ "@basaltkit/mcp": "^6.0.0",
44
+ "@basaltkit/prisma": "^4.0.0",
42
45
  "@basaltkit/generator": "^1.6.0",
43
46
  "@basaltkit/logger": "^1.3.0",
44
- "@basaltkit/env": "^3.1.1",
45
- "@basaltkit/mcp": "^5.1.0",
46
- "@basaltkit/prisma": "^3.1.0",
47
- "@basaltkit/subscriptions": "^5.0.0",
48
- "@basaltkit/subscriptions-prisma": "^3.0.0",
49
- "@basaltkit/teams": "^4.1.0",
50
- "@basaltkit/tenancy": "^3.2.0",
47
+ "@basaltkit/subscriptions": "^6.0.0",
48
+ "@basaltkit/subscriptions-prisma": "^4.0.0",
49
+ "@basaltkit/teams": "^4.1.1",
51
50
  "@basaltkit/teams-prisma": "^1.1.0",
52
- "@basaltkit/tenancy-prisma": "^2.1.0",
53
- "@basaltkit/tsconfig": "^0.24.0",
54
- "@basaltkit/testing": "^2.0.3"
51
+ "@basaltkit/tenancy": "^4.0.0",
52
+ "@basaltkit/tenancy-prisma": "^3.0.0",
53
+ "@basaltkit/testing": "^3.0.0",
54
+ "@basaltkit/tsconfig": "^0.24.0"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public"