@emeryld/manager 1.2.0 → 1.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.
@@ -0,0 +1,158 @@
1
+ import path from 'node:path';
2
+ import { readdir, readFile } from 'node:fs/promises';
3
+ import { IGNORED_DIRECTORIES } from './format-checker/scan/constants.js';
4
+ import { normalizeScripts } from './helper-cli/scripts.js';
5
+ import { promptForScript } from './helper-cli/prompts.js';
6
+ import { rootDir } from './helper-cli/env.js';
7
+ import { loadPackages } from './packages.js';
8
+ const ROOT_ENTRY = {
9
+ label: 'Workspace root',
10
+ relativePath: '.',
11
+ description: 'Root directory',
12
+ color: 'brightBlue',
13
+ };
14
+ export async function promptDirectorySelection(options = {}) {
15
+ const candidates = await buildDirectoryCandidates(options.includeWorkspaceRoot ?? true);
16
+ const entries = normalizeScripts(candidates.map((candidate) => ({
17
+ name: candidate.label,
18
+ emoji: '📁',
19
+ color: candidate.color,
20
+ description: candidate.description ?? candidate.relativePath,
21
+ script: candidate.relativePath,
22
+ })));
23
+ const selection = await promptForScript(entries, options.title ?? 'Select a directory');
24
+ if (!selection)
25
+ return undefined;
26
+ const relativePath = selection.script ?? '.';
27
+ const absolutePath = path.resolve(rootDir, relativePath);
28
+ return { label: selection.displayName, relativePath, absolutePath };
29
+ }
30
+ export function describeDirectorySelection(selection) {
31
+ const normalized = selection.relativePath
32
+ .replace(/\\/g, '/')
33
+ .replace(/^\.\/+/, '');
34
+ if (!normalized || normalized === '.') {
35
+ return selection.label;
36
+ }
37
+ return `${selection.label} (${normalized})`;
38
+ }
39
+ async function buildDirectoryCandidates(includeRoot) {
40
+ const directories = await collectDirectories();
41
+ const packages = await loadPackageMetadata();
42
+ const packageMap = new Map(packages.map((pkg) => [pkg.relativeDir, pkg]));
43
+ const candidates = [];
44
+ const filtered = directories.filter((entry) => includeRoot || entry.relativePath !== '.');
45
+ for (const entry of filtered) {
46
+ const pkg = packageMap.get(entry.relativePath);
47
+ if (pkg) {
48
+ candidates.push({
49
+ label: pkg.name ?? pkg.dirName,
50
+ relativePath: entry.relativePath,
51
+ description: pkg.relativeDir,
52
+ color: pkg.color,
53
+ });
54
+ continue;
55
+ }
56
+ if (entry.relativePath === '.') {
57
+ candidates.push({
58
+ label: ROOT_ENTRY.label,
59
+ relativePath: ROOT_ENTRY.relativePath,
60
+ description: ROOT_ENTRY.description,
61
+ color: ROOT_ENTRY.color,
62
+ });
63
+ continue;
64
+ }
65
+ candidates.push({
66
+ label: path.basename(entry.relativePath) || ROOT_ENTRY.label,
67
+ relativePath: entry.relativePath,
68
+ description: entry.relativePath,
69
+ });
70
+ }
71
+ if (candidates.length === 0 && includeRoot) {
72
+ candidates.push(ROOT_ENTRY);
73
+ }
74
+ return candidates;
75
+ }
76
+ async function collectDirectories() {
77
+ const patterns = await collectIgnorePatterns();
78
+ const directories = [];
79
+ async function walk(current) {
80
+ const relativeRaw = path.relative(rootDir, current);
81
+ const relativePath = normalizeRelativePath(relativeRaw);
82
+ if (current !== rootDir && shouldIgnore(relativePath, patterns)) {
83
+ return;
84
+ }
85
+ directories.push({ absolutePath: current, relativePath });
86
+ let entries;
87
+ try {
88
+ entries = await readdir(current, { withFileTypes: true });
89
+ }
90
+ catch {
91
+ return;
92
+ }
93
+ for (const entry of entries) {
94
+ if (!entry.isDirectory() || entry.isSymbolicLink())
95
+ continue;
96
+ await walk(path.join(current, entry.name));
97
+ }
98
+ }
99
+ await walk(rootDir);
100
+ directories.sort((a, b) => {
101
+ if (a.relativePath === '.')
102
+ return -1;
103
+ if (b.relativePath === '.')
104
+ return 1;
105
+ return a.relativePath.localeCompare(b.relativePath);
106
+ });
107
+ return directories;
108
+ }
109
+ async function collectIgnorePatterns() {
110
+ const patterns = new Set();
111
+ for (const entry of IGNORED_DIRECTORIES) {
112
+ patterns.add(normalizeRelativePath(entry));
113
+ }
114
+ try {
115
+ const contents = await readFile(path.join(rootDir, '.gitignore'), 'utf-8');
116
+ for (const rawLine of contents.split(/\r?\n/)) {
117
+ const trimmed = rawLine.trim();
118
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('!'))
119
+ continue;
120
+ patterns.add(normalizeRelativePath(trimmed));
121
+ }
122
+ }
123
+ catch {
124
+ // ignore failures (missing file)
125
+ }
126
+ return Array.from(patterns).filter(Boolean);
127
+ }
128
+ function normalizeRelativePath(value) {
129
+ const normalized = value.replace(/\\/g, '/').replace(/^\/+/, '');
130
+ const trimmed = normalized.replace(/\/+$/, '');
131
+ return trimmed || '.';
132
+ }
133
+ function shouldIgnore(relativePath, patterns) {
134
+ if (relativePath === '.')
135
+ return false;
136
+ const segments = relativePath.split('/');
137
+ for (const pattern of patterns) {
138
+ if (!pattern)
139
+ continue;
140
+ if (relativePath === pattern)
141
+ return true;
142
+ if (relativePath.startsWith(`${pattern}/`))
143
+ return true;
144
+ const patternSegments = pattern.split('/');
145
+ if (patternSegments.length === 1 && segments.includes(pattern)) {
146
+ return true;
147
+ }
148
+ }
149
+ return false;
150
+ }
151
+ async function loadPackageMetadata() {
152
+ try {
153
+ return await loadPackages();
154
+ }
155
+ catch {
156
+ return [];
157
+ }
158
+ }
@@ -1,6 +1,6 @@
1
1
  import { colors } from '../../utils/log.js';
2
2
  import { stdin as input } from 'node:process';
3
- import { askLine } from '../../prompts.js';
3
+ import { askLine, promptSingleKey } from '../../prompts.js';
4
4
  import { formatValue, parseInteractiveValue, SETTING_DESCRIPTORS, validateLimits, } from './settings.js';
5
5
  import { promptInteractiveSettings, } from '../../cli/interactive-settings.js';
6
6
  const READY_PROMPT = colors.dim('Limits retained. Use the navigation above to adjust and confirm again.');
@@ -20,14 +20,16 @@ export async function promptLimits(defaults) {
20
20
  }
21
21
  async function confirmExecution() {
22
22
  const question = colors.cyan('Run the format checker with these limits? (Y/n): ');
23
- while (true) {
24
- const answer = (await askLine(question)).trim().toLowerCase();
25
- if (!answer || answer === 'y' || answer === 'yes')
23
+ return promptSingleKey(question, (key, raw) => {
24
+ if (!key)
25
+ return undefined;
26
+ if (key === 'y' || key === 'yes')
26
27
  return true;
27
- if (answer === 'n' || answer === 'no')
28
+ if (key === 'n' || key === 'no')
28
29
  return false;
29
- console.log(colors.yellow('Answer "yes" or "no", or press Enter to proceed.'));
30
- }
30
+ console.log(colors.yellow('Answer "yes" or "no".'));
31
+ return undefined;
32
+ });
31
33
  }
32
34
  async function promptLimitsSequential(defaults) {
33
35
  console.log(colors.dim('Enter a number to override a limit or press Enter to keep the default.'));
@@ -1,5 +1,7 @@
1
1
  import { colors } from '../utils/log.js';
2
2
  import { captureConsoleOutput, exportReportLines, promptExportMode, } from '../utils/export.js';
3
+ import { promptDirectorySelection, describeDirectorySelection } from '../directory-picker.js';
4
+ import { runHelperCli } from '../helper-cli.js';
3
5
  import { rootDir } from '../helper-cli/env.js';
4
6
  import { loadFormatLimits, } from './config.js';
5
7
  import { collectSourceFiles, analyzeFiles } from './scan/index.js';
@@ -9,9 +11,54 @@ import { parseScanCliArgs, printScanUsage } from './cli/options.js';
9
11
  export async function runFormatChecker() {
10
12
  console.log(colors.cyan('Gathering defaults from .vscode/settings.json (manager.formatChecker)'));
11
13
  const defaults = await loadFormatLimits();
12
- const limits = await promptLimits(defaults);
13
- const exportMode = await promptExportMode();
14
- await executeFormatCheck(limits, exportMode);
14
+ let limits = defaults;
15
+ let exportMode = 'console';
16
+ while (true) {
17
+ let lastAction;
18
+ const scripts = [
19
+ {
20
+ name: 'Run format checker',
21
+ emoji: '🧮',
22
+ description: `${formatLimitsSummary(limits)} · export=${exportMode}`,
23
+ handler: async () => {
24
+ const selection = await promptDirectorySelection({
25
+ title: 'Select directory for format scan',
26
+ });
27
+ if (!selection)
28
+ return;
29
+ lastAction = 'run';
30
+ await executeFormatCheck(limits, exportMode, selection.absolutePath, describeDirectorySelection(selection));
31
+ },
32
+ },
33
+ {
34
+ name: 'Adjust limits',
35
+ emoji: '⚙️',
36
+ description: formatLimitsSummary(limits),
37
+ handler: async () => {
38
+ limits = await promptLimits(limits);
39
+ lastAction = 'configure';
40
+ },
41
+ },
42
+ {
43
+ name: 'Change export mode',
44
+ emoji: '📤',
45
+ description: `Current: ${exportMode}`,
46
+ handler: async () => {
47
+ exportMode = await promptExportMode();
48
+ lastAction = 'export';
49
+ },
50
+ },
51
+ ];
52
+ const ran = await runHelperCli({
53
+ title: 'Format checker helper',
54
+ scripts,
55
+ argv: [],
56
+ });
57
+ if (!ran)
58
+ return;
59
+ if (lastAction === 'run')
60
+ return;
61
+ }
15
62
  }
16
63
  export async function runFormatCheckerScanCli(argv) {
17
64
  const { overrides, help, exportMode } = parseScanCliArgs(argv);
@@ -24,9 +71,9 @@ export async function runFormatCheckerScanCli(argv) {
24
71
  console.log(colors.cyan('Running format checker scan (machine-friendly)'));
25
72
  await executeFormatCheck(limits, exportMode);
26
73
  }
27
- async function executeFormatCheck(limits, exportMode) {
28
- console.log(colors.magenta('Scanning workspace for source files...'));
29
- const files = await collectSourceFiles(rootDir);
74
+ async function executeFormatCheck(limits, exportMode, scanRoot = rootDir, scopeLabel = 'workspace') {
75
+ console.log(colors.magenta(`Scanning ${scopeLabel} for source files...`));
76
+ const files = await collectSourceFiles(scanRoot);
30
77
  if (files.length === 0) {
31
78
  console.log(colors.yellow('No source files were found to analyze.'));
32
79
  return;
@@ -46,3 +93,6 @@ async function handleViolations(violations, reportingMode, exportMode) {
46
93
  await exportReportLines('format-checker', 'txt', lines);
47
94
  return result;
48
95
  }
96
+ function formatLimitsSummary(limits) {
97
+ return `limits: fn≤${limits.maxFunctionLength} · indent≤${limits.maxIndentationDepth} · reporting=${limits.reportingMode} · exportOnly=${limits.exportOnly}`;
98
+ }
@@ -4,7 +4,8 @@ import { colors } from '../utils/log.js';
4
4
  import { exportReportLines, promptExportMode, } from '../utils/export.js';
5
5
  import { collectSourceFiles } from '../format-checker/scan/collect.js';
6
6
  import { resolveScriptKind } from '../format-checker/scan/utils.js';
7
- import { rootDir } from '../helper-cli/env.js';
7
+ import { promptDirectorySelection, describeDirectorySelection } from '../directory-picker.js';
8
+ import { runHelperCli } from '../helper-cli.js';
8
9
  import { loadRobotSettings } from './config.js';
9
10
  import { promptRobotSettings } from './cli/prompts.js';
10
11
  import { collectFunctions } from './extractors/functions.js';
@@ -14,28 +15,67 @@ import { collectTypes } from './extractors/types.js';
14
15
  import { collectClasses } from './extractors/classes.js';
15
16
  export async function runRobot() {
16
17
  const defaults = await loadRobotSettings();
17
- const settings = await promptRobotSettings(defaults);
18
- const exportMode = await promptExportMode();
18
+ let settings = defaults;
19
+ let exportMode = 'console';
19
20
  console.log(colors.magenta('Running robot analyzer (manager.robot settings)'));
20
- const files = await collectSourceFiles(rootDir);
21
+ while (true) {
22
+ let lastAction;
23
+ let runResult;
24
+ const scripts = [
25
+ {
26
+ name: 'Run robot extractor',
27
+ emoji: '🤖',
28
+ description: robotSettingsSummary(settings),
29
+ handler: async () => {
30
+ const selection = await promptDirectorySelection({
31
+ title: 'Select directory for robot extractor',
32
+ });
33
+ if (!selection)
34
+ return;
35
+ lastAction = 'run';
36
+ runResult = await executeRobotExtraction(settings, exportMode, selection.absolutePath, describeDirectorySelection(selection));
37
+ },
38
+ },
39
+ {
40
+ name: 'Adjust settings',
41
+ emoji: '⚙️',
42
+ description: robotSettingsSummary(settings),
43
+ handler: async () => {
44
+ settings = await promptRobotSettings(settings);
45
+ lastAction = 'configure';
46
+ },
47
+ },
48
+ {
49
+ name: 'Change export mode',
50
+ emoji: '📤',
51
+ description: `Current: ${exportMode}`,
52
+ handler: async () => {
53
+ exportMode = await promptExportMode();
54
+ lastAction = 'export';
55
+ },
56
+ },
57
+ ];
58
+ const ran = await runHelperCli({
59
+ title: 'Robot metadata helper',
60
+ scripts,
61
+ argv: [],
62
+ });
63
+ if (!ran)
64
+ return createEmptyRobotResult();
65
+ if (lastAction === 'run' && runResult) {
66
+ return runResult;
67
+ }
68
+ }
69
+ }
70
+ async function executeRobotExtraction(settings, exportMode, scanRoot, scanLabel) {
71
+ console.log(colors.magenta(`Scanning ${scanLabel} for metadata`));
72
+ const files = await collectSourceFiles(scanRoot);
21
73
  if (files.length === 0) {
22
74
  console.log(colors.yellow('No source files found for robot'));
23
- return {
24
- functions: [],
25
- components: [],
26
- types: [],
27
- consts: [],
28
- classes: [],
29
- };
75
+ return createEmptyRobotResult();
30
76
  }
31
77
  console.log(colors.magenta(`Analyzing ${files.length} files for metadata`));
32
- const result = {
33
- functions: [],
34
- components: [],
35
- types: [],
36
- consts: [],
37
- classes: [],
38
- };
78
+ const result = createEmptyRobotResult();
39
79
  for (const file of files) {
40
80
  let content;
41
81
  try {
@@ -92,6 +132,19 @@ export async function runRobot() {
92
132
  }
93
133
  return result;
94
134
  }
135
+ function createEmptyRobotResult() {
136
+ return {
137
+ functions: [],
138
+ components: [],
139
+ types: [],
140
+ consts: [],
141
+ classes: [],
142
+ };
143
+ }
144
+ function robotSettingsSummary(settings) {
145
+ const kinds = settings.includeKinds.join(',');
146
+ return `kinds: ${kinds} · exportOnly=${settings.exportedOnly ? 'yes' : 'no'} · columns≤${settings.maxColumns}`;
147
+ }
95
148
  function estimateTokenCount(serialized) {
96
149
  const length = Math.max(1, serialized.length);
97
150
  return Math.ceil(length / 4);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",