@bilalimamoglu/sift 0.2.1 → 0.2.2
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 +46 -2
- package/dist/cli.js +303 -19
- package/dist/index.js +39 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# sift
|
|
2
2
|
|
|
3
|
+
<img src="assets/brand/sift-logo-badge-monochrome.svg" alt="sift logo" width="88" />
|
|
4
|
+
|
|
3
5
|
`sift` is a small command-output reducer for agent workflows.
|
|
4
6
|
|
|
5
7
|
Instead of feeding a model the full output of `pytest`, `git diff`, `npm audit`, `tsc --noEmit`, `eslint .`, or `terraform plan`, you run the command through `sift`. It captures the output, trims the noise, and returns a much smaller answer.
|
|
@@ -24,7 +26,21 @@ npm install -g @bilalimamoglu/sift
|
|
|
24
26
|
|
|
25
27
|
## One-time setup
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
The easiest path is the guided setup:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
sift config setup
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That writes a machine-wide config to:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
~/.config/sift/config.yaml
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
After that, any terminal can use `sift` without per-project setup. A repo-local config can still override it later.
|
|
42
|
+
|
|
43
|
+
If you want to set things up manually, for OpenAI-hosted models:
|
|
28
44
|
|
|
29
45
|
```bash
|
|
30
46
|
export SIFT_PROVIDER=openai
|
|
@@ -33,12 +49,30 @@ export SIFT_MODEL=gpt-5-nano
|
|
|
33
49
|
export OPENAI_API_KEY=your_openai_api_key
|
|
34
50
|
```
|
|
35
51
|
|
|
36
|
-
Or
|
|
52
|
+
Or write a template config file:
|
|
37
53
|
|
|
38
54
|
```bash
|
|
39
55
|
sift config init
|
|
40
56
|
```
|
|
41
57
|
|
|
58
|
+
For a manual machine-wide template:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
sift config init --global
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That writes:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
~/.config/sift/config.yaml
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then keep the API key in your shell profile so every terminal can use it:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
export OPENAI_API_KEY=your_openai_api_key
|
|
74
|
+
```
|
|
75
|
+
|
|
42
76
|
If you use a different OpenAI-compatible endpoint, switch to `provider: openai-compatible` and use either the endpoint's native API key env var or the generic fallback:
|
|
43
77
|
|
|
44
78
|
```bash
|
|
@@ -124,6 +158,7 @@ Built-in JSON and verdict flows return strict error objects on provider or model
|
|
|
124
158
|
Useful commands:
|
|
125
159
|
|
|
126
160
|
```bash
|
|
161
|
+
sift config setup
|
|
127
162
|
sift config init
|
|
128
163
|
sift config show
|
|
129
164
|
sift config validate
|
|
@@ -195,6 +230,15 @@ The workflow:
|
|
|
195
230
|
5. creates and pushes the `vX.Y.Z` tag
|
|
196
231
|
6. creates a GitHub Release
|
|
197
232
|
|
|
233
|
+
## Brand assets
|
|
234
|
+
|
|
235
|
+
Curated public logo assets live in `assets/brand/`.
|
|
236
|
+
|
|
237
|
+
Included SVG sets:
|
|
238
|
+
- badge/app: teal, black, monochrome
|
|
239
|
+
- icon-only: teal, black, monochrome
|
|
240
|
+
- 24px icon: teal, black, monochrome
|
|
241
|
+
|
|
198
242
|
## License
|
|
199
243
|
|
|
200
244
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -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,226 @@ 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/commands/config-setup.ts
|
|
379
|
+
import fs3 from "fs";
|
|
380
|
+
import path4 from "path";
|
|
381
|
+
import { createInterface } from "readline/promises";
|
|
382
|
+
import { clearLine, cursorTo, emitKeypressEvents, moveCursor } from "readline";
|
|
383
|
+
import { stdin as defaultStdin, stdout as defaultStdout, stderr as defaultStderr } from "process";
|
|
384
|
+
function createTerminalIO() {
|
|
385
|
+
let rl;
|
|
386
|
+
function getInterface() {
|
|
387
|
+
if (!rl) {
|
|
388
|
+
rl = createInterface({
|
|
389
|
+
input: defaultStdin,
|
|
390
|
+
output: defaultStdout,
|
|
391
|
+
terminal: true
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return rl;
|
|
395
|
+
}
|
|
396
|
+
async function select(prompt, options) {
|
|
397
|
+
const input = defaultStdin;
|
|
398
|
+
const output = defaultStdout;
|
|
399
|
+
const promptLine = `${prompt} (use \u2191/\u2193 and Enter)`;
|
|
400
|
+
let index = 0;
|
|
401
|
+
const lineCount = options.length + 1;
|
|
402
|
+
emitKeypressEvents(input);
|
|
403
|
+
input.resume();
|
|
404
|
+
const wasRaw = input.isTTY ? input.isRaw : false;
|
|
405
|
+
input.setRawMode?.(true);
|
|
406
|
+
const render = () => {
|
|
407
|
+
cursorTo(output, 0);
|
|
408
|
+
clearLine(output, 0);
|
|
409
|
+
output.write(`${promptLine}
|
|
410
|
+
`);
|
|
411
|
+
for (let optionIndex = 0; optionIndex < options.length; optionIndex += 1) {
|
|
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);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
stdinIsTTY: Boolean(defaultStdin.isTTY),
|
|
455
|
+
stdoutIsTTY: Boolean(defaultStdout.isTTY),
|
|
456
|
+
ask(prompt) {
|
|
457
|
+
return getInterface().question(prompt);
|
|
458
|
+
},
|
|
459
|
+
select,
|
|
460
|
+
write(message) {
|
|
461
|
+
defaultStdout.write(message);
|
|
462
|
+
},
|
|
463
|
+
error(message) {
|
|
464
|
+
defaultStderr.write(message);
|
|
465
|
+
},
|
|
466
|
+
close() {
|
|
467
|
+
rl?.close();
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function resolveSetupPath(targetPath) {
|
|
472
|
+
return targetPath ? path4.resolve(targetPath) : getDefaultGlobalConfigPath();
|
|
473
|
+
}
|
|
474
|
+
function buildOpenAISetupConfig(apiKey) {
|
|
475
|
+
return {
|
|
476
|
+
...defaultConfig,
|
|
477
|
+
provider: {
|
|
478
|
+
...defaultConfig.provider,
|
|
479
|
+
provider: "openai",
|
|
480
|
+
model: "gpt-5-nano",
|
|
481
|
+
baseUrl: "https://api.openai.com/v1",
|
|
482
|
+
apiKey
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
async function promptForProvider(io) {
|
|
487
|
+
if (io.select) {
|
|
488
|
+
const choice = await io.select("Select provider", ["OpenAI"]);
|
|
489
|
+
if (choice === "OpenAI") {
|
|
490
|
+
return "openai";
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
while (true) {
|
|
494
|
+
const answer = (await io.ask("Provider [OpenAI]: ")).trim().toLowerCase();
|
|
495
|
+
if (answer === "" || answer === "openai") {
|
|
496
|
+
return "openai";
|
|
497
|
+
}
|
|
498
|
+
io.error("Only OpenAI is supported in guided setup right now.\n");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function promptForApiKey(io) {
|
|
502
|
+
while (true) {
|
|
503
|
+
const answer = (await io.ask("Enter your OpenAI API key: ")).trim();
|
|
504
|
+
if (answer.length > 0) {
|
|
505
|
+
return answer;
|
|
506
|
+
}
|
|
507
|
+
io.error("API key cannot be empty.\n");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function promptForOverwrite(io, targetPath) {
|
|
511
|
+
while (true) {
|
|
512
|
+
const answer = (await io.ask(
|
|
513
|
+
`Config file already exists at ${targetPath}. Overwrite? [y/N]: `
|
|
514
|
+
)).trim().toLowerCase();
|
|
515
|
+
if (answer === "" || answer === "n" || answer === "no") {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
if (answer === "y" || answer === "yes") {
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
io.error("Please answer y or n.\n");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async function configSetup(options = {}) {
|
|
525
|
+
void options.global;
|
|
526
|
+
const io = options.io ?? createTerminalIO();
|
|
527
|
+
try {
|
|
528
|
+
if (!io.stdinIsTTY || !io.stdoutIsTTY) {
|
|
529
|
+
io.error(
|
|
530
|
+
"sift config setup is interactive and requires a TTY. Use 'sift config init --global' for a non-interactive template.\n"
|
|
531
|
+
);
|
|
532
|
+
return 1;
|
|
533
|
+
}
|
|
534
|
+
const resolvedPath = resolveSetupPath(options.targetPath);
|
|
535
|
+
if (fs3.existsSync(resolvedPath)) {
|
|
536
|
+
const shouldOverwrite = await promptForOverwrite(io, resolvedPath);
|
|
537
|
+
if (!shouldOverwrite) {
|
|
538
|
+
io.write("Aborted.\n");
|
|
539
|
+
return 1;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const provider = await promptForProvider(io);
|
|
543
|
+
if (provider !== "openai") {
|
|
544
|
+
io.error("Unsupported provider selection.\n");
|
|
545
|
+
return 1;
|
|
546
|
+
}
|
|
547
|
+
io.write("Using OpenAI defaults.\n");
|
|
548
|
+
io.write("Default model: gpt-5-nano\n");
|
|
549
|
+
io.write("Default base URL: https://api.openai.com/v1\n");
|
|
550
|
+
io.write(
|
|
551
|
+
"You can change these later by editing the config file or running 'sift config show --show-secrets'.\n"
|
|
552
|
+
);
|
|
553
|
+
const apiKey = await promptForApiKey(io);
|
|
554
|
+
const config = buildOpenAISetupConfig(apiKey);
|
|
555
|
+
const writtenPath = writeConfigFile({
|
|
556
|
+
targetPath: resolvedPath,
|
|
557
|
+
config,
|
|
558
|
+
overwrite: true
|
|
559
|
+
});
|
|
560
|
+
io.write(`Wrote ${writtenPath}
|
|
561
|
+
`);
|
|
562
|
+
io.write(
|
|
563
|
+
"This is your machine-wide default config. Repo-local sift.config.yaml can still override it later.\n"
|
|
564
|
+
);
|
|
565
|
+
const activeConfigPath = findConfigPath();
|
|
566
|
+
if (activeConfigPath && path4.resolve(activeConfigPath) !== path4.resolve(writtenPath)) {
|
|
567
|
+
io.write(
|
|
568
|
+
`Note: ${activeConfigPath} currently overrides this machine-wide config in the current directory.
|
|
569
|
+
`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
io.write("Try:\n");
|
|
573
|
+
io.write(" sift doctor\n");
|
|
574
|
+
io.write(" sift exec --preset test-status -- pytest\n");
|
|
575
|
+
return 0;
|
|
576
|
+
} finally {
|
|
577
|
+
io.close?.();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
352
580
|
|
|
353
581
|
// src/commands/config.ts
|
|
354
582
|
var MASKED_SECRET = "***";
|
|
@@ -369,9 +597,12 @@ function maskConfigSecrets(value) {
|
|
|
369
597
|
}
|
|
370
598
|
return output;
|
|
371
599
|
}
|
|
372
|
-
function configInit(targetPath) {
|
|
373
|
-
const
|
|
374
|
-
|
|
600
|
+
function configInit(targetPath, global = false) {
|
|
601
|
+
const path5 = writeExampleConfig({
|
|
602
|
+
targetPath,
|
|
603
|
+
global
|
|
604
|
+
});
|
|
605
|
+
process.stdout.write(`${path5}
|
|
375
606
|
`);
|
|
376
607
|
}
|
|
377
608
|
function configShow(configPath, showSecrets = false) {
|
|
@@ -396,10 +627,11 @@ function configValidate(configPath) {
|
|
|
396
627
|
}
|
|
397
628
|
|
|
398
629
|
// src/commands/doctor.ts
|
|
399
|
-
function runDoctor(config) {
|
|
630
|
+
function runDoctor(config, configPath) {
|
|
400
631
|
const lines = [
|
|
401
632
|
"sift doctor",
|
|
402
633
|
"mode: local config completeness check",
|
|
634
|
+
`configPath: ${configPath ?? "(defaults only)"}`,
|
|
403
635
|
`provider: ${config.provider.provider}`,
|
|
404
636
|
`model: ${config.provider.model}`,
|
|
405
637
|
`baseUrl: ${config.provider.baseUrl}`,
|
|
@@ -1429,6 +1661,15 @@ function buildCommandPreview(request) {
|
|
|
1429
1661
|
}
|
|
1430
1662
|
return (request.command ?? []).join(" ");
|
|
1431
1663
|
}
|
|
1664
|
+
function getExecSuccessShortcut(args) {
|
|
1665
|
+
if (args.exitCode !== 0) {
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
if (args.presetName === "typecheck-summary" && args.capturedOutput.trim() === "") {
|
|
1669
|
+
return "No type errors.";
|
|
1670
|
+
}
|
|
1671
|
+
return null;
|
|
1672
|
+
}
|
|
1432
1673
|
async function runExec(request) {
|
|
1433
1674
|
const hasArgvCommand = Array.isArray(request.command) && request.command.length > 0;
|
|
1434
1675
|
const hasShellCommand = typeof request.shellCommand === "string";
|
|
@@ -1493,6 +1734,7 @@ async function runExec(request) {
|
|
|
1493
1734
|
throw childSpawnError;
|
|
1494
1735
|
}
|
|
1495
1736
|
const exitCode = normalizeChildExitCode(childStatus, childSignal);
|
|
1737
|
+
const capturedOutput = capture.render();
|
|
1496
1738
|
if (request.config.runtime.verbose) {
|
|
1497
1739
|
process.stderr.write(
|
|
1498
1740
|
`${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
|
|
@@ -1500,9 +1742,25 @@ async function runExec(request) {
|
|
|
1500
1742
|
);
|
|
1501
1743
|
}
|
|
1502
1744
|
if (!bypassed) {
|
|
1745
|
+
const execSuccessShortcut = getExecSuccessShortcut({
|
|
1746
|
+
presetName: request.presetName,
|
|
1747
|
+
exitCode,
|
|
1748
|
+
capturedOutput
|
|
1749
|
+
});
|
|
1750
|
+
if (execSuccessShortcut && !request.dryRun) {
|
|
1751
|
+
if (request.config.runtime.verbose) {
|
|
1752
|
+
process.stderr.write(
|
|
1753
|
+
`${pc2.dim("sift")} exec_shortcut=${request.presetName}
|
|
1754
|
+
`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
process.stdout.write(`${execSuccessShortcut}
|
|
1758
|
+
`);
|
|
1759
|
+
return exitCode;
|
|
1760
|
+
}
|
|
1503
1761
|
const output = await runSift({
|
|
1504
1762
|
...request,
|
|
1505
|
-
stdin:
|
|
1763
|
+
stdin: capturedOutput
|
|
1506
1764
|
});
|
|
1507
1765
|
process.stdout.write(`${output}
|
|
1508
1766
|
`);
|
|
@@ -1541,6 +1799,12 @@ function getPreset(config, name) {
|
|
|
1541
1799
|
var require2 = createRequire(import.meta.url);
|
|
1542
1800
|
var pkg = require2("../package.json");
|
|
1543
1801
|
var cli = cac("sift");
|
|
1802
|
+
var HELP_BANNER = [
|
|
1803
|
+
" \\\\ //",
|
|
1804
|
+
" \\\\//",
|
|
1805
|
+
" ||",
|
|
1806
|
+
" o"
|
|
1807
|
+
].join("\n");
|
|
1544
1808
|
function toNumber(value) {
|
|
1545
1809
|
if (value === void 0 || value === null || value === "") {
|
|
1546
1810
|
return void 0;
|
|
@@ -1726,10 +1990,23 @@ applySharedOptions(
|
|
|
1726
1990
|
});
|
|
1727
1991
|
cli.command(
|
|
1728
1992
|
"config <action>",
|
|
1729
|
-
"Config commands: init | show | validate (show/validate use resolved runtime config)"
|
|
1730
|
-
).usage("config <init|show|validate> [options]").example("config
|
|
1993
|
+
"Config commands: setup | init | show | validate (show/validate use resolved runtime config)"
|
|
1994
|
+
).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(
|
|
1995
|
+
"--global",
|
|
1996
|
+
"Use the machine-wide config path (~/.config/sift/config.yaml) for init or setup"
|
|
1997
|
+
).option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action(async (action, options) => {
|
|
1998
|
+
if (action === "setup") {
|
|
1999
|
+
process.exitCode = await configSetup({
|
|
2000
|
+
targetPath: options.path,
|
|
2001
|
+
global: Boolean(options.global)
|
|
2002
|
+
});
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
1731
2005
|
if (action === "init") {
|
|
1732
|
-
configInit(
|
|
2006
|
+
configInit(
|
|
2007
|
+
options.path,
|
|
2008
|
+
Boolean(options.global)
|
|
2009
|
+
);
|
|
1733
2010
|
return;
|
|
1734
2011
|
}
|
|
1735
2012
|
if (action === "show") {
|
|
@@ -1746,11 +2023,12 @@ cli.command(
|
|
|
1746
2023
|
throw new Error(`Unknown config action: ${action}`);
|
|
1747
2024
|
});
|
|
1748
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);
|
|
1749
2027
|
const config = resolveConfig({
|
|
1750
2028
|
configPath: options.config,
|
|
1751
2029
|
env: process.env
|
|
1752
2030
|
});
|
|
1753
|
-
process.exitCode = runDoctor(config);
|
|
2031
|
+
process.exitCode = runDoctor(config, configPath);
|
|
1754
2032
|
});
|
|
1755
2033
|
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) => {
|
|
1756
2034
|
const config = resolveConfig({
|
|
@@ -1783,7 +2061,13 @@ applySharedOptions(
|
|
|
1783
2061
|
options
|
|
1784
2062
|
});
|
|
1785
2063
|
});
|
|
1786
|
-
cli.help()
|
|
2064
|
+
cli.help((sections) => [
|
|
2065
|
+
{
|
|
2066
|
+
body: `${HELP_BANNER}
|
|
2067
|
+
`
|
|
2068
|
+
},
|
|
2069
|
+
...sections
|
|
2070
|
+
]);
|
|
1787
2071
|
cli.version(pkg.version);
|
|
1788
2072
|
async function main() {
|
|
1789
2073
|
cli.parse(process.argv, { run: false });
|
package/dist/index.js
CHANGED
|
@@ -6,12 +6,17 @@ import pc2 from "picocolors";
|
|
|
6
6
|
// src/constants.ts
|
|
7
7
|
import os from "os";
|
|
8
8
|
import path from "path";
|
|
9
|
-
|
|
10
|
-
path.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
function getDefaultGlobalConfigPath() {
|
|
10
|
+
return path.join(os.homedir(), ".config", "sift", "config.yaml");
|
|
11
|
+
}
|
|
12
|
+
function getDefaultConfigSearchPaths() {
|
|
13
|
+
return [
|
|
14
|
+
path.resolve(process.cwd(), "sift.config.yaml"),
|
|
15
|
+
path.resolve(process.cwd(), "sift.config.yml"),
|
|
16
|
+
getDefaultGlobalConfigPath(),
|
|
17
|
+
path.join(os.homedir(), ".config", "sift", "config.yml")
|
|
18
|
+
];
|
|
19
|
+
}
|
|
15
20
|
var INSUFFICIENT_SIGNAL_TEXT = "Insufficient signal in the provided input.";
|
|
16
21
|
var GENERIC_JSON_CONTRACT = '{"answer":string,"evidence":string[],"risks":string[]}';
|
|
17
22
|
var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
|
|
@@ -971,6 +976,15 @@ function buildCommandPreview(request) {
|
|
|
971
976
|
}
|
|
972
977
|
return (request.command ?? []).join(" ");
|
|
973
978
|
}
|
|
979
|
+
function getExecSuccessShortcut(args) {
|
|
980
|
+
if (args.exitCode !== 0) {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
if (args.presetName === "typecheck-summary" && args.capturedOutput.trim() === "") {
|
|
984
|
+
return "No type errors.";
|
|
985
|
+
}
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
974
988
|
async function runExec(request) {
|
|
975
989
|
const hasArgvCommand = Array.isArray(request.command) && request.command.length > 0;
|
|
976
990
|
const hasShellCommand = typeof request.shellCommand === "string";
|
|
@@ -1035,6 +1049,7 @@ async function runExec(request) {
|
|
|
1035
1049
|
throw childSpawnError;
|
|
1036
1050
|
}
|
|
1037
1051
|
const exitCode = normalizeChildExitCode(childStatus, childSignal);
|
|
1052
|
+
const capturedOutput = capture.render();
|
|
1038
1053
|
if (request.config.runtime.verbose) {
|
|
1039
1054
|
process.stderr.write(
|
|
1040
1055
|
`${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
|
|
@@ -1042,9 +1057,25 @@ async function runExec(request) {
|
|
|
1042
1057
|
);
|
|
1043
1058
|
}
|
|
1044
1059
|
if (!bypassed) {
|
|
1060
|
+
const execSuccessShortcut = getExecSuccessShortcut({
|
|
1061
|
+
presetName: request.presetName,
|
|
1062
|
+
exitCode,
|
|
1063
|
+
capturedOutput
|
|
1064
|
+
});
|
|
1065
|
+
if (execSuccessShortcut && !request.dryRun) {
|
|
1066
|
+
if (request.config.runtime.verbose) {
|
|
1067
|
+
process.stderr.write(
|
|
1068
|
+
`${pc2.dim("sift")} exec_shortcut=${request.presetName}
|
|
1069
|
+
`
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
process.stdout.write(`${execSuccessShortcut}
|
|
1073
|
+
`);
|
|
1074
|
+
return exitCode;
|
|
1075
|
+
}
|
|
1045
1076
|
const output = await runSift({
|
|
1046
1077
|
...request,
|
|
1047
|
-
stdin:
|
|
1078
|
+
stdin: capturedOutput
|
|
1048
1079
|
});
|
|
1049
1080
|
process.stdout.write(`${output}
|
|
1050
1081
|
`);
|
|
@@ -1141,7 +1172,7 @@ function findConfigPath(explicitPath) {
|
|
|
1141
1172
|
}
|
|
1142
1173
|
return resolved;
|
|
1143
1174
|
}
|
|
1144
|
-
for (const candidate of
|
|
1175
|
+
for (const candidate of getDefaultConfigSearchPaths()) {
|
|
1145
1176
|
if (fs.existsSync(candidate)) {
|
|
1146
1177
|
return candidate;
|
|
1147
1178
|
}
|