@emeryld/manager 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { mkdir, readdir, readFile, stat } from 'node:fs/promises';
1
+ import { mkdir, readdir, readFile, rm, stat } from 'node:fs/promises';
2
2
  import { spawn } from 'node:child_process';
3
3
  import path from 'node:path';
4
4
  import { stdin as input } from 'node:process';
@@ -105,6 +105,10 @@ function parseCreateCliArgs(argv) {
105
105
  options.skipBuild = true;
106
106
  continue;
107
107
  }
108
+ if (arg === '--reset') {
109
+ options.reset = true;
110
+ continue;
111
+ }
108
112
  if (arg === '--help' || arg === '-h') {
109
113
  options.help = true;
110
114
  continue;
@@ -123,6 +127,7 @@ function printCreateHelp() {
123
127
  console.log(' pnpm manager-cli create --describe rrr-server');
124
128
  console.log(' pnpm manager-cli create --variant rrr-client --dir packages/rrr-client --name @scope/client');
125
129
  console.log(' pnpm manager-cli create --variant rrr-server --contract @scope/contract --skip-install');
130
+ console.log(' pnpm manager-cli create --variant rrr-server --reset # blow away an existing target before scaffolding');
126
131
  console.log('');
127
132
  console.log('Flags:');
128
133
  console.log(' --list, -l Show available templates');
@@ -131,16 +136,28 @@ function printCreateHelp() {
131
136
  console.log(' --dir, --path, -p Target directory (skips path prompt)');
132
137
  console.log(' --name, -n Package name (skips name prompt)');
133
138
  console.log(' --contract Contract import to inject (server/client variants)');
139
+ console.log(' --reset Remove the target directory if it already exists');
134
140
  console.log(' --skip-install Do not run pnpm install after scaffolding');
135
141
  console.log(' --skip-build Skip build after scaffolding');
136
142
  console.log(' --help, -h Show this help');
137
143
  }
138
- async function ensureTargetDir(targetDir) {
144
+ async function ensureTargetDir(targetDir, options) {
145
+ const resolvedTarget = path.resolve(targetDir);
146
+ const shouldReset = options?.reset ?? false;
147
+ if (shouldReset && resolvedTarget === workspaceRoot) {
148
+ throw new Error('Refusing to reset the workspace root directory.');
149
+ }
139
150
  try {
140
151
  const stats = await stat(targetDir);
141
152
  if (!stats.isDirectory()) {
142
153
  throw new Error(`Target "${targetDir}" exists and is not a directory.`);
143
154
  }
155
+ if (shouldReset) {
156
+ logGlobal(`Resetting existing target ${path.relative(workspaceRoot, resolvedTarget) || '.'}…`, colors.yellow);
157
+ await rm(resolvedTarget, { recursive: true, force: true });
158
+ await mkdir(resolvedTarget, { recursive: true });
159
+ return;
160
+ }
144
161
  const entries = await readdir(targetDir);
145
162
  if (entries.length > 0) {
146
163
  logGlobal(`Target ${path.relative(workspaceRoot, targetDir)} is not empty; existing files will be preserved.`, colors.yellow);
@@ -150,7 +167,7 @@ async function ensureTargetDir(targetDir) {
150
167
  if (error &&
151
168
  typeof error === 'object' &&
152
169
  error.code === 'ENOENT') {
153
- await mkdir(targetDir, { recursive: true });
170
+ await mkdir(resolvedTarget, { recursive: true });
154
171
  return;
155
172
  }
156
173
  throw error;
@@ -329,12 +346,13 @@ async function gatherTarget(initial = {}) {
329
346
  ? await askLine(`Package name? (${fallbackName}): `)
330
347
  : initial.pkgName;
331
348
  const pkgName = (nameAnswer || fallbackName).trim() || fallbackName;
332
- await ensureTargetDir(targetDir);
349
+ await ensureTargetDir(targetDir, { reset: initial.reset });
333
350
  return {
334
351
  variant,
335
352
  targetDir,
336
353
  pkgName,
337
354
  contractName: initial.contractName,
355
+ reset: initial.reset,
338
356
  };
339
357
  }
340
358
  export async function createRrrPackage(options = {}) {
@@ -381,6 +399,7 @@ export async function runCreatePackageCli(argv) {
381
399
  targetDir,
382
400
  pkgName: parsed.pkgName,
383
401
  contractName: parsed.contractName,
402
+ reset: parsed.reset,
384
403
  skipInstall: parsed.skipInstall,
385
404
  skipBuild: parsed.skipBuild ?? parsed.skipInstall,
386
405
  });
@@ -1,6 +1,15 @@
1
+ import { readFileSync } from 'node:fs';
1
2
  import { access, mkdir, writeFile } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  export const workspaceRoot = process.cwd();
5
+ function pathExists(target) {
6
+ return access(target)
7
+ .then(() => true)
8
+ .catch(() => false);
9
+ }
10
+ function toPosixPath(value) {
11
+ return value.split(path.sep).join('/');
12
+ }
4
13
  export async function writeFileIfMissing(baseDir, relative, contents) {
5
14
  const fullPath = path.join(baseDir, relative);
6
15
  await mkdir(path.dirname(fullPath), { recursive: true });
@@ -23,7 +32,8 @@ export async function writeFileIfMissing(baseDir, relative, contents) {
23
32
  return 'created';
24
33
  }
25
34
  export function baseTsConfig(options) {
26
- return `${JSON.stringify({
35
+ const config = {
36
+ ...(options?.extends ? { extends: options.extends } : {}),
27
37
  compilerOptions: {
28
38
  target: 'ES2020',
29
39
  module: 'NodeNext',
@@ -41,11 +51,15 @@ export function baseTsConfig(options) {
41
51
  },
42
52
  include: options?.include ?? ['src/**/*'],
43
53
  exclude: options?.exclude ?? ['dist', 'node_modules'],
44
- }, null, 2)}\n`;
54
+ };
55
+ return `${JSON.stringify(config, null, 2)}\n`;
45
56
  }
46
57
  export function baseEslintConfig(tsconfigPath = './tsconfig.json') {
47
58
  return `import tseslint from 'typescript-eslint'
48
59
  import prettierPlugin from 'eslint-plugin-prettier'
60
+ import { fileURLToPath } from 'node:url'
61
+
62
+ const __dirname = fileURLToPath(new URL('.', import.meta.url))
49
63
 
50
64
  export default tseslint.config(
51
65
  { ignores: ['dist', 'node_modules'] },
@@ -55,7 +69,7 @@ export default tseslint.config(
55
69
  languageOptions: {
56
70
  parserOptions: {
57
71
  project: ${JSON.stringify(tsconfigPath)},
58
- tsconfigRootDir: import.meta.dirname,
72
+ tsconfigRootDir: __dirname,
59
73
  },
60
74
  },
61
75
  plugins: {
@@ -125,6 +139,8 @@ const DEFAULT_GITIGNORE_ENTRIES = [
125
139
  '.env',
126
140
  'coverage',
127
141
  '*.log',
142
+ '.vscode',
143
+ '.husky',
128
144
  ];
129
145
  export function gitignoreFrom(entries = DEFAULT_GITIGNORE_ENTRIES) {
130
146
  return entries.join('\n');
@@ -199,6 +215,10 @@ function stripUndefined(obj) {
199
215
  }
200
216
  export function basePackageJson(options) {
201
217
  const applyDefaults = options.useDefaults ?? true;
218
+ const inheritPackageManager = options.inheritPackageManager ?? true;
219
+ const packageManager = inheritPackageManager && !options.extraFields?.packageManager
220
+ ? readRootPackageManager()
221
+ : undefined;
202
222
  const pkg = stripUndefined({
203
223
  name: options.name,
204
224
  version: options.version ?? '0.1.0',
@@ -221,7 +241,47 @@ export function basePackageJson(options) {
221
241
  dependencies: options.dependencies,
222
242
  devDependencies: options.devDependencies,
223
243
  'lint-staged': LINT_STAGED_CONFIG,
244
+ packageManager,
224
245
  ...options.extraFields,
225
246
  });
226
247
  return `${JSON.stringify(pkg, null, 2)}\n`;
227
248
  }
249
+ function readRootPackageManager() {
250
+ try {
251
+ const raw = readFileSync(path.join(workspaceRoot, 'package.json'), 'utf8');
252
+ const pkg = JSON.parse(raw);
253
+ return pkg.packageManager;
254
+ }
255
+ catch (error) {
256
+ if (error &&
257
+ typeof error === 'object' &&
258
+ error.code !== 'ENOENT') {
259
+ console.warn(`Could not read root package.json for packageManager: ${error instanceof Error ? error.message : String(error)}`);
260
+ }
261
+ return undefined;
262
+ }
263
+ }
264
+ async function resolveRootTsconfig() {
265
+ const candidates = ['tsconfig.base.json', 'tsconfig.json'];
266
+ for (const candidate of candidates) {
267
+ const fullPath = path.join(workspaceRoot, candidate);
268
+ if (await pathExists(fullPath)) {
269
+ return fullPath;
270
+ }
271
+ }
272
+ return undefined;
273
+ }
274
+ export async function packageTsConfig(targetDir, options) {
275
+ const extendsFromRoot = options?.extendsFromRoot ?? true;
276
+ let extendsPath;
277
+ if (extendsFromRoot) {
278
+ const rootConfig = await resolveRootTsconfig();
279
+ if (rootConfig) {
280
+ const relative = path.relative(targetDir, rootConfig);
281
+ if (relative !== '') {
282
+ extendsPath = toPosixPath(relative || './tsconfig.json');
283
+ }
284
+ }
285
+ }
286
+ return baseTsConfig({ ...options, extends: extendsPath });
287
+ }
@@ -1,4 +1,4 @@
1
- import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
2
2
  const CLIENT_SCRIPTS = [
3
3
  'dev',
4
4
  'build',
@@ -45,10 +45,14 @@ export function clientPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLD
45
45
  },
46
46
  });
47
47
  }
48
- function clientFiles(pkgName, contractImport) {
48
+ async function clientFiles(pkgName, contractImport, targetDir) {
49
+ const tsconfig = await packageTsConfig(targetDir, {
50
+ lib: ['ES2020', 'DOM'],
51
+ types: ['node'],
52
+ });
49
53
  return {
50
54
  'package.json': clientPackageJson(pkgName, contractImport),
51
- 'tsconfig.json': baseTsConfig({ lib: ['ES2020', 'DOM'], types: ['node'] }),
55
+ 'tsconfig.json': tsconfig,
52
56
  ...basePackageFiles(),
53
57
  'src/index.ts': clientIndexTs(contractImport),
54
58
  'README.md': buildReadme({
@@ -92,7 +96,7 @@ export const clientVariant = {
92
96
  ],
93
97
  async scaffold(ctx) {
94
98
  const contractImport = ctx.contractName ?? CONTRACT_IMPORT_PLACEHOLDER;
95
- const files = clientFiles(ctx.pkgName, contractImport);
99
+ const files = await clientFiles(ctx.pkgName, contractImport, ctx.targetDir);
96
100
  for (const [relative, contents] of Object.entries(files)) {
97
101
  // eslint-disable-next-line no-await-in-loop
98
102
  await writeFileIfMissing(ctx.targetDir, relative, contents);
@@ -1,4 +1,4 @@
1
- import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
2
2
  const CONTRACT_SCRIPTS = [
3
3
  'dev',
4
4
  'build',
@@ -100,10 +100,11 @@ function contractPackageJson(name) {
100
100
  },
101
101
  });
102
102
  }
103
- function contractFiles(pkgName) {
103
+ async function contractFiles(pkgName, targetDir) {
104
+ const tsconfig = await packageTsConfig(targetDir);
104
105
  return {
105
106
  'package.json': contractPackageJson(pkgName),
106
- 'tsconfig.json': baseTsConfig(),
107
+ 'tsconfig.json': tsconfig,
107
108
  ...basePackageFiles(),
108
109
  'src/index.ts': CONTRACT_TS,
109
110
  'README.md': buildReadme({
@@ -141,7 +142,7 @@ export const contractVariant = {
141
142
  'Edit src/index.ts to define routes and socket events; exports registry/socket config.',
142
143
  ],
143
144
  async scaffold(ctx) {
144
- const files = contractFiles(ctx.pkgName);
145
+ const files = await contractFiles(ctx.pkgName, ctx.targetDir);
145
146
  for (const [relative, contents] of Object.entries(files)) {
146
147
  // eslint-disable-next-line no-await-in-loop
147
148
  await writeFileIfMissing(ctx.targetDir, relative, contents);
@@ -1,4 +1,4 @@
1
- import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
2
2
  const DOCKER_SCRIPTS = [
3
3
  'dev',
4
4
  'build',
@@ -41,7 +41,7 @@ function dockerPackageJson(name) {
41
41
  '@types/cors': '^2.8.5',
42
42
  '@types/express': '^5.0.6',
43
43
  '@types/node': '^24.10.2',
44
- 'docker-cli-js': '^3.0.9',
44
+ 'docker-cli-js': '^2.10.0',
45
45
  },
46
46
  });
47
47
  }
@@ -95,10 +95,11 @@ EXPOSE 3000
95
95
  CMD ["node", "dist/index.js"]
96
96
  `;
97
97
  }
98
- function dockerFiles(pkgName) {
98
+ async function dockerFiles(pkgName, targetDir) {
99
+ const tsconfig = await packageTsConfig(targetDir, { types: ['node'] });
99
100
  return {
100
101
  'package.json': dockerPackageJson(pkgName),
101
- 'tsconfig.json': baseTsConfig({ types: ['node'] }),
102
+ 'tsconfig.json': tsconfig,
102
103
  'src/index.ts': dockerIndexTs(),
103
104
  'scripts/docker.ts': dockerCliScript(pkgName),
104
105
  '.dockerignore': DOCKER_DOCKERIGNORE,
@@ -190,8 +191,8 @@ async function main() {
190
191
  async function safe(run: () => Promise<unknown>) {
191
192
  try {
192
193
  await run()
193
- } catch (error) {
194
- console.warn(String(error))
194
+ } catch (error: unknown) {
195
+ console.warn(error instanceof Error ? error.message : String(error))
195
196
  }
196
197
  }
197
198
 
@@ -211,8 +212,8 @@ function printHelp() {
211
212
  )
212
213
  }
213
214
 
214
- main().catch((err) => {
215
- console.error(err)
215
+ main().catch((error: unknown) => {
216
+ console.error(error instanceof Error ? error.message : String(error))
216
217
  process.exit(1)
217
218
  })
218
219
  `;
@@ -234,7 +235,7 @@ export const dockerVariant = {
234
235
  'scripts/docker.ts wraps common docker commands with consistent naming.',
235
236
  ],
236
237
  async scaffold(ctx) {
237
- const files = dockerFiles(ctx.pkgName);
238
+ const files = await dockerFiles(ctx.pkgName, ctx.targetDir);
238
239
  for (const [relative, contents] of Object.entries(files)) {
239
240
  // eslint-disable-next-line no-await-in-loop
240
241
  await writeFileIfMissing(ctx.targetDir, relative, contents);
@@ -1,4 +1,4 @@
1
- import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
2
2
  const EMPTY_SCRIPTS = [
3
3
  'dev',
4
4
  'build',
@@ -19,10 +19,11 @@ function emptyPackageJson(name) {
19
19
  },
20
20
  });
21
21
  }
22
- function emptyFiles(pkgName) {
22
+ async function emptyFiles(pkgName, targetDir) {
23
+ const tsconfig = await packageTsConfig(targetDir, { types: ['node'] });
23
24
  return {
24
25
  'package.json': emptyPackageJson(pkgName),
25
- 'tsconfig.json': baseTsConfig({ types: ['node'] }),
26
+ 'tsconfig.json': tsconfig,
26
27
  ...basePackageFiles(),
27
28
  'src/index.ts': "export const hello = 'world'\n",
28
29
  'README.md': buildReadme({
@@ -55,7 +56,7 @@ export const emptyVariant = {
55
56
  scripts: EMPTY_SCRIPTS,
56
57
  notes: ['Start coding in src/index.ts; everything else is wired up.'],
57
58
  async scaffold(ctx) {
58
- const files = emptyFiles(ctx.pkgName);
59
+ const files = await emptyFiles(ctx.pkgName, ctx.targetDir);
59
60
  for (const [relative, contents] of Object.entries(files)) {
60
61
  // eslint-disable-next-line no-await-in-loop
61
62
  await writeFileIfMissing(ctx.targetDir, relative, contents);
@@ -1,4 +1,4 @@
1
- import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
2
2
  const SERVER_SCRIPTS = [
3
3
  'dev',
4
4
  'build',
@@ -81,10 +81,11 @@ export function serverPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLD
81
81
  },
82
82
  });
83
83
  }
84
- function serverFiles(pkgName, contractImport) {
84
+ async function serverFiles(pkgName, contractImport, targetDir) {
85
+ const tsconfig = await packageTsConfig(targetDir, { types: ['node'] });
85
86
  return {
86
87
  'package.json': serverPackageJson(pkgName, contractImport),
87
- 'tsconfig.json': baseTsConfig({ types: ['node'] }),
88
+ 'tsconfig.json': tsconfig,
88
89
  ...basePackageFiles(),
89
90
  'src/index.ts': serverIndexTs(contractImport),
90
91
  '.env.example': 'PORT=4000\n',
@@ -130,7 +131,7 @@ export const serverVariant = {
130
131
  ],
131
132
  async scaffold(ctx) {
132
133
  const contractImport = ctx.contractName ?? CONTRACT_IMPORT_PLACEHOLDER;
133
- const files = serverFiles(ctx.pkgName, contractImport);
134
+ const files = await serverFiles(ctx.pkgName, contractImport, ctx.targetDir);
134
135
  for (const [relative, contents] of Object.entries(files)) {
135
136
  // eslint-disable-next-line no-await-in-loop
136
137
  await writeFileIfMissing(ctx.targetDir, relative, contents);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",