@emeryld/manager 0.3.3 → 0.4.0

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
@@ -32,13 +32,27 @@ Interactive release helper for pnpm monorepos. Install it as a local dev depende
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
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).
35
+ ## Package creator overview
36
+
37
+ Use `Create package` inside the CLI to scaffold starter packages. The flow will prompt for a variant, target path, and package name, then generate the files and attempt `pnpm install` and `pnpm run build` when a build script exists.
38
+
39
+ ### How to scaffold
40
+ - From the workspace root, run `pnpm manager-cli` choose `Create package`.
41
+ - Pick a variant, confirm the target directory, and override the package name if desired.
42
+ - After scaffolding, the new package’s README covers variant-specific details and scripts.
43
+
44
+ ### Variant quick view
45
+
46
+ | Variant | Default dir | Purpose | Key scripts |
47
+ | --- | --- | --- | --- |
48
+ | rrr contract | `packages/rrr-contract` | Shared RRRoutes contract registry and socket config | `dev`, `build`, `typecheck`, `lint`, `format` |
49
+ | rrr server | `packages/rrr-server` | Express + RRRoutes API wired to a contract import | `dev`, `build`, `start`, `lint`, `format` |
50
+ | rrr client | `packages/rrr-client` | RRRoutes client helpers + React Query setup | `dev`, `build`, `lint`, `format` |
51
+ | empty package | `packages/rrr-empty` | Minimal TypeScript starter | `dev`, `build`, `lint`, `format` |
52
+ | dockerized service | `packages/rrr-docker` | Express service with Dockerfile and helper CLI | `dev`, `build`, `start`, `docker:*` |
53
+ | full stack stack | `rrrfull-stack` | Workspace with contract + server + client + docker | `dev`, `build`, `lint`, `docker:*` (root scripts orchestrate) |
54
+
55
+ ### Common scripts and helpers
56
+ - `dev` (watch), `build`, `typecheck`, `lint`/`lint:fix`, `format`/`format:check`, `clean`, `test` are scaffolded where relevant.
57
+ - Docker-focused variants expose `docker:build`, `docker:up`, `docker:dev`, `docker:logs`, `docker:stop`, `docker:clean`, `docker:reset`.
58
+ - Husky + lint-staged and ESLint/Prettier configs are included in every package for consistent DX.
@@ -40,5 +40,188 @@ export function baseTsConfig(options) {
40
40
  jsx: options?.jsx,
41
41
  },
42
42
  include: options?.include ?? ['src/**/*'],
43
+ exclude: options?.exclude ?? ['dist', 'node_modules'],
43
44
  }, null, 2)}\n`;
44
45
  }
46
+ export function baseEslintConfig(tsconfigPath = './tsconfig.json') {
47
+ return `import tseslint from 'typescript-eslint'
48
+ import prettierPlugin from 'eslint-plugin-prettier'
49
+
50
+ export default tseslint.config(
51
+ { ignores: ['dist', 'node_modules'] },
52
+ ...tseslint.configs.recommendedTypeChecked,
53
+ {
54
+ files: ['**/*.ts', '**/*.tsx'],
55
+ languageOptions: {
56
+ parserOptions: {
57
+ project: ${JSON.stringify(tsconfigPath)},
58
+ tsconfigRootDir: import.meta.dirname,
59
+ },
60
+ },
61
+ plugins: {
62
+ prettier: prettierPlugin,
63
+ },
64
+ rules: {
65
+ 'prettier/prettier': 'error',
66
+ '@typescript-eslint/consistent-type-imports': 'warn',
67
+ '@typescript-eslint/no-unused-vars': [
68
+ 'warn',
69
+ { argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^ignore' },
70
+ ],
71
+ },
72
+ },
73
+ )
74
+ `;
75
+ }
76
+ export function basePrettierConfig() {
77
+ return `export default {
78
+ singleQuote: true,
79
+ semi: false,
80
+ trailingComma: 'all',
81
+ printWidth: 100,
82
+ }
83
+ `;
84
+ }
85
+ export const PRETTIER_IGNORE = ['node_modules', 'dist', '.turbo', 'coverage', '*.log'].join('\n');
86
+ export const LINT_STAGED_CONFIG = {
87
+ '*.{ts,tsx}': ['eslint --fix'],
88
+ '*.{ts,tsx,js,jsx,json,md,css,html}': ['prettier --write'],
89
+ };
90
+ export const HUSKY_PRE_COMMIT = `#!/usr/bin/env sh
91
+ . "$(dirname -- "$0")/_/husky.sh"
92
+
93
+ pnpm lint-staged
94
+ `;
95
+ export function vscodeSettings() {
96
+ return `${JSON.stringify({
97
+ 'editor.defaultFormatter': 'esbenp.prettier-vscode',
98
+ 'editor.formatOnSave': true,
99
+ 'editor.codeActionsOnSave': {
100
+ 'source.fixAll.eslint': true,
101
+ },
102
+ 'eslint.useFlatConfig': true,
103
+ 'eslint.validate': ['typescript', 'javascript'],
104
+ 'files.eol': '\n',
105
+ 'prettier.requireConfig': true,
106
+ }, null, 2)}\n`;
107
+ }
108
+ export const BASE_LINT_DEV_DEPENDENCIES = {
109
+ typescript: '^5.9.3',
110
+ eslint: '^9.12.0',
111
+ 'eslint-plugin-prettier': '^5.2.1',
112
+ prettier: '^3.3.3',
113
+ 'typescript-eslint': '^8.10.0',
114
+ rimraf: '^6.0.1',
115
+ tsx: '^4.19.0',
116
+ husky: '^9.1.6',
117
+ 'lint-staged': '^15.2.10',
118
+ '@emeryld/manager': 'latest',
119
+ };
120
+ const DEFAULT_GITIGNORE_ENTRIES = [
121
+ 'node_modules',
122
+ 'dist',
123
+ '.turbo',
124
+ '.DS_Store',
125
+ '.env',
126
+ 'coverage',
127
+ '*.log',
128
+ ];
129
+ export function gitignoreFrom(entries = DEFAULT_GITIGNORE_ENTRIES) {
130
+ return entries.join('\n');
131
+ }
132
+ export function baseScripts(devCommand, extras) {
133
+ return {
134
+ dev: devCommand,
135
+ build: 'tsc -p tsconfig.json',
136
+ typecheck: 'tsc -p tsconfig.json --noEmit',
137
+ lint: 'eslint . --max-warnings=0',
138
+ 'lint:fix': 'eslint . --fix',
139
+ 'lint-staged': 'lint-staged',
140
+ format: 'prettier . --write',
141
+ 'format:check': 'prettier . --check',
142
+ clean: 'rimraf dist .turbo coverage',
143
+ test: "node -e \"console.log('No tests yet')\"",
144
+ prepare: 'husky install || true',
145
+ ...extras,
146
+ };
147
+ }
148
+ export function basePackageFiles(options) {
149
+ return {
150
+ 'eslint.config.js': baseEslintConfig(options?.tsconfigPath),
151
+ 'prettier.config.js': basePrettierConfig(),
152
+ '.prettierignore': `${PRETTIER_IGNORE}\n`,
153
+ '.vscode/settings.json': vscodeSettings(),
154
+ '.husky/pre-commit': HUSKY_PRE_COMMIT,
155
+ '.gitignore': gitignoreFrom(options?.gitignoreEntries),
156
+ };
157
+ }
158
+ const SCRIPT_DESCRIPTIONS = {
159
+ dev: 'Watch and rebuild on change',
160
+ build: 'Type-check and emit to dist/',
161
+ typecheck: 'Type-check only (no emit)',
162
+ lint: 'Run ESLint with flat config',
163
+ 'lint:fix': 'Fix lint issues automatically',
164
+ 'lint-staged': 'Run lint/format on staged files',
165
+ format: 'Format code with Prettier',
166
+ 'format:check': 'Check formatting without writing',
167
+ clean: 'Remove build artifacts and caches',
168
+ test: 'Placeholder test script',
169
+ start: 'Run the built output',
170
+ 'docker:build': 'Build docker image',
171
+ 'docker:up': 'Build and start container',
172
+ 'docker:dev': 'Build, start, and tail logs',
173
+ 'docker:logs': 'Tail docker logs',
174
+ 'docker:stop': 'Stop running container',
175
+ 'docker:clean': 'Stop and remove container',
176
+ 'docker:reset': 'Remove container and image',
177
+ };
178
+ export function buildReadme(options) {
179
+ const scripts = options.scripts ?? [];
180
+ const scriptLines = scripts.map((script) => {
181
+ const desc = SCRIPT_DESCRIPTIONS[script];
182
+ return desc ? `- \`npm run ${script}\` - ${desc}` : `- \`npm run ${script}\``;
183
+ });
184
+ const sections = [...(options.sections ?? [])];
185
+ if (scriptLines.length > 0) {
186
+ sections.push({ title: 'Scripts', lines: scriptLines });
187
+ }
188
+ const lines = [`# ${options.name}`, ''];
189
+ if (options.description) {
190
+ lines.push(options.description, '');
191
+ }
192
+ sections.forEach((section) => {
193
+ lines.push(`## ${section.title}`, ...section.lines, '');
194
+ });
195
+ return `${lines.join('\n').trim()}\n`;
196
+ }
197
+ function stripUndefined(obj) {
198
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
199
+ }
200
+ export function basePackageJson(options) {
201
+ const applyDefaults = options.useDefaults ?? true;
202
+ const pkg = stripUndefined({
203
+ name: options.name,
204
+ version: options.version ?? '0.1.0',
205
+ private: options.private ?? true,
206
+ ...(applyDefaults
207
+ ? {
208
+ type: options.type ?? 'module',
209
+ main: options.main ?? 'dist/index.js',
210
+ types: options.types ?? 'dist/index.d.ts',
211
+ files: options.files ?? ['dist'],
212
+ }
213
+ : stripUndefined({
214
+ type: options.type,
215
+ main: options.main,
216
+ types: options.types,
217
+ files: options.files,
218
+ })),
219
+ exports: options.exports,
220
+ scripts: options.scripts,
221
+ dependencies: options.dependencies,
222
+ devDependencies: options.devDependencies,
223
+ 'lint-staged': LINT_STAGED_CONFIG,
224
+ ...options.extraFields,
225
+ });
226
+ return `${JSON.stringify(pkg, null, 2)}\n`;
227
+ }
@@ -1,6 +1,6 @@
1
- import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
2
2
  const CONTRACT_IMPORT_PLACEHOLDER = '@your-scope/contract';
3
- function clientIndexTs(contractImport) {
3
+ export function clientIndexTs(contractImport) {
4
4
  return `import { QueryClient } from '@tanstack/react-query'
5
5
  import { createRouteClient } from '@emeryld/rrroutes-client'
6
6
  import { registry } from '${contractImport}'
@@ -18,42 +18,65 @@ export const healthGet = routeClient.build(registry.byKey['GET /api/health'])
18
18
  export const healthPost = routeClient.build(registry.byKey['POST /api/health'])
19
19
  `;
20
20
  }
21
- function clientPackageJson(name) {
22
- return `${JSON.stringify({
21
+ export function clientPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLDER) {
22
+ return basePackageJson({
23
23
  name,
24
- version: '0.1.0',
25
- private: true,
26
- type: 'module',
27
- main: 'dist/index.js',
28
- types: 'dist/index.d.ts',
29
- files: ['dist'],
30
- scripts: {
31
- build: 'tsc -p tsconfig.json',
32
- typecheck: 'tsc -p tsconfig.json --noEmit',
33
- },
24
+ scripts: baseScripts('tsx watch src/index.ts'),
34
25
  dependencies: {
26
+ [contractName]: 'workspace:*',
35
27
  '@emeryld/rrroutes-client': '^2.5.3',
36
- '@emeryld/rrroutes-contract': '^2.5.2',
37
28
  '@tanstack/react-query': '^5.90.12',
38
29
  'socket.io-client': '^4.8.3',
39
30
  },
40
31
  devDependencies: {
32
+ ...BASE_LINT_DEV_DEPENDENCIES,
41
33
  '@types/node': '^24.10.2',
42
- typescript: '^5.9.3',
43
34
  },
44
- }, null, 2)}\n`;
35
+ });
45
36
  }
46
37
  function clientFiles(pkgName, contractImport) {
38
+ const scriptsForReadme = [
39
+ 'dev',
40
+ 'build',
41
+ 'typecheck',
42
+ 'lint',
43
+ 'lint:fix',
44
+ 'format',
45
+ 'format:check',
46
+ 'clean',
47
+ 'test',
48
+ ];
47
49
  return {
48
- 'package.json': clientPackageJson(pkgName),
50
+ 'package.json': clientPackageJson(pkgName, contractImport),
49
51
  'tsconfig.json': baseTsConfig({ lib: ['ES2020', 'DOM'], types: ['node'] }),
52
+ ...basePackageFiles(),
50
53
  'src/index.ts': clientIndexTs(contractImport),
51
- 'README.md': `# ${pkgName}
52
-
53
- Starter RRRoutes client scaffold.
54
- - update the contract import in src/index.ts if needed (${contractImport})
55
- - the generated QueryClient is exported from src/index.ts
56
- `,
54
+ 'README.md': buildReadme({
55
+ name: pkgName,
56
+ description: 'Starter RRRoutes client scaffold.',
57
+ scripts: scriptsForReadme,
58
+ sections: [
59
+ {
60
+ title: 'Getting Started',
61
+ lines: [
62
+ '- Install deps: `npm install` (or `pnpm install`)',
63
+ '- Start dev mode: `npm run dev`',
64
+ '- Build output: `npm run build`',
65
+ ],
66
+ },
67
+ {
68
+ title: 'Usage',
69
+ lines: [
70
+ `- Update the contract import in \`src/index.ts\` if needed (${contractImport}).`,
71
+ '- Use the exported `queryClient` and built route clients from `src/index.ts`.',
72
+ ],
73
+ },
74
+ {
75
+ title: 'Environment',
76
+ lines: ['- `RRR_API_URL` (optional) sets the API base URL (default http://localhost:4000).'],
77
+ },
78
+ ],
79
+ }),
57
80
  };
58
81
  }
59
82
  export const clientVariant = {
@@ -61,7 +84,8 @@ export const clientVariant = {
61
84
  label: 'rrr client',
62
85
  defaultDir: 'packages/rrr-client',
63
86
  async scaffold(ctx) {
64
- const files = clientFiles(ctx.pkgName, CONTRACT_IMPORT_PLACEHOLDER);
87
+ const contractImport = ctx.contractName ?? CONTRACT_IMPORT_PLACEHOLDER;
88
+ const files = clientFiles(ctx.pkgName, contractImport);
65
89
  for (const [relative, contents] of Object.entries(files)) {
66
90
  // eslint-disable-next-line no-await-in-loop
67
91
  await writeFileIfMissing(ctx.targetDir, relative, contents);
@@ -1,5 +1,5 @@
1
- import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
2
- const CONTRACT_TS = `import { defineSocketEvents, finalize, resource } from '@emeryld/rrroutes-contract'
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
2
+ export const CONTRACT_TS = `import { defineSocketEvents, finalize, resource } from '@emeryld/rrroutes-contract'
3
3
  import { z } from 'zod'
4
4
 
5
5
  const routes = resource('/api')
@@ -70,45 +70,64 @@ export const socketEvents = sockets.events
70
70
  export type AppRegistry = typeof registry
71
71
  `;
72
72
  function contractPackageJson(name) {
73
- return `${JSON.stringify({
73
+ return basePackageJson({
74
74
  name,
75
- version: '0.1.0',
76
75
  private: false,
77
- type: 'module',
78
- main: 'dist/index.js',
79
- types: 'dist/index.d.ts',
80
76
  exports: {
81
77
  '.': {
82
78
  types: './dist/index.d.ts',
83
79
  import: './dist/index.js',
84
80
  },
85
81
  },
86
- files: ['dist'],
87
- scripts: {
88
- build: 'tsc -p tsconfig.json',
89
- typecheck: 'tsc -p tsconfig.json --noEmit',
90
- },
82
+ scripts: baseScripts('tsx watch src/index.ts'),
91
83
  dependencies: {
92
84
  '@emeryld/rrroutes-contract': '^2.5.2',
93
85
  zod: '^4.2.1',
94
86
  },
95
87
  devDependencies: {
96
- typescript: '^5.9.3',
88
+ ...BASE_LINT_DEV_DEPENDENCIES,
97
89
  },
98
- }, null, 2)}\n`;
90
+ });
99
91
  }
100
92
  function contractFiles(pkgName) {
93
+ const scriptsForReadme = [
94
+ 'dev',
95
+ 'build',
96
+ 'typecheck',
97
+ 'lint',
98
+ 'lint:fix',
99
+ 'format',
100
+ 'format:check',
101
+ 'clean',
102
+ 'test',
103
+ ];
101
104
  return {
102
105
  'package.json': contractPackageJson(pkgName),
103
106
  'tsconfig.json': baseTsConfig(),
107
+ ...basePackageFiles(),
104
108
  'src/index.ts': CONTRACT_TS,
105
- 'README.md': `# ${pkgName}
106
-
107
- Contract package scaffolded by manager-cli.
108
- - edit src/index.ts to add routes and socket events
109
- - build with \`npm run build\`
110
- - import the registry in your server/client packages
111
- `,
109
+ 'README.md': buildReadme({
110
+ name: pkgName,
111
+ description: 'Contract package scaffolded by manager-cli.',
112
+ scripts: scriptsForReadme,
113
+ sections: [
114
+ {
115
+ title: 'Getting Started',
116
+ lines: [
117
+ '- Install deps: `npm install` (or `pnpm install`)',
118
+ '- Start dev mode: `npm run dev`',
119
+ '- Build output: `npm run build`',
120
+ ],
121
+ },
122
+ {
123
+ title: 'Usage',
124
+ lines: [
125
+ '- Edit `src/index.ts` to add routes and socket events.',
126
+ '- Import and share the generated registry in server/client packages.',
127
+ ],
128
+ },
129
+ ],
130
+ }),
112
131
  };
113
132
  }
114
133
  export const contractVariant = {
@@ -1,38 +1,30 @@
1
- import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
2
2
  function dockerPackageJson(name) {
3
- return `${JSON.stringify({
3
+ return basePackageJson({
4
4
  name,
5
- version: '0.1.0',
6
- private: true,
7
- type: 'module',
8
- main: 'dist/index.js',
9
- types: 'dist/index.d.ts',
10
- files: ['dist'],
11
- scripts: {
12
- dev: 'tsx watch src/index.ts',
13
- build: 'tsc -p tsconfig.json',
14
- typecheck: 'tsc -p tsconfig.json --noEmit',
5
+ scripts: baseScripts('tsx watch src/index.ts', {
15
6
  start: 'node dist/index.js',
16
7
  'docker:cli': 'tsx scripts/docker.ts',
17
8
  'docker:build': 'npm run docker:cli -- build',
18
9
  'docker:up': 'npm run docker:cli -- up',
10
+ 'docker:dev': 'npm run docker:cli -- dev',
19
11
  'docker:logs': 'npm run docker:cli -- logs',
20
12
  'docker:stop': 'npm run docker:cli -- stop',
21
13
  'docker:clean': 'npm run docker:cli -- clean',
22
- },
14
+ 'docker:reset': 'npm run docker:cli -- reset',
15
+ }),
23
16
  dependencies: {
24
17
  cors: '^2.8.5',
25
18
  express: '^5.1.0',
26
19
  },
27
20
  devDependencies: {
21
+ ...BASE_LINT_DEV_DEPENDENCIES,
28
22
  '@types/cors': '^2.8.5',
29
23
  '@types/express': '^5.0.6',
30
24
  '@types/node': '^24.10.2',
31
25
  'docker-cli-js': '^3.0.9',
32
- tsx: '^4.19.0',
33
- typescript: '^5.9.3',
34
26
  },
35
- }, null, 2)}\n`;
27
+ });
36
28
  }
37
29
  function dockerIndexTs() {
38
30
  return `import express from 'express'
@@ -85,20 +77,65 @@ CMD ["node", "dist/index.js"]
85
77
  `;
86
78
  }
87
79
  function dockerFiles(pkgName) {
80
+ const scriptsForReadme = [
81
+ 'dev',
82
+ 'build',
83
+ 'typecheck',
84
+ 'lint',
85
+ 'lint:fix',
86
+ 'format',
87
+ 'format:check',
88
+ 'clean',
89
+ 'test',
90
+ 'start',
91
+ 'docker:build',
92
+ 'docker:up',
93
+ 'docker:dev',
94
+ 'docker:logs',
95
+ 'docker:stop',
96
+ 'docker:clean',
97
+ 'docker:reset',
98
+ ];
88
99
  return {
89
100
  'package.json': dockerPackageJson(pkgName),
90
101
  'tsconfig.json': baseTsConfig({ types: ['node'] }),
91
102
  'src/index.ts': dockerIndexTs(),
92
103
  'scripts/docker.ts': dockerCliScript(pkgName),
93
104
  '.dockerignore': DOCKER_DOCKERIGNORE,
105
+ ...basePackageFiles(),
94
106
  Dockerfile: dockerDockerfile(),
95
- 'README.md': `# ${pkgName}
96
-
97
- Dockerized service scaffolded by manager-cli.
98
- - develop locally with \`npm run dev\`
99
- - build with \`npm run build\` and start with \`npm start\`
100
- - docker helper: \`npm run docker:up\` (build + run), \`npm run docker:logs\`, \`npm run docker:stop\`
101
- `,
107
+ 'README.md': buildReadme({
108
+ name: pkgName,
109
+ description: 'Dockerized service scaffolded by manager-cli.',
110
+ scripts: scriptsForReadme,
111
+ sections: [
112
+ {
113
+ title: 'Getting Started',
114
+ lines: [
115
+ '- Install deps: `npm install` (or `pnpm install`)',
116
+ '- Local dev: `npm run dev`',
117
+ '- Build output: `npm run build`',
118
+ '- Start built app: `npm start`',
119
+ ],
120
+ },
121
+ {
122
+ title: 'Docker Helpers',
123
+ lines: [
124
+ '- Build image: `npm run docker:build`',
125
+ '- Build + run detached: `npm run docker:up`',
126
+ '- Build + run + tail logs: `npm run docker:dev`',
127
+ '- Tail logs: `npm run docker:logs`',
128
+ '- Stop container: `npm run docker:stop`',
129
+ '- Clean container: `npm run docker:clean`',
130
+ '- Reset container/image: `npm run docker:reset`',
131
+ ],
132
+ },
133
+ {
134
+ title: 'Environment',
135
+ lines: ['- `PORT` sets the exposed port (default 3000).'],
136
+ },
137
+ ],
138
+ }),
102
139
  };
103
140
  }
104
141
  function dockerCliScript(pkgName) {
@@ -126,32 +163,50 @@ async function main() {
126
163
  await docker.command(\`build -t \${image} .\`)
127
164
  return docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
128
165
  }
166
+ if (command === 'dev') {
167
+ await docker.command(\`build -t \${image} .\`)
168
+ await docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
169
+ return docker.command(\`logs -f \${container}\`)
170
+ }
129
171
  if (command === 'run') {
130
172
  return docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
131
173
  }
132
174
  if (command === 'logs') return docker.command(\`logs -f \${container}\`)
133
175
  if (command === 'stop') return docker.command(\`stop \${container}\`)
134
176
  if (command === 'clean') {
135
- try {
136
- await docker.command(\`rm -f \${container}\`)
137
- } catch (error) {
138
- console.warn(String(error))
139
- }
177
+ await safe(() => docker.command(\`stop \${container}\`))
178
+ await safe(() => docker.command(\`rm -f \${container}\`))
179
+ return
180
+ }
181
+ if (command === 'reset') {
182
+ await safe(() => docker.command(\`stop \${container}\`))
183
+ await safe(() => docker.command(\`rm -f \${container}\`))
184
+ await safe(() => docker.command(\`rmi -f \${image}\`))
140
185
  return
141
186
  }
142
187
  return printHelp()
143
188
  }
144
189
 
190
+ async function safe(run: () => Promise<unknown>) {
191
+ try {
192
+ await run()
193
+ } catch (error) {
194
+ console.warn(String(error))
195
+ }
196
+ }
197
+
145
198
  function printHelp() {
146
199
  console.log(
147
200
  [
148
201
  'Docker helper commands:',
149
202
  ' build -> docker build -t ${pkgName}:latest .',
150
203
  ' up -> build then run in detached mode',
204
+ ' dev -> build, run, and tail logs',
151
205
  ' run -> run existing image detached',
152
206
  ' logs -> docker logs -f <container>',
153
207
  ' stop -> docker stop <container>',
154
- ' clean -> docker rm -f <container>',
208
+ ' clean -> docker stop/rm <container>',
209
+ ' reset -> clean container and remove image',
155
210
  ].join('\\n'),
156
211
  )
157
212
  }
@@ -1,33 +1,49 @@
1
- import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
2
2
  function emptyPackageJson(name) {
3
- return `${JSON.stringify({
3
+ return basePackageJson({
4
4
  name,
5
- version: '0.1.0',
6
- private: true,
7
- type: 'module',
8
- main: 'dist/index.js',
9
- types: 'dist/index.d.ts',
10
- files: ['dist'],
11
- scripts: {
12
- build: 'tsc -p tsconfig.json',
13
- typecheck: 'tsc -p tsconfig.json --noEmit',
14
- },
5
+ scripts: baseScripts('tsx watch src/index.ts'),
15
6
  devDependencies: {
16
- typescript: '^5.9.3',
7
+ ...BASE_LINT_DEV_DEPENDENCIES,
17
8
  },
18
- }, null, 2)}\n`;
9
+ });
19
10
  }
20
11
  function emptyFiles(pkgName) {
12
+ const scriptsForReadme = [
13
+ 'dev',
14
+ 'build',
15
+ 'typecheck',
16
+ 'lint',
17
+ 'lint:fix',
18
+ 'format',
19
+ 'format:check',
20
+ 'clean',
21
+ 'test',
22
+ ];
21
23
  return {
22
24
  'package.json': emptyPackageJson(pkgName),
23
25
  'tsconfig.json': baseTsConfig({ types: ['node'] }),
26
+ ...basePackageFiles(),
24
27
  'src/index.ts': "export const hello = 'world'\n",
25
- 'README.md': `# ${pkgName}
26
-
27
- Empty package scaffolded by manager-cli.
28
- - edit src/index.ts to start coding
29
- - build with \`npm run build\`
30
- `,
28
+ 'README.md': buildReadme({
29
+ name: pkgName,
30
+ description: 'Empty package scaffolded by manager-cli.',
31
+ scripts: scriptsForReadme,
32
+ sections: [
33
+ {
34
+ title: 'Getting Started',
35
+ lines: [
36
+ '- Install deps: `npm install` (or `pnpm install`)',
37
+ '- Start dev mode: `npm run dev`',
38
+ '- Build output: `npm run build`',
39
+ ],
40
+ },
41
+ {
42
+ title: 'Notes',
43
+ lines: ['- Edit `src/index.ts` to start coding.'],
44
+ },
45
+ ],
46
+ }),
31
47
  };
32
48
  }
33
49
  export const emptyVariant = {
@@ -1,219 +1,65 @@
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
+ function deriveNames(baseName) {
8
+ const normalized = baseName.trim();
9
+ return {
10
+ contract: `@${normalized}/contract`,
11
+ server: `@${normalized}/server`,
12
+ client: `@${normalized}/client`,
13
+ docker: `${normalized}-docker`,
14
+ };
15
+ }
16
+ function deriveDirs(rootDir, baseName) {
17
+ const packagesRoot = path.join(rootDir, 'packages');
18
+ return {
19
+ root: rootDir,
20
+ packagesRoot,
21
+ contract: path.join(packagesRoot, `${baseName}-contract`),
22
+ server: path.join(packagesRoot, `${baseName}-server`),
23
+ client: path.join(packagesRoot, `${baseName}-client`),
24
+ docker: path.join(packagesRoot, `${baseName}-docker`),
25
+ };
26
+ }
27
+ function rootPackageJson(baseName) {
28
+ const dockerPackageDir = `packages/${baseName}-docker`;
29
+ return basePackageJson({
30
+ name: `${baseName}-stack`,
6
31
  private: true,
7
- type: 'module',
8
- main: 'dist/server/index.js',
9
- files: ['dist'],
32
+ useDefaults: false,
10
33
  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',
34
+ setup: 'pnpm install && pnpm exec husky install',
35
+ dev: 'pnpm -r dev --parallel --if-present',
36
+ build: 'pnpm -r build',
37
+ typecheck: 'pnpm -r typecheck',
38
+ lint: 'pnpm -r lint --if-present',
39
+ 'lint:fix': 'pnpm -r lint:fix --if-present',
40
+ 'lint-staged': 'lint-staged',
41
+ format: 'pnpm -r format --if-present',
42
+ 'format:check': 'pnpm -r format:check --if-present',
43
+ test: 'pnpm -r test --if-present',
44
+ clean: 'pnpm -r clean --if-present && rimraf node_modules .turbo coverage',
45
+ prepare: 'husky install || true',
46
+ 'docker:up': `pnpm -C ${dockerPackageDir} run docker:up`,
47
+ 'docker:dev': `pnpm -C ${dockerPackageDir} run docker:dev`,
48
+ 'docker:logs': `pnpm -C ${dockerPackageDir} run docker:logs`,
49
+ 'docker:stop': `pnpm -C ${dockerPackageDir} run docker:stop`,
50
+ 'docker:reset': `pnpm -C ${dockerPackageDir} run docker:reset`,
39
51
  },
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',
52
+ devDependencies: { ...BASE_LINT_DEV_DEPENDENCIES },
53
+ extraFields: {
54
+ workspaces: ['packages/*'],
52
55
  },
53
- }, null, 2)}\n`;
56
+ });
54
57
  }
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
- `;
58
+ function rootPnpmWorkspace() {
59
+ return "packages:\n - 'packages/*'\n";
207
60
  }
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:
61
+ function stackComposeYaml() {
62
+ return `services:
217
63
  db:
218
64
  image: postgres:15
219
65
  restart: unless-stopped
@@ -229,211 +75,55 @@ const FULLSTACK_DOCKER_COMPOSE = `services:
229
75
  volumes:
230
76
  db-data:
231
77
  `;
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
78
  }
256
- function fullstackFiles(pkgName) {
257
- return {
258
- 'package.json': fullstackPackageJson(pkgName),
79
+ async function scaffoldRootFiles(baseDir, baseName) {
80
+ const files = {
81
+ 'package.json': rootPackageJson(baseName),
82
+ 'pnpm-workspace.yaml': rootPnpmWorkspace(),
83
+ 'docker-compose.yml': stackComposeYaml(),
259
84
  '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',
85
+ rootDir: '.',
268
86
  outDir: 'dist',
269
- include: ['src/server/**/*', 'src/contract/**/*'],
87
+ include: ['packages/**/*', 'scripts/**/*', 'src/**/*'],
270
88
  }),
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
- `,
89
+ ...basePackageFiles(),
294
90
  };
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))
91
+ for (const [relative, contents] of Object.entries(files)) {
92
+ // eslint-disable-next-line no-await-in-loop
93
+ await writeFileIfMissing(baseDir, relative, contents);
402
94
  }
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
95
  }
428
96
  export const fullstackVariant = {
429
97
  id: 'rrr-fullstack',
430
- label: 'full stack service (api + web)',
431
- defaultDir: 'packages/rrr-fullstack',
98
+ label: 'rrr fullstack (contract + server + client + docker)',
99
+ defaultDir: 'rrrfull-stack',
432
100
  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
- }
101
+ const baseName = ctx.pkgName;
102
+ const names = deriveNames(baseName);
103
+ const dirs = deriveDirs(ctx.targetDir, baseName);
104
+ // Root workspace files
105
+ await scaffoldRootFiles(dirs.root, baseName);
106
+ // Contract package (reuse contract variant)
107
+ await contractVariant.scaffold({
108
+ targetDir: dirs.contract,
109
+ pkgName: names.contract,
110
+ });
111
+ // Server package
112
+ await serverVariant.scaffold({
113
+ targetDir: dirs.server,
114
+ pkgName: names.server,
115
+ contractName: names.contract,
116
+ });
117
+ // Client package
118
+ await clientVariant.scaffold({
119
+ targetDir: dirs.client,
120
+ pkgName: names.client,
121
+ contractName: names.contract,
122
+ });
123
+ // Docker helper package (reuse docker variant)
124
+ await dockerVariant.scaffold({
125
+ targetDir: dirs.docker,
126
+ pkgName: names.docker,
127
+ });
438
128
  },
439
129
  };
@@ -1,6 +1,6 @@
1
- import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
2
2
  const CONTRACT_IMPORT_PLACEHOLDER = '@your-scope/contract';
3
- function serverIndexTs(contractImport) {
3
+ export function serverIndexTs(contractImport) {
4
4
  return `import 'dotenv/config'
5
5
  import http from 'node:http'
6
6
  import express from 'express'
@@ -46,23 +46,15 @@ server.listen(PORT, () => {
46
46
  })
47
47
  `;
48
48
  }
49
- function serverPackageJson(name) {
50
- return `${JSON.stringify({
49
+ export function serverPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLDER) {
50
+ return basePackageJson({
51
51
  name,
52
- version: '0.1.0',
53
52
  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',
53
+ scripts: baseScripts('tsx watch --env-file .env src/index.ts', {
62
54
  start: 'node dist/index.js',
63
- },
55
+ }),
64
56
  dependencies: {
65
- '@emeryld/rrroutes-contract': '^2.5.2',
57
+ [contractName]: 'workspace:*',
66
58
  '@emeryld/rrroutes-server': '^2.4.1',
67
59
  cors: '^2.8.5',
68
60
  dotenv: '^16.4.5',
@@ -70,26 +62,59 @@ function serverPackageJson(name) {
70
62
  zod: '^4.2.1',
71
63
  },
72
64
  devDependencies: {
65
+ ...BASE_LINT_DEV_DEPENDENCIES,
73
66
  '@types/cors': '^2.8.5',
74
67
  '@types/express': '^5.0.6',
75
68
  '@types/node': '^24.10.2',
76
- 'ts-node': '^10.9.2',
77
- typescript: '^5.9.3',
78
69
  },
79
- }, null, 2)}\n`;
70
+ });
80
71
  }
81
72
  function serverFiles(pkgName, contractImport) {
73
+ const scriptsForReadme = [
74
+ 'dev',
75
+ 'build',
76
+ 'typecheck',
77
+ 'lint',
78
+ 'lint:fix',
79
+ 'format',
80
+ 'format:check',
81
+ 'clean',
82
+ 'test',
83
+ 'start',
84
+ ];
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: scriptsForReadme,
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 = {
@@ -97,7 +122,8 @@ export const serverVariant = {
97
122
  label: 'rrr server',
98
123
  defaultDir: 'packages/rrr-server',
99
124
  async scaffold(ctx) {
100
- const files = serverFiles(ctx.pkgName, CONTRACT_IMPORT_PLACEHOLDER);
125
+ const contractImport = ctx.contractName ?? CONTRACT_IMPORT_PLACEHOLDER;
126
+ const files = serverFiles(ctx.pkgName, contractImport);
101
127
  for (const [relative, contents] of Object.entries(files)) {
102
128
  // eslint-disable-next-line no-await-in-loop
103
129
  await writeFileIfMissing(ctx.targetDir, relative, contents);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",