@emeryld/manager 0.6.0 โ†’ 0.6.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.
@@ -80,16 +80,29 @@ async function promptForContractName() {
80
80
  }
81
81
  async function promptForClientKind(existing) {
82
82
  const defaultKind = normalizeClientKind(existing) ?? DEFAULT_CLIENT_KIND;
83
- const optionLabels = CLIENT_KIND_OPTIONS.map((opt, idx) => `[${idx + 1}] ${opt.label}`).join(', ');
84
- const answer = await askLine(`Client template? ${optionLabels} (${defaultKind}): `);
85
- const normalized = normalizeClientKind(answer);
86
- if (normalized)
87
- return normalized;
88
- if (answer === '1')
89
- return CLIENT_KIND_OPTIONS[0]?.id ?? defaultKind;
90
- if (answer === '2')
91
- return CLIENT_KIND_OPTIONS[1]?.id ?? defaultKind;
92
- return defaultKind;
83
+ let selection;
84
+ const scripts = CLIENT_KIND_OPTIONS.map((opt) => ({
85
+ name: opt.label,
86
+ emoji: '๐Ÿ’ป',
87
+ description: opt.summary,
88
+ handler: () => {
89
+ selection = opt.id;
90
+ },
91
+ }));
92
+ scripts.push({
93
+ name: `Use default (${defaultKind})`,
94
+ emoji: 'โœ…',
95
+ description: 'Keep the prior/default choice',
96
+ handler: () => {
97
+ selection = defaultKind;
98
+ },
99
+ });
100
+ await runHelperCli({
101
+ title: 'Select a client template',
102
+ scripts,
103
+ argv: [],
104
+ });
105
+ return selection ?? defaultKind;
93
106
  }
94
107
  function resolveVariant(key) {
95
108
  if (!key)
@@ -18,19 +18,29 @@ const DOCKER_SCRIPTS = [
18
18
  'docker:clean',
19
19
  'docker:reset',
20
20
  ];
21
+ function sanitizeContainerName(name) {
22
+ const normalized = name
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9]/gi, '-')
25
+ .replace(/-+/g, '-')
26
+ .replace(/^-+|-+$/g, '') || 'rrr-service';
27
+ return normalized;
28
+ }
21
29
  function dockerPackageJson(name, options) {
30
+ const image = `${name}:latest`;
31
+ const container = sanitizeContainerName(name);
32
+ const portVar = '${PORT:-3000}';
22
33
  return basePackageJson({
23
34
  name,
24
35
  scripts: baseScripts('tsx watch src/index.ts', {
25
36
  start: 'node dist/index.js',
26
- 'docker:cli': 'tsx scripts/docker.ts',
27
- 'docker:build': 'npm run docker:cli -- build',
28
- 'docker:up': 'npm run docker:cli -- up',
29
- 'docker:dev': 'npm run docker:cli -- dev',
30
- 'docker:logs': 'npm run docker:cli -- logs',
31
- 'docker:stop': 'npm run docker:cli -- stop',
32
- 'docker:clean': 'npm run docker:cli -- clean',
33
- 'docker:reset': 'npm run docker:cli -- reset',
37
+ 'docker:build': `docker build -t ${image} .`,
38
+ 'docker:up': `npm run docker:build && PORT=${portVar} docker run -d --rm -p ${portVar}:${portVar} --name ${container} ${image}`,
39
+ 'docker:dev': `npm run docker:build && PORT=${portVar} docker run -d --rm -p ${portVar}:${portVar} --name ${container} ${image} && docker logs -f ${container}`,
40
+ 'docker:logs': `docker logs -f ${container}`,
41
+ 'docker:stop': `docker stop ${container}`,
42
+ 'docker:clean': `docker stop ${container} || true; docker rm -f ${container} || true`,
43
+ 'docker:reset': `npm run docker:clean && docker rmi -f ${image} || true`,
34
44
  }, { includePrepare: options?.includePrepare }),
35
45
  dependencies: {
36
46
  cors: '^2.8.5',
@@ -41,7 +51,6 @@ function dockerPackageJson(name, options) {
41
51
  '@types/cors': '^2.8.5',
42
52
  '@types/express': '^5.0.6',
43
53
  '@types/node': '^24.10.2',
44
- 'docker-cli-js': '^2.10.0',
45
54
  },
46
55
  });
47
56
  }
@@ -105,7 +114,6 @@ async function dockerFiles(pkgName, targetDir) {
105
114
  'package.json': dockerPackageJson(pkgName, { includePrepare }),
106
115
  'tsconfig.json': tsconfig,
107
116
  'src/index.ts': dockerIndexTs(),
108
- 'scripts/docker.ts': dockerCliScript(pkgName),
109
117
  '.dockerignore': DOCKER_DOCKERIGNORE,
110
118
  ...basePackageFiles(),
111
119
  Dockerfile: dockerDockerfile(),
@@ -143,145 +151,16 @@ async function dockerFiles(pkgName, targetDir) {
143
151
  }),
144
152
  };
145
153
  }
146
- function dockerCliScript(pkgName) {
147
- return `#!/usr/bin/env tsx
148
- import { readFile } from 'node:fs/promises'
149
- import path from 'node:path'
150
- import { fileURLToPath } from 'node:url'
151
- import { Docker } from 'docker-cli-js'
152
- import { runHelperCli } from '@emeryld/manager/dist/helper-cli.js'
153
-
154
- const __filename = fileURLToPath(import.meta.url)
155
- const __dirname = path.dirname(__filename)
156
- const pkgRoot = path.join(__dirname, '..')
157
- const pkgRaw = await readFile(path.join(pkgRoot, 'package.json'), 'utf8')
158
- const pkg = JSON.parse(pkgRaw) as { name?: string }
159
- const image = \`\${pkg.name ?? '${pkgName}'}:latest\`
160
- const container =
161
- (pkg.name ?? '${pkgName}')
162
- .replace(/[^a-z0-9]/gi, '-')
163
- .replace(/^-+|-+$/g, '') || 'rrr-service'
164
- const port = process.env.PORT ?? '3000'
165
- const docker = new Docker({ spawnOptions: { stdio: 'inherit' } })
166
-
167
- async function safe(run: () => Promise<unknown>) {
168
- try {
169
- await run()
170
- } catch (error: unknown) {
171
- console.warn(error instanceof Error ? error.message : String(error))
172
- }
173
- }
174
-
175
- function printHelp() {
176
- console.log(
177
- [
178
- 'Docker helper commands:',
179
- ' build -> docker build -t ${pkgName}:latest .',
180
- ' up -> build then run in detached mode',
181
- ' dev -> build, run, and tail logs',
182
- ' run -> run existing image detached',
183
- ' logs -> docker logs -f <container>',
184
- ' stop -> docker stop <container>',
185
- ' clean -> docker stop/rm <container>',
186
- ' reset -> clean container and remove image',
187
- ].join('\\n'),
188
- )
189
- }
190
-
191
- await runHelperCli({
192
- title: 'Docker helper',
193
- argv: process.argv.slice(2),
194
- scripts: [
195
- {
196
- name: 'build',
197
- emoji: '๐Ÿ”จ',
198
- description: 'docker build -t image .',
199
- handler: () => docker.command(\`build -t \${image} .\`),
200
- },
201
- {
202
- name: 'up',
203
- emoji: 'โฌ†๏ธ',
204
- description: 'Build + run detached',
205
- handler: async () => {
206
- await docker.command(\`build -t \${image} .\`)
207
- await docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
208
- },
209
- },
210
- {
211
- name: 'dev',
212
- emoji: '๐Ÿ› ๏ธ',
213
- description: 'Build, run, tail logs',
214
- handler: async () => {
215
- await docker.command(\`build -t \${image} .\`)
216
- await docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
217
- await docker.command(\`logs -f \${container}\`)
218
- },
219
- },
220
- {
221
- name: 'run',
222
- emoji: '๐Ÿƒ',
223
- description: 'Run existing image detached',
224
- handler: () =>
225
- docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`),
226
- },
227
- {
228
- name: 'logs',
229
- emoji: '๐Ÿ“œ',
230
- description: 'Tail docker logs',
231
- handler: () => docker.command(\`logs -f \${container}\`),
232
- },
233
- {
234
- name: 'stop',
235
- emoji: '๐Ÿ›‘',
236
- description: 'Stop container',
237
- handler: () => docker.command(\`stop \${container}\`),
238
- },
239
- {
240
- name: 'clean',
241
- emoji: '๐Ÿงน',
242
- description: 'Stop + remove container',
243
- handler: async () => {
244
- await safe(() => docker.command(\`stop \${container}\`))
245
- await safe(() => docker.command(\`rm -f \${container}\`))
246
- },
247
- },
248
- {
249
- name: 'reset',
250
- emoji: 'โ™ป๏ธ',
251
- description: 'Clean container and remove image',
252
- handler: async () => {
253
- await safe(() => docker.command(\`stop \${container}\`))
254
- await safe(() => docker.command(\`rm -f \${container}\`))
255
- await safe(() => docker.command(\`rmi -f \${image}\`))
256
- },
257
- },
258
- {
259
- name: 'help',
260
- emoji: 'โ„น๏ธ',
261
- description: 'Print commands',
262
- handler: () => {
263
- printHelp()
264
- },
265
- },
266
- ],
267
- })
268
- `;
269
- }
270
154
  export const dockerVariant = {
271
155
  id: 'rrr-docker',
272
156
  label: 'dockerized service',
273
157
  defaultDir: 'packages/rrr-docker',
274
- summary: 'Express service plus Dockerfile and a helper CLI for local runs.',
275
- keyFiles: [
276
- 'src/index.ts',
277
- 'scripts/docker.ts',
278
- 'Dockerfile',
279
- 'README.md',
280
- ],
158
+ summary: 'Express service plus Dockerfile and docker scripts for local runs.',
159
+ keyFiles: ['src/index.ts', 'Dockerfile', 'README.md'],
281
160
  scripts: DOCKER_SCRIPTS,
282
161
  notes: [
283
162
  'Use docker:dev or docker:up to build and run the container quickly.',
284
- 'scripts/docker.ts wraps common docker commands with consistent naming.',
163
+ 'Manager CLI surfaces Docker helpers automatically when a Dockerfile is present.',
285
164
  ],
286
165
  async scaffold(ctx) {
287
166
  const files = await dockerFiles(ctx.pkgName, ctx.targetDir);
@@ -17,6 +17,7 @@ function emptyPackageJson(name, options) {
17
17
  includePrepare: options?.includePrepare,
18
18
  }),
19
19
  devDependencies: {
20
+ '@types/node': '^24.10.2',
20
21
  ...BASE_LINT_DEV_DEPENDENCIES,
21
22
  },
22
23
  });
package/dist/docker.js ADDED
@@ -0,0 +1,128 @@
1
+ import { access } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { runHelperCli } from './helper-cli.js';
4
+ import { run } from './utils/run.js';
5
+ function sanitizeName(name) {
6
+ return name
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9]/gi, '-')
9
+ .replace(/-+/g, '-')
10
+ .replace(/^-+|-+$/g, '');
11
+ }
12
+ async function ensureDockerfileExists(dockerfilePath) {
13
+ try {
14
+ await access(dockerfilePath);
15
+ }
16
+ catch {
17
+ throw new Error(`Dockerfile not found at ${dockerfilePath}`);
18
+ }
19
+ }
20
+ async function dockerCommand(args, pkg, options) {
21
+ try {
22
+ await run('docker', args, { cwd: pkg.path });
23
+ }
24
+ catch (error) {
25
+ if (!options?.ignoreErrors)
26
+ throw error;
27
+ // eslint-disable-next-line no-console
28
+ console.warn(error instanceof Error ? error.message : String(error));
29
+ }
30
+ }
31
+ function printHelp(image, container) {
32
+ console.log([
33
+ 'Docker helper commands:',
34
+ ` build -> docker build -t ${image} .`,
35
+ ' up -> build then run in detached mode',
36
+ ' dev -> build, run, and tail logs',
37
+ ' run -> run existing image detached',
38
+ ` logs -> docker logs -f ${container}`,
39
+ ` stop -> docker stop ${container}`,
40
+ ` clean -> docker stop/rm ${container}`,
41
+ ` reset -> clean container and remove ${image}`,
42
+ ].join('\n'));
43
+ }
44
+ export async function openDockerHelper(pkg) {
45
+ const dockerfilePath = pkg.dockerfilePath ?? path.join(pkg.path, 'Dockerfile');
46
+ await ensureDockerfileExists(dockerfilePath);
47
+ const image = `${pkg.name ?? pkg.dirName}:latest`;
48
+ const container = sanitizeName(pkg.name ?? pkg.dirName) || 'rrr-service';
49
+ const port = process.env.PORT ?? '3000';
50
+ const dockerfileLabel = path
51
+ .relative(process.cwd(), dockerfilePath)
52
+ .replace(/\\/g, '/');
53
+ const scripts = [
54
+ {
55
+ name: 'build',
56
+ emoji: '๐Ÿ”จ',
57
+ description: 'docker build -t image .',
58
+ handler: () => dockerCommand(['build', '-t', image, '.'], pkg),
59
+ },
60
+ {
61
+ name: 'up',
62
+ emoji: 'โฌ†๏ธ',
63
+ description: 'Build + run detached',
64
+ handler: async () => {
65
+ await dockerCommand(['build', '-t', image, '.'], pkg);
66
+ await dockerCommand(['run', '-d', '--rm', '-p', `${port}:${port}`, '--name', container, image], pkg);
67
+ },
68
+ },
69
+ {
70
+ name: 'dev',
71
+ emoji: '๐Ÿ› ๏ธ',
72
+ description: 'Build, run, tail logs',
73
+ handler: async () => {
74
+ await dockerCommand(['build', '-t', image, '.'], pkg);
75
+ await dockerCommand(['run', '-d', '--rm', '-p', `${port}:${port}`, '--name', container, image], pkg);
76
+ await dockerCommand(['logs', '-f', container], pkg);
77
+ },
78
+ },
79
+ {
80
+ name: 'run',
81
+ emoji: '๐Ÿƒ',
82
+ description: 'Run existing image detached',
83
+ handler: () => dockerCommand(['run', '-d', '--rm', '-p', `${port}:${port}`, '--name', container, image], pkg),
84
+ },
85
+ {
86
+ name: 'logs',
87
+ emoji: '๐Ÿ“œ',
88
+ description: 'Tail docker logs',
89
+ handler: () => dockerCommand(['logs', '-f', container], pkg),
90
+ },
91
+ {
92
+ name: 'stop',
93
+ emoji: '๐Ÿ›‘',
94
+ description: 'Stop container',
95
+ handler: () => dockerCommand(['stop', container], pkg),
96
+ },
97
+ {
98
+ name: 'clean',
99
+ emoji: '๐Ÿงน',
100
+ description: 'Stop + remove container',
101
+ handler: async () => {
102
+ await dockerCommand(['stop', container], pkg, { ignoreErrors: true });
103
+ await dockerCommand(['rm', '-f', container], pkg, { ignoreErrors: true });
104
+ },
105
+ },
106
+ {
107
+ name: 'reset',
108
+ emoji: 'โ™ป๏ธ',
109
+ description: 'Clean container and remove image',
110
+ handler: async () => {
111
+ await dockerCommand(['stop', container], pkg, { ignoreErrors: true });
112
+ await dockerCommand(['rm', '-f', container], pkg, { ignoreErrors: true });
113
+ await dockerCommand(['rmi', '-f', image], pkg, { ignoreErrors: true });
114
+ },
115
+ },
116
+ {
117
+ name: 'help',
118
+ emoji: 'โ„น๏ธ',
119
+ description: 'Print commands',
120
+ handler: () => printHelp(image, container),
121
+ },
122
+ ];
123
+ await runHelperCli({
124
+ title: `Docker helper for ${pkg.name ?? pkg.dirName} (${dockerfileLabel})`,
125
+ scripts,
126
+ argv: [],
127
+ });
128
+ }
package/dist/menu.js CHANGED
@@ -1,9 +1,12 @@
1
+ // src/menu.js
2
+ import path from 'node:path';
1
3
  import { colors, globalEmoji } from './utils/log.js';
2
4
  import { updateDependencies, testAll, testSingle, buildAll, buildSingle, } from './workspace.js';
3
5
  import { releaseMultiple, releaseSingle } from './release.js';
4
6
  import { getOrderedPackages } from './packages.js';
5
7
  import { runHelperCli } from './helper-cli.js';
6
8
  import { ensureWorkingTreeCommitted } from './preflight.js';
9
+ import { openDockerHelper } from './docker.js';
7
10
  import { run } from './utils/run.js';
8
11
  function makeManagerStepEntries(targets, packages, state, options) {
9
12
  const includeBack = options?.includeBack ?? true;
@@ -93,6 +96,19 @@ function makeManagerStepEntries(targets, packages, state, options) {
93
96
  function makePackageScriptEntries(pkg) {
94
97
  const scripts = pkg.json?.scripts ?? {};
95
98
  const entries = [];
99
+ if (pkg.dockerfilePath) {
100
+ const dockerLabel = path
101
+ .relative(process.cwd(), pkg.dockerfilePath)
102
+ .replace(/\\/g, '/');
103
+ entries.push({
104
+ name: `Dockerfile (${dockerLabel})`,
105
+ emoji: '๐Ÿณ',
106
+ description: 'manager -> opens the docker cli',
107
+ handler: async () => {
108
+ await openDockerHelper(pkg);
109
+ },
110
+ });
111
+ }
96
112
  Object.entries(scripts)
97
113
  .sort(([a], [b]) => a.localeCompare(b))
98
114
  .forEach(([name, command]) => {
@@ -147,9 +163,20 @@ export async function runStepLoop(targets, packages) {
147
163
  emoji: globalEmoji,
148
164
  description: 'update/test/build/publish',
149
165
  handler: async () => {
150
- const managerEntries = makeManagerStepEntries(targets, packages, state, {
151
- includeBack: false,
152
- });
166
+ const managerEntries = [
167
+ ...makeManagerStepEntries(targets, packages, state, {
168
+ includeBack: false,
169
+ }),
170
+ {
171
+ name: 'back',
172
+ emoji: 'โ†ฉ๏ธ',
173
+ description: 'Return to package scripts',
174
+ handler: () => {
175
+ // no state change; just exit to the package scripts menu
176
+ state.lastStep = undefined;
177
+ },
178
+ },
179
+ ];
153
180
  await runHelperCli({
154
181
  title: `Manager actions for ${pkg.name}`,
155
182
  scripts: managerEntries,
package/dist/packages.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/packages.js
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
- import { readdir, readFile } from 'node:fs/promises';
4
+ import { readdir, readFile, stat } from 'node:fs/promises';
5
5
  const rootDir = process.cwd();
6
6
  const ignoredDirs = new Set([
7
7
  'node_modules',
@@ -181,6 +181,16 @@ export async function loadPackages() {
181
181
  byName.get((pkgName ?? '').toLowerCase());
182
182
  const substitute = meta?.substitute ?? deriveSubstitute(pkgName) ?? path.basename(pkgDir);
183
183
  const color = meta?.color ?? colorFromSeed(pkgName);
184
+ let dockerfilePath;
185
+ try {
186
+ const candidate = path.join(pkgDir, 'Dockerfile');
187
+ const stats = await stat(candidate);
188
+ if (stats.isFile())
189
+ dockerfilePath = candidate;
190
+ }
191
+ catch {
192
+ // ignore missing Dockerfile
193
+ }
184
194
  packages.push({
185
195
  dirName: path.basename(pkgDir),
186
196
  relativeDir: relativePath,
@@ -191,6 +201,7 @@ export async function loadPackages() {
191
201
  name: pkgName ?? path.basename(pkgDir),
192
202
  substitute,
193
203
  color,
204
+ dockerfilePath,
194
205
  });
195
206
  }
196
207
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",