Repository navigation
Expand file tree
/
Copy pathcli.js
More file actions
executable file
·119 lines (108 loc) · 5.47 KB
/
Copy pathcli.js
File metadata and controls
executable file
·119 lines (108 loc) · 5.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/usr/bin/env node
'use strict';
const fs = require('node:fs/promises');
const path = require('node:path');
const { analyzeRepository } = require('./lib');
const pkg = require('./package.json');
const HELP = `Usage: hotfiles --repo <path> [options]
Options:
-r, --repo <path> Git repository (required)
-p, --path <path> Limit results to a directory
-l, --limit <number> Newest eligible commits to analyze
-m, --message <regex> Filter commit messages
-e, --ext <extension> Include extension (repeatable)
-i, --ignoreExt <extension> Exclude extension (repeatable)
--since <date> Include commits on or after date
-t, --till <date> Deprecated alias for --since
--format <text|json> Output format (default: text)
--color Force colors in text output
--no-color Disable colors
-o, --output <file> Write output atomically to a file
-j, --json <file> Shorthand for --format json --output <file>
--force Replace an existing output file
-h, --help Show help
-v, --version Show version
Examples:
hotfiles --repo ./my-project
hotfiles -r ./my-project --path src --limit 100 --ext js --ext ts
hotfiles -r ./my-project --since 2026-01-01 --format json
Documentation: https://github.com/Shastel/hotfiles#readme`;
function parse(argv) {
const valueFlags = new Map([
['-r', 'repo'], ['--repo', 'repo'], ['-p', 'path'], ['--path', 'path'], ['-l', 'limit'], ['--limit', 'limit'],
['-m', 'message'], ['--message', 'message'], ['-e', 'extensions'], ['--ext', 'extensions'],
['-i', 'ignoreExtensions'], ['--ignoreExt', 'ignoreExtensions'], ['--since', 'since'], ['-t', 'till'], ['--till', 'till'],
['--format', 'format'], ['-o', 'output'], ['--output', 'output'], ['-j', 'json'], ['--json', 'json']
]);
const result = { extensions: [], ignoreExtensions: [] };
for (let i = 0; i < argv.length; i += 1) {
let flag = argv[i];
let inline;
if (flag.startsWith('--') && flag.includes('=')) [flag, inline] = flag.split(/=(.*)/s, 2);
if (flag === '--help' || flag === '-h') result.help = true;
else if (flag === '--version' || flag === '-v') result.version = true;
else if (flag === '--force') result.force = true;
else if (flag === '--color') result.color = true;
else if (flag === '--no-color') result.color = false;
else if (valueFlags.has(flag)) {
const key = valueFlags.get(flag);
const value = inline === undefined ? argv[++i] : inline;
if (value === undefined) throw new Error(`${flag} requires a value`);
if (Array.isArray(result[key])) result[key].push(value); else result[key] = value;
} else throw new Error(`Unknown option: ${flag}`);
}
if (result.till && result.since) throw new Error('--till and --since cannot be used together');
if (result.till) result.since = result.till;
if (result.json) { result.format = 'json'; result.output = result.json; }
result.format ||= 'text';
if (!['text', 'json'].includes(result.format)) throw new Error('--format must be text or json');
if (result.limit !== undefined) {
if (!/^\d+$/.test(result.limit)) throw new Error('--limit must be a non-negative integer');
result.limit = Number(result.limit);
}
return result;
}
async function writeAtomic(destination, content, force) {
const resolved = path.resolve(destination);
if (!force) {
try { await fs.access(resolved); throw new Error(`Output already exists: ${resolved} (use --force to replace it)`); }
catch (error) { if (error.code !== 'ENOENT') throw error; }
}
const temporary = `${resolved}.${process.pid}.tmp`;
try {
await fs.writeFile(temporary, content, { flag: 'wx' });
await fs.rename(temporary, resolved);
} catch (error) {
await fs.unlink(temporary).catch(() => {});
throw error;
}
}
function shouldUseColor(args, stream = process.stdout) {
return args.format === 'text' && !args.output && (args.color === true || (args.color === undefined && stream.isTTY === true));
}
async function main(argv = process.argv.slice(2)) {
const args = parse(argv);
if (args.help) { process.stdout.write(`${HELP}\n`); return; }
if (args.version) { process.stdout.write(`${pkg.version}\n`); return; }
if (!args.repo) throw new Error('--repo is required\n\nUsage: hotfiles --repo <path> [options]\nRun "hotfiles --help" for examples and all options.');
if (args.till) process.stderr.write('Warning: --till is deprecated; use --since instead.\n');
const results = await analyzeRepository(args);
const useColor = shouldUseColor(args);
const paint = (code, value) => useColor ? `\u001b[${code}m${value}\u001b[0m` : value;
const text = results.flatMap(item => {
const lines = [`${paint('36', item.path)} => ${paint('33', String(item.commits))}`];
for (const detail of item.details) {
const subject = detail.message.split(/\r?\n/, 1)[0];
lines.push(` ${paint('90', detail.hash.slice(0, 12))} ${detail.date} ${subject}`);
}
return lines;
}).join('\n');
const content = args.format === 'json' ? `${JSON.stringify(results, null, 2)}\n` : text + (results.length ? '\n' : '');
if (args.output) await writeAtomic(args.output, content, args.force);
else process.stdout.write(content);
}
if (require.main === module) main().catch(error => {
process.stderr.write(`hotfiles: ${error.message}\n`);
process.exitCode = 1;
});
module.exports = { main, parse, shouldUseColor };