@emeryld/manager 0.6.2 → 0.6.4

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/dist/menu.js CHANGED
@@ -1,13 +1,14 @@
1
1
  // src/menu.js
2
2
  import path from 'node:path';
3
3
  import { colors, globalEmoji } from './utils/log.js';
4
- import { updateDependencies, testAll, testSingle, buildAll, buildSingle, } from './workspace.js';
4
+ import { updateDependencies, testAll, testSingle, buildAll, buildSingle, cleanPackages, rebuildPackages, } from './workspace.js';
5
5
  import { releaseMultiple, releaseSingle } from './release.js';
6
6
  import { getOrderedPackages } from './packages.js';
7
7
  import { runHelperCli } from './helper-cli.js';
8
8
  import { ensureWorkingTreeCommitted } from './preflight.js';
9
9
  import { openDockerHelper } from './docker.js';
10
10
  import { run } from './utils/run.js';
11
+ import { makeBaseScriptEntries, getPackageMarker } from './menu/script-helpers.js';
11
12
  function makeManagerStepEntries(targets, packages, state, options) {
12
13
  const includeBack = options?.includeBack ?? true;
13
14
  return [
@@ -20,6 +21,24 @@ function makeManagerStepEntries(targets, packages, state, options) {
20
21
  state.lastStep = 'update';
21
22
  },
22
23
  },
24
+ {
25
+ name: 'clean',
26
+ emoji: '🧹',
27
+ description: 'Remove node_modules, dist, and caches',
28
+ handler: async () => {
29
+ await cleanPackages(targets);
30
+ state.lastStep = 'clean';
31
+ },
32
+ },
33
+ {
34
+ name: 'rebuild',
35
+ emoji: '🔁',
36
+ description: 'Clean then build',
37
+ handler: async () => {
38
+ await rebuildPackages(targets);
39
+ state.lastStep = 'rebuild';
40
+ },
41
+ },
23
42
  {
24
43
  name: 'test',
25
44
  emoji: '🧪',
@@ -93,25 +112,32 @@ function makeManagerStepEntries(targets, packages, state, options) {
93
112
  : []),
94
113
  ];
95
114
  }
96
- function makePackageScriptEntries(pkg) {
97
- const scripts = pkg.json?.scripts ?? {};
98
- const entries = [];
115
+ function makeDockerEntry(pkg) {
99
116
  if (pkg.dockerfilePath) {
100
117
  const dockerLabel = path
101
118
  .relative(process.cwd(), pkg.dockerfilePath)
102
119
  .replace(/\\/g, '/');
103
- entries.push({
120
+ return {
104
121
  name: `Dockerfile (${dockerLabel})`,
105
122
  emoji: '🐳',
106
123
  description: 'manager -> opens the docker cli',
107
124
  handler: async () => {
108
125
  await openDockerHelper(pkg);
109
126
  },
110
- });
127
+ };
111
128
  }
129
+ return undefined;
130
+ }
131
+ function makePackageScriptEntries(pkg) {
132
+ const scripts = pkg.json?.scripts ?? {};
133
+ const baseEntries = makeBaseScriptEntries(pkg);
134
+ const baseNames = new Set(baseEntries.map((entry) => entry.name));
135
+ const entries = [];
112
136
  Object.entries(scripts)
113
137
  .sort(([a], [b]) => a.localeCompare(b))
114
138
  .forEach(([name, command]) => {
139
+ if (baseNames.has(name))
140
+ return;
115
141
  entries.push({
116
142
  name,
117
143
  emoji: '▶️',
@@ -121,23 +147,31 @@ function makePackageScriptEntries(pkg) {
121
147
  },
122
148
  });
123
149
  });
124
- return entries;
150
+ return [...baseEntries, ...entries];
125
151
  }
126
152
  export function buildPackageSelectionMenu(packages, onStepComplete) {
127
153
  const ordered = getOrderedPackages(packages);
128
- const entries = ordered.map((pkg) => ({
129
- name: pkg.name ?? pkg.substitute ?? pkg.dirName,
130
- emoji: colors[pkg.color](''),
131
- description: pkg.relativeDir ?? pkg.dirName,
132
- handler: async () => {
133
- const step = await runStepLoop([pkg], packages);
134
- onStepComplete?.(step);
135
- },
136
- }));
154
+ const entries = ordered.map((pkg) => {
155
+ const marker = getPackageMarker(pkg);
156
+ const pkgColor = pkg.color ?? 'cyan';
157
+ const descriptionMeta = pkg.relativeDir ?? pkg.dirName;
158
+ const markerHint = marker.label ? ` · ${marker.label}` : '';
159
+ return {
160
+ name: pkg.name ?? pkg.substitute ?? pkg.dirName,
161
+ emoji: marker.colorize('●'),
162
+ description: `${descriptionMeta}${colors.dim(markerHint)}`,
163
+ color: pkgColor,
164
+ handler: async () => {
165
+ const step = await runStepLoop([pkg], packages);
166
+ onStepComplete?.(step);
167
+ },
168
+ };
169
+ });
137
170
  if (ordered.length === 0)
138
171
  return entries;
139
172
  entries.push({
140
173
  name: 'All packages',
174
+ color: 'gray',
141
175
  emoji: globalEmoji,
142
176
  description: 'Select all packages',
143
177
  handler: async () => {
@@ -154,10 +188,12 @@ export async function runStepLoop(targets, packages) {
154
188
  // Single package: show combined menu (manager actions + package.json scripts)
155
189
  if (targets.length === 1) {
156
190
  const pkg = targets[0];
191
+ const dockerEntry = makeDockerEntry(pkg);
157
192
  // eslint-disable-next-line no-constant-condition
158
193
  while (true) {
159
194
  const scriptEntries = makePackageScriptEntries(pkg);
160
195
  const entries = [
196
+ ...(dockerEntry ? [dockerEntry] : []),
161
197
  {
162
198
  name: 'manager actions',
163
199
  emoji: globalEmoji,
@@ -170,9 +206,8 @@ export async function runStepLoop(targets, packages) {
170
206
  {
171
207
  name: 'back',
172
208
  emoji: '↩️',
173
- description: 'Return to package scripts',
209
+ description: 'Return to package menu',
174
210
  handler: () => {
175
- // no state change; just exit to the package scripts menu
176
211
  state.lastStep = undefined;
177
212
  },
178
213
  },
@@ -184,7 +219,31 @@ export async function runStepLoop(targets, packages) {
184
219
  });
185
220
  },
186
221
  },
187
- ...scriptEntries,
222
+ {
223
+ name: 'package scripts',
224
+ emoji: '📜',
225
+ description: 'Run package.json scripts',
226
+ handler: async () => {
227
+ if (scriptEntries.length === 0) {
228
+ console.log(colors.yellow(`No package.json scripts found for ${pkg.name}.`));
229
+ return;
230
+ }
231
+ const scriptsMenu = [
232
+ ...scriptEntries,
233
+ {
234
+ name: 'back',
235
+ emoji: '↩️',
236
+ description: 'Return to package menu',
237
+ handler: () => { },
238
+ },
239
+ ];
240
+ await runHelperCli({
241
+ title: `${pkg.name} scripts`,
242
+ scripts: scriptsMenu,
243
+ argv: [],
244
+ });
245
+ },
246
+ },
188
247
  {
189
248
  name: 'back',
190
249
  emoji: '↩️',
package/dist/packages.js CHANGED
@@ -1,161 +1,17 @@
1
1
  // src/packages.js
2
2
  import path from 'node:path';
3
- import { pathToFileURL } from 'node:url';
4
- import { readdir, readFile, stat } from 'node:fs/promises';
3
+ import { readFile, stat } from 'node:fs/promises';
4
+ import { colorFromSeed, deriveSubstitute, inferManifestFromWorkspace, loadWorkspaceManifest, mergeManifestEntries, normalizeManifestPath, } from './packages/manifest-utils.js';
5
5
  const rootDir = process.cwd();
6
- const ignoredDirs = new Set([
7
- 'node_modules',
8
- '.git',
9
- '.turbo',
10
- '.next',
11
- 'dist',
12
- 'build',
13
- '.cache',
14
- 'coverage',
15
- ]);
16
- const colorPalette = ['cyan', 'green', 'yellow', 'magenta', 'red'];
17
6
  let manifestState;
18
- function manifestFilePath() {
19
- return path.join(rootDir, 'scripts', 'packages.mjs');
20
- }
21
- function isManifestMissing(error) {
22
- if (typeof error !== 'object' || error === null)
23
- return false;
24
- const code = error.code;
25
- return code === 'ERR_MODULE_NOT_FOUND' || code === 'ENOENT';
26
- }
27
- function normalizeManifestPath(value) {
28
- const absolute = path.resolve(rootDir, value || '');
29
- let relative = path.relative(rootDir, absolute);
30
- if (!relative)
31
- return '.';
32
- relative = relative.replace(/\\/g, '/');
33
- return relative.replace(/^(?:\.\/)+/, '');
34
- }
35
- function colorFromSeed(seed) {
36
- const normalized = `${seed}`.trim() || 'package';
37
- let hash = 0;
38
- for (let i = 0; i < normalized.length; i++) {
39
- hash = (hash * 31 + normalized.charCodeAt(i)) >>> 0;
40
- }
41
- return colorPalette[hash % colorPalette.length];
42
- }
43
- function deriveSubstitute(name) {
44
- const trimmed = (name || '').trim();
45
- if (!trimmed)
46
- return '';
47
- const segments = trimmed.split(/[@\/\-]/).filter(Boolean);
48
- const transformed = segments
49
- .map((segment) => segment)
50
- .filter(Boolean)
51
- .join(' ');
52
- return transformed || trimmed;
53
- }
54
- async function findPackageJsonFiles(baseDir) {
55
- const results = new Set();
56
- const queue = [baseDir];
57
- while (queue.length) {
58
- const current = queue.shift();
59
- let entries;
60
- try {
61
- entries = await readdir(current, { withFileTypes: true });
62
- }
63
- catch {
64
- continue;
65
- }
66
- for (const entry of entries) {
67
- if (entry.isFile() && entry.name === 'package.json') {
68
- results.add(path.join(current, entry.name));
69
- }
70
- }
71
- for (const entry of entries) {
72
- if (!entry.isDirectory())
73
- continue;
74
- if (entry.isSymbolicLink())
75
- continue;
76
- if (ignoredDirs.has(entry.name))
77
- continue;
78
- queue.push(path.join(current, entry.name));
79
- }
80
- }
81
- return [...results].sort();
82
- }
83
- async function loadWorkspaceManifest() {
84
- const manifestPath = manifestFilePath();
85
- try {
86
- const manifestModule = await import(pathToFileURL(manifestPath).href);
87
- if (Array.isArray(manifestModule?.PACKAGE_MANIFEST)) {
88
- return manifestModule.PACKAGE_MANIFEST;
89
- }
90
- }
91
- catch (error) {
92
- if (isManifestMissing(error))
93
- return undefined;
94
- throw error;
95
- }
96
- return undefined;
97
- }
98
- async function inferManifestFromWorkspace() {
99
- const manifest = [];
100
- const pkgJsonPaths = await findPackageJsonFiles(rootDir);
101
- for (const pkgJsonPath of pkgJsonPaths) {
102
- try {
103
- const raw = await readFile(pkgJsonPath, 'utf8');
104
- const json = JSON.parse(raw);
105
- const pkgDir = path.dirname(pkgJsonPath);
106
- const pkgName = json.name?.trim() || path.basename(pkgDir) || 'package';
107
- manifest.push({
108
- name: pkgName,
109
- path: normalizeManifestPath(path.relative(rootDir, pkgDir)),
110
- color: colorFromSeed(pkgName),
111
- substitute: deriveSubstitute(pkgName),
112
- });
113
- }
114
- catch {
115
- continue;
116
- }
117
- }
118
- return manifest;
119
- }
120
- function mergeManifestEntries(inferred, overrides) {
121
- const normalizedOverrides = new Map();
122
- overrides?.forEach((entry) => {
123
- const normalized = normalizeManifestPath(entry.path);
124
- if (!normalized)
125
- return;
126
- normalizedOverrides.set(normalized, { ...entry, path: normalized });
127
- });
128
- const merged = [];
129
- for (const baseEntry of inferred) {
130
- const normalized = normalizeManifestPath(baseEntry.path);
131
- const override = normalizedOverrides.get(normalized);
132
- if (override) {
133
- normalizedOverrides.delete(normalized);
134
- const name = override.name || baseEntry.name;
135
- const color = override.color ?? baseEntry.color ?? colorFromSeed(name);
136
- const substitute = override.substitute ?? baseEntry.substitute ?? deriveSubstitute(name) ?? name;
137
- merged.push({ name, path: normalized, color, substitute });
138
- }
139
- else {
140
- merged.push({ ...baseEntry, path: normalized });
141
- }
142
- }
143
- normalizedOverrides.forEach((entry) => {
144
- const name = entry.name || path.basename(entry.path) || 'package';
145
- const color = entry.color ?? colorFromSeed(name);
146
- const substitute = entry.substitute ?? deriveSubstitute(name) ?? name;
147
- merged.push({ name, path: entry.path, color, substitute });
148
- });
149
- return merged;
150
- }
151
7
  async function ensureManifestState(forceReload = false) {
152
8
  if (manifestState && !forceReload)
153
9
  return manifestState;
154
10
  const [workspaceManifest, inferred] = await Promise.all([
155
- loadWorkspaceManifest(),
156
- inferManifestFromWorkspace(),
11
+ loadWorkspaceManifest(rootDir),
12
+ inferManifestFromWorkspace(rootDir),
157
13
  ]);
158
- const entries = mergeManifestEntries(inferred, workspaceManifest);
14
+ const entries = mergeManifestEntries(rootDir, inferred, workspaceManifest);
159
15
  const byName = new Map(entries.map((pkg) => [pkg.name.toLowerCase(), pkg]));
160
16
  const byPath = new Map(entries.map((pkg) => [pkg.path.toLowerCase(), pkg]));
161
17
  manifestState = {
@@ -180,7 +36,7 @@ export async function loadPackages() {
180
36
  const meta = byPath.get(relativePath.toLowerCase()) ??
181
37
  byName.get((pkgName ?? '').toLowerCase());
182
38
  const substitute = meta?.substitute ?? deriveSubstitute(pkgName) ?? path.basename(pkgDir);
183
- const color = meta?.color ?? colorFromSeed(pkgName);
39
+ const color = (meta?.color ?? colorFromSeed(pkgName));
184
40
  let dockerfilePath;
185
41
  try {
186
42
  const candidate = path.join(pkgDir, 'Dockerfile');
@@ -1,11 +1,2 @@
1
1
  // src/utils/colors.js
2
- const ansi = (code) => (text) => `\x1b[${code}m${text}\x1b[0m`;
3
- export const colors = {
4
- cyan: ansi(36),
5
- green: ansi(32),
6
- yellow: ansi(33),
7
- magenta: ansi(35),
8
- red: ansi(31),
9
- bold: ansi(1),
10
- dim: ansi(2),
11
- };
2
+ export { colors } from '../colors-shared.js';
package/dist/utils/run.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/utils/run.js
2
2
  import { spawn } from 'node:child_process';
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- const __filename = fileURLToPath(import.meta.url);
6
- export const rootDir = path.resolve(path.dirname(__filename), '..', '..');
3
+ // Always execute commands from the workspace where the CLI is invoked.
4
+ // This makes installs from node_modules behave correctly instead of
5
+ // running in the package's own directory.
6
+ export const rootDir = process.cwd();
7
7
  export function run(command, args, options = {}) {
8
8
  return new Promise((resolve, reject) => {
9
9
  const child = spawn(command, args, {
package/dist/workspace.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // src/workspace.js
2
2
  import { spawnSync } from 'node:child_process';
3
+ import { rm } from 'node:fs/promises';
4
+ import path from 'node:path';
3
5
  import { run, rootDir } from './utils/run.js';
4
6
  import { logGlobal, logPkg, colors } from './utils/log.js';
5
7
  import { collectGitStatus, gitAdd, gitCommit } from './git.js';
@@ -191,12 +193,83 @@ function packageFilterArg(pkg) {
191
193
  return '.';
192
194
  return `./${pkg.relativeDir}`;
193
195
  }
196
+ async function runLifecycleScenario(pkg, scenario) {
197
+ if (pkg) {
198
+ logPkg(pkg, scenario.singleMessage);
199
+ await run('pnpm', scenario.singleArgs(pkg));
200
+ return;
201
+ }
202
+ logGlobal(scenario.allMessage, colors.cyan);
203
+ await run('pnpm', scenario.allArgs);
204
+ }
205
+ const TYPECHECK_SCENARIO = {
206
+ allArgs: ['typecheck'],
207
+ allMessage: 'Running typecheck for all packages…',
208
+ singleArgs: (pkg) => ['run', '--filter', packageFilterArg(pkg), 'typecheck'],
209
+ singleMessage: 'Running typecheck…',
210
+ };
211
+ const BUILD_SCENARIO = {
212
+ allArgs: ['build'],
213
+ allMessage: 'Running build for all packages…',
214
+ singleArgs: (pkg) => ['run', '--filter', packageFilterArg(pkg), 'build'],
215
+ singleMessage: 'Running build…',
216
+ };
217
+ const TEST_SCENARIO = {
218
+ allArgs: ['test'],
219
+ allMessage: 'Running tests for all packages…',
220
+ singleArgs: (pkg) => [
221
+ 'test',
222
+ '--',
223
+ pkg.relativeDir === '.' ? '.' : pkg.relativeDir,
224
+ ],
225
+ singleMessage: 'Running tests…',
226
+ };
194
227
  export async function runCleanInstall() {
195
228
  logGlobal('Cleaning workspace…', colors.cyan);
196
229
  await run('pnpm', ['run', 'clean']);
197
230
  logGlobal('Reinstalling dependencies…', colors.cyan);
198
231
  await run('pnpm', ['install']);
199
232
  }
233
+ const CLEAN_TARGETS = [
234
+ 'node_modules',
235
+ 'dist',
236
+ 'build',
237
+ '.turbo',
238
+ '.next',
239
+ '.expo',
240
+ '.parcel-cache',
241
+ '.cache',
242
+ 'coverage',
243
+ 'tmp',
244
+ '.tmp',
245
+ ];
246
+ async function removePath(target) {
247
+ try {
248
+ await rm(target, { recursive: true, force: true });
249
+ return true;
250
+ }
251
+ catch (error) {
252
+ console.warn(colors.yellow(` failed to remove ${target}: ${String(error)}`));
253
+ return false;
254
+ }
255
+ }
256
+ export async function cleanPackages(targets) {
257
+ for (const pkg of targets) {
258
+ logPkg(pkg, 'Cleaning build artifacts and caches…');
259
+ const results = await Promise.all(CLEAN_TARGETS.map(async (name) => {
260
+ const target = path.join(pkg.path, name);
261
+ const removed = await removePath(target);
262
+ return { name, removed };
263
+ }));
264
+ const removed = results.filter((r) => r.removed).map((r) => r.name);
265
+ if (removed.length === 0) {
266
+ console.log(colors.dim(' nothing to clean'));
267
+ }
268
+ else {
269
+ console.log(colors.dim(` removed ${removed.join(', ')}`));
270
+ }
271
+ }
272
+ }
200
273
  export async function updateDependencies(targets) {
201
274
  const preStatus = await collectGitStatus();
202
275
  if (targets.length === 1) {
@@ -231,32 +304,33 @@ export async function updateDependencies(targets) {
231
304
  logGlobal('Push complete.', colors.green);
232
305
  }
233
306
  export async function typecheckAll() {
234
- logGlobal('Running typecheck for all packages…', colors.cyan);
235
- await run('pnpm', ['typecheck']);
307
+ await runLifecycleScenario(undefined, TYPECHECK_SCENARIO);
236
308
  }
237
309
  export async function typecheckSingle(pkg) {
238
- const filterArg = packageFilterArg(pkg);
239
- logPkg(pkg, `Running typecheck…`);
240
- await run('pnpm', ['run', '--filter', filterArg, 'typecheck']);
310
+ await runLifecycleScenario(pkg, TYPECHECK_SCENARIO);
241
311
  }
242
312
  export async function buildAll() {
243
- logGlobal('Running build for all packages…', colors.cyan);
244
- await run('pnpm', ['build']);
313
+ await runLifecycleScenario(undefined, BUILD_SCENARIO);
245
314
  }
246
315
  export async function buildSingle(pkg) {
247
- const filterArg = packageFilterArg(pkg);
248
- logPkg(pkg, `Running build…`);
249
- await run('pnpm', ['run', '--filter', filterArg, 'build']);
316
+ await runLifecycleScenario(pkg, BUILD_SCENARIO);
317
+ }
318
+ export async function rebuildPackages(targets) {
319
+ await cleanPackages(targets);
320
+ if (targets.length === 1) {
321
+ await buildSingle(targets[0]);
322
+ }
323
+ else {
324
+ await buildAll();
325
+ }
250
326
  }
251
327
  export async function buildPackageLocally(pkg) {
252
328
  logPkg(pkg, 'Building local dist before publish…');
253
329
  await run('pnpm', ['run', 'build'], { cwd: pkg.path });
254
330
  }
255
331
  export async function testAll() {
256
- logGlobal('Running tests for all packages…', colors.cyan);
257
- await run('pnpm', ['test']);
332
+ await runLifecycleScenario(undefined, TEST_SCENARIO);
258
333
  }
259
334
  export async function testSingle(pkg) {
260
- logPkg(pkg, `Running tests…`);
261
- await run('pnpm', ['test', '--', pkg.relativeDir === '.' ? '.' : pkg.relativeDir]);
335
+ await runLifecycleScenario(pkg, TEST_SCENARIO);
262
336
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",