@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 +2 -0
- package/dist/bin.js +81 -0
- package/dist/chunk-P4YULL3I.js +1275 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/package.json +35 -0
package/dist/bin.d.ts
ADDED
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
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -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
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
|
+
}
|