@lakindu_perera/toren 1.0.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.
- package/LICENSE +21 -0
- package/README.md +354 -0
- package/bin/toren.js +203 -0
- package/package.json +39 -0
- package/src/lifecycle.js +124 -0
- package/src/renderers/console-renderer.js +218 -0
- package/src/renderers/html-renderer.js +913 -0
- package/src/renderers/index.js +41 -0
- package/src/renderers/json-renderer.js +54 -0
- package/src/renderers/markdown-renderer.js +328 -0
- package/src/renderers/tree-renderer.js +67 -0
- package/src/scanner/scan.js +341 -0
package/src/lifecycle.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
|
|
5
|
+
const C = {
|
|
6
|
+
reset: '\x1b[0m',
|
|
7
|
+
cyan: '\x1b[36m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
yellow: '\x1b[33m',
|
|
10
|
+
red: '\x1b[31m',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function paint(text, ...codes) {
|
|
14
|
+
return `${codes.join('')}${text}${C.reset}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getGlobalNpmRoot() {
|
|
18
|
+
try {
|
|
19
|
+
return execSync('npm root -g', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getInstallStatus() {
|
|
26
|
+
const status = {
|
|
27
|
+
isGlobal: false,
|
|
28
|
+
method: 'unknown',
|
|
29
|
+
binaryPath: process.argv[1] || 'unknown',
|
|
30
|
+
isBroken: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (!status.binaryPath || !fs.existsSync(status.binaryPath)) {
|
|
35
|
+
status.isBroken = true;
|
|
36
|
+
return status;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const realPath = fs.realpathSync(status.binaryPath);
|
|
40
|
+
const globalRoot = getGlobalNpmRoot();
|
|
41
|
+
|
|
42
|
+
if (globalRoot) {
|
|
43
|
+
if (realPath.includes(globalRoot)) {
|
|
44
|
+
status.isGlobal = true;
|
|
45
|
+
status.method = 'npm install -g';
|
|
46
|
+
} else if (status.binaryPath !== realPath) {
|
|
47
|
+
status.isGlobal = true;
|
|
48
|
+
status.method = 'npm link';
|
|
49
|
+
} else {
|
|
50
|
+
status.isGlobal = false;
|
|
51
|
+
status.method = 'local';
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
if (status.binaryPath !== realPath) {
|
|
55
|
+
status.isGlobal = true;
|
|
56
|
+
status.method = 'npm link';
|
|
57
|
+
} else {
|
|
58
|
+
status.isGlobal = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
status.isBroken = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return status;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function runDoctor(pkgVersion) {
|
|
69
|
+
const status = getInstallStatus();
|
|
70
|
+
|
|
71
|
+
if (status.isBroken) {
|
|
72
|
+
console.log(paint('⚠ Broken installation detected', C.red));
|
|
73
|
+
console.log(`→ Run: ${paint('npm uninstall -g toren', C.cyan)}`);
|
|
74
|
+
console.log('');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (status.isGlobal) {
|
|
79
|
+
console.log(`${paint('✔', C.green)} Toren installed globally`);
|
|
80
|
+
} else {
|
|
81
|
+
console.log(`${paint('✔', C.green)} Toren installed locally`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`${paint('✔', C.green)} Binary path valid`);
|
|
85
|
+
console.log(`${paint('✔', C.green)} Version match confirmed`);
|
|
86
|
+
console.log('');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function runUninstall() {
|
|
90
|
+
const status = getInstallStatus();
|
|
91
|
+
|
|
92
|
+
if (status.isBroken) {
|
|
93
|
+
console.log(paint('⚠ Toren installation is corrupted. Please reinstall using npm install -g toren', C.yellow));
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const rl = readline.createInterface({
|
|
98
|
+
input: process.stdin,
|
|
99
|
+
output: process.stdout
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
rl.question('Are you sure you want to remove Toren from global environment? (y/n) ', (answer) => {
|
|
103
|
+
rl.close();
|
|
104
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log('Uninstalling Toren...');
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
execSync('npm uninstall -g toren', { stdio: 'inherit' });
|
|
113
|
+
console.log('');
|
|
114
|
+
console.log(paint('✔ Successfully removed Toren from the global environment.', C.green));
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(paint('❌ Failed to uninstall Toren.', C.red));
|
|
118
|
+
console.log('Please try manually: npm uninstall -g toren');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('');
|
|
122
|
+
process.exit(0);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Toren — Console Renderer
|
|
3
|
+
*
|
|
4
|
+
* Consumes a {@link ScanResult} and produces styled terminal output.
|
|
5
|
+
*
|
|
6
|
+
* Design contract:
|
|
7
|
+
* - No business logic. Every value rendered is taken directly from ScanResult.
|
|
8
|
+
* - No imports from the scanner or any domain module.
|
|
9
|
+
* - Stateless: render() may be called multiple times safely.
|
|
10
|
+
* - The shape expected here matches the ScanResult typedef in scan.js.
|
|
11
|
+
* When ScanResult grows new fields, add new render sections; never mutate data.
|
|
12
|
+
*
|
|
13
|
+
* Adding a new output format (JSON, Markdown, HTML …):
|
|
14
|
+
* - Create src/renderers/<format>-renderer.js
|
|
15
|
+
* - Export a render(result, options?) function with the same signature
|
|
16
|
+
* - Import and call it from bin/toren.js based on a --format flag
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const pkg = require('../../package.json');
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// ANSI palette — zero external dependencies
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const C = {
|
|
30
|
+
reset: '\x1b[0m',
|
|
31
|
+
bold: '\x1b[1m',
|
|
32
|
+
dim: '\x1b[2m',
|
|
33
|
+
cyan: '\x1b[36m',
|
|
34
|
+
green: '\x1b[32m',
|
|
35
|
+
yellow: '\x1b[33m',
|
|
36
|
+
blue: '\x1b[34m',
|
|
37
|
+
magenta: '\x1b[35m',
|
|
38
|
+
red: '\x1b[31m',
|
|
39
|
+
white: '\x1b[97m',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Maximum files shown in the structure preview. */
|
|
43
|
+
const PREVIEW_LIMIT = 20;
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Low-level paint / layout helpers (private to this module)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Wrap `text` with one or more ANSI codes, resetting after.
|
|
51
|
+
* @param {string} text
|
|
52
|
+
* @param {...string} codes
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function paint(text, ...codes) {
|
|
56
|
+
return `${codes.join('')}${text}${C.reset}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Print a full-width horizontal rule (≤ 80 chars).
|
|
61
|
+
* @param {string} [char='─']
|
|
62
|
+
*/
|
|
63
|
+
function divider(char = '─') {
|
|
64
|
+
const width = Math.min(process.stdout.columns ?? 72, 80);
|
|
65
|
+
console.log(paint(char.repeat(width), C.dim));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Print a titled section header followed by a divider.
|
|
70
|
+
* @param {string} emoji
|
|
71
|
+
* @param {string} title
|
|
72
|
+
*/
|
|
73
|
+
function section(emoji, title) {
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log(`${emoji} ${paint(title, C.bold, C.white)}`);
|
|
76
|
+
divider();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Print a single labelled key-value row.
|
|
81
|
+
* @param {string} label - Left-hand label (dim)
|
|
82
|
+
* @param {string} value - Right-hand value
|
|
83
|
+
* @param {string} [valueColor] - Optional ANSI code(s) for the value
|
|
84
|
+
*/
|
|
85
|
+
function row(label, value, ...valueCodes) {
|
|
86
|
+
const coloured = valueCodes.length ? paint(value, ...valueCodes) : value;
|
|
87
|
+
console.log(` ${paint(label, C.dim)} ${coloured}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// File-tree renderer (private)
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Recursively print a file tree with classic tree connectors.
|
|
96
|
+
* Stops after PREVIEW_LIMIT files have been printed.
|
|
97
|
+
*
|
|
98
|
+
* @param {import('../scanner/scan.js').DirNode | import('../scanner/scan.js').FileNode} node
|
|
99
|
+
* @param {string} prefix - Accumulated indentation
|
|
100
|
+
* @param {boolean} isLast - Whether this is the last sibling
|
|
101
|
+
* @param {{ count: number }} counter - Shared mutable file counter
|
|
102
|
+
*/
|
|
103
|
+
function renderTree(node, prefix, isLast, counter, depth = 0, maxDepth = 4) {
|
|
104
|
+
if (counter.count >= PREVIEW_LIMIT) return;
|
|
105
|
+
if (depth >= maxDepth) return;
|
|
106
|
+
|
|
107
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
108
|
+
const extension = isLast ? ' ' : '│ ';
|
|
109
|
+
|
|
110
|
+
if (node.type === 'directory') {
|
|
111
|
+
console.log(`${prefix}${connector}${paint(`${node.name}/`, C.bold, C.blue)}`);
|
|
112
|
+
const children = node.children ?? [];
|
|
113
|
+
|
|
114
|
+
if (depth === maxDepth - 1 && children.length > 0) {
|
|
115
|
+
console.log(`${prefix}${extension}└── ${paint('...', C.dim)}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < children.length; i++) {
|
|
120
|
+
if (counter.count >= PREVIEW_LIMIT) break;
|
|
121
|
+
renderTree(children[i], prefix + extension, i === children.length - 1, counter, depth + 1, maxDepth);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
console.log(`${prefix}${connector}${paint(node.name, C.white)}`);
|
|
125
|
+
counter.count += 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatDuration(ms) {
|
|
130
|
+
if (ms < 1) return '< 1 ms';
|
|
131
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(2)} s`;
|
|
132
|
+
return `${Math.round(ms)} ms`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Banner (private)
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
function printBanner() {
|
|
140
|
+
const name = paint('Toren', C.bold, C.cyan);
|
|
141
|
+
const version = paint(`v${pkg.version}`, C.dim);
|
|
142
|
+
const tagline = paint('Codebase Onboarding Intelligence', C.dim);
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(` ${name} ${version} — ${tagline}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Public API
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Render a ScanResult to the terminal.
|
|
153
|
+
*
|
|
154
|
+
* All sections read exclusively from the ScanResult; no derivations or
|
|
155
|
+
* business decisions are made here.
|
|
156
|
+
*
|
|
157
|
+
* @param {import('../scanner/scan.js').ScanResult} result
|
|
158
|
+
* @param {{ cwd?: string }} [options]
|
|
159
|
+
*/
|
|
160
|
+
export function render(result, options = {}) {
|
|
161
|
+
const {
|
|
162
|
+
rootPath,
|
|
163
|
+
projectType,
|
|
164
|
+
entryPoints,
|
|
165
|
+
tree,
|
|
166
|
+
flatFiles,
|
|
167
|
+
totalFolders,
|
|
168
|
+
scanDurationMs,
|
|
169
|
+
} = result;
|
|
170
|
+
|
|
171
|
+
const cwd = options.cwd ?? process.cwd();
|
|
172
|
+
const relRoot = path.relative(cwd, rootPath) || '.';
|
|
173
|
+
|
|
174
|
+
// ── Banner ────────────────────────────────────────────────────────────────
|
|
175
|
+
printBanner();
|
|
176
|
+
console.log('');
|
|
177
|
+
|
|
178
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
179
|
+
section('🔍', 'Project Summary');
|
|
180
|
+
row('Path: ', paint(relRoot, C.cyan));
|
|
181
|
+
row('Project type: ', paint(projectType, C.bold, C.green));
|
|
182
|
+
row('Total files: ', paint(String(flatFiles.length), C.yellow));
|
|
183
|
+
row('Total folders:', paint(String(totalFolders), C.yellow));
|
|
184
|
+
row('Scan duration:', paint(formatDuration(scanDurationMs), C.magenta));
|
|
185
|
+
|
|
186
|
+
// ── Entry Points ──────────────────────────────────────────────────────────
|
|
187
|
+
section('🚪', 'Entry Points');
|
|
188
|
+
if (entryPoints.length === 0) {
|
|
189
|
+
console.log(paint(' ⚠ No entry points detected (this may be a library or utility project)', C.yellow));
|
|
190
|
+
} else {
|
|
191
|
+
for (const ep of entryPoints) {
|
|
192
|
+
console.log(` ${paint('→', C.cyan)} ${paint(ep, C.white)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Structure Preview ─────────────────────────────────────────────────────
|
|
197
|
+
section('📁', `Folder Structure ${paint(`(first ${PREVIEW_LIMIT} files)`, C.dim)}`);
|
|
198
|
+
|
|
199
|
+
console.log(paint(`${tree.name || '.'}/`, C.bold, C.blue));
|
|
200
|
+
|
|
201
|
+
const counter = { count: 0 };
|
|
202
|
+
const children = tree.children ?? [];
|
|
203
|
+
for (let i = 0; i < children.length; i++) {
|
|
204
|
+
if (counter.count >= PREVIEW_LIMIT) break;
|
|
205
|
+
renderTree(children[i], '', i === children.length - 1, counter);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (flatFiles.length > PREVIEW_LIMIT) {
|
|
209
|
+
const hidden = flatFiles.length - PREVIEW_LIMIT;
|
|
210
|
+
console.log(paint(` … and ${hidden} more file(s) not shown`, C.dim));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Footer ────────────────────────────────────────────────────────────────
|
|
214
|
+
console.log('');
|
|
215
|
+
divider();
|
|
216
|
+
console.log(paint(' ✅ Scan complete.', C.green));
|
|
217
|
+
console.log('');
|
|
218
|
+
}
|