atris 3.50.0 → 3.51.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/bin/atris.js +1 -1
- package/commands/youtube.js +208 -0
- package/package.json +1 -1
- package/scripts/det/ytnotes +1 -1
- package/scripts/det/ytrail-race.js +192 -0
package/bin/atris.js
CHANGED
|
@@ -572,7 +572,7 @@ function showHelp() {
|
|
|
572
572
|
console.log(' router - inspect ax lane outcomes and promote gated reflex overrides');
|
|
573
573
|
console.log(' sign - Co-author trailer on every commit in an atris workspace (on/off/status)');
|
|
574
574
|
console.log(' visualize - Generate a Slack/deck-ready visual from a prompt');
|
|
575
|
-
console.log(' youtube - Free local notes or 5-credit cloud process for YouTube videos');
|
|
575
|
+
console.log(' youtube - Free local notes, weekly digest, or 5-credit cloud process for YouTube videos');
|
|
576
576
|
console.log('');
|
|
577
577
|
console.log('Experiments:');
|
|
578
578
|
console.log(' experiments init [slug] - Prepare atris/experiments/ or scaffold a pack');
|
package/commands/youtube.js
CHANGED
|
@@ -26,6 +26,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
26
26
|
output('');
|
|
27
27
|
output(`Usage: ${commandName} notes <youtube-url> [engine]`);
|
|
28
28
|
output(` ${commandName} process <youtube-url> [options]`);
|
|
29
|
+
output(` ${commandName} digest [--days N]`);
|
|
29
30
|
output(` ${commandName} watch add <channel-url-or-@handle>`);
|
|
30
31
|
output(` ${commandName} watch list`);
|
|
31
32
|
output(` ${commandName} watch remove <number>`);
|
|
@@ -33,6 +34,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
33
34
|
output(` ${commandName} <youtube-url> [options]`);
|
|
34
35
|
output('');
|
|
35
36
|
output('notes = free local notes, process = 5 credits cloud knowledge');
|
|
37
|
+
output('digest = one decision page from this week\'s video briefs');
|
|
36
38
|
output('watch = subscribed channels turn into briefs without a human');
|
|
37
39
|
output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
|
|
38
40
|
output('Falls back to cloud video processing when local captions are unavailable.');
|
|
@@ -52,6 +54,8 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
52
54
|
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID`);
|
|
53
55
|
output(` ${commandName} https://www.youtube.com/watch?v=VIDEO_ID`);
|
|
54
56
|
output(` ${commandName} process https://youtu.be/VIDEO_ID --query "Key takeaways"`);
|
|
57
|
+
output(` ${commandName} digest`);
|
|
58
|
+
output(` ${commandName} digest --days 14`);
|
|
55
59
|
output(` ${commandName} watch add @veritasium`);
|
|
56
60
|
output(` ${commandName} watch tick`);
|
|
57
61
|
output('');
|
|
@@ -513,6 +517,201 @@ function fileBriefFromNotes({ cwd, url, workDir, now } = {}) {
|
|
|
513
517
|
}
|
|
514
518
|
}
|
|
515
519
|
|
|
520
|
+
const DIGEST_ENGINE_TIMEOUT_MS = 240000;
|
|
521
|
+
const DEFAULT_DIGEST_DAYS = 7;
|
|
522
|
+
|
|
523
|
+
function addUtcDays(stamp, delta) {
|
|
524
|
+
const [year, month, day] = String(stamp || '').split('-').map(Number);
|
|
525
|
+
const value = new Date(Date.UTC(year, month - 1, day + Number(delta || 0)));
|
|
526
|
+
return value.toISOString().slice(0, 10);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function parseBriefDate(text) {
|
|
530
|
+
const match = String(text || '').match(/^date:\s*(\d{4}-\d{2}-\d{2})\s*$/m);
|
|
531
|
+
return match ? match[1] : null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function isVideoBriefText(text) {
|
|
535
|
+
return /^source:\s*http/m.test(String(text || ''));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function briefTitleLine(text) {
|
|
539
|
+
const heading = firstHeading(text);
|
|
540
|
+
if (heading) return heading;
|
|
541
|
+
const first = String(text || '').split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
542
|
+
return first || '';
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function dateInDigestWindow(dateStr, now, days) {
|
|
546
|
+
if (!dateStr) return false;
|
|
547
|
+
const today = dateStamp(now);
|
|
548
|
+
const start = addUtcDays(today, -(Number(days) - 1));
|
|
549
|
+
return dateStr >= start && dateStr <= today;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function parseDigestArgs(argv = []) {
|
|
553
|
+
const args = [...argv];
|
|
554
|
+
const options = { help: false, days: DEFAULT_DIGEST_DAYS };
|
|
555
|
+
for (let i = 0; i < args.length; i++) {
|
|
556
|
+
const arg = args[i];
|
|
557
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
558
|
+
options.help = true;
|
|
559
|
+
} else if (arg === '--days') {
|
|
560
|
+
const raw = args[i + 1];
|
|
561
|
+
const value = Number(raw);
|
|
562
|
+
if (raw == null || String(raw).startsWith('--') || !Number.isInteger(value) || value <= 0) {
|
|
563
|
+
throw new Error('--days must be a positive integer');
|
|
564
|
+
}
|
|
565
|
+
options.days = value;
|
|
566
|
+
i += 1;
|
|
567
|
+
} else if (arg.startsWith('--days=')) {
|
|
568
|
+
const value = Number(arg.slice('--days='.length));
|
|
569
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
570
|
+
throw new Error('--days must be a positive integer');
|
|
571
|
+
}
|
|
572
|
+
options.days = value;
|
|
573
|
+
} else if (arg.startsWith('-')) {
|
|
574
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
575
|
+
} else {
|
|
576
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return options;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function collectVideoBriefs({ cwd, now, days } = {}) {
|
|
583
|
+
const root = cwd || process.cwd();
|
|
584
|
+
const briefsDir = path.join(root, 'atris', 'wiki', 'briefs');
|
|
585
|
+
if (!fs.existsSync(briefsDir)) return [];
|
|
586
|
+
|
|
587
|
+
const rows = [];
|
|
588
|
+
for (const name of fs.readdirSync(briefsDir).sort()) {
|
|
589
|
+
if (!name.endsWith('.md') || name.startsWith('digest-')) continue;
|
|
590
|
+
const abs = path.join(briefsDir, name);
|
|
591
|
+
let body = '';
|
|
592
|
+
try {
|
|
593
|
+
if (!fs.statSync(abs).isFile()) continue;
|
|
594
|
+
body = fs.readFileSync(abs, 'utf8');
|
|
595
|
+
} catch {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (!isVideoBriefText(body)) continue;
|
|
599
|
+
const date = parseBriefDate(body);
|
|
600
|
+
if (!dateInDigestWindow(date, now, days)) continue;
|
|
601
|
+
rows.push({
|
|
602
|
+
name,
|
|
603
|
+
relPath: `atris/wiki/briefs/${name}`,
|
|
604
|
+
title: briefTitleLine(body),
|
|
605
|
+
date,
|
|
606
|
+
body,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
rows.sort((a, b) => (a.date === b.date ? a.name.localeCompare(b.name) : a.date.localeCompare(b.date)));
|
|
610
|
+
return rows;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function buildDigestPrompt(briefs = []) {
|
|
614
|
+
const blocks = briefs.map((row) => [
|
|
615
|
+
`filename: ${row.name}`,
|
|
616
|
+
`title: ${row.title}`,
|
|
617
|
+
`path: ${row.relPath}`,
|
|
618
|
+
'',
|
|
619
|
+
row.body,
|
|
620
|
+
].join('\n'));
|
|
621
|
+
return [
|
|
622
|
+
'Turn these video briefs into one decision-focused page.',
|
|
623
|
+
'Write a heading exactly: # what this week\'s videos changed',
|
|
624
|
+
'Then 3-6 decision-shaped findings. Each finding must name the brief it came from as a path.',
|
|
625
|
+
'Then one contradictions or tensions paragraph if any exist.',
|
|
626
|
+
'Then a 3-item do next list.',
|
|
627
|
+
'Use plain prose. Do not use em dashes.',
|
|
628
|
+
'',
|
|
629
|
+
'Briefs:',
|
|
630
|
+
'',
|
|
631
|
+
blocks.join('\n\n'),
|
|
632
|
+
].join('\n');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function defaultDigestRunner(prompt, deps = {}) {
|
|
636
|
+
const spawn = deps.spawnSync || spawnSync;
|
|
637
|
+
return spawn('claude', ['-p', prompt, '--model', 'claude-haiku-4-5'], {
|
|
638
|
+
encoding: 'utf8',
|
|
639
|
+
timeout: DIGEST_ENGINE_TIMEOUT_MS,
|
|
640
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function invokeDigestEngine(prompt, deps = {}) {
|
|
645
|
+
const runner = deps.runner || ((nextPrompt) => defaultDigestRunner(nextPrompt, deps));
|
|
646
|
+
const result = runner(prompt, deps);
|
|
647
|
+
if (typeof result === 'string') return result.trim();
|
|
648
|
+
if (!result || result.error || (result.status != null && result.status !== 0)) {
|
|
649
|
+
const detail = String(result?.stderr || result?.error?.message || 'digest engine failed').trim();
|
|
650
|
+
throw new Error(detail || 'digest engine failed');
|
|
651
|
+
}
|
|
652
|
+
return String(result.stdout || '').trim();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function appendDigestJournal({ cwd, date, relDigest }) {
|
|
656
|
+
const year = date.slice(0, 4);
|
|
657
|
+
const journalPath = path.join(cwd, 'atris', 'logs', year, `${date}.md`);
|
|
658
|
+
fs.mkdirSync(path.dirname(journalPath), { recursive: true });
|
|
659
|
+
let existing = '';
|
|
660
|
+
if (fs.existsSync(journalPath)) existing = fs.readFileSync(journalPath, 'utf8');
|
|
661
|
+
const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
662
|
+
const line = `- [claimable] digest: what this week's videos changed -> ${relDigest}`;
|
|
663
|
+
fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function runYoutubeDigest(args = [], deps = {}) {
|
|
667
|
+
const output = deps.output || ((line = '') => console.log(line));
|
|
668
|
+
let options;
|
|
669
|
+
try {
|
|
670
|
+
options = parseDigestArgs(args);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
output(err.message);
|
|
673
|
+
return 2;
|
|
674
|
+
}
|
|
675
|
+
if (options.help) {
|
|
676
|
+
showYoutubeHelp(output, deps.commandName || 'atris youtube');
|
|
677
|
+
return 0;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const cwd = deps.cwd || process.cwd();
|
|
681
|
+
const now = deps.now || new Date();
|
|
682
|
+
const briefs = collectVideoBriefs({ cwd, now, days: options.days });
|
|
683
|
+
if (!briefs.length) {
|
|
684
|
+
output(`no video briefs in the last ${options.days} days`);
|
|
685
|
+
return 0;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const prompt = buildDigestPrompt(briefs);
|
|
689
|
+
let text;
|
|
690
|
+
try {
|
|
691
|
+
text = invokeDigestEngine(prompt, deps);
|
|
692
|
+
} catch (err) {
|
|
693
|
+
output(err.message || 'digest engine failed');
|
|
694
|
+
return 1;
|
|
695
|
+
}
|
|
696
|
+
if (!text) {
|
|
697
|
+
output('digest engine returned no text');
|
|
698
|
+
return 1;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const date = dateStamp(now);
|
|
702
|
+
const relDigest = `atris/wiki/briefs/digest-${date}.md`;
|
|
703
|
+
const header = [
|
|
704
|
+
`date: ${date}`,
|
|
705
|
+
`window: ${options.days} days`,
|
|
706
|
+
`sources: ${briefs.map((row) => row.relPath).join(', ')}`,
|
|
707
|
+
].join('\n');
|
|
708
|
+
fs.mkdirSync(path.join(cwd, 'atris', 'wiki', 'briefs'), { recursive: true });
|
|
709
|
+
fs.writeFileSync(path.join(cwd, relDigest), `${header}\n\n${text}\n`);
|
|
710
|
+
appendDigestJournal({ cwd, date, relDigest });
|
|
711
|
+
output(`digest filed: ${relDigest} (${briefs.length} briefs)`);
|
|
712
|
+
return 0;
|
|
713
|
+
}
|
|
714
|
+
|
|
516
715
|
function watchStatePath(cwd = process.cwd()) {
|
|
517
716
|
return path.join(cwd, '.atris', 'state', 'youtube_watch.json');
|
|
518
717
|
}
|
|
@@ -849,6 +1048,11 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
849
1048
|
if (!deps.output && !deps.spawnSync) process.exit(code);
|
|
850
1049
|
return code;
|
|
851
1050
|
}
|
|
1051
|
+
if (argv[0] === 'digest') {
|
|
1052
|
+
const code = runYoutubeDigest(argv.slice(1), { ...deps, output });
|
|
1053
|
+
if (!deps.output && !deps.runner) process.exit(code);
|
|
1054
|
+
return code;
|
|
1055
|
+
}
|
|
852
1056
|
if (argv[0] === 'watch') {
|
|
853
1057
|
const code = await watchCommand(argv.slice(1), { ...deps, output });
|
|
854
1058
|
if (!deps.output && !deps.fetcher && !deps.runner && !deps.briefFiler) process.exit(code);
|
|
@@ -874,6 +1078,10 @@ module.exports = {
|
|
|
874
1078
|
shouldRetryWithLocalTranscript,
|
|
875
1079
|
formatYoutubeResult,
|
|
876
1080
|
fileBriefFromNotes,
|
|
1081
|
+
parseDigestArgs,
|
|
1082
|
+
collectVideoBriefs,
|
|
1083
|
+
buildDigestPrompt,
|
|
1084
|
+
runYoutubeDigest,
|
|
877
1085
|
normalizeWatchChannel,
|
|
878
1086
|
channelVideosUrl,
|
|
879
1087
|
parseFlatPlaylist,
|
package/package.json
CHANGED
package/scripts/det/ytnotes
CHANGED
|
@@ -163,7 +163,7 @@ process.stdin.on("end", () => {
|
|
|
163
163
|
|
|
164
164
|
run_grok() {
|
|
165
165
|
run_with_timeout 240 grok --verbatim --no-memory --no-plan --no-subagents \
|
|
166
|
-
--disable-web-search --max-turns
|
|
166
|
+
--disable-web-search --max-turns 3 -p "$PROMPT"
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
run_haiku() {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Race graded ytnotes engines. Reports only; never edits ytnotes.
|
|
5
|
+
// Usage: node scripts/det/ytrail-race.js [url] [engines-csv]
|
|
6
|
+
// Default url: https://www.youtube.com/watch?v=Z3JyAqh4ixg
|
|
7
|
+
// Default engines: haiku,grok,cursor,atris-fast
|
|
8
|
+
|
|
9
|
+
const { spawnSync } = require('node:child_process');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_URL = 'https://www.youtube.com/watch?v=Z3JyAqh4ixg';
|
|
14
|
+
const DEFAULT_ENGINES = ['haiku', 'grok', 'cursor', 'atris-fast'];
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 300000;
|
|
16
|
+
const DEFAULT_ROOT = path.resolve(__dirname, '..', '..');
|
|
17
|
+
const EVAL_LINE = /ytrail (pass|fail) (\S+) ([\d.]+)s words=\d+ quotes=(\d+)\/(\d+) heading=(?:yes|no)/;
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const url = argv[0] || DEFAULT_URL;
|
|
21
|
+
const engines = argv[1]
|
|
22
|
+
? String(argv[1]).split(',').map((name) => name.trim()).filter(Boolean)
|
|
23
|
+
: DEFAULT_ENGINES.slice();
|
|
24
|
+
return { url, engines };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseEvalLine(text) {
|
|
28
|
+
const match = String(text || '').match(EVAL_LINE);
|
|
29
|
+
if (!match) return null;
|
|
30
|
+
return {
|
|
31
|
+
engine: match[2],
|
|
32
|
+
seconds: Number(match[3]),
|
|
33
|
+
pass: match[1] === 'pass',
|
|
34
|
+
quotesVerified: Number(match[4]),
|
|
35
|
+
quotesTotal: Number(match[5]),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function failedResult(engine, seconds) {
|
|
40
|
+
return {
|
|
41
|
+
engine,
|
|
42
|
+
seconds: Number(seconds) || 0,
|
|
43
|
+
pass: false,
|
|
44
|
+
quotesVerified: 0,
|
|
45
|
+
quotesTotal: 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createDefaultRunner(options = {}) {
|
|
50
|
+
const root = options.root || DEFAULT_ROOT;
|
|
51
|
+
const evalPath = options.evalPath || path.join(root, 'scripts', 'det', 'ytrail-eval.js');
|
|
52
|
+
const cwd = options.cwd || root;
|
|
53
|
+
const env = options.env || process.env;
|
|
54
|
+
return function defaultRunner({ url, engine, timeoutMs }) {
|
|
55
|
+
const run = spawnSync(process.execPath, [evalPath, url, engine], {
|
|
56
|
+
encoding: 'utf8',
|
|
57
|
+
cwd,
|
|
58
|
+
env,
|
|
59
|
+
timeout: timeoutMs,
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
stdout: String(run.stdout || ''),
|
|
63
|
+
stderr: String(run.stderr || ''),
|
|
64
|
+
status: run.status,
|
|
65
|
+
error: run.error || null,
|
|
66
|
+
timedOut: Boolean(run.error && run.error.code === 'ETIMEDOUT'),
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runOneEngine(engine, ctx) {
|
|
72
|
+
let run;
|
|
73
|
+
try {
|
|
74
|
+
run = ctx.runner({
|
|
75
|
+
url: ctx.url,
|
|
76
|
+
engine,
|
|
77
|
+
timeoutMs: ctx.timeoutMs,
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
return failedResult(engine, 0);
|
|
81
|
+
}
|
|
82
|
+
if (!run || run.timedOut) {
|
|
83
|
+
return failedResult(engine, ctx.timeoutMs / 1000);
|
|
84
|
+
}
|
|
85
|
+
const parsed = parseEvalLine(run.stdout);
|
|
86
|
+
if (!parsed) return failedResult(engine, 0);
|
|
87
|
+
return {
|
|
88
|
+
engine,
|
|
89
|
+
seconds: parsed.seconds,
|
|
90
|
+
pass: parsed.pass,
|
|
91
|
+
quotesVerified: parsed.quotesVerified,
|
|
92
|
+
quotesTotal: parsed.quotesTotal,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function pickWinner(results) {
|
|
97
|
+
return results
|
|
98
|
+
.filter((row) => row.pass)
|
|
99
|
+
.slice()
|
|
100
|
+
.sort((a, b) => a.seconds - b.seconds)[0] || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pad(value, width) {
|
|
104
|
+
const text = String(value);
|
|
105
|
+
return text.length >= width ? text : text + ' '.repeat(width - text.length);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatTable(results) {
|
|
109
|
+
const engineWidth = Math.max(6, ...results.map((row) => String(row.engine).length));
|
|
110
|
+
const lines = [
|
|
111
|
+
`${pad('engine', engineWidth)} seconds pass quotes`,
|
|
112
|
+
];
|
|
113
|
+
for (const row of results) {
|
|
114
|
+
lines.push(
|
|
115
|
+
`${pad(row.engine, engineWidth)} ${pad(row.seconds, 7)} ${pad(row.pass ? 'yes' : 'no', 4)} ${row.quotesVerified}/${row.quotesTotal}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return lines.join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatVerdict(winner) {
|
|
122
|
+
if (!winner) return 'race winner: none';
|
|
123
|
+
return `race winner: ${winner.engine} (${winner.seconds}s, quotes ${winner.quotesVerified}/${winner.quotesTotal})`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function writeLatest(root, report) {
|
|
127
|
+
const outDir = path.join(root, 'atris', 'benchmarks');
|
|
128
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
129
|
+
const payload = {
|
|
130
|
+
ts: report.ts,
|
|
131
|
+
url: report.url,
|
|
132
|
+
results: report.results,
|
|
133
|
+
winner: report.winner,
|
|
134
|
+
};
|
|
135
|
+
fs.writeFileSync(
|
|
136
|
+
path.join(outDir, 'ytrail-race-latest.json'),
|
|
137
|
+
`${JSON.stringify(payload, null, 2)}\n`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function runRace(options = {}) {
|
|
142
|
+
const url = options.url || DEFAULT_URL;
|
|
143
|
+
const engines = options.engines && options.engines.length
|
|
144
|
+
? options.engines.slice()
|
|
145
|
+
: DEFAULT_ENGINES.slice();
|
|
146
|
+
const timeoutMs = options.timeoutMs == null ? DEFAULT_TIMEOUT_MS : options.timeoutMs;
|
|
147
|
+
const root = options.root || DEFAULT_ROOT;
|
|
148
|
+
const runner = options.runner || createDefaultRunner({ root });
|
|
149
|
+
const log = options.log || console.log;
|
|
150
|
+
const ts = options.ts || new Date().toISOString();
|
|
151
|
+
|
|
152
|
+
const results = [];
|
|
153
|
+
for (const engine of engines) {
|
|
154
|
+
results.push(runOneEngine(engine, { url, runner, timeoutMs }));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const winner = pickWinner(results);
|
|
158
|
+
const report = { ts, url, results, winner };
|
|
159
|
+
writeLatest(root, report);
|
|
160
|
+
|
|
161
|
+
log(formatTable(results));
|
|
162
|
+
log(formatVerdict(winner));
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...report,
|
|
166
|
+
exitCode: winner ? 0 : 1,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function main(argv = process.argv.slice(2), options = {}) {
|
|
171
|
+
const parsed = parseArgs(argv);
|
|
172
|
+
const report = runRace({ ...options, ...parsed });
|
|
173
|
+
return report.exitCode;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (require.main === module) {
|
|
177
|
+
process.exit(main());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
DEFAULT_URL,
|
|
182
|
+
DEFAULT_ENGINES,
|
|
183
|
+
DEFAULT_TIMEOUT_MS,
|
|
184
|
+
parseArgs,
|
|
185
|
+
parseEvalLine,
|
|
186
|
+
createDefaultRunner,
|
|
187
|
+
pickWinner,
|
|
188
|
+
formatTable,
|
|
189
|
+
formatVerdict,
|
|
190
|
+
runRace,
|
|
191
|
+
main,
|
|
192
|
+
};
|