ktg-plugin-marketplace/plugins/ultraplan-local/examples/02-real-cli/tally.mjs
Kjell Tore Guttormsen c8146c143d feat(ultraplan-local): tally CLI baseline fixture for examples/02-real-cli (Spor B B2) [skip-docs]
Adds the runnable counterpart to examples/01-add-verbose-flag (which is
artifacts-only). The fixture is the measurement target for Spor B's
end-to-end pipeline run (B3) and Spor C's cache-prefix experiment.

Baseline:
- tally.mjs (80 lines, hand-rolled argv parser, zero deps)
- 3 flags: --json, -i/--ignore-case, --lines + --help
- Exit codes: 0 success, 1 file error, 2 invalid argv
- 10 node:test cases, all green (~2.2s wall-clock)
- Deterministic fixtures: sample.txt (foo×7, Foo×1, regex fo+×9) +
  poem.txt (--lines vs total distinction)
- REGENERATED.md skeleton (B3 fills the pipeline walk-through)

Brief preconditions verified:
- grep -c 'foo' sample.txt = 4 (>= 1)
- regex /fo+/g count = 9 (> grep count)
- Brief assumptions for B3 SC #1, #3 hold

This is the first runnable example in plugins/ultraplan-local/examples/.
Next: B3 runs /ultraresearch-local + /ultraplan-local + /ultraexecute-local
against the brief to add --regex/-r, then verifies all 10 Success Criteria.
2026-05-04 20:18:57 +02:00

80 lines
2.5 KiB
JavaScript
Executable file

#!/usr/bin/env node
import { readFileSync } from 'node:fs';
const HELP = `Usage: tally [options] <pattern> <file>
Count literal-substring occurrences of <pattern> in <file>.
Options:
-i, --ignore-case Case-insensitive matching
--lines Count lines containing pattern (not total occurrences)
--json Emit a JSON object on stdout
-h, --help Show this help and exit
Exit codes: 0=success 1=file error 2=invalid argv
`;
function fail(msg, code = 2) {
process.stderr.write(`tally: ${msg}\n`);
process.exit(code);
}
function parseArgs(argv) {
const positional = [];
const flags = { json: false, ignoreCase: false, lines: false };
for (const a of argv) {
if (a === '--json') flags.json = true;
else if (a === '-i' || a === '--ignore-case') flags.ignoreCase = true;
else if (a === '--lines') flags.lines = true;
else if (a === '-h' || a === '--help') { process.stdout.write(HELP); process.exit(0); }
else if (a.startsWith('-')) fail(`unknown flag: ${a}`);
else positional.push(a);
}
if (positional.length !== 2) fail('expected <pattern> <file>');
return { pattern: positional[0], file: positional[1], flags };
}
function countOccurrences(text, pattern, ignoreCase) {
if (pattern.length === 0) return 0;
const haystack = ignoreCase ? text.toLowerCase() : text;
const needle = ignoreCase ? pattern.toLowerCase() : pattern;
let count = 0, idx = 0;
while ((idx = haystack.indexOf(needle, idx)) !== -1) { count++; idx += needle.length; }
return count;
}
function countLines(text, pattern, ignoreCase) {
if (pattern.length === 0) return 0;
const needle = ignoreCase ? pattern.toLowerCase() : pattern;
let count = 0;
for (const line of text.split('\n')) {
const haystack = ignoreCase ? line.toLowerCase() : line;
if (haystack.includes(needle)) count++;
}
return count;
}
function main() {
const { pattern, file, flags } = parseArgs(process.argv.slice(2));
let text;
try {
text = readFileSync(file, 'utf8');
} catch (err) {
const what = err.code === 'ENOENT' ? 'file not found' : 'read error';
process.stderr.write(`tally: ${what}: ${file}\n`);
process.exit(1);
}
const count = flags.lines
? countLines(text, pattern, flags.ignoreCase)
: countOccurrences(text, pattern, flags.ignoreCase);
if (flags.json) {
process.stdout.write(JSON.stringify({
pattern, file, count,
flags: { json: flags.json, ignoreCase: flags.ignoreCase, lines: flags.lines },
}) + '\n');
} else {
process.stdout.write(count + '\n');
}
}
main();