create-basalt 1.4.2 → 1.6.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.
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Scaffold-time dependency resolution: a freshly generated app should depend on
3
+ * the LATEST PUBLISHED version of every package, not on the ranges that were
4
+ * current the day this create-basalt release was built.
5
+ *
6
+ * - `@basaltkit/*` packages always take the registry's `latest` dist-tag — the
7
+ * framework's own release line is the whole point of the scaffold.
8
+ * - Third-party packages take `latest` only while it stays on the major the
9
+ * templates are written for (the major of the fallback range in
10
+ * {@link THIRD_PARTY_VERSIONS}). A new breaking major keeps the fallback range
11
+ * and is reported as a notice, so it can never silently produce a broken app.
12
+ * - Third-party packages also respect pnpm's `minimumReleaseAge` (pnpm 11
13
+ * defaults it to one day): `^<latest>` for a version published minutes ago is
14
+ * UNSATISFIABLE under that policy, so the first `pnpm install` would fail. A
15
+ * cheap `HEAD <registry>/<name>` reads the packument's `last-modified` (which
16
+ * is never older than the latest publish): older than the window proves the
17
+ * latest mature and `^<latest>` is written; newer — or no header / a failed
18
+ * probe — keeps the fallback range, from which pnpm picks the newest MATURE
19
+ * version itself. The full packument (the only per-version `time` source) is
20
+ * megabytes for packages like `@types/node`, so it is never fetched; a
21
+ * false "fresh" only costs a lower range floor. `@basaltkit/*` is never
22
+ * probed: the scaffolded pnpm-workspace.yaml excludes that scope.
23
+ * - Any registry failure (offline, timeout, non-2xx, malformed JSON, a bogus
24
+ * version string) keeps the embedded fallback range. The registry never fails
25
+ * a scaffold.
26
+ */
27
+ /**
28
+ * Fallback ranges for every third-party package the templates emit (API +
29
+ * web UI). The major of each range is also the TEMPLATE-COMPATIBLE major: the
30
+ * generated code is written for it, and `latest` is accepted only on that
31
+ * major. Bump a range to a new major only together with the templates.
32
+ */
33
+ export const THIRD_PARTY_VERSIONS = {
34
+ '@tailwindcss/vite': '^4.3.3',
35
+ '@types/node': '^26.6.2',
36
+ '@types/react': '^19.3.0',
37
+ '@types/react-dom': '^19.3.0',
38
+ '@vitejs/plugin-react': '^6.1.1',
39
+ 'pino-pretty': '^13.1.3',
40
+ react: '^19.3.0',
41
+ 'react-dom': '^19.3.0',
42
+ tailwindcss: '^4.3.3',
43
+ tsx: '^4.23.13',
44
+ typescript: '^7.0.2',
45
+ vite: '^8.3.0',
46
+ vitest: '^5.0.1',
47
+ zod: '^4.6.5',
48
+ };
49
+ export const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
50
+ /** pnpm 11's default `minimumReleaseAge`, in minutes (one day). */
51
+ export const DEFAULT_MINIMUM_RELEASE_AGE_MINUTES = 1440;
52
+ const ABBREVIATED_METADATA = 'application/vnd.npm.install-v1+json';
53
+ const PER_REQUEST_TIMEOUT_MS = 5_000;
54
+ const OVERALL_TIMEOUT_MS = 15_000;
55
+ /**
56
+ * Requests in flight at once. A burst of ~35 simultaneous TLS handshakes to one
57
+ * host is markedly slower (and flakier) on poor links than a small pool that
58
+ * reuses keep-alive connections.
59
+ */
60
+ const CONCURRENCY = 8;
61
+ /** A stable `x.y.z` release (pre-releases and build metadata are refused). */
62
+ const STABLE_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
63
+ const CARET_RANGE = /^\^(0|[1-9]\d*)\.\d+\.\d+$/;
64
+ /** Packages whose latest is always taken, regardless of major. */
65
+ export const alwaysLatest = (name) => name.startsWith('@basaltkit/') || name === 'create-basalt';
66
+ /** The major the templates are written for, derived from the fallback range. */
67
+ export function compatibleMajor(fallbackRange) {
68
+ const match = CARET_RANGE.exec(fallbackRange);
69
+ return match ? Number(match[1]) : undefined;
70
+ }
71
+ /**
72
+ * The release-age window in minutes: an explicit option, else the pnpm/npm
73
+ * config env var (a `pnpm create` exports its config), else pnpm 11's default.
74
+ */
75
+ export function minimumReleaseAgeMinutes(explicit, env = process.env) {
76
+ if (explicit !== undefined && Number.isFinite(explicit) && explicit >= 0)
77
+ return explicit;
78
+ for (const key of ['pnpm_config_minimum_release_age', 'npm_config_minimum_release_age']) {
79
+ const raw = env[key];
80
+ if (raw !== undefined && /^\d+$/.test(raw.trim()))
81
+ return Number(raw.trim());
82
+ }
83
+ return DEFAULT_MINIMUM_RELEASE_AGE_MINUTES;
84
+ }
85
+ /**
86
+ * The registry to query: an explicit option, else `npm_config_registry` (npm
87
+ * and pnpm export it to `create` binaries, so a private mirror is honored),
88
+ * else the public registry. Trailing slashes are normalized; anything that is
89
+ * not an http(s) URL falls back to the default.
90
+ */
91
+ export function registryUrl(explicit, env = process.env) {
92
+ const candidate = explicit ?? env['npm_config_registry'] ?? env['NPM_CONFIG_REGISTRY'];
93
+ if (!candidate)
94
+ return DEFAULT_REGISTRY;
95
+ try {
96
+ const url = new URL(candidate);
97
+ if (url.protocol !== 'https:' && url.protocol !== 'http:')
98
+ return DEFAULT_REGISTRY;
99
+ }
100
+ catch {
101
+ return DEFAULT_REGISTRY;
102
+ }
103
+ let end = candidate.length;
104
+ while (end > 0 && candidate[end - 1] === '/')
105
+ end--;
106
+ return candidate.slice(0, end);
107
+ }
108
+ /** `<registry>/<name>/latest`, with a scoped name's `/` encoded as `%2f`. */
109
+ export const latestUrl = (registry, name) => `${registry}/${name.replaceAll('/', '%2f')}/latest`;
110
+ /** `<registry>/<name>` — the packument (probed with HEAD for its `last-modified`). */
111
+ export const packumentUrl = (registry, name) => `${registry}/${name.replaceAll('/', '%2f')}`;
112
+ /**
113
+ * Whether `name`'s latest is provably older than `windowMs`. A packument's
114
+ * `last-modified` is never older than its latest publish, so an old header
115
+ * proves maturity. Age is measured against the registry's own `Date` header
116
+ * when present (immune to local clock skew). Never throws.
117
+ */
118
+ async function probeAge(fetchImpl, registry, name, windowMs, now, signal) {
119
+ try {
120
+ const response = await fetchImpl(packumentUrl(registry, name), {
121
+ method: 'HEAD',
122
+ headers: { accept: ABBREVIATED_METADATA },
123
+ signal,
124
+ });
125
+ if (!response.ok)
126
+ return 'unknown';
127
+ const modified = Date.parse(response.headers.get('last-modified') ?? '');
128
+ if (Number.isNaN(modified))
129
+ return 'unknown';
130
+ const serverNow = Date.parse(response.headers.get('date') ?? '');
131
+ const reference = Number.isNaN(serverNow) ? now() : serverNow;
132
+ return reference - modified >= windowMs ? 'mature' : 'recent';
133
+ }
134
+ catch {
135
+ return 'unknown';
136
+ }
137
+ }
138
+ async function lookupLatest(fetchImpl, registry, name, signal) {
139
+ try {
140
+ const response = await fetchImpl(latestUrl(registry, name), {
141
+ headers: { accept: 'application/json' },
142
+ signal,
143
+ });
144
+ if (response.status >= 500 || response.status === 429)
145
+ return 'transient';
146
+ if (!response.ok)
147
+ return 'miss';
148
+ const body = (await response.json());
149
+ const version = body?.version;
150
+ return typeof version === 'string' && STABLE_SEMVER.test(version) ? { version } : 'miss';
151
+ }
152
+ catch {
153
+ // Network error, per-request timeout, or malformed JSON.
154
+ return 'transient';
155
+ }
156
+ }
157
+ /**
158
+ * The latest stable version of `name`, or undefined. A transient failure
159
+ * (network error, timeout, 5xx/429) is retried once — on slow links the first
160
+ * wave of cold TLS handshakes is the part that times out, and the retry rides
161
+ * a warm keep-alive connection. The overall signal bounds both attempts.
162
+ */
163
+ async function fetchLatest(fetchImpl, registry, name, timeoutMs, overall) {
164
+ for (let attempt = 0; attempt < 2 && !overall.aborted; attempt++) {
165
+ const result = await lookupLatest(fetchImpl, registry, name, AbortSignal.any([AbortSignal.timeout(timeoutMs), overall]));
166
+ if (result === 'miss')
167
+ return undefined;
168
+ if (result !== 'transient')
169
+ return result.version;
170
+ }
171
+ return undefined;
172
+ }
173
+ /**
174
+ * Resolves the latest published version of every package in `fallbacks`
175
+ * (name → fallback range) through a small pool of parallel requests. Never throws: a package the registry
176
+ * cannot answer for keeps its fallback range and is listed in `failed`.
177
+ */
178
+ export async function resolveLatestVersions(fallbacks, options = {}) {
179
+ const fetchImpl = options.fetch ?? globalThis.fetch;
180
+ const registry = registryUrl(options.registry);
181
+ const versions = { ...fallbacks };
182
+ const resolved = [];
183
+ const failed = [];
184
+ const heldBack = [];
185
+ const tooFresh = [];
186
+ const names = Object.keys(fallbacks).sort();
187
+ const windowMs = minimumReleaseAgeMinutes(options.minimumReleaseAge) * 60_000;
188
+ const now = options.now ?? Date.now;
189
+ const timeoutMs = options.timeoutMs ?? PER_REQUEST_TIMEOUT_MS;
190
+ if (typeof fetchImpl !== 'function') {
191
+ return { versions, resolved, failed: names, heldBack, tooFresh, registry };
192
+ }
193
+ /** A third-party latest on the template-compatible major (else undefined). */
194
+ const compatible = (name, version) => {
195
+ if (alwaysLatest(name))
196
+ return true;
197
+ const major = compatibleMajor(fallbacks[name]);
198
+ return major !== undefined && Number(version.split('.')[0]) === major;
199
+ };
200
+ const overall = new AbortController();
201
+ const cap = setTimeout(() => overall.abort(), options.overallTimeoutMs ?? OVERALL_TIMEOUT_MS);
202
+ try {
203
+ const latest = [];
204
+ const age = [];
205
+ let next = 0;
206
+ const worker = async () => {
207
+ while (next < names.length) {
208
+ const i = next++;
209
+ const name = names[i];
210
+ const version = await fetchLatest(fetchImpl, registry, name, timeoutMs, overall.signal);
211
+ latest[i] = version;
212
+ age[i] =
213
+ version === undefined || alwaysLatest(name) || windowMs === 0 || !compatible(name, version)
214
+ ? 'mature'
215
+ : await probeAge(fetchImpl, registry, name, windowMs, now, AbortSignal.any([AbortSignal.timeout(timeoutMs), overall.signal]));
216
+ }
217
+ };
218
+ const pool = Math.max(1, Math.min(options.concurrency ?? CONCURRENCY, names.length));
219
+ await Promise.all(Array.from({ length: pool }, worker));
220
+ names.forEach((name, i) => {
221
+ const version = latest[i];
222
+ const fallback = fallbacks[name];
223
+ if (version === undefined) {
224
+ failed.push(name);
225
+ return;
226
+ }
227
+ if (!compatible(name, version)) {
228
+ heldBack.push({ name, latest: version, range: fallback });
229
+ return;
230
+ }
231
+ const verdict = age[i];
232
+ if (verdict === 'recent' || verdict === 'unknown') {
233
+ tooFresh.push({ name, latest: version, range: fallback, reason: verdict });
234
+ return;
235
+ }
236
+ versions[name] = `^${version}`;
237
+ resolved.push(name);
238
+ });
239
+ }
240
+ finally {
241
+ clearTimeout(cap);
242
+ }
243
+ return { versions, resolved, failed, heldBack, tooFresh, registry };
244
+ }
245
+ /**
246
+ * Rewrites the dependency ranges of a generated package.json with the
247
+ * resolved versions (keys, order and every other field untouched).
248
+ */
249
+ export function applyVersions(packageJsonText, versions) {
250
+ const pkg = JSON.parse(packageJsonText);
251
+ for (const field of ['dependencies', 'devDependencies']) {
252
+ const deps = pkg[field];
253
+ if (!deps)
254
+ continue;
255
+ for (const name of Object.keys(deps)) {
256
+ const range = versions[name];
257
+ if (range !== undefined)
258
+ deps[name] = range;
259
+ }
260
+ }
261
+ return `${JSON.stringify(pkg, null, 2)}\n`;
262
+ }
263
+ /** Every dependency/devDependency name → range declared in the given package.json texts. */
264
+ export function collectDependencies(packageJsonTexts) {
265
+ const all = {};
266
+ for (const text of packageJsonTexts) {
267
+ const pkg = JSON.parse(text);
268
+ Object.assign(all, pkg.dependencies, pkg.devDependencies);
269
+ }
270
+ return all;
271
+ }
272
+ /** Human-readable lines summarizing a resolution (for the CLI). */
273
+ export function describeResolution(resolution) {
274
+ const lines = [];
275
+ if (resolution.resolved.length > 0) {
276
+ lines.push(`Resolved ${resolution.resolved.length} dependencies to their latest published versions:`, ` ${resolution.resolved.map((name) => `${name}@${resolution.versions[name]}`).join(', ')}`);
277
+ }
278
+ for (const held of resolution.heldBack) {
279
+ lines.push(`Note: ${held.name} ${held.latest} is a new major this template does not target yet — kept ${held.range}.`);
280
+ }
281
+ const recent = resolution.tooFresh.filter((entry) => entry.reason === 'recent');
282
+ const unknown = resolution.tooFresh.filter((entry) => entry.reason === 'unknown');
283
+ for (const fresh of recent) {
284
+ lines.push(`Note: ${fresh.name} ${fresh.latest} was published inside pnpm's minimumReleaseAge window — kept ${fresh.range} (pnpm installs the newest mature version).`);
285
+ }
286
+ if (unknown.length > 0) {
287
+ lines.push(`Note: could not prove the release age of ${unknown.map((entry) => `${entry.name}@${entry.latest}`).join(', ')} — kept the bundled range(s) so minimumReleaseAge cannot block the install.`);
288
+ }
289
+ if (resolution.failed.length > 0) {
290
+ lines.push(`Warning: could not reach ${resolution.registry} for ${resolution.failed.length} package(s) — used the bundled fallback ranges for: ${resolution.failed.join(', ')}.`);
291
+ }
292
+ return lines;
293
+ }
@@ -1,4 +1,4 @@
1
- import type { ProjectOptions } from './templates.js';
1
+ import { type ProjectOptions } from './templates.js';
2
2
  /**
3
3
  * The `web/` frontend emitted when `--ui` is passed: a Vite + React app on
4
4
  * @basaltkit/admin-shadcn (authentic shadcn/ui components) talking to the API
@@ -1,5 +1,4 @@
1
- /** Kept in sync with the monorepo release line (mirror of templates.ts). */
2
- const BASALT_VERSION = '^1.0.0';
1
+ import { thirdPartyVersionOf, versionOf } from './templates.js';
3
2
  /**
4
3
  * The `web/` frontend emitted when `--ui` is passed: a Vite + React app on
5
4
  * @basaltkit/admin-shadcn (authentic shadcn/ui components) talking to the API
@@ -13,8 +12,6 @@ export function uiFiles(options) {
13
12
  'web/vite.config.ts': webViteConfig(),
14
13
  'web/tsconfig.json': webTsconfig(),
15
14
  'web/index.html': webIndexHtml(options),
16
- 'web/postcss.config.js': webPostcss(),
17
- 'web/tailwind.config.js': webTailwind(),
18
15
  'web/src/main.tsx': webMain(),
19
16
  'web/src/crypto-shim.ts': webCryptoShim(),
20
17
  'web/src/index.css': webCss(),
@@ -27,34 +24,38 @@ function webPackageJson(options) {
27
24
  name: `${options.name}-web`,
28
25
  private: true,
29
26
  type: 'module',
30
- scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
27
+ scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview', typecheck: 'tsc --noEmit' },
31
28
  dependencies: {
32
- '@basaltkit/admin': BASALT_VERSION,
33
- '@basaltkit/admin-shadcn': BASALT_VERSION,
34
- '@basaltkit/sdk': BASALT_VERSION,
35
- react: '^18.3.1',
36
- 'react-dom': '^18.3.1',
37
- zod: '^3.24.0',
38
- },
39
- devDependencies: {
40
- '@types/react': '^18.3.0',
41
- '@types/react-dom': '^18.3.0',
42
- '@vitejs/plugin-react': '^4.3.0',
43
- autoprefixer: '^10.4.0',
44
- postcss: '^8.4.0',
45
- tailwindcss: '^3.4.0',
46
- typescript: '^5.8.0',
47
- vite: '^6.0.0',
29
+ '@basaltkit/admin': versionOf('@basaltkit/admin'),
30
+ '@basaltkit/admin-shadcn': versionOf('@basaltkit/admin-shadcn'),
31
+ '@basaltkit/sdk': versionOf('@basaltkit/sdk'),
32
+ react: thirdPartyVersionOf('react'),
33
+ 'react-dom': thirdPartyVersionOf('react-dom'),
34
+ // Matches the peer every @basaltkit package declares (zod 4).
35
+ zod: thirdPartyVersionOf('zod'),
48
36
  },
37
+ // Tailwind 4 runs as a Vite plugin (@tailwindcss/vite) — no PostCSS or
38
+ // autoprefixer config; the theme lives in src/index.css.
39
+ devDependencies: Object.fromEntries([
40
+ '@tailwindcss/vite',
41
+ '@types/node',
42
+ '@types/react',
43
+ '@types/react-dom',
44
+ '@vitejs/plugin-react',
45
+ 'tailwindcss',
46
+ 'typescript',
47
+ 'vite',
48
+ ].map((pkg) => [pkg, thirdPartyVersionOf(pkg)])),
49
49
  }, null, 2)}\n`;
50
50
  }
51
51
  function webViteConfig() {
52
52
  return `import { fileURLToPath } from 'node:url'
53
+ import tailwindcss from '@tailwindcss/vite'
53
54
  import react from '@vitejs/plugin-react'
54
55
  import { defineConfig } from 'vite'
55
56
 
56
57
  export default defineConfig({
57
- plugins: [react()],
58
+ plugins: [react(), tailwindcss()],
58
59
  resolve: {
59
60
  alias: {
60
61
  // @basaltkit/admin uses randomUUID from crypto; map it to Web Crypto in the browser.
@@ -89,7 +90,9 @@ function webTsconfig() {
89
90
  noEmit: true,
90
91
  allowImportingTsExtensions: true,
91
92
  verbatimModuleSyntax: true,
92
- types: ['node'],
93
+ // vite/client types the CSS/asset imports (TS 6+ checks side-effect
94
+ // imports such as `import './index.css'`) and import.meta.env.
95
+ types: ['node', 'vite/client'],
93
96
  },
94
97
  include: ['src'],
95
98
  }, null, 2)}\n`;
@@ -109,41 +112,6 @@ function webIndexHtml(options) {
109
112
  </html>
110
113
  `;
111
114
  }
112
- function webPostcss() {
113
- return `export default {
114
- plugins: {
115
- tailwindcss: {},
116
- autoprefixer: {},
117
- },
118
- }
119
- `;
120
- }
121
- function webTailwind() {
122
- return `export default {
123
- darkMode: ['class'],
124
- content: ['./index.html', './src/**/*.{ts,tsx}', './node_modules/@basaltkit/admin-shadcn/dist/**/*.js'],
125
- theme: {
126
- extend: {
127
- colors: {
128
- border: 'hsl(var(--border))',
129
- input: 'hsl(var(--input))',
130
- ring: 'hsl(var(--ring))',
131
- background: 'hsl(var(--background))',
132
- foreground: 'hsl(var(--foreground))',
133
- primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' },
134
- secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))' },
135
- destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))' },
136
- muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))' },
137
- accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))' },
138
- card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))' },
139
- },
140
- borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' },
141
- },
142
- },
143
- plugins: [],
144
- }
145
- `;
146
- }
147
115
  function webMain() {
148
116
  return `import { StrictMode } from 'react'
149
117
  import { createRoot } from 'react-dom/client'
@@ -162,9 +130,39 @@ function webCryptoShim() {
162
130
  `;
163
131
  }
164
132
  function webCss() {
165
- return `@tailwind base;
166
- @tailwind components;
167
- @tailwind utilities;
133
+ return `@import 'tailwindcss';
134
+
135
+ /* @basaltkit/admin-shadcn ships precompiled components: let Tailwind scan them
136
+ (paths are relative to this file, so ../node_modules is web/node_modules). */
137
+ @source '../node_modules/@basaltkit/admin-shadcn/dist';
138
+
139
+ /* Class-based dark mode: <html class="dark"> (the theme toggle sets it). */
140
+ @custom-variant dark (&:is(.dark *));
141
+
142
+ /* shadcn/ui tokens → Tailwind utilities (bg-primary, text-muted-foreground,
143
+ border-input, rounded-lg, …). The values are the CSS variables below. */
144
+ @theme inline {
145
+ --color-border: hsl(var(--border));
146
+ --color-input: hsl(var(--input));
147
+ --color-ring: hsl(var(--ring));
148
+ --color-background: hsl(var(--background));
149
+ --color-foreground: hsl(var(--foreground));
150
+ --color-primary: hsl(var(--primary));
151
+ --color-primary-foreground: hsl(var(--primary-foreground));
152
+ --color-secondary: hsl(var(--secondary));
153
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
154
+ --color-destructive: hsl(var(--destructive));
155
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
156
+ --color-muted: hsl(var(--muted));
157
+ --color-muted-foreground: hsl(var(--muted-foreground));
158
+ --color-accent: hsl(var(--accent));
159
+ --color-accent-foreground: hsl(var(--accent-foreground));
160
+ --color-card: hsl(var(--card));
161
+ --color-card-foreground: hsl(var(--card-foreground));
162
+ --radius-lg: var(--radius);
163
+ --radius-md: calc(var(--radius) - 2px);
164
+ --radius-sm: calc(var(--radius) - 4px);
165
+ }
168
166
 
169
167
  @layer base {
170
168
  :root {
@@ -206,9 +204,19 @@ function webCss() {
206
204
  --input: 240 3.7% 18%;
207
205
  --ring: 243 75% 68%;
208
206
  }
209
- * {
207
+ /* Tailwind 4 defaults borders to currentColor; keep the themed border. */
208
+ *,
209
+ ::after,
210
+ ::before,
211
+ ::backdrop,
212
+ ::file-selector-button {
210
213
  border-color: hsl(var(--border));
211
214
  }
215
+ /* Tailwind 4 dropped the pointer cursor on buttons; keep it. */
216
+ button:not(:disabled),
217
+ [role='button']:not(:disabled) {
218
+ cursor: pointer;
219
+ }
212
220
  body {
213
221
  margin: 0;
214
222
  background-color: hsl(var(--background));
@@ -10,15 +10,44 @@ export interface ProjectOptions {
10
10
  /** Expose opted-in routes as MCP tools (@basaltkit/mcp) over HTTP at `/mcp`. */
11
11
  mcp: boolean;
12
12
  }
13
- /** The dependency range for a @basalt package (override, else the base line). */
13
+ export { SCAFFOLD_VERSIONS } from './versions.js';
14
+ export { THIRD_PARTY_VERSIONS } from './latest-versions.js';
15
+ /**
16
+ * The dependency range for a @basaltkit/* package: the current release line
17
+ * (`^<major>.<minor>.0`) of its workspace version, from the generated
18
+ * {@link SCAFFOLD_VERSIONS} map. Throws for an unknown package rather than
19
+ * guessing a range — a stale guess once pinned new apps to frozen 1.x lines.
20
+ */
14
21
  export declare const versionOf: (pkg: string) => string;
22
+ /**
23
+ * The fallback range for a third-party package the templates emit, from
24
+ * {@link THIRD_PARTY_VERSIONS}. At scaffold time the CLI replaces it with
25
+ * `^<latest>` when the registry's latest is on the same major. Throws for an
26
+ * unknown package so every emitted range lives in that single table.
27
+ */
28
+ export declare const thirdPartyVersionOf: (pkg: string) => string;
29
+ /** True when the scaffold binds users to the tenant they act on (tenancy + auth). */
30
+ export declare const enforcesMembership: (options: ProjectOptions) => boolean;
15
31
  export declare function packageJson(options: ProjectOptions): string;
16
32
  export declare function tsconfigJson(): string;
17
33
  export declare function envTs(options: ProjectOptions): string;
34
+ /**
35
+ * An app-specific env-var prefix derived from the project name
36
+ * (`my-saas` → `MY_SAAS`), suggested for variables whose generic names
37
+ * (`DATABASE_URL`, …) other projects export too.
38
+ */
39
+ export declare function envPrefix(name: string): string;
18
40
  export declare function envExample(options: ProjectOptions): string;
19
41
  export declare function appTs(options: ProjectOptions): string;
20
42
  export declare function routesTs(options: ProjectOptions): string;
21
43
  export declare function serverTs(): string;
44
+ /**
45
+ * `pnpm dev` entrypoint. Opts into NODE_ENV=development (dev secret defaults,
46
+ * pretty logs) unless NODE_ENV is already set, then starts the server. Kept out
47
+ * of server.ts on purpose: `pnpm start` runs server.ts directly, where an unset
48
+ * NODE_ENV counts as production and secrets are required (fail-closed).
49
+ */
50
+ export declare function devTs(): string;
22
51
  export declare function basaltBin(): string;
23
52
  export declare function appTest(options: ProjectOptions): string;
24
53
  export declare function readme(options: ProjectOptions): string;
@@ -28,11 +57,25 @@ export declare function readme(options: ProjectOptions): string;
28
57
  * project root automatically; Claude Desktop uses the same server entry.
29
58
  */
30
59
  export declare function mcpJson(_options: ProjectOptions): string;
60
+ /**
61
+ * Keeps secrets and local state out of Docker build contexts, so a
62
+ * `COPY . .` can never bake `.env` or private keys into an image layer. Every
63
+ * rule is `**`-prefixed: .dockerignore patterns are anchored at the context
64
+ * root, so a bare `.env` or `*.pem` would still let `prisma/.env` or
65
+ * `certs/server.key` through.
66
+ */
67
+ export declare function dockerignore(): string;
31
68
  export declare function gitignore(): string;
32
69
  /**
33
70
  * pnpm settings: esbuild's build script is required by tsx;
34
71
  * msgpackr-extract (optional native accelerator via BullMQ) is declined —
35
72
  * msgpackr falls back to pure JS. With --ui, `web` is a workspace member so
36
73
  * its own dependencies resolve.
74
+ *
75
+ * pnpm 11 notes (BK-002), spelled out in the file because both bite silently:
76
+ * `minimumReleaseAgeExclude` is evaluated first-match-wins BY PACKAGE NAME, so
77
+ * per-version exclusions of one package must be a single `||` union entry; and
78
+ * `verifyDepsBeforeRun` defaults to `install`, so any `pnpm <script>` may run
79
+ * `pnpm install` first — `warn` is offered commented out, never set for you.
37
80
  */
38
81
  export declare function pnpmWorkspaceYaml(options: ProjectOptions): string;