create-basalt 1.2.1 → 1.2.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.
@@ -0,0 +1,30 @@
1
+ export interface ProjectOptions {
2
+ name: string;
3
+ tenancy: boolean;
4
+ auth: boolean;
5
+ billing: boolean;
6
+ /** Scaffold a web/ frontend (React + shadcn + SDK). */
7
+ ui: boolean;
8
+ /** Scaffold the `basalt` CLI entrypoint (code generators + built-in commands). */
9
+ cli: boolean;
10
+ }
11
+ /** The dependency range for a @basalt package (override, else the base line). */
12
+ export declare const versionOf: (pkg: string) => string;
13
+ export declare function packageJson(options: ProjectOptions): string;
14
+ export declare function tsconfigJson(): string;
15
+ export declare function envTs(options: ProjectOptions): string;
16
+ export declare function envExample(options: ProjectOptions): string;
17
+ export declare function appTs(options: ProjectOptions): string;
18
+ export declare function routesTs(options: ProjectOptions): string;
19
+ export declare function serverTs(): string;
20
+ export declare function basaltBin(): string;
21
+ export declare function appTest(options: ProjectOptions): string;
22
+ export declare function readme(options: ProjectOptions): string;
23
+ export declare function gitignore(): string;
24
+ /**
25
+ * pnpm settings: esbuild's build script is required by tsx;
26
+ * msgpackr-extract (optional native accelerator via BullMQ) is declined —
27
+ * msgpackr falls back to pure JS. With --ui, `web` is a workspace member so
28
+ * its own dependencies resolve.
29
+ */
30
+ export declare function pnpmWorkspaceYaml(options: ProjectOptions): string;
@@ -0,0 +1,386 @@
1
+ /** Base release line for @basaltkit/* deps. */
2
+ const BASALT_VERSION = '^1.0.0';
3
+ /**
4
+ * Per-package range overrides. In semver 0.x, `^0.4.0` locks the minor, so a
5
+ * package that advances past the base line needs its own range or a fresh app
6
+ * can't install it. Add an entry when a package crosses a minor.
7
+ */
8
+ const VERSIONS = {};
9
+ /** The dependency range for a @basalt package (override, else the base line). */
10
+ export const versionOf = (pkg) => VERSIONS[pkg] ?? BASALT_VERSION;
11
+ export function packageJson(options) {
12
+ const dependencies = {
13
+ '@basaltkit/config': BASALT_VERSION,
14
+ '@basaltkit/core': BASALT_VERSION,
15
+ '@basaltkit/env': BASALT_VERSION,
16
+ '@basaltkit/events': BASALT_VERSION,
17
+ '@basaltkit/fastify': BASALT_VERSION,
18
+ '@basaltkit/logger': BASALT_VERSION,
19
+ zod: '^4.0.0',
20
+ };
21
+ if (options.tenancy)
22
+ dependencies['@basaltkit/tenancy'] = BASALT_VERSION;
23
+ if (options.auth)
24
+ dependencies['@basaltkit/auth'] = BASALT_VERSION;
25
+ if (options.billing)
26
+ dependencies['@basaltkit/subscriptions'] = BASALT_VERSION;
27
+ if (options.cli) {
28
+ // Runtime deps: app.ts uses commandsPlugin (@basaltkit/cli); prisma powers
29
+ // prismaPlugin + `basalt prisma:sync`. @basaltkit/generator is dev-only (below).
30
+ dependencies['@basaltkit/cli'] = BASALT_VERSION;
31
+ dependencies['@basaltkit/prisma'] = BASALT_VERSION;
32
+ }
33
+ // Apply per-package range overrides. Only @basaltkit/* packages: versionOf
34
+ // falls back to BASALT_VERSION, which would clobber third-party ranges
35
+ // (this once rewrote zod's range to the @basalt base line).
36
+ for (const pkg of Object.keys(dependencies)) {
37
+ if (pkg.startsWith('@basaltkit/'))
38
+ dependencies[pkg] = versionOf(pkg);
39
+ }
40
+ const devDependencies = {
41
+ '@basaltkit/testing': versionOf('@basaltkit/testing'),
42
+ '@types/node': '^22.15.0',
43
+ 'pino-pretty': '^13.0.0',
44
+ tsx: '^4.19.0',
45
+ typescript: '^5.8.0',
46
+ vitest: '^3.1.0',
47
+ };
48
+ if (options.cli) {
49
+ // Dev-only: only bin/basalt.ts imports the code generator — the runtime server
50
+ // never does, so the app runs completely without the codegen/AI layer.
51
+ devDependencies['@basaltkit/generator'] = versionOf('@basaltkit/generator');
52
+ }
53
+ return `${JSON.stringify({
54
+ name: options.name,
55
+ version: '0.1.0',
56
+ private: true,
57
+ type: 'module',
58
+ scripts: {
59
+ dev: 'tsx watch src/server.ts',
60
+ start: 'tsx src/server.ts',
61
+ test: 'vitest run',
62
+ typecheck: 'tsc --noEmit',
63
+ ...(options.cli ? { basalt: 'tsx bin/basalt.ts' } : {}),
64
+ },
65
+ dependencies: Object.fromEntries(Object.entries(dependencies).sort()),
66
+ devDependencies: Object.fromEntries(Object.entries(devDependencies).sort()),
67
+ }, null, 2)}\n`;
68
+ }
69
+ export function tsconfigJson() {
70
+ return `${JSON.stringify({
71
+ compilerOptions: {
72
+ target: 'ES2022',
73
+ module: 'NodeNext',
74
+ moduleResolution: 'NodeNext',
75
+ strict: true,
76
+ noUncheckedIndexedAccess: true,
77
+ verbatimModuleSyntax: true,
78
+ skipLibCheck: true,
79
+ noEmit: true,
80
+ types: ['node'],
81
+ },
82
+ include: ['src', 'tests'],
83
+ }, null, 2)}\n`;
84
+ }
85
+ export function envTs(options) {
86
+ return `import { defineEnv${options.auth ? ', secret' : ''} } from '@basaltkit/env'
87
+ import { z } from 'zod'
88
+
89
+ export const env = defineEnv({
90
+ PORT: z.coerce.number().default(3000),
91
+ HOST: z.string().default('0.0.0.0'),
92
+ LOG_LEVEL: z.string().default('info'),
93
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),${options.auth
94
+ ? `
95
+ // Signs JWTs and sessions. secret() is fail-closed: required in production
96
+ // (no fallback), rejected if it looks like a placeholder. Dev uses devDefault.
97
+ APP_SECRET: secret({ minLength: 32, devDefault: 'dev-only-insecure-secret-please-change-me' }),`
98
+ : ''}
99
+ })
100
+ `;
101
+ }
102
+ export function envExample(options) {
103
+ return `PORT=3000
104
+ HOST=0.0.0.0
105
+ LOG_LEVEL=info
106
+ NODE_ENV=development
107
+ ${options.auth ? '# Required in production (the app refuses to boot without it).\n# Generate a strong one: openssl rand -base64 48\n# APP_SECRET=\n' : ''}`;
108
+ }
109
+ export function appTs(options) {
110
+ const imports = [
111
+ `import { createApp } from '@basaltkit/core'`,
112
+ `import { configPlugin } from '@basaltkit/config'`,
113
+ `import { eventsPlugin } from '@basaltkit/events'`,
114
+ `import { fastifyPlugin, securityPlugin } from '@basaltkit/fastify'`,
115
+ `import { loggerPlugin } from '@basaltkit/logger'`,
116
+ ];
117
+ const plugins = [
118
+ `configPlugin({ app: { name: '${options.name}' } })`,
119
+ `loggerPlugin({ level: options.logLevel ?? 'info', ...(options.pretty ? { pretty: true } : {}) })`,
120
+ `eventsPlugin()`,
121
+ `securityPlugin({
122
+ // Secure response headers (HSTS, X-Frame-Options, nosniff, …) are on by
123
+ // default. Turn on rate limiting and a CORS allow-list for production:
124
+ // rateLimit: { limit: 120, windowMs: 60_000 },
125
+ // cors: { origin: ['https://app.example.com'], credentials: true },
126
+ })`,
127
+ ];
128
+ let routesExpression = 'appRoutes';
129
+ if (options.tenancy) {
130
+ imports.push(`import { headerResolver, MemoryTenantSource, subdomainResolver, tenancyPlugin } from '@basaltkit/tenancy'`);
131
+ plugins.push(`tenancyPlugin({
132
+ // Replace MemoryTenantSource with your database-backed source.
133
+ source: new MemoryTenantSource().add({ id: 'demo', name: 'Demo Tenant' }),
134
+ resolvers: [headerResolver(), subdomainResolver({ base: 'localhost' })],
135
+ })`);
136
+ }
137
+ if (options.auth) {
138
+ imports.push(`import { authPlugin, authRoutes, mfaRoutes, MemoryUserSource } from '@basaltkit/auth'`);
139
+ imports.push(`import { env } from './env.js'`);
140
+ plugins.push(`authPlugin({
141
+ // Replace MemoryUserSource with your database-backed source (e.g.
142
+ // @basaltkit/auth-sqlite or @basaltkit/auth-prisma). The default in-memory
143
+ // MFA store is enough for the ready-made TOTP flow below.
144
+ users: new MemoryUserSource(),
145
+ secret: env.APP_SECRET,
146
+ })`);
147
+ // authRoutes(): register, login, logout, refresh, me, email verification and
148
+ // password recovery. mfaRoutes(): TOTP enroll/activate/status/disable.
149
+ routesExpression = '[...appRoutes, ...authRoutes(), ...mfaRoutes()]';
150
+ }
151
+ if (options.billing) {
152
+ imports.push(`import { definePlans, subscriptionsPlugin } from '@basaltkit/subscriptions'`);
153
+ plugins.push(`subscriptionsPlugin({
154
+ plans: definePlans({
155
+ free: { price: 0, features: { projects: 3 } },
156
+ pro: { price: { monthly: 29, yearly: 290 }, trial: '14d', features: { projects: 50 } },
157
+ }),
158
+ fallbackPlan: 'free',
159
+ })`);
160
+ }
161
+ if (options.cli) {
162
+ // Only the commandsPlugin lives here. The dev tools (@basaltkit/generator, and
163
+ // @basaltkit/ai if installed) are imported ONLY by bin/basalt.ts and passed via
164
+ // \`options.commands\`, so the runtime server never imports them — the SaaS runs
165
+ // completely without the AI/codegen layer. Built-ins (routes, schedule:list) come free.
166
+ imports.push(`import { commandsPlugin, type CommandDefinition } from '@basaltkit/cli'`);
167
+ plugins.push(`...(options.commands && options.commands.length > 0 ? [commandsPlugin(options.commands)] : [])`);
168
+ }
169
+ plugins.push(`fastifyPlugin({ routes: ${routesExpression} })`);
170
+ const commandsField = options.cli
171
+ ? `\n /** Dev/CLI commands (make:*, ai:*, prisma:sync) — passed ONLY by bin/basalt.ts. */\n commands?: CommandDefinition[]`
172
+ : '';
173
+ return `${imports.join('\n')}
174
+ import { appRoutes } from './routes.js'
175
+
176
+ export interface BuildAppOptions {
177
+ logLevel?: string
178
+ pretty?: boolean${commandsField}
179
+ }
180
+
181
+ export function buildApp(options: BuildAppOptions = {}) {
182
+ return createApp({
183
+ plugins: [
184
+ ${plugins.join(',\n ')},
185
+ ],
186
+ })
187
+ }
188
+ `;
189
+ }
190
+ export function routesTs(options) {
191
+ const endpoints = [
192
+ "'GET /'",
193
+ "'GET /health'",
194
+ ...(options.auth
195
+ ? [
196
+ "'POST /auth/register'",
197
+ "'POST /auth/login'",
198
+ "'POST /auth/refresh'",
199
+ "'POST /auth/logout'",
200
+ "'GET /auth/me'",
201
+ ]
202
+ : []),
203
+ ];
204
+ return `import { ctx } from '@basaltkit/core'
205
+ import { route } from '@basaltkit/fastify'
206
+
207
+ export const appRoutes = [
208
+ // Friendly index so \`GET /\` is never a bare 404 — lists what the API exposes.
209
+ route({
210
+ method: 'GET',
211
+ url: '/',
212
+ async handler() {
213
+ return {
214
+ name: '${options.name}',
215
+ status: 'ok',
216
+ endpoints: [${endpoints.join(', ')}],
217
+ }
218
+ },
219
+ }),
220
+ route({
221
+ method: 'GET',
222
+ url: '/health',
223
+ async handler() {
224
+ return { ok: true, requestId: ctx().requestId${options.tenancy ? ', tenant: ctx().tenant?.id ?? null' : ''} }
225
+ },
226
+ }),
227
+ ]
228
+ `;
229
+ }
230
+ export function serverTs() {
231
+ return `import { FASTIFY } from '@basaltkit/fastify'
232
+ import { buildApp } from './app.js'
233
+ import { env } from './env.js'
234
+
235
+ const app = await buildApp({
236
+ logLevel: env.LOG_LEVEL,
237
+ pretty: env.NODE_ENV === 'development',
238
+ }).boot()
239
+
240
+ const server = app.container.get(FASTIFY)
241
+ await server.listen({ port: env.PORT, host: env.HOST })
242
+ console.log('ready at http://' + env.HOST + ':' + env.PORT)
243
+
244
+ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
245
+ process.once(signal, async () => {
246
+ await app.shutdown()
247
+ process.exit(0)
248
+ })
249
+ }
250
+ `;
251
+ }
252
+ export function basaltBin() {
253
+ return `#!/usr/bin/env node
254
+ import { runCli } from '@basaltkit/cli'
255
+ import { generatorCommands } from '@basaltkit/generator'
256
+ import { prismaSyncCommand } from '@basaltkit/prisma'
257
+ import { buildApp } from '../src/app.js'
258
+
259
+ // The 'basalt' CLI: boots the app WITH the dev/CLI commands, runs one, shuts down.
260
+ // The dev tools (@basaltkit/generator; add @basaltkit/ai for ai:*) are imported
261
+ // ONLY here — the runtime server (src/server.ts) never loads them, so the SaaS
262
+ // runs without the codegen/AI layer.
263
+ // pnpm basalt dev — dev server: route table + watch (--worker for a queue worker)
264
+ // pnpm basalt list — show available commands
265
+ // pnpm basalt routes — list registered HTTP routes
266
+ // pnpm basalt make:resource Project — generate a full resource vertical
267
+ // pnpm basalt make:service Project — generate a single artifact (schema/service/…)
268
+ const app = buildApp({
269
+ logLevel: 'silent',
270
+ commands: [...generatorCommands(), prismaSyncCommand()],
271
+ })
272
+ process.exit(await runCli({ app }))
273
+ `;
274
+ }
275
+ export function appTest(options) {
276
+ return `import { describe, expect, it } from 'vitest'
277
+ import { FASTIFY } from '@basaltkit/fastify'
278
+ import { buildApp } from '../src/app.js'
279
+
280
+ describe('app', () => {
281
+ it('boots and responds on /health', async () => {
282
+ const app = await buildApp({ logLevel: 'silent' }).boot()
283
+ const server = app.container.get(FASTIFY)
284
+
285
+ const index = await server.inject({ method: 'GET', url: '/' })
286
+ expect(index.statusCode).toBe(200)
287
+ expect(index.json().name).toBe('${options.name}')
288
+
289
+ const response = await server.inject({ method: 'GET', url: '/health' })
290
+ expect(response.statusCode).toBe(200)
291
+ expect(response.json().ok).toBe(true)${options.tenancy
292
+ ? `
293
+
294
+ const tenant = await server.inject({
295
+ method: 'GET',
296
+ url: '/health',
297
+ headers: { 'x-tenant-id': 'demo' },
298
+ })
299
+ expect(tenant.json().tenant).toBe('demo')`
300
+ : ''}
301
+ await app.shutdown()
302
+ })
303
+ })
304
+ `;
305
+ }
306
+ export function readme(options) {
307
+ const features = [
308
+ 'typed routes with Zod validation',
309
+ 'structured logging with request/tenant context',
310
+ 'typed domain events',
311
+ ...(options.tenancy ? ['multi-tenancy (header + subdomain resolvers)'] : []),
312
+ ...(options.auth ? ['authentication (register/login/refresh/me)'] : []),
313
+ ...(options.billing ? ['subscriptions with plans and feature limits'] : []),
314
+ ...(options.ui ? ['web UI (React + shadcn/ui on @basaltkit/admin-shadcn + @basaltkit/sdk)'] : []),
315
+ ...(options.cli ? ['`basalt` CLI with code generators (`make:*`) and built-in commands'] : []),
316
+ ];
317
+ return `# ${options.name}
318
+
319
+ A SaaS app scaffolded with [Basalt](https://github.com/basaltkit/basalt).
320
+
321
+ Included: ${features.join(' · ')}.
322
+
323
+ ## Getting started
324
+
325
+ \`\`\`bash
326
+ pnpm install
327
+ pnpm dev # API on http://localhost:3000
328
+ pnpm test
329
+ \`\`\`
330
+ ${options.cli
331
+ ? `
332
+ ## The \`basalt\` CLI
333
+
334
+ \`\`\`bash
335
+ pnpm basalt list # available commands
336
+ pnpm basalt routes # registered HTTP routes
337
+ pnpm basalt make:resource Project # schema → repository → service → plugin → routes → test
338
+ pnpm basalt make:service Project # a single artifact (--force to overwrite)
339
+ \`\`\`
340
+
341
+ Generated resources land in \`src/modules/<name>/\`. Register the generated
342
+ plugin in \`src/app.ts\` to wire it up.
343
+ `
344
+ : ''}${options.ui
345
+ ? `
346
+ ## Web UI
347
+
348
+ \`\`\`bash
349
+ pnpm dev # terminal 1 — API on :3000
350
+ pnpm --filter ${options.name}-web dev # terminal 2 — UI on http://localhost:5180
351
+ \`\`\`
352
+
353
+ Open <http://localhost:5180>. The Vite dev server proxies \`/api\` to the API,
354
+ so there is no CORS to configure${options.auth ? '. Register, then sign in' : ''}.
355
+ `
356
+ : ''}`;
357
+ }
358
+ export function gitignore() {
359
+ return `node_modules/
360
+ dist/
361
+ .env
362
+ .env.*
363
+ !.env.example
364
+ *.log
365
+ `;
366
+ }
367
+ /**
368
+ * pnpm settings: esbuild's build script is required by tsx;
369
+ * msgpackr-extract (optional native accelerator via BullMQ) is declined —
370
+ * msgpackr falls back to pure JS. With --ui, `web` is a workspace member so
371
+ * its own dependencies resolve.
372
+ */
373
+ export function pnpmWorkspaceYaml(options) {
374
+ return `${options.ui
375
+ ? `packages:
376
+ - web
377
+ `
378
+ : ''}allowBuilds:
379
+ esbuild: true
380
+ msgpackr-extract: false
381
+ # @basaltkit/* releases in lockstep, often within hours — exclude the scope from
382
+ # pnpm's minimumReleaseAge policy so \`pnpm up\` is never blocked on a fresh release.
383
+ minimumReleaseAgeExclude:
384
+ - '@basaltkit/*'
385
+ `;
386
+ }
@@ -0,0 +1,39 @@
1
+ import type { PackageManager } from './index.js';
2
+ import { type Choice, type Prompter } from './prompt.js';
3
+ export type FeatureKey = 'tenancy' | 'auth' | 'billing' | 'ui' | 'cli';
4
+ /** The feature toggles offered in the "custom" preset (and shown in the summary). */
5
+ export declare const FEATURES: Choice<FeatureKey>[];
6
+ interface Preset {
7
+ value: string;
8
+ label: string;
9
+ hint: string;
10
+ /** null → let the user pick features (custom). */
11
+ features: FeatureKey[] | null;
12
+ }
13
+ export declare const PRESETS: Preset[];
14
+ /** Validates a project name as an npm-installable package name. */
15
+ export declare function validateProjectName(name: string): string | undefined;
16
+ export interface WizardResult {
17
+ name: string;
18
+ tenancy: boolean;
19
+ auth: boolean;
20
+ billing: boolean;
21
+ ui: boolean;
22
+ cli: boolean;
23
+ pm: PackageManager;
24
+ install: boolean;
25
+ git: boolean;
26
+ }
27
+ export interface WizardOptions {
28
+ /** Pre-filled name (from argv) — skips the name prompt when valid. */
29
+ name?: string;
30
+ /** Detected package manager, used as the default selection. */
31
+ defaultPm?: PackageManager;
32
+ }
33
+ /**
34
+ * The interactive scaffolding flow. Pure with respect to I/O — it drives the
35
+ * injected {@link Prompter}, so tests script the answers and assert the result.
36
+ * Throws {@link WizardCancelledError} if the user declines the final confirm.
37
+ */
38
+ export declare function runWizard(prompter: Prompter, options?: WizardOptions): Promise<WizardResult>;
39
+ export {};
package/dist/wizard.js ADDED
@@ -0,0 +1,95 @@
1
+ import { WizardCancelledError } from './prompt.js';
2
+ /** The feature toggles offered in the "custom" preset (and shown in the summary). */
3
+ export const FEATURES = [
4
+ { value: 'tenancy', label: 'Multi-tenancy', hint: 'per-tenant data isolation' },
5
+ { value: 'auth', label: 'Authentication', hint: 'JWT, sessions, MFA, OAuth' },
6
+ { value: 'billing', label: 'Subscriptions / billing', hint: 'Stripe, Paddle, Lemon Squeezy' },
7
+ { value: 'ui', label: 'Web UI', hint: 'React + shadcn + SDK (pnpm workspace)' },
8
+ { value: 'cli', label: 'basalt CLI', hint: 'code generators + commands' },
9
+ ];
10
+ export const PRESETS = [
11
+ { value: 'saas', label: 'SaaS starter', hint: 'tenancy + auth + billing + CLI', features: ['tenancy', 'auth', 'billing', 'cli'] },
12
+ { value: 'api', label: 'API only', hint: 'auth, no tenancy or UI', features: ['auth'] },
13
+ { value: 'full', label: 'Full stack', hint: 'everything + web UI', features: ['tenancy', 'auth', 'billing', 'ui', 'cli'] },
14
+ { value: 'minimal', label: 'Minimal', hint: 'no batteries, add them later', features: [] },
15
+ { value: 'custom', label: 'Custom', hint: 'pick features yourself', features: null },
16
+ ];
17
+ const PACKAGE_MANAGERS = [
18
+ { value: 'pnpm', label: 'pnpm', hint: 'recommended' },
19
+ { value: 'npm', label: 'npm' },
20
+ { value: 'yarn', label: 'yarn' },
21
+ { value: 'bun', label: 'bun' },
22
+ ];
23
+ /** Validates a project name as an npm-installable package name. */
24
+ export function validateProjectName(name) {
25
+ const trimmed = name.trim();
26
+ if (!trimmed)
27
+ return 'Please enter a project name.';
28
+ if (trimmed.length > 214)
29
+ return 'Name is too long (max 214 characters).';
30
+ if (!/^(?:@[a-z0-9-~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(trimmed)) {
31
+ return 'Use a valid npm package name (lowercase, no spaces).';
32
+ }
33
+ return undefined;
34
+ }
35
+ /**
36
+ * The interactive scaffolding flow. Pure with respect to I/O — it drives the
37
+ * injected {@link Prompter}, so tests script the answers and assert the result.
38
+ * Throws {@link WizardCancelledError} if the user declines the final confirm.
39
+ */
40
+ export async function runWizard(prompter, options = {}) {
41
+ prompter.intro('create-basalt — scaffold a Basalt application');
42
+ const name = options.name && !validateProjectName(options.name)
43
+ ? options.name
44
+ : await prompter.text({
45
+ message: 'Project name',
46
+ placeholder: 'my-saas',
47
+ initial: 'my-saas',
48
+ validate: validateProjectName,
49
+ });
50
+ const presetValue = await prompter.select({
51
+ message: 'Choose a starting point',
52
+ choices: PRESETS.map((p) => ({ value: p.value, label: p.label, hint: p.hint })),
53
+ initial: 0,
54
+ });
55
+ const preset = PRESETS.find((p) => p.value === presetValue) ?? PRESETS[0];
56
+ const featureSet = new Set(preset.features ??
57
+ (await prompter.multiselect({
58
+ message: 'Select features',
59
+ choices: FEATURES,
60
+ initial: ['tenancy', 'auth'],
61
+ })));
62
+ let pm = await prompter.select({
63
+ message: 'Package manager',
64
+ choices: PACKAGE_MANAGERS,
65
+ initial: Math.max(0, PACKAGE_MANAGERS.findIndex((m) => m.value === (options.defaultPm ?? 'pnpm'))),
66
+ });
67
+ // The web/ frontend is a pnpm workspace member — force pnpm when UI is on.
68
+ if (featureSet.has('ui') && pm !== 'pnpm') {
69
+ prompter.note(` Note: the Web UI is a pnpm workspace — using pnpm instead of ${pm}.`);
70
+ pm = 'pnpm';
71
+ }
72
+ const install = await prompter.confirm({ message: 'Install dependencies now?', initial: false });
73
+ const git = await prompter.confirm({ message: 'Initialize a git repository?', initial: false });
74
+ const enabled = FEATURES.filter((f) => featureSet.has(f.value)).map((f) => f.label);
75
+ prompter.note(`\n Summary\n` +
76
+ ` name ${name}\n` +
77
+ ` features ${enabled.length ? enabled.join(', ') : '(none)'}\n` +
78
+ ` pm ${pm}\n` +
79
+ ` install ${install ? 'yes' : 'no'}\n` +
80
+ ` git ${git ? 'yes' : 'no'}\n`);
81
+ const ok = await prompter.confirm({ message: 'Create project?', initial: true });
82
+ if (!ok)
83
+ throw new WizardCancelledError();
84
+ return {
85
+ name,
86
+ tenancy: featureSet.has('tenancy'),
87
+ auth: featureSet.has('auth'),
88
+ billing: featureSet.has('billing'),
89
+ ui: featureSet.has('ui'),
90
+ cli: featureSet.has('cli'),
91
+ pm,
92
+ install,
93
+ git,
94
+ };
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-basalt",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Scaffold a production-shaped Basalt SaaS app: npm create basalt my-saas [--billing] [--ui] [--cli] [--install] [--git]",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,10 +17,9 @@
17
17
  "dist"
18
18
  ],
19
19
  "devDependencies": {
20
- "@types/node": "^22.15.0",
21
- "tsup": "^8.4.0",
22
- "typescript": "^5.8.0",
23
- "vitest": "^3.1.0",
20
+ "@types/node": "^26.3.0",
21
+ "typescript": "^7.0.2",
22
+ "vitest": "^4.1.11",
24
23
  "@basaltkit/tsconfig": "^0.24.0"
25
24
  },
26
25
  "publishConfig": {
@@ -28,11 +27,11 @@
28
27
  },
29
28
  "repository": {
30
29
  "type": "git",
31
- "url": "git+https://github.com/Zebedeu/basalt.git",
30
+ "url": "git+https://github.com/basaltkit/basalt.git",
32
31
  "directory": "packages/create-app"
33
32
  },
34
- "homepage": "https://github.com/Zebedeu/basalt/tree/main/packages/create-app#readme",
35
- "bugs": "https://github.com/Zebedeu/basalt/issues",
33
+ "homepage": "https://github.com/basaltkit/basalt/tree/main/packages/create-app#readme",
34
+ "bugs": "https://github.com/basaltkit/basalt/issues",
36
35
  "keywords": [
37
36
  "basalt",
38
37
  "typescript",
@@ -43,7 +42,7 @@
43
42
  "boilerplate"
44
43
  ],
45
44
  "scripts": {
46
- "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
45
+ "build": "tsc -p tsconfig.build.json",
47
46
  "test": "vitest run",
48
47
  "typecheck": "tsc --noEmit"
49
48
  }