@bilalimamoglu/sift 0.2.2 → 0.2.3
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/README.md +139 -89
- package/dist/cli.js +1132 -347
- package/dist/index.d.ts +29 -1
- package/dist/index.js +530 -47
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli.ts
|
|
3
|
+
// src/cli-app.ts
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
import { cac } from "cac";
|
|
6
6
|
|
|
@@ -375,18 +375,211 @@ function writeConfigFile(options) {
|
|
|
375
375
|
return resolved;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
// src/ui/presentation.ts
|
|
379
|
+
import pc from "picocolors";
|
|
380
|
+
function applyColor(enabled, formatter, value) {
|
|
381
|
+
return enabled ? formatter(value) : value;
|
|
382
|
+
}
|
|
383
|
+
function createPresentation(useColor) {
|
|
384
|
+
return {
|
|
385
|
+
useColor,
|
|
386
|
+
banner(_version) {
|
|
387
|
+
const mark = [
|
|
388
|
+
" \\\\ //",
|
|
389
|
+
" \\\\//",
|
|
390
|
+
" | |",
|
|
391
|
+
" o"
|
|
392
|
+
].map((line) => applyColor(useColor, pc.cyan, line)).join("\n");
|
|
393
|
+
const tagline = applyColor(useColor, pc.dim, "Trim the noise. Keep the signal.");
|
|
394
|
+
return `${mark}
|
|
395
|
+
${tagline}`;
|
|
396
|
+
},
|
|
397
|
+
welcome(text) {
|
|
398
|
+
return useColor ? `${pc.bold(pc.cyan("Welcome to sift."))} ${text}` : `Welcome to sift. ${text}`;
|
|
399
|
+
},
|
|
400
|
+
success(text) {
|
|
401
|
+
return useColor ? `${pc.green("\u2713")} ${text}` : text;
|
|
402
|
+
},
|
|
403
|
+
warning(text) {
|
|
404
|
+
return useColor ? `${pc.yellow("!")} ${text}` : text;
|
|
405
|
+
},
|
|
406
|
+
error(text) {
|
|
407
|
+
return useColor ? `${pc.red("x")} ${text}` : text;
|
|
408
|
+
},
|
|
409
|
+
info(text) {
|
|
410
|
+
return useColor ? `${pc.cyan("\u2022")} ${text}` : text;
|
|
411
|
+
},
|
|
412
|
+
note(text) {
|
|
413
|
+
return applyColor(useColor, pc.dim, text);
|
|
414
|
+
},
|
|
415
|
+
section(text) {
|
|
416
|
+
return applyColor(useColor, (value) => pc.bold(pc.yellow(value)), text);
|
|
417
|
+
},
|
|
418
|
+
labelValue(label, value) {
|
|
419
|
+
return `${applyColor(useColor, (entry) => pc.bold(pc.cyan(entry)), label)}: ${value}`;
|
|
420
|
+
},
|
|
421
|
+
command(text) {
|
|
422
|
+
return applyColor(useColor, (value) => pc.bold(pc.cyan(value)), text);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
378
427
|
// src/commands/config-setup.ts
|
|
379
428
|
import fs3 from "fs";
|
|
380
429
|
import path4 from "path";
|
|
430
|
+
import { emitKeypressEvents } from "readline";
|
|
381
431
|
import { createInterface } from "readline/promises";
|
|
382
|
-
import {
|
|
383
|
-
|
|
432
|
+
import { stderr as defaultStderr, stdin as defaultStdin2, stdout as defaultStdout } from "process";
|
|
433
|
+
|
|
434
|
+
// src/ui/terminal.ts
|
|
435
|
+
import { execFileSync } from "child_process";
|
|
436
|
+
import { clearScreenDown, cursorTo, moveCursor } from "readline";
|
|
437
|
+
import { stdin as defaultStdin } from "process";
|
|
438
|
+
function setPosixEcho(enabled) {
|
|
439
|
+
const command = enabled ? "echo" : "-echo";
|
|
440
|
+
try {
|
|
441
|
+
execFileSync("sh", ["-c", `stty ${command} < /dev/tty`], {
|
|
442
|
+
stdio: ["inherit", "inherit", "ignore"]
|
|
443
|
+
});
|
|
444
|
+
return;
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
execFileSync("stty", [command], {
|
|
449
|
+
stdio: ["inherit", "inherit", "ignore"]
|
|
450
|
+
});
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function renderSelectionBlock(args) {
|
|
455
|
+
return [
|
|
456
|
+
`${args.prompt} (use \u2191/\u2193 and Enter)`,
|
|
457
|
+
...args.options.map(
|
|
458
|
+
(option, index) => `${index === args.selectedIndex ? "\u203A" : " "} ${option}${index === args.selectedIndex ? " (selected)" : ""}`
|
|
459
|
+
)
|
|
460
|
+
];
|
|
461
|
+
}
|
|
462
|
+
async function promptSelect(args) {
|
|
463
|
+
const { input, output, prompt, options } = args;
|
|
464
|
+
const stream = output;
|
|
465
|
+
const selectedLabel = args.selectedLabel ?? prompt;
|
|
466
|
+
let index = 0;
|
|
467
|
+
let previousLineCount = 0;
|
|
468
|
+
const render = () => {
|
|
469
|
+
if (previousLineCount > 0) {
|
|
470
|
+
moveCursor(stream, 0, -previousLineCount);
|
|
471
|
+
cursorTo(stream, 0);
|
|
472
|
+
clearScreenDown(stream);
|
|
473
|
+
}
|
|
474
|
+
const lines = renderSelectionBlock({
|
|
475
|
+
prompt,
|
|
476
|
+
options,
|
|
477
|
+
selectedIndex: index
|
|
478
|
+
});
|
|
479
|
+
output.write(`${lines.join("\n")}
|
|
480
|
+
`);
|
|
481
|
+
previousLineCount = lines.length;
|
|
482
|
+
};
|
|
483
|
+
const cleanup = (selected) => {
|
|
484
|
+
if (previousLineCount > 0) {
|
|
485
|
+
moveCursor(stream, 0, -previousLineCount);
|
|
486
|
+
cursorTo(stream, 0);
|
|
487
|
+
clearScreenDown(stream);
|
|
488
|
+
}
|
|
489
|
+
if (selected) {
|
|
490
|
+
output.write(`${selectedLabel}: ${selected}
|
|
491
|
+
`);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
input.resume();
|
|
495
|
+
const wasRaw = Boolean(input.isRaw);
|
|
496
|
+
input.setRawMode?.(true);
|
|
497
|
+
render();
|
|
498
|
+
return await new Promise((resolve, reject) => {
|
|
499
|
+
const onKeypress = (_value, key) => {
|
|
500
|
+
if (key.ctrl && key.name === "c") {
|
|
501
|
+
input.off("keypress", onKeypress);
|
|
502
|
+
cleanup();
|
|
503
|
+
input.setRawMode?.(wasRaw);
|
|
504
|
+
input.pause?.();
|
|
505
|
+
reject(new Error("Aborted."));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (key.name === "up") {
|
|
509
|
+
index = index === 0 ? options.length - 1 : index - 1;
|
|
510
|
+
render();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (key.name === "down") {
|
|
514
|
+
index = (index + 1) % options.length;
|
|
515
|
+
render();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (key.name === "return" || key.name === "enter") {
|
|
519
|
+
const selected = options[index] ?? options[0] ?? "";
|
|
520
|
+
input.off("keypress", onKeypress);
|
|
521
|
+
cleanup(selected);
|
|
522
|
+
input.setRawMode?.(wasRaw);
|
|
523
|
+
input.pause?.();
|
|
524
|
+
resolve(selected);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
input.on("keypress", onKeypress);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
async function promptSecret(args) {
|
|
531
|
+
const { input, output, prompt } = args;
|
|
532
|
+
let value = "";
|
|
533
|
+
const shouldToggleEcho = process.platform !== "win32" && input === defaultStdin && Boolean(defaultStdin.isTTY);
|
|
534
|
+
output.write(prompt);
|
|
535
|
+
input.resume();
|
|
536
|
+
const wasRaw = Boolean(input.isRaw);
|
|
537
|
+
input.setRawMode?.(true);
|
|
538
|
+
if (shouldToggleEcho) {
|
|
539
|
+
setPosixEcho(false);
|
|
540
|
+
}
|
|
541
|
+
return await new Promise((resolve, reject) => {
|
|
542
|
+
const restoreInputState = () => {
|
|
543
|
+
input.setRawMode?.(wasRaw);
|
|
544
|
+
input.pause?.();
|
|
545
|
+
if (shouldToggleEcho) {
|
|
546
|
+
setPosixEcho(true);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
const onKeypress = (chunk, key) => {
|
|
550
|
+
if (key.ctrl && key.name === "c") {
|
|
551
|
+
input.off("keypress", onKeypress);
|
|
552
|
+
restoreInputState();
|
|
553
|
+
output.write("\n");
|
|
554
|
+
reject(new Error("Aborted."));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (key.name === "return" || key.name === "enter") {
|
|
558
|
+
input.off("keypress", onKeypress);
|
|
559
|
+
restoreInputState();
|
|
560
|
+
output.write("\n");
|
|
561
|
+
resolve(value);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (key.name === "backspace" || key.name === "delete") {
|
|
565
|
+
value = value.slice(0, -1);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (!key.ctrl && chunk.length > 0) {
|
|
569
|
+
value += chunk;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
input.on("keypress", onKeypress);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/commands/config-setup.ts
|
|
384
577
|
function createTerminalIO() {
|
|
385
578
|
let rl;
|
|
386
579
|
function getInterface() {
|
|
387
580
|
if (!rl) {
|
|
388
581
|
rl = createInterface({
|
|
389
|
-
input:
|
|
582
|
+
input: defaultStdin2,
|
|
390
583
|
output: defaultStdout,
|
|
391
584
|
terminal: true
|
|
392
585
|
});
|
|
@@ -394,69 +587,31 @@ function createTerminalIO() {
|
|
|
394
587
|
return rl;
|
|
395
588
|
}
|
|
396
589
|
async function select(prompt, options) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
clearLine(output, 0);
|
|
413
|
-
output.write(`${optionIndex === index ? "\u203A" : " "} ${options[optionIndex]}
|
|
414
|
-
`);
|
|
415
|
-
}
|
|
416
|
-
moveCursor(output, 0, -lineCount);
|
|
417
|
-
};
|
|
418
|
-
render();
|
|
419
|
-
return await new Promise((resolve, reject) => {
|
|
420
|
-
const onKeypress = (_value, key) => {
|
|
421
|
-
if (key.ctrl && key.name === "c") {
|
|
422
|
-
cleanup();
|
|
423
|
-
reject(new Error("Aborted."));
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
if (key.name === "up") {
|
|
427
|
-
index = index === 0 ? options.length - 1 : index - 1;
|
|
428
|
-
render();
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
if (key.name === "down") {
|
|
432
|
-
index = (index + 1) % options.length;
|
|
433
|
-
render();
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
if (key.name === "return" || key.name === "enter") {
|
|
437
|
-
const selected = options[index] ?? options[0];
|
|
438
|
-
cleanup();
|
|
439
|
-
resolve(selected ?? "OpenAI");
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
const cleanup = () => {
|
|
443
|
-
input.off("keypress", onKeypress);
|
|
444
|
-
moveCursor(output, 0, lineCount);
|
|
445
|
-
cursorTo(output, 0);
|
|
446
|
-
clearLine(output, 0);
|
|
447
|
-
output.write("\n");
|
|
448
|
-
input.setRawMode?.(Boolean(wasRaw));
|
|
449
|
-
};
|
|
450
|
-
input.on("keypress", onKeypress);
|
|
590
|
+
emitKeypressEvents(defaultStdin2);
|
|
591
|
+
return await promptSelect({
|
|
592
|
+
input: defaultStdin2,
|
|
593
|
+
output: defaultStdout,
|
|
594
|
+
prompt,
|
|
595
|
+
options,
|
|
596
|
+
selectedLabel: "Provider"
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
async function secret(prompt) {
|
|
600
|
+
emitKeypressEvents(defaultStdin2);
|
|
601
|
+
return await promptSecret({
|
|
602
|
+
input: defaultStdin2,
|
|
603
|
+
output: defaultStdout,
|
|
604
|
+
prompt
|
|
451
605
|
});
|
|
452
606
|
}
|
|
453
607
|
return {
|
|
454
|
-
stdinIsTTY: Boolean(
|
|
608
|
+
stdinIsTTY: Boolean(defaultStdin2.isTTY),
|
|
455
609
|
stdoutIsTTY: Boolean(defaultStdout.isTTY),
|
|
456
610
|
ask(prompt) {
|
|
457
611
|
return getInterface().question(prompt);
|
|
458
612
|
},
|
|
459
613
|
select,
|
|
614
|
+
secret,
|
|
460
615
|
write(message) {
|
|
461
616
|
defaultStdout.write(message);
|
|
462
617
|
},
|
|
@@ -483,9 +638,12 @@ function buildOpenAISetupConfig(apiKey) {
|
|
|
483
638
|
}
|
|
484
639
|
};
|
|
485
640
|
}
|
|
641
|
+
function getSetupPresenter(io) {
|
|
642
|
+
return createPresentation(io.stdoutIsTTY);
|
|
643
|
+
}
|
|
486
644
|
async function promptForProvider(io) {
|
|
487
645
|
if (io.select) {
|
|
488
|
-
const choice = await io.select("Select provider", ["OpenAI"]);
|
|
646
|
+
const choice = await io.select("Select provider for this machine", ["OpenAI"]);
|
|
489
647
|
if (choice === "OpenAI") {
|
|
490
648
|
return "openai";
|
|
491
649
|
}
|
|
@@ -500,7 +658,7 @@ async function promptForProvider(io) {
|
|
|
500
658
|
}
|
|
501
659
|
async function promptForApiKey(io) {
|
|
502
660
|
while (true) {
|
|
503
|
-
const answer = (await io.ask("Enter your OpenAI API key: ")).trim();
|
|
661
|
+
const answer = (await (io.secret ? io.secret("Enter your OpenAI API key (input hidden): ") : io.ask("Enter your OpenAI API key: "))).trim();
|
|
504
662
|
if (answer.length > 0) {
|
|
505
663
|
return answer;
|
|
506
664
|
}
|
|
@@ -521,9 +679,41 @@ async function promptForOverwrite(io, targetPath) {
|
|
|
521
679
|
io.error("Please answer y or n.\n");
|
|
522
680
|
}
|
|
523
681
|
}
|
|
682
|
+
function writeSetupSuccess(io, writtenPath) {
|
|
683
|
+
const ui = getSetupPresenter(io);
|
|
684
|
+
io.write(`
|
|
685
|
+
${ui.success("You're set.")}
|
|
686
|
+
`);
|
|
687
|
+
io.write(`${ui.info(`Machine-wide config: ${writtenPath}`)}
|
|
688
|
+
`);
|
|
689
|
+
io.write(`${ui.note("sift is ready to use from any terminal on this machine.")}
|
|
690
|
+
`);
|
|
691
|
+
io.write(
|
|
692
|
+
`${ui.note("A repo-local sift.config.yaml can still override it when a project needs its own settings.")}
|
|
693
|
+
`
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
function writeOverrideWarning(io, activeConfigPath) {
|
|
697
|
+
const ui = getSetupPresenter(io);
|
|
698
|
+
io.write(
|
|
699
|
+
`${ui.warning(`Heads-up: ${activeConfigPath} currently overrides this machine-wide config in this directory.`)}
|
|
700
|
+
`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
function writeNextSteps(io) {
|
|
704
|
+
const ui = getSetupPresenter(io);
|
|
705
|
+
io.write(`
|
|
706
|
+
${ui.section("Try next")}
|
|
707
|
+
`);
|
|
708
|
+
io.write(` ${ui.command("sift doctor")}
|
|
709
|
+
`);
|
|
710
|
+
io.write(` ${ui.command("sift exec --preset test-status -- pytest")}
|
|
711
|
+
`);
|
|
712
|
+
}
|
|
524
713
|
async function configSetup(options = {}) {
|
|
525
714
|
void options.global;
|
|
526
715
|
const io = options.io ?? createTerminalIO();
|
|
716
|
+
const ui = getSetupPresenter(io);
|
|
527
717
|
try {
|
|
528
718
|
if (!io.stdinIsTTY || !io.stdoutIsTTY) {
|
|
529
719
|
io.error(
|
|
@@ -531,24 +721,31 @@ async function configSetup(options = {}) {
|
|
|
531
721
|
);
|
|
532
722
|
return 1;
|
|
533
723
|
}
|
|
724
|
+
io.write(`${ui.welcome("Let's keep the expensive model for the interesting bits.")}
|
|
725
|
+
`);
|
|
534
726
|
const resolvedPath = resolveSetupPath(options.targetPath);
|
|
535
727
|
if (fs3.existsSync(resolvedPath)) {
|
|
536
728
|
const shouldOverwrite = await promptForOverwrite(io, resolvedPath);
|
|
537
729
|
if (!shouldOverwrite) {
|
|
538
|
-
io.write("Aborted
|
|
730
|
+
io.write(`${ui.note("Aborted.")}
|
|
731
|
+
`);
|
|
539
732
|
return 1;
|
|
540
733
|
}
|
|
541
734
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
io.write("
|
|
548
|
-
|
|
549
|
-
io.write("Default base URL: https://api.openai.com/v1\n");
|
|
735
|
+
await promptForProvider(io);
|
|
736
|
+
io.write(`${ui.info("Using OpenAI defaults for your first run.")}
|
|
737
|
+
`);
|
|
738
|
+
io.write(`${ui.labelValue("Default model", "gpt-5-nano")}
|
|
739
|
+
`);
|
|
740
|
+
io.write(`${ui.labelValue("Default base URL", "https://api.openai.com/v1")}
|
|
741
|
+
`);
|
|
550
742
|
io.write(
|
|
551
|
-
|
|
743
|
+
`${ui.note(`Want to switch providers or tweak defaults later? Edit ${resolvedPath}.`)}
|
|
744
|
+
`
|
|
745
|
+
);
|
|
746
|
+
io.write(
|
|
747
|
+
`${ui.note("Want to inspect the active values first? Run 'sift config show --show-secrets'.")}
|
|
748
|
+
`
|
|
552
749
|
);
|
|
553
750
|
const apiKey = await promptForApiKey(io);
|
|
554
751
|
const config = buildOpenAISetupConfig(apiKey);
|
|
@@ -557,21 +754,12 @@ async function configSetup(options = {}) {
|
|
|
557
754
|
config,
|
|
558
755
|
overwrite: true
|
|
559
756
|
});
|
|
560
|
-
io
|
|
561
|
-
`);
|
|
562
|
-
io.write(
|
|
563
|
-
"This is your machine-wide default config. Repo-local sift.config.yaml can still override it later.\n"
|
|
564
|
-
);
|
|
757
|
+
writeSetupSuccess(io, writtenPath);
|
|
565
758
|
const activeConfigPath = findConfigPath();
|
|
566
759
|
if (activeConfigPath && path4.resolve(activeConfigPath) !== path4.resolve(writtenPath)) {
|
|
567
|
-
io
|
|
568
|
-
`Note: ${activeConfigPath} currently overrides this machine-wide config in the current directory.
|
|
569
|
-
`
|
|
570
|
-
);
|
|
760
|
+
writeOverrideWarning(io, activeConfigPath);
|
|
571
761
|
}
|
|
572
|
-
io
|
|
573
|
-
io.write(" sift doctor\n");
|
|
574
|
-
io.write(" sift exec --preset test-status -- pytest\n");
|
|
762
|
+
writeNextSteps(io);
|
|
575
763
|
return 0;
|
|
576
764
|
} finally {
|
|
577
765
|
io.close?.();
|
|
@@ -602,8 +790,16 @@ function configInit(targetPath, global = false) {
|
|
|
602
790
|
targetPath,
|
|
603
791
|
global
|
|
604
792
|
});
|
|
605
|
-
process.stdout.
|
|
793
|
+
if (!process.stdout.isTTY) {
|
|
794
|
+
process.stdout.write(`${path5}
|
|
606
795
|
`);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const ui = createPresentation(true);
|
|
799
|
+
process.stdout.write(
|
|
800
|
+
`${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path5}`)}
|
|
801
|
+
`
|
|
802
|
+
);
|
|
607
803
|
}
|
|
608
804
|
function configShow(configPath, showSecrets = false) {
|
|
609
805
|
const config = resolveConfig({
|
|
@@ -620,25 +816,32 @@ function configValidate(configPath) {
|
|
|
620
816
|
env: process.env
|
|
621
817
|
});
|
|
622
818
|
const resolvedPath = findConfigPath(configPath);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
819
|
+
const message = `Resolved config is valid${resolvedPath ? ` (${resolvedPath})` : " (using defaults)"}.`;
|
|
820
|
+
if (!process.stdout.isTTY) {
|
|
821
|
+
process.stdout.write(`${message}
|
|
822
|
+
`);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const ui = createPresentation(true);
|
|
826
|
+
process.stdout.write(`${ui.success(message)}
|
|
827
|
+
`);
|
|
627
828
|
}
|
|
628
829
|
|
|
629
830
|
// src/commands/doctor.ts
|
|
630
831
|
function runDoctor(config, configPath) {
|
|
832
|
+
const ui = createPresentation(Boolean(process.stdout.isTTY));
|
|
631
833
|
const lines = [
|
|
632
834
|
"sift doctor",
|
|
835
|
+
"A quick check for your local setup.",
|
|
633
836
|
"mode: local config completeness check",
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
837
|
+
ui.labelValue("configPath", configPath ?? "(defaults only)"),
|
|
838
|
+
ui.labelValue("provider", config.provider.provider),
|
|
839
|
+
ui.labelValue("model", config.provider.model),
|
|
840
|
+
ui.labelValue("baseUrl", config.provider.baseUrl),
|
|
841
|
+
ui.labelValue("apiKey", config.provider.apiKey ? "set" : "not set"),
|
|
842
|
+
ui.labelValue("maxCaptureChars", String(config.input.maxCaptureChars)),
|
|
843
|
+
ui.labelValue("maxInputChars", String(config.input.maxInputChars)),
|
|
844
|
+
ui.labelValue("rawFallback", String(config.runtime.rawFallback))
|
|
642
845
|
];
|
|
643
846
|
process.stdout.write(`${lines.join("\n")}
|
|
644
847
|
`);
|
|
@@ -659,8 +862,16 @@ function runDoctor(config, configPath) {
|
|
|
659
862
|
);
|
|
660
863
|
}
|
|
661
864
|
if (problems.length > 0) {
|
|
662
|
-
process.stderr.
|
|
865
|
+
if (process.stderr.isTTY) {
|
|
866
|
+
const errorUi = createPresentation(true);
|
|
867
|
+
process.stderr.write(
|
|
868
|
+
`${problems.map((problem) => errorUi.error(problem)).join("\n")}
|
|
869
|
+
`
|
|
870
|
+
);
|
|
871
|
+
} else {
|
|
872
|
+
process.stderr.write(`${problems.join("\n")}
|
|
663
873
|
`);
|
|
874
|
+
}
|
|
664
875
|
return 1;
|
|
665
876
|
}
|
|
666
877
|
return 0;
|
|
@@ -689,7 +900,7 @@ function showPreset(config, name, includeInternal = false) {
|
|
|
689
900
|
// src/core/exec.ts
|
|
690
901
|
import { spawn } from "child_process";
|
|
691
902
|
import { constants as osConstants } from "os";
|
|
692
|
-
import
|
|
903
|
+
import pc3 from "picocolors";
|
|
693
904
|
|
|
694
905
|
// src/core/gate.ts
|
|
695
906
|
var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
|
|
@@ -738,8 +949,31 @@ function evaluateGate(args) {
|
|
|
738
949
|
return { shouldFail: false };
|
|
739
950
|
}
|
|
740
951
|
|
|
952
|
+
// src/core/insufficient.ts
|
|
953
|
+
function isInsufficientSignalOutput(output) {
|
|
954
|
+
const trimmed = output.trim();
|
|
955
|
+
return trimmed === INSUFFICIENT_SIGNAL_TEXT || trimmed.startsWith(`${INSUFFICIENT_SIGNAL_TEXT}
|
|
956
|
+
Hint:`);
|
|
957
|
+
}
|
|
958
|
+
function buildInsufficientSignalOutput(input) {
|
|
959
|
+
let hint;
|
|
960
|
+
if (input.originalLength === 0) {
|
|
961
|
+
hint = "Hint: no command output was captured.";
|
|
962
|
+
} else if (input.truncatedApplied) {
|
|
963
|
+
hint = "Hint: captured output was truncated before a clear summary was found.";
|
|
964
|
+
} else if (input.presetName === "test-status" && input.exitCode === 0) {
|
|
965
|
+
hint = "Hint: command succeeded, but no recognizable test summary was found.";
|
|
966
|
+
} else if (input.presetName === "test-status" && typeof input.exitCode === "number") {
|
|
967
|
+
hint = "Hint: command failed, but the captured output did not include a recognizable test summary.";
|
|
968
|
+
} else {
|
|
969
|
+
hint = "Hint: the captured output did not contain a clear answer for this preset.";
|
|
970
|
+
}
|
|
971
|
+
return `${INSUFFICIENT_SIGNAL_TEXT}
|
|
972
|
+
${hint}`;
|
|
973
|
+
}
|
|
974
|
+
|
|
741
975
|
// src/core/run.ts
|
|
742
|
-
import
|
|
976
|
+
import pc2 from "picocolors";
|
|
743
977
|
|
|
744
978
|
// src/providers/systemInstruction.ts
|
|
745
979
|
var REDUCTION_SYSTEM_INSTRUCTION = "You reduce noisy command output into compact answers for agents and automation.";
|
|
@@ -815,7 +1049,7 @@ var OpenAIProvider = class {
|
|
|
815
1049
|
if (!text) {
|
|
816
1050
|
throw new Error("Provider returned an empty response");
|
|
817
1051
|
}
|
|
818
|
-
|
|
1052
|
+
const result = {
|
|
819
1053
|
text,
|
|
820
1054
|
usage: data?.usage ? {
|
|
821
1055
|
inputTokens: data.usage.input_tokens,
|
|
@@ -824,13 +1058,14 @@ var OpenAIProvider = class {
|
|
|
824
1058
|
} : void 0,
|
|
825
1059
|
raw: data
|
|
826
1060
|
};
|
|
1061
|
+
clearTimeout(timeout);
|
|
1062
|
+
return result;
|
|
827
1063
|
} catch (error) {
|
|
1064
|
+
clearTimeout(timeout);
|
|
828
1065
|
if (error.name === "AbortError") {
|
|
829
1066
|
throw new Error("Provider request timed out");
|
|
830
1067
|
}
|
|
831
1068
|
throw error;
|
|
832
|
-
} finally {
|
|
833
|
-
clearTimeout(timeout);
|
|
834
1069
|
}
|
|
835
1070
|
}
|
|
836
1071
|
};
|
|
@@ -912,7 +1147,7 @@ var OpenAICompatibleProvider = class {
|
|
|
912
1147
|
if (!text.trim()) {
|
|
913
1148
|
throw new Error("Provider returned an empty response");
|
|
914
1149
|
}
|
|
915
|
-
|
|
1150
|
+
const result = {
|
|
916
1151
|
text,
|
|
917
1152
|
usage: data?.usage ? {
|
|
918
1153
|
inputTokens: data.usage.prompt_tokens,
|
|
@@ -921,13 +1156,14 @@ var OpenAICompatibleProvider = class {
|
|
|
921
1156
|
} : void 0,
|
|
922
1157
|
raw: data
|
|
923
1158
|
};
|
|
1159
|
+
clearTimeout(timeout);
|
|
1160
|
+
return result;
|
|
924
1161
|
} catch (error) {
|
|
1162
|
+
clearTimeout(timeout);
|
|
925
1163
|
if (error.name === "AbortError") {
|
|
926
1164
|
throw new Error("Provider request timed out");
|
|
927
1165
|
}
|
|
928
1166
|
throw error;
|
|
929
|
-
} finally {
|
|
930
|
-
clearTimeout(timeout);
|
|
931
1167
|
}
|
|
932
1168
|
}
|
|
933
1169
|
};
|
|
@@ -1132,6 +1368,19 @@ function buildPrompt(args) {
|
|
|
1132
1368
|
policyName: args.policyName,
|
|
1133
1369
|
outputContract: args.outputContract
|
|
1134
1370
|
});
|
|
1371
|
+
const detailRules = args.policyName === "test-status" && args.detail === "focused" ? [
|
|
1372
|
+
"Use a focused failure view.",
|
|
1373
|
+
"When the output clearly maps failures to specific tests or modules, group them by dominant error type first.",
|
|
1374
|
+
"Within each error group, prefer compact bullets in the form '- test-or-module -> dominant reason'.",
|
|
1375
|
+
"Cap focused entries at 6 per error group and end with '- and N more failing modules' if more clear mappings are visible.",
|
|
1376
|
+
"If per-test or per-module mapping is unclear, fall back to grouped root causes instead of guessing."
|
|
1377
|
+
] : args.policyName === "test-status" && args.detail === "verbose" ? [
|
|
1378
|
+
"Use a verbose failure view.",
|
|
1379
|
+
"When the output clearly maps failures to specific tests or modules, list each visible failing test or module on its own line in the form '- test-or-module -> normalized reason'.",
|
|
1380
|
+
"Preserve the original file or module order when the mapping is visible.",
|
|
1381
|
+
"Prefer concrete normalized reasons such as missing modules or assertion failures over traceback plumbing.",
|
|
1382
|
+
"If per-test or per-module mapping is unclear, fall back to the focused grouped-cause view instead of guessing."
|
|
1383
|
+
] : [];
|
|
1135
1384
|
const prompt = [
|
|
1136
1385
|
"You are Sift, a CLI output reduction assistant for downstream agents and automation.",
|
|
1137
1386
|
"Hard rules:",
|
|
@@ -1139,6 +1388,7 @@ function buildPrompt(args) {
|
|
|
1139
1388
|
"",
|
|
1140
1389
|
`Task policy: ${policy.name}`,
|
|
1141
1390
|
...policy.taskRules.map((rule) => `- ${rule}`),
|
|
1391
|
+
...detailRules.map((rule) => `- ${rule}`),
|
|
1142
1392
|
...policy.outputContract ? ["", `Output contract: ${policy.outputContract}`] : [],
|
|
1143
1393
|
"",
|
|
1144
1394
|
`Question: ${args.question}`,
|
|
@@ -1251,6 +1501,410 @@ function inferPackage(line) {
|
|
|
1251
1501
|
function inferRemediation(pkg2) {
|
|
1252
1502
|
return `Upgrade ${pkg2} to a patched version.`;
|
|
1253
1503
|
}
|
|
1504
|
+
function getCount(input, label) {
|
|
1505
|
+
const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
|
|
1506
|
+
const lastMatch = matches.at(-1);
|
|
1507
|
+
return lastMatch ? Number(lastMatch[1]) : 0;
|
|
1508
|
+
}
|
|
1509
|
+
function formatCount(count, singular, plural = `${singular}s`) {
|
|
1510
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
1511
|
+
}
|
|
1512
|
+
function countPattern(input, matcher) {
|
|
1513
|
+
return [...input.matchAll(matcher)].length;
|
|
1514
|
+
}
|
|
1515
|
+
function collectUniqueMatches(input, matcher, limit = 6) {
|
|
1516
|
+
const values = [];
|
|
1517
|
+
for (const match of input.matchAll(matcher)) {
|
|
1518
|
+
const candidate = match[1]?.trim();
|
|
1519
|
+
if (!candidate || values.includes(candidate)) {
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
values.push(candidate);
|
|
1523
|
+
if (values.length >= limit) {
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return values;
|
|
1528
|
+
}
|
|
1529
|
+
function cleanFailureLabel(label) {
|
|
1530
|
+
return label.trim().replace(/^['"]|['"]$/g, "");
|
|
1531
|
+
}
|
|
1532
|
+
function isLowValueInternalReason(normalized) {
|
|
1533
|
+
return /^Hint:\s+make sure your test modules\/packages have valid Python names\.?$/i.test(
|
|
1534
|
+
normalized
|
|
1535
|
+
) || /^Traceback\b/i.test(normalized) || /^return _bootstrap\._gcd_import/i.test(normalized) || /(?:^|[/\\])(?:site-packages[/\\])?_pytest(?:[/\\]|$)/i.test(normalized) || /(?:^|[/\\])importlib[/\\]__init__\.py:\d+:\s+in\s+import_module\b/i.test(
|
|
1536
|
+
normalized
|
|
1537
|
+
) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
|
|
1538
|
+
}
|
|
1539
|
+
function scoreFailureReason(reason) {
|
|
1540
|
+
if (reason.startsWith("missing module:")) {
|
|
1541
|
+
return 5;
|
|
1542
|
+
}
|
|
1543
|
+
if (reason.startsWith("assertion failed:")) {
|
|
1544
|
+
return 4;
|
|
1545
|
+
}
|
|
1546
|
+
if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
|
|
1547
|
+
return 3;
|
|
1548
|
+
}
|
|
1549
|
+
if (reason === "import error during collection") {
|
|
1550
|
+
return 2;
|
|
1551
|
+
}
|
|
1552
|
+
return 1;
|
|
1553
|
+
}
|
|
1554
|
+
function classifyFailureReason(line, options) {
|
|
1555
|
+
const normalized = line.trim().replace(/^[A-Z]\s+/, "");
|
|
1556
|
+
if (normalized.length === 0) {
|
|
1557
|
+
return null;
|
|
1558
|
+
}
|
|
1559
|
+
if (isLowValueInternalReason(normalized)) {
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
const pythonMissingModule = normalized.match(
|
|
1563
|
+
/ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
|
|
1564
|
+
);
|
|
1565
|
+
if (pythonMissingModule) {
|
|
1566
|
+
return {
|
|
1567
|
+
reason: `missing module: ${pythonMissingModule[1]}`,
|
|
1568
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
const nodeMissingModule = normalized.match(/Cannot find module ['"]([^'"]+)['"]/i);
|
|
1572
|
+
if (nodeMissingModule) {
|
|
1573
|
+
return {
|
|
1574
|
+
reason: `missing module: ${nodeMissingModule[1]}`,
|
|
1575
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
|
|
1579
|
+
if (assertionFailure) {
|
|
1580
|
+
return {
|
|
1581
|
+
reason: `assertion failed: ${assertionFailure[1]}`.slice(0, 120),
|
|
1582
|
+
group: "assertion failures"
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
|
|
1586
|
+
if (genericError) {
|
|
1587
|
+
const errorType = genericError[1];
|
|
1588
|
+
return {
|
|
1589
|
+
reason: `${errorType}: ${genericError[2]}`.slice(0, 120),
|
|
1590
|
+
group: options.duringCollection && errorType === "ImportError" ? "import/dependency errors during collection" : `${errorType} failures`
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
if (/ImportError while importing test module/i.test(normalized)) {
|
|
1594
|
+
return {
|
|
1595
|
+
reason: "import error during collection",
|
|
1596
|
+
group: "import/dependency errors during collection"
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
if (!/[A-Za-z]/.test(normalized)) {
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
return {
|
|
1603
|
+
reason: normalized.slice(0, 120),
|
|
1604
|
+
group: options.duringCollection ? "collection/import errors" : "other failures"
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
function pushFocusedFailureItem(items, candidate) {
|
|
1608
|
+
if (items.some((item) => item.label === candidate.label && item.reason === candidate.reason)) {
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
items.push(candidate);
|
|
1612
|
+
}
|
|
1613
|
+
function chooseStrongestFailureItems(items) {
|
|
1614
|
+
const strongest = /* @__PURE__ */ new Map();
|
|
1615
|
+
const order = [];
|
|
1616
|
+
for (const item of items) {
|
|
1617
|
+
const existing = strongest.get(item.label);
|
|
1618
|
+
if (!existing) {
|
|
1619
|
+
strongest.set(item.label, item);
|
|
1620
|
+
order.push(item.label);
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
if (scoreFailureReason(item.reason) > scoreFailureReason(existing.reason)) {
|
|
1624
|
+
strongest.set(item.label, item);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return order.map((label) => strongest.get(label));
|
|
1628
|
+
}
|
|
1629
|
+
function collectCollectionFailureItems(input) {
|
|
1630
|
+
const items = [];
|
|
1631
|
+
const lines = input.split("\n");
|
|
1632
|
+
let currentLabel = null;
|
|
1633
|
+
let pendingGenericReason = null;
|
|
1634
|
+
for (const line of lines) {
|
|
1635
|
+
const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
|
|
1636
|
+
if (collecting) {
|
|
1637
|
+
if (currentLabel && pendingGenericReason) {
|
|
1638
|
+
pushFocusedFailureItem(
|
|
1639
|
+
items,
|
|
1640
|
+
{
|
|
1641
|
+
label: currentLabel,
|
|
1642
|
+
reason: pendingGenericReason.reason,
|
|
1643
|
+
group: pendingGenericReason.group
|
|
1644
|
+
}
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
currentLabel = cleanFailureLabel(collecting[1]);
|
|
1648
|
+
pendingGenericReason = null;
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
if (!currentLabel) {
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
const classification = classifyFailureReason(line, {
|
|
1655
|
+
duringCollection: true
|
|
1656
|
+
});
|
|
1657
|
+
if (!classification) {
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
if (classification.reason === "import error during collection") {
|
|
1661
|
+
pendingGenericReason = classification;
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
pushFocusedFailureItem(
|
|
1665
|
+
items,
|
|
1666
|
+
{
|
|
1667
|
+
label: currentLabel,
|
|
1668
|
+
reason: classification.reason,
|
|
1669
|
+
group: classification.group
|
|
1670
|
+
}
|
|
1671
|
+
);
|
|
1672
|
+
currentLabel = null;
|
|
1673
|
+
pendingGenericReason = null;
|
|
1674
|
+
}
|
|
1675
|
+
if (currentLabel && pendingGenericReason) {
|
|
1676
|
+
pushFocusedFailureItem(
|
|
1677
|
+
items,
|
|
1678
|
+
{
|
|
1679
|
+
label: currentLabel,
|
|
1680
|
+
reason: pendingGenericReason.reason,
|
|
1681
|
+
group: pendingGenericReason.group
|
|
1682
|
+
}
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
return items;
|
|
1686
|
+
}
|
|
1687
|
+
function collectInlineFailureItems(input) {
|
|
1688
|
+
const items = [];
|
|
1689
|
+
for (const line of input.split("\n")) {
|
|
1690
|
+
const inlineFailure = line.match(/^(FAILED|ERROR)\s+(.+?)\s+-\s+(.+)$/);
|
|
1691
|
+
if (!inlineFailure) {
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
const classification = classifyFailureReason(inlineFailure[3], {
|
|
1695
|
+
duringCollection: false
|
|
1696
|
+
});
|
|
1697
|
+
if (!classification) {
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
pushFocusedFailureItem(
|
|
1701
|
+
items,
|
|
1702
|
+
{
|
|
1703
|
+
label: cleanFailureLabel(inlineFailure[2]),
|
|
1704
|
+
reason: classification.reason,
|
|
1705
|
+
group: classification.group
|
|
1706
|
+
}
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
return items;
|
|
1710
|
+
}
|
|
1711
|
+
function formatFocusedFailureGroups(args) {
|
|
1712
|
+
const maxGroups = args.maxGroups ?? 3;
|
|
1713
|
+
const maxPerGroup = args.maxPerGroup ?? 6;
|
|
1714
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1715
|
+
for (const item of args.items) {
|
|
1716
|
+
const entries = grouped.get(item.group) ?? [];
|
|
1717
|
+
entries.push(item);
|
|
1718
|
+
grouped.set(item.group, entries);
|
|
1719
|
+
}
|
|
1720
|
+
const lines = [];
|
|
1721
|
+
const visibleGroups = [...grouped.entries()].slice(0, maxGroups);
|
|
1722
|
+
for (const [group, entries] of visibleGroups) {
|
|
1723
|
+
lines.push(`- ${group}`);
|
|
1724
|
+
for (const item of entries.slice(0, maxPerGroup)) {
|
|
1725
|
+
lines.push(` - ${item.label} -> ${item.reason}`);
|
|
1726
|
+
}
|
|
1727
|
+
const remaining = entries.length - Math.min(entries.length, maxPerGroup);
|
|
1728
|
+
if (remaining > 0) {
|
|
1729
|
+
lines.push(` - and ${remaining} more failing ${args.remainderLabel}`);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
const hiddenGroups = grouped.size - visibleGroups.length;
|
|
1733
|
+
if (hiddenGroups > 0) {
|
|
1734
|
+
lines.push(`- and ${hiddenGroups} more error group${hiddenGroups === 1 ? "" : "s"}`);
|
|
1735
|
+
}
|
|
1736
|
+
return lines;
|
|
1737
|
+
}
|
|
1738
|
+
function formatVerboseFailureItems(args) {
|
|
1739
|
+
return chooseStrongestFailureItems(args.items).map(
|
|
1740
|
+
(item) => `- ${item.label} -> ${item.reason}`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
function summarizeRepeatedTestCauses(input, options) {
|
|
1744
|
+
const pythonMissingModules = collectUniqueMatches(
|
|
1745
|
+
input,
|
|
1746
|
+
/ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
|
|
1747
|
+
);
|
|
1748
|
+
const nodeMissingModules = collectUniqueMatches(
|
|
1749
|
+
input,
|
|
1750
|
+
/Cannot find module ['"]([^'"]+)['"]/gi
|
|
1751
|
+
);
|
|
1752
|
+
const missingModules = [...pythonMissingModules];
|
|
1753
|
+
for (const moduleName of nodeMissingModules) {
|
|
1754
|
+
if (!missingModules.includes(moduleName)) {
|
|
1755
|
+
missingModules.push(moduleName);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
const missingModuleHits = countPattern(
|
|
1759
|
+
input,
|
|
1760
|
+
/ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
|
|
1761
|
+
) + countPattern(input, /Cannot find module ['"]([^'"]+)['"]/gi);
|
|
1762
|
+
const importCollectionHits = countPattern(input, /ImportError while importing test module/gi) + countPattern(input, /^\s*_+\s+ERROR collecting\b/gim);
|
|
1763
|
+
const genericErrorTypes = collectUniqueMatches(
|
|
1764
|
+
input,
|
|
1765
|
+
/\b((?:Assertion|Import|Type|Value|Runtime|Reference|Key|Attribute)[A-Za-z]*Error)\b/gi,
|
|
1766
|
+
4
|
|
1767
|
+
);
|
|
1768
|
+
const bullets = [];
|
|
1769
|
+
if (options.duringCollection && (importCollectionHits >= 2 || missingModuleHits >= 2) || !options.duringCollection && missingModuleHits >= 2) {
|
|
1770
|
+
bullets.push(
|
|
1771
|
+
options.duringCollection ? "- Most failures are import/dependency errors during test collection." : "- Most failures are import/dependency errors."
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
if (missingModules.length > 1) {
|
|
1775
|
+
bullets.push(`- Missing modules include ${missingModules.join(", ")}.`);
|
|
1776
|
+
} else if (missingModules.length === 1 && missingModuleHits >= 2) {
|
|
1777
|
+
bullets.push(`- Missing module repeated across failures: ${missingModules[0]}.`);
|
|
1778
|
+
}
|
|
1779
|
+
if (bullets.length < 2 && genericErrorTypes.length >= 2) {
|
|
1780
|
+
bullets.push(`- Repeated error types include ${genericErrorTypes.join(", ")}.`);
|
|
1781
|
+
}
|
|
1782
|
+
return bullets.slice(0, 2);
|
|
1783
|
+
}
|
|
1784
|
+
function testStatusHeuristic(input, detail = "standard") {
|
|
1785
|
+
const normalized = input.trim();
|
|
1786
|
+
if (normalized === "") {
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
const passed = getCount(input, "passed");
|
|
1790
|
+
const failed = getCount(input, "failed");
|
|
1791
|
+
const errors = Math.max(
|
|
1792
|
+
getCount(input, "errors"),
|
|
1793
|
+
getCount(input, "error")
|
|
1794
|
+
);
|
|
1795
|
+
const skipped = getCount(input, "skipped");
|
|
1796
|
+
const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
|
|
1797
|
+
const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
|
|
1798
|
+
const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
|
|
1799
|
+
const inlineItems = collectInlineFailureItems(input);
|
|
1800
|
+
if (collectionErrors) {
|
|
1801
|
+
const count = Number(collectionErrors[1]);
|
|
1802
|
+
const items = chooseStrongestFailureItems(collectCollectionFailureItems(input));
|
|
1803
|
+
if (detail === "verbose") {
|
|
1804
|
+
if (items.length > 0) {
|
|
1805
|
+
return [
|
|
1806
|
+
"- Tests did not complete.",
|
|
1807
|
+
`- ${formatCount(count, "error")} occurred during collection.`,
|
|
1808
|
+
...formatVerboseFailureItems({
|
|
1809
|
+
items
|
|
1810
|
+
})
|
|
1811
|
+
].join("\n");
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
if (detail === "focused") {
|
|
1815
|
+
if (items.length > 0) {
|
|
1816
|
+
const groupedLines = formatFocusedFailureGroups({
|
|
1817
|
+
items,
|
|
1818
|
+
remainderLabel: "modules"
|
|
1819
|
+
});
|
|
1820
|
+
if (groupedLines.length > 0) {
|
|
1821
|
+
return [
|
|
1822
|
+
"- Tests did not complete.",
|
|
1823
|
+
`- ${formatCount(count, "error")} occurred during collection.`,
|
|
1824
|
+
...groupedLines
|
|
1825
|
+
].join("\n");
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const causes = summarizeRepeatedTestCauses(input, {
|
|
1830
|
+
duringCollection: true
|
|
1831
|
+
});
|
|
1832
|
+
return [
|
|
1833
|
+
"- Tests did not complete.",
|
|
1834
|
+
`- ${formatCount(count, "error")} occurred during collection.`,
|
|
1835
|
+
...causes
|
|
1836
|
+
].join("\n");
|
|
1837
|
+
}
|
|
1838
|
+
if (noTestsCollected) {
|
|
1839
|
+
return ["- Tests did not run.", "- Collected 0 items."].join("\n");
|
|
1840
|
+
}
|
|
1841
|
+
if (interrupted && failed === 0 && errors === 0) {
|
|
1842
|
+
return "- Test run was interrupted.";
|
|
1843
|
+
}
|
|
1844
|
+
if (failed === 0 && errors === 0 && passed > 0) {
|
|
1845
|
+
const details = [formatCount(passed, "test")];
|
|
1846
|
+
if (skipped > 0) {
|
|
1847
|
+
details.push(formatCount(skipped, "skip"));
|
|
1848
|
+
}
|
|
1849
|
+
return [
|
|
1850
|
+
"- Tests passed.",
|
|
1851
|
+
`- ${details.join(", ")}.`
|
|
1852
|
+
].join("\n");
|
|
1853
|
+
}
|
|
1854
|
+
if (failed > 0 || errors > 0 || inlineItems.length > 0) {
|
|
1855
|
+
const summarizedInlineItems = chooseStrongestFailureItems(inlineItems);
|
|
1856
|
+
if (detail === "verbose") {
|
|
1857
|
+
if (summarizedInlineItems.length > 0) {
|
|
1858
|
+
const detailLines2 = [];
|
|
1859
|
+
if (failed > 0) {
|
|
1860
|
+
detailLines2.push(`- ${formatCount(failed, "test")} failed.`);
|
|
1861
|
+
}
|
|
1862
|
+
if (errors > 0) {
|
|
1863
|
+
detailLines2.push(`- ${formatCount(errors, "error")} occurred.`);
|
|
1864
|
+
}
|
|
1865
|
+
return [
|
|
1866
|
+
"- Tests did not pass.",
|
|
1867
|
+
...detailLines2,
|
|
1868
|
+
...formatVerboseFailureItems({
|
|
1869
|
+
items: summarizedInlineItems
|
|
1870
|
+
})
|
|
1871
|
+
].join("\n");
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (detail === "focused") {
|
|
1875
|
+
if (summarizedInlineItems.length > 0) {
|
|
1876
|
+
const detailLines2 = [];
|
|
1877
|
+
if (failed > 0) {
|
|
1878
|
+
detailLines2.push(`- ${formatCount(failed, "test")} failed.`);
|
|
1879
|
+
}
|
|
1880
|
+
if (errors > 0) {
|
|
1881
|
+
detailLines2.push(`- ${formatCount(errors, "error")} occurred.`);
|
|
1882
|
+
}
|
|
1883
|
+
return [
|
|
1884
|
+
"- Tests did not pass.",
|
|
1885
|
+
...detailLines2,
|
|
1886
|
+
...formatFocusedFailureGroups({
|
|
1887
|
+
items: summarizedInlineItems,
|
|
1888
|
+
remainderLabel: "tests or modules"
|
|
1889
|
+
})
|
|
1890
|
+
].join("\n");
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
const detailLines = [];
|
|
1894
|
+
const causes = summarizeRepeatedTestCauses(input, {
|
|
1895
|
+
duringCollection: false
|
|
1896
|
+
});
|
|
1897
|
+
if (failed > 0) {
|
|
1898
|
+
detailLines.push(`- ${formatCount(failed, "test")} failed.`);
|
|
1899
|
+
}
|
|
1900
|
+
if (errors > 0) {
|
|
1901
|
+
detailLines.push(`- ${formatCount(errors, "error")} occurred.`);
|
|
1902
|
+
}
|
|
1903
|
+
const evidence = input.split("\n").map((line) => line.trim()).filter((line) => /\b(FAILED|ERROR)\b/.test(line)).slice(0, 3).map((line) => `- ${line}`);
|
|
1904
|
+
return ["- Tests did not pass.", ...detailLines, ...causes, ...evidence].join("\n");
|
|
1905
|
+
}
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1254
1908
|
function auditCriticalHeuristic(input) {
|
|
1255
1909
|
const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
1256
1910
|
if (!/\b(critical|high)\b/i.test(line)) {
|
|
@@ -1321,7 +1975,7 @@ function infraRiskHeuristic(input) {
|
|
|
1321
1975
|
}
|
|
1322
1976
|
return null;
|
|
1323
1977
|
}
|
|
1324
|
-
function applyHeuristicPolicy(policyName, input) {
|
|
1978
|
+
function applyHeuristicPolicy(policyName, input, detail) {
|
|
1325
1979
|
if (!policyName) {
|
|
1326
1980
|
return null;
|
|
1327
1981
|
}
|
|
@@ -1331,6 +1985,9 @@ function applyHeuristicPolicy(policyName, input) {
|
|
|
1331
1985
|
if (policyName === "infra-risk") {
|
|
1332
1986
|
return infraRiskHeuristic(input);
|
|
1333
1987
|
}
|
|
1988
|
+
if (policyName === "test-status") {
|
|
1989
|
+
return testStatusHeuristic(input, detail);
|
|
1990
|
+
}
|
|
1334
1991
|
return null;
|
|
1335
1992
|
}
|
|
1336
1993
|
|
|
@@ -1460,6 +2117,7 @@ function buildDryRunOutput(args) {
|
|
|
1460
2117
|
},
|
|
1461
2118
|
question: args.request.question,
|
|
1462
2119
|
format: args.request.format,
|
|
2120
|
+
detail: args.request.detail ?? null,
|
|
1463
2121
|
responseMode: args.responseMode,
|
|
1464
2122
|
policy: args.request.policyName ?? null,
|
|
1465
2123
|
heuristicOutput: args.heuristicOutput ?? null,
|
|
@@ -1479,35 +2137,42 @@ function buildDryRunOutput(args) {
|
|
|
1479
2137
|
async function delay(ms) {
|
|
1480
2138
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1481
2139
|
}
|
|
2140
|
+
function withInsufficientHint(args) {
|
|
2141
|
+
if (!isInsufficientSignalOutput(args.output)) {
|
|
2142
|
+
return args.output;
|
|
2143
|
+
}
|
|
2144
|
+
return buildInsufficientSignalOutput({
|
|
2145
|
+
presetName: args.request.presetName,
|
|
2146
|
+
originalLength: args.prepared.meta.originalLength,
|
|
2147
|
+
truncatedApplied: args.prepared.meta.truncatedApplied
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
1482
2150
|
async function generateWithRetry(args) {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
process.stderr.write(
|
|
1503
|
-
`${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
|
|
2151
|
+
const generate = () => args.provider.generate({
|
|
2152
|
+
model: args.request.config.provider.model,
|
|
2153
|
+
prompt: args.prompt,
|
|
2154
|
+
temperature: args.request.config.provider.temperature,
|
|
2155
|
+
maxOutputTokens: args.request.config.provider.maxOutputTokens,
|
|
2156
|
+
timeoutMs: args.request.config.provider.timeoutMs,
|
|
2157
|
+
responseMode: args.responseMode,
|
|
2158
|
+
jsonResponseFormat: args.request.config.provider.jsonResponseFormat
|
|
2159
|
+
});
|
|
2160
|
+
try {
|
|
2161
|
+
return await generate();
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
2164
|
+
if (!isRetriableReason(reason)) {
|
|
2165
|
+
throw error;
|
|
2166
|
+
}
|
|
2167
|
+
if (args.request.config.runtime.verbose) {
|
|
2168
|
+
process.stderr.write(
|
|
2169
|
+
`${pc2.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
|
|
1504
2170
|
`
|
|
1505
|
-
|
|
1506
|
-
}
|
|
1507
|
-
await delay(RETRY_DELAY_MS);
|
|
2171
|
+
);
|
|
1508
2172
|
}
|
|
2173
|
+
await delay(RETRY_DELAY_MS);
|
|
1509
2174
|
}
|
|
1510
|
-
|
|
2175
|
+
return generate();
|
|
1511
2176
|
}
|
|
1512
2177
|
async function runSift(request) {
|
|
1513
2178
|
const prepared = prepareInput(request.stdin, request.config.input);
|
|
@@ -1515,23 +2180,25 @@ async function runSift(request) {
|
|
|
1515
2180
|
question: request.question,
|
|
1516
2181
|
format: request.format,
|
|
1517
2182
|
input: prepared.truncated,
|
|
2183
|
+
detail: request.detail,
|
|
1518
2184
|
policyName: request.policyName,
|
|
1519
2185
|
outputContract: request.outputContract
|
|
1520
2186
|
});
|
|
1521
2187
|
const provider = createProvider(request.config);
|
|
1522
2188
|
if (request.config.runtime.verbose) {
|
|
1523
2189
|
process.stderr.write(
|
|
1524
|
-
`${
|
|
2190
|
+
`${pc2.dim("sift")} provider=${provider.name} model=${request.config.provider.model} base_url=${request.config.provider.baseUrl} input_chars=${prepared.meta.finalLength}
|
|
1525
2191
|
`
|
|
1526
2192
|
);
|
|
1527
2193
|
}
|
|
1528
2194
|
const heuristicOutput = applyHeuristicPolicy(
|
|
1529
2195
|
request.policyName,
|
|
1530
|
-
prepared.truncated
|
|
2196
|
+
prepared.truncated,
|
|
2197
|
+
request.detail
|
|
1531
2198
|
);
|
|
1532
2199
|
if (heuristicOutput) {
|
|
1533
2200
|
if (request.config.runtime.verbose) {
|
|
1534
|
-
process.stderr.write(`${
|
|
2201
|
+
process.stderr.write(`${pc2.dim("sift")} heuristic=${request.policyName}
|
|
1535
2202
|
`);
|
|
1536
2203
|
}
|
|
1537
2204
|
if (request.dryRun) {
|
|
@@ -1544,7 +2211,11 @@ async function runSift(request) {
|
|
|
1544
2211
|
heuristicOutput
|
|
1545
2212
|
});
|
|
1546
2213
|
}
|
|
1547
|
-
return
|
|
2214
|
+
return withInsufficientHint({
|
|
2215
|
+
output: heuristicOutput,
|
|
2216
|
+
request,
|
|
2217
|
+
prepared
|
|
2218
|
+
});
|
|
1548
2219
|
}
|
|
1549
2220
|
if (request.dryRun) {
|
|
1550
2221
|
return buildDryRunOutput({
|
|
@@ -1570,15 +2241,23 @@ async function runSift(request) {
|
|
|
1570
2241
|
})) {
|
|
1571
2242
|
throw new Error("Model output rejected by quality gate");
|
|
1572
2243
|
}
|
|
1573
|
-
return
|
|
2244
|
+
return withInsufficientHint({
|
|
2245
|
+
output: normalizeOutput(result.text, responseMode),
|
|
2246
|
+
request,
|
|
2247
|
+
prepared
|
|
2248
|
+
});
|
|
1574
2249
|
} catch (error) {
|
|
1575
2250
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
1576
|
-
return
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2251
|
+
return withInsufficientHint({
|
|
2252
|
+
output: buildFallbackOutput({
|
|
2253
|
+
format: request.format,
|
|
2254
|
+
reason,
|
|
2255
|
+
rawInput: prepared.truncated,
|
|
2256
|
+
rawFallback: request.config.runtime.rawFallback,
|
|
2257
|
+
jsonFallback: request.fallbackJson
|
|
2258
|
+
}),
|
|
2259
|
+
request,
|
|
2260
|
+
prepared
|
|
1582
2261
|
});
|
|
1583
2262
|
}
|
|
1584
2263
|
}
|
|
@@ -1679,7 +2358,7 @@ async function runExec(request) {
|
|
|
1679
2358
|
const shellPath = process.env.SHELL || "/bin/bash";
|
|
1680
2359
|
if (request.config.runtime.verbose) {
|
|
1681
2360
|
process.stderr.write(
|
|
1682
|
-
`${
|
|
2361
|
+
`${pc3.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${buildCommandPreview(request)}
|
|
1683
2362
|
`
|
|
1684
2363
|
);
|
|
1685
2364
|
}
|
|
@@ -1688,7 +2367,6 @@ async function runExec(request) {
|
|
|
1688
2367
|
let bypassed = false;
|
|
1689
2368
|
let childStatus = null;
|
|
1690
2369
|
let childSignal = null;
|
|
1691
|
-
let childSpawnError = null;
|
|
1692
2370
|
const child = hasShellCommand ? spawn(shellPath, ["-lc", request.shellCommand], {
|
|
1693
2371
|
stdio: ["inherit", "pipe", "pipe"]
|
|
1694
2372
|
}) : spawn(request.command[0], request.command.slice(1), {
|
|
@@ -1707,7 +2385,7 @@ async function runExec(request) {
|
|
|
1707
2385
|
}
|
|
1708
2386
|
bypassed = true;
|
|
1709
2387
|
if (request.config.runtime.verbose) {
|
|
1710
|
-
process.stderr.write(`${
|
|
2388
|
+
process.stderr.write(`${pc3.dim("sift")} bypass=interactive-prompt
|
|
1711
2389
|
`);
|
|
1712
2390
|
}
|
|
1713
2391
|
process.stderr.write(capture.render());
|
|
@@ -1716,7 +2394,6 @@ async function runExec(request) {
|
|
|
1716
2394
|
child.stderr.on("data", handleChunk);
|
|
1717
2395
|
await new Promise((resolve, reject) => {
|
|
1718
2396
|
child.on("error", (error) => {
|
|
1719
|
-
childSpawnError = error;
|
|
1720
2397
|
reject(error);
|
|
1721
2398
|
});
|
|
1722
2399
|
child.on("close", (status, signal) => {
|
|
@@ -1730,18 +2407,21 @@ async function runExec(request) {
|
|
|
1730
2407
|
}
|
|
1731
2408
|
throw new Error("Failed to start child process.");
|
|
1732
2409
|
});
|
|
1733
|
-
if (childSpawnError) {
|
|
1734
|
-
throw childSpawnError;
|
|
1735
|
-
}
|
|
1736
2410
|
const exitCode = normalizeChildExitCode(childStatus, childSignal);
|
|
1737
2411
|
const capturedOutput = capture.render();
|
|
1738
2412
|
if (request.config.runtime.verbose) {
|
|
1739
2413
|
process.stderr.write(
|
|
1740
|
-
`${
|
|
2414
|
+
`${pc3.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
|
|
1741
2415
|
`
|
|
1742
2416
|
);
|
|
1743
2417
|
}
|
|
1744
2418
|
if (!bypassed) {
|
|
2419
|
+
if (request.showRaw && capturedOutput.length > 0) {
|
|
2420
|
+
process.stderr.write(capturedOutput);
|
|
2421
|
+
if (!capturedOutput.endsWith("\n")) {
|
|
2422
|
+
process.stderr.write("\n");
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
1745
2425
|
const execSuccessShortcut = getExecSuccessShortcut({
|
|
1746
2426
|
presetName: request.presetName,
|
|
1747
2427
|
exitCode,
|
|
@@ -1750,7 +2430,7 @@ async function runExec(request) {
|
|
|
1750
2430
|
if (execSuccessShortcut && !request.dryRun) {
|
|
1751
2431
|
if (request.config.runtime.verbose) {
|
|
1752
2432
|
process.stderr.write(
|
|
1753
|
-
`${
|
|
2433
|
+
`${pc3.dim("sift")} exec_shortcut=${request.presetName}
|
|
1754
2434
|
`
|
|
1755
2435
|
);
|
|
1756
2436
|
}
|
|
@@ -1758,10 +2438,18 @@ async function runExec(request) {
|
|
|
1758
2438
|
`);
|
|
1759
2439
|
return exitCode;
|
|
1760
2440
|
}
|
|
1761
|
-
|
|
2441
|
+
let output = await runSift({
|
|
1762
2442
|
...request,
|
|
1763
2443
|
stdin: capturedOutput
|
|
1764
2444
|
});
|
|
2445
|
+
if (isInsufficientSignalOutput(output)) {
|
|
2446
|
+
output = buildInsufficientSignalOutput({
|
|
2447
|
+
presetName: request.presetName,
|
|
2448
|
+
originalLength: capture.getTotalChars(),
|
|
2449
|
+
truncatedApplied: capture.wasTruncated(),
|
|
2450
|
+
exitCode
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
1765
2453
|
process.stdout.write(`${output}
|
|
1766
2454
|
`);
|
|
1767
2455
|
if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
|
|
@@ -1795,16 +2483,27 @@ function getPreset(config, name) {
|
|
|
1795
2483
|
return preset;
|
|
1796
2484
|
}
|
|
1797
2485
|
|
|
1798
|
-
// src/cli.ts
|
|
2486
|
+
// src/cli-app.ts
|
|
1799
2487
|
var require2 = createRequire(import.meta.url);
|
|
1800
2488
|
var pkg = require2("../package.json");
|
|
1801
|
-
var
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
2489
|
+
var defaultCliDeps = {
|
|
2490
|
+
configInit,
|
|
2491
|
+
configSetup,
|
|
2492
|
+
configShow,
|
|
2493
|
+
configValidate,
|
|
2494
|
+
runDoctor,
|
|
2495
|
+
listPresets,
|
|
2496
|
+
showPreset,
|
|
2497
|
+
resolveConfig,
|
|
2498
|
+
findConfigPath,
|
|
2499
|
+
runExec,
|
|
2500
|
+
assertSupportedFailOnFormat,
|
|
2501
|
+
assertSupportedFailOnPreset,
|
|
2502
|
+
evaluateGate,
|
|
2503
|
+
readStdin,
|
|
2504
|
+
runSift,
|
|
2505
|
+
getPreset
|
|
2506
|
+
};
|
|
1808
2507
|
function toNumber(value) {
|
|
1809
2508
|
if (value === void 0 || value === null || value === "") {
|
|
1810
2509
|
return void 0;
|
|
@@ -1849,44 +2548,32 @@ function applySharedOptions(command) {
|
|
|
1849
2548
|
).option(
|
|
1850
2549
|
"--json-response-format <mode>",
|
|
1851
2550
|
"JSON response format mode: auto | on | off"
|
|
1852
|
-
).option("--timeout-ms <ms>", "Request timeout in milliseconds").option("--format <format>", "brief | bullets | json | verdict").option(
|
|
2551
|
+
).option("--timeout-ms <ms>", "Request timeout in milliseconds").option("--format <format>", "brief | bullets | json | verdict").option(
|
|
2552
|
+
"--detail <mode>",
|
|
2553
|
+
"Detail level for supported presets: standard | focused | verbose"
|
|
2554
|
+
).option("--max-capture-chars <n>", "Maximum raw child output chars kept in memory").option("--max-input-chars <n>", "Maximum input chars sent to the model").option("--head-chars <n>", "Head chars to preserve during truncation").option("--tail-chars <n>", "Tail chars to preserve during truncation").option("--strip-ansi", "Force ANSI stripping").option("--redact", "Enable standard redaction").option("--redact-strict", "Enable strict redaction").option("--raw-fallback", "Enable raw fallback text output").option("--dry-run", "Show the reduced input and prompt without calling the provider").option("--show-raw", "Print the captured raw input to stderr for debugging").option(
|
|
1853
2555
|
"--fail-on",
|
|
1854
2556
|
"Fail with exit code 1 when a supported built-in preset produces a blocking result"
|
|
1855
2557
|
).option("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
|
|
1856
2558
|
}
|
|
1857
|
-
|
|
1858
|
-
if (
|
|
1859
|
-
|
|
1860
|
-
assertSupportedFailOnFormat({
|
|
1861
|
-
presetName: args.presetName,
|
|
1862
|
-
format: args.format
|
|
1863
|
-
});
|
|
2559
|
+
function normalizeDetail(value) {
|
|
2560
|
+
if (value === void 0 || value === null || value === "") {
|
|
2561
|
+
return void 0;
|
|
1864
2562
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
presetName: args.presetName,
|
|
1878
|
-
policyName: args.policyName,
|
|
1879
|
-
outputContract: args.outputContract,
|
|
1880
|
-
fallbackJson: args.fallbackJson
|
|
1881
|
-
});
|
|
1882
|
-
process.stdout.write(`${output}
|
|
1883
|
-
`);
|
|
1884
|
-
if (Boolean(args.options.failOn) && !Boolean(args.options.dryRun) && args.presetName && evaluateGate({
|
|
1885
|
-
presetName: args.presetName,
|
|
1886
|
-
output
|
|
1887
|
-
}).shouldFail) {
|
|
1888
|
-
process.exitCode = 1;
|
|
2563
|
+
if (value === "standard" || value === "focused" || value === "verbose") {
|
|
2564
|
+
return value;
|
|
2565
|
+
}
|
|
2566
|
+
throw new Error("Invalid --detail value. Use standard, focused, or verbose.");
|
|
2567
|
+
}
|
|
2568
|
+
function resolveDetail(args) {
|
|
2569
|
+
const requested = normalizeDetail(args.options.detail);
|
|
2570
|
+
if (!requested) {
|
|
2571
|
+
return args.presetName === "test-status" ? "standard" : void 0;
|
|
2572
|
+
}
|
|
2573
|
+
if (args.presetName !== "test-status") {
|
|
2574
|
+
throw new Error("--detail is supported only with --preset test-status.");
|
|
1889
2575
|
}
|
|
2576
|
+
return requested;
|
|
1890
2577
|
}
|
|
1891
2578
|
function extractExecCommand(options) {
|
|
1892
2579
|
const passthrough = Array.isArray(options["--"]) ? options["--"].map((value) => String(value)) : [];
|
|
@@ -1902,180 +2589,278 @@ function extractExecCommand(options) {
|
|
|
1902
2589
|
shellCommand
|
|
1903
2590
|
};
|
|
1904
2591
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2592
|
+
function cleanHelpSectionBody(body, escapedVersion) {
|
|
2593
|
+
return body.replace(
|
|
2594
|
+
new RegExp(`(^|\\n)sift/${escapedVersion}\\n\\n?`, "g"),
|
|
2595
|
+
"\n"
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
function createCliApp(args = {}) {
|
|
2599
|
+
const deps = {
|
|
2600
|
+
...defaultCliDeps,
|
|
2601
|
+
...args.deps
|
|
2602
|
+
};
|
|
2603
|
+
const env = args.env ?? process.env;
|
|
2604
|
+
const stdout = args.stdout ?? process.stdout;
|
|
2605
|
+
const stderr = args.stderr ?? process.stderr;
|
|
2606
|
+
const version = args.version ?? pkg.version;
|
|
2607
|
+
const cli = cac("sift");
|
|
2608
|
+
const ui = createPresentation(Boolean(stdout.isTTY));
|
|
2609
|
+
const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2610
|
+
async function executeRun(input) {
|
|
2611
|
+
if (Boolean(input.options.failOn)) {
|
|
2612
|
+
deps.assertSupportedFailOnPreset(input.presetName);
|
|
2613
|
+
deps.assertSupportedFailOnFormat({
|
|
2614
|
+
presetName: input.presetName,
|
|
2615
|
+
format: input.format
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
const config = deps.resolveConfig({
|
|
2619
|
+
configPath: input.options.config,
|
|
2620
|
+
env,
|
|
2621
|
+
cliOverrides: buildCliOverrides(input.options)
|
|
1911
2622
|
});
|
|
2623
|
+
const stdin = await deps.readStdin();
|
|
2624
|
+
if (Boolean(input.options.showRaw) && stdin.length > 0) {
|
|
2625
|
+
stderr.write(stdin);
|
|
2626
|
+
if (!stdin.endsWith("\n")) {
|
|
2627
|
+
stderr.write("\n");
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
const output = await deps.runSift({
|
|
2631
|
+
question: input.question,
|
|
2632
|
+
format: input.format,
|
|
2633
|
+
stdin,
|
|
2634
|
+
config,
|
|
2635
|
+
dryRun: Boolean(input.options.dryRun),
|
|
2636
|
+
showRaw: Boolean(input.options.showRaw),
|
|
2637
|
+
detail: input.detail,
|
|
2638
|
+
presetName: input.presetName,
|
|
2639
|
+
policyName: input.policyName,
|
|
2640
|
+
outputContract: input.outputContract,
|
|
2641
|
+
fallbackJson: input.fallbackJson
|
|
2642
|
+
});
|
|
2643
|
+
stdout.write(`${output}
|
|
2644
|
+
`);
|
|
2645
|
+
if (Boolean(input.options.failOn) && !Boolean(input.options.dryRun) && input.presetName && deps.evaluateGate({
|
|
2646
|
+
presetName: input.presetName,
|
|
2647
|
+
output
|
|
2648
|
+
}).shouldFail) {
|
|
2649
|
+
process.exitCode = 1;
|
|
2650
|
+
}
|
|
1912
2651
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
cli.command("exec [question]", "Run a command and reduce its output").allowUnknownOptions()
|
|
1953
|
-
).usage("exec [question] [options] -- <program> [args...]").example('exec "what changed?" -- git diff').example("exec --preset test-status -- pytest").example('exec --preset infra-risk --shell "terraform plan"').option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").action(async (question, options) => {
|
|
1954
|
-
if (question === "preset") {
|
|
1955
|
-
throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
|
|
1956
|
-
}
|
|
1957
|
-
const presetName = typeof options.preset === "string" && options.preset.length > 0 ? options.preset : void 0;
|
|
1958
|
-
if (presetName) {
|
|
1959
|
-
if (question) {
|
|
1960
|
-
throw new Error("Use either a freeform question or --preset <name>, not both.");
|
|
1961
|
-
}
|
|
1962
|
-
const preset = getPreset(
|
|
1963
|
-
resolveConfig({
|
|
1964
|
-
configPath: options.config,
|
|
1965
|
-
env: process.env,
|
|
1966
|
-
cliOverrides: buildCliOverrides(options)
|
|
1967
|
-
}),
|
|
1968
|
-
presetName
|
|
1969
|
-
);
|
|
1970
|
-
await executeExec({
|
|
2652
|
+
async function executeExec(input) {
|
|
2653
|
+
if (Boolean(input.options.failOn)) {
|
|
2654
|
+
deps.assertSupportedFailOnPreset(input.presetName);
|
|
2655
|
+
deps.assertSupportedFailOnFormat({
|
|
2656
|
+
presetName: input.presetName,
|
|
2657
|
+
format: input.format
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
const config = deps.resolveConfig({
|
|
2661
|
+
configPath: input.options.config,
|
|
2662
|
+
env,
|
|
2663
|
+
cliOverrides: buildCliOverrides(input.options)
|
|
2664
|
+
});
|
|
2665
|
+
const command = extractExecCommand(input.options);
|
|
2666
|
+
process.exitCode = await deps.runExec({
|
|
2667
|
+
question: input.question,
|
|
2668
|
+
format: input.format,
|
|
2669
|
+
config,
|
|
2670
|
+
dryRun: Boolean(input.options.dryRun),
|
|
2671
|
+
failOn: Boolean(input.options.failOn),
|
|
2672
|
+
showRaw: Boolean(input.options.showRaw),
|
|
2673
|
+
detail: input.detail,
|
|
2674
|
+
presetName: input.presetName,
|
|
2675
|
+
policyName: input.policyName,
|
|
2676
|
+
outputContract: input.outputContract,
|
|
2677
|
+
fallbackJson: input.fallbackJson,
|
|
2678
|
+
...command
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
applySharedOptions(
|
|
2682
|
+
cli.command("preset <name>", "Run a named preset against piped output")
|
|
2683
|
+
).usage("preset <name> [options]").example("preset test-status < test-output.txt").action(async (name, options) => {
|
|
2684
|
+
const config = deps.resolveConfig({
|
|
2685
|
+
configPath: options.config,
|
|
2686
|
+
env,
|
|
2687
|
+
cliOverrides: buildCliOverrides(options)
|
|
2688
|
+
});
|
|
2689
|
+
const preset = deps.getPreset(config, name);
|
|
2690
|
+
await executeRun({
|
|
1971
2691
|
question: preset.question,
|
|
1972
2692
|
format: options.format ?? preset.format,
|
|
1973
|
-
presetName,
|
|
2693
|
+
presetName: name,
|
|
2694
|
+
detail: resolveDetail({
|
|
2695
|
+
presetName: name,
|
|
2696
|
+
options
|
|
2697
|
+
}),
|
|
1974
2698
|
policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
|
|
1975
2699
|
options,
|
|
1976
2700
|
outputContract: preset.outputContract,
|
|
1977
2701
|
fallbackJson: preset.fallbackJson
|
|
1978
2702
|
});
|
|
1979
|
-
return;
|
|
1980
|
-
}
|
|
1981
|
-
if (!question) {
|
|
1982
|
-
throw new Error("Missing question or preset.");
|
|
1983
|
-
}
|
|
1984
|
-
const format = options.format ?? "brief";
|
|
1985
|
-
await executeExec({
|
|
1986
|
-
question,
|
|
1987
|
-
format,
|
|
1988
|
-
options
|
|
1989
2703
|
});
|
|
1990
|
-
|
|
1991
|
-
cli.command(
|
|
1992
|
-
"
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2704
|
+
applySharedOptions(
|
|
2705
|
+
cli.command("exec [question]", "Run a command and shrink its output for the model").allowUnknownOptions()
|
|
2706
|
+
).usage("exec [question] [options] -- <program> [args...]").example('exec "what changed?" -- git diff').example("exec --preset test-status -- pytest").example('exec --preset infra-risk --shell "terraform plan"').option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").action(async (question, options) => {
|
|
2707
|
+
if (question === "preset") {
|
|
2708
|
+
throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
|
|
2709
|
+
}
|
|
2710
|
+
const presetName = typeof options.preset === "string" && options.preset.length > 0 ? options.preset : void 0;
|
|
2711
|
+
if (presetName) {
|
|
2712
|
+
if (question) {
|
|
2713
|
+
throw new Error("Use either a freeform question or --preset <name>, not both.");
|
|
2714
|
+
}
|
|
2715
|
+
const preset = deps.getPreset(
|
|
2716
|
+
deps.resolveConfig({
|
|
2717
|
+
configPath: options.config,
|
|
2718
|
+
env,
|
|
2719
|
+
cliOverrides: buildCliOverrides(options)
|
|
2720
|
+
}),
|
|
2721
|
+
presetName
|
|
2722
|
+
);
|
|
2723
|
+
await executeExec({
|
|
2724
|
+
question: preset.question,
|
|
2725
|
+
format: options.format ?? preset.format,
|
|
2726
|
+
presetName,
|
|
2727
|
+
detail: resolveDetail({
|
|
2728
|
+
presetName,
|
|
2729
|
+
options
|
|
2730
|
+
}),
|
|
2731
|
+
policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
|
|
2732
|
+
options,
|
|
2733
|
+
outputContract: preset.outputContract,
|
|
2734
|
+
fallbackJson: preset.fallbackJson
|
|
2735
|
+
});
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (!question) {
|
|
2739
|
+
throw new Error("Missing question or preset.");
|
|
2740
|
+
}
|
|
2741
|
+
const format = options.format ?? "brief";
|
|
2742
|
+
await executeExec({
|
|
2743
|
+
question,
|
|
2744
|
+
format,
|
|
2745
|
+
detail: resolveDetail({
|
|
2746
|
+
options
|
|
2747
|
+
}),
|
|
2748
|
+
options
|
|
2002
2749
|
});
|
|
2003
|
-
return;
|
|
2004
|
-
}
|
|
2005
|
-
if (action === "init") {
|
|
2006
|
-
configInit(
|
|
2007
|
-
options.path,
|
|
2008
|
-
Boolean(options.global)
|
|
2009
|
-
);
|
|
2010
|
-
return;
|
|
2011
|
-
}
|
|
2012
|
-
if (action === "show") {
|
|
2013
|
-
configShow(
|
|
2014
|
-
options.config,
|
|
2015
|
-
Boolean(options.showSecrets)
|
|
2016
|
-
);
|
|
2017
|
-
return;
|
|
2018
|
-
}
|
|
2019
|
-
if (action === "validate") {
|
|
2020
|
-
configValidate(options.config);
|
|
2021
|
-
return;
|
|
2022
|
-
}
|
|
2023
|
-
throw new Error(`Unknown config action: ${action}`);
|
|
2024
|
-
});
|
|
2025
|
-
cli.command("doctor", "Check local runtime config completeness").usage("doctor [options]").option("--config <path>", "Path to config file").action((options) => {
|
|
2026
|
-
const configPath = findConfigPath(options.config);
|
|
2027
|
-
const config = resolveConfig({
|
|
2028
|
-
configPath: options.config,
|
|
2029
|
-
env: process.env
|
|
2030
2750
|
});
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2751
|
+
cli.command("config <action>", "Config commands: setup | init | show | validate").usage("config <setup|init|show|validate> [options]").example("config setup").example("config setup --global").example("config setup --path ~/.config/sift/config.yaml").example("config init").example("config init --global").example("config show").example("config validate --config ./sift.config.yaml").option("--path <path>", "Target config path for init or setup").option(
|
|
2752
|
+
"--global",
|
|
2753
|
+
"Use the machine-wide config path (~/.config/sift/config.yaml) for init or setup"
|
|
2754
|
+
).option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action(async (action, options) => {
|
|
2755
|
+
if (action === "setup") {
|
|
2756
|
+
process.exitCode = await deps.configSetup({
|
|
2757
|
+
targetPath: options.path,
|
|
2758
|
+
global: Boolean(options.global)
|
|
2759
|
+
});
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
if (action === "init") {
|
|
2763
|
+
deps.configInit(options.path, Boolean(options.global));
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
if (action === "show") {
|
|
2767
|
+
deps.configShow(
|
|
2768
|
+
options.config,
|
|
2769
|
+
Boolean(options.showSecrets)
|
|
2770
|
+
);
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
if (action === "validate") {
|
|
2774
|
+
deps.configValidate(options.config);
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
throw new Error(`Unknown config action: ${action}`);
|
|
2037
2778
|
});
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2779
|
+
cli.command("doctor", "Check which config is active and whether local setup looks complete").usage("doctor [options]").option("--config <path>", "Path to config file").action((options) => {
|
|
2780
|
+
const configPath = deps.findConfigPath(options.config);
|
|
2781
|
+
const config = deps.resolveConfig({
|
|
2782
|
+
configPath: options.config,
|
|
2783
|
+
env
|
|
2784
|
+
});
|
|
2785
|
+
process.exitCode = deps.runDoctor(config, configPath);
|
|
2786
|
+
});
|
|
2787
|
+
cli.command("presets <action> [name]", "Preset commands: list | show").usage("presets <list|show> [name] [options]").example("presets list").example("presets show infra-risk").option("--config <path>", "Path to config file").option("--internal", "Show internal preset fields in presets show").action((action, name, options) => {
|
|
2788
|
+
const config = deps.resolveConfig({
|
|
2789
|
+
configPath: options.config,
|
|
2790
|
+
env
|
|
2791
|
+
});
|
|
2792
|
+
if (action === "list") {
|
|
2793
|
+
deps.listPresets(config);
|
|
2794
|
+
return;
|
|
2045
2795
|
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
if (!question) {
|
|
2055
|
-
throw new Error("Missing question.");
|
|
2056
|
-
}
|
|
2057
|
-
const format = options.format ?? "brief";
|
|
2058
|
-
await executeRun({
|
|
2059
|
-
question,
|
|
2060
|
-
format,
|
|
2061
|
-
options
|
|
2796
|
+
if (action === "show") {
|
|
2797
|
+
if (!name) {
|
|
2798
|
+
throw new Error("Missing preset name.");
|
|
2799
|
+
}
|
|
2800
|
+
deps.showPreset(config, name, Boolean(options.internal));
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
throw new Error(`Unknown presets action: ${action}`);
|
|
2062
2804
|
});
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2805
|
+
applySharedOptions(cli.command("[question]", "Ask a question about piped output")).action(
|
|
2806
|
+
async (question, options) => {
|
|
2807
|
+
if (!question) {
|
|
2808
|
+
throw new Error("Missing question.");
|
|
2809
|
+
}
|
|
2810
|
+
const format = options.format ?? "brief";
|
|
2811
|
+
await executeRun({
|
|
2812
|
+
question,
|
|
2813
|
+
format,
|
|
2814
|
+
detail: resolveDetail({
|
|
2815
|
+
options
|
|
2816
|
+
}),
|
|
2817
|
+
options
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
);
|
|
2821
|
+
cli.help((sections) => {
|
|
2822
|
+
const cleanedSections = sections.map((section) => ({
|
|
2823
|
+
...section,
|
|
2824
|
+
body: cleanHelpSectionBody(section.body, escapedVersion)
|
|
2825
|
+
}));
|
|
2826
|
+
return [
|
|
2827
|
+
{
|
|
2828
|
+
body: `${ui.banner(version)}
|
|
2067
2829
|
`
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2830
|
+
},
|
|
2831
|
+
{
|
|
2832
|
+
title: ui.section("Quick start"),
|
|
2833
|
+
body: [
|
|
2834
|
+
` ${ui.command("sift config setup")}`,
|
|
2835
|
+
` ${ui.command("sift exec --preset test-status -- pytest")}`,
|
|
2836
|
+
` ${ui.command("sift exec --preset test-status --show-raw -- pytest")}`,
|
|
2837
|
+
` ${ui.command('sift exec "what changed?" -- git diff')}`
|
|
2838
|
+
].join("\n")
|
|
2839
|
+
},
|
|
2840
|
+
...cleanedSections
|
|
2841
|
+
];
|
|
2842
|
+
});
|
|
2843
|
+
cli.version(version);
|
|
2844
|
+
return cli;
|
|
2845
|
+
}
|
|
2846
|
+
async function runCli(args = {}) {
|
|
2847
|
+
const cli = createCliApp(args);
|
|
2848
|
+
cli.parse(args.argv ?? process.argv, { run: false });
|
|
2074
2849
|
await cli.runMatchedCommand();
|
|
2075
2850
|
}
|
|
2076
|
-
|
|
2851
|
+
function handleCliError(error, stderr = process.stderr) {
|
|
2077
2852
|
const message = error instanceof Error ? error.message : "Unexpected error.";
|
|
2078
|
-
|
|
2853
|
+
if (stderr.isTTY) {
|
|
2854
|
+
stderr.write(`${createPresentation(true).error(message)}
|
|
2855
|
+
`);
|
|
2856
|
+
} else {
|
|
2857
|
+
stderr.write(`${message}
|
|
2079
2858
|
`);
|
|
2859
|
+
}
|
|
2080
2860
|
process.exitCode = 1;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// src/cli.ts
|
|
2864
|
+
runCli().catch((error) => {
|
|
2865
|
+
handleCliError(error);
|
|
2081
2866
|
});
|