@bilalimamoglu/sift 0.2.1 → 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 +163 -69
- package/dist/cli.js +1321 -252
- package/dist/index.d.ts +29 -1
- package/dist/index.js +569 -55
- 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
|
|
|
@@ -13,12 +13,17 @@ import YAML from "yaml";
|
|
|
13
13
|
import os from "os";
|
|
14
14
|
import path from "path";
|
|
15
15
|
var DEFAULT_CONFIG_FILENAME = "sift.config.yaml";
|
|
16
|
-
|
|
17
|
-
path.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
function getDefaultGlobalConfigPath() {
|
|
17
|
+
return path.join(os.homedir(), ".config", "sift", "config.yaml");
|
|
18
|
+
}
|
|
19
|
+
function getDefaultConfigSearchPaths() {
|
|
20
|
+
return [
|
|
21
|
+
path.resolve(process.cwd(), "sift.config.yaml"),
|
|
22
|
+
path.resolve(process.cwd(), "sift.config.yml"),
|
|
23
|
+
getDefaultGlobalConfigPath(),
|
|
24
|
+
path.join(os.homedir(), ".config", "sift", "config.yml")
|
|
25
|
+
];
|
|
26
|
+
}
|
|
22
27
|
var INSUFFICIENT_SIGNAL_TEXT = "Insufficient signal in the provided input.";
|
|
23
28
|
var GENERIC_JSON_CONTRACT = '{"answer":string,"evidence":string[],"risks":string[]}';
|
|
24
29
|
var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
|
|
@@ -32,7 +37,7 @@ function findConfigPath(explicitPath) {
|
|
|
32
37
|
}
|
|
33
38
|
return resolved;
|
|
34
39
|
}
|
|
35
|
-
for (const candidate of
|
|
40
|
+
for (const candidate of getDefaultConfigSearchPaths()) {
|
|
36
41
|
if (fs.existsSync(candidate)) {
|
|
37
42
|
return candidate;
|
|
38
43
|
}
|
|
@@ -339,8 +344,11 @@ function resolveConfig(options = {}) {
|
|
|
339
344
|
import fs2 from "fs";
|
|
340
345
|
import path3 from "path";
|
|
341
346
|
import YAML2 from "yaml";
|
|
342
|
-
function writeExampleConfig(
|
|
343
|
-
|
|
347
|
+
function writeExampleConfig(options = {}) {
|
|
348
|
+
if (options.global && options.targetPath) {
|
|
349
|
+
throw new Error("Use either --path <path> or --global, not both.");
|
|
350
|
+
}
|
|
351
|
+
const resolved = options.global ? getDefaultGlobalConfigPath() : path3.resolve(options.targetPath ?? DEFAULT_CONFIG_FILENAME);
|
|
344
352
|
if (fs2.existsSync(resolved)) {
|
|
345
353
|
throw new Error(`Config file already exists at ${resolved}`);
|
|
346
354
|
}
|
|
@@ -349,6 +357,414 @@ function writeExampleConfig(targetPath) {
|
|
|
349
357
|
fs2.writeFileSync(resolved, yaml, "utf8");
|
|
350
358
|
return resolved;
|
|
351
359
|
}
|
|
360
|
+
function writeConfigFile(options) {
|
|
361
|
+
const resolved = path3.resolve(options.targetPath);
|
|
362
|
+
if (!options.overwrite && fs2.existsSync(resolved)) {
|
|
363
|
+
throw new Error(`Config file already exists at ${resolved}`);
|
|
364
|
+
}
|
|
365
|
+
const yaml = YAML2.stringify(options.config);
|
|
366
|
+
fs2.mkdirSync(path3.dirname(resolved), { recursive: true });
|
|
367
|
+
fs2.writeFileSync(resolved, yaml, {
|
|
368
|
+
encoding: "utf8",
|
|
369
|
+
mode: 384
|
|
370
|
+
});
|
|
371
|
+
try {
|
|
372
|
+
fs2.chmodSync(resolved, 384);
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
return resolved;
|
|
376
|
+
}
|
|
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
|
+
|
|
427
|
+
// src/commands/config-setup.ts
|
|
428
|
+
import fs3 from "fs";
|
|
429
|
+
import path4 from "path";
|
|
430
|
+
import { emitKeypressEvents } from "readline";
|
|
431
|
+
import { createInterface } from "readline/promises";
|
|
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
|
|
577
|
+
function createTerminalIO() {
|
|
578
|
+
let rl;
|
|
579
|
+
function getInterface() {
|
|
580
|
+
if (!rl) {
|
|
581
|
+
rl = createInterface({
|
|
582
|
+
input: defaultStdin2,
|
|
583
|
+
output: defaultStdout,
|
|
584
|
+
terminal: true
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return rl;
|
|
588
|
+
}
|
|
589
|
+
async function select(prompt, options) {
|
|
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
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
stdinIsTTY: Boolean(defaultStdin2.isTTY),
|
|
609
|
+
stdoutIsTTY: Boolean(defaultStdout.isTTY),
|
|
610
|
+
ask(prompt) {
|
|
611
|
+
return getInterface().question(prompt);
|
|
612
|
+
},
|
|
613
|
+
select,
|
|
614
|
+
secret,
|
|
615
|
+
write(message) {
|
|
616
|
+
defaultStdout.write(message);
|
|
617
|
+
},
|
|
618
|
+
error(message) {
|
|
619
|
+
defaultStderr.write(message);
|
|
620
|
+
},
|
|
621
|
+
close() {
|
|
622
|
+
rl?.close();
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function resolveSetupPath(targetPath) {
|
|
627
|
+
return targetPath ? path4.resolve(targetPath) : getDefaultGlobalConfigPath();
|
|
628
|
+
}
|
|
629
|
+
function buildOpenAISetupConfig(apiKey) {
|
|
630
|
+
return {
|
|
631
|
+
...defaultConfig,
|
|
632
|
+
provider: {
|
|
633
|
+
...defaultConfig.provider,
|
|
634
|
+
provider: "openai",
|
|
635
|
+
model: "gpt-5-nano",
|
|
636
|
+
baseUrl: "https://api.openai.com/v1",
|
|
637
|
+
apiKey
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function getSetupPresenter(io) {
|
|
642
|
+
return createPresentation(io.stdoutIsTTY);
|
|
643
|
+
}
|
|
644
|
+
async function promptForProvider(io) {
|
|
645
|
+
if (io.select) {
|
|
646
|
+
const choice = await io.select("Select provider for this machine", ["OpenAI"]);
|
|
647
|
+
if (choice === "OpenAI") {
|
|
648
|
+
return "openai";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
while (true) {
|
|
652
|
+
const answer = (await io.ask("Provider [OpenAI]: ")).trim().toLowerCase();
|
|
653
|
+
if (answer === "" || answer === "openai") {
|
|
654
|
+
return "openai";
|
|
655
|
+
}
|
|
656
|
+
io.error("Only OpenAI is supported in guided setup right now.\n");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async function promptForApiKey(io) {
|
|
660
|
+
while (true) {
|
|
661
|
+
const answer = (await (io.secret ? io.secret("Enter your OpenAI API key (input hidden): ") : io.ask("Enter your OpenAI API key: "))).trim();
|
|
662
|
+
if (answer.length > 0) {
|
|
663
|
+
return answer;
|
|
664
|
+
}
|
|
665
|
+
io.error("API key cannot be empty.\n");
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async function promptForOverwrite(io, targetPath) {
|
|
669
|
+
while (true) {
|
|
670
|
+
const answer = (await io.ask(
|
|
671
|
+
`Config file already exists at ${targetPath}. Overwrite? [y/N]: `
|
|
672
|
+
)).trim().toLowerCase();
|
|
673
|
+
if (answer === "" || answer === "n" || answer === "no") {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
if (answer === "y" || answer === "yes") {
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
io.error("Please answer y or n.\n");
|
|
680
|
+
}
|
|
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
|
+
}
|
|
713
|
+
async function configSetup(options = {}) {
|
|
714
|
+
void options.global;
|
|
715
|
+
const io = options.io ?? createTerminalIO();
|
|
716
|
+
const ui = getSetupPresenter(io);
|
|
717
|
+
try {
|
|
718
|
+
if (!io.stdinIsTTY || !io.stdoutIsTTY) {
|
|
719
|
+
io.error(
|
|
720
|
+
"sift config setup is interactive and requires a TTY. Use 'sift config init --global' for a non-interactive template.\n"
|
|
721
|
+
);
|
|
722
|
+
return 1;
|
|
723
|
+
}
|
|
724
|
+
io.write(`${ui.welcome("Let's keep the expensive model for the interesting bits.")}
|
|
725
|
+
`);
|
|
726
|
+
const resolvedPath = resolveSetupPath(options.targetPath);
|
|
727
|
+
if (fs3.existsSync(resolvedPath)) {
|
|
728
|
+
const shouldOverwrite = await promptForOverwrite(io, resolvedPath);
|
|
729
|
+
if (!shouldOverwrite) {
|
|
730
|
+
io.write(`${ui.note("Aborted.")}
|
|
731
|
+
`);
|
|
732
|
+
return 1;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
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
|
+
`);
|
|
742
|
+
io.write(
|
|
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
|
+
`
|
|
749
|
+
);
|
|
750
|
+
const apiKey = await promptForApiKey(io);
|
|
751
|
+
const config = buildOpenAISetupConfig(apiKey);
|
|
752
|
+
const writtenPath = writeConfigFile({
|
|
753
|
+
targetPath: resolvedPath,
|
|
754
|
+
config,
|
|
755
|
+
overwrite: true
|
|
756
|
+
});
|
|
757
|
+
writeSetupSuccess(io, writtenPath);
|
|
758
|
+
const activeConfigPath = findConfigPath();
|
|
759
|
+
if (activeConfigPath && path4.resolve(activeConfigPath) !== path4.resolve(writtenPath)) {
|
|
760
|
+
writeOverrideWarning(io, activeConfigPath);
|
|
761
|
+
}
|
|
762
|
+
writeNextSteps(io);
|
|
763
|
+
return 0;
|
|
764
|
+
} finally {
|
|
765
|
+
io.close?.();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
352
768
|
|
|
353
769
|
// src/commands/config.ts
|
|
354
770
|
var MASKED_SECRET = "***";
|
|
@@ -369,10 +785,21 @@ function maskConfigSecrets(value) {
|
|
|
369
785
|
}
|
|
370
786
|
return output;
|
|
371
787
|
}
|
|
372
|
-
function configInit(targetPath) {
|
|
373
|
-
const
|
|
374
|
-
|
|
788
|
+
function configInit(targetPath, global = false) {
|
|
789
|
+
const path5 = writeExampleConfig({
|
|
790
|
+
targetPath,
|
|
791
|
+
global
|
|
792
|
+
});
|
|
793
|
+
if (!process.stdout.isTTY) {
|
|
794
|
+
process.stdout.write(`${path5}
|
|
375
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
|
+
);
|
|
376
803
|
}
|
|
377
804
|
function configShow(configPath, showSecrets = false) {
|
|
378
805
|
const config = resolveConfig({
|
|
@@ -389,24 +816,32 @@ function configValidate(configPath) {
|
|
|
389
816
|
env: process.env
|
|
390
817
|
});
|
|
391
818
|
const resolvedPath = findConfigPath(configPath);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
`);
|
|
396
828
|
}
|
|
397
829
|
|
|
398
830
|
// src/commands/doctor.ts
|
|
399
|
-
function runDoctor(config) {
|
|
831
|
+
function runDoctor(config, configPath) {
|
|
832
|
+
const ui = createPresentation(Boolean(process.stdout.isTTY));
|
|
400
833
|
const lines = [
|
|
401
834
|
"sift doctor",
|
|
835
|
+
"A quick check for your local setup.",
|
|
402
836
|
"mode: local config completeness check",
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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))
|
|
410
845
|
];
|
|
411
846
|
process.stdout.write(`${lines.join("\n")}
|
|
412
847
|
`);
|
|
@@ -427,8 +862,16 @@ function runDoctor(config) {
|
|
|
427
862
|
);
|
|
428
863
|
}
|
|
429
864
|
if (problems.length > 0) {
|
|
430
|
-
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")}
|
|
431
873
|
`);
|
|
874
|
+
}
|
|
432
875
|
return 1;
|
|
433
876
|
}
|
|
434
877
|
return 0;
|
|
@@ -457,7 +900,7 @@ function showPreset(config, name, includeInternal = false) {
|
|
|
457
900
|
// src/core/exec.ts
|
|
458
901
|
import { spawn } from "child_process";
|
|
459
902
|
import { constants as osConstants } from "os";
|
|
460
|
-
import
|
|
903
|
+
import pc3 from "picocolors";
|
|
461
904
|
|
|
462
905
|
// src/core/gate.ts
|
|
463
906
|
var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
|
|
@@ -506,8 +949,31 @@ function evaluateGate(args) {
|
|
|
506
949
|
return { shouldFail: false };
|
|
507
950
|
}
|
|
508
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
|
+
|
|
509
975
|
// src/core/run.ts
|
|
510
|
-
import
|
|
976
|
+
import pc2 from "picocolors";
|
|
511
977
|
|
|
512
978
|
// src/providers/systemInstruction.ts
|
|
513
979
|
var REDUCTION_SYSTEM_INSTRUCTION = "You reduce noisy command output into compact answers for agents and automation.";
|
|
@@ -583,7 +1049,7 @@ var OpenAIProvider = class {
|
|
|
583
1049
|
if (!text) {
|
|
584
1050
|
throw new Error("Provider returned an empty response");
|
|
585
1051
|
}
|
|
586
|
-
|
|
1052
|
+
const result = {
|
|
587
1053
|
text,
|
|
588
1054
|
usage: data?.usage ? {
|
|
589
1055
|
inputTokens: data.usage.input_tokens,
|
|
@@ -592,13 +1058,14 @@ var OpenAIProvider = class {
|
|
|
592
1058
|
} : void 0,
|
|
593
1059
|
raw: data
|
|
594
1060
|
};
|
|
1061
|
+
clearTimeout(timeout);
|
|
1062
|
+
return result;
|
|
595
1063
|
} catch (error) {
|
|
1064
|
+
clearTimeout(timeout);
|
|
596
1065
|
if (error.name === "AbortError") {
|
|
597
1066
|
throw new Error("Provider request timed out");
|
|
598
1067
|
}
|
|
599
1068
|
throw error;
|
|
600
|
-
} finally {
|
|
601
|
-
clearTimeout(timeout);
|
|
602
1069
|
}
|
|
603
1070
|
}
|
|
604
1071
|
};
|
|
@@ -680,7 +1147,7 @@ var OpenAICompatibleProvider = class {
|
|
|
680
1147
|
if (!text.trim()) {
|
|
681
1148
|
throw new Error("Provider returned an empty response");
|
|
682
1149
|
}
|
|
683
|
-
|
|
1150
|
+
const result = {
|
|
684
1151
|
text,
|
|
685
1152
|
usage: data?.usage ? {
|
|
686
1153
|
inputTokens: data.usage.prompt_tokens,
|
|
@@ -689,13 +1156,14 @@ var OpenAICompatibleProvider = class {
|
|
|
689
1156
|
} : void 0,
|
|
690
1157
|
raw: data
|
|
691
1158
|
};
|
|
1159
|
+
clearTimeout(timeout);
|
|
1160
|
+
return result;
|
|
692
1161
|
} catch (error) {
|
|
1162
|
+
clearTimeout(timeout);
|
|
693
1163
|
if (error.name === "AbortError") {
|
|
694
1164
|
throw new Error("Provider request timed out");
|
|
695
1165
|
}
|
|
696
1166
|
throw error;
|
|
697
|
-
} finally {
|
|
698
|
-
clearTimeout(timeout);
|
|
699
1167
|
}
|
|
700
1168
|
}
|
|
701
1169
|
};
|
|
@@ -900,6 +1368,19 @@ function buildPrompt(args) {
|
|
|
900
1368
|
policyName: args.policyName,
|
|
901
1369
|
outputContract: args.outputContract
|
|
902
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
|
+
] : [];
|
|
903
1384
|
const prompt = [
|
|
904
1385
|
"You are Sift, a CLI output reduction assistant for downstream agents and automation.",
|
|
905
1386
|
"Hard rules:",
|
|
@@ -907,6 +1388,7 @@ function buildPrompt(args) {
|
|
|
907
1388
|
"",
|
|
908
1389
|
`Task policy: ${policy.name}`,
|
|
909
1390
|
...policy.taskRules.map((rule) => `- ${rule}`),
|
|
1391
|
+
...detailRules.map((rule) => `- ${rule}`),
|
|
910
1392
|
...policy.outputContract ? ["", `Output contract: ${policy.outputContract}`] : [],
|
|
911
1393
|
"",
|
|
912
1394
|
`Question: ${args.question}`,
|
|
@@ -1019,6 +1501,410 @@ function inferPackage(line) {
|
|
|
1019
1501
|
function inferRemediation(pkg2) {
|
|
1020
1502
|
return `Upgrade ${pkg2} to a patched version.`;
|
|
1021
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
|
+
}
|
|
1022
1908
|
function auditCriticalHeuristic(input) {
|
|
1023
1909
|
const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
1024
1910
|
if (!/\b(critical|high)\b/i.test(line)) {
|
|
@@ -1089,7 +1975,7 @@ function infraRiskHeuristic(input) {
|
|
|
1089
1975
|
}
|
|
1090
1976
|
return null;
|
|
1091
1977
|
}
|
|
1092
|
-
function applyHeuristicPolicy(policyName, input) {
|
|
1978
|
+
function applyHeuristicPolicy(policyName, input, detail) {
|
|
1093
1979
|
if (!policyName) {
|
|
1094
1980
|
return null;
|
|
1095
1981
|
}
|
|
@@ -1099,6 +1985,9 @@ function applyHeuristicPolicy(policyName, input) {
|
|
|
1099
1985
|
if (policyName === "infra-risk") {
|
|
1100
1986
|
return infraRiskHeuristic(input);
|
|
1101
1987
|
}
|
|
1988
|
+
if (policyName === "test-status") {
|
|
1989
|
+
return testStatusHeuristic(input, detail);
|
|
1990
|
+
}
|
|
1102
1991
|
return null;
|
|
1103
1992
|
}
|
|
1104
1993
|
|
|
@@ -1228,6 +2117,7 @@ function buildDryRunOutput(args) {
|
|
|
1228
2117
|
},
|
|
1229
2118
|
question: args.request.question,
|
|
1230
2119
|
format: args.request.format,
|
|
2120
|
+
detail: args.request.detail ?? null,
|
|
1231
2121
|
responseMode: args.responseMode,
|
|
1232
2122
|
policy: args.request.policyName ?? null,
|
|
1233
2123
|
heuristicOutput: args.heuristicOutput ?? null,
|
|
@@ -1247,35 +2137,42 @@ function buildDryRunOutput(args) {
|
|
|
1247
2137
|
async function delay(ms) {
|
|
1248
2138
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1249
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
|
+
}
|
|
1250
2150
|
async function generateWithRetry(args) {
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
process.stderr.write(
|
|
1271
|
-
`${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}
|
|
1272
2170
|
`
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1275
|
-
await delay(RETRY_DELAY_MS);
|
|
2171
|
+
);
|
|
1276
2172
|
}
|
|
2173
|
+
await delay(RETRY_DELAY_MS);
|
|
1277
2174
|
}
|
|
1278
|
-
|
|
2175
|
+
return generate();
|
|
1279
2176
|
}
|
|
1280
2177
|
async function runSift(request) {
|
|
1281
2178
|
const prepared = prepareInput(request.stdin, request.config.input);
|
|
@@ -1283,23 +2180,25 @@ async function runSift(request) {
|
|
|
1283
2180
|
question: request.question,
|
|
1284
2181
|
format: request.format,
|
|
1285
2182
|
input: prepared.truncated,
|
|
2183
|
+
detail: request.detail,
|
|
1286
2184
|
policyName: request.policyName,
|
|
1287
2185
|
outputContract: request.outputContract
|
|
1288
2186
|
});
|
|
1289
2187
|
const provider = createProvider(request.config);
|
|
1290
2188
|
if (request.config.runtime.verbose) {
|
|
1291
2189
|
process.stderr.write(
|
|
1292
|
-
`${
|
|
2190
|
+
`${pc2.dim("sift")} provider=${provider.name} model=${request.config.provider.model} base_url=${request.config.provider.baseUrl} input_chars=${prepared.meta.finalLength}
|
|
1293
2191
|
`
|
|
1294
2192
|
);
|
|
1295
2193
|
}
|
|
1296
2194
|
const heuristicOutput = applyHeuristicPolicy(
|
|
1297
2195
|
request.policyName,
|
|
1298
|
-
prepared.truncated
|
|
2196
|
+
prepared.truncated,
|
|
2197
|
+
request.detail
|
|
1299
2198
|
);
|
|
1300
2199
|
if (heuristicOutput) {
|
|
1301
2200
|
if (request.config.runtime.verbose) {
|
|
1302
|
-
process.stderr.write(`${
|
|
2201
|
+
process.stderr.write(`${pc2.dim("sift")} heuristic=${request.policyName}
|
|
1303
2202
|
`);
|
|
1304
2203
|
}
|
|
1305
2204
|
if (request.dryRun) {
|
|
@@ -1312,7 +2211,11 @@ async function runSift(request) {
|
|
|
1312
2211
|
heuristicOutput
|
|
1313
2212
|
});
|
|
1314
2213
|
}
|
|
1315
|
-
return
|
|
2214
|
+
return withInsufficientHint({
|
|
2215
|
+
output: heuristicOutput,
|
|
2216
|
+
request,
|
|
2217
|
+
prepared
|
|
2218
|
+
});
|
|
1316
2219
|
}
|
|
1317
2220
|
if (request.dryRun) {
|
|
1318
2221
|
return buildDryRunOutput({
|
|
@@ -1338,15 +2241,23 @@ async function runSift(request) {
|
|
|
1338
2241
|
})) {
|
|
1339
2242
|
throw new Error("Model output rejected by quality gate");
|
|
1340
2243
|
}
|
|
1341
|
-
return
|
|
2244
|
+
return withInsufficientHint({
|
|
2245
|
+
output: normalizeOutput(result.text, responseMode),
|
|
2246
|
+
request,
|
|
2247
|
+
prepared
|
|
2248
|
+
});
|
|
1342
2249
|
} catch (error) {
|
|
1343
2250
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
1344
|
-
return
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
|
1350
2261
|
});
|
|
1351
2262
|
}
|
|
1352
2263
|
}
|
|
@@ -1429,6 +2340,15 @@ function buildCommandPreview(request) {
|
|
|
1429
2340
|
}
|
|
1430
2341
|
return (request.command ?? []).join(" ");
|
|
1431
2342
|
}
|
|
2343
|
+
function getExecSuccessShortcut(args) {
|
|
2344
|
+
if (args.exitCode !== 0) {
|
|
2345
|
+
return null;
|
|
2346
|
+
}
|
|
2347
|
+
if (args.presetName === "typecheck-summary" && args.capturedOutput.trim() === "") {
|
|
2348
|
+
return "No type errors.";
|
|
2349
|
+
}
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
1432
2352
|
async function runExec(request) {
|
|
1433
2353
|
const hasArgvCommand = Array.isArray(request.command) && request.command.length > 0;
|
|
1434
2354
|
const hasShellCommand = typeof request.shellCommand === "string";
|
|
@@ -1438,7 +2358,7 @@ async function runExec(request) {
|
|
|
1438
2358
|
const shellPath = process.env.SHELL || "/bin/bash";
|
|
1439
2359
|
if (request.config.runtime.verbose) {
|
|
1440
2360
|
process.stderr.write(
|
|
1441
|
-
`${
|
|
2361
|
+
`${pc3.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${buildCommandPreview(request)}
|
|
1442
2362
|
`
|
|
1443
2363
|
);
|
|
1444
2364
|
}
|
|
@@ -1447,7 +2367,6 @@ async function runExec(request) {
|
|
|
1447
2367
|
let bypassed = false;
|
|
1448
2368
|
let childStatus = null;
|
|
1449
2369
|
let childSignal = null;
|
|
1450
|
-
let childSpawnError = null;
|
|
1451
2370
|
const child = hasShellCommand ? spawn(shellPath, ["-lc", request.shellCommand], {
|
|
1452
2371
|
stdio: ["inherit", "pipe", "pipe"]
|
|
1453
2372
|
}) : spawn(request.command[0], request.command.slice(1), {
|
|
@@ -1466,7 +2385,7 @@ async function runExec(request) {
|
|
|
1466
2385
|
}
|
|
1467
2386
|
bypassed = true;
|
|
1468
2387
|
if (request.config.runtime.verbose) {
|
|
1469
|
-
process.stderr.write(`${
|
|
2388
|
+
process.stderr.write(`${pc3.dim("sift")} bypass=interactive-prompt
|
|
1470
2389
|
`);
|
|
1471
2390
|
}
|
|
1472
2391
|
process.stderr.write(capture.render());
|
|
@@ -1475,7 +2394,6 @@ async function runExec(request) {
|
|
|
1475
2394
|
child.stderr.on("data", handleChunk);
|
|
1476
2395
|
await new Promise((resolve, reject) => {
|
|
1477
2396
|
child.on("error", (error) => {
|
|
1478
|
-
childSpawnError = error;
|
|
1479
2397
|
reject(error);
|
|
1480
2398
|
});
|
|
1481
2399
|
child.on("close", (status, signal) => {
|
|
@@ -1489,21 +2407,49 @@ async function runExec(request) {
|
|
|
1489
2407
|
}
|
|
1490
2408
|
throw new Error("Failed to start child process.");
|
|
1491
2409
|
});
|
|
1492
|
-
if (childSpawnError) {
|
|
1493
|
-
throw childSpawnError;
|
|
1494
|
-
}
|
|
1495
2410
|
const exitCode = normalizeChildExitCode(childStatus, childSignal);
|
|
2411
|
+
const capturedOutput = capture.render();
|
|
1496
2412
|
if (request.config.runtime.verbose) {
|
|
1497
2413
|
process.stderr.write(
|
|
1498
|
-
`${
|
|
2414
|
+
`${pc3.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
|
|
1499
2415
|
`
|
|
1500
2416
|
);
|
|
1501
2417
|
}
|
|
1502
2418
|
if (!bypassed) {
|
|
1503
|
-
|
|
2419
|
+
if (request.showRaw && capturedOutput.length > 0) {
|
|
2420
|
+
process.stderr.write(capturedOutput);
|
|
2421
|
+
if (!capturedOutput.endsWith("\n")) {
|
|
2422
|
+
process.stderr.write("\n");
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
const execSuccessShortcut = getExecSuccessShortcut({
|
|
2426
|
+
presetName: request.presetName,
|
|
2427
|
+
exitCode,
|
|
2428
|
+
capturedOutput
|
|
2429
|
+
});
|
|
2430
|
+
if (execSuccessShortcut && !request.dryRun) {
|
|
2431
|
+
if (request.config.runtime.verbose) {
|
|
2432
|
+
process.stderr.write(
|
|
2433
|
+
`${pc3.dim("sift")} exec_shortcut=${request.presetName}
|
|
2434
|
+
`
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
process.stdout.write(`${execSuccessShortcut}
|
|
2438
|
+
`);
|
|
2439
|
+
return exitCode;
|
|
2440
|
+
}
|
|
2441
|
+
let output = await runSift({
|
|
1504
2442
|
...request,
|
|
1505
|
-
stdin:
|
|
2443
|
+
stdin: capturedOutput
|
|
1506
2444
|
});
|
|
2445
|
+
if (isInsufficientSignalOutput(output)) {
|
|
2446
|
+
output = buildInsufficientSignalOutput({
|
|
2447
|
+
presetName: request.presetName,
|
|
2448
|
+
originalLength: capture.getTotalChars(),
|
|
2449
|
+
truncatedApplied: capture.wasTruncated(),
|
|
2450
|
+
exitCode
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
1507
2453
|
process.stdout.write(`${output}
|
|
1508
2454
|
`);
|
|
1509
2455
|
if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
|
|
@@ -1537,10 +2483,27 @@ function getPreset(config, name) {
|
|
|
1537
2483
|
return preset;
|
|
1538
2484
|
}
|
|
1539
2485
|
|
|
1540
|
-
// src/cli.ts
|
|
2486
|
+
// src/cli-app.ts
|
|
1541
2487
|
var require2 = createRequire(import.meta.url);
|
|
1542
2488
|
var pkg = require2("../package.json");
|
|
1543
|
-
var
|
|
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
|
+
};
|
|
1544
2507
|
function toNumber(value) {
|
|
1545
2508
|
if (value === void 0 || value === null || value === "") {
|
|
1546
2509
|
return void 0;
|
|
@@ -1585,44 +2548,32 @@ function applySharedOptions(command) {
|
|
|
1585
2548
|
).option(
|
|
1586
2549
|
"--json-response-format <mode>",
|
|
1587
2550
|
"JSON response format mode: auto | on | off"
|
|
1588
|
-
).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(
|
|
1589
2555
|
"--fail-on",
|
|
1590
2556
|
"Fail with exit code 1 when a supported built-in preset produces a blocking result"
|
|
1591
2557
|
).option("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
|
|
1592
2558
|
}
|
|
1593
|
-
|
|
1594
|
-
if (
|
|
1595
|
-
|
|
1596
|
-
assertSupportedFailOnFormat({
|
|
1597
|
-
presetName: args.presetName,
|
|
1598
|
-
format: args.format
|
|
1599
|
-
});
|
|
2559
|
+
function normalizeDetail(value) {
|
|
2560
|
+
if (value === void 0 || value === null || value === "") {
|
|
2561
|
+
return void 0;
|
|
1600
2562
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
env: process.env,
|
|
1604
|
-
cliOverrides: buildCliOverrides(args.options)
|
|
1605
|
-
});
|
|
1606
|
-
const stdin = await readStdin();
|
|
1607
|
-
const output = await runSift({
|
|
1608
|
-
question: args.question,
|
|
1609
|
-
format: args.format,
|
|
1610
|
-
stdin,
|
|
1611
|
-
config,
|
|
1612
|
-
dryRun: Boolean(args.options.dryRun),
|
|
1613
|
-
presetName: args.presetName,
|
|
1614
|
-
policyName: args.policyName,
|
|
1615
|
-
outputContract: args.outputContract,
|
|
1616
|
-
fallbackJson: args.fallbackJson
|
|
1617
|
-
});
|
|
1618
|
-
process.stdout.write(`${output}
|
|
1619
|
-
`);
|
|
1620
|
-
if (Boolean(args.options.failOn) && !Boolean(args.options.dryRun) && args.presetName && evaluateGate({
|
|
1621
|
-
presetName: args.presetName,
|
|
1622
|
-
output
|
|
1623
|
-
}).shouldFail) {
|
|
1624
|
-
process.exitCode = 1;
|
|
2563
|
+
if (value === "standard" || value === "focused" || value === "verbose") {
|
|
2564
|
+
return value;
|
|
1625
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.");
|
|
2575
|
+
}
|
|
2576
|
+
return requested;
|
|
1626
2577
|
}
|
|
1627
2578
|
function extractExecCommand(options) {
|
|
1628
2579
|
const passthrough = Array.isArray(options["--"]) ? options["--"].map((value) => String(value)) : [];
|
|
@@ -1638,160 +2589,278 @@ function extractExecCommand(options) {
|
|
|
1638
2589
|
shellCommand
|
|
1639
2590
|
};
|
|
1640
2591
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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)
|
|
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
|
|
1647
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
|
+
}
|
|
1648
2651
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
cli.command("exec [question]", "Run a command and reduce its output").allowUnknownOptions()
|
|
1689
|
-
).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) => {
|
|
1690
|
-
if (question === "preset") {
|
|
1691
|
-
throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
|
|
1692
|
-
}
|
|
1693
|
-
const presetName = typeof options.preset === "string" && options.preset.length > 0 ? options.preset : void 0;
|
|
1694
|
-
if (presetName) {
|
|
1695
|
-
if (question) {
|
|
1696
|
-
throw new Error("Use either a freeform question or --preset <name>, not both.");
|
|
1697
|
-
}
|
|
1698
|
-
const preset = getPreset(
|
|
1699
|
-
resolveConfig({
|
|
1700
|
-
configPath: options.config,
|
|
1701
|
-
env: process.env,
|
|
1702
|
-
cliOverrides: buildCliOverrides(options)
|
|
1703
|
-
}),
|
|
1704
|
-
presetName
|
|
1705
|
-
);
|
|
1706
|
-
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({
|
|
1707
2691
|
question: preset.question,
|
|
1708
2692
|
format: options.format ?? preset.format,
|
|
1709
|
-
presetName,
|
|
2693
|
+
presetName: name,
|
|
2694
|
+
detail: resolveDetail({
|
|
2695
|
+
presetName: name,
|
|
2696
|
+
options
|
|
2697
|
+
}),
|
|
1710
2698
|
policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
|
|
1711
2699
|
options,
|
|
1712
2700
|
outputContract: preset.outputContract,
|
|
1713
2701
|
fallbackJson: preset.fallbackJson
|
|
1714
2702
|
});
|
|
1715
|
-
return;
|
|
1716
|
-
}
|
|
1717
|
-
if (!question) {
|
|
1718
|
-
throw new Error("Missing question or preset.");
|
|
1719
|
-
}
|
|
1720
|
-
const format = options.format ?? "brief";
|
|
1721
|
-
await executeExec({
|
|
1722
|
-
question,
|
|
1723
|
-
format,
|
|
1724
|
-
options
|
|
1725
2703
|
});
|
|
1726
|
-
|
|
1727
|
-
cli.command(
|
|
1728
|
-
"
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
|
2749
|
+
});
|
|
1752
2750
|
});
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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}`);
|
|
1759
2778
|
});
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
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;
|
|
1767
2795
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
if (!question) {
|
|
1777
|
-
throw new Error("Missing question.");
|
|
1778
|
-
}
|
|
1779
|
-
const format = options.format ?? "brief";
|
|
1780
|
-
await executeRun({
|
|
1781
|
-
question,
|
|
1782
|
-
format,
|
|
1783
|
-
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}`);
|
|
1784
2804
|
});
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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)}
|
|
2829
|
+
`
|
|
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 });
|
|
1790
2849
|
await cli.runMatchedCommand();
|
|
1791
2850
|
}
|
|
1792
|
-
|
|
2851
|
+
function handleCliError(error, stderr = process.stderr) {
|
|
1793
2852
|
const message = error instanceof Error ? error.message : "Unexpected error.";
|
|
1794
|
-
|
|
2853
|
+
if (stderr.isTTY) {
|
|
2854
|
+
stderr.write(`${createPresentation(true).error(message)}
|
|
2855
|
+
`);
|
|
2856
|
+
} else {
|
|
2857
|
+
stderr.write(`${message}
|
|
1795
2858
|
`);
|
|
2859
|
+
}
|
|
1796
2860
|
process.exitCode = 1;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// src/cli.ts
|
|
2864
|
+
runCli().catch((error) => {
|
|
2865
|
+
handleCliError(error);
|
|
1797
2866
|
});
|