@emeryld/manager 0.6.4 → 0.6.6

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.
@@ -0,0 +1,11 @@
1
+ const ansi = (code) => (text) => `\x1b[${code}m${text}\x1b[0m`;
2
+ export const colors = {
3
+ cyan: ansi(36),
4
+ green: ansi(32),
5
+ yellow: ansi(33),
6
+ magenta: ansi(35),
7
+ red: ansi(31),
8
+ bold: ansi(1),
9
+ dim: ansi(2),
10
+ gray: ansi(90),
11
+ };
@@ -45,5 +45,5 @@ export function printPaginatedScriptList(state, title) {
45
45
  const display = option.enabled ? label : colors.dim(label);
46
46
  console.log(` ${numberLabel} ${icon} ${display}`);
47
47
  });
48
- console.log(colors.dim(`Enter 1-${PAGE_SIZE} to run, ${PREVIOUS_KEY} for previous page, ${NEXT_KEY} for next page, ${BACK_KEY} to go back, or type a name.`));
48
+ console.log(colors.dim(`Enter 1-${PAGE_SIZE} to run, ${PREVIOUS_KEY} for previous page (if shown), ${NEXT_KEY} for next page (if shown), ${BACK_KEY} to go back, or type a name.`));
49
49
  }
@@ -15,18 +15,22 @@ export function buildVisibleOptions(entries, page) {
15
15
  }));
16
16
  const hasPrevious = safePage > 0;
17
17
  const hasNext = safePage < pageCount - 1;
18
- options.push({
19
- type: 'nav',
20
- action: 'previous',
21
- hotkey: PREVIOUS_KEY,
22
- enabled: hasPrevious,
23
- });
24
- options.push({
25
- type: 'nav',
26
- action: 'next',
27
- hotkey: NEXT_KEY,
28
- enabled: hasNext,
29
- });
18
+ if (hasPrevious) {
19
+ options.push({
20
+ type: 'nav',
21
+ action: 'previous',
22
+ hotkey: PREVIOUS_KEY,
23
+ enabled: true,
24
+ });
25
+ }
26
+ if (hasNext) {
27
+ options.push({
28
+ type: 'nav',
29
+ action: 'next',
30
+ hotkey: NEXT_KEY,
31
+ enabled: true,
32
+ });
33
+ }
30
34
  options.push({
31
35
  type: 'nav',
32
36
  action: 'back',
@@ -86,7 +86,7 @@ function formatInteractiveLines(state, selectedIndex, title) {
86
86
  lines.push(`${pointer}${numberLabel}. ${icon} ${navLabel}`);
87
87
  });
88
88
  lines.push('');
89
- lines.push(colors.dim(`Use ↑/↓ (or j/k) to move, 1-${PAGE_SIZE} to run, ${PREVIOUS_KEY} prev page, ${NEXT_KEY} next page, ${BACK_KEY} back, Enter to confirm, Esc/Ctrl+C to exit.`));
89
+ lines.push(colors.dim(`Use ↑/↓ (or j/k) to move, 1-${PAGE_SIZE} to run, ${PREVIOUS_KEY} prev page (when shown), ${NEXT_KEY} next page (when shown), ${BACK_KEY} back, Enter to confirm, Esc/Ctrl+C to exit.`));
90
90
  return lines;
91
91
  }
92
92
  function renderInteractiveList(lines, previousLineCount) {
@@ -1,5 +1,48 @@
1
1
  import { colors } from "../utils/log.js";
2
2
  import { run } from "../utils/run.js";
3
+ const FRONTEND_INDICATORS = [
4
+ { dep: 'next', label: 'Next' },
5
+ { dep: 'remix', label: 'Remix' },
6
+ { dep: '@remix-run/node', label: 'Remix' },
7
+ { dep: 'expo', label: 'Expo' },
8
+ { dep: 'react-native', label: 'React Native' },
9
+ { dep: 'react', label: 'React' },
10
+ { dep: 'vite', label: 'Vite' },
11
+ { dep: '@vitejs/plugin-react', label: 'Vite' },
12
+ { dep: 'svelte', label: 'Svelte' },
13
+ { dep: '@sveltejs/vite-plugin-svelte', label: 'SvelteKit' },
14
+ { dep: '@angular/core', label: 'Angular' },
15
+ { dep: 'vue', label: 'Vue' },
16
+ ];
17
+ const BACKEND_INDICATORS = [
18
+ { dep: 'express', label: 'Express' },
19
+ { dep: 'fastify', label: 'Fastify' },
20
+ { dep: 'koa', label: 'Koa' },
21
+ { dep: 'hono', label: 'Hono' },
22
+ { dep: '@nestjs/core', label: 'NestJS' },
23
+ { dep: '@trpc/server', label: 'tRPC' },
24
+ ];
25
+ const CLI_INDICATORS = [
26
+ { dep: '@oclif/core', label: 'OClif' },
27
+ { dep: '@oclif/command', label: 'OClif' },
28
+ { dep: 'oclif', label: 'OClif' },
29
+ { dep: 'commander', label: 'Commander' },
30
+ { dep: 'yargs', label: 'Yargs' },
31
+ { dep: 'cac', label: 'CAC' },
32
+ { dep: 'clipanion', label: 'Clipanion' },
33
+ { dep: 'caporal', label: 'Caporal' },
34
+ { dep: 'ink', label: 'Ink' },
35
+ { dep: 'meow', label: 'Meow' },
36
+ { dep: 'enquirer', label: 'Enquirer' },
37
+ { dep: 'zx', label: 'ZX' },
38
+ ];
39
+ const CLI_KEYWORDS = new Set(['cli', 'command-line', 'commandline', 'tooling']);
40
+ const KIND_COLOR_MAP = {
41
+ frontend: colors.magenta,
42
+ backend: colors.green,
43
+ library: colors.cyan,
44
+ cli: colors.red,
45
+ };
3
46
  export function collectDependencies(pkg) {
4
47
  return new Set([
5
48
  ...Object.keys(pkg.json?.dependencies ?? {}),
@@ -37,6 +80,7 @@ function resolveFormatScript(pkg, scripts, dependencies) {
37
80
  description: commandDescription(args),
38
81
  args,
39
82
  available: true,
83
+ isDefault: true,
40
84
  };
41
85
  }
42
86
  return {
@@ -56,6 +100,7 @@ function resolveTypecheckScript(pkg, scripts, dependencies) {
56
100
  description: commandDescription(args),
57
101
  args,
58
102
  available: true,
103
+ isDefault: true,
59
104
  };
60
105
  }
61
106
  return {
@@ -75,6 +120,7 @@ function resolveLintScript(pkg, scripts, dependencies) {
75
120
  description: commandDescription(args),
76
121
  args,
77
122
  available: true,
123
+ isDefault: true,
78
124
  };
79
125
  }
80
126
  return {
@@ -86,7 +132,7 @@ function resolveLintScript(pkg, scripts, dependencies) {
86
132
  function resolveDevScript(pkg, scripts, dependencies, kind) {
87
133
  const devCandidates = kind === 'frontend'
88
134
  ? ['dev', 'start', 'serve']
89
- : kind === 'backend'
135
+ : kind === 'backend' || kind === 'cli'
90
136
  ? ['dev', 'start', 'watch', 'serve', 'start:dev']
91
137
  : ['dev', 'start'];
92
138
  const direct = resolveRunScript(scripts, devCandidates);
@@ -99,6 +145,7 @@ function resolveDevScript(pkg, scripts, dependencies, kind) {
99
145
  description: commandDescription(args),
100
146
  args,
101
147
  available: true,
148
+ isDefault: true,
102
149
  };
103
150
  }
104
151
  if (dependencies.has('vite')) {
@@ -108,6 +155,7 @@ function resolveDevScript(pkg, scripts, dependencies, kind) {
108
155
  description: commandDescription(args),
109
156
  args,
110
157
  available: true,
158
+ isDefault: true,
111
159
  };
112
160
  }
113
161
  if (dependencies.has('expo')) {
@@ -117,6 +165,7 @@ function resolveDevScript(pkg, scripts, dependencies, kind) {
117
165
  description: commandDescription(args),
118
166
  args,
119
167
  available: true,
168
+ isDefault: true,
120
169
  };
121
170
  }
122
171
  return {
@@ -145,7 +194,9 @@ export function makeBaseScriptEntries(pkg) {
145
194
  return BASE_SCRIPT_KEYS.map((key) => {
146
195
  const resolution = resolveBaseScript(pkg, key, scripts, dependencies, marker.kind);
147
196
  const description = resolution.available
148
- ? resolution.description
197
+ ? resolution.isDefault
198
+ ? `${resolution.description} (default)`
199
+ : resolution.description
149
200
  : `No ${key} command detected`;
150
201
  return {
151
202
  name: key,
@@ -162,17 +213,73 @@ export function makeBaseScriptEntries(pkg) {
162
213
  });
163
214
  }
164
215
  export function getPackageMarker(pkg, dependencies = collectDependencies(pkg)) {
165
- const hasFrontend = ['react', 'next', 'expo', 'react-native', 'vite'].some((dep) => dependencies.has(dep));
166
- const hasBackend = ['express', 'fastify', 'hono', 'koa', '@nestjs/core'].some((dep) => dependencies.has(dep));
216
+ const normalizedDeps = new Set([...dependencies].map((dep) => String(dep).toLowerCase().trim()));
217
+ function findIndicator(indicators) {
218
+ for (const indicator of indicators) {
219
+ if (normalizedDeps.has(indicator.dep.toLowerCase())) {
220
+ return indicator;
221
+ }
222
+ }
223
+ return undefined;
224
+ }
167
225
  const pkgType = pkg.json?.type?.toLowerCase();
168
- if (hasFrontend) {
169
- return { label: 'frontend', colorize: colors.magenta, kind: 'frontend' };
226
+ const keywords = (() => {
227
+ const raw = pkg.json?.keywords;
228
+ if (typeof raw === 'string') {
229
+ return raw
230
+ .split(/\s*,\s*/)
231
+ .map((keyword) => keyword.trim())
232
+ .filter(Boolean);
233
+ }
234
+ if (Array.isArray(raw)) {
235
+ return raw
236
+ .map((keyword) => String(keyword).trim())
237
+ .filter(Boolean);
238
+ }
239
+ return [];
240
+ })();
241
+ const hasCliKeyword = keywords
242
+ .map((keyword) => keyword.toLowerCase())
243
+ .some((keyword) => CLI_KEYWORDS.has(keyword));
244
+ const hasBinField = !!pkg.json?.bin &&
245
+ (typeof pkg.json?.bin === 'string' ||
246
+ typeof pkg.json?.bin === 'object' ||
247
+ Array.isArray(pkg.json?.bin));
248
+ const frontendIndicator = findIndicator(FRONTEND_INDICATORS);
249
+ if (frontendIndicator) {
250
+ return {
251
+ label: frontendIndicator.label ?? frontendIndicator.dep,
252
+ colorize: KIND_COLOR_MAP.frontend,
253
+ kind: 'frontend',
254
+ kindColorize: KIND_COLOR_MAP.frontend,
255
+ };
170
256
  }
171
- if (hasBackend) {
172
- return { label: 'backend', colorize: colors.green, kind: 'backend' };
257
+ const backendIndicator = findIndicator(BACKEND_INDICATORS);
258
+ if (backendIndicator) {
259
+ return {
260
+ label: backendIndicator.label ?? backendIndicator.dep,
261
+ colorize: KIND_COLOR_MAP.backend,
262
+ kind: 'backend',
263
+ kindColorize: KIND_COLOR_MAP.backend,
264
+ };
173
265
  }
174
- if (pkgType === 'module') {
175
- return { label: 'esm', colorize: colors.yellow, kind: 'library' };
266
+ const cliIndicator = findIndicator(CLI_INDICATORS);
267
+ if (cliIndicator || hasCliKeyword || hasBinField) {
268
+ const label = cliIndicator?.label ?? 'CLI';
269
+ return {
270
+ label,
271
+ colorize: KIND_COLOR_MAP.cli,
272
+ kind: 'cli',
273
+ kindColorize: KIND_COLOR_MAP.cli,
274
+ };
176
275
  }
177
- return { label: 'node', colorize: colors.cyan, kind: 'library' };
276
+ const isModule = pkgType === 'module';
277
+ const label = isModule ? 'ESM' : 'Node';
278
+ const labelColorizer = isModule ? colors.yellow : colors.cyan;
279
+ return {
280
+ label,
281
+ colorize: labelColorizer,
282
+ kind: 'library',
283
+ kindColorize: KIND_COLOR_MAP.library,
284
+ };
178
285
  }
package/dist/menu.js CHANGED
@@ -9,6 +9,11 @@ import { ensureWorkingTreeCommitted } from './preflight.js';
9
9
  import { openDockerHelper } from './docker.js';
10
10
  import { run } from './utils/run.js';
11
11
  import { makeBaseScriptEntries, getPackageMarker } from './menu/script-helpers.js';
12
+ function formatKindLabel(kind) {
13
+ if (kind === 'cli')
14
+ return 'CLI';
15
+ return `${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
16
+ }
12
17
  function makeManagerStepEntries(targets, packages, state, options) {
13
18
  const includeBack = options?.includeBack ?? true;
14
19
  return [
@@ -154,12 +159,15 @@ export function buildPackageSelectionMenu(packages, onStepComplete) {
154
159
  const entries = ordered.map((pkg) => {
155
160
  const marker = getPackageMarker(pkg);
156
161
  const pkgColor = pkg.color ?? 'cyan';
157
- const descriptionMeta = pkg.relativeDir ?? pkg.dirName;
158
- const markerHint = marker.label ? ` · ${marker.label}` : '';
162
+ const descriptionMeta = colors.dim(pkg.relativeDir ?? pkg.dirName);
163
+ const labelBadge = marker.label ? marker.colorize(marker.label) : '';
164
+ const kindLabel = formatKindLabel(marker.kind);
165
+ const kindBadge = marker.kindColorize(kindLabel);
166
+ const badgeMeta = [labelBadge, kindBadge].filter(Boolean).join(' ');
159
167
  return {
160
168
  name: pkg.name ?? pkg.substitute ?? pkg.dirName,
161
169
  emoji: marker.colorize('●'),
162
- description: `${descriptionMeta}${colors.dim(markerHint)}`,
170
+ description: [descriptionMeta, badgeMeta].filter(Boolean).join(' '),
163
171
  color: pkgColor,
164
172
  handler: async () => {
165
173
  const step = await runStepLoop([pkg], packages);
@@ -0,0 +1,145 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ const DEFAULT_COLOR_PALETTE = ['cyan', 'green', 'yellow', 'magenta', 'red'];
5
+ const IGNORED_DIRS = new Set([
6
+ 'node_modules',
7
+ '.git',
8
+ '.turbo',
9
+ '.next',
10
+ 'dist',
11
+ 'build',
12
+ '.cache',
13
+ 'coverage',
14
+ ]);
15
+ export function manifestFilePath(rootDir) {
16
+ return path.join(rootDir, 'scripts', 'packages.mjs');
17
+ }
18
+ export function normalizeManifestPath(rootDir, value) {
19
+ const absolute = path.resolve(rootDir, value || '');
20
+ let relative = path.relative(rootDir, absolute);
21
+ if (!relative)
22
+ return '.';
23
+ relative = relative.replace(/\\/g, '/');
24
+ return relative.replace(/^(?:\.\/)+/, '');
25
+ }
26
+ export async function findPackageJsonFiles(rootDir) {
27
+ const results = new Set();
28
+ const queue = [rootDir];
29
+ while (queue.length) {
30
+ const current = queue.shift();
31
+ let entries;
32
+ try {
33
+ entries = await readdir(current, { withFileTypes: true });
34
+ }
35
+ catch {
36
+ continue;
37
+ }
38
+ for (const entry of entries) {
39
+ if (entry.isFile() && entry.name === 'package.json') {
40
+ results.add(path.join(current, entry.name));
41
+ }
42
+ }
43
+ for (const entry of entries) {
44
+ if (!entry.isDirectory())
45
+ continue;
46
+ if (entry.isSymbolicLink())
47
+ continue;
48
+ if (IGNORED_DIRS.has(entry.name))
49
+ continue;
50
+ queue.push(path.join(current, entry.name));
51
+ }
52
+ }
53
+ return [...results].sort();
54
+ }
55
+ export async function loadWorkspaceManifest(rootDir) {
56
+ const manifestPath = manifestFilePath(rootDir);
57
+ try {
58
+ const manifestModule = await import(pathToFileURL(manifestPath).href);
59
+ if (Array.isArray(manifestModule?.PACKAGE_MANIFEST)) {
60
+ return manifestModule.PACKAGE_MANIFEST;
61
+ }
62
+ }
63
+ catch (error) {
64
+ if (isManifestMissing(error))
65
+ return undefined;
66
+ throw error;
67
+ }
68
+ return undefined;
69
+ }
70
+ export async function inferManifestFromWorkspace(rootDir) {
71
+ const manifest = [];
72
+ const pkgJsonPaths = await findPackageJsonFiles(rootDir);
73
+ for (const pkgJsonPath of pkgJsonPaths) {
74
+ try {
75
+ const raw = await readFile(pkgJsonPath, 'utf8');
76
+ const json = JSON.parse(raw);
77
+ const pkgDir = path.dirname(pkgJsonPath);
78
+ const pkgName = (json.name || path.basename(pkgDir)).trim() || 'package';
79
+ manifest.push({
80
+ name: pkgName,
81
+ path: normalizeManifestPath(rootDir, path.relative(rootDir, pkgDir)),
82
+ color: colorFromSeed(pkgName),
83
+ });
84
+ }
85
+ catch {
86
+ continue;
87
+ }
88
+ }
89
+ return manifest;
90
+ }
91
+ export function mergeManifestEntries(rootDir, inferred, overrides) {
92
+ const normalizedOverrides = new Map();
93
+ overrides?.forEach((entry) => {
94
+ const normalized = normalizeManifestPath(rootDir, entry.path);
95
+ if (!normalized)
96
+ return;
97
+ normalizedOverrides.set(normalized, { ...entry, path: normalized });
98
+ });
99
+ const merged = [];
100
+ for (const baseEntry of inferred) {
101
+ const normalized = normalizeManifestPath(rootDir, baseEntry.path);
102
+ const override = normalizedOverrides.get(normalized);
103
+ if (override) {
104
+ normalizedOverrides.delete(normalized);
105
+ const name = override.name || baseEntry.name;
106
+ const color = override.color ?? baseEntry.color ?? colorFromSeed(name);
107
+ merged.push({ name, path: normalized, color });
108
+ }
109
+ else {
110
+ merged.push({ ...baseEntry, path: normalized });
111
+ }
112
+ }
113
+ normalizedOverrides.forEach((entry) => {
114
+ const name = entry.name || path.basename(entry.path) || 'package';
115
+ const color = entry.color ?? colorFromSeed(name);
116
+ merged.push({ name, path: entry.path, color });
117
+ });
118
+ return merged;
119
+ }
120
+ export function colorFromSeed(seed) {
121
+ const normalized = `${seed}`.trim() || 'package';
122
+ let hash = 0;
123
+ for (let i = 0; i < normalized.length; i++) {
124
+ hash = (hash * 31 + normalized.charCodeAt(i)) >>> 0;
125
+ }
126
+ return DEFAULT_COLOR_PALETTE[hash % DEFAULT_COLOR_PALETTE.length];
127
+ }
128
+ export function deriveSubstitute(name) {
129
+ const trimmed = (name || '').trim();
130
+ if (!trimmed)
131
+ return '';
132
+ const segments = trimmed.split(/[@\/\-]/).filter(Boolean);
133
+ const transformed = segments
134
+ .map((segment) => segment)
135
+ .filter(Boolean)
136
+ .join(' ');
137
+ return transformed || trimmed;
138
+ }
139
+ function isManifestMissing(error) {
140
+ if (typeof error !== 'object' || error === null)
141
+ return false;
142
+ const code = error.code;
143
+ return code === 'ERR_MODULE_NOT_FOUND' || code === 'ENOENT';
144
+ }
145
+ export { DEFAULT_COLOR_PALETTE };
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { colorFromSeed, inferManifestFromWorkspace, loadWorkspaceManifest, mergeManifestEntries, } from './packages/manifest-utils.js';
6
+ import { colors } from './colors-shared.js';
7
+ const rootDir = process.cwd();
8
+ const registerPackageEntry = (map, pkgPath, entry) => {
9
+ if (!map.has(pkgPath)) {
10
+ map.set(pkgPath, entry);
11
+ }
12
+ };
13
+ async function collectPackageEntries() {
14
+ const records = new Map();
15
+ const [workspaceManifest, inferred] = await Promise.all([
16
+ loadWorkspaceManifest(rootDir),
17
+ inferManifestFromWorkspace(rootDir),
18
+ ]);
19
+ const manifestEntries = mergeManifestEntries(rootDir, inferred, workspaceManifest);
20
+ for (const manifestEntry of manifestEntries) {
21
+ if (!manifestEntry.path)
22
+ continue;
23
+ const pkgPath = path.join(rootDir, manifestEntry.path, 'package.json');
24
+ registerPackageEntry(records, pkgPath, {
25
+ name: manifestEntry.name,
26
+ pkgPath,
27
+ color: manifestEntry.color ?? colorFromSeed(manifestEntry.name),
28
+ });
29
+ }
30
+ return Array.from(records.values());
31
+ }
32
+ async function main() {
33
+ const version = process.argv[2];
34
+ if (!version) {
35
+ console.error('Usage: pnpm sync <version>');
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ const semverPattern = /^\d+\.\d+\.\d+(?:[-+].*)?$/;
40
+ if (!semverPattern.test(version)) {
41
+ console.error(`Invalid version "${version}". Expected semver (e.g. 1.6.0).`);
42
+ process.exitCode = 1;
43
+ return;
44
+ }
45
+ const packageEntries = await collectPackageEntries();
46
+ await Promise.all(packageEntries.map(async (entry) => {
47
+ if (!existsSync(entry.pkgPath)) {
48
+ console.warn(`${colors[entry.color ?? 'cyan']('●')} ${colors.yellow(`Skipping missing package file: ${path.relative(rootDir, entry.pkgPath)}`)}`);
49
+ return;
50
+ }
51
+ const raw = await readFile(entry.pkgPath, 'utf8');
52
+ const json = JSON.parse(raw);
53
+ const previousVersion = json.version ?? 'unknown';
54
+ json.version = version;
55
+ const formatted = `${JSON.stringify(json, null, 2)}\n`;
56
+ await writeFile(entry.pkgPath, formatted);
57
+ console.log(`${colors[entry.color ?? 'cyan']('●')} ${colors.cyan('[sync]')} ${path.relative(rootDir, entry.pkgPath)} ${colors.dim(`${previousVersion} -> ${version}`)}`);
58
+ }));
59
+ }
60
+ main().catch((error) => {
61
+ console.error(error);
62
+ process.exitCode = 1;
63
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -3,6 +3,7 @@
3
3
  "target": "ES2022",
4
4
  "module": "NodeNext",
5
5
  "moduleResolution": "NodeNext",
6
+ "allowJs": true,
6
7
  "rootDir": "src",
7
8
  "outDir": "dist",
8
9
  "lib": ["ES2022"],