@emeryld/manager 0.2.3 → 0.3.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.
@@ -1,28 +1,46 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process'
3
+ import { existsSync } from 'node:fs'
3
4
  import path from 'node:path'
4
- import { fileURLToPath, pathToFileURL } from 'node:url'
5
+ import { fileURLToPath } from 'node:url'
5
6
  import { createRequire } from 'node:module'
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url)
8
9
  const __dirname = path.dirname(__filename)
9
10
  const packageRoot = path.resolve(__dirname, '..')
10
11
  const tsconfigPath = path.join(packageRoot, 'tsconfig.base.json')
11
- const entryPoint = path.join(packageRoot, 'src', 'publish.ts')
12
+ const compiledEntry = path.join(packageRoot, 'dist', 'publish.js')
13
+ const sourceEntry = path.join(packageRoot, 'src', 'publish.ts')
14
+ const args = process.argv.slice(2)
12
15
 
13
- const require = createRequire(import.meta.url)
14
- const tsNodeLoader = require.resolve('ts-node/esm.mjs')
15
- const registerCode = [
16
- 'import { register } from "node:module";',
17
- 'import { pathToFileURL } from "node:url";',
18
- `register(${JSON.stringify(tsNodeLoader)}, pathToFileURL(${JSON.stringify(packageRoot + path.sep)}));`,
19
- ].join(' ')
20
- const registerImport = `data:text/javascript,${encodeURIComponent(registerCode)}`
16
+ const hasCompiledEntry = existsSync(compiledEntry)
17
+ const hasSourceEntry = existsSync(sourceEntry)
21
18
 
22
- const nodeArgs = ['--import', registerImport, entryPoint, ...process.argv.slice(2)]
23
- const execOptions = {
24
- env: { ...process.env, TS_NODE_PROJECT: tsconfigPath },
25
- stdio: 'inherit',
19
+ let nodeArgs
20
+ let execOptions
21
+
22
+ if (hasCompiledEntry) {
23
+ nodeArgs = [compiledEntry, ...args]
24
+ execOptions = { env: process.env, stdio: 'inherit' }
25
+ } else if (hasSourceEntry) {
26
+ const require = createRequire(import.meta.url)
27
+ const tsNodeLoader = require.resolve('ts-node/esm.mjs')
28
+ const registerCode = [
29
+ 'import { register } from "node:module";',
30
+ 'import { pathToFileURL } from "node:url";',
31
+ `register(${JSON.stringify(tsNodeLoader)}, pathToFileURL(${JSON.stringify(packageRoot + path.sep)}));`,
32
+ ].join(' ')
33
+ const registerImport = `data:text/javascript,${encodeURIComponent(registerCode)}`
34
+ nodeArgs = ['--import', registerImport, sourceEntry, ...args]
35
+ execOptions = {
36
+ env: { ...process.env, TS_NODE_PROJECT: tsconfigPath },
37
+ stdio: 'inherit',
38
+ }
39
+ } else {
40
+ console.error(
41
+ `manager-cli could not find a publish entry point (checked ${compiledEntry} and ${sourceEntry}).`,
42
+ )
43
+ process.exit(1)
26
44
  }
27
45
 
28
46
  const child = spawnSync(process.execPath, nodeArgs, execOptions)
@@ -0,0 +1,388 @@
1
+ import { access, mkdir, readdir, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { askLine, promptSingleKey } from './prompts.js';
4
+ import { colors, logGlobal } from './utils/log.js';
5
+ const VARIANTS = [
6
+ { id: 'rrr-contract', label: 'rrr contract', defaultDir: 'packages/rrr-contract' },
7
+ { id: 'rrr-server', label: 'rrr server', defaultDir: 'packages/rrr-server' },
8
+ { id: 'rrr-client', label: 'rrr client', defaultDir: 'packages/rrr-client' },
9
+ ];
10
+ const workspaceRoot = process.cwd();
11
+ function derivePackageName(targetDir) {
12
+ const base = path.basename(targetDir) || 'rrr-package';
13
+ return base;
14
+ }
15
+ async function ensureTargetDir(targetDir) {
16
+ try {
17
+ const stats = await stat(targetDir);
18
+ if (!stats.isDirectory()) {
19
+ throw new Error(`Target "${targetDir}" exists and is not a directory.`);
20
+ }
21
+ const entries = await readdir(targetDir);
22
+ if (entries.length > 0) {
23
+ logGlobal(`Target ${path.relative(workspaceRoot, targetDir)} is not empty; existing files will be preserved.`, colors.yellow);
24
+ }
25
+ }
26
+ catch (error) {
27
+ if (error &&
28
+ typeof error === 'object' &&
29
+ error.code === 'ENOENT') {
30
+ await mkdir(targetDir, { recursive: true });
31
+ return;
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+ async function writeFileIfMissing(baseDir, relative, contents) {
37
+ const fullPath = path.join(baseDir, relative);
38
+ await mkdir(path.dirname(fullPath), { recursive: true });
39
+ try {
40
+ await access(fullPath);
41
+ const rel = path.relative(workspaceRoot, fullPath);
42
+ console.log(` skipped ${rel} (already exists)`);
43
+ return 'skipped';
44
+ }
45
+ catch (error) {
46
+ if (error &&
47
+ typeof error === 'object' &&
48
+ error.code !== 'ENOENT') {
49
+ throw error;
50
+ }
51
+ }
52
+ await writeFile(fullPath, contents, 'utf8');
53
+ const rel = path.relative(workspaceRoot, fullPath);
54
+ console.log(` created ${rel}`);
55
+ return 'created';
56
+ }
57
+ function contractIndexTs() {
58
+ return `import { defineSocketEvents, finalize, resource } from '@emeryld/rrroutes-contract'
59
+ import { z } from 'zod'
60
+
61
+ const routes = resource('/api')
62
+ .sub(
63
+ resource('health')
64
+ .get({
65
+ outputSchema: z.object({
66
+ status: z.literal('ok'),
67
+ html: z.string().optional(),
68
+ }),
69
+ description: 'Basic GET health probe for uptime + docs.',
70
+ })
71
+ .post({
72
+ bodySchema: z.object({
73
+ echo: z.string().optional(),
74
+ }),
75
+ outputSchema: z.object({
76
+ status: z.literal('ok'),
77
+ received: z.string().optional(),
78
+ }),
79
+ description: 'POST health probe that echoes a payload.',
80
+ })
81
+ .done(),
82
+ )
83
+ .done()
84
+
85
+ export const registry = finalize(routes)
86
+
87
+ const sockets = defineSocketEvents(
88
+ {
89
+ joinMetaMessage: z.object({ room: z.string().optional() }),
90
+ leaveMetaMessage: z.object({ room: z.string().optional() }),
91
+ pingPayload: z.object({
92
+ note: z.string().default('ping'),
93
+ sentAt: z.string(),
94
+ }),
95
+ pongPayload: z.object({
96
+ ok: z.boolean(),
97
+ receivedAt: z.string(),
98
+ echo: z.string().optional(),
99
+ }),
100
+ },
101
+ {
102
+ 'health:connected': {
103
+ message: z.object({
104
+ socketId: z.string(),
105
+ at: z.string(),
106
+ message: z.string(),
107
+ }),
108
+ },
109
+ 'health:ping': {
110
+ message: z.object({
111
+ note: z.string().default('ping'),
112
+ }),
113
+ },
114
+ 'health:pong': {
115
+ message: z.object({
116
+ ok: z.literal(true),
117
+ at: z.string(),
118
+ echo: z.string().optional(),
119
+ }),
120
+ },
121
+ },
122
+ )
123
+
124
+ export const socketConfig = sockets.config
125
+ export const socketEvents = sockets.events
126
+ export type AppRegistry = typeof registry
127
+ `;
128
+ }
129
+ function serverIndexTs(contractImport) {
130
+ return `import 'dotenv/config'
131
+ import http from 'node:http'
132
+ import express from 'express'
133
+ import cors from 'cors'
134
+ import { createRRRoute } from '@emeryld/rrroutes-server'
135
+ import { registry } from '${contractImport}'
136
+
137
+ const app = express()
138
+ app.use(cors({ origin: '*', credentials: true }))
139
+ app.use(express.json())
140
+
141
+ app.get('/', (_req, res) => {
142
+ res.send('<h1>rrr server ready</h1>')
143
+ })
144
+
145
+ const routes = createRRRoute(app, {
146
+ buildCtx: async () => ({
147
+ requestId: Math.random().toString(36).slice(2),
148
+ }),
149
+ debug:
150
+ process.env.NODE_ENV === 'development'
151
+ ? { request: true, handler: true }
152
+ : undefined,
153
+ })
154
+
155
+ routes.registerControllers(registry, {
156
+ 'GET /api/health': {
157
+ handler: async ({ ctx }) => ({
158
+ out: {
159
+ status: 'ok',
160
+ requestId: ctx.requestId,
161
+ at: new Date().toISOString(),
162
+ },
163
+ }),
164
+ },
165
+ })
166
+
167
+ const PORT = Number.parseInt(process.env.PORT ?? '4000', 10)
168
+ const server = http.createServer(app)
169
+
170
+ server.listen(PORT, () => {
171
+ console.log(\`rrr server listening on http://localhost:\${PORT}\`)
172
+ })
173
+ `;
174
+ }
175
+ function clientIndexTs(contractImport) {
176
+ return `import { QueryClient } from '@tanstack/react-query'
177
+ import { createRouteClient } from '@emeryld/rrroutes-client'
178
+ import { registry } from '${contractImport}'
179
+
180
+ const baseUrl = process.env.RRR_API_URL ?? 'http://localhost:4000'
181
+ export const queryClient = new QueryClient()
182
+
183
+ export const routeClient = createRouteClient({
184
+ baseUrl,
185
+ queryClient,
186
+ environment: process.env.NODE_ENV === 'production' ? 'production' : 'development',
187
+ })
188
+
189
+ export const healthGet = routeClient.build(registry.byKey['GET /api/health'])
190
+ export const healthPost = routeClient.build(registry.byKey['POST /api/health'])
191
+ `;
192
+ }
193
+ function baseTsConfig(options) {
194
+ return `${JSON.stringify({
195
+ compilerOptions: {
196
+ target: 'ES2020',
197
+ module: 'NodeNext',
198
+ moduleResolution: 'NodeNext',
199
+ outDir: 'dist',
200
+ rootDir: 'src',
201
+ declaration: true,
202
+ sourceMap: true,
203
+ strict: true,
204
+ esModuleInterop: true,
205
+ skipLibCheck: true,
206
+ lib: options?.lib,
207
+ types: options?.types,
208
+ },
209
+ include: ['src/**/*'],
210
+ }, null, 2)}\n`;
211
+ }
212
+ function contractPackageJson(name) {
213
+ return `${JSON.stringify({
214
+ name,
215
+ version: '0.1.0',
216
+ private: false,
217
+ type: 'module',
218
+ main: 'dist/index.js',
219
+ types: 'dist/index.d.ts',
220
+ exports: {
221
+ '.': {
222
+ types: './dist/index.d.ts',
223
+ import: './dist/index.js',
224
+ },
225
+ },
226
+ files: ['dist'],
227
+ scripts: {
228
+ build: 'tsc -p tsconfig.json',
229
+ typecheck: 'tsc -p tsconfig.json --noEmit',
230
+ },
231
+ dependencies: {
232
+ '@emeryld/rrroutes-contract': '^2.5.2',
233
+ zod: '^4.2.1',
234
+ },
235
+ devDependencies: {
236
+ typescript: '^5.9.3',
237
+ },
238
+ }, null, 2)}\n`;
239
+ }
240
+ function serverPackageJson(name) {
241
+ return `${JSON.stringify({
242
+ name,
243
+ version: '0.1.0',
244
+ private: false,
245
+ type: 'module',
246
+ main: 'dist/index.js',
247
+ types: 'dist/index.d.ts',
248
+ files: ['dist'],
249
+ scripts: {
250
+ dev: 'node --loader ts-node/esm src/index.ts',
251
+ build: 'tsc -p tsconfig.json',
252
+ typecheck: 'tsc -p tsconfig.json --noEmit',
253
+ start: 'node dist/index.js',
254
+ },
255
+ dependencies: {
256
+ '@emeryld/rrroutes-contract': '^2.5.2',
257
+ '@emeryld/rrroutes-server': '^2.4.1',
258
+ cors: '^2.8.5',
259
+ dotenv: '^16.4.5',
260
+ express: '^5.1.0',
261
+ zod: '^4.2.1',
262
+ },
263
+ devDependencies: {
264
+ '@types/cors': '^2.8.5',
265
+ '@types/express': '^5.0.6',
266
+ '@types/node': '^24.10.2',
267
+ 'ts-node': '^10.9.2',
268
+ typescript: '^5.9.3',
269
+ },
270
+ }, null, 2)}\n`;
271
+ }
272
+ function clientPackageJson(name) {
273
+ return `${JSON.stringify({
274
+ name,
275
+ version: '0.1.0',
276
+ private: true,
277
+ type: 'module',
278
+ main: 'dist/index.js',
279
+ types: 'dist/index.d.ts',
280
+ files: ['dist'],
281
+ scripts: {
282
+ build: 'tsc -p tsconfig.json',
283
+ typecheck: 'tsc -p tsconfig.json --noEmit',
284
+ },
285
+ dependencies: {
286
+ '@emeryld/rrroutes-client': '^2.5.3',
287
+ '@emeryld/rrroutes-contract': '^2.5.2',
288
+ '@tanstack/react-query': '^5.90.12',
289
+ 'socket.io-client': '^4.8.3',
290
+ },
291
+ devDependencies: {
292
+ '@types/node': '^24.10.2',
293
+ typescript: '^5.9.3',
294
+ },
295
+ }, null, 2)}\n`;
296
+ }
297
+ function contractFiles(pkgName) {
298
+ return {
299
+ 'package.json': contractPackageJson(pkgName),
300
+ 'tsconfig.json': baseTsConfig(),
301
+ 'src/index.ts': contractIndexTs(),
302
+ 'README.md': `# ${pkgName}
303
+
304
+ Contract package scaffolded by manager-cli.
305
+ - edit src/index.ts to add routes and socket events
306
+ - build with \`npm run build\`
307
+ - import the registry in your server/client packages
308
+ `,
309
+ };
310
+ }
311
+ function serverFiles(pkgName, contractImport) {
312
+ return {
313
+ 'package.json': serverPackageJson(pkgName),
314
+ 'tsconfig.json': baseTsConfig({ types: ['node'] }),
315
+ 'src/index.ts': serverIndexTs(contractImport),
316
+ '.env.example': 'PORT=4000\n',
317
+ 'README.md': `# ${pkgName}
318
+
319
+ Starter RRRoutes server scaffold.
320
+ - update the contract import in src/index.ts if needed (${contractImport})
321
+ - run \`npm install\` then \`npm run dev\` to start the API
322
+ `,
323
+ };
324
+ }
325
+ function clientFiles(pkgName, contractImport) {
326
+ return {
327
+ 'package.json': clientPackageJson(pkgName),
328
+ 'tsconfig.json': baseTsConfig({ lib: ['ES2020', 'DOM'], types: ['node'] }),
329
+ 'src/index.ts': clientIndexTs(contractImport),
330
+ 'README.md': `# ${pkgName}
331
+
332
+ Starter RRRoutes client scaffold.
333
+ - update the contract import in src/index.ts if needed (${contractImport})
334
+ - the generated QueryClient is exported from src/index.ts
335
+ `,
336
+ };
337
+ }
338
+ function resolveContractImport(variant) {
339
+ if (variant === 'rrr-contract')
340
+ return '';
341
+ return '@your-scope/contract';
342
+ }
343
+ async function scaffoldVariant(variant, targetDir, pkgName) {
344
+ const contractImport = resolveContractImport(variant);
345
+ let files;
346
+ if (variant === 'rrr-contract')
347
+ files = contractFiles(pkgName);
348
+ else if (variant === 'rrr-server')
349
+ files = serverFiles(pkgName, contractImport);
350
+ else
351
+ files = clientFiles(pkgName, contractImport);
352
+ for (const [relative, contents] of Object.entries(files)) {
353
+ // eslint-disable-next-line no-await-in-loop
354
+ await writeFileIfMissing(targetDir, relative, contents);
355
+ }
356
+ }
357
+ async function promptForVariant() {
358
+ const messageLines = [
359
+ 'Pick a package template:',
360
+ VARIANTS.map((opt, idx) => ` [${idx + 1}] ${opt.label}`).join('\n'),
361
+ 'Enter 1, 2, or 3: ',
362
+ ];
363
+ const message = `${messageLines.join('\n')}`;
364
+ const variant = await promptSingleKey(message, (key) => {
365
+ if (key === '1')
366
+ return VARIANTS[0];
367
+ if (key === '2')
368
+ return VARIANTS[1];
369
+ if (key === '3')
370
+ return VARIANTS[2];
371
+ return undefined;
372
+ });
373
+ return variant;
374
+ }
375
+ async function promptForTargetDir(fallback) {
376
+ const answer = await askLine(`Path for the new package? (${fallback}): `);
377
+ const normalized = answer || fallback;
378
+ return path.resolve(workspaceRoot, normalized);
379
+ }
380
+ export async function createRrrPackage() {
381
+ const variant = await promptForVariant();
382
+ const targetDir = await promptForTargetDir(variant.defaultDir);
383
+ await ensureTargetDir(targetDir);
384
+ const pkgName = derivePackageName(targetDir);
385
+ logGlobal(`Creating ${variant.label} in ${path.relative(workspaceRoot, targetDir) || '.'}`, colors.green);
386
+ await scaffoldVariant(variant.id, targetDir, pkgName);
387
+ logGlobal('Scaffold complete. Install deps and start building!', colors.green);
388
+ }
@@ -1,28 +1,46 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process'
3
+ import { existsSync } from 'node:fs'
3
4
  import path from 'node:path'
4
- import { fileURLToPath, pathToFileURL } from 'node:url'
5
+ import { fileURLToPath } from 'node:url'
5
6
  import { createRequire } from 'node:module'
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url)
8
9
  const __dirname = path.dirname(__filename)
9
10
  const packageRoot = path.resolve(__dirname, '..')
10
11
  const tsconfigPath = path.join(packageRoot, 'tsconfig.base.json')
11
- const entryPoint = path.join(packageRoot, 'src', 'publish.ts')
12
+ const compiledEntry = path.join(packageRoot, 'dist', 'publish.js')
13
+ const sourceEntry = path.join(packageRoot, 'src', 'publish.ts')
14
+ const args = process.argv.slice(2)
12
15
 
13
- const require = createRequire(import.meta.url)
14
- const tsNodeLoader = require.resolve('ts-node/esm.mjs')
15
- const registerCode = [
16
- 'import { register } from "node:module";',
17
- 'import { pathToFileURL } from "node:url";',
18
- `register(${JSON.stringify(tsNodeLoader)}, pathToFileURL(${JSON.stringify(packageRoot + path.sep)}));`,
19
- ].join(' ')
20
- const registerImport = `data:text/javascript,${encodeURIComponent(registerCode)}`
16
+ const hasCompiledEntry = existsSync(compiledEntry)
17
+ const hasSourceEntry = existsSync(sourceEntry)
21
18
 
22
- const nodeArgs = ['--import', registerImport, entryPoint, ...process.argv.slice(2)]
23
- const execOptions = {
24
- env: { ...process.env, TS_NODE_PROJECT: tsconfigPath },
25
- stdio: 'inherit',
19
+ let nodeArgs
20
+ let execOptions
21
+
22
+ if (hasCompiledEntry) {
23
+ nodeArgs = [compiledEntry, ...args]
24
+ execOptions = { env: process.env, stdio: 'inherit' }
25
+ } else if (hasSourceEntry) {
26
+ const require = createRequire(import.meta.url)
27
+ const tsNodeLoader = require.resolve('ts-node/esm.mjs')
28
+ const registerCode = [
29
+ 'import { register } from "node:module";',
30
+ 'import { pathToFileURL } from "node:url";',
31
+ `register(${JSON.stringify(tsNodeLoader)}, pathToFileURL(${JSON.stringify(packageRoot + path.sep)}));`,
32
+ ].join(' ')
33
+ const registerImport = `data:text/javascript,${encodeURIComponent(registerCode)}`
34
+ nodeArgs = ['--import', registerImport, sourceEntry, ...args]
35
+ execOptions = {
36
+ env: { ...process.env, TS_NODE_PROJECT: tsconfigPath },
37
+ stdio: 'inherit',
38
+ }
39
+ } else {
40
+ console.error(
41
+ `manager-cli could not find a publish entry point (checked ${compiledEntry} and ${sourceEntry}).`,
42
+ )
43
+ process.exit(1)
26
44
  }
27
45
 
28
46
  const child = spawnSync(process.execPath, nodeArgs, execOptions)
package/dist/menu.js CHANGED
@@ -95,6 +95,8 @@ export function buildPackageSelectionMenu(packages, onStepComplete) {
95
95
  onStepComplete?.(step);
96
96
  },
97
97
  }));
98
+ if (ordered.length === 0)
99
+ return entries;
98
100
  entries.push({
99
101
  name: 'All packages',
100
102
  emoji: globalEmoji,
package/dist/packages.js CHANGED
@@ -37,7 +37,7 @@ function deriveSubstitute(name) {
37
37
  return '';
38
38
  const segments = trimmed.split(/[@\/\-]/).filter(Boolean);
39
39
  const transformed = segments
40
- .map((segment) => segment.slice(0, 2))
40
+ .map((segment) => segment)
41
41
  .filter(Boolean)
42
42
  .join(' ');
43
43
  return transformed || trimmed;
package/dist/publish.js CHANGED
@@ -5,6 +5,8 @@ import { getOrderedPackages, loadPackages, resolvePackage } from './packages.js'
5
5
  import { releaseMultiple, releaseSingle, } from './release.js';
6
6
  import { ensureWorkingTreeCommitted } from './preflight.js';
7
7
  import { publishCliState } from './prompts.js';
8
+ import { createRrrPackage } from './create-package.js';
9
+ import { colors, logGlobal } from './utils/log.js';
8
10
  function resolveTargetsFromArg(packages, arg) {
9
11
  if (arg.toLowerCase() === 'all')
10
12
  return getOrderedPackages(packages);
@@ -89,14 +91,27 @@ function optsFromParsed(p) {
89
91
  }
90
92
  async function runPackageSelectionLoop(packages, helperArgs) {
91
93
  let argv = [...helperArgs];
94
+ let currentPackages = packages;
92
95
  // eslint-disable-next-line no-constant-condition
93
96
  while (true) {
94
97
  let lastStep;
95
98
  await runHelperCli({
96
99
  title: 'Pick one of the packages or all',
97
- scripts: buildPackageSelectionMenu(packages, (step) => {
98
- lastStep = step;
99
- }),
100
+ scripts: [
101
+ ...buildPackageSelectionMenu(currentPackages, (step) => {
102
+ lastStep = step;
103
+ }),
104
+ {
105
+ name: 'Create package',
106
+ emoji: '✨',
107
+ description: 'Scaffold a new rrr package (contract/server/client)',
108
+ handler: async () => {
109
+ await createRrrPackage();
110
+ currentPackages = await loadPackages();
111
+ lastStep = 'back';
112
+ },
113
+ },
114
+ ],
100
115
  argv, // pass through CLI args only once; subsequent loops rely on selection
101
116
  });
102
117
  argv = [];
@@ -108,10 +123,11 @@ async function main() {
108
123
  const cliArgs = process.argv.slice(2);
109
124
  const parsed = parseCliArgs(cliArgs);
110
125
  const packages = await loadPackages();
111
- if (packages.length === 0)
112
- throw new Error('No packages found in ./packages');
113
126
  // If user provided non-interactive flags, run headless path
114
127
  if (parsed.nonInteractive) {
128
+ if (packages.length === 0) {
129
+ throw new Error('No packages found in ./packages');
130
+ }
115
131
  publishCliState.autoConfirmAll = true;
116
132
  if (!parsed.selectionArg) {
117
133
  throw new Error('Non-interactive mode requires a package selection: <pkg> or "all".');
@@ -128,6 +144,9 @@ async function main() {
128
144
  await releaseSingle(targets[0], packages, opts);
129
145
  return;
130
146
  }
147
+ if (packages.length === 0) {
148
+ logGlobal('No packages found in ./packages. Use "Create package" to scaffold one.', colors.yellow);
149
+ }
131
150
  // Interactive flow (unchanged): selection menu then step menu
132
151
  if (parsed.selectionArg) {
133
152
  const targets = resolveTargetsFromArg(packages, parsed.selectionArg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",