@bilalimamoglu/sift 0.2.0 → 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 +125 -196
- package/dist/cli.js +559 -40
- package/dist/index.d.ts +4 -2
- package/dist/index.js +247 -24
- package/package.json +1 -1
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
|
}
|
|
@@ -51,23 +56,23 @@ function loadRawConfig(explicitPath) {
|
|
|
51
56
|
// src/config/defaults.ts
|
|
52
57
|
var defaultConfig = {
|
|
53
58
|
provider: {
|
|
54
|
-
provider: "openai
|
|
55
|
-
model: "gpt-
|
|
59
|
+
provider: "openai",
|
|
60
|
+
model: "gpt-5-nano",
|
|
56
61
|
baseUrl: "https://api.openai.com/v1",
|
|
57
62
|
apiKey: "",
|
|
58
63
|
jsonResponseFormat: "auto",
|
|
59
64
|
timeoutMs: 2e4,
|
|
60
65
|
temperature: 0.1,
|
|
61
|
-
maxOutputTokens:
|
|
66
|
+
maxOutputTokens: 400
|
|
62
67
|
},
|
|
63
68
|
input: {
|
|
64
69
|
stripAnsi: true,
|
|
65
70
|
redact: false,
|
|
66
71
|
redactStrict: false,
|
|
67
|
-
maxCaptureChars:
|
|
68
|
-
maxInputChars:
|
|
69
|
-
headChars:
|
|
70
|
-
tailChars:
|
|
72
|
+
maxCaptureChars: 4e5,
|
|
73
|
+
maxInputChars: 6e4,
|
|
74
|
+
headChars: 2e4,
|
|
75
|
+
tailChars: 2e4
|
|
71
76
|
},
|
|
72
77
|
runtime: {
|
|
73
78
|
rawFallback: true,
|
|
@@ -101,6 +106,16 @@ var defaultConfig = {
|
|
|
101
106
|
format: "bullets",
|
|
102
107
|
policy: "log-errors"
|
|
103
108
|
},
|
|
109
|
+
"typecheck-summary": {
|
|
110
|
+
question: "Summarize the blocking typecheck failures. Group repeated errors by root cause and point to the first files or symbols to fix.",
|
|
111
|
+
format: "bullets",
|
|
112
|
+
policy: "typecheck-summary"
|
|
113
|
+
},
|
|
114
|
+
"lint-failures": {
|
|
115
|
+
question: "Summarize the blocking lint failures. Group repeated rules, highlight the top offending files, and call out only failures that matter for fixing the run.",
|
|
116
|
+
format: "bullets",
|
|
117
|
+
policy: "lint-failures"
|
|
118
|
+
},
|
|
104
119
|
"infra-risk": {
|
|
105
120
|
question: "Assess whether the infrastructure changes are risky and whether they look safe to apply.",
|
|
106
121
|
format: "verdict",
|
|
@@ -141,18 +156,21 @@ function resolveCompatibleEnvName(baseUrl) {
|
|
|
141
156
|
return match?.envName;
|
|
142
157
|
}
|
|
143
158
|
function resolveProviderApiKey(provider, baseUrl, env) {
|
|
144
|
-
if (env.SIFT_PROVIDER_API_KEY) {
|
|
145
|
-
return env.SIFT_PROVIDER_API_KEY;
|
|
146
|
-
}
|
|
147
159
|
if (provider === "openai-compatible") {
|
|
160
|
+
if (env.SIFT_PROVIDER_API_KEY) {
|
|
161
|
+
return env.SIFT_PROVIDER_API_KEY;
|
|
162
|
+
}
|
|
148
163
|
const envName2 = resolveCompatibleEnvName(baseUrl);
|
|
149
164
|
return envName2 ? env[envName2] : void 0;
|
|
150
165
|
}
|
|
151
166
|
if (!provider) {
|
|
152
|
-
return
|
|
167
|
+
return env.SIFT_PROVIDER_API_KEY;
|
|
153
168
|
}
|
|
154
169
|
const envName = PROVIDER_API_KEY_ENV[provider];
|
|
155
|
-
|
|
170
|
+
if (envName && env[envName]) {
|
|
171
|
+
return env[envName];
|
|
172
|
+
}
|
|
173
|
+
return env.SIFT_PROVIDER_API_KEY;
|
|
156
174
|
}
|
|
157
175
|
function getProviderApiKeyEnvNames(provider, baseUrl) {
|
|
158
176
|
const envNames = ["SIFT_PROVIDER_API_KEY"];
|
|
@@ -168,14 +186,14 @@ function getProviderApiKeyEnvNames(provider, baseUrl) {
|
|
|
168
186
|
}
|
|
169
187
|
const envName = PROVIDER_API_KEY_ENV[provider];
|
|
170
188
|
if (envName) {
|
|
171
|
-
envNames
|
|
189
|
+
return [envName, ...envNames];
|
|
172
190
|
}
|
|
173
191
|
return envNames;
|
|
174
192
|
}
|
|
175
193
|
|
|
176
194
|
// src/config/schema.ts
|
|
177
195
|
import { z } from "zod";
|
|
178
|
-
var providerNameSchema = z.enum(["openai-compatible"]);
|
|
196
|
+
var providerNameSchema = z.enum(["openai", "openai-compatible"]);
|
|
179
197
|
var outputFormatSchema = z.enum([
|
|
180
198
|
"brief",
|
|
181
199
|
"bullets",
|
|
@@ -190,7 +208,9 @@ var promptPolicyNameSchema = z.enum([
|
|
|
190
208
|
"diff-summary",
|
|
191
209
|
"build-failure",
|
|
192
210
|
"log-errors",
|
|
193
|
-
"infra-risk"
|
|
211
|
+
"infra-risk",
|
|
212
|
+
"typecheck-summary",
|
|
213
|
+
"lint-failures"
|
|
194
214
|
]);
|
|
195
215
|
var providerConfigSchema = z.object({
|
|
196
216
|
provider: providerNameSchema,
|
|
@@ -324,8 +344,11 @@ function resolveConfig(options = {}) {
|
|
|
324
344
|
import fs2 from "fs";
|
|
325
345
|
import path3 from "path";
|
|
326
346
|
import YAML2 from "yaml";
|
|
327
|
-
function writeExampleConfig(
|
|
328
|
-
|
|
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);
|
|
329
352
|
if (fs2.existsSync(resolved)) {
|
|
330
353
|
throw new Error(`Config file already exists at ${resolved}`);
|
|
331
354
|
}
|
|
@@ -334,6 +357,226 @@ function writeExampleConfig(targetPath) {
|
|
|
334
357
|
fs2.writeFileSync(resolved, yaml, "utf8");
|
|
335
358
|
return resolved;
|
|
336
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
|
+
}
|
|
337
580
|
|
|
338
581
|
// src/commands/config.ts
|
|
339
582
|
var MASKED_SECRET = "***";
|
|
@@ -354,9 +597,12 @@ function maskConfigSecrets(value) {
|
|
|
354
597
|
}
|
|
355
598
|
return output;
|
|
356
599
|
}
|
|
357
|
-
function configInit(targetPath) {
|
|
358
|
-
const
|
|
359
|
-
|
|
600
|
+
function configInit(targetPath, global = false) {
|
|
601
|
+
const path5 = writeExampleConfig({
|
|
602
|
+
targetPath,
|
|
603
|
+
global
|
|
604
|
+
});
|
|
605
|
+
process.stdout.write(`${path5}
|
|
360
606
|
`);
|
|
361
607
|
}
|
|
362
608
|
function configShow(configPath, showSecrets = false) {
|
|
@@ -381,10 +627,11 @@ function configValidate(configPath) {
|
|
|
381
627
|
}
|
|
382
628
|
|
|
383
629
|
// src/commands/doctor.ts
|
|
384
|
-
function runDoctor(config) {
|
|
630
|
+
function runDoctor(config, configPath) {
|
|
385
631
|
const lines = [
|
|
386
632
|
"sift doctor",
|
|
387
633
|
"mode: local config completeness check",
|
|
634
|
+
`configPath: ${configPath ?? "(defaults only)"}`,
|
|
388
635
|
`provider: ${config.provider.provider}`,
|
|
389
636
|
`model: ${config.provider.model}`,
|
|
390
637
|
`baseUrl: ${config.provider.baseUrl}`,
|
|
@@ -402,7 +649,7 @@ function runDoctor(config) {
|
|
|
402
649
|
if (!config.provider.model) {
|
|
403
650
|
problems.push("Missing provider.model");
|
|
404
651
|
}
|
|
405
|
-
if (config.provider.provider === "openai-compatible" && !config.provider.apiKey) {
|
|
652
|
+
if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible") && !config.provider.apiKey) {
|
|
406
653
|
problems.push("Missing provider.apiKey");
|
|
407
654
|
problems.push(
|
|
408
655
|
`Set one of: ${getProviderApiKeyEnvNames(
|
|
@@ -444,9 +691,150 @@ import { spawn } from "child_process";
|
|
|
444
691
|
import { constants as osConstants } from "os";
|
|
445
692
|
import pc2 from "picocolors";
|
|
446
693
|
|
|
694
|
+
// src/core/gate.ts
|
|
695
|
+
var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
|
|
696
|
+
function parseJson(output) {
|
|
697
|
+
try {
|
|
698
|
+
return JSON.parse(output);
|
|
699
|
+
} catch {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function supportsFailOnPreset(presetName) {
|
|
704
|
+
return typeof presetName === "string" && FAIL_ON_SUPPORTED_PRESETS.has(presetName);
|
|
705
|
+
}
|
|
706
|
+
function assertSupportedFailOnPreset(presetName) {
|
|
707
|
+
if (!supportsFailOnPreset(presetName)) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
"--fail-on is supported only for built-in presets: infra-risk, audit-critical."
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function assertSupportedFailOnFormat(args) {
|
|
714
|
+
const expectedFormat = args.presetName === "infra-risk" ? "verdict" : "json";
|
|
715
|
+
if (args.format !== expectedFormat) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`--fail-on requires the default ${expectedFormat} format for preset ${args.presetName}.`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function evaluateGate(args) {
|
|
722
|
+
const parsed = parseJson(args.output);
|
|
723
|
+
if (!parsed || typeof parsed !== "object") {
|
|
724
|
+
return { shouldFail: false };
|
|
725
|
+
}
|
|
726
|
+
if (args.presetName === "infra-risk") {
|
|
727
|
+
return {
|
|
728
|
+
shouldFail: parsed["verdict"] === "fail"
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
if (args.presetName === "audit-critical") {
|
|
732
|
+
const status = parsed["status"];
|
|
733
|
+
const vulnerabilities = parsed["vulnerabilities"];
|
|
734
|
+
return {
|
|
735
|
+
shouldFail: status === "ok" && Array.isArray(vulnerabilities) && vulnerabilities.length > 0
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
return { shouldFail: false };
|
|
739
|
+
}
|
|
740
|
+
|
|
447
741
|
// src/core/run.ts
|
|
448
742
|
import pc from "picocolors";
|
|
449
743
|
|
|
744
|
+
// src/providers/systemInstruction.ts
|
|
745
|
+
var REDUCTION_SYSTEM_INSTRUCTION = "You reduce noisy command output into compact answers for agents and automation.";
|
|
746
|
+
|
|
747
|
+
// src/providers/openai.ts
|
|
748
|
+
function usesNativeJsonResponseFormat(mode) {
|
|
749
|
+
return mode !== "off";
|
|
750
|
+
}
|
|
751
|
+
function extractResponseText(payload) {
|
|
752
|
+
if (typeof payload?.output_text === "string") {
|
|
753
|
+
return payload.output_text.trim();
|
|
754
|
+
}
|
|
755
|
+
if (!Array.isArray(payload?.output)) {
|
|
756
|
+
return "";
|
|
757
|
+
}
|
|
758
|
+
return payload.output.flatMap((item) => Array.isArray(item?.content) ? item.content : []).map((item) => item?.type === "output_text" ? item.text : "").filter((text) => typeof text === "string" && text.trim().length > 0).join("").trim();
|
|
759
|
+
}
|
|
760
|
+
async function buildOpenAIError(response) {
|
|
761
|
+
let detail = `Provider returned HTTP ${response.status}`;
|
|
762
|
+
try {
|
|
763
|
+
const data = await response.json();
|
|
764
|
+
const message = data?.error?.message;
|
|
765
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
766
|
+
detail = `${detail}: ${message.trim()}`;
|
|
767
|
+
}
|
|
768
|
+
} catch {
|
|
769
|
+
}
|
|
770
|
+
return new Error(detail);
|
|
771
|
+
}
|
|
772
|
+
var OpenAIProvider = class {
|
|
773
|
+
name = "openai";
|
|
774
|
+
baseUrl;
|
|
775
|
+
apiKey;
|
|
776
|
+
constructor(options) {
|
|
777
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
778
|
+
this.apiKey = options.apiKey;
|
|
779
|
+
}
|
|
780
|
+
async generate(input) {
|
|
781
|
+
const controller = new AbortController();
|
|
782
|
+
const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
783
|
+
try {
|
|
784
|
+
const url = new URL("responses", `${this.baseUrl}/`);
|
|
785
|
+
const response = await fetch(url, {
|
|
786
|
+
method: "POST",
|
|
787
|
+
signal: controller.signal,
|
|
788
|
+
headers: {
|
|
789
|
+
"content-type": "application/json",
|
|
790
|
+
...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
|
|
791
|
+
},
|
|
792
|
+
body: JSON.stringify({
|
|
793
|
+
model: input.model,
|
|
794
|
+
instructions: REDUCTION_SYSTEM_INSTRUCTION,
|
|
795
|
+
input: input.prompt,
|
|
796
|
+
reasoning: {
|
|
797
|
+
effort: "minimal"
|
|
798
|
+
},
|
|
799
|
+
text: {
|
|
800
|
+
verbosity: "low",
|
|
801
|
+
...input.responseMode === "json" && usesNativeJsonResponseFormat(input.jsonResponseFormat) ? {
|
|
802
|
+
format: {
|
|
803
|
+
type: "json_object"
|
|
804
|
+
}
|
|
805
|
+
} : {}
|
|
806
|
+
},
|
|
807
|
+
max_output_tokens: input.maxOutputTokens
|
|
808
|
+
})
|
|
809
|
+
});
|
|
810
|
+
if (!response.ok) {
|
|
811
|
+
throw await buildOpenAIError(response);
|
|
812
|
+
}
|
|
813
|
+
const data = await response.json();
|
|
814
|
+
const text = extractResponseText(data);
|
|
815
|
+
if (!text) {
|
|
816
|
+
throw new Error("Provider returned an empty response");
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
text,
|
|
820
|
+
usage: data?.usage ? {
|
|
821
|
+
inputTokens: data.usage.input_tokens,
|
|
822
|
+
outputTokens: data.usage.output_tokens,
|
|
823
|
+
totalTokens: data.usage.total_tokens
|
|
824
|
+
} : void 0,
|
|
825
|
+
raw: data
|
|
826
|
+
};
|
|
827
|
+
} catch (error) {
|
|
828
|
+
if (error.name === "AbortError") {
|
|
829
|
+
throw new Error("Provider request timed out");
|
|
830
|
+
}
|
|
831
|
+
throw error;
|
|
832
|
+
} finally {
|
|
833
|
+
clearTimeout(timeout);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
|
|
450
838
|
// src/providers/openaiCompatible.ts
|
|
451
839
|
function supportsNativeJsonResponseFormat(baseUrl, mode) {
|
|
452
840
|
if (mode === "off") {
|
|
@@ -467,6 +855,18 @@ function extractMessageText(payload) {
|
|
|
467
855
|
}
|
|
468
856
|
return "";
|
|
469
857
|
}
|
|
858
|
+
async function buildOpenAICompatibleError(response) {
|
|
859
|
+
let detail = `Provider returned HTTP ${response.status}`;
|
|
860
|
+
try {
|
|
861
|
+
const data = await response.json();
|
|
862
|
+
const message = data?.error?.message;
|
|
863
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
864
|
+
detail = `${detail}: ${message.trim()}`;
|
|
865
|
+
}
|
|
866
|
+
} catch {
|
|
867
|
+
}
|
|
868
|
+
return new Error(detail);
|
|
869
|
+
}
|
|
470
870
|
var OpenAICompatibleProvider = class {
|
|
471
871
|
name = "openai-compatible";
|
|
472
872
|
baseUrl;
|
|
@@ -495,7 +895,7 @@ var OpenAICompatibleProvider = class {
|
|
|
495
895
|
messages: [
|
|
496
896
|
{
|
|
497
897
|
role: "system",
|
|
498
|
-
content:
|
|
898
|
+
content: REDUCTION_SYSTEM_INSTRUCTION
|
|
499
899
|
},
|
|
500
900
|
{
|
|
501
901
|
role: "user",
|
|
@@ -505,7 +905,7 @@ var OpenAICompatibleProvider = class {
|
|
|
505
905
|
})
|
|
506
906
|
});
|
|
507
907
|
if (!response.ok) {
|
|
508
|
-
throw
|
|
908
|
+
throw await buildOpenAICompatibleError(response);
|
|
509
909
|
}
|
|
510
910
|
const data = await response.json();
|
|
511
911
|
const text = extractMessageText(data);
|
|
@@ -534,6 +934,12 @@ var OpenAICompatibleProvider = class {
|
|
|
534
934
|
|
|
535
935
|
// src/providers/factory.ts
|
|
536
936
|
function createProvider(config) {
|
|
937
|
+
if (config.provider.provider === "openai") {
|
|
938
|
+
return new OpenAIProvider({
|
|
939
|
+
baseUrl: config.provider.baseUrl,
|
|
940
|
+
apiKey: config.provider.apiKey
|
|
941
|
+
});
|
|
942
|
+
}
|
|
537
943
|
if (config.provider.provider === "openai-compatible") {
|
|
538
944
|
return new OpenAICompatibleProvider({
|
|
539
945
|
baseUrl: config.provider.baseUrl,
|
|
@@ -659,6 +1065,33 @@ var BUILT_IN_POLICIES = {
|
|
|
659
1065
|
`If there is no clear error signal, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
|
|
660
1066
|
]
|
|
661
1067
|
},
|
|
1068
|
+
"typecheck-summary": {
|
|
1069
|
+
name: "typecheck-summary",
|
|
1070
|
+
responseMode: "text",
|
|
1071
|
+
taskRules: [
|
|
1072
|
+
"Return at most 5 short bullet points.",
|
|
1073
|
+
"Determine whether the typecheck failed or passed.",
|
|
1074
|
+
"Group repeated diagnostics into root-cause buckets instead of echoing many duplicate lines.",
|
|
1075
|
+
"Mention the first concrete files, symbols, or error categories to fix when they are visible.",
|
|
1076
|
+
"Prefer compiler or type-system errors over timing, progress, or summary noise.",
|
|
1077
|
+
"If the output clearly indicates success, say that briefly and do not add extra bullets.",
|
|
1078
|
+
`If you cannot tell whether the typecheck failed, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
|
|
1079
|
+
]
|
|
1080
|
+
},
|
|
1081
|
+
"lint-failures": {
|
|
1082
|
+
name: "lint-failures",
|
|
1083
|
+
responseMode: "text",
|
|
1084
|
+
taskRules: [
|
|
1085
|
+
"Return at most 5 short bullet points.",
|
|
1086
|
+
"Determine whether lint failed or whether there are no blocking lint failures.",
|
|
1087
|
+
"Group repeated rule violations instead of listing the same rule many times.",
|
|
1088
|
+
"Mention the top offending files and rule names when they are visible.",
|
|
1089
|
+
"Distinguish blocking failures from warnings only when that distinction is clearly visible in the input.",
|
|
1090
|
+
"Do not invent autofixability; only mention autofix or --fix support when the tool output explicitly says so.",
|
|
1091
|
+
"If the output clearly indicates success or no blocking failures, say that briefly and stop.",
|
|
1092
|
+
`If there is not enough evidence to determine the lint result, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
|
|
1093
|
+
]
|
|
1094
|
+
},
|
|
662
1095
|
"infra-risk": {
|
|
663
1096
|
name: "infra-risk",
|
|
664
1097
|
responseMode: "json",
|
|
@@ -1228,6 +1661,15 @@ function buildCommandPreview(request) {
|
|
|
1228
1661
|
}
|
|
1229
1662
|
return (request.command ?? []).join(" ");
|
|
1230
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
|
+
}
|
|
1231
1673
|
async function runExec(request) {
|
|
1232
1674
|
const hasArgvCommand = Array.isArray(request.command) && request.command.length > 0;
|
|
1233
1675
|
const hasShellCommand = typeof request.shellCommand === "string";
|
|
@@ -1292,6 +1734,7 @@ async function runExec(request) {
|
|
|
1292
1734
|
throw childSpawnError;
|
|
1293
1735
|
}
|
|
1294
1736
|
const exitCode = normalizeChildExitCode(childStatus, childSignal);
|
|
1737
|
+
const capturedOutput = capture.render();
|
|
1295
1738
|
if (request.config.runtime.verbose) {
|
|
1296
1739
|
process.stderr.write(
|
|
1297
1740
|
`${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
|
|
@@ -1299,12 +1742,34 @@ async function runExec(request) {
|
|
|
1299
1742
|
);
|
|
1300
1743
|
}
|
|
1301
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
|
+
}
|
|
1302
1761
|
const output = await runSift({
|
|
1303
1762
|
...request,
|
|
1304
|
-
stdin:
|
|
1763
|
+
stdin: capturedOutput
|
|
1305
1764
|
});
|
|
1306
1765
|
process.stdout.write(`${output}
|
|
1307
1766
|
`);
|
|
1767
|
+
if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
|
|
1768
|
+
presetName: request.presetName,
|
|
1769
|
+
output
|
|
1770
|
+
}).shouldFail) {
|
|
1771
|
+
return 1;
|
|
1772
|
+
}
|
|
1308
1773
|
}
|
|
1309
1774
|
return exitCode;
|
|
1310
1775
|
}
|
|
@@ -1334,6 +1799,12 @@ function getPreset(config, name) {
|
|
|
1334
1799
|
var require2 = createRequire(import.meta.url);
|
|
1335
1800
|
var pkg = require2("../package.json");
|
|
1336
1801
|
var cli = cac("sift");
|
|
1802
|
+
var HELP_BANNER = [
|
|
1803
|
+
" \\\\ //",
|
|
1804
|
+
" \\\\//",
|
|
1805
|
+
" ||",
|
|
1806
|
+
" o"
|
|
1807
|
+
].join("\n");
|
|
1337
1808
|
function toNumber(value) {
|
|
1338
1809
|
if (value === void 0 || value === null || value === "") {
|
|
1339
1810
|
return void 0;
|
|
@@ -1372,15 +1843,25 @@ function buildCliOverrides(options) {
|
|
|
1372
1843
|
return overrides;
|
|
1373
1844
|
}
|
|
1374
1845
|
function applySharedOptions(command) {
|
|
1375
|
-
return command.option("--provider <provider>", "Provider: openai-compatible").option("--model <model>", "Model name").option("--base-url <url>", "Provider base URL").option(
|
|
1846
|
+
return command.option("--provider <provider>", "Provider: openai | openai-compatible").option("--model <model>", "Model name").option("--base-url <url>", "Provider base URL").option(
|
|
1376
1847
|
"--api-key <key>",
|
|
1377
|
-
"Provider API key (or set
|
|
1848
|
+
"Provider API key (or set OPENAI_API_KEY for provider=openai; use SIFT_PROVIDER_API_KEY or endpoint-native envs for openai-compatible)"
|
|
1378
1849
|
).option(
|
|
1379
1850
|
"--json-response-format <mode>",
|
|
1380
1851
|
"JSON response format mode: auto | on | off"
|
|
1381
|
-
).option("--timeout-ms <ms>", "Request timeout in milliseconds").option("--format <format>", "brief | bullets | json | verdict").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(
|
|
1852
|
+
).option("--timeout-ms <ms>", "Request timeout in milliseconds").option("--format <format>", "brief | bullets | json | verdict").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(
|
|
1853
|
+
"--fail-on",
|
|
1854
|
+
"Fail with exit code 1 when a supported built-in preset produces a blocking result"
|
|
1855
|
+
).option("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
|
|
1382
1856
|
}
|
|
1383
1857
|
async function executeRun(args) {
|
|
1858
|
+
if (Boolean(args.options.failOn)) {
|
|
1859
|
+
assertSupportedFailOnPreset(args.presetName);
|
|
1860
|
+
assertSupportedFailOnFormat({
|
|
1861
|
+
presetName: args.presetName,
|
|
1862
|
+
format: args.format
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1384
1865
|
const config = resolveConfig({
|
|
1385
1866
|
configPath: args.options.config,
|
|
1386
1867
|
env: process.env,
|
|
@@ -1393,12 +1874,19 @@ async function executeRun(args) {
|
|
|
1393
1874
|
stdin,
|
|
1394
1875
|
config,
|
|
1395
1876
|
dryRun: Boolean(args.options.dryRun),
|
|
1877
|
+
presetName: args.presetName,
|
|
1396
1878
|
policyName: args.policyName,
|
|
1397
1879
|
outputContract: args.outputContract,
|
|
1398
1880
|
fallbackJson: args.fallbackJson
|
|
1399
1881
|
});
|
|
1400
1882
|
process.stdout.write(`${output}
|
|
1401
1883
|
`);
|
|
1884
|
+
if (Boolean(args.options.failOn) && !Boolean(args.options.dryRun) && args.presetName && evaluateGate({
|
|
1885
|
+
presetName: args.presetName,
|
|
1886
|
+
output
|
|
1887
|
+
}).shouldFail) {
|
|
1888
|
+
process.exitCode = 1;
|
|
1889
|
+
}
|
|
1402
1890
|
}
|
|
1403
1891
|
function extractExecCommand(options) {
|
|
1404
1892
|
const passthrough = Array.isArray(options["--"]) ? options["--"].map((value) => String(value)) : [];
|
|
@@ -1415,6 +1903,13 @@ function extractExecCommand(options) {
|
|
|
1415
1903
|
};
|
|
1416
1904
|
}
|
|
1417
1905
|
async function executeExec(args) {
|
|
1906
|
+
if (Boolean(args.options.failOn)) {
|
|
1907
|
+
assertSupportedFailOnPreset(args.presetName);
|
|
1908
|
+
assertSupportedFailOnFormat({
|
|
1909
|
+
presetName: args.presetName,
|
|
1910
|
+
format: args.format
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1418
1913
|
const config = resolveConfig({
|
|
1419
1914
|
configPath: args.options.config,
|
|
1420
1915
|
env: process.env,
|
|
@@ -1426,6 +1921,8 @@ async function executeExec(args) {
|
|
|
1426
1921
|
format: args.format,
|
|
1427
1922
|
config,
|
|
1428
1923
|
dryRun: Boolean(args.options.dryRun),
|
|
1924
|
+
failOn: Boolean(args.options.failOn),
|
|
1925
|
+
presetName: args.presetName,
|
|
1429
1926
|
policyName: args.policyName,
|
|
1430
1927
|
outputContract: args.outputContract,
|
|
1431
1928
|
fallbackJson: args.fallbackJson,
|
|
@@ -1444,6 +1941,7 @@ applySharedOptions(
|
|
|
1444
1941
|
await executeRun({
|
|
1445
1942
|
question: preset.question,
|
|
1446
1943
|
format: options.format ?? preset.format,
|
|
1944
|
+
presetName: name,
|
|
1447
1945
|
policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
|
|
1448
1946
|
options,
|
|
1449
1947
|
outputContract: preset.outputContract,
|
|
@@ -1472,6 +1970,7 @@ applySharedOptions(
|
|
|
1472
1970
|
await executeExec({
|
|
1473
1971
|
question: preset.question,
|
|
1474
1972
|
format: options.format ?? preset.format,
|
|
1973
|
+
presetName,
|
|
1475
1974
|
policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
|
|
1476
1975
|
options,
|
|
1477
1976
|
outputContract: preset.outputContract,
|
|
@@ -1491,10 +1990,23 @@ applySharedOptions(
|
|
|
1491
1990
|
});
|
|
1492
1991
|
cli.command(
|
|
1493
1992
|
"config <action>",
|
|
1494
|
-
"Config commands: init | show | validate (show/validate use resolved runtime config)"
|
|
1495
|
-
).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
|
+
}
|
|
1496
2005
|
if (action === "init") {
|
|
1497
|
-
configInit(
|
|
2006
|
+
configInit(
|
|
2007
|
+
options.path,
|
|
2008
|
+
Boolean(options.global)
|
|
2009
|
+
);
|
|
1498
2010
|
return;
|
|
1499
2011
|
}
|
|
1500
2012
|
if (action === "show") {
|
|
@@ -1511,11 +2023,12 @@ cli.command(
|
|
|
1511
2023
|
throw new Error(`Unknown config action: ${action}`);
|
|
1512
2024
|
});
|
|
1513
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);
|
|
1514
2027
|
const config = resolveConfig({
|
|
1515
2028
|
configPath: options.config,
|
|
1516
2029
|
env: process.env
|
|
1517
2030
|
});
|
|
1518
|
-
process.exitCode = runDoctor(config);
|
|
2031
|
+
process.exitCode = runDoctor(config, configPath);
|
|
1519
2032
|
});
|
|
1520
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) => {
|
|
1521
2034
|
const config = resolveConfig({
|
|
@@ -1548,7 +2061,13 @@ applySharedOptions(
|
|
|
1548
2061
|
options
|
|
1549
2062
|
});
|
|
1550
2063
|
});
|
|
1551
|
-
cli.help()
|
|
2064
|
+
cli.help((sections) => [
|
|
2065
|
+
{
|
|
2066
|
+
body: `${HELP_BANNER}
|
|
2067
|
+
`
|
|
2068
|
+
},
|
|
2069
|
+
...sections
|
|
2070
|
+
]);
|
|
1552
2071
|
cli.version(pkg.version);
|
|
1553
2072
|
async function main() {
|
|
1554
2073
|
cli.parse(process.argv, { run: false });
|