@lakindu_perera/toren 1.0.7 → 1.0.8
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/CHANGELOG.md +19 -0
- package/README.md +120 -31
- package/bin/toren.js +3 -0
- package/package.json +7 -3
- package/src/detectors/health-detector.js +72 -0
- package/src/detectors/important-files-detector.js +515 -0
- package/src/detectors/package-manager-detector.js +112 -0
- package/src/detectors/project-info-detector.js +89 -0
- package/src/detectors/script-detector.js +226 -10
- package/src/focused-output.js +78 -8
- package/src/renderers/console-renderer.js +53 -4
- package/src/renderers/html-renderer.js +128 -6
- package/src/renderers/json-renderer.js +15 -1
- package/src/renderers/markdown-renderer.js +70 -10
- package/src/scanner/scan.js +28 -6
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detect project info from the scanned repository.
|
|
6
|
+
*/
|
|
7
|
+
export function detectProjectInfo({
|
|
8
|
+
rootPath,
|
|
9
|
+
projectType,
|
|
10
|
+
flatFiles,
|
|
11
|
+
entryPoints,
|
|
12
|
+
packageManager,
|
|
13
|
+
scripts
|
|
14
|
+
}) {
|
|
15
|
+
const fileSet = new Set(flatFiles);
|
|
16
|
+
|
|
17
|
+
let name = null;
|
|
18
|
+
const pkgPath = path.join(rootPath, 'package.json');
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(pkgPath)) {
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
22
|
+
if (pkg.name) name = pkg.name;
|
|
23
|
+
}
|
|
24
|
+
} catch {}
|
|
25
|
+
if (!name) {
|
|
26
|
+
name = path.basename(rootPath) || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let runtime = null;
|
|
30
|
+
const pt = projectType.toLowerCase();
|
|
31
|
+
if (pt.includes('node') || fileSet.has('package.json')) runtime = 'Node.js';
|
|
32
|
+
else if (pt.includes('python')) runtime = 'Python';
|
|
33
|
+
else if (pt.includes('java') || pt.includes('spring')) runtime = 'JVM / Java';
|
|
34
|
+
else if (pt.includes('go')) runtime = 'Go';
|
|
35
|
+
else if (pt.includes('rust')) runtime = 'Rust';
|
|
36
|
+
else if (pt.includes('ruby')) runtime = 'Ruby';
|
|
37
|
+
else if (pt.includes('php') || pt.includes('composer')) runtime = 'PHP';
|
|
38
|
+
else if (pt.includes('elixir') || pt.includes('phoenix')) runtime = 'Erlang / BEAM';
|
|
39
|
+
|
|
40
|
+
let language = null;
|
|
41
|
+
if (fileSet.has('tsconfig.json') || pt.includes('angular')) {
|
|
42
|
+
language = 'TypeScript';
|
|
43
|
+
} else if (fileSet.has('package.json') && !fileSet.has('tsconfig.json')) {
|
|
44
|
+
language = 'JavaScript';
|
|
45
|
+
} else if (pt.includes('python')) language = 'Python';
|
|
46
|
+
else if (pt.includes('java') || pt.includes('spring')) language = 'Java';
|
|
47
|
+
else if (pt.includes('go')) language = 'Go';
|
|
48
|
+
else if (pt.includes('rust')) language = 'Rust';
|
|
49
|
+
else if (pt.includes('ruby')) language = 'Ruby';
|
|
50
|
+
else if (pt.includes('php')) language = 'PHP';
|
|
51
|
+
else if (pt.includes('elixir')) language = 'Elixir';
|
|
52
|
+
|
|
53
|
+
let framework = null;
|
|
54
|
+
if (projectType && projectType !== 'Unknown') {
|
|
55
|
+
framework = projectType;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let architecture = null;
|
|
59
|
+
if (pt.includes('next')) {
|
|
60
|
+
const hasApp = Array.from(fileSet).some(f => f.startsWith('app/'));
|
|
61
|
+
const hasPages = Array.from(fileSet).some(f => f.startsWith('pages/'));
|
|
62
|
+
if (hasApp && hasPages) architecture = 'Hybrid App/Pages Router';
|
|
63
|
+
else if (hasApp) architecture = 'App Router';
|
|
64
|
+
else if (hasPages) architecture = 'Pages Router';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const entryPoint = entryPoints.length > 0 ? entryPoints[0] : null;
|
|
68
|
+
|
|
69
|
+
let sourceDirectory = null;
|
|
70
|
+
if (entryPoint) {
|
|
71
|
+
if (entryPoint.startsWith('src/')) sourceDirectory = 'src';
|
|
72
|
+
else if (entryPoint.startsWith('app/')) sourceDirectory = 'app';
|
|
73
|
+
else if (entryPoint.startsWith('pages/')) sourceDirectory = 'pages';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
projectInfo: {
|
|
78
|
+
name,
|
|
79
|
+
projectType,
|
|
80
|
+
runtime,
|
|
81
|
+
language,
|
|
82
|
+
framework,
|
|
83
|
+
architecture,
|
|
84
|
+
packageManager,
|
|
85
|
+
entryPoint,
|
|
86
|
+
sourceDirectory
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -1,30 +1,246 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Toren — Script Detector with Intelligence Layer
|
|
3
|
+
*
|
|
4
|
+
* Reads package.json scripts and enriches each entry with:
|
|
5
|
+
* - category — functional grouping (development, build, testing, etc.)
|
|
6
|
+
* - description — human-readable purpose string
|
|
7
|
+
* - usage — ready-to-run command string for the detected package manager
|
|
8
|
+
*
|
|
9
|
+
* Design contract:
|
|
10
|
+
* - Backward compatible: name and command are always present and unchanged.
|
|
11
|
+
* - category and description are null when no deterministic match exists.
|
|
12
|
+
* - usage is always a string — derived from packageManager or falls back to
|
|
13
|
+
* "npm run <name>" when packageManager is null.
|
|
14
|
+
* - Descriptions are never fabricated. Only canonical mappings and known
|
|
15
|
+
* command-keyword inferences are used.
|
|
16
|
+
* - Deterministic: same input always produces the same output.
|
|
17
|
+
* - Graceful: malformed or missing package.json returns an empty array.
|
|
18
|
+
*
|
|
19
|
+
* @module detectors/script-detector
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'node:fs';
|
|
2
23
|
import path from 'node:path';
|
|
3
24
|
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Description and category tables — keyed by exact script name (lowercase)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Maps a known script name to its canonical human-readable description.
|
|
31
|
+
* @type {Map<string, string>}
|
|
32
|
+
*/
|
|
33
|
+
const NAME_DESCRIPTIONS = new Map([
|
|
34
|
+
['dev', 'Start the development server'],
|
|
35
|
+
['develop', 'Start the development server'],
|
|
36
|
+
['start', 'Start the application'],
|
|
37
|
+
['serve', 'Serve the application'],
|
|
38
|
+
['preview', 'Preview the production build'],
|
|
39
|
+
['build', 'Create a production build'],
|
|
40
|
+
['test', 'Run the test suite'],
|
|
41
|
+
['test:watch', 'Run tests in watch mode'],
|
|
42
|
+
['test:e2e', 'Run end-to-end tests'],
|
|
43
|
+
['lint', 'Run code-quality checks'],
|
|
44
|
+
['lint:fix', 'Run code-quality checks and fix supported issues'],
|
|
45
|
+
['format', 'Format the codebase'],
|
|
46
|
+
['typecheck', 'Run static type checks'],
|
|
47
|
+
['check', 'Run project checks'],
|
|
48
|
+
['clean', 'Remove generated build artifacts'],
|
|
49
|
+
['generate', 'Generate project files or code'],
|
|
50
|
+
['migrate', 'Run database migrations'],
|
|
51
|
+
['seed', 'Seed the database'],
|
|
52
|
+
['deploy', 'Deploy the application'],
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Maps a known script name to its functional category.
|
|
57
|
+
* @type {Map<string, string>}
|
|
58
|
+
*/
|
|
59
|
+
const NAME_CATEGORIES = new Map([
|
|
60
|
+
['dev', 'development'],
|
|
61
|
+
['develop', 'development'],
|
|
62
|
+
['start', 'development'],
|
|
63
|
+
['serve', 'development'],
|
|
64
|
+
['preview', 'development'],
|
|
65
|
+
['build', 'build'],
|
|
66
|
+
['test', 'testing'],
|
|
67
|
+
['test:watch', 'testing'],
|
|
68
|
+
['test:e2e', 'testing'],
|
|
69
|
+
['lint', 'quality'],
|
|
70
|
+
['lint:fix', 'quality'],
|
|
71
|
+
['format', 'quality'],
|
|
72
|
+
['typecheck', 'quality'],
|
|
73
|
+
['check', 'quality'],
|
|
74
|
+
['clean', 'utility'],
|
|
75
|
+
['generate', 'utility'],
|
|
76
|
+
['migrate', 'database'],
|
|
77
|
+
['seed', 'database'],
|
|
78
|
+
['deploy', 'deployment'],
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Command-keyword inference — ordered most-specific first
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Ordered list of command-keyword → { description, category } inferences.
|
|
87
|
+
* Evaluated only when a script name has no entry in NAME_DESCRIPTIONS.
|
|
88
|
+
* More specific patterns (e.g. "playwright test") must appear before broader
|
|
89
|
+
* ones (e.g. "jest") to prevent false matches.
|
|
90
|
+
*
|
|
91
|
+
* @type {Array<{ keyword: string, description: string, category: string }>}
|
|
92
|
+
*/
|
|
93
|
+
const COMMAND_INFERENCE = [
|
|
94
|
+
{ keyword: 'playwright test', description: 'Run end-to-end tests', category: 'testing' },
|
|
95
|
+
{ keyword: 'cypress run', description: 'Run end-to-end tests', category: 'testing' },
|
|
96
|
+
{ keyword: 'vitest', description: 'Run the test suite', category: 'testing' },
|
|
97
|
+
{ keyword: 'jest', description: 'Run the test suite', category: 'testing' },
|
|
98
|
+
{ keyword: 'tsc --noEmit', description: 'Run static type checks', category: 'quality' },
|
|
99
|
+
{ keyword: 'eslint', description: 'Run code-quality checks', category: 'quality' },
|
|
100
|
+
{ keyword: 'prettier', description: 'Format the codebase', category: 'quality' },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Usage builder
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Script names that npm treats as built-in lifecycle commands.
|
|
109
|
+
* These do NOT need the "run" subcommand: `npm test`, `npm start`, etc.
|
|
110
|
+
* @type {Set<string>}
|
|
111
|
+
*/
|
|
112
|
+
const NPM_LIFECYCLE = new Set(['test', 'start', 'stop', 'restart']);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Script names that bun exposes as direct subcommands (no "run" needed).
|
|
116
|
+
* @type {Set<string>}
|
|
117
|
+
*/
|
|
118
|
+
const BUN_LIFECYCLE = new Set(['test', 'start']);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build the ready-to-run usage string for a script.
|
|
122
|
+
*
|
|
123
|
+
* Rules:
|
|
124
|
+
* npm: lifecycle commands (test, start…) → `npm <name>`
|
|
125
|
+
* all others → `npm run <name>`
|
|
126
|
+
* pnpm: all scripts → `pnpm <name>` (pnpm forwards directly)
|
|
127
|
+
* yarn: all scripts → `yarn <name>`
|
|
128
|
+
* bun: lifecycle commands (test, start) → `bun <name>`
|
|
129
|
+
* all others → `bun run <name>`
|
|
130
|
+
* null: conservative fallback → `npm run <name>` (always valid)
|
|
131
|
+
*
|
|
132
|
+
* @param {string} name - Script name
|
|
133
|
+
* @param {string|null} packageManager - Detected package manager or null
|
|
134
|
+
* @returns {string}
|
|
135
|
+
*/
|
|
136
|
+
function buildUsage(name, packageManager) {
|
|
137
|
+
switch (packageManager) {
|
|
138
|
+
case 'npm':
|
|
139
|
+
return NPM_LIFECYCLE.has(name) ? `npm ${name}` : `npm run ${name}`;
|
|
140
|
+
|
|
141
|
+
case 'pnpm':
|
|
142
|
+
// pnpm forwards all script names without requiring the 'run' sub-command.
|
|
143
|
+
return `pnpm ${name}`;
|
|
144
|
+
|
|
145
|
+
case 'yarn':
|
|
146
|
+
// yarn similarly runs scripts directly without 'run'.
|
|
147
|
+
return `yarn ${name}`;
|
|
148
|
+
|
|
149
|
+
case 'bun':
|
|
150
|
+
return BUN_LIFECYCLE.has(name) ? `bun ${name}` : `bun run ${name}`;
|
|
151
|
+
|
|
152
|
+
default:
|
|
153
|
+
// null or unknown — conservative: `npm run <name>` is always valid even
|
|
154
|
+
// for lifecycle names, so we keep it unconditionally safe.
|
|
155
|
+
return `npm run ${name}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Description + category resolver
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve description and category for a single script entry.
|
|
165
|
+
* Priority: exact name match → command-keyword inference → null.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} name
|
|
168
|
+
* @param {string} command
|
|
169
|
+
* @returns {{ description: string|null, category: string|null }}
|
|
170
|
+
*/
|
|
171
|
+
function resolveIntelligence(name, command) {
|
|
172
|
+
// 1. Exact name match (canonical mapping — highest confidence).
|
|
173
|
+
const nameDescription = NAME_DESCRIPTIONS.get(name) ?? null;
|
|
174
|
+
const nameCategory = NAME_CATEGORIES.get(name) ?? null;
|
|
175
|
+
|
|
176
|
+
if (nameDescription !== null || nameCategory !== null) {
|
|
177
|
+
return { description: nameDescription, category: nameCategory };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 2. Command-keyword inference (fallback — only for unknown names).
|
|
181
|
+
for (const { keyword, description, category } of COMMAND_INFERENCE) {
|
|
182
|
+
if (command.includes(keyword)) {
|
|
183
|
+
return { description, category };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 3. Unknown — do not fabricate.
|
|
188
|
+
return { description: null, category: null };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Types (JSDoc — no TypeScript dependency required)
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
4
195
|
/**
|
|
5
|
-
*
|
|
196
|
+
* @typedef {Object} ScriptItem
|
|
197
|
+
* @property {string} name - Script name as declared in package.json
|
|
198
|
+
* @property {string} command - Raw script command string
|
|
199
|
+
* @property {string|null} category - Functional category, or null when unknown
|
|
200
|
+
* @property {string|null} description - Human-readable purpose, or null when unknown
|
|
201
|
+
* @property {string} usage - Ready-to-run invocation string
|
|
202
|
+
*/
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Public API
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Detect and enrich npm/package scripts from package.json.
|
|
6
210
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
211
|
+
* The `packageManager` parameter is optional for backward compatibility.
|
|
212
|
+
* When omitted (or null), usage strings fall back to `npm run <name>`.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} rootPath - Absolute path of the scanned root
|
|
215
|
+
* @param {string|null} [packageManager=null] - Detected package manager name
|
|
216
|
+
* @returns {{ scripts: ScriptItem[] }}
|
|
9
217
|
*/
|
|
10
|
-
export function detectScripts(rootPath) {
|
|
218
|
+
export function detectScripts(rootPath, packageManager = null) {
|
|
11
219
|
const scripts = [];
|
|
12
220
|
const pkgPath = path.join(rootPath, 'package.json');
|
|
13
221
|
|
|
14
222
|
try {
|
|
15
223
|
if (fs.existsSync(pkgPath)) {
|
|
16
224
|
const content = fs.readFileSync(pkgPath, 'utf8');
|
|
17
|
-
const pkg
|
|
225
|
+
const pkg = JSON.parse(content);
|
|
18
226
|
|
|
19
227
|
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
20
228
|
for (const [name, command] of Object.entries(pkg.scripts)) {
|
|
21
|
-
if (typeof command
|
|
22
|
-
|
|
23
|
-
}
|
|
229
|
+
if (typeof command !== 'string') continue;
|
|
230
|
+
|
|
231
|
+
const { description, category } = resolveIntelligence(name, command);
|
|
232
|
+
|
|
233
|
+
scripts.push({
|
|
234
|
+
name,
|
|
235
|
+
command,
|
|
236
|
+
category,
|
|
237
|
+
description,
|
|
238
|
+
usage: buildUsage(name, packageManager),
|
|
239
|
+
});
|
|
24
240
|
}
|
|
25
241
|
}
|
|
26
242
|
}
|
|
27
|
-
} catch
|
|
243
|
+
} catch {
|
|
28
244
|
// Return empty scripts on malformed or unreadable package.json
|
|
29
245
|
}
|
|
30
246
|
|
package/src/focused-output.js
CHANGED
|
@@ -22,10 +22,12 @@ import { renderStructure } from './renderers/console-renderer.js';
|
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
23
|
|
|
24
24
|
const EMPTY = {
|
|
25
|
-
frameworks:
|
|
26
|
-
entryPoints:
|
|
27
|
-
configs:
|
|
28
|
-
scripts:
|
|
25
|
+
frameworks: 'No frameworks detected.',
|
|
26
|
+
entryPoints: 'No entry points detected.',
|
|
27
|
+
configs: 'No configuration files detected.',
|
|
28
|
+
scripts: 'No package scripts detected.',
|
|
29
|
+
importantFiles: 'No important files detected.',
|
|
30
|
+
health: 'No project health observations available.',
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export const FOCUSED_FLAGS = [
|
|
@@ -34,7 +36,10 @@ export const FOCUSED_FLAGS = [
|
|
|
34
36
|
'--entry-points',
|
|
35
37
|
'--structure',
|
|
36
38
|
'--configs',
|
|
37
|
-
'--scripts'
|
|
39
|
+
'--scripts',
|
|
40
|
+
'--summary',
|
|
41
|
+
'--important-files',
|
|
42
|
+
'--health'
|
|
38
43
|
];
|
|
39
44
|
|
|
40
45
|
/**
|
|
@@ -133,14 +138,79 @@ export function renderFocusedMode(mode, result) {
|
|
|
133
138
|
case '--scripts': {
|
|
134
139
|
const scriptsList = [];
|
|
135
140
|
if (result.scripts && result.scripts.length > 0) {
|
|
136
|
-
const maxNameLen = Math.max(...result.scripts.map(s => s.name.length));
|
|
137
141
|
for (const s of result.scripts) {
|
|
138
|
-
const
|
|
139
|
-
scriptsList.push(`\x1b[97m${
|
|
142
|
+
const usage = s.usage || `npm run ${s.name}`;
|
|
143
|
+
scriptsList.push(`\x1b[97m${usage}\x1b[0m`);
|
|
144
|
+
if (s.description) {
|
|
145
|
+
scriptsList.push(` \x1b[2m${s.description}\x1b[0m\n`);
|
|
146
|
+
} else {
|
|
147
|
+
scriptsList.push(` \x1b[2m${s.command}\x1b[0m\n`);
|
|
148
|
+
}
|
|
140
149
|
}
|
|
141
150
|
}
|
|
151
|
+
// Remove trailing newline from last element if it exists
|
|
152
|
+
if (scriptsList.length > 0) {
|
|
153
|
+
scriptsList[scriptsList.length - 1] = scriptsList[scriptsList.length - 1].replace(/\n$/, '');
|
|
154
|
+
}
|
|
142
155
|
section('Package Scripts', scriptsList, EMPTY.scripts);
|
|
143
156
|
break;
|
|
144
157
|
}
|
|
158
|
+
|
|
159
|
+
case '--summary': {
|
|
160
|
+
const p = result.projectInfo || {};
|
|
161
|
+
const lines = [
|
|
162
|
+
`Name ${p.name || 'Unknown'}`,
|
|
163
|
+
`Type ${result.projectType || 'Unknown'}`,
|
|
164
|
+
];
|
|
165
|
+
if (p.runtime) lines.push(`Runtime ${p.runtime}`);
|
|
166
|
+
if (p.language) lines.push(`Language ${p.language}`);
|
|
167
|
+
if (p.framework) lines.push(`Framework ${p.framework}`);
|
|
168
|
+
if (p.architecture) lines.push(`Architecture ${p.architecture}`);
|
|
169
|
+
if (result.packageManager) lines.push(`Package Manager ${result.packageManager}`);
|
|
170
|
+
if (p.entryPoint) lines.push(`Entry Point ${p.entryPoint}`);
|
|
171
|
+
if (p.sourceDirectory) lines.push(`Source Directory ${p.sourceDirectory}`);
|
|
172
|
+
|
|
173
|
+
lines.push(`Files ${result.flatFiles ? result.flatFiles.length : 0}`);
|
|
174
|
+
lines.push(`Folders ${result.totalFolders || 0}`);
|
|
175
|
+
|
|
176
|
+
section('Project Summary', lines, 'No summary available.');
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case '--important-files': {
|
|
181
|
+
const items = [];
|
|
182
|
+
if (result.importantFiles && result.importantFiles.length > 0) {
|
|
183
|
+
const top10 = result.importantFiles.slice(0, 10);
|
|
184
|
+
top10.forEach((f, idx) => {
|
|
185
|
+
items.push(`${idx + 1}. \x1b[97m${f.path}\x1b[0m`);
|
|
186
|
+
items.push(` \x1b[2m${f.reason}\x1b[0m\n`);
|
|
187
|
+
});
|
|
188
|
+
// Remove trailing newline
|
|
189
|
+
if (items.length > 0) {
|
|
190
|
+
items[items.length - 1] = items[items.length - 1].replace(/\n$/, '');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
section('Important Files', items, EMPTY.importantFiles);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case '--health': {
|
|
198
|
+
const items = [];
|
|
199
|
+
if (result.health && result.health.length > 0) {
|
|
200
|
+
for (const h of result.health) {
|
|
201
|
+
let icon = 'ℹ';
|
|
202
|
+
if (h.status === 'pass') icon = '✓';
|
|
203
|
+
else if (h.status === 'warning') icon = '⚠';
|
|
204
|
+
|
|
205
|
+
let color = '\x1b[97m';
|
|
206
|
+
if (h.status === 'pass') color = '\x1b[32m';
|
|
207
|
+
else if (h.status === 'warning') color = '\x1b[33m';
|
|
208
|
+
|
|
209
|
+
items.push(`${color}${icon}\x1b[0m ${h.message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
section('Project Health', items, EMPTY.health);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
145
215
|
}
|
|
146
216
|
}
|
|
@@ -229,6 +229,10 @@ export function render(result, options = {}) {
|
|
|
229
229
|
entryPoints,
|
|
230
230
|
configs = [],
|
|
231
231
|
scripts = [],
|
|
232
|
+
packageManager,
|
|
233
|
+
importantFiles = [],
|
|
234
|
+
projectInfo = null,
|
|
235
|
+
health = [],
|
|
232
236
|
tree,
|
|
233
237
|
flatFiles,
|
|
234
238
|
totalFolders,
|
|
@@ -245,11 +249,50 @@ export function render(result, options = {}) {
|
|
|
245
249
|
// ── Summary ───────────────────────────────────────────────────────────────
|
|
246
250
|
section('Project Summary');
|
|
247
251
|
row('Path: ', paint(relRoot, C.cyan));
|
|
252
|
+
if (projectInfo && projectInfo.name) {
|
|
253
|
+
row('Name: ', paint(projectInfo.name, C.white));
|
|
254
|
+
}
|
|
248
255
|
row('Project type: ', paint(projectType, C.bold, C.green));
|
|
256
|
+
if (packageManager) {
|
|
257
|
+
row('Pkg manager: ', paint(packageManager, C.white));
|
|
258
|
+
}
|
|
249
259
|
row('Total files: ', paint(String(flatFiles.length), C.yellow));
|
|
250
260
|
row('Total folders:', paint(String(totalFolders), C.yellow));
|
|
251
261
|
console.log('');
|
|
252
262
|
|
|
263
|
+
// ── Project Health ────────────────────────────────────────────────────────
|
|
264
|
+
section('Project Health');
|
|
265
|
+
if (health.length === 0) {
|
|
266
|
+
console.log(paint('No project health observations available.', C.dim));
|
|
267
|
+
} else {
|
|
268
|
+
for (const h of health) {
|
|
269
|
+
let icon = 'ℹ';
|
|
270
|
+
let color = C.white;
|
|
271
|
+
if (h.status === 'pass') {
|
|
272
|
+
icon = '✓';
|
|
273
|
+
color = C.green;
|
|
274
|
+
} else if (h.status === 'warning') {
|
|
275
|
+
icon = '⚠';
|
|
276
|
+
color = C.yellow;
|
|
277
|
+
}
|
|
278
|
+
console.log(`${paint(icon, color)} ${h.message}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
console.log('');
|
|
282
|
+
|
|
283
|
+
// ── Important Files ───────────────────────────────────────────────────────
|
|
284
|
+
section('Important Files');
|
|
285
|
+
if (importantFiles.length === 0) {
|
|
286
|
+
console.log(paint('No important files detected.', C.dim));
|
|
287
|
+
} else {
|
|
288
|
+
importantFiles.forEach((f, idx) => {
|
|
289
|
+
console.log(`${idx + 1}. ${paint(f.path, C.white)}`);
|
|
290
|
+
console.log(` ${paint(f.reason, C.dim)}`);
|
|
291
|
+
if (idx < importantFiles.length - 1) console.log('');
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
console.log('');
|
|
295
|
+
|
|
253
296
|
// ── Frameworks ────────────────────────────────────────────────────────────
|
|
254
297
|
section('Frameworks');
|
|
255
298
|
const frameworks = deriveFrameworks(projectType);
|
|
@@ -289,10 +332,16 @@ export function render(result, options = {}) {
|
|
|
289
332
|
if (scripts.length === 0) {
|
|
290
333
|
console.log(paint('No package scripts detected.', C.dim));
|
|
291
334
|
} else {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
console.log(
|
|
335
|
+
for (let i = 0; i < scripts.length; i++) {
|
|
336
|
+
const s = scripts[i];
|
|
337
|
+
const usage = s.usage || `npm run ${s.name}`;
|
|
338
|
+
console.log(paint(usage, C.white));
|
|
339
|
+
if (s.description) {
|
|
340
|
+
console.log(` ${paint(s.description, C.dim)}`);
|
|
341
|
+
} else {
|
|
342
|
+
console.log(` ${paint(s.command, C.dim)}`);
|
|
343
|
+
}
|
|
344
|
+
if (i < scripts.length - 1) console.log('');
|
|
296
345
|
}
|
|
297
346
|
}
|
|
298
347
|
console.log('');
|