@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.
package/README.md CHANGED
@@ -1,44 +1,42 @@
1
1
  # @emeryld/manager
2
2
 
3
- Interactive release helper for pnpm monorepos. Install it as a local dev dependency, then run `pnpm manager-cli` (or call a `scripts.manager-cli` alias) from your workspace root to walk through selecting packages, running updates/tests/builds, and publishing.
4
-
5
- ## Installation
6
-
7
- 1. Ensure your workspace has a `packages/` directory (or configure one via `packages.mjs`).
8
- 2. Add the manager to your dev dependencies so its CLI and helpers are hoisted into the consuming repo:
9
- ```sh
10
- pnpm add -D @emeryld/manager
11
- ```
12
- 3. Include a script in your `package.json` so you can invoke it consistently:
13
- ```json
14
- {
15
- "scripts": {
16
- "manager-cli": "manager-cli"
17
- }
18
- }
19
- ```
20
- 4. Run `pnpm install` to fetch `ts-node`, `semver`, and other runtime deps.
21
-
22
- ## Running the CLI
23
-
24
- - Always run `pnpm manager-cli` (or `pnpm run manager-cli`) from the **workspace root** where your `packages/` folder lives. The manager operates against the workspace you are in, not the package that ships the tool.
25
- - Each TypeScript helper script registers the bundled `ts-node/esm` loader with the exact script path, so the CLI works even from pnpm workspaces that hoist dependencies.
26
- - To avoid the warning/error you saw earlier, make sure:
27
- - the workspace has `packages/` (or a configured manifest) so `loadPackages()` can find targets,
28
- - `pnpm install` has already written `ts-node` into `node_modules`,
29
- - you execute the CLI from the workspace that owns those packages.
30
-
31
- ## Testing the loader registration
32
-
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).
3
+ Dev dependency built for Codex agents: scaffold RRRoutes packages, read the generated structure, and ship changes without rebuilding boilerplate. Install it in a pnpm workspace and call `pnpm manager-cli` from the workspace root.
4
+
5
+ ## Quick start (agents)
6
+ - Install: `pnpm add -D @emeryld/manager`
7
+ - Add a script: `"manager-cli": "manager-cli"` in the workspace `package.json`
8
+ - Discover templates fast: `pnpm manager-cli templates` (alias `pnpm manager-cli create --list`)
9
+ - Scaffold without prompts:
10
+ `pnpm manager-cli create --variant rrr-server --dir packages/api --name @scope/api --contract @scope/contract --skip-install`
11
+ - Prefer prompts: `pnpm manager-cli create` and follow the menu
12
+ - After scaffolding, open the new package `README.md` for variant-specific scripts and usage
13
+
14
+ ## Templates at a glance
15
+ | id | default dir | summary | key files |
16
+ | --- | --- | --- | --- |
17
+ | rrr-contract | `packages/rrr-contract` | Shared RRRoutes contract + sockets for clients/servers | `src/index.ts`, `README.md` |
18
+ | rrr-server | `packages/rrr-server` | Express + RRRoutes API wired to a contract import | `src/index.ts`, `.env.example`, `README.md` |
19
+ | rrr-client | `packages/rrr-client` | React Query-ready RRRoutes client bound to a contract import | `src/index.ts`, `README.md` |
20
+ | rrr-empty | `packages/rrr-empty` | Minimal TypeScript package with lint/format/test wiring | `src/index.ts`, `README.md` |
21
+ | rrr-docker | `packages/rrr-docker` | Express service with Dockerfile + helper CLI | `src/index.ts`, `scripts/docker.ts`, `Dockerfile`, `README.md` |
22
+ | rrr-fullstack | `rrrfull-stack` | pnpm workspace with contract, server, client, and docker helper | root `package.json`, `pnpm-workspace.yaml`, `packages/*` |
23
+
24
+ Use `pnpm manager-cli create --describe <variant>` to print the scripts, key files, and notes for any template before scaffolding.
25
+
26
+ ## Create command reference
27
+ - `pnpm manager-cli create --list` show templates
28
+ - `pnpm manager-cli create --describe rrr-server` see scripts/files/notes for one template
29
+ - `pnpm manager-cli create --variant rrr-client --dir packages/web-client --name @scope/web-client` scaffold directly (adds `--contract <id>` for server/client)
30
+ - `--skip-install` / `--skip-build` — bypass post-create steps when you want to install/build later
31
+
32
+ ## Release helper (existing packages)
33
+ - Run from the workspace root where `packages/` lives.
34
+ - `pnpm manager-cli` launches an interactive menu: pick packages, then run update → test → build → publish (or any single step).
35
+ - Non-interactive publish: `pnpm manager-cli <pkg|all> --non-interactive --bump patch` (see CLI prompts for flags like `--sync` and `--tag`).
36
+
37
+ ## Notes
38
+ - Each helper script registers the bundled `ts-node/esm` loader with the correct path so the CLI works even when dependencies are hoisted.
39
+ - The create flow runs `pnpm install` and `pnpm run build` when a build script exists; skip with `--skip-install`/`--skip-build` if you want to control timing.
40
+
41
+ ## Tests
42
+ - `pnpm test` verifies the helper CLI loader registration.
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { stdin as input } from 'node:process';
5
5
  import { askLine, promptSingleKey } from '../prompts.js';
6
6
  import { colors, logGlobal } from '../utils/log.js';
7
- import { workspaceRoot } from './shared.js';
7
+ import { SCRIPT_DESCRIPTIONS, workspaceRoot, } from './shared.js';
8
8
  import { clientVariant } from './variants/client.js';
9
9
  import { contractVariant } from './variants/contract.js';
10
10
  import { dockerVariant } from './variants/docker.js';
@@ -19,10 +19,122 @@ const VARIANTS = [
19
19
  dockerVariant,
20
20
  fullstackVariant,
21
21
  ];
22
+ const VARIANT_LOOKUP = new Map(VARIANTS.map((variant) => [variant.id, variant]));
22
23
  function derivePackageName(targetDir) {
23
24
  const base = path.basename(targetDir) || 'rrr-package';
24
25
  return base;
25
26
  }
27
+ function resolveVariant(key) {
28
+ if (!key)
29
+ return undefined;
30
+ const normalized = key.toLowerCase();
31
+ return (VARIANT_LOOKUP.get(normalized) ??
32
+ VARIANTS.find((variant) => {
33
+ const label = variant.label.toLowerCase();
34
+ const id = variant.id.toLowerCase();
35
+ return (id === normalized ||
36
+ id.replace(/^rrr-/, '') === normalized ||
37
+ label === normalized ||
38
+ label.includes(normalized));
39
+ }));
40
+ }
41
+ function printVariantList() {
42
+ logGlobal('Available templates', colors.magenta);
43
+ VARIANTS.forEach((variant) => {
44
+ const summary = variant.summary ? ` ${colors.dim(`– ${variant.summary}`)}` : '';
45
+ console.log(`- ${colors.green(variant.label)} ${colors.dim(`(${variant.id})`)} → ${colors.cyan(variant.defaultDir)}${summary}`);
46
+ });
47
+ console.log(colors.dim('Use "--describe <variant>" for details or "--variant <id> --dir <path>" to scaffold without prompts.'));
48
+ }
49
+ function printVariantDetails(variant) {
50
+ logGlobal(`${variant.label} (${variant.id})`, colors.green);
51
+ console.log(` default dir: ${colors.cyan(variant.defaultDir)}`);
52
+ if (variant.summary) {
53
+ console.log(` ${variant.summary}`);
54
+ }
55
+ if (variant.keyFiles?.length) {
56
+ console.log(' key files:');
57
+ variant.keyFiles.forEach((file) => console.log(` - ${file}`));
58
+ }
59
+ if (variant.scripts?.length) {
60
+ console.log(' scripts:');
61
+ variant.scripts.forEach((script) => {
62
+ const desc = SCRIPT_DESCRIPTIONS[script];
63
+ const detail = desc ? colors.dim(` – ${desc}`) : '';
64
+ console.log(` - ${script}${detail}`);
65
+ });
66
+ }
67
+ if (variant.notes?.length) {
68
+ console.log(' notes:');
69
+ variant.notes.forEach((note) => console.log(` - ${note}`));
70
+ }
71
+ }
72
+ function parseCreateCliArgs(argv) {
73
+ const options = {};
74
+ for (let i = 0; i < argv.length; i++) {
75
+ const arg = argv[i];
76
+ if (arg === '--list' || arg === '-l' || arg === 'list' || arg === 'ls') {
77
+ options.list = true;
78
+ continue;
79
+ }
80
+ if (arg === '--describe' || arg === '-d' || arg === 'describe') {
81
+ options.describe = argv[++i];
82
+ continue;
83
+ }
84
+ if (arg === '--variant' || arg === '-v') {
85
+ options.variant = argv[++i];
86
+ continue;
87
+ }
88
+ if (arg === '--dir' || arg === '--path' || arg === '-p') {
89
+ options.targetDir = argv[++i];
90
+ continue;
91
+ }
92
+ if (arg === '--name' || arg === '-n') {
93
+ options.pkgName = argv[++i];
94
+ continue;
95
+ }
96
+ if (arg === '--contract') {
97
+ options.contractName = argv[++i];
98
+ continue;
99
+ }
100
+ if (arg === '--skip-install') {
101
+ options.skipInstall = true;
102
+ continue;
103
+ }
104
+ if (arg === '--skip-build') {
105
+ options.skipBuild = true;
106
+ continue;
107
+ }
108
+ if (arg === '--help' || arg === '-h') {
109
+ options.help = true;
110
+ continue;
111
+ }
112
+ if (!arg.startsWith('-') && !options.variant) {
113
+ options.variant = arg;
114
+ }
115
+ }
116
+ return options;
117
+ }
118
+ function printCreateHelp() {
119
+ logGlobal('Create package help', colors.magenta);
120
+ console.log('Usage:');
121
+ console.log(' pnpm manager-cli create # interactive prompts');
122
+ console.log(' pnpm manager-cli create --list # list templates');
123
+ console.log(' pnpm manager-cli create --describe rrr-server');
124
+ console.log(' pnpm manager-cli create --variant rrr-client --dir packages/rrr-client --name @scope/client');
125
+ console.log(' pnpm manager-cli create --variant rrr-server --contract @scope/contract --skip-install');
126
+ console.log('');
127
+ console.log('Flags:');
128
+ console.log(' --list, -l Show available templates');
129
+ console.log(' --describe, -d Print details for a template');
130
+ console.log(' --variant, -v Pick a template by id/label (skips variant prompt)');
131
+ console.log(' --dir, --path, -p Target directory (skips path prompt)');
132
+ console.log(' --name, -n Package name (skips name prompt)');
133
+ console.log(' --contract Contract import to inject (server/client variants)');
134
+ console.log(' --skip-install Do not run pnpm install after scaffolding');
135
+ console.log(' --skip-build Skip build after scaffolding');
136
+ console.log(' --help, -h Show this help');
137
+ }
26
138
  async function ensureTargetDir(targetDir) {
27
139
  try {
28
140
  const stats = await stat(targetDir);
@@ -177,7 +289,11 @@ async function promptForTargetDir(fallback) {
177
289
  const normalized = answer || fallback;
178
290
  return path.resolve(workspaceRoot, normalized);
179
291
  }
180
- async function postCreateTasks(targetDir) {
292
+ async function postCreateTasks(targetDir, options) {
293
+ if (options?.skipInstall) {
294
+ logGlobal('Skipping pnpm install (flag).', colors.dim);
295
+ return;
296
+ }
181
297
  try {
182
298
  logGlobal('Running pnpm install…', colors.cyan);
183
299
  await runCommand('pnpm', ['install'], workspaceRoot);
@@ -186,6 +302,10 @@ async function postCreateTasks(targetDir) {
186
302
  logGlobal(`pnpm install failed: ${error instanceof Error ? error.message : String(error)}`, colors.yellow);
187
303
  return;
188
304
  }
305
+ if (options?.skipBuild) {
306
+ logGlobal('Skipping build (flag).', colors.dim);
307
+ return;
308
+ }
189
309
  try {
190
310
  const pkgJsonPath = path.join(targetDir, 'package.json');
191
311
  const pkgRaw = await readFile(pkgJsonPath, 'utf8');
@@ -199,22 +319,69 @@ async function postCreateTasks(targetDir) {
199
319
  logGlobal(`Skipping build (could not read package.json): ${error instanceof Error ? error.message : String(error)}`, colors.yellow);
200
320
  }
201
321
  }
202
- async function gatherTarget() {
203
- const variant = await promptForVariant();
204
- const targetDir = await promptForTargetDir(variant.defaultDir);
322
+ async function gatherTarget(initial = {}) {
323
+ const variant = initial.variant ?? (await promptForVariant());
324
+ const targetDir = initial.targetDir !== undefined
325
+ ? path.resolve(workspaceRoot, initial.targetDir)
326
+ : await promptForTargetDir(variant.defaultDir);
205
327
  const fallbackName = derivePackageName(targetDir);
206
- const nameAnswer = await askLine(`Package name? (${fallbackName}): `);
328
+ const nameAnswer = initial.pkgName === undefined
329
+ ? await askLine(`Package name? (${fallbackName}): `)
330
+ : initial.pkgName;
207
331
  const pkgName = (nameAnswer || fallbackName).trim() || fallbackName;
208
332
  await ensureTargetDir(targetDir);
209
- return { variant, targetDir, pkgName };
333
+ return {
334
+ variant,
335
+ targetDir,
336
+ pkgName,
337
+ contractName: initial.contractName,
338
+ };
210
339
  }
211
- export async function createRrrPackage() {
212
- const target = await gatherTarget();
340
+ export async function createRrrPackage(options = {}) {
341
+ const target = await gatherTarget(options);
213
342
  logGlobal(`Creating ${target.variant.label} in ${path.relative(workspaceRoot, target.targetDir) || '.'}`, colors.green);
214
343
  await target.variant.scaffold({
215
344
  targetDir: target.targetDir,
216
345
  pkgName: target.pkgName,
346
+ contractName: target.contractName ?? options.contractName,
347
+ });
348
+ await postCreateTasks(target.targetDir, {
349
+ skipInstall: options.skipInstall,
350
+ skipBuild: options.skipBuild ?? options.skipInstall,
217
351
  });
218
- await postCreateTasks(target.targetDir);
219
352
  logGlobal('Scaffold complete. Install/build steps were attempted; ready to run!', colors.green);
220
353
  }
354
+ export async function runCreatePackageCli(argv) {
355
+ const parsed = parseCreateCliArgs(argv);
356
+ if (parsed.help) {
357
+ printCreateHelp();
358
+ return;
359
+ }
360
+ if (parsed.list) {
361
+ printVariantList();
362
+ return;
363
+ }
364
+ if (parsed.describe) {
365
+ const variant = resolveVariant(parsed.describe);
366
+ if (!variant) {
367
+ throw new Error(`Unknown variant "${parsed.describe}". Use --list to see available templates.`);
368
+ }
369
+ printVariantDetails(variant);
370
+ return;
371
+ }
372
+ const variant = resolveVariant(parsed.variant);
373
+ if (parsed.variant && !variant) {
374
+ throw new Error(`Unknown variant "${parsed.variant}". Use --list to see available templates.`);
375
+ }
376
+ const targetDir = parsed.targetDir
377
+ ? path.resolve(workspaceRoot, parsed.targetDir)
378
+ : undefined;
379
+ await createRrrPackage({
380
+ variant,
381
+ targetDir,
382
+ pkgName: parsed.pkgName,
383
+ contractName: parsed.contractName,
384
+ skipInstall: parsed.skipInstall,
385
+ skipBuild: parsed.skipBuild ?? parsed.skipInstall,
386
+ });
387
+ }
@@ -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
+ export 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,17 @@
1
- import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
2
+ const CLIENT_SCRIPTS = [
3
+ 'dev',
4
+ 'build',
5
+ 'typecheck',
6
+ 'lint',
7
+ 'lint:fix',
8
+ 'format',
9
+ 'format:check',
10
+ 'clean',
11
+ 'test',
12
+ ];
2
13
  const CONTRACT_IMPORT_PLACEHOLDER = '@your-scope/contract';
3
- function clientIndexTs(contractImport) {
14
+ export function clientIndexTs(contractImport) {
4
15
  return `import { QueryClient } from '@tanstack/react-query'
5
16
  import { createRouteClient } from '@emeryld/rrroutes-client'
6
17
  import { registry } from '${contractImport}'
@@ -18,50 +29,70 @@ export const healthGet = routeClient.build(registry.byKey['GET /api/health'])
18
29
  export const healthPost = routeClient.build(registry.byKey['POST /api/health'])
19
30
  `;
20
31
  }
21
- function clientPackageJson(name) {
22
- return `${JSON.stringify({
32
+ export function clientPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLDER) {
33
+ return basePackageJson({
23
34
  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
- },
35
+ scripts: baseScripts('tsx watch src/index.ts'),
34
36
  dependencies: {
37
+ [contractName]: 'workspace:*',
35
38
  '@emeryld/rrroutes-client': '^2.5.3',
36
- '@emeryld/rrroutes-contract': '^2.5.2',
37
39
  '@tanstack/react-query': '^5.90.12',
38
40
  'socket.io-client': '^4.8.3',
39
41
  },
40
42
  devDependencies: {
43
+ ...BASE_LINT_DEV_DEPENDENCIES,
41
44
  '@types/node': '^24.10.2',
42
- typescript: '^5.9.3',
43
45
  },
44
- }, null, 2)}\n`;
46
+ });
45
47
  }
46
48
  function clientFiles(pkgName, contractImport) {
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: CLIENT_SCRIPTS,
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 = {
60
83
  id: 'rrr-client',
61
84
  label: 'rrr client',
62
85
  defaultDir: 'packages/rrr-client',
86
+ summary: 'React Query-ready RRRoutes client bound to a shared contract import.',
87
+ keyFiles: ['src/index.ts', 'README.md'],
88
+ scripts: CLIENT_SCRIPTS,
89
+ notes: [
90
+ 'Set the contract import via --contract or by editing src/index.ts.',
91
+ 'Exports query client + typed route builders to plug into React apps.',
92
+ ],
63
93
  async scaffold(ctx) {
64
- const files = clientFiles(ctx.pkgName, CONTRACT_IMPORT_PLACEHOLDER);
94
+ const contractImport = ctx.contractName ?? CONTRACT_IMPORT_PLACEHOLDER;
95
+ const files = clientFiles(ctx.pkgName, contractImport);
65
96
  for (const [relative, contents] of Object.entries(files)) {
66
97
  // eslint-disable-next-line no-await-in-loop
67
98
  await writeFileIfMissing(ctx.targetDir, relative, contents);