create-basalt 1.2.0 → 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.
- package/README.md +6 -0
- package/dist/{chunk-YJ7RXFTU.js → chunk-XUW6ZAPV.js} +2 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +127 -119
- package/dist/index.d.ts +10 -114
- package/dist/index.js +58 -24
- package/dist/prompt.d.ts +57 -0
- package/dist/prompt.js +166 -0
- package/dist/templates-ui.d.ts +9 -0
- package/dist/templates-ui.js +684 -0
- package/dist/templates.d.ts +30 -0
- package/dist/templates.js +386 -0
- package/dist/wizard.d.ts +39 -0
- package/dist/wizard.js +95 -0
- package/package.json +8 -9
|
@@ -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
|
+
}
|
package/dist/wizard.d.ts
ADDED
|
@@ -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.
|
|
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": "^
|
|
21
|
-
"
|
|
22
|
-
"
|
|
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/
|
|
30
|
+
"url": "git+https://github.com/basaltkit/basalt.git",
|
|
32
31
|
"directory": "packages/create-app"
|
|
33
32
|
},
|
|
34
|
-
"homepage": "https://github.com/
|
|
35
|
-
"bugs": "https://github.com/
|
|
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": "
|
|
45
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
46
|
"test": "vitest run",
|
|
48
47
|
"typecheck": "tsc --noEmit"
|
|
49
48
|
}
|