@emeryld/manager 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,3 +31,14 @@ Interactive release helper for pnpm monorepos. Install it as a local dev depende
31
31
  ## Testing the loader registration
32
32
 
33
33
  Run `pnpm test` to ensure the helper CLI always generates a `--import data:text...` snippet that registers `ts-node/esm.mjs` with **the actual script file path**. This guards against regressions that would make Node reject the loader and crash before the interactive menu appears.
34
+
35
+ ## Package creator variants
36
+
37
+ Use `Create package` inside the CLI to scaffold a starter workspace package (the flow auto-installs deps and builds when a `build` script exists). Templates:
38
+
39
+ - **rrr contract** – Shared RRRoutes registry and socket config for server/client packages. Exported registry lives in `src/index.ts`.
40
+ - **rrr server** – Express + RRRoutes API wired to the contract placeholder, `dev` via ts-node, `.env.example` included.
41
+ - **rrr client** – Backend-agnostic RRRoutes client helper with React Query setup in `src/index.ts`.
42
+ - **empty package** – Minimal TypeScript library with build/typecheck scripts.
43
+ - **dockerized service** – Express health API with Dockerfile, .dockerignore, and Docker helper scripts (`docker:build`, `docker:up`, `docker:logs`, `docker:stop`, `docker:clean`) powered by `docker-cli-js`.
44
+ - **full stack service (api + web)** – Express API + Vite React client, unified build/start, Dockerfile + helper scripts (same `docker:*` commands as above).
@@ -202,7 +202,9 @@ async function postCreateTasks(targetDir) {
202
202
  async function gatherTarget() {
203
203
  const variant = await promptForVariant();
204
204
  const targetDir = await promptForTargetDir(variant.defaultDir);
205
- const pkgName = derivePackageName(targetDir);
205
+ const fallbackName = derivePackageName(targetDir);
206
+ const nameAnswer = await askLine(`Package name? (${fallbackName}): `);
207
+ const pkgName = (nameAnswer || fallbackName).trim() || fallbackName;
206
208
  await ensureTargetDir(targetDir);
207
209
  return { variant, targetDir, pkgName };
208
210
  }
@@ -13,6 +13,12 @@ function dockerPackageJson(name) {
13
13
  build: 'tsc -p tsconfig.json',
14
14
  typecheck: 'tsc -p tsconfig.json --noEmit',
15
15
  start: 'node dist/index.js',
16
+ 'docker:cli': 'tsx scripts/docker.ts',
17
+ 'docker:build': 'npm run docker:cli -- build',
18
+ 'docker:up': 'npm run docker:cli -- up',
19
+ 'docker:logs': 'npm run docker:cli -- logs',
20
+ 'docker:stop': 'npm run docker:cli -- stop',
21
+ 'docker:clean': 'npm run docker:cli -- clean',
16
22
  },
17
23
  dependencies: {
18
24
  cors: '^2.8.5',
@@ -22,6 +28,7 @@ function dockerPackageJson(name) {
22
28
  '@types/cors': '^2.8.5',
23
29
  '@types/express': '^5.0.6',
24
30
  '@types/node': '^24.10.2',
31
+ 'docker-cli-js': '^3.0.9',
25
32
  tsx: '^4.19.0',
26
33
  typescript: '^5.9.3',
27
34
  },
@@ -82,6 +89,7 @@ function dockerFiles(pkgName) {
82
89
  'package.json': dockerPackageJson(pkgName),
83
90
  'tsconfig.json': baseTsConfig({ types: ['node'] }),
84
91
  'src/index.ts': dockerIndexTs(),
92
+ 'scripts/docker.ts': dockerCliScript(pkgName),
85
93
  '.dockerignore': DOCKER_DOCKERIGNORE,
86
94
  Dockerfile: dockerDockerfile(),
87
95
  'README.md': `# ${pkgName}
@@ -89,11 +97,71 @@ function dockerFiles(pkgName) {
89
97
  Dockerized service scaffolded by manager-cli.
90
98
  - develop locally with \`npm run dev\`
91
99
  - build with \`npm run build\` and start with \`npm start\`
92
- - build/publish container: \`docker build -t ${pkgName}:latest .\`
93
- - run container: \`docker run -p 3000:3000 ${pkgName}:latest\`
100
+ - docker helper: \`npm run docker:up\` (build + run), \`npm run docker:logs\`, \`npm run docker:stop\`
94
101
  `,
95
102
  };
96
103
  }
104
+ function dockerCliScript(pkgName) {
105
+ return `#!/usr/bin/env tsx
106
+ import { readFile } from 'node:fs/promises'
107
+ import path from 'node:path'
108
+ import { fileURLToPath } from 'node:url'
109
+ import { Docker } from 'docker-cli-js'
110
+
111
+ const __filename = fileURLToPath(import.meta.url)
112
+ const __dirname = path.dirname(__filename)
113
+ const pkgRaw = await readFile(path.join(__dirname, '..', 'package.json'), 'utf8')
114
+ const pkg = JSON.parse(pkgRaw) as { name?: string }
115
+ const image = \`\${pkg.name ?? '${pkgName}'}:latest\`
116
+ const container =
117
+ (pkg.name ?? '${pkgName}').replace(/[^a-z0-9]/gi, '-').replace(/^-+|-+$/g, '') || 'rrr-service'
118
+ const port = process.env.PORT ?? '3000'
119
+ const docker = new Docker({ spawnOptions: { stdio: 'inherit' } })
120
+
121
+ async function main() {
122
+ const [command = 'help'] = process.argv.slice(2)
123
+ if (command === 'help') return printHelp()
124
+ if (command === 'build') return docker.command(\`build -t \${image} .\`)
125
+ if (command === 'up') {
126
+ await docker.command(\`build -t \${image} .\`)
127
+ return docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
128
+ }
129
+ if (command === 'run') {
130
+ return docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
131
+ }
132
+ if (command === 'logs') return docker.command(\`logs -f \${container}\`)
133
+ if (command === 'stop') return docker.command(\`stop \${container}\`)
134
+ if (command === 'clean') {
135
+ try {
136
+ await docker.command(\`rm -f \${container}\`)
137
+ } catch (error) {
138
+ console.warn(String(error))
139
+ }
140
+ return
141
+ }
142
+ return printHelp()
143
+ }
144
+
145
+ function printHelp() {
146
+ console.log(
147
+ [
148
+ 'Docker helper commands:',
149
+ ' build -> docker build -t ${pkgName}:latest .',
150
+ ' up -> build then run in detached mode',
151
+ ' run -> run existing image detached',
152
+ ' logs -> docker logs -f <container>',
153
+ ' stop -> docker stop <container>',
154
+ ' clean -> docker rm -f <container>',
155
+ ].join('\\n'),
156
+ )
157
+ }
158
+
159
+ main().catch((err) => {
160
+ console.error(err)
161
+ process.exit(1)
162
+ })
163
+ `;
164
+ }
97
165
  export const dockerVariant = {
98
166
  id: 'rrr-docker',
99
167
  label: 'dockerized service',
@@ -16,20 +16,36 @@ function fullstackPackageJson(name) {
16
16
  'build:client': 'vite build',
17
17
  start: 'node dist/server/index.js',
18
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',
19
27
  },
20
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',
21
33
  cors: '^2.8.5',
22
34
  express: '^5.1.0',
35
+ pg: '^8.13.1',
23
36
  react: '^18.3.1',
24
37
  'react-dom': '^18.3.1',
38
+ zod: '^4.2.1',
25
39
  },
26
40
  devDependencies: {
27
41
  '@types/express': '^5.0.6',
28
42
  '@types/node': '^24.10.2',
29
43
  '@types/react': '^18.3.27',
30
44
  '@types/react-dom': '^18.3.7',
45
+ '@types/pg': '^8.11.10',
31
46
  '@vitejs/plugin-react': '^4.3.4',
32
47
  concurrently: '^8.2.0',
48
+ 'docker-cli-js': '^3.0.9',
33
49
  tsx: '^4.19.0',
34
50
  typescript: '^5.9.3',
35
51
  vite: '^6.4.1',
@@ -41,13 +57,49 @@ function fullstackServerIndexTs() {
41
57
  import path from 'node:path'
42
58
  import express from 'express'
43
59
  import cors from 'cors'
60
+ import { Pool } from 'pg'
61
+ import { createRRRoute } from '@emeryld/rrroutes-server'
62
+ import { registry } from '../contract/index.js'
44
63
 
45
64
  const app = express()
46
65
  app.use(cors({ origin: '*' }))
47
66
  app.use(express.json())
48
67
 
49
- app.get('/api/health', (_req, res) => {
50
- res.json({ status: 'ok', at: new Date().toISOString() })
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
+ },
51
103
  })
52
104
 
53
105
  const clientDir = path.resolve(__dirname, '../client')
@@ -68,14 +120,43 @@ app.listen(PORT, () => {
68
120
  `;
69
121
  }
70
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
+ }
71
146
 
72
147
  export function App() {
73
148
  return (
74
- <main style={{ fontFamily: 'Inter, system-ui, sans-serif', padding: 24 }}>
75
- <h1>Full stack service</h1>
76
- <p>Backend: <code>/api/health</code> responds with status + timestamp.</p>
77
- <p>Edit <code>src/client/App.tsx</code> to start building.</p>
78
- </main>
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>
79
160
  )
80
161
  }
81
162
  `;
@@ -132,6 +213,22 @@ npm-debug.log*
132
213
  pnpm-lock.yaml
133
214
  yarn.lock
134
215
  `;
216
+ const FULLSTACK_DOCKER_COMPOSE = `services:
217
+ db:
218
+ image: postgres:15
219
+ restart: unless-stopped
220
+ environment:
221
+ POSTGRES_USER: postgres
222
+ POSTGRES_PASSWORD: postgres
223
+ POSTGRES_DB: rrroutes
224
+ ports:
225
+ - '5432:5432'
226
+ volumes:
227
+ - db-data:/var/lib/postgresql/data
228
+
229
+ volumes:
230
+ db-data:
231
+ `;
135
232
  function fullstackDockerfile() {
136
233
  return `FROM node:20-slim AS builder
137
234
  WORKDIR /app
@@ -167,29 +264,167 @@ function fullstackFiles(pkgName) {
167
264
  }),
168
265
  'tsconfig.server.json': baseTsConfig({
169
266
  types: ['node'],
170
- rootDir: 'src/server',
171
- outDir: 'dist/server',
172
- include: ['src/server/**/*'],
267
+ rootDir: 'src',
268
+ outDir: 'dist',
269
+ include: ['src/server/**/*', 'src/contract/**/*'],
173
270
  }),
174
271
  'vite.config.ts': fullstackViteConfig(),
272
+ 'src/contract/index.ts': fullstackContractTs(),
175
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(),
176
276
  'src/client/App.tsx': FULLSTACK_APP_TSX,
177
277
  'src/client/main.tsx': FULLSTACK_MAIN_TSX,
178
278
  'src/client/styles.css': FULLSTACK_STYLES,
179
279
  'index.html': FULLSTACK_INDEX_HTML,
280
+ 'scripts/docker.ts': fullstackDockerCliScript(pkgName),
180
281
  '.dockerignore': FULLSTACK_DOCKERIGNORE,
282
+ 'docker-compose.yml': FULLSTACK_DOCKER_COMPOSE,
181
283
  Dockerfile: fullstackDockerfile(),
182
- '.env.example': 'PORT=8080\n',
284
+ '.env.example': 'PORT=8080\nDATABASE_URL=postgresql://postgres:postgres@localhost:5432/rrroutes\nVITE_API_URL=http://localhost:8080\n',
183
285
  'README.md': `# ${pkgName}
184
286
 
185
287
  Full stack (API + Vite web) scaffolded by manager-cli.
186
288
  - dev: \`npm run dev\` (runs API + Vite client)
187
289
  - build: \`npm run build\` (server to dist/server, client to dist/client)
188
290
  - start: \`npm start\` (serves API and static client from dist)
189
- - docker: \`docker build -t ${pkgName}:latest .\` then \`docker run -p 8080:8080 ${pkgName}:latest\`
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)
190
293
  `,
191
294
  };
192
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))
402
+ }
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
+ }
193
428
  export const fullstackVariant = {
194
429
  id: 'rrr-fullstack',
195
430
  label: 'full stack service (api + web)',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",