@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 +40 -42
- package/dist/create-package/index.js +177 -10
- package/dist/create-package/shared.js +183 -0
- package/dist/create-package/variants/client.js +56 -25
- package/dist/create-package/variants/contract.js +46 -21
- package/dist/create-package/variants/docker.js +96 -29
- package/dist/create-package/variants/empty.js +40 -20
- package/dist/create-package/variants/fullstack.js +130 -406
- package/dist/create-package/variants/server.js +59 -26
- package/dist/publish.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,44 +1,42 @@
|
|
|
1
1
|
# @emeryld/manager
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Run
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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
|
|
32
|
+
export function clientPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLDER) {
|
|
33
|
+
return basePackageJson({
|
|
23
34
|
name,
|
|
24
|
-
|
|
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
|
-
}
|
|
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':
|
|
52
|
-
|
|
53
|
-
Starter RRRoutes client scaffold.
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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);
|