@emeryld/manager 0.3.3 → 0.4.1

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.
@@ -1,219 +1,84 @@
1
- import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
2
- function fullstackPackageJson(name) {
3
- return `${JSON.stringify({
4
- name,
5
- version: '0.1.0',
1
+ import path from 'node:path';
2
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, baseTsConfig, writeFileIfMissing, } from '../shared.js';
3
+ import { clientVariant } from './client.js';
4
+ import { serverVariant } from './server.js';
5
+ import { dockerVariant } from './docker.js';
6
+ import { contractVariant } from './contract.js';
7
+ const FULLSTACK_SCRIPTS = [
8
+ 'setup',
9
+ 'dev',
10
+ 'build',
11
+ 'typecheck',
12
+ 'lint',
13
+ 'lint:fix',
14
+ 'lint-staged',
15
+ 'format',
16
+ 'format:check',
17
+ 'test',
18
+ 'clean',
19
+ 'prepare',
20
+ 'docker:up',
21
+ 'docker:dev',
22
+ 'docker:logs',
23
+ 'docker:stop',
24
+ 'docker:reset',
25
+ ];
26
+ function deriveNames(baseName) {
27
+ const normalized = baseName.trim();
28
+ return {
29
+ contract: `@${normalized}/contract`,
30
+ server: `@${normalized}/server`,
31
+ client: `@${normalized}/client`,
32
+ docker: `${normalized}-docker`,
33
+ };
34
+ }
35
+ function deriveDirs(rootDir, baseName) {
36
+ const packagesRoot = path.join(rootDir, 'packages');
37
+ return {
38
+ root: rootDir,
39
+ packagesRoot,
40
+ contract: path.join(packagesRoot, `${baseName}-contract`),
41
+ server: path.join(packagesRoot, `${baseName}-server`),
42
+ client: path.join(packagesRoot, `${baseName}-client`),
43
+ docker: path.join(packagesRoot, `${baseName}-docker`),
44
+ };
45
+ }
46
+ function rootPackageJson(baseName) {
47
+ const dockerPackageDir = `packages/${baseName}-docker`;
48
+ return basePackageJson({
49
+ name: `${baseName}-stack`,
6
50
  private: true,
7
- type: 'module',
8
- main: 'dist/server/index.js',
9
- files: ['dist'],
51
+ useDefaults: false,
10
52
  scripts: {
11
- dev: 'concurrently "npm:dev:server" "npm:dev:client"',
12
- 'dev:server': 'tsx watch src/server/index.ts',
13
- 'dev:client': 'vite --host --port 5173',
14
- build: 'npm run build:server && npm run build:client',
15
- 'build:server': 'tsc -p tsconfig.server.json',
16
- 'build:client': 'vite build',
17
- start: 'node dist/server/index.js',
18
- typecheck: 'tsc -p tsconfig.json --noEmit',
19
- 'docker:cli': 'tsx scripts/docker.ts',
20
- 'docker:build': 'npm run docker:cli -- build',
21
- 'docker:up': 'npm run docker:cli -- up',
22
- 'docker:logs': 'npm run docker:cli -- logs',
23
- 'docker:stop': 'npm run docker:cli -- stop',
24
- 'docker:clean': 'npm run docker:cli -- clean',
25
- 'db:up': 'docker compose up -d db',
26
- 'db:down': 'docker compose down',
27
- },
28
- dependencies: {
29
- '@emeryld/rrroutes-client': '^2.5.3',
30
- '@emeryld/rrroutes-contract': '^2.5.2',
31
- '@emeryld/rrroutes-server': '^2.4.1',
32
- '@tanstack/react-query': '^5.90.12',
33
- cors: '^2.8.5',
34
- express: '^5.1.0',
35
- pg: '^8.13.1',
36
- react: '^18.3.1',
37
- 'react-dom': '^18.3.1',
38
- zod: '^4.2.1',
53
+ setup: 'pnpm install && pnpm exec husky install',
54
+ dev: 'pnpm -r dev --parallel --if-present',
55
+ build: 'pnpm -r build',
56
+ typecheck: 'pnpm -r typecheck',
57
+ lint: 'pnpm -r lint --if-present',
58
+ 'lint:fix': 'pnpm -r lint:fix --if-present',
59
+ 'lint-staged': 'lint-staged',
60
+ format: 'pnpm -r format --if-present',
61
+ 'format:check': 'pnpm -r format:check --if-present',
62
+ test: 'pnpm -r test --if-present',
63
+ clean: 'pnpm -r clean --if-present && rimraf node_modules .turbo coverage',
64
+ prepare: 'husky install || true',
65
+ 'docker:up': `pnpm -C ${dockerPackageDir} run docker:up`,
66
+ 'docker:dev': `pnpm -C ${dockerPackageDir} run docker:dev`,
67
+ 'docker:logs': `pnpm -C ${dockerPackageDir} run docker:logs`,
68
+ 'docker:stop': `pnpm -C ${dockerPackageDir} run docker:stop`,
69
+ 'docker:reset': `pnpm -C ${dockerPackageDir} run docker:reset`,
39
70
  },
40
- devDependencies: {
41
- '@types/express': '^5.0.6',
42
- '@types/node': '^24.10.2',
43
- '@types/react': '^18.3.27',
44
- '@types/react-dom': '^18.3.7',
45
- '@types/pg': '^8.11.10',
46
- '@vitejs/plugin-react': '^4.3.4',
47
- concurrently: '^8.2.0',
48
- 'docker-cli-js': '^3.0.9',
49
- tsx: '^4.19.0',
50
- typescript: '^5.9.3',
51
- vite: '^6.4.1',
71
+ devDependencies: { ...BASE_LINT_DEV_DEPENDENCIES },
72
+ extraFields: {
73
+ workspaces: ['packages/*'],
52
74
  },
53
- }, null, 2)}\n`;
75
+ });
54
76
  }
55
- function fullstackServerIndexTs() {
56
- return `import { existsSync } from 'node:fs'
57
- import path from 'node:path'
58
- import express from 'express'
59
- import cors from 'cors'
60
- import { Pool } from 'pg'
61
- import { createRRRoute } from '@emeryld/rrroutes-server'
62
- import { registry } from '../contract/index.js'
63
-
64
- const app = express()
65
- app.use(cors({ origin: '*' }))
66
- app.use(express.json())
67
-
68
- const pool = new Pool({
69
- connectionString:
70
- process.env.DATABASE_URL ??
71
- 'postgresql://postgres:postgres@localhost:5432/rrroutes',
72
- })
73
-
74
- const routes = createRRRoute(app, {
75
- buildCtx: async () => ({
76
- requestId: Math.random().toString(36).slice(2),
77
- }),
78
- debug:
79
- process.env.NODE_ENV === 'development'
80
- ? { request: true, handler: true }
81
- : undefined,
82
- })
83
-
84
- routes.registerControllers(registry, {
85
- 'GET /api/health': {
86
- handler: async () => {
87
- let db = 'skipped'
88
- try {
89
- await pool.query('select 1')
90
- db = 'ok'
91
- } catch (error) {
92
- db = 'error: ' + String(error)
93
- }
94
- return {
95
- out: {
96
- status: 'ok',
97
- at: new Date().toISOString(),
98
- db,
99
- },
100
- }
101
- },
102
- },
103
- })
104
-
105
- const clientDir = path.resolve(__dirname, '../client')
106
- if (existsSync(clientDir)) {
107
- app.use(express.static(clientDir))
108
- app.get('*', (_req, res) => {
109
- res.sendFile(path.join(clientDir, 'index.html'))
110
- })
111
- } else {
112
- console.warn('Client bundle missing; run "npm run build:client" to enable static assets.')
113
- }
114
-
115
- const PORT = Number.parseInt(process.env.PORT ?? '8080', 10)
116
-
117
- app.listen(PORT, () => {
118
- console.log(\`Full stack service running on http://localhost:\${PORT}\`)
119
- })
120
- `;
121
- }
122
- const FULLSTACK_APP_TSX = `import React from 'react'
123
- import { QueryClientProvider } from '@tanstack/react-query'
124
- import { queryClient, healthGet } from './client/api'
125
-
126
- function HealthCard() {
127
- const health = healthGet.useEndpoint()
128
- return (
129
- <section
130
- style={{
131
- background: '#fff',
132
- padding: 16,
133
- borderRadius: 12,
134
- boxShadow: '0 8px 24px rgba(12, 18, 32, 0.05)',
135
- border: '1px solid #e5e7eb',
136
- }}
137
- >
138
- <h2>Health</h2>
139
- <button onClick={() => health.refetch()}>Ping API</button>
140
- <pre style={{ whiteSpace: 'pre-wrap' }}>
141
- {JSON.stringify(health.data ?? { note: 'Waiting for request...' }, null, 2)}
142
- </pre>
143
- </section>
144
- )
145
- }
146
-
147
- export function App() {
148
- return (
149
- <QueryClientProvider client={queryClient}>
150
- <main style={{ fontFamily: 'Inter, system-ui, sans-serif', padding: 24 }}>
151
- <h1>Full stack service</h1>
152
- <p>
153
- Contract-driven API + Vite client. Health endpoint uses RRRoutes and checks the
154
- Postgres connection.
155
- </p>
156
- <p>Edit <code>src/contract/index.ts</code> or <code>src/server/index.ts</code> to extend the API.</p>
157
- <HealthCard />
158
- </main>
159
- </QueryClientProvider>
160
- )
161
- }
162
- `;
163
- const FULLSTACK_MAIN_TSX = `import React from 'react'
164
- import ReactDOM from 'react-dom/client'
165
- import { App } from './App'
166
- import './styles.css'
167
-
168
- ReactDOM.createRoot(document.getElementById('root')!).render(
169
- <React.StrictMode>
170
- <App />
171
- </React.StrictMode>,
172
- )
173
- `;
174
- const FULLSTACK_STYLES = `:root {
175
- background: radial-gradient(circle at 20% 20%, #eef2ff, #f7f8fb 45%);
176
- color: #0b1021;
177
- }
178
-
179
- body {
180
- margin: 0;
181
- }
182
- `;
183
- const FULLSTACK_INDEX_HTML = `<!doctype html>
184
- <html lang="en">
185
- <head>
186
- <meta charset="UTF-8" />
187
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
188
- <title>Full stack service</title>
189
- </head>
190
- <body>
191
- <div id="root"></div>
192
- <script type="module" src="/src/client/main.tsx"></script>
193
- </body>
194
- </html>
195
- `;
196
- function fullstackViteConfig() {
197
- return `import { defineConfig } from 'vite'
198
- import react from '@vitejs/plugin-react'
199
-
200
- export default defineConfig({
201
- plugins: [react()],
202
- build: {
203
- outDir: 'dist/client',
204
- },
205
- })
206
- `;
77
+ function rootPnpmWorkspace() {
78
+ return "packages:\n - 'packages/*'\n";
207
79
  }
208
- const FULLSTACK_DOCKERIGNORE = `node_modules
209
- dist
210
- .git
211
- .env
212
- npm-debug.log*
213
- pnpm-lock.yaml
214
- yarn.lock
215
- `;
216
- const FULLSTACK_DOCKER_COMPOSE = `services:
80
+ function stackComposeYaml() {
81
+ return `services:
217
82
  db:
218
83
  image: postgres:15
219
84
  restart: unless-stopped
@@ -229,211 +94,70 @@ const FULLSTACK_DOCKER_COMPOSE = `services:
229
94
  volumes:
230
95
  db-data:
231
96
  `;
232
- function fullstackDockerfile() {
233
- return `FROM node:20-slim AS builder
234
- WORKDIR /app
235
-
236
- COPY package*.json ./
237
- COPY pnpm-lock.yaml* ./
238
- RUN npm install
239
-
240
- COPY . .
241
- RUN npm run build
242
-
243
- FROM node:20-slim AS runner
244
- WORKDIR /app
245
- ENV NODE_ENV=production
246
-
247
- COPY --from=builder /app/package*.json ./
248
- COPY --from=builder /app/node_modules ./node_modules
249
- COPY --from=builder /app/dist ./dist
250
- RUN npm prune --omit=dev || true
251
-
252
- EXPOSE 8080
253
- CMD ["node", "dist/server/index.js"]
254
- `;
255
97
  }
256
- function fullstackFiles(pkgName) {
257
- return {
258
- 'package.json': fullstackPackageJson(pkgName),
98
+ async function scaffoldRootFiles(baseDir, baseName) {
99
+ const files = {
100
+ 'package.json': rootPackageJson(baseName),
101
+ 'pnpm-workspace.yaml': rootPnpmWorkspace(),
102
+ 'docker-compose.yml': stackComposeYaml(),
259
103
  'tsconfig.json': baseTsConfig({
260
- lib: ['ES2020', 'DOM'],
261
- types: ['node'],
262
- jsx: 'react-jsx',
263
- include: ['src/**/*', 'vite.config.ts'],
264
- }),
265
- 'tsconfig.server.json': baseTsConfig({
266
- types: ['node'],
267
- rootDir: 'src',
104
+ rootDir: '.',
268
105
  outDir: 'dist',
269
- include: ['src/server/**/*', 'src/contract/**/*'],
106
+ include: ['packages/**/*', 'scripts/**/*', 'src/**/*'],
270
107
  }),
271
- 'vite.config.ts': fullstackViteConfig(),
272
- 'src/contract/index.ts': fullstackContractTs(),
273
- 'src/server/index.ts': fullstackServerIndexTs(),
274
- 'src/server/env.d.ts': "declare namespace NodeJS { interface ProcessEnv { DATABASE_URL?: string; PORT?: string } }\n",
275
- 'src/client/api.ts': fullstackClientApi(),
276
- 'src/client/App.tsx': FULLSTACK_APP_TSX,
277
- 'src/client/main.tsx': FULLSTACK_MAIN_TSX,
278
- 'src/client/styles.css': FULLSTACK_STYLES,
279
- 'index.html': FULLSTACK_INDEX_HTML,
280
- 'scripts/docker.ts': fullstackDockerCliScript(pkgName),
281
- '.dockerignore': FULLSTACK_DOCKERIGNORE,
282
- 'docker-compose.yml': FULLSTACK_DOCKER_COMPOSE,
283
- Dockerfile: fullstackDockerfile(),
284
- '.env.example': 'PORT=8080\nDATABASE_URL=postgresql://postgres:postgres@localhost:5432/rrroutes\nVITE_API_URL=http://localhost:8080\n',
285
- 'README.md': `# ${pkgName}
286
-
287
- Full stack (API + Vite web) scaffolded by manager-cli.
288
- - dev: \`npm run dev\` (runs API + Vite client)
289
- - build: \`npm run build\` (server to dist/server, client to dist/client)
290
- - start: \`npm start\` (serves API and static client from dist)
291
- - docker helper: \`npm run docker:up\` (build + run), \`npm run docker:logs\`, \`npm run docker:stop\`
292
- - db helper: \`npm run db:up\` to start Postgres via docker-compose (uses DATABASE_URL)
293
- `,
108
+ ...basePackageFiles(),
294
109
  };
295
- }
296
- function fullstackContractTs() {
297
- return `import { defineSocketEvents, finalize, resource } from '@emeryld/rrroutes-contract'
298
- import { z } from 'zod'
299
-
300
- const routes = resource('/api')
301
- .sub(
302
- resource('health')
303
- .get({
304
- outputSchema: z.object({
305
- status: z.literal('ok'),
306
- at: z.string(),
307
- db: z.string(),
308
- }),
309
- description: 'Health check with DB status.',
310
- })
311
- .done(),
312
- )
313
- .done()
314
-
315
- export const registry = finalize(routes)
316
-
317
- const sockets = defineSocketEvents(
318
- {
319
- pingPayload: z.object({
320
- note: z.string().default('ping'),
321
- sentAt: z.string(),
322
- }),
323
- pongPayload: z.object({
324
- ok: z.boolean(),
325
- receivedAt: z.string(),
326
- echo: z.string().optional(),
327
- }),
328
- },
329
- {
330
- 'health:ping': {
331
- message: z.object({
332
- note: z.string().default('ping'),
333
- }),
334
- },
335
- 'health:pong': {
336
- message: z.object({
337
- ok: z.literal(true),
338
- at: z.string(),
339
- echo: z.string().optional(),
340
- }),
341
- },
342
- },
343
- )
344
-
345
- export const socketConfig = sockets.config
346
- export const socketEvents = sockets.events
347
- export type AppRegistry = typeof registry
348
- `;
349
- }
350
- function fullstackClientApi() {
351
- return `import { QueryClient } from '@tanstack/react-query'
352
- import { createRouteClient } from '@emeryld/rrroutes-client'
353
- import { registry } from '../contract'
354
-
355
- const baseUrl = import.meta.env.VITE_API_URL ?? 'http://localhost:8080'
356
- export const queryClient = new QueryClient()
357
-
358
- export const routeClient = createRouteClient({
359
- baseUrl,
360
- queryClient,
361
- environment: import.meta.env.MODE === 'production' ? 'production' : 'development',
362
- })
363
-
364
- export const healthGet = routeClient.build(registry.byKey['GET /api/health'])
365
- `;
366
- }
367
- function fullstackDockerCliScript(pkgName) {
368
- return `#!/usr/bin/env tsx
369
- import { readFile } from 'node:fs/promises'
370
- import path from 'node:path'
371
- import { fileURLToPath } from 'node:url'
372
- import { Docker } from 'docker-cli-js'
373
-
374
- const __filename = fileURLToPath(import.meta.url)
375
- const __dirname = path.dirname(__filename)
376
- const pkgRaw = await readFile(path.join(__dirname, '..', 'package.json'), 'utf8')
377
- const pkg = JSON.parse(pkgRaw) as { name?: string }
378
- const image = \`\${pkg.name ?? '${pkgName}'}:latest\`
379
- const container =
380
- (pkg.name ?? '${pkgName}').replace(/[^a-z0-9]/gi, '-').replace(/^-+|-+$/g, '') || 'rrr-fullstack'
381
- const port = process.env.PORT ?? '8080'
382
- const docker = new Docker({ spawnOptions: { stdio: 'inherit' } })
383
-
384
- async function main() {
385
- const [command = 'help'] = process.argv.slice(2)
386
- if (command === 'help') return printHelp()
387
- if (command === 'build') return docker.command(\`build -t \${image} .\`)
388
- if (command === 'up') {
389
- await docker.command(\`build -t \${image} .\`)
390
- return docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
391
- }
392
- if (command === 'run') {
393
- return docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
394
- }
395
- if (command === 'logs') return docker.command(\`logs -f \${container}\`)
396
- if (command === 'stop') return docker.command(\`stop \${container}\`)
397
- if (command === 'clean') {
398
- try {
399
- await docker.command(\`rm -f \${container}\`)
400
- } catch (error) {
401
- console.warn(String(error))
110
+ for (const [relative, contents] of Object.entries(files)) {
111
+ // eslint-disable-next-line no-await-in-loop
112
+ await writeFileIfMissing(baseDir, relative, contents);
402
113
  }
403
- return
404
- }
405
- return printHelp()
406
- }
407
-
408
- function printHelp() {
409
- console.log(
410
- [
411
- 'Docker helper commands:',
412
- ' build -> docker build -t ${pkgName}:latest .',
413
- ' up -> build then run in detached mode',
414
- ' run -> run existing image detached',
415
- ' logs -> docker logs -f <container>',
416
- ' stop -> docker stop <container>',
417
- ' clean -> docker rm -f <container>',
418
- ].join('\\n'),
419
- )
420
- }
421
-
422
- main().catch((err) => {
423
- console.error(err)
424
- process.exit(1)
425
- })
426
- `;
427
114
  }
428
115
  export const fullstackVariant = {
429
116
  id: 'rrr-fullstack',
430
- label: 'full stack service (api + web)',
431
- defaultDir: 'packages/rrr-fullstack',
117
+ label: 'rrr fullstack (contract + server + client + docker)',
118
+ defaultDir: 'rrrfull-stack',
119
+ summary: 'End-to-end RRRoutes stack: pnpm workspace with contract, server, client, and docker helper packages.',
120
+ keyFiles: [
121
+ 'package.json',
122
+ 'pnpm-workspace.yaml',
123
+ 'docker-compose.yml',
124
+ 'packages/<name>-contract',
125
+ 'packages/<name>-server',
126
+ 'packages/<name>-client',
127
+ 'packages/<name>-docker',
128
+ ],
129
+ scripts: FULLSTACK_SCRIPTS,
130
+ notes: [
131
+ 'Generates four packages and a workspace root; great starting point when you need the whole stack.',
132
+ 'Use --name to control the workspace prefix (default is the target folder name).',
133
+ ],
432
134
  async scaffold(ctx) {
433
- const files = fullstackFiles(ctx.pkgName);
434
- for (const [relative, contents] of Object.entries(files)) {
435
- // eslint-disable-next-line no-await-in-loop
436
- await writeFileIfMissing(ctx.targetDir, relative, contents);
437
- }
135
+ const baseName = ctx.pkgName;
136
+ const names = deriveNames(baseName);
137
+ const dirs = deriveDirs(ctx.targetDir, baseName);
138
+ // Root workspace files
139
+ await scaffoldRootFiles(dirs.root, baseName);
140
+ // Contract package (reuse contract variant)
141
+ await contractVariant.scaffold({
142
+ targetDir: dirs.contract,
143
+ pkgName: names.contract,
144
+ });
145
+ // Server package
146
+ await serverVariant.scaffold({
147
+ targetDir: dirs.server,
148
+ pkgName: names.server,
149
+ contractName: names.contract,
150
+ });
151
+ // Client package
152
+ await clientVariant.scaffold({
153
+ targetDir: dirs.client,
154
+ pkgName: names.client,
155
+ contractName: names.contract,
156
+ });
157
+ // Docker helper package (reuse docker variant)
158
+ await dockerVariant.scaffold({
159
+ targetDir: dirs.docker,
160
+ pkgName: names.docker,
161
+ });
438
162
  },
439
163
  };
@@ -1,6 +1,18 @@
1
- import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
2
+ const SERVER_SCRIPTS = [
3
+ 'dev',
4
+ 'build',
5
+ 'typecheck',
6
+ 'lint',
7
+ 'lint:fix',
8
+ 'format',
9
+ 'format:check',
10
+ 'clean',
11
+ 'test',
12
+ 'start',
13
+ ];
2
14
  const CONTRACT_IMPORT_PLACEHOLDER = '@your-scope/contract';
3
- function serverIndexTs(contractImport) {
15
+ export function serverIndexTs(contractImport) {
4
16
  return `import 'dotenv/config'
5
17
  import http from 'node:http'
6
18
  import express from 'express'
@@ -46,23 +58,15 @@ server.listen(PORT, () => {
46
58
  })
47
59
  `;
48
60
  }
49
- function serverPackageJson(name) {
50
- return `${JSON.stringify({
61
+ export function serverPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLDER) {
62
+ return basePackageJson({
51
63
  name,
52
- version: '0.1.0',
53
64
  private: false,
54
- type: 'module',
55
- main: 'dist/index.js',
56
- types: 'dist/index.d.ts',
57
- files: ['dist'],
58
- scripts: {
59
- dev: 'node --loader ts-node/esm src/index.ts',
60
- build: 'tsc -p tsconfig.json',
61
- typecheck: 'tsc -p tsconfig.json --noEmit',
65
+ scripts: baseScripts('tsx watch --env-file .env src/index.ts', {
62
66
  start: 'node dist/index.js',
63
- },
67
+ }),
64
68
  dependencies: {
65
- '@emeryld/rrroutes-contract': '^2.5.2',
69
+ [contractName]: 'workspace:*',
66
70
  '@emeryld/rrroutes-server': '^2.4.1',
67
71
  cors: '^2.8.5',
68
72
  dotenv: '^16.4.5',
@@ -70,34 +74,63 @@ function serverPackageJson(name) {
70
74
  zod: '^4.2.1',
71
75
  },
72
76
  devDependencies: {
77
+ ...BASE_LINT_DEV_DEPENDENCIES,
73
78
  '@types/cors': '^2.8.5',
74
79
  '@types/express': '^5.0.6',
75
80
  '@types/node': '^24.10.2',
76
- 'ts-node': '^10.9.2',
77
- typescript: '^5.9.3',
78
81
  },
79
- }, null, 2)}\n`;
82
+ });
80
83
  }
81
84
  function serverFiles(pkgName, contractImport) {
82
85
  return {
83
- 'package.json': serverPackageJson(pkgName),
86
+ 'package.json': serverPackageJson(pkgName, contractImport),
84
87
  'tsconfig.json': baseTsConfig({ types: ['node'] }),
88
+ ...basePackageFiles(),
85
89
  'src/index.ts': serverIndexTs(contractImport),
86
90
  '.env.example': 'PORT=4000\n',
87
- 'README.md': `# ${pkgName}
88
-
89
- Starter RRRoutes server scaffold.
90
- - update the contract import in src/index.ts if needed (${contractImport})
91
- - run \`npm install\` then \`npm run dev\` to start the API
92
- `,
91
+ 'README.md': buildReadme({
92
+ name: pkgName,
93
+ description: 'Starter RRRoutes server scaffold.',
94
+ scripts: SERVER_SCRIPTS,
95
+ sections: [
96
+ {
97
+ title: 'Getting Started',
98
+ lines: [
99
+ '- Install deps: `npm install` (or `pnpm install`)',
100
+ '- Copy `.env.example` to `.env` as needed.',
101
+ '- Start dev API: `npm run dev`',
102
+ '- Build output: `npm run build`',
103
+ ],
104
+ },
105
+ {
106
+ title: 'Usage',
107
+ lines: [
108
+ `- Update the contract import in \`src/index.ts\` if needed (${contractImport}).`,
109
+ '- Start compiled server: `npm run start` after building.',
110
+ ],
111
+ },
112
+ {
113
+ title: 'Environment',
114
+ lines: ['- `PORT` sets the HTTP port (default 4000).'],
115
+ },
116
+ ],
117
+ }),
93
118
  };
94
119
  }
95
120
  export const serverVariant = {
96
121
  id: 'rrr-server',
97
122
  label: 'rrr server',
98
123
  defaultDir: 'packages/rrr-server',
124
+ summary: 'Express + RRRoutes server wired to a contract import; ships a health route.',
125
+ keyFiles: ['src/index.ts', '.env.example', 'README.md'],
126
+ scripts: SERVER_SCRIPTS,
127
+ notes: [
128
+ 'Set the contract import via --contract or by editing src/index.ts.',
129
+ 'Includes start script for compiled output and dotenv-ready dev script.',
130
+ ],
99
131
  async scaffold(ctx) {
100
- const files = serverFiles(ctx.pkgName, CONTRACT_IMPORT_PLACEHOLDER);
132
+ const contractImport = ctx.contractName ?? CONTRACT_IMPORT_PLACEHOLDER;
133
+ const files = serverFiles(ctx.pkgName, contractImport);
101
134
  for (const [relative, contents] of Object.entries(files)) {
102
135
  // eslint-disable-next-line no-await-in-loop
103
136
  await writeFileIfMissing(ctx.targetDir, relative, contents);