@maestro-js/init 1.0.0-alpha.10

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/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/bin.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildProjectTree,
4
+ scaffold
5
+ } from "./chunk-P4YULL3I.js";
6
+
7
+ // src/bin.ts
8
+ import crypto from "crypto";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { execSync } from "child_process";
12
+ import { access } from "fs/promises";
13
+ import { fileURLToPath } from "url";
14
+ var name = process.argv[2];
15
+ if (!name || !/^[a-z][a-z0-9-]*$/.test(name)) {
16
+ console.error("Project name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens.");
17
+ process.exit(1);
18
+ }
19
+ var targetDir = path.join(process.cwd(), name);
20
+ if (await fileExists(targetDir)) {
21
+ console.error(`Directory "${name}" already exists.`);
22
+ process.exit(1);
23
+ }
24
+ var tokens = {
25
+ name,
26
+ Name: toPascalCase(name),
27
+ name_snake: name.replace(/-/g, "_"),
28
+ ENCRYPTION_KEY: crypto.randomBytes(32).toString("base64"),
29
+ HASH_KEY: crypto.randomBytes(32).toString("base64"),
30
+ DB_PORT: String(deriveDbPort(name))
31
+ };
32
+ var tree = buildProjectTree(name);
33
+ var created = await scaffold(tree, targetDir, tokens);
34
+ console.log(`
35
+ Created ${created.length} files in ./${name}/
36
+ `);
37
+ installSkills(targetDir);
38
+ console.log("Next steps:");
39
+ console.log(` cd ${name}`);
40
+ console.log(" pnpm install");
41
+ console.log(" maestro db:make-container");
42
+ console.log(" maestro db:migrate");
43
+ console.log(` cd ${name}-web && pnpm dev`);
44
+ function toPascalCase(str) {
45
+ return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
46
+ }
47
+ function deriveDbPort(name2) {
48
+ let hash = 0;
49
+ for (const char of name2) hash = hash * 31 + char.charCodeAt(0) & 65535;
50
+ return 3307 + hash % 100;
51
+ }
52
+ function installSkills(targetDir2) {
53
+ const skillSources = [
54
+ { cmd: `npx --yes @maestro-js/agent-skills@${getCurrentVersion()}`, label: "maestro" },
55
+ { cmd: "npx skills add https://react-aria.adobe.com --all -a claude-code", label: "react-aria" },
56
+ { cmd: "npx skills add anthropics/skills --skill skill-creator -a claude-code -y", label: "skill-creator" },
57
+ { cmd: "npx skills add mattpocock/skills --skill grill-me -a claude-code -y", label: "grill-me" },
58
+ { cmd: "playwright-cli install --skills", label: "playwright-cli" }
59
+ ];
60
+ for (const { cmd, label } of skillSources) {
61
+ try {
62
+ execSync(cmd, { cwd: targetDir2, stdio: "inherit" });
63
+ } catch {
64
+ console.warn(`Warning: Failed to install ${label} skills.`);
65
+ }
66
+ }
67
+ }
68
+ async function fileExists(p) {
69
+ try {
70
+ await access(p);
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+ function getCurrentVersion() {
77
+ const dir = path.dirname(fileURLToPath(import.meta.url));
78
+ const raw = fs.readFileSync(path.join(dir, "../package.json"), "utf-8");
79
+ const pkg = JSON.parse(raw);
80
+ return pkg.version;
81
+ }
@@ -0,0 +1,1275 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/templates/root.ts
4
+ function rootPackageJson(name) {
5
+ return JSON.stringify(
6
+ {
7
+ type: "module",
8
+ private: true,
9
+ scripts: {
10
+ "fks-cascade": "maestro config:run --config services -- node ./scripts/foreign-key-delete-behavior.ts cascade",
11
+ "fks-restrict": "maestro config:run --config services -- node ./scripts/foreign-key-delete-behavior.ts restrict"
12
+ },
13
+ dependencies: {
14
+ "@maestro-js/config": "latest",
15
+ [`@${name}/services`]: "workspace:*"
16
+ },
17
+ devDependencies: {
18
+ "@maestro-js/cli": "latest",
19
+ "@maestro-js/db-migrate": "latest",
20
+ "@maestro-js/db": "latest",
21
+ "@types/node": "^22.19.11",
22
+ tsx: "^4.21.0"
23
+ }
24
+ },
25
+ null,
26
+ 2
27
+ );
28
+ }
29
+ var rootTsconfig = `{
30
+ "compilerOptions": {
31
+ "target": "ES2022",
32
+ "module": "es2022",
33
+ "lib": ["ES2022"],
34
+ "moduleResolution": "bundler",
35
+ "jsx": "react-jsx",
36
+ "strict": true,
37
+ "noEmit": true,
38
+ "allowImportingTsExtensions": true,
39
+ "verbatimModuleSyntax": true,
40
+ "noUncheckedIndexedAccess": true
41
+ },
42
+ "exclude": ["node_modules"],
43
+ "include": ["./src", "./packages/*/src", "./packages/*/tests", "./scripts"]
44
+ }
45
+ `;
46
+ var rootEnv = `DB_HOST=localhost
47
+ DB_PORT={{DB_PORT}}
48
+ DB_USER=root
49
+ DB_PASSWORD=root
50
+ DB_DATABASE={{name}}
51
+
52
+ ENCRYPTION_KEY={{ENCRYPTION_KEY}}
53
+ HASH_KEY={{HASH_KEY}}
54
+
55
+ MAIL_FROM=noreply@{{name}}.app
56
+ BASE_URL=http://localhost:3000
57
+ `;
58
+ var rootGitignore = `node_modules
59
+ dist
60
+ .env
61
+ .env.*
62
+ !.env.example
63
+ !.env.encrypted
64
+ !.env.*.encrypted
65
+ .react-router
66
+ .DS_Store
67
+ build
68
+ .e2e-mail/
69
+ cdk.out
70
+ `;
71
+ function pnpmWorkspace(name) {
72
+ return `packages:
73
+ - packages/*
74
+ - ${name}-web
75
+ - queue-processor
76
+ - infrastructure
77
+ - scripts
78
+ `;
79
+ }
80
+
81
+ // src/templates/config.ts
82
+ var servicesConfig = `export default async function (env: Record<string, string | undefined>) {
83
+ return {
84
+ DB_HOST: env.DB_HOST ?? 'localhost',
85
+ DB_PORT: env.DB_PORT ?? '3306',
86
+ DB_USER: env.DB_USER ?? '{{name}}',
87
+ DB_PASSWORD: env.DB_PASSWORD ?? '',
88
+ DB_DATABASE: env.DB_DATABASE ?? '{{name}}',
89
+ REDIS_URL: env.REDIS_URL ?? 'localhost',
90
+ REDIS_PORT: env.REDIS_PORT ?? '6379',
91
+ ENCRYPTION_KEY: env.ENCRYPTION_KEY,
92
+ HASH_KEY: env.HASH_KEY,
93
+ MAIL_FROM: env.MAIL_FROM ?? 'noreply@{{name}}.app',
94
+ BASE_URL: env.BASE_URL ?? 'http://localhost:3000',
95
+ ENV_NAME: env.ENV_NAME ?? 'local',
96
+ AWS_REGION: env.AWS_REGION ?? 'us-east-1'
97
+ }
98
+ }
99
+ `;
100
+ var webConfig = `import getServicesConfig from './services.ts'
101
+
102
+ export default async function (env: Record<string, string | undefined>) {
103
+ const servicesConfig = await getServicesConfig(env)
104
+
105
+ return {
106
+ ...servicesConfig,
107
+ PORT: env.PORT ?? '3000',
108
+ SESSION_COOKIE_NAME: '{{name_snake}}_session',
109
+ SESSION_COOKIE_SECURE: servicesConfig.ENV_NAME !== 'local' ? 'true' : 'false'
110
+ }
111
+ }
112
+ `;
113
+ var queueProcessorConfig = `import getServicesConfig from './services.ts'
114
+
115
+ export default async function (env: Record<string, string | undefined>) {
116
+ const servicesConfig = await getServicesConfig(env)
117
+
118
+ return {
119
+ ...servicesConfig,
120
+ QUEUE_CONCURRENCY: env.QUEUE_CONCURRENCY ?? '1',
121
+ QUEUE_POLL_INTERVAL: env.QUEUE_POLL_INTERVAL ?? '5000'
122
+ }
123
+ }
124
+ `;
125
+ var dbMigrateConfig = `export default async function (env: Record<string, string | undefined>) {
126
+ return {
127
+ DB_HOST: env.DB_HOST ?? 'localhost',
128
+ DB_PORT: env.DB_PORT ?? '3306',
129
+ DB_USER: env.DB_USER ?? '{{name}}',
130
+ DB_PASSWORD: env.DB_PASSWORD ?? '',
131
+ DB_DATABASE: env.DB_DATABASE ?? '{{name}}'
132
+ }
133
+ }
134
+ `;
135
+
136
+ // src/templates/schemas.ts
137
+ function schemasPackageJson(name) {
138
+ return JSON.stringify(
139
+ {
140
+ name: `@${name}/schemas`,
141
+ private: true,
142
+ type: "module",
143
+ exports: { ".": "./src/index.ts" },
144
+ scripts: { typecheck: "tsc --noEmit" },
145
+ dependencies: {
146
+ "iso-fns": "npm:iso-fns@2.0.0-alpha.26",
147
+ zod: "^4.3.0",
148
+ "@maestro-js/iso-zod": "latest"
149
+ },
150
+ devDependencies: {}
151
+ },
152
+ null,
153
+ 2
154
+ );
155
+ }
156
+ var schemasIndex = `// Export your Zod schemas here
157
+ // Example: export { UserSchemas } from './user/index.ts'
158
+ `;
159
+
160
+ // src/templates/services.ts
161
+ function servicesPackageJson(name) {
162
+ return JSON.stringify(
163
+ {
164
+ name: `@${name}/services`,
165
+ private: true,
166
+ type: "module",
167
+ exports: { ".": "./src/index.ts" },
168
+ scripts: {
169
+ test: `maestro config:run --config services --root $PWD/../.. -- node ./run-tests.ts`,
170
+ typecheck: "tsc --noEmit"
171
+ },
172
+ dependencies: {
173
+ [`@${name}/schemas`]: "workspace:*",
174
+ "@aws-sdk/client-ses": "^3.1004.0",
175
+ "@maestro-js/auth": "latest",
176
+ "@maestro-js/cache": "latest",
177
+ "@maestro-js/config": "latest",
178
+ "@maestro-js/crypt": "latest",
179
+ "@maestro-js/custom-errors": "latest",
180
+ "@maestro-js/db": "latest",
181
+ "@maestro-js/exceptions": "latest",
182
+ "@maestro-js/hash": "latest",
183
+ "@maestro-js/helpers": "latest",
184
+ "@maestro-js/iso-zod": "latest",
185
+ "@maestro-js/log": "latest",
186
+ "@maestro-js/mail": "latest",
187
+ "@maestro-js/password": "latest",
188
+ "@maestro-js/permissions": "latest",
189
+ "@maestro-js/queue": "latest",
190
+ "@maestro-js/rate-limiting": "latest",
191
+ "@maestro-js/recurring-jobs": "latest",
192
+ "@maestro-js/session": "latest",
193
+ "iso-fns": "npm:iso-fns@2.0.0-alpha.26",
194
+ zod: "^4.3.0"
195
+ },
196
+ devDependencies: {
197
+ "@maestro-js/cli": "latest",
198
+ "@types/node": "^22.0.0",
199
+ "beartest-js": "^8.0.2"
200
+ }
201
+ },
202
+ null,
203
+ 2
204
+ );
205
+ }
206
+ var servicesTsconfig = `{
207
+ "extends": "../../tsconfig.json",
208
+ "compilerOptions": {
209
+ "module": "es2022",
210
+ "moduleResolution": "bundler",
211
+ "allowImportingTsExtensions": true
212
+ },
213
+ "include": ["./src"]
214
+ }
215
+ `;
216
+ var maestroTs = `import fs from 'node:fs'
217
+ import path from 'node:path'
218
+ import { Db } from '@maestro-js/db'
219
+ import { Cache } from '@maestro-js/cache'
220
+ import { Hash } from '@maestro-js/hash'
221
+ import { Crypt } from '@maestro-js/crypt'
222
+ import { Session } from '@maestro-js/session'
223
+ import { RateLimiting } from '@maestro-js/rate-limiting'
224
+ import { Queue } from '@maestro-js/queue'
225
+ import { RecurringJobs } from '@maestro-js/recurring-jobs'
226
+ import { Mail } from '@maestro-js/mail'
227
+ import { Log } from '@maestro-js/log'
228
+ import { SESClient, SendRawEmailCommand, type SendRawEmailCommandInput } from '@aws-sdk/client-ses'
229
+
230
+ Db.Provider.register(
231
+ 'default',
232
+ Db.Provider.create({
233
+ driver: Db.drivers.mysql({
234
+ user: process.env.DB_USER!,
235
+ password: process.env.DB_PASSWORD!,
236
+ host: process.env.DB_HOST!,
237
+ database: process.env.DB_DATABASE!,
238
+ port: Number(process.env.DB_PORT!)
239
+ })
240
+ })
241
+ )
242
+
243
+ Cache.Provider.register(
244
+ 'default',
245
+ Cache.Provider.create({
246
+ driver:
247
+ process.env.ENV_NAME === 'local'
248
+ ? Cache.drivers.mockRedis({})
249
+ : Cache.drivers.redis({ url: process.env.REDIS_URL!, port: Number(process.env.REDIS_PORT!) })
250
+ })
251
+ )
252
+
253
+ Hash.Provider.register(
254
+ 'default',
255
+ Hash.Provider.create(Hash.drivers.sha256({ key: process.env.HASH_KEY!, previousKeys: [] }))
256
+ )
257
+ Hash.Provider.register('bcrypt', Hash.Provider.create(Hash.drivers.bcrypt({ rounds: 12 })))
258
+
259
+ Crypt.Provider.register(
260
+ 'default',
261
+ Crypt.Provider.create({
262
+ key: process.env.ENCRYPTION_KEY!,
263
+ previousKeys: []
264
+ })
265
+ )
266
+
267
+ Session.Provider.register(
268
+ 'default',
269
+ Session.Provider.create({
270
+ driver: Session.drivers.store({
271
+ store: Cache,
272
+ crypt: Crypt,
273
+ cookie: {
274
+ name: '{{name_snake}}_session',
275
+ secure: process.env.ENV_NAME !== 'local',
276
+ httpOnly: true,
277
+ sameSite: 'Lax',
278
+ maxAge: 30 * 24 * 60 * 60
279
+ }
280
+ })
281
+ })
282
+ )
283
+
284
+ RateLimiting.Provider.register('default', RateLimiting.Provider.create(Cache))
285
+
286
+ Queue.Provider.register(
287
+ 'default',
288
+ Queue.Provider.create({
289
+ driver: Queue.drivers.db({
290
+ db: Db,
291
+ databaseTable: 'queueMessages'
292
+ }),
293
+ logger: Log
294
+ })
295
+ )
296
+
297
+ RecurringJobs.Provider.register(
298
+ 'default',
299
+ RecurringJobs.Provider.create({
300
+ driver: RecurringJobs.drivers.db({
301
+ db: Db,
302
+ databaseTable: 'recurringJobs'
303
+ }),
304
+ logger: Log
305
+ })
306
+ )
307
+
308
+ if (process.env.ENV_NAME === 'local') {
309
+ const e2eMailDir = path.resolve(process.cwd(), '.e2e-mail')
310
+ fs.mkdirSync(e2eMailDir, { recursive: true })
311
+ Mail.Provider.register(
312
+ 'default',
313
+ Mail.Provider.create({
314
+ driver: Mail.drivers.stub({
315
+ logger: {
316
+ info(envelope: Mail.Envelope) {
317
+ fs.writeFileSync(path.join(e2eMailDir, 'latest.json'), JSON.stringify(envelope, null, 2))
318
+ }
319
+ } as any
320
+ }),
321
+ name: 'default',
322
+ globalFrom: process.env.MAIL_FROM!,
323
+ logger: Log,
324
+ queue: Queue
325
+ })
326
+ )
327
+ } else {
328
+ const sesClient = new SESClient({ region: process.env.AWS_REGION! })
329
+ Mail.Provider.register(
330
+ 'default',
331
+ Mail.Provider.create({
332
+ driver: Mail.drivers.ses({
333
+ send(command: { input: SendRawEmailCommandInput }) {
334
+ return sesClient.send(new SendRawEmailCommand(command.input))
335
+ },
336
+ configurationSetName: '{{name}}'
337
+ }),
338
+ name: 'default',
339
+ globalFrom: process.env.MAIL_FROM!,
340
+ logger: Log,
341
+ queue: Queue
342
+ })
343
+ )
344
+ }
345
+
346
+ Log.addTransport(Log.transports.console())
347
+
348
+ // TODO: Register Auth.Provider with your credentials type
349
+ // TODO: Register Password.Provider if using password auth
350
+ // TODO: Register Permissions.Provider with your action map
351
+
352
+ declare module '@maestro-js/hash' {
353
+ namespace Hash {
354
+ namespace Provider {
355
+ interface Keys {
356
+ default: true
357
+ bcrypt: true
358
+ }
359
+ }
360
+ }
361
+ }
362
+ `;
363
+ var servicesIndex = `import './maestro.ts'
364
+ export { Db } from '@maestro-js/db'
365
+ export { Cache } from '@maestro-js/cache'
366
+ export { Hash } from '@maestro-js/hash'
367
+ export { Crypt } from '@maestro-js/crypt'
368
+ // TODO: export { Auth } from '@maestro-js/auth'
369
+ // TODO: export { Password } from '@maestro-js/password'
370
+ export { Helpers } from '@maestro-js/helpers'
371
+ export { Session } from '@maestro-js/session'
372
+ export { RateLimiting } from '@maestro-js/rate-limiting'
373
+ export { Queue } from '@maestro-js/queue'
374
+ export { RecurringJobs } from '@maestro-js/recurring-jobs'
375
+ export { Mail } from '@maestro-js/mail'
376
+ export { Log } from '@maestro-js/log'
377
+ // TODO: export { Permissions } from '@maestro-js/permissions'
378
+ export { CustomErrors } from '@maestro-js/custom-errors'
379
+ `;
380
+
381
+ // src/templates/web.ts
382
+ function webPackageJson(name) {
383
+ return JSON.stringify(
384
+ {
385
+ name: `@${name}/${name}-web`,
386
+ private: true,
387
+ type: "module",
388
+ scripts: {
389
+ dev: `NODE_OPTIONS='--import ./instrument.server.mjs' maestro config:run --config ${name}-web --root $PWD/.. -- react-router dev`,
390
+ "dev-test": "IS_TESTING=true pnpm dev",
391
+ build: "react-router build",
392
+ start: `NODE_ENV=production NODE_OPTIONS='--import ./instrument.server.mjs' maestro config:run --config ${name}-web --root $PWD/.. -- react-router-serve ./build/server/index.js`,
393
+ typecheck: "react-router typegen && tsc --noEmit",
394
+ "test:e2e": `maestro config:run --config ${name}-web --root $PWD/.. -- playwright test`,
395
+ format: "prettier --write app/"
396
+ },
397
+ dependencies: {
398
+ [`@${name}/schemas`]: "workspace:*",
399
+ [`@${name}/services`]: "workspace:*",
400
+ "@maestro-js/custom-errors": "latest",
401
+ "@maestro-js/log": "latest",
402
+ "@maestro-js/form": "latest",
403
+ "@react-router/node": "^7.9.0",
404
+ "@react-router/serve": "^7.9.0",
405
+ "@sentry/react-router": "^9",
406
+ isbot: "^5",
407
+ "iso-fns": "npm:iso-fns@2.0.0-alpha.26",
408
+ react: "^19.0.0",
409
+ "react-aria-components": "^1.15.1",
410
+ "react-dom": "^19.0.0",
411
+ "react-router": "^7.9.0",
412
+ "tailwind-merge": "^3.5.0",
413
+ "vite-env-only": "^3.0.3",
414
+ zod: "^4.0.0"
415
+ },
416
+ devDependencies: {
417
+ "@maestro-js/cli": "latest",
418
+ "@maestro-js/react-router-file-routes": "latest",
419
+ "@playwright/test": "^1.52.0",
420
+ "@react-router/dev": "^7.9.0",
421
+ "@tailwindcss/vite": "^4.2.0",
422
+ "@types/node": "^22.19.11",
423
+ "@types/react": "^19.0.0",
424
+ "@types/react-dom": "^19.0.0",
425
+ tailwindcss: "^4.2.1",
426
+ typescript: "^5.7.0",
427
+ vite: "^8.0.0",
428
+ "vite-plugin-icons-spritesheet": "^3.0.1",
429
+ "vite-tsconfig-paths": "^6.1.1"
430
+ }
431
+ },
432
+ null,
433
+ 2
434
+ );
435
+ }
436
+ var webTsconfig = `{
437
+ "extends": "../tsconfig.json",
438
+ "compilerOptions": {
439
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
440
+ "module": "ESNext",
441
+ "moduleResolution": "bundler",
442
+ "types": ["vite/client"],
443
+ "rootDirs": [".", ".react-router/types"],
444
+ "paths": {
445
+ "~/*": ["./app/*"]
446
+ }
447
+ },
448
+ "include": [
449
+ "app",
450
+ "tests",
451
+ "vite.config.ts",
452
+ "react-router.config.ts",
453
+ ".react-router",
454
+ "./environment.d.ts",
455
+ "playwright.config.ts"
456
+ ]
457
+ }
458
+ `;
459
+ var viteConfig = `import { reactRouter } from '@react-router/dev/vite'
460
+ import { sentryReactRouter } from '@sentry/react-router'
461
+ import tailwindcss from '@tailwindcss/vite'
462
+ import { defineConfig } from 'vite'
463
+ import { iconsSpritesheet } from 'vite-plugin-icons-spritesheet'
464
+ import tsconfigPaths from 'vite-tsconfig-paths'
465
+ import { envOnlyMacros } from 'vite-env-only'
466
+
467
+ export default defineConfig((config) => ({
468
+ plugins: [
469
+ iconsSpritesheet({
470
+ inputDir: 'icons',
471
+ outputDir: 'app/icons',
472
+ fileName: 'sprite.svg',
473
+ withTypes: true,
474
+ typesOutputFile: 'app/icons/types.ts',
475
+ iconNameTransformer: (name) => name,
476
+ formatter: 'prettier'
477
+ }),
478
+ envOnlyMacros(),
479
+ tsconfigPaths(),
480
+ tailwindcss(),
481
+ reactRouter(),
482
+ sentryReactRouter(
483
+ {
484
+ org: process.env.SENTRY_ORG,
485
+ project: process.env.SENTRY_PROJECT,
486
+ authToken: process.env.SENTRY_AUTH_TOKEN
487
+ },
488
+ config
489
+ )
490
+ ],
491
+ server: {
492
+ port: 3000,
493
+ strictPort: true
494
+ }
495
+ }))
496
+ `;
497
+ var reactRouterConfig = `import type { Config } from '@react-router/dev/config'
498
+ import { sentryOnBuildEnd } from '@sentry/react-router'
499
+
500
+ export default {
501
+ ssr: true,
502
+ future: {
503
+ v8_middleware: true
504
+ },
505
+ async buildEnd({ viteConfig, reactRouterConfig, buildManifest }) {
506
+ await sentryOnBuildEnd({ viteConfig, reactRouterConfig, buildManifest })
507
+ }
508
+ } satisfies Config
509
+ `;
510
+ var instrumentServerMjs = `import * as Sentry from '@sentry/react-router'
511
+ import { Log } from '@maestro-js/log'
512
+
513
+ Sentry.init({
514
+ dsn: process.env.SENTRY_DSN,
515
+ tracesSampleRate: 1.0,
516
+ sendDefaultPii: false
517
+ })
518
+
519
+ Log.addTransport(
520
+ Log.transports.sentry({
521
+ sentry: Sentry,
522
+ filter: { level: ['error', 'emergency'] },
523
+ getCaptureContext(message) {
524
+ return {
525
+ tags: { channel: message.channel ?? 'default' },
526
+ extra: {},
527
+ level: message.level === 'emergency' ? 'fatal' : message.level
528
+ }
529
+ }
530
+ })
531
+ )
532
+
533
+ Log.addTransport(
534
+ Log.transports.sentryBreadcrumbs({
535
+ sentry: Sentry,
536
+ filter: { level: ['info', 'warn'] }
537
+ })
538
+ )
539
+ `;
540
+ var environmentDts = `declare module '*.svg' {
541
+ const href: string
542
+ export default href
543
+ }
544
+
545
+ interface ImportMetaEnv {
546
+ VITE_SENTRY_DSN: string
547
+ }
548
+ `;
549
+
550
+ // src/templates/web-app.ts
551
+ var entryServerTsx = `import * as Sentry from '@sentry/react-router'
552
+ import { createReadableStreamFromReadable } from '@react-router/node'
553
+ import { renderToPipeableStream } from 'react-dom/server'
554
+ import { ServerRouter } from 'react-router'
555
+ import '~/services.server'
556
+
557
+ export default Sentry.createSentryHandleRequest({
558
+ ServerRouter,
559
+ renderToPipeableStream,
560
+ createReadableStreamFromReadable
561
+ })
562
+
563
+ export const handleError = Sentry.createSentryHandleError({ logErrors: true })
564
+ `;
565
+ var entryClientTsx = `import * as Sentry from '@sentry/react-router'
566
+ import { startTransition, StrictMode } from 'react'
567
+ import { hydrateRoot } from 'react-dom/client'
568
+ import { HydratedRouter } from 'react-router/dom'
569
+
570
+ Sentry.init({
571
+ dsn: import.meta.env.VITE_SENTRY_DSN,
572
+ integrations: [Sentry.reactRouterTracingIntegration(), Sentry.replayIntegration()],
573
+ tracesSampleRate: 1.0,
574
+ replaysSessionSampleRate: 0.1,
575
+ replaysOnErrorSampleRate: 1.0
576
+ })
577
+
578
+ startTransition(() => {
579
+ hydrateRoot(
580
+ document,
581
+ <StrictMode>
582
+ <HydratedRouter />
583
+ </StrictMode>
584
+ )
585
+ })
586
+ `;
587
+ var rootTsx = `import * as Sentry from '@sentry/react-router'
588
+ import { HeadlessForm } from '@maestro-js/form'
589
+ import {
590
+ Links,
591
+ Meta,
592
+ Outlet,
593
+ Scripts,
594
+ ScrollRestoration,
595
+ useLoaderData,
596
+ useRouteLoaderData,
597
+ isRouteErrorResponse,
598
+ useRouteError,
599
+ Link,
600
+ useActionData,
601
+ useFetchers
602
+ } from 'react-router'
603
+ import type { LoaderFunctionArgs, MiddlewareFunction } from 'react-router'
604
+ import { getRequestContext, requestContextMiddleware } from '~/features/request-context.server'
605
+ import { csrfMiddleware } from '~/features/csrf-middleware.server'
606
+ import { customErrorStatusCodeMiddleware } from '~/features/custom-error-status-code-middleware.server'
607
+ import { securityHeadersMiddleware } from '~/features/security-headers-middleware.server'
608
+ import { getSessionToast } from '~/features/redirect-with-toast.server'
609
+ import './app.css'
610
+
611
+ export const middleware: MiddlewareFunction<Response>[] = [
612
+ customErrorStatusCodeMiddleware,
613
+ requestContextMiddleware,
614
+ csrfMiddleware,
615
+ securityHeadersMiddleware
616
+ ]
617
+
618
+ export async function loader({ context }: LoaderFunctionArgs) {
619
+ const requestContext = getRequestContext(context)
620
+
621
+ return {
622
+ csrfToken: requestContext.session.getCsrfToken(),
623
+ toast: getSessionToast(requestContext.session)
624
+ }
625
+ }
626
+
627
+ function Document({ children }: { children: React.ReactNode }) {
628
+ const loaderData = useRouteLoaderData<typeof loader>('root')
629
+ return (
630
+ <html lang="en">
631
+ <head>
632
+ <meta charSet="utf-8" />
633
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
634
+ <Meta />
635
+ <Links />
636
+ </head>
637
+ <body className="h-full bg-white text-neutral-900 antialiased">
638
+ {children}
639
+ <ScrollRestoration />
640
+ <Scripts />
641
+ </body>
642
+ </html>
643
+ )
644
+ }
645
+
646
+ export default function Root() {
647
+ const { csrfToken, toast } = useLoaderData<typeof loader>()
648
+
649
+ return (
650
+ <Document>
651
+ <HeadlessForm.GlobalProvider useServerErrors={useServerErrors} csrfToken={csrfToken}>
652
+ <Outlet />
653
+ </HeadlessForm.GlobalProvider>
654
+ </Document>
655
+ )
656
+ }
657
+
658
+ export function ErrorBoundary() {
659
+ const error = useRouteError()
660
+
661
+ if (isRouteErrorResponse(error)) {
662
+ const { status } = error
663
+
664
+ let title = 'Something went wrong'
665
+ let description = 'An unexpected error occurred.'
666
+
667
+ if (status === 404) {
668
+ title = 'Page not found'
669
+ description = 'The page you were looking for could not be found.'
670
+ } else if (status === 401) {
671
+ title = 'Unauthorized'
672
+ description = 'You need to be logged in to access this page.'
673
+ } else if (status === 403) {
674
+ title = 'Forbidden'
675
+ description = "You don't have permission to access this resource."
676
+ } else if (status === 429) {
677
+ title = 'Too many requests'
678
+ description = 'Please slow down and try again in a moment.'
679
+ }
680
+
681
+ return (
682
+ <Document>
683
+ <div className="flex min-h-screen items-center justify-center bg-neutral-50 px-4">
684
+ <div className="text-center">
685
+ <p className="text-5xl font-bold text-neutral-300">{status}</p>
686
+ <h1 className="mt-4 text-xl font-semibold text-neutral-900">{title}</h1>
687
+ <p className="mt-2 text-sm text-neutral-600">{description}</p>
688
+ <Link to="/" className="mt-6 inline-block typography-link">
689
+ Go home
690
+ </Link>
691
+ </div>
692
+ </div>
693
+ </Document>
694
+ )
695
+ }
696
+
697
+ if (error instanceof Error) {
698
+ Sentry.captureException(error)
699
+ }
700
+
701
+ return (
702
+ <Document>
703
+ <div className="flex min-h-screen items-center justify-center bg-neutral-50 px-4">
704
+ <div className="text-center">
705
+ <p className="text-5xl font-bold text-neutral-300">500</p>
706
+ <h1 className="mt-4 text-xl font-semibold text-neutral-900">Something went wrong</h1>
707
+ <p className="mt-2 text-sm text-neutral-600">An unexpected error occurred. Please try again later.</p>
708
+ <Link to="/" className="mt-6 inline-block typography-link">
709
+ Go home
710
+ </Link>
711
+ </div>
712
+ </div>
713
+ </Document>
714
+ )
715
+ }
716
+
717
+ function useServerErrors(formId: string | undefined) {
718
+ const actionData = useActionData<any>()
719
+ const fetchers = useFetchers()
720
+
721
+ const serverError =
722
+ actionData && typeof actionData === 'object' && 'formSubmissionError' in actionData
723
+ ? actionData['formSubmissionError']
724
+ : null
725
+ if (formId && serverError?.formId === formId) {
726
+ return serverError.issues
727
+ }
728
+
729
+ const fetcherMatch =
730
+ fetchers
731
+ .map((fetcher) => {
732
+ const fetcherData = fetcher.data
733
+ const serverError =
734
+ fetcherData && typeof fetcherData === 'object' && 'formSubmissionError' in fetcherData
735
+ ? fetcherData['formSubmissionError']
736
+ : null
737
+ if (formId && serverError?.formId === formId) {
738
+ return serverError.issues
739
+ }
740
+ return null
741
+ })
742
+ .filter(Boolean)[0] ?? null
743
+ return fetcherMatch
744
+ }
745
+ `;
746
+ var routesTs = `import { type RouteConfig } from '@react-router/dev/routes'
747
+ import { ReactRouterFileRoutes } from '@maestro-js/react-router-file-routes'
748
+
749
+ export default ReactRouterFileRoutes.getRoutes('routes') satisfies RouteConfig
750
+ `;
751
+ var servicesServerTs = `export * from '@{{name}}/services'
752
+ `;
753
+ var appCss = `@import 'tailwindcss';
754
+
755
+ @theme {
756
+ --color-primary-50: var(--color-blue-50);
757
+ --color-primary-100: var(--color-blue-100);
758
+ --color-primary-200: var(--color-blue-200);
759
+ --color-primary-300: var(--color-blue-300);
760
+ --color-primary-400: var(--color-blue-400);
761
+ --color-primary-500: var(--color-blue-500);
762
+ --color-primary-600: var(--color-blue-600);
763
+ --color-primary-700: var(--color-blue-700);
764
+ --color-primary-800: var(--color-blue-800);
765
+ --color-primary-900: var(--color-blue-900);
766
+ --color-primary-950: var(--color-blue-950);
767
+
768
+ --color-positive-50: var(--color-emerald-50);
769
+ --color-positive-100: var(--color-emerald-100);
770
+ --color-positive-200: var(--color-emerald-200);
771
+ --color-positive-300: var(--color-emerald-300);
772
+ --color-positive-400: var(--color-emerald-400);
773
+ --color-positive-500: var(--color-emerald-500);
774
+ --color-positive-600: var(--color-emerald-600);
775
+ --color-positive-700: var(--color-emerald-700);
776
+ --color-positive-800: var(--color-emerald-800);
777
+ --color-positive-900: var(--color-emerald-900);
778
+ --color-positive-950: var(--color-emerald-950);
779
+
780
+ --color-negative-50: var(--color-red-50);
781
+ --color-negative-100: var(--color-red-100);
782
+ --color-negative-200: var(--color-red-200);
783
+ --color-negative-300: var(--color-red-300);
784
+ --color-negative-400: var(--color-red-400);
785
+ --color-negative-500: var(--color-red-500);
786
+ --color-negative-600: var(--color-red-600);
787
+ --color-negative-700: var(--color-red-700);
788
+ --color-negative-800: var(--color-red-800);
789
+ --color-negative-900: var(--color-red-900);
790
+ --color-negative-950: var(--color-red-950);
791
+
792
+ --color-neutral-50: var(--color-gray-50);
793
+ --color-neutral-100: var(--color-gray-100);
794
+ --color-neutral-200: var(--color-gray-200);
795
+ --color-neutral-300: var(--color-gray-300);
796
+ --color-neutral-400: var(--color-gray-400);
797
+ --color-neutral-500: var(--color-gray-500);
798
+ --color-neutral-600: var(--color-gray-600);
799
+ --color-neutral-700: var(--color-gray-700);
800
+ --color-neutral-800: var(--color-gray-800);
801
+ --color-neutral-900: var(--color-gray-900);
802
+ --color-neutral-950: var(--color-gray-950);
803
+
804
+ --font-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
805
+ }
806
+
807
+ /* Base styles */
808
+ html {
809
+ @apply h-full antialiased;
810
+ }
811
+
812
+ body {
813
+ @apply h-full bg-white text-neutral-900 font-sans text-sm leading-relaxed;
814
+ }
815
+
816
+ /* Interactive element base states */
817
+ button,
818
+ [role='button'] {
819
+ @apply cursor-pointer;
820
+ }
821
+
822
+ a,
823
+ button,
824
+ [role='button'],
825
+ input,
826
+ select,
827
+ textarea {
828
+ @apply outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40 focus-visible:ring-offset-1 rounded-sm;
829
+ }
830
+
831
+ /* Typography utilities */
832
+ .typography-h1 {
833
+ @apply text-2xl font-semibold tracking-tight text-balance text-neutral-900;
834
+ }
835
+
836
+ .typography-h2 {
837
+ @apply text-base font-semibold text-neutral-900;
838
+ }
839
+
840
+ .typography-h3 {
841
+ @apply text-sm font-semibold text-neutral-900;
842
+ }
843
+
844
+ .typography-link {
845
+ @apply text-sm font-medium text-blue-600 hover:text-blue-500;
846
+ }
847
+
848
+ .typography-button {
849
+ @apply text-sm font-medium;
850
+ }
851
+ `;
852
+ var indexRoute = `export default function Index() {
853
+ return (
854
+ <div className="flex min-h-screen items-center justify-center">
855
+ <div className="text-center">
856
+ <h1 className="typography-h1">Welcome to {{Name}}</h1>
857
+ <p className="mt-2 text-sm text-neutral-600">Your Maestro project is ready.</p>
858
+ </div>
859
+ </div>
860
+ )
861
+ }
862
+ `;
863
+
864
+ // src/templates/web-features.ts
865
+ var requestContextServer = `import { Session } from '~/services.server'
866
+ import { createContext, RouterContextProvider, type MiddlewareFunction } from 'react-router'
867
+ import { randomUUID } from 'node:crypto'
868
+ import assert from 'node:assert'
869
+
870
+ interface RequestContext {
871
+ requestId: string
872
+ session: Session
873
+ // TODO: Add profile field once Auth is configured
874
+ // profile: YourProfileType | null
875
+ }
876
+
877
+ const requestContext = createContext<RequestContext | null>(null)
878
+
879
+ export const requestContextMiddleware: MiddlewareFunction<Response> = async ({ request, context }, next) => {
880
+ const requestId = randomUUID()
881
+ const cookieHeader = request.headers.get('Cookie')
882
+ const session = await Session.getSession(cookieHeader)
883
+
884
+ // TODO: Add auth once Auth.Provider is configured
885
+ // const authResult = await Auth.authenticate({ session, cookieHeader, deviceContext })
886
+ // const profile = authResult.id ? await YourService.GetProfileById({ id: authResult.id }) : null
887
+
888
+ context.set(requestContext, { requestId, session })
889
+
890
+ const response = await next()
891
+
892
+ // TODO: Append auth cookie once Auth is configured
893
+ // if (authResult.cookie) {
894
+ // response.headers.append('Set-Cookie', authResult.cookie)
895
+ // }
896
+ response.headers.append('Set-Cookie', await session.commit())
897
+
898
+ return response
899
+ }
900
+
901
+ export function getRequestContext(context: Readonly<RouterContextProvider>) {
902
+ const request = context.get(requestContext)
903
+ assert.ok(request)
904
+ return request
905
+ }
906
+ `;
907
+ var csrfMiddlewareServer = `import type { MiddlewareFunction } from 'react-router'
908
+ import { getRequestContext } from './request-context.server'
909
+
910
+ export const csrfMiddleware: MiddlewareFunction<Response> = async ({ request, context }, next) => {
911
+ const mutatingMethods = ['POST', 'PUT', 'DELETE']
912
+ if (mutatingMethods.includes(request.method)) {
913
+ const cloned = request.clone()
914
+ const formDataForMiddleware = await cloned.formData()
915
+ const clientCsrfToken = formDataForMiddleware.get('__csrfToken')
916
+ const requestContext = getRequestContext(context)
917
+ const sessionCsrfToken = requestContext.session.getCsrfToken()
918
+ if (clientCsrfToken !== sessionCsrfToken) {
919
+ throw new Response('Invalid CSRF token', { status: 403 })
920
+ }
921
+ }
922
+ return next()
923
+ }
924
+ `;
925
+ var securityHeadersMiddlewareServer = `import type { MiddlewareFunction } from 'react-router'
926
+
927
+ export const securityHeadersMiddleware: MiddlewareFunction<Response> = async (_args, next) => {
928
+ const response = await next()
929
+ response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
930
+ response.headers.set('X-Content-Type-Options', 'nosniff')
931
+ response.headers.set('X-Frame-Options', 'DENY')
932
+ response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
933
+ return response
934
+ }
935
+ `;
936
+ var customErrorStatusCodeMiddlewareServer = `import * as Sentry from '@sentry/react-router'
937
+ import { type MiddlewareFunction } from 'react-router'
938
+ import { CustomErrors, Log } from '~/services.server'
939
+
940
+ const log = Log.channel('http')
941
+
942
+ export const customErrorStatusCodeMiddleware: MiddlewareFunction<Response> = async ({ request }, next) => {
943
+ try {
944
+ return await next()
945
+ } catch (e) {
946
+ if (e instanceof Response) throw e
947
+
948
+ const statusCode = CustomErrors.getHttpStatusCode(e)
949
+ if (statusCode) {
950
+ if (statusCode >= 500) {
951
+ log.error('Server error', {
952
+ status: statusCode,
953
+ url: request.url,
954
+ error: e instanceof Error ? e.message : String(e)
955
+ })
956
+ } else {
957
+ log.warn('Client error', { status: statusCode, url: request.url, error: e instanceof Error ? e.name : String(e) })
958
+ }
959
+ throw new Response(null, { status: statusCode })
960
+ } else {
961
+ Sentry.captureException(e)
962
+ log.error('Unhandled error', { url: request.url, error: e instanceof Error ? (e.stack ?? e.message) : String(e) })
963
+ throw e
964
+ }
965
+ }
966
+ }
967
+ `;
968
+ var redirectWithToastServer = `import { redirect } from 'react-router'
969
+ import type { Session } from '~/services.server'
970
+
971
+ type Toast = { type: 'success' | 'error' | 'info'; message: string }
972
+
973
+ const TOAST_SESSION_KEY = '_toast'
974
+
975
+ export function redirectWithToast(url: string, toast: Toast, session: Session) {
976
+ session.flash(TOAST_SESSION_KEY, toast)
977
+ return redirect(url)
978
+ }
979
+
980
+ export function getSessionToast(session: Session): Toast | null {
981
+ return (session.get(TOAST_SESSION_KEY) as Toast) ?? null
982
+ }
983
+ `;
984
+
985
+ // src/templates/queue-processor.ts
986
+ function queueProcessorPackageJson(name) {
987
+ return JSON.stringify(
988
+ {
989
+ name: `@${name}/queue-processor`,
990
+ private: true,
991
+ type: "module",
992
+ scripts: {
993
+ start: "maestro config:run --config queue-processor -- tsx src/index.ts"
994
+ },
995
+ dependencies: {
996
+ "@maestro-js/config": "latest",
997
+ [`@${name}/services`]: "workspace:*"
998
+ },
999
+ devDependencies: {
1000
+ "@maestro-js/cli": "latest",
1001
+ "@types/node": "^22.19.11",
1002
+ tsx: "^4.21.0"
1003
+ }
1004
+ },
1005
+ null,
1006
+ 2
1007
+ );
1008
+ }
1009
+ var queueProcessorTsconfig = `{
1010
+ "extends": "../tsconfig.json",
1011
+ "include": ["src"]
1012
+ }
1013
+ `;
1014
+ var queueProcessorIndex = `import { Queue, RecurringJobs, Log } from '@{{name}}/services'
1015
+ import './recurring-jobs.ts'
1016
+
1017
+ const log = Log.channel('queue-processor')
1018
+
1019
+ const concurrency = Number(process.env.QUEUE_CONCURRENCY ?? '1')
1020
+ const sleepSeconds = Number(process.env.QUEUE_POLL_INTERVAL ?? '5000') / 1000
1021
+
1022
+ log.info({ event: 'queueProcessorStarting', concurrency, sleepSeconds })
1023
+
1024
+ const queueHandle = Queue.work({ concurrency, sleepSeconds })
1025
+ const recurringJobsHandle = RecurringJobs.work()
1026
+
1027
+ process.on('SIGTERM', () => {
1028
+ log.info({ event: 'queueProcessorStopping', reason: 'SIGTERM' })
1029
+ queueHandle.abort('SIGTERM')
1030
+ recurringJobsHandle.abort()
1031
+ })
1032
+
1033
+ process.on('SIGINT', () => {
1034
+ log.info({ event: 'queueProcessorStopping', reason: 'SIGINT' })
1035
+ queueHandle.abort('SIGINT')
1036
+ recurringJobsHandle.abort()
1037
+ })
1038
+
1039
+ queueHandle.then(() => {
1040
+ log.info({ event: 'queueProcessorStopped' })
1041
+ process.exit(0)
1042
+ })
1043
+ `;
1044
+ var recurringJobsTs = `// import { RecurringJobs, Log } from '@{{name}}/services'
1045
+
1046
+ // Define recurring jobs here
1047
+ // Example:
1048
+ // RecurringJobs.create({
1049
+ // name: 'my-recurring-job',
1050
+ // cron: '0 3 * * *',
1051
+ // async handle() {
1052
+ // // ...
1053
+ // return {}
1054
+ // }
1055
+ // })
1056
+ `;
1057
+
1058
+ // src/templates/infrastructure.ts
1059
+ var dbMigrationsSqlsGitkeep = "";
1060
+
1061
+ // src/templates/scripts.ts
1062
+ var foreignKeyDeleteBehavior = `import { Db } from '@maestro-js/db'
1063
+
1064
+ Db.Provider.register(
1065
+ 'default',
1066
+ Db.Provider.create({
1067
+ driver: Db.drivers.mysql({
1068
+ user: process.env.DB_USER!,
1069
+ password: process.env.DB_PASSWORD!,
1070
+ host: process.env.DB_HOST!,
1071
+ database: process.env.DB_DATABASE!,
1072
+ port: Number(process.env.DB_PORT!)
1073
+ })
1074
+ })
1075
+ )
1076
+
1077
+ // TODO: Add foreign key entries as you create migrations
1078
+ // Map constraint names to their ON DELETE behavior when using cascade mode
1079
+ const FKS: Record<string, string> = {
1080
+ // Example: fk_posts_userId: 'CASCADE'
1081
+ }
1082
+
1083
+ type ForeignKey = {
1084
+ TABLE_NAME: string
1085
+ COLUMN_NAME: string
1086
+ REFERENCED_TABLE_NAME: string
1087
+ REFERENCED_COLUMN_NAME: string
1088
+ CONSTRAINT_NAME: string
1089
+ }
1090
+
1091
+ const foreignKeys = await Db.select<ForeignKey>(
1092
+ \`SELECT
1093
+ TABLE_NAME,
1094
+ COLUMN_NAME,
1095
+ REFERENCED_TABLE_NAME,
1096
+ REFERENCED_COLUMN_NAME,
1097
+ CONSTRAINT_NAME
1098
+ FROM information_schema.KEY_COLUMN_USAGE
1099
+ WHERE TABLE_SCHEMA = ?
1100
+ AND REFERENCED_TABLE_NAME IS NOT NULL
1101
+ ORDER BY TABLE_NAME, COLUMN_NAME\`,
1102
+ [process.env.DB_DATABASE!]
1103
+ )
1104
+
1105
+ const dbFkNames = new Set(foreignKeys.map((fk) => fk.CONSTRAINT_NAME))
1106
+ const localFkNames = new Set(Object.keys(FKS))
1107
+
1108
+ const missingFromLocal = [...dbFkNames].filter((name) => !localFkNames.has(name))
1109
+ const extraInLocal = [...localFkNames].filter((name) => !dbFkNames.has(name))
1110
+
1111
+ if (missingFromLocal.length > 0) {
1112
+ console.error('Foreign keys in database but missing from FKS:', missingFromLocal)
1113
+ }
1114
+ if (extraInLocal.length > 0) {
1115
+ console.error('Foreign keys in FKS but not in database:', extraInLocal)
1116
+ }
1117
+ if (missingFromLocal.length > 0 || extraInLocal.length > 0) {
1118
+ process.exit(1)
1119
+ }
1120
+
1121
+ console.log('FKS matches database foreign keys')
1122
+
1123
+ async function applyOnDeleteRestrict() {
1124
+ for (const constraintName of Object.keys(FKS)) {
1125
+ const fk = foreignKeys.find((f) => f.CONSTRAINT_NAME === constraintName)!
1126
+ await Db.statement(\`ALTER TABLE \${fk.TABLE_NAME} DROP FOREIGN KEY \${constraintName}\`)
1127
+ await Db.statement(
1128
+ \`ALTER TABLE \${fk.TABLE_NAME}
1129
+ ADD CONSTRAINT \${constraintName}
1130
+ FOREIGN KEY (\${fk.COLUMN_NAME})
1131
+ REFERENCES \${fk.REFERENCED_TABLE_NAME}(\${fk.REFERENCED_COLUMN_NAME})
1132
+ ON DELETE RESTRICT\`
1133
+ )
1134
+ console.log(\`\${constraintName}: ON DELETE RESTRICT applied\`)
1135
+ }
1136
+ }
1137
+
1138
+ async function applyOnDeleteCascade() {
1139
+ for (const [constraintName, behavior] of Object.entries(FKS)) {
1140
+ const fk = foreignKeys.find((f) => f.CONSTRAINT_NAME === constraintName)!
1141
+ await Db.statement(\`ALTER TABLE \${fk.TABLE_NAME} DROP FOREIGN KEY \${constraintName}\`)
1142
+ await Db.statement(
1143
+ \`ALTER TABLE \${fk.TABLE_NAME}
1144
+ ADD CONSTRAINT \${constraintName}
1145
+ FOREIGN KEY (\${fk.COLUMN_NAME})
1146
+ REFERENCES \${fk.REFERENCED_TABLE_NAME}(\${fk.REFERENCED_COLUMN_NAME})
1147
+ ON DELETE \${behavior}\`
1148
+ )
1149
+ console.log(\`\${constraintName}: ON DELETE \${behavior} applied\`)
1150
+ }
1151
+ }
1152
+
1153
+ const action = process.argv[2]
1154
+
1155
+ if (action === 'cascade') {
1156
+ await applyOnDeleteCascade()
1157
+ } else if (action === 'restrict') {
1158
+ await applyOnDeleteRestrict()
1159
+ } else {
1160
+ console.error('Usage: foreign-key-delete-behavior <cascade|restrict>')
1161
+ process.exit(1)
1162
+ }
1163
+
1164
+ process.exit(0)
1165
+ `;
1166
+
1167
+ // src/scaffold.ts
1168
+ import { mkdir, writeFile } from "fs/promises";
1169
+ import { join, dirname } from "path";
1170
+ async function scaffold(tree, basePath, tokens) {
1171
+ const created = [];
1172
+ await writeTree(tree, basePath, tokens, created);
1173
+ return created;
1174
+ }
1175
+ async function writeTree(tree, dir, tokens, created) {
1176
+ for (const [name, value] of Object.entries(tree)) {
1177
+ const fullPath = join(dir, name);
1178
+ if (typeof value === "string") {
1179
+ await mkdir(dirname(fullPath), { recursive: true });
1180
+ const content = tokens ? replaceTokens(value, tokens) : value;
1181
+ await writeFile(fullPath, content);
1182
+ created.push(fullPath);
1183
+ } else {
1184
+ await writeTree(value, fullPath, tokens, created);
1185
+ }
1186
+ }
1187
+ }
1188
+ function replaceTokens(content, tokens) {
1189
+ return content.replace(/\{\{(\w+)\}\}/g, (match, key) => tokens[key] ?? match);
1190
+ }
1191
+
1192
+ // src/index.ts
1193
+ function buildProjectTree(name) {
1194
+ return {
1195
+ "package.json": rootPackageJson(name),
1196
+ "pnpm-workspace.yaml": pnpmWorkspace(name),
1197
+ "tsconfig.json": rootTsconfig,
1198
+ ".env": rootEnv,
1199
+ ".gitignore": rootGitignore,
1200
+ config: {
1201
+ "services.ts": servicesConfig,
1202
+ [`${name}-web.ts`]: webConfig,
1203
+ "queue-processor.ts": queueProcessorConfig,
1204
+ "db-migrate.ts": dbMigrateConfig
1205
+ },
1206
+ packages: {
1207
+ schemas: {
1208
+ "package.json": schemasPackageJson(name),
1209
+ src: {
1210
+ "index.ts": schemasIndex
1211
+ }
1212
+ },
1213
+ services: {
1214
+ "package.json": servicesPackageJson(name),
1215
+ "tsconfig.json": servicesTsconfig,
1216
+ src: {
1217
+ "maestro.ts": maestroTs,
1218
+ "index.ts": servicesIndex
1219
+ }
1220
+ }
1221
+ },
1222
+ [`${name}-web`]: {
1223
+ "package.json": webPackageJson(name),
1224
+ "tsconfig.json": webTsconfig,
1225
+ "vite.config.ts": viteConfig,
1226
+ "react-router.config.ts": reactRouterConfig,
1227
+ "instrument.server.mjs": instrumentServerMjs,
1228
+ "environment.d.ts": environmentDts,
1229
+ icons: {
1230
+ ".gitkeep": ""
1231
+ },
1232
+ app: {
1233
+ "entry.server.tsx": entryServerTsx,
1234
+ "entry.client.tsx": entryClientTsx,
1235
+ "root.tsx": rootTsx,
1236
+ "routes.ts": routesTs,
1237
+ "services.server.ts": servicesServerTs,
1238
+ "app.css": appCss,
1239
+ routes: {
1240
+ "index.tsx": indexRoute
1241
+ },
1242
+ features: {
1243
+ "request-context.server.ts": requestContextServer,
1244
+ "csrf-middleware.server.ts": csrfMiddlewareServer,
1245
+ "security-headers-middleware.server.ts": securityHeadersMiddlewareServer,
1246
+ "custom-error-status-code-middleware.server.ts": customErrorStatusCodeMiddlewareServer,
1247
+ "redirect-with-toast.server.ts": redirectWithToastServer
1248
+ }
1249
+ }
1250
+ },
1251
+ "queue-processor": {
1252
+ "package.json": queueProcessorPackageJson(name),
1253
+ "tsconfig.json": queueProcessorTsconfig,
1254
+ src: {
1255
+ "index.ts": queueProcessorIndex,
1256
+ "recurring-jobs.ts": recurringJobsTs
1257
+ }
1258
+ },
1259
+ infrastructure: {
1260
+ "db-migrations": {
1261
+ sqls: {
1262
+ ".gitkeep": dbMigrationsSqlsGitkeep
1263
+ }
1264
+ }
1265
+ },
1266
+ scripts: {
1267
+ "foreign-key-delete-behavior.ts": foreignKeyDeleteBehavior
1268
+ }
1269
+ };
1270
+ }
1271
+
1272
+ export {
1273
+ scaffold,
1274
+ buildProjectTree
1275
+ };
@@ -0,0 +1,9 @@
1
+ type Tree = {
2
+ [name: string]: string | Tree;
3
+ };
4
+ type Tokens = Record<string, string>;
5
+ declare function scaffold(tree: Tree, basePath: string, tokens?: Tokens): Promise<string[]>;
6
+
7
+ declare function buildProjectTree(name: string): Tree;
8
+
9
+ export { type Tokens, type Tree, buildProjectTree, scaffold };
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildProjectTree,
4
+ scaffold
5
+ } from "./chunk-P4YULL3I.js";
6
+ export {
7
+ buildProjectTree,
8
+ scaffold
9
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@maestro-js/init",
3
+ "description": "Project scaffolding for the Maestro framework. Generates a complete monorepo with config, schemas, services, web app, queue processor, and infrastructure.",
4
+ "type": "module",
5
+ "bin": {
6
+ "maestro-init": "./dist/bin.js"
7
+ },
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^22.19.11"
16
+ },
17
+ "version": "1.0.0-alpha.10",
18
+ "publishConfig": {
19
+ "access": "restricted"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "license": "UNLICENSED",
25
+ "engines": {
26
+ "node": ">=22.18.0"
27
+ },
28
+ "repository": "https://github.com/Marcato-Partners/maestro-js",
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "typecheck": "tsc --noEmit",
32
+ "format": "prettier --write src/",
33
+ "lint": "prettier --check src/"
34
+ }
35
+ }