@bilalimamoglu/sift 0.4.3 → 0.4.5
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 +66 -21
- package/dist/cli.js +1328 -155
- package/dist/index.d.ts +11 -2
- package/dist/index.js +202 -23
- package/package.json +17 -5
package/dist/cli.js
CHANGED
|
@@ -5,13 +5,15 @@ import { createRequire } from "module";
|
|
|
5
5
|
import { cac } from "cac";
|
|
6
6
|
|
|
7
7
|
// src/config/load.ts
|
|
8
|
-
import
|
|
8
|
+
import fs2 from "fs";
|
|
9
9
|
import path2 from "path";
|
|
10
10
|
import YAML from "yaml";
|
|
11
11
|
|
|
12
12
|
// src/constants.ts
|
|
13
|
+
import fs from "fs";
|
|
13
14
|
import os from "os";
|
|
14
15
|
import path from "path";
|
|
16
|
+
import crypto from "crypto";
|
|
15
17
|
var DEFAULT_CONFIG_FILENAME = "sift.config.yaml";
|
|
16
18
|
function getDefaultCodexGlobalInstructionsPath(homeDir = os.homedir()) {
|
|
17
19
|
return path.join(homeDir, ".codex", "AGENTS.md");
|
|
@@ -28,6 +30,26 @@ function getDefaultGlobalStateDir(homeDir = os.homedir()) {
|
|
|
28
30
|
function getDefaultTestStatusStatePath(homeDir = os.homedir()) {
|
|
29
31
|
return path.join(getDefaultGlobalStateDir(homeDir), "last-test-status.json");
|
|
30
32
|
}
|
|
33
|
+
function getDefaultScopedTestStatusStateDir(homeDir = os.homedir()) {
|
|
34
|
+
return path.join(getDefaultGlobalStateDir(homeDir), "test-status", "by-cwd");
|
|
35
|
+
}
|
|
36
|
+
function getScopedTestStatusStatePath(cwd, homeDir = os.homedir()) {
|
|
37
|
+
const normalizedCwd = normalizeScopedCacheCwd(cwd);
|
|
38
|
+
const baseName = slugCachePathSegment(path.basename(normalizedCwd)) || "root";
|
|
39
|
+
const shortHash = crypto.createHash("sha256").update(normalizedCwd).digest("hex").slice(0, 10);
|
|
40
|
+
return path.join(getDefaultScopedTestStatusStateDir(homeDir), `${baseName}-${shortHash}.json`);
|
|
41
|
+
}
|
|
42
|
+
function slugCachePathSegment(value) {
|
|
43
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
44
|
+
}
|
|
45
|
+
function normalizeScopedCacheCwd(cwd) {
|
|
46
|
+
const absoluteCwd = path.resolve(cwd);
|
|
47
|
+
try {
|
|
48
|
+
return fs.realpathSync.native(absoluteCwd);
|
|
49
|
+
} catch {
|
|
50
|
+
return absoluteCwd;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
31
53
|
function getDefaultConfigSearchPaths() {
|
|
32
54
|
return [
|
|
33
55
|
path.resolve(process.cwd(), "sift.config.yaml"),
|
|
@@ -44,13 +66,13 @@ var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
|
|
|
44
66
|
function findConfigPath(explicitPath) {
|
|
45
67
|
if (explicitPath) {
|
|
46
68
|
const resolved = path2.resolve(explicitPath);
|
|
47
|
-
if (!
|
|
69
|
+
if (!fs2.existsSync(resolved)) {
|
|
48
70
|
throw new Error(`Config file not found: ${resolved}`);
|
|
49
71
|
}
|
|
50
72
|
return resolved;
|
|
51
73
|
}
|
|
52
74
|
for (const candidate of getDefaultConfigSearchPaths()) {
|
|
53
|
-
if (
|
|
75
|
+
if (fs2.existsSync(candidate)) {
|
|
54
76
|
return candidate;
|
|
55
77
|
}
|
|
56
78
|
}
|
|
@@ -61,7 +83,7 @@ function loadRawConfig(explicitPath) {
|
|
|
61
83
|
if (!configPath) {
|
|
62
84
|
return {};
|
|
63
85
|
}
|
|
64
|
-
const content =
|
|
86
|
+
const content = fs2.readFileSync(configPath, "utf8");
|
|
65
87
|
return YAML.parse(content) ?? {};
|
|
66
88
|
}
|
|
67
89
|
|
|
@@ -87,6 +109,7 @@ var defaultConfig = {
|
|
|
87
109
|
tailChars: 2e4
|
|
88
110
|
},
|
|
89
111
|
runtime: {
|
|
112
|
+
operationMode: "agent-escalation",
|
|
90
113
|
rawFallback: true,
|
|
91
114
|
verbose: false
|
|
92
115
|
},
|
|
@@ -136,12 +159,62 @@ var defaultConfig = {
|
|
|
136
159
|
}
|
|
137
160
|
};
|
|
138
161
|
|
|
162
|
+
// src/config/provider-models.ts
|
|
163
|
+
var OPENAI_MODELS = [
|
|
164
|
+
{
|
|
165
|
+
model: "gpt-5-nano",
|
|
166
|
+
label: "gpt-5-nano",
|
|
167
|
+
note: "default, cheapest, fast enough for most fallback passes",
|
|
168
|
+
isDefault: true
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
model: "gpt-5.4-nano",
|
|
172
|
+
label: "gpt-5.4-nano",
|
|
173
|
+
note: "newer nano backup, a touch smarter, a touch pricier"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
model: "gpt-5-mini",
|
|
177
|
+
label: "gpt-5-mini",
|
|
178
|
+
note: "smarter fallback, still saner than the expensive stuff"
|
|
179
|
+
}
|
|
180
|
+
];
|
|
181
|
+
var OPENROUTER_MODELS = [
|
|
182
|
+
{
|
|
183
|
+
model: "openrouter/free",
|
|
184
|
+
label: "openrouter/free",
|
|
185
|
+
note: "default, free, a little slower sometimes, still hard to argue with free",
|
|
186
|
+
isDefault: true
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
model: "qwen/qwen3-coder:free",
|
|
190
|
+
label: "qwen/qwen3-coder:free",
|
|
191
|
+
note: "free, code-focused, good when you want a named coding fallback"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
model: "deepseek/deepseek-r1:free",
|
|
195
|
+
label: "deepseek/deepseek-r1:free",
|
|
196
|
+
note: "free, stronger reasoning, usually slower"
|
|
197
|
+
}
|
|
198
|
+
];
|
|
199
|
+
function getProviderModelOptions(provider) {
|
|
200
|
+
return provider === "openrouter" ? OPENROUTER_MODELS : OPENAI_MODELS;
|
|
201
|
+
}
|
|
202
|
+
function getDefaultProviderModel(provider) {
|
|
203
|
+
return getProviderModelOptions(provider).find((option) => option.isDefault)?.model ?? getProviderModelOptions(provider)[0]?.model ?? "";
|
|
204
|
+
}
|
|
205
|
+
function findProviderModelOption(provider, model) {
|
|
206
|
+
if (!model) {
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
return getProviderModelOptions(provider).find((option) => option.model === model);
|
|
210
|
+
}
|
|
211
|
+
|
|
139
212
|
// src/config/native-provider.ts
|
|
140
213
|
function getNativeProviderDefaults(provider) {
|
|
141
214
|
if (provider === "openrouter") {
|
|
142
215
|
return {
|
|
143
216
|
provider,
|
|
144
|
-
model: "openrouter
|
|
217
|
+
model: getDefaultProviderModel("openrouter"),
|
|
145
218
|
baseUrl: "https://openrouter.ai/api/v1"
|
|
146
219
|
};
|
|
147
220
|
}
|
|
@@ -283,6 +356,11 @@ function getProviderApiKeyEnvNames(provider, baseUrl) {
|
|
|
283
356
|
|
|
284
357
|
// src/config/schema.ts
|
|
285
358
|
import { z } from "zod";
|
|
359
|
+
var operationModeSchema = z.enum([
|
|
360
|
+
"agent-escalation",
|
|
361
|
+
"provider-assisted",
|
|
362
|
+
"local-only"
|
|
363
|
+
]);
|
|
286
364
|
var providerNameSchema = z.enum([
|
|
287
365
|
"openai",
|
|
288
366
|
"openai-compatible",
|
|
@@ -335,6 +413,7 @@ var inputConfigSchema = z.object({
|
|
|
335
413
|
tailChars: z.number().int().positive()
|
|
336
414
|
});
|
|
337
415
|
var runtimeConfigSchema = z.object({
|
|
416
|
+
operationMode: operationModeSchema,
|
|
338
417
|
rawFallback: z.boolean(),
|
|
339
418
|
verbose: z.boolean()
|
|
340
419
|
});
|
|
@@ -357,7 +436,7 @@ var siftConfigSchema = z.object({
|
|
|
357
436
|
var PROVIDER_DEFAULT_OVERRIDES = {
|
|
358
437
|
openrouter: {
|
|
359
438
|
provider: {
|
|
360
|
-
model: "openrouter
|
|
439
|
+
model: getDefaultProviderModel("openrouter"),
|
|
361
440
|
baseUrl: "https://openrouter.ai/api/v1"
|
|
362
441
|
}
|
|
363
442
|
}
|
|
@@ -397,13 +476,16 @@ function stripApiKey(overrides) {
|
|
|
397
476
|
}
|
|
398
477
|
function buildNonCredentialEnvOverrides(env) {
|
|
399
478
|
const overrides = {};
|
|
400
|
-
if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_TIMEOUT_MS) {
|
|
479
|
+
if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_TIMEOUT_MS || env.SIFT_OPERATION_MODE) {
|
|
401
480
|
overrides.provider = {
|
|
402
481
|
provider: env.SIFT_PROVIDER,
|
|
403
482
|
model: env.SIFT_MODEL,
|
|
404
483
|
baseUrl: env.SIFT_BASE_URL,
|
|
405
484
|
timeoutMs: env.SIFT_TIMEOUT_MS ? Number(env.SIFT_TIMEOUT_MS) : void 0
|
|
406
485
|
};
|
|
486
|
+
overrides.runtime = {
|
|
487
|
+
operationMode: env.SIFT_OPERATION_MODE
|
|
488
|
+
};
|
|
407
489
|
}
|
|
408
490
|
if (env.SIFT_MAX_INPUT_CHARS || env.SIFT_MAX_CAPTURE_CHARS) {
|
|
409
491
|
overrides.input = {
|
|
@@ -470,9 +552,18 @@ function resolveConfig(options = {}) {
|
|
|
470
552
|
);
|
|
471
553
|
return siftConfigSchema.parse(merged);
|
|
472
554
|
}
|
|
555
|
+
function hasUsableProvider(config) {
|
|
556
|
+
return config.provider.apiKey !== void 0 && config.provider.apiKey.trim().length > 0;
|
|
557
|
+
}
|
|
558
|
+
function resolveEffectiveOperationMode(config) {
|
|
559
|
+
if (config.runtime.operationMode === "provider-assisted") {
|
|
560
|
+
return hasUsableProvider(config) ? "provider-assisted" : "agent-escalation";
|
|
561
|
+
}
|
|
562
|
+
return config.runtime.operationMode;
|
|
563
|
+
}
|
|
473
564
|
|
|
474
565
|
// src/config/write.ts
|
|
475
|
-
import
|
|
566
|
+
import fs3 from "fs";
|
|
476
567
|
import path3 from "path";
|
|
477
568
|
import YAML2 from "yaml";
|
|
478
569
|
function writeExampleConfig(options = {}) {
|
|
@@ -480,41 +571,41 @@ function writeExampleConfig(options = {}) {
|
|
|
480
571
|
throw new Error("Use either --path <path> or --global, not both.");
|
|
481
572
|
}
|
|
482
573
|
const resolved = options.global ? getDefaultGlobalConfigPath() : path3.resolve(options.targetPath ?? DEFAULT_CONFIG_FILENAME);
|
|
483
|
-
if (
|
|
574
|
+
if (fs3.existsSync(resolved)) {
|
|
484
575
|
throw new Error(`Config file already exists at ${resolved}`);
|
|
485
576
|
}
|
|
486
577
|
const yaml = YAML2.stringify(defaultConfig);
|
|
487
|
-
|
|
488
|
-
|
|
578
|
+
fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
|
|
579
|
+
fs3.writeFileSync(resolved, yaml, {
|
|
489
580
|
encoding: "utf8",
|
|
490
581
|
mode: 384
|
|
491
582
|
});
|
|
492
583
|
try {
|
|
493
|
-
|
|
584
|
+
fs3.chmodSync(resolved, 384);
|
|
494
585
|
} catch {
|
|
495
586
|
}
|
|
496
587
|
return resolved;
|
|
497
588
|
}
|
|
498
589
|
function writeConfigFile(options) {
|
|
499
590
|
const resolved = path3.resolve(options.targetPath);
|
|
500
|
-
if (!options.overwrite &&
|
|
591
|
+
if (!options.overwrite && fs3.existsSync(resolved)) {
|
|
501
592
|
throw new Error(`Config file already exists at ${resolved}`);
|
|
502
593
|
}
|
|
503
594
|
const yaml = YAML2.stringify(options.config);
|
|
504
|
-
|
|
505
|
-
|
|
595
|
+
fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
|
|
596
|
+
fs3.writeFileSync(resolved, yaml, {
|
|
506
597
|
encoding: "utf8",
|
|
507
598
|
mode: 384
|
|
508
599
|
});
|
|
509
600
|
try {
|
|
510
|
-
|
|
601
|
+
fs3.chmodSync(resolved, 384);
|
|
511
602
|
} catch {
|
|
512
603
|
}
|
|
513
604
|
return resolved;
|
|
514
605
|
}
|
|
515
606
|
|
|
516
607
|
// src/config/editable.ts
|
|
517
|
-
import
|
|
608
|
+
import fs4 from "fs";
|
|
518
609
|
import path4 from "path";
|
|
519
610
|
function isRecord2(value) {
|
|
520
611
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -527,7 +618,7 @@ function resolveEditableConfigPath(explicitPath) {
|
|
|
527
618
|
}
|
|
528
619
|
function loadEditableConfig(explicitPath) {
|
|
529
620
|
const resolvedPath = resolveEditableConfigPath(explicitPath);
|
|
530
|
-
const existed =
|
|
621
|
+
const existed = fs4.existsSync(resolvedPath);
|
|
531
622
|
const rawConfig = existed ? loadRawConfig(resolvedPath) : {};
|
|
532
623
|
const config = siftConfigSchema.parse(
|
|
533
624
|
mergeDefined(defaultConfig, isRecord2(rawConfig) ? rawConfig : {})
|
|
@@ -594,10 +685,129 @@ import { emitKeypressEvents } from "readline";
|
|
|
594
685
|
import { createInterface } from "readline/promises";
|
|
595
686
|
import { stderr as defaultStderr, stdin as defaultStdin2, stdout as defaultStdout } from "process";
|
|
596
687
|
|
|
688
|
+
// src/config/operation-mode.ts
|
|
689
|
+
function getOperationModeLabel(mode) {
|
|
690
|
+
switch (mode) {
|
|
691
|
+
case "agent-escalation":
|
|
692
|
+
return "Agent escalation";
|
|
693
|
+
case "provider-assisted":
|
|
694
|
+
return "Provider-assisted";
|
|
695
|
+
case "local-only":
|
|
696
|
+
return "Local-only";
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function describeOperationMode(mode) {
|
|
700
|
+
switch (mode) {
|
|
701
|
+
case "agent-escalation":
|
|
702
|
+
return "Best when you already have an agent open. sift does the quick first pass, then the agent can read code, tests, or logs and keep going.";
|
|
703
|
+
case "provider-assisted":
|
|
704
|
+
return "Best when you want sift itself to take one more cheap swing at the problem before you fall back to an agent or raw logs. Built-in rules first, API-backed backup second.";
|
|
705
|
+
case "local-only":
|
|
706
|
+
return "Best when you are not pairing sift with another agent and do not want API keys. Everything stays local.";
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function describeInsufficientBehavior(mode) {
|
|
710
|
+
switch (mode) {
|
|
711
|
+
case "agent-escalation":
|
|
712
|
+
return "If sift is still not enough, that is the handoff point: log it, narrow the problem, and let the agent keep digging.";
|
|
713
|
+
case "provider-assisted":
|
|
714
|
+
return "If the first pass is still fuzzy, sift can ask the fallback model so you do not have to escalate every annoying edge case by hand.";
|
|
715
|
+
case "local-only":
|
|
716
|
+
return "If sift is still not enough, stay local: rerun something narrower, use a better preset, or read the relevant source and tests yourself.";
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
597
720
|
// src/ui/terminal.ts
|
|
598
721
|
import { execFileSync } from "child_process";
|
|
599
722
|
import { clearScreenDown, cursorTo, moveCursor } from "readline";
|
|
600
723
|
import { stdin as defaultStdin } from "process";
|
|
724
|
+
var PROMPT_BACK = "__sift_back__";
|
|
725
|
+
var PROMPT_BACK_LABEL = "\u2190 Back";
|
|
726
|
+
function color(text, rgb, args = {}) {
|
|
727
|
+
const codes = [];
|
|
728
|
+
if (args.bold) {
|
|
729
|
+
codes.push("1");
|
|
730
|
+
}
|
|
731
|
+
if (args.dim) {
|
|
732
|
+
codes.push("2");
|
|
733
|
+
}
|
|
734
|
+
codes.push(`38;2;${rgb[0]};${rgb[1]};${rgb[2]}`);
|
|
735
|
+
return `\x1B[${codes.join(";")}m${text}\x1B[0m`;
|
|
736
|
+
}
|
|
737
|
+
function splitOptionLeading(option) {
|
|
738
|
+
const boundaries = [" - ", ": ", ":", " ("].map((token) => {
|
|
739
|
+
const index = option.indexOf(token);
|
|
740
|
+
return index >= 0 ? { index, token } : void 0;
|
|
741
|
+
}).filter((entry) => Boolean(entry)).sort((left, right) => left.index - right.index);
|
|
742
|
+
const boundary = boundaries[0];
|
|
743
|
+
if (!boundary) {
|
|
744
|
+
return { leading: option, trailing: "" };
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
leading: option.slice(0, boundary.index),
|
|
748
|
+
trailing: option.slice(boundary.index)
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function getOptionPalette(leading) {
|
|
752
|
+
const normalized = leading.trim();
|
|
753
|
+
if (normalized.startsWith("With an agent")) {
|
|
754
|
+
return { rgb: [214, 168, 76] };
|
|
755
|
+
}
|
|
756
|
+
if (normalized.startsWith("With provider fallback")) {
|
|
757
|
+
return { rgb: [100, 141, 214] };
|
|
758
|
+
}
|
|
759
|
+
if (normalized.startsWith("Solo, local-only")) {
|
|
760
|
+
return { rgb: [122, 142, 116], dimWhenIdle: true };
|
|
761
|
+
}
|
|
762
|
+
if (normalized.startsWith("Codex")) {
|
|
763
|
+
return { rgb: [233, 183, 78] };
|
|
764
|
+
}
|
|
765
|
+
if (normalized.startsWith("Claude")) {
|
|
766
|
+
return { rgb: [171, 138, 224] };
|
|
767
|
+
}
|
|
768
|
+
if (normalized === "All") {
|
|
769
|
+
return { rgb: [95, 181, 201] };
|
|
770
|
+
}
|
|
771
|
+
if (normalized.startsWith("Global")) {
|
|
772
|
+
return { rgb: [205, 168, 83] };
|
|
773
|
+
}
|
|
774
|
+
if (normalized.startsWith("Local")) {
|
|
775
|
+
return { rgb: [138, 144, 150], dimWhenIdle: true };
|
|
776
|
+
}
|
|
777
|
+
if (normalized.startsWith("OpenAI")) {
|
|
778
|
+
return { rgb: [82, 177, 124] };
|
|
779
|
+
}
|
|
780
|
+
if (normalized.startsWith("OpenRouter")) {
|
|
781
|
+
return { rgb: [106, 144, 221] };
|
|
782
|
+
}
|
|
783
|
+
if (normalized.startsWith("Use saved key") || normalized.startsWith("Use existing key")) {
|
|
784
|
+
return { rgb: [111, 181, 123] };
|
|
785
|
+
}
|
|
786
|
+
if (normalized.startsWith("Use environment key")) {
|
|
787
|
+
return { rgb: [102, 146, 219] };
|
|
788
|
+
}
|
|
789
|
+
if (normalized.startsWith("Enter a different key") || normalized.startsWith("Custom model")) {
|
|
790
|
+
return { rgb: [191, 157, 92], dimWhenIdle: true };
|
|
791
|
+
}
|
|
792
|
+
return void 0;
|
|
793
|
+
}
|
|
794
|
+
function styleOption(option, selected, colorize) {
|
|
795
|
+
if (!colorize) {
|
|
796
|
+
return option;
|
|
797
|
+
}
|
|
798
|
+
if (option === PROMPT_BACK_LABEL) {
|
|
799
|
+
return color(option, [164, 169, 178], { bold: selected, dim: !selected });
|
|
800
|
+
}
|
|
801
|
+
const { leading, trailing } = splitOptionLeading(option);
|
|
802
|
+
const palette = getOptionPalette(leading);
|
|
803
|
+
if (!palette) {
|
|
804
|
+
return option;
|
|
805
|
+
}
|
|
806
|
+
return `${color(leading, palette.rgb, {
|
|
807
|
+
bold: selected,
|
|
808
|
+
dim: !selected && Boolean(palette.dimWhenIdle)
|
|
809
|
+
})}${trailing}`;
|
|
810
|
+
}
|
|
601
811
|
function setPosixEcho(enabled) {
|
|
602
812
|
const command = enabled ? "echo" : "-echo";
|
|
603
813
|
try {
|
|
@@ -615,10 +825,15 @@ function setPosixEcho(enabled) {
|
|
|
615
825
|
}
|
|
616
826
|
}
|
|
617
827
|
function renderSelectionBlock(args) {
|
|
828
|
+
const options = args.allowBack ? [...args.options, args.backLabel ?? PROMPT_BACK_LABEL] : args.options;
|
|
618
829
|
return [
|
|
619
|
-
`${args.prompt} (use \u2191/\u2193 and Enter)`,
|
|
620
|
-
...
|
|
621
|
-
(option, index) => `${index === args.selectedIndex ? "\u203A" : " "} ${
|
|
830
|
+
`${args.prompt}${args.allowBack ? " (use \u2191/\u2193 to move, Enter to select, Esc to go back)" : " (use \u2191/\u2193 and Enter)"}`,
|
|
831
|
+
...options.map(
|
|
832
|
+
(option, index) => `${index === args.selectedIndex ? "\u203A" : " "} ${styleOption(
|
|
833
|
+
option,
|
|
834
|
+
index === args.selectedIndex,
|
|
835
|
+
Boolean(args.colorize)
|
|
836
|
+
)}${index === args.selectedIndex ? " (selected)" : ""}`
|
|
622
837
|
)
|
|
623
838
|
];
|
|
624
839
|
}
|
|
@@ -626,6 +841,8 @@ async function promptSelect(args) {
|
|
|
626
841
|
const { input, output, prompt, options } = args;
|
|
627
842
|
const stream = output;
|
|
628
843
|
const selectedLabel = args.selectedLabel ?? prompt;
|
|
844
|
+
const backLabel = args.backLabel ?? PROMPT_BACK_LABEL;
|
|
845
|
+
const allOptions = args.allowBack ? [...options, backLabel] : options;
|
|
629
846
|
let index = 0;
|
|
630
847
|
let previousLineCount = 0;
|
|
631
848
|
const render = () => {
|
|
@@ -637,7 +854,10 @@ async function promptSelect(args) {
|
|
|
637
854
|
const lines = renderSelectionBlock({
|
|
638
855
|
prompt,
|
|
639
856
|
options,
|
|
640
|
-
selectedIndex: index
|
|
857
|
+
selectedIndex: index,
|
|
858
|
+
allowBack: args.allowBack,
|
|
859
|
+
backLabel,
|
|
860
|
+
colorize: Boolean(stream?.isTTY)
|
|
641
861
|
});
|
|
642
862
|
output.write(`${lines.join("\n")}
|
|
643
863
|
`);
|
|
@@ -669,22 +889,30 @@ async function promptSelect(args) {
|
|
|
669
889
|
return;
|
|
670
890
|
}
|
|
671
891
|
if (key.name === "up") {
|
|
672
|
-
index = index === 0 ?
|
|
892
|
+
index = index === 0 ? allOptions.length - 1 : index - 1;
|
|
673
893
|
render();
|
|
674
894
|
return;
|
|
675
895
|
}
|
|
676
896
|
if (key.name === "down") {
|
|
677
|
-
index = (index + 1) %
|
|
897
|
+
index = (index + 1) % allOptions.length;
|
|
678
898
|
render();
|
|
679
899
|
return;
|
|
680
900
|
}
|
|
901
|
+
if (args.allowBack && key.name === "escape") {
|
|
902
|
+
input.off("keypress", onKeypress);
|
|
903
|
+
cleanup();
|
|
904
|
+
input.setRawMode?.(wasRaw);
|
|
905
|
+
input.pause?.();
|
|
906
|
+
resolve(PROMPT_BACK);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
681
909
|
if (key.name === "return" || key.name === "enter") {
|
|
682
|
-
const selected =
|
|
910
|
+
const selected = allOptions[index] ?? allOptions[0] ?? "";
|
|
683
911
|
input.off("keypress", onKeypress);
|
|
684
|
-
cleanup(selected);
|
|
912
|
+
cleanup(selected === backLabel ? void 0 : selected);
|
|
685
913
|
input.setRawMode?.(wasRaw);
|
|
686
914
|
input.pause?.();
|
|
687
|
-
resolve(selected);
|
|
915
|
+
resolve(selected === backLabel ? PROMPT_BACK : selected);
|
|
688
916
|
}
|
|
689
917
|
};
|
|
690
918
|
input.on("keypress", onKeypress);
|
|
@@ -717,6 +945,13 @@ async function promptSecret(args) {
|
|
|
717
945
|
reject(new Error("Aborted."));
|
|
718
946
|
return;
|
|
719
947
|
}
|
|
948
|
+
if (args.allowBack && key.name === "escape") {
|
|
949
|
+
input.off("keypress", onKeypress);
|
|
950
|
+
restoreInputState();
|
|
951
|
+
output.write("\n");
|
|
952
|
+
resolve(PROMPT_BACK);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
720
955
|
if (key.name === "return" || key.name === "enter") {
|
|
721
956
|
input.off("keypress", onKeypress);
|
|
722
957
|
restoreInputState();
|
|
@@ -737,6 +972,7 @@ async function promptSecret(args) {
|
|
|
737
972
|
}
|
|
738
973
|
|
|
739
974
|
// src/commands/config-setup.ts
|
|
975
|
+
var CONFIG_SETUP_BACK = 2;
|
|
740
976
|
function createTerminalIO() {
|
|
741
977
|
let rl;
|
|
742
978
|
function getInterface() {
|
|
@@ -749,22 +985,24 @@ function createTerminalIO() {
|
|
|
749
985
|
}
|
|
750
986
|
return rl;
|
|
751
987
|
}
|
|
752
|
-
async function select(prompt, options) {
|
|
988
|
+
async function select(prompt, options, selectedLabel, allowBack) {
|
|
753
989
|
emitKeypressEvents(defaultStdin2);
|
|
754
990
|
return await promptSelect({
|
|
755
991
|
input: defaultStdin2,
|
|
756
992
|
output: defaultStdout,
|
|
757
993
|
prompt,
|
|
758
994
|
options,
|
|
759
|
-
selectedLabel
|
|
995
|
+
selectedLabel,
|
|
996
|
+
allowBack
|
|
760
997
|
});
|
|
761
998
|
}
|
|
762
|
-
async function secret(prompt) {
|
|
999
|
+
async function secret(prompt, allowBack) {
|
|
763
1000
|
emitKeypressEvents(defaultStdin2);
|
|
764
1001
|
return await promptSecret({
|
|
765
1002
|
input: defaultStdin2,
|
|
766
1003
|
output: defaultStdout,
|
|
767
|
-
prompt
|
|
1004
|
+
prompt,
|
|
1005
|
+
allowBack
|
|
768
1006
|
});
|
|
769
1007
|
}
|
|
770
1008
|
return {
|
|
@@ -795,12 +1033,58 @@ function getSetupPresenter(io) {
|
|
|
795
1033
|
function getProviderLabel(provider) {
|
|
796
1034
|
return provider === "openrouter" ? "OpenRouter" : "OpenAI";
|
|
797
1035
|
}
|
|
1036
|
+
function isBackSelection(value) {
|
|
1037
|
+
return value === PROMPT_BACK || value === PROMPT_BACK_LABEL;
|
|
1038
|
+
}
|
|
1039
|
+
async function promptForOperationMode(io) {
|
|
1040
|
+
if (io.select) {
|
|
1041
|
+
const choice = await io.select(
|
|
1042
|
+
"Choose how sift should work",
|
|
1043
|
+
[
|
|
1044
|
+
"With an agent: recommended if Codex or Claude is already with you; sift does the fast local first pass, the agent only steps in when repo context is truly needed",
|
|
1045
|
+
"With provider fallback: recommended if you want sift to finish more ambiguous cases on its own before handing them back to you or your agent; requires an API key, cheap model only when needed",
|
|
1046
|
+
"Solo, local-only: recommended if you want zero model calls; great for supported presets, ambiguous cases stay with you"
|
|
1047
|
+
],
|
|
1048
|
+
"Mode"
|
|
1049
|
+
);
|
|
1050
|
+
if (choice.startsWith("With an agent")) {
|
|
1051
|
+
return "agent-escalation";
|
|
1052
|
+
}
|
|
1053
|
+
if (choice.startsWith("With provider fallback")) {
|
|
1054
|
+
return "provider-assisted";
|
|
1055
|
+
}
|
|
1056
|
+
if (choice.startsWith("Solo, local-only")) {
|
|
1057
|
+
return "local-only";
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
while (true) {
|
|
1061
|
+
const answer = (await io.ask("Use style [agent/provider/local]: ")).trim().toLowerCase();
|
|
1062
|
+
if (answer === "" || answer === "agent" || answer === "agent-escalation") {
|
|
1063
|
+
return "agent-escalation";
|
|
1064
|
+
}
|
|
1065
|
+
if (answer === "provider" || answer === "provider-assisted") {
|
|
1066
|
+
return "provider-assisted";
|
|
1067
|
+
}
|
|
1068
|
+
if (answer === "local" || answer === "local-only") {
|
|
1069
|
+
return "local-only";
|
|
1070
|
+
}
|
|
1071
|
+
io.error("Please answer agent, provider, or local.\n");
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
798
1074
|
async function promptForProvider(io) {
|
|
799
1075
|
if (io.select) {
|
|
800
|
-
const choice = await io.select(
|
|
801
|
-
"
|
|
802
|
-
|
|
803
|
-
|
|
1076
|
+
const choice = await io.select(
|
|
1077
|
+
"Okay, whose API key are we borrowing for fallback duty?",
|
|
1078
|
+
[
|
|
1079
|
+
"OpenAI",
|
|
1080
|
+
"OpenRouter"
|
|
1081
|
+
],
|
|
1082
|
+
"Provider",
|
|
1083
|
+
true
|
|
1084
|
+
);
|
|
1085
|
+
if (isBackSelection(choice)) {
|
|
1086
|
+
return PROMPT_BACK;
|
|
1087
|
+
}
|
|
804
1088
|
if (choice === "OpenAI") {
|
|
805
1089
|
return "openai";
|
|
806
1090
|
}
|
|
@@ -810,6 +1094,9 @@ async function promptForProvider(io) {
|
|
|
810
1094
|
}
|
|
811
1095
|
while (true) {
|
|
812
1096
|
const answer = (await io.ask("Provider [OpenAI/OpenRouter]: ")).trim().toLowerCase();
|
|
1097
|
+
if (answer === "back" || answer === "b") {
|
|
1098
|
+
return PROMPT_BACK;
|
|
1099
|
+
}
|
|
813
1100
|
if (answer === "" || answer === "openai") {
|
|
814
1101
|
return "openai";
|
|
815
1102
|
}
|
|
@@ -824,7 +1111,13 @@ async function promptForApiKey(io, provider) {
|
|
|
824
1111
|
const promptText = `Enter your ${providerLabel} API key (input hidden): `;
|
|
825
1112
|
const visiblePromptText = `Enter your ${providerLabel} API key: `;
|
|
826
1113
|
while (true) {
|
|
827
|
-
const answer = (await (io.secret ? io.secret(promptText) : io.ask(visiblePromptText))).trim();
|
|
1114
|
+
const answer = (await (io.secret ? io.secret(promptText, true) : io.ask(visiblePromptText))).trim();
|
|
1115
|
+
if (answer === PROMPT_BACK) {
|
|
1116
|
+
return PROMPT_BACK;
|
|
1117
|
+
}
|
|
1118
|
+
if (!io.secret && (answer.toLowerCase() === "back" || answer.toLowerCase() === "b")) {
|
|
1119
|
+
return PROMPT_BACK;
|
|
1120
|
+
}
|
|
828
1121
|
if (answer.length > 0) {
|
|
829
1122
|
return answer;
|
|
830
1123
|
}
|
|
@@ -840,17 +1133,25 @@ async function promptForApiKeyChoice(args) {
|
|
|
840
1133
|
if (args.io.select) {
|
|
841
1134
|
const choice = await args.io.select(
|
|
842
1135
|
`Found both a saved ${providerLabel} API key and ${args.envName} in your environment`,
|
|
843
|
-
["Use saved key", "Use
|
|
1136
|
+
["Use saved key", "Use environment key", "Enter a different key"],
|
|
1137
|
+
"API key",
|
|
1138
|
+
true
|
|
844
1139
|
);
|
|
1140
|
+
if (isBackSelection(choice)) {
|
|
1141
|
+
return PROMPT_BACK;
|
|
1142
|
+
}
|
|
845
1143
|
if (choice === "Use saved key") {
|
|
846
1144
|
return "saved";
|
|
847
1145
|
}
|
|
848
|
-
if (choice === "Use
|
|
1146
|
+
if (choice === "Use environment key") {
|
|
849
1147
|
return "env";
|
|
850
1148
|
}
|
|
851
1149
|
}
|
|
852
1150
|
while (true) {
|
|
853
1151
|
const answer = (await args.io.ask("API key choice [saved/env/override]: ")).trim().toLowerCase();
|
|
1152
|
+
if (answer === "back" || answer === "b") {
|
|
1153
|
+
return PROMPT_BACK;
|
|
1154
|
+
}
|
|
854
1155
|
if (answer === "" || answer === "saved") {
|
|
855
1156
|
return "saved";
|
|
856
1157
|
}
|
|
@@ -867,15 +1168,23 @@ async function promptForApiKeyChoice(args) {
|
|
|
867
1168
|
if (args.io.select) {
|
|
868
1169
|
const choice = await args.io.select(
|
|
869
1170
|
`Found an existing ${providerLabel} API key via ${sourceLabel}`,
|
|
870
|
-
["Use
|
|
1171
|
+
["Use saved key", "Enter a different key"],
|
|
1172
|
+
"API key",
|
|
1173
|
+
true
|
|
871
1174
|
);
|
|
872
|
-
if (choice
|
|
1175
|
+
if (isBackSelection(choice)) {
|
|
1176
|
+
return PROMPT_BACK;
|
|
1177
|
+
}
|
|
1178
|
+
if (choice === "Enter a different key") {
|
|
873
1179
|
return "override";
|
|
874
1180
|
}
|
|
875
1181
|
return args.hasSavedKey ? "saved" : "env";
|
|
876
1182
|
}
|
|
877
1183
|
while (true) {
|
|
878
1184
|
const answer = (await args.io.ask("API key choice [existing/override]: ")).trim().toLowerCase();
|
|
1185
|
+
if (answer === "back" || answer === "b") {
|
|
1186
|
+
return PROMPT_BACK;
|
|
1187
|
+
}
|
|
879
1188
|
if (answer === "" || answer === "existing") {
|
|
880
1189
|
return args.hasSavedKey ? "saved" : "env";
|
|
881
1190
|
}
|
|
@@ -885,12 +1194,41 @@ async function promptForApiKeyChoice(args) {
|
|
|
885
1194
|
args.io.error("Please answer existing or override.\n");
|
|
886
1195
|
}
|
|
887
1196
|
}
|
|
888
|
-
function
|
|
1197
|
+
function writeModeSummary(io, mode) {
|
|
1198
|
+
const ui = getSetupPresenter(io);
|
|
1199
|
+
io.write(`${ui.info(`Operating mode: ${getOperationModeLabel(mode)}`)}
|
|
1200
|
+
`);
|
|
1201
|
+
io.write(`${ui.note(describeOperationMode(mode))}
|
|
1202
|
+
`);
|
|
1203
|
+
io.write(`${ui.note(describeInsufficientBehavior(mode))}
|
|
1204
|
+
`);
|
|
1205
|
+
if (mode === "agent-escalation") {
|
|
1206
|
+
io.write(
|
|
1207
|
+
`${ui.note("Plain English: pick this if you already use Codex or Claude. sift gives the first answer; the agent only steps in when deeper repo context is really needed. No API key.")}
|
|
1208
|
+
`
|
|
1209
|
+
);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (mode === "provider-assisted") {
|
|
1213
|
+
io.write(
|
|
1214
|
+
`${ui.note("Plain English: pick this if you want sift itself to finish more fuzzy cases before you have to step in or re-prompt an agent. Yes, that means an API key, but the model is intentionally the cheap backup, not the fancy main act.")}
|
|
1215
|
+
`
|
|
1216
|
+
);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
io.write(
|
|
1220
|
+
`${ui.note("Plain English: pick this if sift is working alone. No API key, no model fallback. If the answer is still fuzzy, you inspect the code or logs yourself.")}
|
|
1221
|
+
`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
function writeSetupSuccess(io, writtenPath, mode) {
|
|
889
1225
|
const ui = getSetupPresenter(io);
|
|
890
1226
|
io.write(`
|
|
891
1227
|
${ui.success("You're set.")}
|
|
892
1228
|
`);
|
|
893
1229
|
io.write(`${ui.info(`Machine-wide config: ${writtenPath}`)}
|
|
1230
|
+
`);
|
|
1231
|
+
io.write(`${ui.labelValue("operation mode", getOperationModeLabel(mode))}
|
|
894
1232
|
`);
|
|
895
1233
|
io.write(`${ui.note("sift is ready to use from any terminal on this machine.")}
|
|
896
1234
|
`);
|
|
@@ -906,31 +1244,114 @@ function writeOverrideWarning(io, activeConfigPath) {
|
|
|
906
1244
|
`
|
|
907
1245
|
);
|
|
908
1246
|
}
|
|
909
|
-
function writeNextSteps(io) {
|
|
1247
|
+
function writeNextSteps(io, mode) {
|
|
910
1248
|
const ui = getSetupPresenter(io);
|
|
911
1249
|
io.write(`
|
|
912
1250
|
${ui.section("Try next")}
|
|
913
1251
|
`);
|
|
914
1252
|
io.write(` ${ui.command("sift doctor")}
|
|
915
1253
|
`);
|
|
1254
|
+
if (mode === "provider-assisted") {
|
|
1255
|
+
io.write(` ${ui.command("sift config show --show-secrets")}
|
|
1256
|
+
`);
|
|
1257
|
+
} else {
|
|
1258
|
+
io.write(
|
|
1259
|
+
` ${ui.command("sift config show")}${ui.note(" # rerun setup later if you want provider-assisted fallback")}
|
|
1260
|
+
`
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
916
1263
|
io.write(` ${ui.command("sift exec --preset test-status -- npm test")}
|
|
917
1264
|
`);
|
|
918
1265
|
}
|
|
919
|
-
function
|
|
1266
|
+
async function promptForProviderModel(args) {
|
|
1267
|
+
const options = getProviderModelOptions(args.provider);
|
|
1268
|
+
const customCurrent = args.currentModel && !findProviderModelOption(args.provider, args.currentModel) ? `Keep current custom model (${args.currentModel})` : "Custom model";
|
|
1269
|
+
if (args.io.select) {
|
|
1270
|
+
const labels = options.map((option) => `${option.label} - ${option.note}`);
|
|
1271
|
+
labels.push(customCurrent);
|
|
1272
|
+
const choice = await args.io.select(
|
|
1273
|
+
"Pick the fallback model. Cheap is usually the right answer here; this only wakes up when sift needs help.",
|
|
1274
|
+
labels,
|
|
1275
|
+
"Model",
|
|
1276
|
+
true
|
|
1277
|
+
);
|
|
1278
|
+
if (isBackSelection(choice)) {
|
|
1279
|
+
return PROMPT_BACK;
|
|
1280
|
+
}
|
|
1281
|
+
const match = options.find((option) => choice.startsWith(option.label));
|
|
1282
|
+
if (match) {
|
|
1283
|
+
return match.model;
|
|
1284
|
+
}
|
|
1285
|
+
if (customCurrent.startsWith("Keep current custom model")) {
|
|
1286
|
+
return args.currentModel ?? getDefaultProviderModel(args.provider);
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
args.io.write("\nPick the fallback model.\n\n");
|
|
1290
|
+
options.forEach((option, index) => {
|
|
1291
|
+
args.io.write(` ${index + 1}) ${option.label} - ${option.note}
|
|
1292
|
+
`);
|
|
1293
|
+
});
|
|
1294
|
+
args.io.write(` ${options.length + 1}) ${customCurrent}
|
|
1295
|
+
|
|
1296
|
+
`);
|
|
1297
|
+
while (true) {
|
|
1298
|
+
const answer = (await args.io.ask("Model choice [1]: ")).trim();
|
|
1299
|
+
if (answer.toLowerCase() === "back" || answer.toLowerCase() === "b") {
|
|
1300
|
+
return PROMPT_BACK;
|
|
1301
|
+
}
|
|
1302
|
+
if (answer === "") {
|
|
1303
|
+
return options[0].model;
|
|
1304
|
+
}
|
|
1305
|
+
const index = Number(answer);
|
|
1306
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length) {
|
|
1307
|
+
return options[index - 1].model;
|
|
1308
|
+
}
|
|
1309
|
+
if (Number.isInteger(index) && index === options.length + 1) {
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
args.io.error(`Please enter a number between 1 and ${options.length + 1}.
|
|
1313
|
+
`);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
while (true) {
|
|
1317
|
+
const answer = (await args.io.ask("Custom model id: ")).trim();
|
|
1318
|
+
if (answer.toLowerCase() === "back" || answer.toLowerCase() === "b") {
|
|
1319
|
+
return PROMPT_BACK;
|
|
1320
|
+
}
|
|
1321
|
+
if (answer.length > 0) {
|
|
1322
|
+
return answer;
|
|
1323
|
+
}
|
|
1324
|
+
args.io.error("Model id cannot be empty.\n");
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
function writeProviderDefaults(io, provider, selectedModel) {
|
|
920
1328
|
const ui = getSetupPresenter(io);
|
|
1329
|
+
const options = getProviderModelOptions(provider);
|
|
921
1330
|
if (provider === "openrouter") {
|
|
922
|
-
io.write(`${ui.info("
|
|
1331
|
+
io.write(`${ui.info("OpenRouter fallback it is. Free is lovely right up until latency develops a personality.")}
|
|
923
1332
|
`);
|
|
924
|
-
io.write(`${ui.labelValue("Default model", "openrouter
|
|
1333
|
+
io.write(`${ui.labelValue("Default model", getDefaultProviderModel("openrouter"))}
|
|
925
1334
|
`);
|
|
926
1335
|
io.write(`${ui.labelValue("Default base URL", "https://openrouter.ai/api/v1")}
|
|
927
1336
|
`);
|
|
928
1337
|
} else {
|
|
929
|
-
io.write(`${ui.info("
|
|
1338
|
+
io.write(`${ui.info("OpenAI fallback it is. Start cheap, save the fancy stuff for when the logs deserve it.")}
|
|
930
1339
|
`);
|
|
931
|
-
io.write(`${ui.labelValue("Default model", "
|
|
1340
|
+
io.write(`${ui.labelValue("Default model", getDefaultProviderModel("openai"))}
|
|
932
1341
|
`);
|
|
933
1342
|
io.write(`${ui.labelValue("Default base URL", "https://api.openai.com/v1")}
|
|
1343
|
+
`);
|
|
1344
|
+
}
|
|
1345
|
+
io.write(`${ui.labelValue("Selected model", selectedModel)}
|
|
1346
|
+
`);
|
|
1347
|
+
io.write(
|
|
1348
|
+
`${ui.note("This fallback only wakes up when sift's own rules are not enough. The idea is fewer dead ends, not paying for a second opinion on every command.")}
|
|
1349
|
+
`
|
|
1350
|
+
);
|
|
1351
|
+
io.write(`${ui.note("Popular alternatives:")}
|
|
1352
|
+
`);
|
|
1353
|
+
for (const option of options.filter((option2) => option2.model !== selectedModel)) {
|
|
1354
|
+
io.write(` ${ui.command(option.label)}${ui.note(` # ${option.note}`)}
|
|
934
1355
|
`);
|
|
935
1356
|
}
|
|
936
1357
|
io.write(
|
|
@@ -942,54 +1363,101 @@ function writeProviderDefaults(io, provider) {
|
|
|
942
1363
|
`
|
|
943
1364
|
);
|
|
944
1365
|
}
|
|
945
|
-
function materializeProfile(provider, profile,
|
|
1366
|
+
function materializeProfile(provider, profile, overrides = {}) {
|
|
946
1367
|
return {
|
|
1368
|
+
...profile,
|
|
947
1369
|
...getProfileProviderState(provider, profile),
|
|
948
|
-
...
|
|
1370
|
+
...overrides.model !== void 0 ? { model: overrides.model } : {},
|
|
1371
|
+
...overrides.apiKey !== void 0 ? { apiKey: overrides.apiKey } : {}
|
|
949
1372
|
};
|
|
950
1373
|
}
|
|
951
1374
|
function buildSetupConfig(args) {
|
|
952
1375
|
const preservedConfig = preserveActiveNativeProviderProfile(args.config);
|
|
1376
|
+
if (args.mode !== "provider-assisted") {
|
|
1377
|
+
return {
|
|
1378
|
+
...preservedConfig,
|
|
1379
|
+
runtime: {
|
|
1380
|
+
...preservedConfig.runtime,
|
|
1381
|
+
operationMode: args.mode
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
if (!args.provider || !args.apiKeyChoice) {
|
|
1386
|
+
throw new Error("Provider-assisted setup requires provider and API key choice.");
|
|
1387
|
+
}
|
|
953
1388
|
const storedProfile = getStoredProviderProfile(preservedConfig, args.provider);
|
|
954
1389
|
if (args.apiKeyChoice === "saved") {
|
|
955
1390
|
const profile2 = materializeProfile(
|
|
956
1391
|
args.provider,
|
|
957
1392
|
storedProfile,
|
|
958
|
-
|
|
1393
|
+
{
|
|
1394
|
+
apiKey: storedProfile?.apiKey ?? "",
|
|
1395
|
+
model: args.model
|
|
1396
|
+
}
|
|
959
1397
|
);
|
|
960
1398
|
const configWithProfile2 = setStoredProviderProfile(
|
|
961
1399
|
preservedConfig,
|
|
962
1400
|
args.provider,
|
|
963
1401
|
profile2
|
|
964
1402
|
);
|
|
965
|
-
|
|
1403
|
+
const applied2 = applyActiveProvider(
|
|
966
1404
|
configWithProfile2,
|
|
967
1405
|
args.provider,
|
|
968
1406
|
profile2,
|
|
969
1407
|
profile2.apiKey ?? ""
|
|
970
1408
|
);
|
|
1409
|
+
return {
|
|
1410
|
+
...applied2,
|
|
1411
|
+
runtime: {
|
|
1412
|
+
...applied2.runtime,
|
|
1413
|
+
operationMode: args.mode
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
971
1416
|
}
|
|
972
1417
|
if (args.apiKeyChoice === "env") {
|
|
973
|
-
const profile2 =
|
|
974
|
-
|
|
975
|
-
|
|
1418
|
+
const profile2 = materializeProfile(args.provider, storedProfile, {
|
|
1419
|
+
model: args.model
|
|
1420
|
+
});
|
|
1421
|
+
const configWithProfile2 = setStoredProviderProfile(
|
|
1422
|
+
preservedConfig,
|
|
1423
|
+
args.provider,
|
|
1424
|
+
profile2
|
|
1425
|
+
);
|
|
1426
|
+
const applied2 = applyActiveProvider(configWithProfile2, args.provider, profile2, "");
|
|
1427
|
+
return {
|
|
1428
|
+
...applied2,
|
|
1429
|
+
runtime: {
|
|
1430
|
+
...applied2.runtime,
|
|
1431
|
+
operationMode: args.mode
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
976
1434
|
}
|
|
977
1435
|
const profile = materializeProfile(
|
|
978
1436
|
args.provider,
|
|
979
1437
|
storedProfile,
|
|
980
|
-
|
|
1438
|
+
{
|
|
1439
|
+
apiKey: args.nextApiKey ?? "",
|
|
1440
|
+
model: args.model
|
|
1441
|
+
}
|
|
981
1442
|
);
|
|
982
1443
|
const configWithProfile = setStoredProviderProfile(
|
|
983
1444
|
preservedConfig,
|
|
984
1445
|
args.provider,
|
|
985
1446
|
profile
|
|
986
1447
|
);
|
|
987
|
-
|
|
1448
|
+
const applied = applyActiveProvider(
|
|
988
1449
|
configWithProfile,
|
|
989
1450
|
args.provider,
|
|
990
1451
|
profile,
|
|
991
1452
|
args.nextApiKey ?? ""
|
|
992
1453
|
);
|
|
1454
|
+
return {
|
|
1455
|
+
...applied,
|
|
1456
|
+
runtime: {
|
|
1457
|
+
...applied.runtime,
|
|
1458
|
+
operationMode: args.mode
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
993
1461
|
}
|
|
994
1462
|
async function configSetup(options = {}) {
|
|
995
1463
|
void options.global;
|
|
@@ -1003,29 +1471,119 @@ async function configSetup(options = {}) {
|
|
|
1003
1471
|
);
|
|
1004
1472
|
return 1;
|
|
1005
1473
|
}
|
|
1006
|
-
|
|
1474
|
+
if (!options.embedded) {
|
|
1475
|
+
io.write(`${ui.welcome("Let's keep the expensive model for the interesting bits.")}
|
|
1007
1476
|
`);
|
|
1477
|
+
io.write(`${ui.note('"Sharp first, expensive later."')}
|
|
1478
|
+
`);
|
|
1479
|
+
} else {
|
|
1480
|
+
io.write(`${ui.info("Next: provider, model, and credentials. Press Esc any time if you want to step back.")}
|
|
1481
|
+
`);
|
|
1482
|
+
}
|
|
1008
1483
|
const resolvedPath = resolveSetupPath(options.targetPath);
|
|
1009
1484
|
const { config: existingConfig, existed } = loadEditableConfig(resolvedPath);
|
|
1010
1485
|
if (existed) {
|
|
1011
1486
|
io.write(`${ui.info(`Updating existing config at ${resolvedPath}.`)}
|
|
1012
1487
|
`);
|
|
1013
1488
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1489
|
+
let mode = options.forcedMode ?? await promptForOperationMode(io);
|
|
1490
|
+
let provider;
|
|
1491
|
+
let model;
|
|
1492
|
+
let apiKeyChoice;
|
|
1493
|
+
let nextApiKey;
|
|
1494
|
+
let modeSummaryShown = false;
|
|
1495
|
+
while (true) {
|
|
1496
|
+
if (!modeSummaryShown) {
|
|
1497
|
+
writeModeSummary(io, mode);
|
|
1498
|
+
modeSummaryShown = true;
|
|
1499
|
+
}
|
|
1500
|
+
if (mode !== "provider-assisted") {
|
|
1501
|
+
io.write(
|
|
1502
|
+
`${ui.note("No provider credentials are required for this mode. You can switch later by running `sift config setup` again.")}
|
|
1503
|
+
`
|
|
1504
|
+
);
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
let providerStep = "provider";
|
|
1508
|
+
while (true) {
|
|
1509
|
+
if (providerStep === "provider") {
|
|
1510
|
+
const providerChoice = await promptForProvider(io);
|
|
1511
|
+
if (providerChoice === PROMPT_BACK) {
|
|
1512
|
+
if (options.forcedMode) {
|
|
1513
|
+
return options.embedded ? CONFIG_SETUP_BACK : 1;
|
|
1514
|
+
}
|
|
1515
|
+
mode = await promptForOperationMode(io);
|
|
1516
|
+
modeSummaryShown = false;
|
|
1517
|
+
provider = void 0;
|
|
1518
|
+
model = void 0;
|
|
1519
|
+
apiKeyChoice = void 0;
|
|
1520
|
+
nextApiKey = void 0;
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
provider = providerChoice;
|
|
1524
|
+
providerStep = "model";
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
const storedProfile = getStoredProviderProfile(existingConfig, provider);
|
|
1528
|
+
if (providerStep === "model") {
|
|
1529
|
+
const modelChoice = await promptForProviderModel({
|
|
1530
|
+
io,
|
|
1531
|
+
provider,
|
|
1532
|
+
currentModel: storedProfile?.model
|
|
1533
|
+
});
|
|
1534
|
+
if (modelChoice === PROMPT_BACK) {
|
|
1535
|
+
providerStep = "provider";
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
model = modelChoice;
|
|
1539
|
+
writeProviderDefaults(io, provider, model);
|
|
1540
|
+
providerStep = "api-key-choice";
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
const envName = getNativeProviderApiKeyEnvName(provider);
|
|
1544
|
+
if (providerStep === "api-key-choice") {
|
|
1545
|
+
const keyChoice = await promptForApiKeyChoice({
|
|
1546
|
+
io,
|
|
1547
|
+
provider,
|
|
1548
|
+
envName,
|
|
1549
|
+
hasSavedKey: Boolean(storedProfile?.apiKey),
|
|
1550
|
+
hasEnvKey: Boolean(env[envName])
|
|
1551
|
+
});
|
|
1552
|
+
if (keyChoice === PROMPT_BACK) {
|
|
1553
|
+
providerStep = "model";
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
apiKeyChoice = keyChoice;
|
|
1557
|
+
if (apiKeyChoice === "override") {
|
|
1558
|
+
io.write(`${ui.note("Press Esc if you want to go back instead of entering a key right now.")}
|
|
1559
|
+
`);
|
|
1560
|
+
providerStep = "api-key-entry";
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
nextApiKey = void 0;
|
|
1564
|
+
break;
|
|
1565
|
+
}
|
|
1566
|
+
const apiKey = await promptForApiKey(io, provider);
|
|
1567
|
+
if (apiKey === PROMPT_BACK) {
|
|
1568
|
+
providerStep = "api-key-choice";
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
nextApiKey = apiKey;
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
if (mode !== "provider-assisted") {
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
if (!provider || !apiKeyChoice) {
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1026
1582
|
const config = buildSetupConfig({
|
|
1027
1583
|
config: existingConfig,
|
|
1584
|
+
mode,
|
|
1028
1585
|
provider,
|
|
1586
|
+
model,
|
|
1029
1587
|
apiKeyChoice,
|
|
1030
1588
|
nextApiKey
|
|
1031
1589
|
});
|
|
@@ -1034,18 +1592,19 @@ async function configSetup(options = {}) {
|
|
|
1034
1592
|
config,
|
|
1035
1593
|
overwrite: existed
|
|
1036
1594
|
});
|
|
1037
|
-
if (apiKeyChoice === "env") {
|
|
1595
|
+
if (mode === "provider-assisted" && provider && apiKeyChoice === "env") {
|
|
1596
|
+
const envName = getNativeProviderApiKeyEnvName(provider);
|
|
1038
1597
|
io.write(
|
|
1039
1598
|
`${ui.note(`Using ${envName} from the environment. No API key was written to config.`)}
|
|
1040
1599
|
`
|
|
1041
1600
|
);
|
|
1042
1601
|
}
|
|
1043
|
-
writeSetupSuccess(io, writtenPath);
|
|
1602
|
+
writeSetupSuccess(io, writtenPath, mode);
|
|
1044
1603
|
const activeConfigPath = findConfigPath();
|
|
1045
1604
|
if (activeConfigPath && path5.resolve(activeConfigPath) !== path5.resolve(writtenPath)) {
|
|
1046
1605
|
writeOverrideWarning(io, activeConfigPath);
|
|
1047
1606
|
}
|
|
1048
|
-
writeNextSteps(io);
|
|
1607
|
+
writeNextSteps(io, mode);
|
|
1049
1608
|
return 0;
|
|
1050
1609
|
} finally {
|
|
1051
1610
|
io.close?.();
|
|
@@ -1072,18 +1631,18 @@ function maskConfigSecrets(value) {
|
|
|
1072
1631
|
return output;
|
|
1073
1632
|
}
|
|
1074
1633
|
function configInit(targetPath, global = false) {
|
|
1075
|
-
const
|
|
1634
|
+
const path9 = writeExampleConfig({
|
|
1076
1635
|
targetPath,
|
|
1077
1636
|
global
|
|
1078
1637
|
});
|
|
1079
1638
|
if (!process.stdout.isTTY) {
|
|
1080
|
-
process.stdout.write(`${
|
|
1639
|
+
process.stdout.write(`${path9}
|
|
1081
1640
|
`);
|
|
1082
1641
|
return;
|
|
1083
1642
|
}
|
|
1084
1643
|
const ui = createPresentation(true);
|
|
1085
1644
|
process.stdout.write(
|
|
1086
|
-
`${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${
|
|
1645
|
+
`${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path9}`)}
|
|
1087
1646
|
`
|
|
1088
1647
|
);
|
|
1089
1648
|
}
|
|
@@ -1164,7 +1723,7 @@ function configUse(provider, configPath, env = process.env) {
|
|
|
1164
1723
|
}
|
|
1165
1724
|
|
|
1166
1725
|
// src/commands/agent.ts
|
|
1167
|
-
import
|
|
1726
|
+
import fs5 from "fs";
|
|
1168
1727
|
import os2 from "os";
|
|
1169
1728
|
import path6 from "path";
|
|
1170
1729
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
@@ -1246,16 +1805,34 @@ function getManagedBlockMarkers(agent) {
|
|
|
1246
1805
|
end: `<!-- sift:end ${agent} -->`
|
|
1247
1806
|
};
|
|
1248
1807
|
}
|
|
1249
|
-
function
|
|
1808
|
+
function inferOperationMode(args) {
|
|
1809
|
+
if (args.operationMode) {
|
|
1810
|
+
return args.operationMode;
|
|
1811
|
+
}
|
|
1812
|
+
try {
|
|
1813
|
+
const cwd = args.cwd ?? process.cwd();
|
|
1814
|
+
const homeDir = args.homeDir ?? os2.homedir();
|
|
1815
|
+
const repoConfigPath = path6.resolve(cwd, "sift.config.yaml");
|
|
1816
|
+
const globalConfigPath = getDefaultGlobalConfigPath(homeDir);
|
|
1817
|
+
const configPath = fs5.existsSync(repoConfigPath) ? repoConfigPath : fs5.existsSync(globalConfigPath) ? globalConfigPath : void 0;
|
|
1818
|
+
const config = resolveConfig(configPath ? { configPath } : {});
|
|
1819
|
+
return resolveEffectiveOperationMode(config);
|
|
1820
|
+
} catch {
|
|
1821
|
+
return "agent-escalation";
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function renderInstructionBody(mode) {
|
|
1250
1825
|
return [
|
|
1251
|
-
"Use `sift` as the default
|
|
1252
|
-
"The goal is to
|
|
1826
|
+
"Use `sift` as the default local-first output guidance layer for long, noisy, non-interactive command output.",
|
|
1827
|
+
"The goal is to turn failure walls into grouped issues, likely root causes, and the next useful step before reading raw logs.",
|
|
1828
|
+
"",
|
|
1829
|
+
`Default operating mode: ${getOperationModeLabel(mode)}.`,
|
|
1830
|
+
describeOperationMode(mode),
|
|
1831
|
+
describeInsufficientBehavior(mode),
|
|
1253
1832
|
"",
|
|
1254
1833
|
"Start with:",
|
|
1255
1834
|
'- `sift exec "question" -- <command> [args...]`',
|
|
1256
1835
|
"- `sift exec --preset test-status -- <test command>`",
|
|
1257
|
-
'- `sift watch "what changed between cycles?" < watcher-output.txt`',
|
|
1258
|
-
'- `sift exec --watch "what changed between cycles?" -- <command> [args...]`',
|
|
1259
1836
|
"- `sift exec --preset audit-critical -- npm audit`",
|
|
1260
1837
|
"- `sift exec --preset infra-risk -- terraform plan`",
|
|
1261
1838
|
"",
|
|
@@ -1294,9 +1871,9 @@ function renderInstructionBody() {
|
|
|
1294
1871
|
"Do not pass API keys inline."
|
|
1295
1872
|
].join("\n");
|
|
1296
1873
|
}
|
|
1297
|
-
function renderManagedBlock(agent, eol = "\n") {
|
|
1874
|
+
function renderManagedBlock(agent, eol = "\n", mode = "agent-escalation") {
|
|
1298
1875
|
const markers = getManagedBlockMarkers(agent);
|
|
1299
|
-
return [markers.start, renderInstructionBody(), markers.end].join(eol);
|
|
1876
|
+
return [markers.start, renderInstructionBody(mode), markers.end].join(eol);
|
|
1300
1877
|
}
|
|
1301
1878
|
function inspectManagedBlock(content, agent) {
|
|
1302
1879
|
const markers = getManagedBlockMarkers(agent);
|
|
@@ -1321,7 +1898,7 @@ function inspectManagedBlock(content, agent) {
|
|
|
1321
1898
|
}
|
|
1322
1899
|
function planManagedInstall(args) {
|
|
1323
1900
|
const eol = args.existingContent?.includes("\r\n") ? "\r\n" : "\n";
|
|
1324
|
-
const block = renderManagedBlock(args.agent, eol);
|
|
1901
|
+
const block = renderManagedBlock(args.agent, eol, args.operationMode ?? "agent-escalation");
|
|
1325
1902
|
if (args.existingContent === void 0) {
|
|
1326
1903
|
return {
|
|
1327
1904
|
action: "create",
|
|
@@ -1411,6 +1988,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1411
1988
|
const params = typeof args === "string" ? {
|
|
1412
1989
|
agent: args,
|
|
1413
1990
|
scope: "repo",
|
|
1991
|
+
operationMode: void 0,
|
|
1414
1992
|
raw: false,
|
|
1415
1993
|
targetPath: void 0,
|
|
1416
1994
|
cwd: void 0,
|
|
@@ -1419,6 +1997,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1419
1997
|
} : {
|
|
1420
1998
|
agent: args.agent,
|
|
1421
1999
|
scope: args.scope ?? "repo",
|
|
2000
|
+
operationMode: args.operationMode,
|
|
1422
2001
|
raw: args.raw ?? false,
|
|
1423
2002
|
targetPath: args.targetPath,
|
|
1424
2003
|
cwd: args.cwd,
|
|
@@ -1427,8 +2006,13 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1427
2006
|
};
|
|
1428
2007
|
const agent = normalizeAgentName(params.agent);
|
|
1429
2008
|
const io = params.io;
|
|
2009
|
+
const operationMode = inferOperationMode({
|
|
2010
|
+
cwd: params.cwd,
|
|
2011
|
+
homeDir: params.homeDir,
|
|
2012
|
+
operationMode: params.operationMode
|
|
2013
|
+
});
|
|
1430
2014
|
if (params.raw) {
|
|
1431
|
-
io.write(`${renderManagedBlock(agent)}
|
|
2015
|
+
io.write(`${renderManagedBlock(agent, "\n", operationMode)}
|
|
1432
2016
|
`);
|
|
1433
2017
|
return;
|
|
1434
2018
|
}
|
|
@@ -1467,6 +2051,8 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1467
2051
|
)}
|
|
1468
2052
|
`
|
|
1469
2053
|
);
|
|
2054
|
+
io.write(`${ui.labelValue("operation mode", getOperationModeLabel(operationMode))}
|
|
2055
|
+
`);
|
|
1470
2056
|
if (currentInstalled) {
|
|
1471
2057
|
io.write(`${ui.warning(`Already installed in ${params.scope} scope.`)}
|
|
1472
2058
|
`);
|
|
@@ -1484,13 +2070,17 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1484
2070
|
`
|
|
1485
2071
|
);
|
|
1486
2072
|
io.write(
|
|
1487
|
-
`${ui.info("The point is to
|
|
2073
|
+
`${ui.info("The point is to narrow long command output before your agent burns time and tokens on the raw log wall.")}
|
|
1488
2074
|
`
|
|
1489
2075
|
);
|
|
1490
2076
|
io.write(
|
|
1491
2077
|
`${ui.info("The managed block teaches the agent to default to sift first, keep raw as the last resort, and treat standard as the usual stop point.")}
|
|
1492
2078
|
`
|
|
1493
2079
|
);
|
|
2080
|
+
io.write(`${ui.note(describeOperationMode(operationMode))}
|
|
2081
|
+
`);
|
|
2082
|
+
io.write(`${ui.note(describeInsufficientBehavior(operationMode))}
|
|
2083
|
+
`);
|
|
1494
2084
|
io.write(` ${ui.command('sift exec "question" -- <command> [args...]')}
|
|
1495
2085
|
`);
|
|
1496
2086
|
io.write(` ${ui.command("sift exec --preset test-status -- <test command>")}
|
|
@@ -1500,7 +2090,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1500
2090
|
io.write(` ${ui.command("sift exec --preset infra-risk -- terraform plan")}
|
|
1501
2091
|
`);
|
|
1502
2092
|
io.write(
|
|
1503
|
-
`${ui.info("For test debugging, standard should usually be enough for first-pass
|
|
2093
|
+
`${ui.info("For test debugging, standard should usually be enough for first-pass guidance.")}
|
|
1504
2094
|
`
|
|
1505
2095
|
);
|
|
1506
2096
|
io.write(
|
|
@@ -1550,6 +2140,11 @@ async function installAgent(args) {
|
|
|
1550
2140
|
homeDir: args.homeDir
|
|
1551
2141
|
});
|
|
1552
2142
|
const ui = createPresentation(io.stdoutIsTTY);
|
|
2143
|
+
const operationMode = inferOperationMode({
|
|
2144
|
+
cwd: args.cwd,
|
|
2145
|
+
homeDir: args.homeDir,
|
|
2146
|
+
operationMode: args.operationMode
|
|
2147
|
+
});
|
|
1553
2148
|
try {
|
|
1554
2149
|
const existingContent = readOptionalFile(targetPath);
|
|
1555
2150
|
const fileExists = existingContent !== void 0;
|
|
@@ -1557,7 +2152,8 @@ async function installAgent(args) {
|
|
|
1557
2152
|
const plan = planManagedInstall({
|
|
1558
2153
|
agent,
|
|
1559
2154
|
targetPath,
|
|
1560
|
-
existingContent
|
|
2155
|
+
existingContent,
|
|
2156
|
+
operationMode
|
|
1561
2157
|
});
|
|
1562
2158
|
if (args.dryRun) {
|
|
1563
2159
|
if (args.raw) {
|
|
@@ -1613,6 +2209,8 @@ async function installAgent(args) {
|
|
|
1613
2209
|
io.write(`${ui.labelValue("scope", scope)}
|
|
1614
2210
|
`);
|
|
1615
2211
|
io.write(`${ui.labelValue("target", targetPath)}
|
|
2212
|
+
`);
|
|
2213
|
+
io.write(`${ui.labelValue("operation mode", getOperationModeLabel(operationMode))}
|
|
1616
2214
|
`);
|
|
1617
2215
|
io.write(`${ui.info("This will only manage the sift block.")}
|
|
1618
2216
|
`);
|
|
@@ -1794,25 +2392,484 @@ function joinAroundRemoval(before, after, eol) {
|
|
|
1794
2392
|
return `${left}${eol}${eol}${right}`;
|
|
1795
2393
|
}
|
|
1796
2394
|
function readOptionalFile(targetPath) {
|
|
1797
|
-
if (!
|
|
2395
|
+
if (!fs5.existsSync(targetPath)) {
|
|
1798
2396
|
return void 0;
|
|
1799
2397
|
}
|
|
1800
|
-
const stats =
|
|
2398
|
+
const stats = fs5.statSync(targetPath);
|
|
1801
2399
|
if (!stats.isFile()) {
|
|
1802
2400
|
throw new Error(`${targetPath} exists but is not a file.`);
|
|
1803
2401
|
}
|
|
1804
|
-
return
|
|
2402
|
+
return fs5.readFileSync(targetPath, "utf8");
|
|
1805
2403
|
}
|
|
1806
2404
|
function writeTextFileAtomic(targetPath, content) {
|
|
1807
|
-
|
|
2405
|
+
fs5.mkdirSync(path6.dirname(targetPath), { recursive: true });
|
|
1808
2406
|
const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
1809
|
-
|
|
1810
|
-
|
|
2407
|
+
fs5.writeFileSync(tempPath, content, "utf8");
|
|
2408
|
+
fs5.renameSync(tempPath, targetPath);
|
|
1811
2409
|
}
|
|
1812
2410
|
function escapeRegExp(value) {
|
|
1813
2411
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1814
2412
|
}
|
|
1815
2413
|
|
|
2414
|
+
// src/commands/install.ts
|
|
2415
|
+
import os3 from "os";
|
|
2416
|
+
import path7 from "path";
|
|
2417
|
+
import { emitKeypressEvents as emitKeypressEvents2 } from "readline";
|
|
2418
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
2419
|
+
import {
|
|
2420
|
+
stderr as defaultStderr3,
|
|
2421
|
+
stdin as defaultStdin4,
|
|
2422
|
+
stdout as defaultStdout3
|
|
2423
|
+
} from "process";
|
|
2424
|
+
var INSTALL_TITLES = {
|
|
2425
|
+
codex: "Codex",
|
|
2426
|
+
claude: "Claude"
|
|
2427
|
+
};
|
|
2428
|
+
function createInstallTerminalIO() {
|
|
2429
|
+
let rl;
|
|
2430
|
+
function getInterface() {
|
|
2431
|
+
if (!rl) {
|
|
2432
|
+
rl = createInterface3({
|
|
2433
|
+
input: defaultStdin4,
|
|
2434
|
+
output: defaultStdout3,
|
|
2435
|
+
terminal: true
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
return rl;
|
|
2439
|
+
}
|
|
2440
|
+
async function select(prompt, options, selectedLabel, allowBack) {
|
|
2441
|
+
emitKeypressEvents2(defaultStdin4);
|
|
2442
|
+
return await promptSelect({
|
|
2443
|
+
input: defaultStdin4,
|
|
2444
|
+
output: defaultStdout3,
|
|
2445
|
+
prompt,
|
|
2446
|
+
options,
|
|
2447
|
+
selectedLabel,
|
|
2448
|
+
allowBack
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
return {
|
|
2452
|
+
stdinIsTTY: Boolean(defaultStdin4.isTTY),
|
|
2453
|
+
stdoutIsTTY: Boolean(defaultStdout3.isTTY),
|
|
2454
|
+
ask(prompt) {
|
|
2455
|
+
return getInterface().question(prompt);
|
|
2456
|
+
},
|
|
2457
|
+
select,
|
|
2458
|
+
write(message) {
|
|
2459
|
+
defaultStdout3.write(message);
|
|
2460
|
+
},
|
|
2461
|
+
error(message) {
|
|
2462
|
+
defaultStderr3.write(message);
|
|
2463
|
+
},
|
|
2464
|
+
close() {
|
|
2465
|
+
rl?.close();
|
|
2466
|
+
}
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
function normalizeInstallRuntime(value) {
|
|
2470
|
+
if (value === void 0 || value === null || value === "") {
|
|
2471
|
+
return void 0;
|
|
2472
|
+
}
|
|
2473
|
+
if (value === "codex" || value === "claude" || value === "all") {
|
|
2474
|
+
return value;
|
|
2475
|
+
}
|
|
2476
|
+
throw new Error("Invalid runtime. Use codex, claude, or all.");
|
|
2477
|
+
}
|
|
2478
|
+
function normalizeInstallScope(value) {
|
|
2479
|
+
if (value === void 0 || value === null || value === "") {
|
|
2480
|
+
return void 0;
|
|
2481
|
+
}
|
|
2482
|
+
if (value === "global") {
|
|
2483
|
+
return "global";
|
|
2484
|
+
}
|
|
2485
|
+
if (value === "local" || value === "repo") {
|
|
2486
|
+
return "repo";
|
|
2487
|
+
}
|
|
2488
|
+
throw new Error("Invalid --scope value. Use local or global.");
|
|
2489
|
+
}
|
|
2490
|
+
function renderInstallBanner(version) {
|
|
2491
|
+
const teal = (text) => `\x1B[38;2;34;173;169m${text}\x1B[0m`;
|
|
2492
|
+
return [
|
|
2493
|
+
teal(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557"),
|
|
2494
|
+
teal(" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D"),
|
|
2495
|
+
teal(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 "),
|
|
2496
|
+
teal(" \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 "),
|
|
2497
|
+
teal(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 "),
|
|
2498
|
+
teal(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D "),
|
|
2499
|
+
"",
|
|
2500
|
+
` sift v${version}`,
|
|
2501
|
+
" Small, sharp, and mildly sarcastic output guidance.",
|
|
2502
|
+
' "Loading the loading screen..." energy, minus the loading screen.'
|
|
2503
|
+
].join("\n");
|
|
2504
|
+
}
|
|
2505
|
+
function getInstallTargets(runtime) {
|
|
2506
|
+
if (runtime === "all") {
|
|
2507
|
+
return ["codex", "claude"];
|
|
2508
|
+
}
|
|
2509
|
+
return [runtime];
|
|
2510
|
+
}
|
|
2511
|
+
function getGlobalTargetLabel(agent, homeDir = os3.homedir()) {
|
|
2512
|
+
return agent === "codex" ? getDefaultCodexGlobalInstructionsPath(homeDir) : getDefaultClaudeGlobalInstructionsPath(homeDir);
|
|
2513
|
+
}
|
|
2514
|
+
function getLocalTargetLabel(agent, cwd = process.cwd()) {
|
|
2515
|
+
return agent === "codex" ? path7.join(cwd, "AGENTS.md") : path7.join(cwd, "CLAUDE.md");
|
|
2516
|
+
}
|
|
2517
|
+
function describeScopeChoice(args) {
|
|
2518
|
+
const targets = getInstallTargets(args.runtime);
|
|
2519
|
+
const labels = targets.map(
|
|
2520
|
+
(agent) => args.scope === "global" ? getGlobalTargetLabel(agent, args.homeDir) : getLocalTargetLabel(agent, args.cwd)
|
|
2521
|
+
);
|
|
2522
|
+
return labels.join(" + ");
|
|
2523
|
+
}
|
|
2524
|
+
async function promptWithMenu(args) {
|
|
2525
|
+
const defaultIndex = args.defaultIndex ?? 0;
|
|
2526
|
+
if (args.io.select) {
|
|
2527
|
+
const labels = args.choices.map((choice) => choice.label);
|
|
2528
|
+
const selected = await args.io.select(args.prompt, labels, args.selectedLabel, args.allowBack);
|
|
2529
|
+
if (selected === PROMPT_BACK) {
|
|
2530
|
+
return PROMPT_BACK;
|
|
2531
|
+
}
|
|
2532
|
+
const match = args.choices.find((choice) => choice.label === selected);
|
|
2533
|
+
if (match) {
|
|
2534
|
+
return match.value;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
args.io.write(`
|
|
2538
|
+
${args.prompt}
|
|
2539
|
+
|
|
2540
|
+
`);
|
|
2541
|
+
args.choices.forEach((choice, index) => {
|
|
2542
|
+
args.io.write(` ${index + 1}) ${choice.label}
|
|
2543
|
+
`);
|
|
2544
|
+
});
|
|
2545
|
+
if (args.allowBack) {
|
|
2546
|
+
args.io.write(` ${args.choices.length + 1}) Back
|
|
2547
|
+
`);
|
|
2548
|
+
}
|
|
2549
|
+
args.io.write("\n");
|
|
2550
|
+
while (true) {
|
|
2551
|
+
const answer = (await args.io.ask(`Choice [${defaultIndex + 1}]: `)).trim();
|
|
2552
|
+
if (args.allowBack && (answer.toLowerCase() === "back" || answer.toLowerCase() === "b")) {
|
|
2553
|
+
return PROMPT_BACK;
|
|
2554
|
+
}
|
|
2555
|
+
if (answer === "") {
|
|
2556
|
+
return args.choices[defaultIndex]?.value ?? args.choices[0].value;
|
|
2557
|
+
}
|
|
2558
|
+
const choiceIndex = Number(answer);
|
|
2559
|
+
if (Number.isInteger(choiceIndex) && choiceIndex >= 1 && choiceIndex <= args.choices.length) {
|
|
2560
|
+
return args.choices[choiceIndex - 1].value;
|
|
2561
|
+
}
|
|
2562
|
+
if (args.allowBack && Number.isInteger(choiceIndex) && choiceIndex === args.choices.length + 1) {
|
|
2563
|
+
return PROMPT_BACK;
|
|
2564
|
+
}
|
|
2565
|
+
const max = args.allowBack ? args.choices.length + 1 : args.choices.length;
|
|
2566
|
+
args.io.error(`Please enter a number between 1 and ${max}.
|
|
2567
|
+
`);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
async function promptForRuntime(io) {
|
|
2571
|
+
return await promptWithMenu({
|
|
2572
|
+
io,
|
|
2573
|
+
prompt: "Choose your runtime",
|
|
2574
|
+
selectedLabel: "Runtime",
|
|
2575
|
+
allowBack: true,
|
|
2576
|
+
choices: [
|
|
2577
|
+
{
|
|
2578
|
+
label: "Codex (AGENTS.md / ~/.codex/AGENTS.md) - first-class if you live in Codex",
|
|
2579
|
+
value: "codex"
|
|
2580
|
+
},
|
|
2581
|
+
{
|
|
2582
|
+
label: "Claude (CLAUDE.md / ~/.claude/CLAUDE.md) - same good manners, Claude-flavored",
|
|
2583
|
+
value: "claude"
|
|
2584
|
+
},
|
|
2585
|
+
{
|
|
2586
|
+
label: "All - if you refuse to pick favorites today",
|
|
2587
|
+
value: "all"
|
|
2588
|
+
}
|
|
2589
|
+
]
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
async function promptForScope(args) {
|
|
2593
|
+
return await promptWithMenu({
|
|
2594
|
+
io: args.io,
|
|
2595
|
+
prompt: "Choose where to install the runtime support",
|
|
2596
|
+
selectedLabel: "Location",
|
|
2597
|
+
allowBack: true,
|
|
2598
|
+
choices: [
|
|
2599
|
+
{
|
|
2600
|
+
label: `Global (${describeScopeChoice({
|
|
2601
|
+
runtime: args.runtime,
|
|
2602
|
+
scope: "global",
|
|
2603
|
+
cwd: args.cwd,
|
|
2604
|
+
homeDir: args.homeDir
|
|
2605
|
+
})}) - use this if you want sift ready everywhere`,
|
|
2606
|
+
value: "global"
|
|
2607
|
+
},
|
|
2608
|
+
{
|
|
2609
|
+
label: `Local (${describeScopeChoice({
|
|
2610
|
+
runtime: args.runtime,
|
|
2611
|
+
scope: "repo",
|
|
2612
|
+
cwd: args.cwd,
|
|
2613
|
+
homeDir: args.homeDir
|
|
2614
|
+
})}) - keep it here if this repo is the only one that matters`,
|
|
2615
|
+
value: "repo"
|
|
2616
|
+
}
|
|
2617
|
+
]
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
async function promptForOperationMode2(io) {
|
|
2621
|
+
return await promptWithMenu({
|
|
2622
|
+
io,
|
|
2623
|
+
prompt: "Choose how sift should work",
|
|
2624
|
+
selectedLabel: "Mode",
|
|
2625
|
+
allowBack: true,
|
|
2626
|
+
choices: [
|
|
2627
|
+
{
|
|
2628
|
+
label: "With an agent - recommended if Codex or Claude is already with you; sift does the fast local first pass, the agent only steps in when repo context is truly needed",
|
|
2629
|
+
value: "agent-escalation"
|
|
2630
|
+
},
|
|
2631
|
+
{
|
|
2632
|
+
label: "With provider fallback - recommended if you want sift to finish more ambiguous cases on its own before handing them back to you or your agent; needs an API key, cheap model only when needed",
|
|
2633
|
+
value: "provider-assisted"
|
|
2634
|
+
},
|
|
2635
|
+
{
|
|
2636
|
+
label: "Solo, local-only - recommended if you want zero model calls; great for supported presets, ambiguous cases stay with you",
|
|
2637
|
+
value: "local-only"
|
|
2638
|
+
}
|
|
2639
|
+
]
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
function createNestedInstallIO(parent) {
|
|
2643
|
+
return {
|
|
2644
|
+
stdinIsTTY: parent.stdinIsTTY,
|
|
2645
|
+
stdoutIsTTY: parent.stdoutIsTTY,
|
|
2646
|
+
ask: async () => "",
|
|
2647
|
+
write() {
|
|
2648
|
+
},
|
|
2649
|
+
error(message) {
|
|
2650
|
+
parent.error(message);
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
function writeSuccessSummary(args) {
|
|
2655
|
+
const ui = createPresentation(args.io.stdoutIsTTY);
|
|
2656
|
+
const targets = getInstallTargets(args.runtime);
|
|
2657
|
+
const scopeLabel = args.scope === "global" ? "global" : "local";
|
|
2658
|
+
const targetLabel = describeScopeChoice({
|
|
2659
|
+
runtime: args.runtime,
|
|
2660
|
+
scope: args.scope,
|
|
2661
|
+
cwd: args.cwd,
|
|
2662
|
+
homeDir: args.homeDir
|
|
2663
|
+
});
|
|
2664
|
+
if (args.io.stdoutIsTTY) {
|
|
2665
|
+
args.io.write(`
|
|
2666
|
+
${ui.success("Installed runtime support.")}
|
|
2667
|
+
`);
|
|
2668
|
+
} else {
|
|
2669
|
+
args.io.write("Installed runtime support.\n");
|
|
2670
|
+
}
|
|
2671
|
+
args.io.write(
|
|
2672
|
+
`${ui.note(`sift v${args.version} now manages ${targets.map((target) => INSTALL_TITLES[target]).join(" + ")} in ${scopeLabel} scope.`)}
|
|
2673
|
+
`
|
|
2674
|
+
);
|
|
2675
|
+
args.io.write(`${ui.note(`Operating mode: ${getOperationModeLabel(args.operationMode)}`)}
|
|
2676
|
+
`);
|
|
2677
|
+
args.io.write(`${ui.note(describeOperationMode(args.operationMode))}
|
|
2678
|
+
`);
|
|
2679
|
+
args.io.write(`${ui.note(targetLabel)}
|
|
2680
|
+
`);
|
|
2681
|
+
args.io.write(`
|
|
2682
|
+
${ui.section("Try next")}
|
|
2683
|
+
`);
|
|
2684
|
+
args.io.write(` ${ui.command("sift doctor")}
|
|
2685
|
+
`);
|
|
2686
|
+
if (args.operationMode === "provider-assisted") {
|
|
2687
|
+
args.io.write(` ${ui.command("sift config show --show-secrets")}
|
|
2688
|
+
`);
|
|
2689
|
+
} else {
|
|
2690
|
+
args.io.write(
|
|
2691
|
+
` ${ui.command("sift config setup")}${ui.note(" # optional if you want provider-assisted fallback later")}
|
|
2692
|
+
`
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2695
|
+
args.io.write(` ${ui.command("sift exec --preset test-status -- npm test")}
|
|
2696
|
+
`);
|
|
2697
|
+
}
|
|
2698
|
+
async function installRuntimeSupport(options) {
|
|
2699
|
+
const io = options.io ?? createInstallTerminalIO();
|
|
2700
|
+
const getPreviousEditableStep = (step) => {
|
|
2701
|
+
if (step === "runtime") {
|
|
2702
|
+
return void 0;
|
|
2703
|
+
}
|
|
2704
|
+
if (step === "mode") {
|
|
2705
|
+
return options.runtime ? void 0 : "runtime";
|
|
2706
|
+
}
|
|
2707
|
+
if (step === "scope") {
|
|
2708
|
+
if (!options.operationMode) {
|
|
2709
|
+
return "mode";
|
|
2710
|
+
}
|
|
2711
|
+
if (!options.runtime) {
|
|
2712
|
+
return "runtime";
|
|
2713
|
+
}
|
|
2714
|
+
return void 0;
|
|
2715
|
+
}
|
|
2716
|
+
if (step === "provider") {
|
|
2717
|
+
if (!options.scope) {
|
|
2718
|
+
return "scope";
|
|
2719
|
+
}
|
|
2720
|
+
if (!options.operationMode) {
|
|
2721
|
+
return "mode";
|
|
2722
|
+
}
|
|
2723
|
+
if (!options.runtime) {
|
|
2724
|
+
return "runtime";
|
|
2725
|
+
}
|
|
2726
|
+
return void 0;
|
|
2727
|
+
}
|
|
2728
|
+
return void 0;
|
|
2729
|
+
};
|
|
2730
|
+
try {
|
|
2731
|
+
if ((!io.stdinIsTTY || !io.stdoutIsTTY) && (!options.runtime || !options.scope || !options.yes)) {
|
|
2732
|
+
io.error(
|
|
2733
|
+
"sift install is interactive and requires a TTY. For non-interactive use `sift install codex --scope global --yes`.\n"
|
|
2734
|
+
);
|
|
2735
|
+
return 1;
|
|
2736
|
+
}
|
|
2737
|
+
if (io.stdoutIsTTY) {
|
|
2738
|
+
io.write(`${renderInstallBanner(options.version)}
|
|
2739
|
+
`);
|
|
2740
|
+
}
|
|
2741
|
+
let runtime = options.runtime;
|
|
2742
|
+
let operationMode = options.operationMode;
|
|
2743
|
+
let scope = options.scope;
|
|
2744
|
+
let step;
|
|
2745
|
+
if (!io.stdinIsTTY || !io.stdoutIsTTY) {
|
|
2746
|
+
runtime ??= options.runtime;
|
|
2747
|
+
operationMode ??= "agent-escalation";
|
|
2748
|
+
step = void 0;
|
|
2749
|
+
} else if (!runtime) {
|
|
2750
|
+
step = "runtime";
|
|
2751
|
+
} else if (!operationMode) {
|
|
2752
|
+
step = "mode";
|
|
2753
|
+
} else if (!scope) {
|
|
2754
|
+
step = "scope";
|
|
2755
|
+
} else if (operationMode === "provider-assisted") {
|
|
2756
|
+
step = "provider";
|
|
2757
|
+
}
|
|
2758
|
+
while (step) {
|
|
2759
|
+
if (step === "runtime") {
|
|
2760
|
+
const runtimeChoice = await promptForRuntime(io);
|
|
2761
|
+
if (runtimeChoice === PROMPT_BACK) {
|
|
2762
|
+
io.write(`
|
|
2763
|
+
${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
|
|
2764
|
+
`);
|
|
2765
|
+
return 0;
|
|
2766
|
+
}
|
|
2767
|
+
runtime = runtimeChoice;
|
|
2768
|
+
step = !operationMode ? "mode" : !scope ? "scope" : operationMode === "provider-assisted" ? "provider" : void 0;
|
|
2769
|
+
continue;
|
|
2770
|
+
}
|
|
2771
|
+
if (step === "mode") {
|
|
2772
|
+
const modeChoice = await promptForOperationMode2(io);
|
|
2773
|
+
if (modeChoice === PROMPT_BACK) {
|
|
2774
|
+
const previous = getPreviousEditableStep("mode");
|
|
2775
|
+
if (!previous) {
|
|
2776
|
+
io.write(`
|
|
2777
|
+
${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
|
|
2778
|
+
`);
|
|
2779
|
+
return 0;
|
|
2780
|
+
}
|
|
2781
|
+
step = previous;
|
|
2782
|
+
continue;
|
|
2783
|
+
}
|
|
2784
|
+
operationMode = modeChoice;
|
|
2785
|
+
step = !scope ? "scope" : operationMode === "provider-assisted" ? "provider" : void 0;
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
if (step === "scope") {
|
|
2789
|
+
const scopeChoice = await promptForScope({
|
|
2790
|
+
io,
|
|
2791
|
+
runtime,
|
|
2792
|
+
cwd: options.cwd,
|
|
2793
|
+
homeDir: options.homeDir
|
|
2794
|
+
});
|
|
2795
|
+
if (scopeChoice === PROMPT_BACK) {
|
|
2796
|
+
const previous = getPreviousEditableStep("scope");
|
|
2797
|
+
if (!previous) {
|
|
2798
|
+
io.write(`
|
|
2799
|
+
${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
|
|
2800
|
+
`);
|
|
2801
|
+
return 0;
|
|
2802
|
+
}
|
|
2803
|
+
step = previous;
|
|
2804
|
+
continue;
|
|
2805
|
+
}
|
|
2806
|
+
scope = scopeChoice;
|
|
2807
|
+
step = operationMode === "provider-assisted" ? "provider" : void 0;
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
if (scope === "repo") {
|
|
2811
|
+
io.write(
|
|
2812
|
+
`
|
|
2813
|
+
${createPresentation(io.stdoutIsTTY).note("Local only applies to the runtime instructions in this repo. Provider fallback config is still machine-wide so sift can reuse it anywhere.")}
|
|
2814
|
+
`
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
io.write(`
|
|
2818
|
+
${createPresentation(io.stdoutIsTTY).info("Next: provider setup. Press Esc at any step to go back.")}
|
|
2819
|
+
`);
|
|
2820
|
+
const setupStatus = await configSetup({
|
|
2821
|
+
io,
|
|
2822
|
+
env: process.env,
|
|
2823
|
+
embedded: true,
|
|
2824
|
+
forcedMode: "provider-assisted",
|
|
2825
|
+
targetPath: getDefaultGlobalConfigPath(options.homeDir)
|
|
2826
|
+
});
|
|
2827
|
+
if (setupStatus === CONFIG_SETUP_BACK) {
|
|
2828
|
+
const previous = getPreviousEditableStep("provider");
|
|
2829
|
+
if (!previous) {
|
|
2830
|
+
io.write(`
|
|
2831
|
+
${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
|
|
2832
|
+
`);
|
|
2833
|
+
return 0;
|
|
2834
|
+
}
|
|
2835
|
+
step = previous;
|
|
2836
|
+
continue;
|
|
2837
|
+
}
|
|
2838
|
+
if (setupStatus !== 0) {
|
|
2839
|
+
return setupStatus;
|
|
2840
|
+
}
|
|
2841
|
+
step = void 0;
|
|
2842
|
+
}
|
|
2843
|
+
const nestedIo = createNestedInstallIO(io);
|
|
2844
|
+
for (const agent of getInstallTargets(runtime)) {
|
|
2845
|
+
const status = await installAgent({
|
|
2846
|
+
agent,
|
|
2847
|
+
scope,
|
|
2848
|
+
yes: true,
|
|
2849
|
+
io: nestedIo,
|
|
2850
|
+
operationMode,
|
|
2851
|
+
cwd: options.cwd,
|
|
2852
|
+
homeDir: options.homeDir
|
|
2853
|
+
});
|
|
2854
|
+
if (status !== 0) {
|
|
2855
|
+
return status;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
writeSuccessSummary({
|
|
2859
|
+
io,
|
|
2860
|
+
version: options.version,
|
|
2861
|
+
runtime,
|
|
2862
|
+
scope,
|
|
2863
|
+
operationMode,
|
|
2864
|
+
cwd: options.cwd,
|
|
2865
|
+
homeDir: options.homeDir
|
|
2866
|
+
});
|
|
2867
|
+
return 0;
|
|
2868
|
+
} finally {
|
|
2869
|
+
io.close?.();
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
|
|
1816
2873
|
// src/commands/doctor.ts
|
|
1817
2874
|
var PLACEHOLDER_API_KEYS = [
|
|
1818
2875
|
"YOUR_API_KEY",
|
|
@@ -1836,16 +2893,21 @@ function isRealApiKey(key) {
|
|
|
1836
2893
|
}
|
|
1837
2894
|
function runDoctor(config, configPath) {
|
|
1838
2895
|
const ui = createPresentation(Boolean(process.stdout.isTTY));
|
|
2896
|
+
const effectiveMode = resolveEffectiveOperationMode(config);
|
|
1839
2897
|
const apiKeyStatus = isRealApiKey(config.provider.apiKey) ? "set" : isPlaceholderApiKey(config.provider.apiKey) ? "placeholder (not a real key)" : "not set";
|
|
1840
2898
|
const lines = [
|
|
1841
2899
|
"sift doctor",
|
|
1842
2900
|
"A quick check for your local setup.",
|
|
1843
|
-
"mode:
|
|
2901
|
+
"mode: operation-mode health check",
|
|
1844
2902
|
ui.labelValue("configPath", configPath ?? "(defaults only)"),
|
|
2903
|
+
ui.labelValue("configuredMode", getOperationModeLabel(config.runtime.operationMode)),
|
|
2904
|
+
ui.labelValue("effectiveMode", getOperationModeLabel(effectiveMode)),
|
|
1845
2905
|
ui.labelValue("provider", config.provider.provider),
|
|
1846
2906
|
ui.labelValue("model", config.provider.model),
|
|
1847
2907
|
ui.labelValue("baseUrl", config.provider.baseUrl),
|
|
1848
2908
|
ui.labelValue("apiKey", apiKeyStatus),
|
|
2909
|
+
ui.labelValue("modeSummary", describeOperationMode(effectiveMode)),
|
|
2910
|
+
ui.labelValue("insufficientBehavior", describeInsufficientBehavior(effectiveMode)),
|
|
1849
2911
|
ui.labelValue("maxCaptureChars", String(config.input.maxCaptureChars)),
|
|
1850
2912
|
ui.labelValue("maxInputChars", String(config.input.maxInputChars)),
|
|
1851
2913
|
ui.labelValue("rawFallback", String(config.runtime.rawFallback))
|
|
@@ -1853,24 +2915,31 @@ function runDoctor(config, configPath) {
|
|
|
1853
2915
|
process.stdout.write(`${lines.join("\n")}
|
|
1854
2916
|
`);
|
|
1855
2917
|
const problems = [];
|
|
1856
|
-
if (
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
if (
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
2918
|
+
if (config.runtime.operationMode === "provider-assisted") {
|
|
2919
|
+
if (!config.provider.baseUrl) {
|
|
2920
|
+
problems.push("Missing provider.baseUrl");
|
|
2921
|
+
}
|
|
2922
|
+
if (!config.provider.model) {
|
|
2923
|
+
problems.push("Missing provider.model");
|
|
2924
|
+
}
|
|
2925
|
+
if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible" || config.provider.provider === "openrouter") && !isRealApiKey(config.provider.apiKey)) {
|
|
2926
|
+
if (isPlaceholderApiKey(config.provider.apiKey)) {
|
|
2927
|
+
problems.push(`provider.apiKey looks like a placeholder: "${config.provider.apiKey}"`);
|
|
2928
|
+
} else {
|
|
2929
|
+
problems.push("Missing provider.apiKey");
|
|
2930
|
+
}
|
|
2931
|
+
problems.push(
|
|
2932
|
+
`Set one of: ${getProviderApiKeyEnvNames(
|
|
2933
|
+
config.provider.provider,
|
|
2934
|
+
config.provider.baseUrl
|
|
2935
|
+
).join(", ")}`
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
if (effectiveMode !== "provider-assisted") {
|
|
2939
|
+
problems.push(
|
|
2940
|
+
"Configured provider-assisted mode cannot activate yet, so sift will fall back to agent-escalation until provider credentials are usable."
|
|
2941
|
+
);
|
|
1867
2942
|
}
|
|
1868
|
-
problems.push(
|
|
1869
|
-
`Set one of: ${getProviderApiKeyEnvNames(
|
|
1870
|
-
config.provider.provider,
|
|
1871
|
-
config.provider.baseUrl
|
|
1872
|
-
).join(", ")}`
|
|
1873
|
-
);
|
|
1874
2943
|
}
|
|
1875
2944
|
if (problems.length > 0) {
|
|
1876
2945
|
if (process.stderr.isTTY) {
|
|
@@ -1983,6 +3052,7 @@ var OpenAIProvider = class {
|
|
|
1983
3052
|
signal: controller.signal,
|
|
1984
3053
|
headers: {
|
|
1985
3054
|
"content-type": "application/json",
|
|
3055
|
+
connection: "close",
|
|
1986
3056
|
...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
|
|
1987
3057
|
},
|
|
1988
3058
|
body: JSON.stringify({
|
|
@@ -2083,6 +3153,7 @@ var OpenAICompatibleProvider = class {
|
|
|
2083
3153
|
signal: controller.signal,
|
|
2084
3154
|
headers: {
|
|
2085
3155
|
"content-type": "application/json",
|
|
3156
|
+
connection: "close",
|
|
2086
3157
|
...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
|
|
2087
3158
|
},
|
|
2088
3159
|
body: JSON.stringify({
|
|
@@ -2725,16 +3796,16 @@ function extractBucketPathCandidates(args) {
|
|
|
2725
3796
|
}
|
|
2726
3797
|
return [...candidates];
|
|
2727
3798
|
}
|
|
2728
|
-
function isConfigPathCandidate(
|
|
2729
|
-
return /^\.github\/workflows\/.+\.(?:yml|yaml)$/i.test(
|
|
2730
|
-
|
|
2731
|
-
) || /^(?:[A-Za-z0-9._/-]+\/)?(?:vitest|jest)\.config\.[A-Za-z0-9._-]+$/i.test(
|
|
3799
|
+
function isConfigPathCandidate(path9) {
|
|
3800
|
+
return /^\.github\/workflows\/.+\.(?:yml|yaml)$/i.test(path9) || /^(?:package\.json|pytest\.ini|pyproject\.toml|tox\.ini|(?:[A-Za-z0-9._/-]+\/)?conftest\.py)$/i.test(
|
|
3801
|
+
path9
|
|
3802
|
+
) || /^(?:[A-Za-z0-9._/-]+\/)?(?:vitest|jest)\.config\.[A-Za-z0-9._-]+$/i.test(path9) || /^(?:[A-Za-z0-9._/-]+\/)?tsconfig(?:\.[A-Za-z0-9_-]+)?\.json$/i.test(path9) || /^[A-Za-z0-9._/-]*config[A-Za-z0-9._/-]*\.(?:json|yml|yaml)$/i.test(path9);
|
|
2732
3803
|
}
|
|
2733
|
-
function isAppPathCandidate(
|
|
2734
|
-
return
|
|
3804
|
+
function isAppPathCandidate(path9) {
|
|
3805
|
+
return path9.startsWith("src/");
|
|
2735
3806
|
}
|
|
2736
|
-
function isTestPathCandidate(
|
|
2737
|
-
return
|
|
3807
|
+
function isTestPathCandidate(path9) {
|
|
3808
|
+
return path9.startsWith("test/") || path9.startsWith("tests/");
|
|
2738
3809
|
}
|
|
2739
3810
|
function looksLikeMatcherLiteralComparison(detail) {
|
|
2740
3811
|
return /\bexpected\b[\s\S]*\bto (?:be|contain)\b/i.test(detail);
|
|
@@ -3297,13 +4368,13 @@ function buildExtendedBucketSearchHint(bucket, anchor) {
|
|
|
3297
4368
|
return detail.replace(/^of\s+/i, "") || anchor.label;
|
|
3298
4369
|
}
|
|
3299
4370
|
if (extended.type === "file_not_found_failure") {
|
|
3300
|
-
const
|
|
3301
|
-
return
|
|
4371
|
+
const path9 = detail.match(/['"]([^'"]+)['"]/)?.[1];
|
|
4372
|
+
return path9 ?? detail;
|
|
3302
4373
|
}
|
|
3303
4374
|
if (extended.type === "permission_denied_failure") {
|
|
3304
|
-
const
|
|
4375
|
+
const path9 = detail.match(/['"]([^'"]+)['"]/)?.[1];
|
|
3305
4376
|
const port = detail.match(/\bport\s+(\d+)\b/i)?.[1];
|
|
3306
|
-
return
|
|
4377
|
+
return path9 ?? (port ? `port ${port}` : detail);
|
|
3307
4378
|
}
|
|
3308
4379
|
return detail;
|
|
3309
4380
|
}
|
|
@@ -4411,7 +5482,7 @@ function buildPrompt(args) {
|
|
|
4411
5482
|
"If per-test or per-module mapping is unclear, fall back to the focused grouped-cause view instead of guessing."
|
|
4412
5483
|
] : [];
|
|
4413
5484
|
const prompt = [
|
|
4414
|
-
"You are Sift, a CLI output reduction assistant for downstream agents and automation.",
|
|
5485
|
+
"You are Sift, a CLI output-guidance and reduction assistant for downstream agents and automation.",
|
|
4415
5486
|
"Hard rules:",
|
|
4416
5487
|
...policy.sharedRules.map((rule) => `- ${rule}`),
|
|
4417
5488
|
"",
|
|
@@ -6189,7 +7260,7 @@ function extractContractDriftEntities(input) {
|
|
|
6189
7260
|
}
|
|
6190
7261
|
function buildContractRepresentativeReason(args) {
|
|
6191
7262
|
if (/openapi/i.test(args.label) && args.entities.apiPaths.length > 0) {
|
|
6192
|
-
const nextPath = args.entities.apiPaths.find((
|
|
7263
|
+
const nextPath = args.entities.apiPaths.find((path9) => !args.usedPaths.has(path9)) ?? args.entities.apiPaths[0];
|
|
6193
7264
|
args.usedPaths.add(nextPath);
|
|
6194
7265
|
return `added path: ${nextPath}`;
|
|
6195
7266
|
}
|
|
@@ -8265,8 +9336,8 @@ function emitStatsFooter(args) {
|
|
|
8265
9336
|
}
|
|
8266
9337
|
|
|
8267
9338
|
// src/core/testStatusState.ts
|
|
8268
|
-
import
|
|
8269
|
-
import
|
|
9339
|
+
import fs6 from "fs";
|
|
9340
|
+
import path8 from "path";
|
|
8270
9341
|
import { z as z3 } from "zod";
|
|
8271
9342
|
var detailSchema = z3.enum(["standard", "focused", "verbose"]);
|
|
8272
9343
|
var failureBucketTypeSchema = z3.enum([
|
|
@@ -8428,7 +9499,7 @@ function buildBucketSignature(bucket) {
|
|
|
8428
9499
|
]);
|
|
8429
9500
|
}
|
|
8430
9501
|
function basenameMatches(value, matcher) {
|
|
8431
|
-
return matcher.test(
|
|
9502
|
+
return matcher.test(path8.basename(value));
|
|
8432
9503
|
}
|
|
8433
9504
|
function isPytestExecutable(value) {
|
|
8434
9505
|
return basenameMatches(value, /^pytest(?:\.exe)?$/i);
|
|
@@ -8587,7 +9658,7 @@ function buildCachedRunnerState(args) {
|
|
|
8587
9658
|
};
|
|
8588
9659
|
}
|
|
8589
9660
|
function normalizeCwd(value) {
|
|
8590
|
-
return
|
|
9661
|
+
return path8.resolve(value).replace(/\\/g, "/");
|
|
8591
9662
|
}
|
|
8592
9663
|
function buildTestStatusBaselineIdentity(args) {
|
|
8593
9664
|
const cwd = normalizeCwd(args.cwd);
|
|
@@ -8715,7 +9786,7 @@ function migrateCachedTestStatusRun(state) {
|
|
|
8715
9786
|
function readCachedTestStatusRun(statePath = getDefaultTestStatusStatePath()) {
|
|
8716
9787
|
let raw = "";
|
|
8717
9788
|
try {
|
|
8718
|
-
raw =
|
|
9789
|
+
raw = fs6.readFileSync(statePath, "utf8");
|
|
8719
9790
|
} catch (error) {
|
|
8720
9791
|
if (error.code === "ENOENT") {
|
|
8721
9792
|
throw new MissingCachedTestStatusRunError();
|
|
@@ -8736,10 +9807,10 @@ function tryReadCachedTestStatusRun(statePath = getDefaultTestStatusStatePath())
|
|
|
8736
9807
|
}
|
|
8737
9808
|
}
|
|
8738
9809
|
function writeCachedTestStatusRun(state, statePath = getDefaultTestStatusStatePath()) {
|
|
8739
|
-
|
|
9810
|
+
fs6.mkdirSync(path8.dirname(statePath), {
|
|
8740
9811
|
recursive: true
|
|
8741
9812
|
});
|
|
8742
|
-
|
|
9813
|
+
fs6.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}
|
|
8743
9814
|
`, "utf8");
|
|
8744
9815
|
}
|
|
8745
9816
|
function getNextEscalationDetail(detail) {
|
|
@@ -8901,7 +9972,8 @@ function resolveEscalationDetail(state, requested, showRaw = false) {
|
|
|
8901
9972
|
return nextDetail;
|
|
8902
9973
|
}
|
|
8903
9974
|
async function runEscalate(request) {
|
|
8904
|
-
const
|
|
9975
|
+
const scopedStatePath = getScopedTestStatusStatePath(process.cwd());
|
|
9976
|
+
const state = readCachedTestStatusRun(scopedStatePath);
|
|
8905
9977
|
const detail = resolveEscalationDetail(state, request.detail, request.showRaw);
|
|
8906
9978
|
if (request.verbose) {
|
|
8907
9979
|
process.stderr.write(
|
|
@@ -8949,10 +10021,13 @@ async function runEscalate(request) {
|
|
|
8949
10021
|
quiet: Boolean(request.quiet)
|
|
8950
10022
|
});
|
|
8951
10023
|
try {
|
|
8952
|
-
writeCachedTestStatusRun(
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
10024
|
+
writeCachedTestStatusRun(
|
|
10025
|
+
{
|
|
10026
|
+
...state,
|
|
10027
|
+
detail
|
|
10028
|
+
},
|
|
10029
|
+
scopedStatePath
|
|
10030
|
+
);
|
|
8956
10031
|
} catch (error) {
|
|
8957
10032
|
if (request.verbose) {
|
|
8958
10033
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
@@ -9275,13 +10350,97 @@ function buildCommandPreview(request) {
|
|
|
9275
10350
|
}
|
|
9276
10351
|
return (request.command ?? []).join(" ");
|
|
9277
10352
|
}
|
|
10353
|
+
function detectPackageManagerScriptKind(commandPreview) {
|
|
10354
|
+
const trimmed = commandPreview.trim();
|
|
10355
|
+
if (/^npm(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
|
|
10356
|
+
return "npm";
|
|
10357
|
+
}
|
|
10358
|
+
if (/^pnpm(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
|
|
10359
|
+
return "pnpm";
|
|
10360
|
+
}
|
|
10361
|
+
if (/^yarn(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
|
|
10362
|
+
return "yarn";
|
|
10363
|
+
}
|
|
10364
|
+
if (/^bun(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
|
|
10365
|
+
return "bun";
|
|
10366
|
+
}
|
|
10367
|
+
return null;
|
|
10368
|
+
}
|
|
10369
|
+
function normalizeScriptWrapperOutput(args) {
|
|
10370
|
+
const kind = detectPackageManagerScriptKind(args.commandPreview);
|
|
10371
|
+
if (!kind) {
|
|
10372
|
+
return args.capturedOutput;
|
|
10373
|
+
}
|
|
10374
|
+
const lines = args.capturedOutput.split(/\r?\n/);
|
|
10375
|
+
const trimBlankEdges = () => {
|
|
10376
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
10377
|
+
lines.shift();
|
|
10378
|
+
}
|
|
10379
|
+
while (lines.length > 0 && lines.at(-1).trim() === "") {
|
|
10380
|
+
lines.pop();
|
|
10381
|
+
}
|
|
10382
|
+
};
|
|
10383
|
+
const stripLeadingWrapperNoise = () => {
|
|
10384
|
+
let removed = false;
|
|
10385
|
+
while (lines.length > 0) {
|
|
10386
|
+
const line = lines[0];
|
|
10387
|
+
const trimmed = line.trim();
|
|
10388
|
+
if (trimmed === "") {
|
|
10389
|
+
lines.shift();
|
|
10390
|
+
removed = true;
|
|
10391
|
+
continue;
|
|
10392
|
+
}
|
|
10393
|
+
if (/^(?:npm|pnpm)\s+warn\s+unknown user config\b/i.test(trimmed) || /^(?:npm|pnpm)\s+warn\s+unknown env config\b/i.test(trimmed) || /^npm\s+warn\s+config\b/i.test(trimmed) || /^yarn\s+warning\b/i.test(trimmed) || /^bun\s+warn\b/i.test(trimmed)) {
|
|
10394
|
+
lines.shift();
|
|
10395
|
+
removed = true;
|
|
10396
|
+
continue;
|
|
10397
|
+
}
|
|
10398
|
+
break;
|
|
10399
|
+
}
|
|
10400
|
+
if (removed) {
|
|
10401
|
+
trimBlankEdges();
|
|
10402
|
+
}
|
|
10403
|
+
};
|
|
10404
|
+
trimBlankEdges();
|
|
10405
|
+
stripLeadingWrapperNoise();
|
|
10406
|
+
if (kind === "npm" || kind === "pnpm") {
|
|
10407
|
+
let removed = 0;
|
|
10408
|
+
while (lines.length > 0 && removed < 2 && /^\s*>\s+/.test(lines[0])) {
|
|
10409
|
+
lines.shift();
|
|
10410
|
+
removed += 1;
|
|
10411
|
+
}
|
|
10412
|
+
trimBlankEdges();
|
|
10413
|
+
}
|
|
10414
|
+
if (kind === "yarn") {
|
|
10415
|
+
if (lines[0] && /^\s*yarn run v/i.test(lines[0])) {
|
|
10416
|
+
lines.shift();
|
|
10417
|
+
}
|
|
10418
|
+
if (lines[0] && /^\s*\$\s+/.test(lines[0])) {
|
|
10419
|
+
lines.shift();
|
|
10420
|
+
}
|
|
10421
|
+
trimBlankEdges();
|
|
10422
|
+
if (lines.at(-1) && /^\s*Done in\b/i.test(lines.at(-1))) {
|
|
10423
|
+
lines.pop();
|
|
10424
|
+
}
|
|
10425
|
+
}
|
|
10426
|
+
if (kind === "bun") {
|
|
10427
|
+
if (lines[0] && /^\s*\$\s+/.test(lines[0])) {
|
|
10428
|
+
lines.shift();
|
|
10429
|
+
}
|
|
10430
|
+
}
|
|
10431
|
+
trimBlankEdges();
|
|
10432
|
+
return lines.join("\n");
|
|
10433
|
+
}
|
|
9278
10434
|
function getExecSuccessShortcut(args) {
|
|
9279
10435
|
if (args.exitCode !== 0) {
|
|
9280
10436
|
return null;
|
|
9281
10437
|
}
|
|
9282
|
-
if (args.presetName === "typecheck-summary" && args.
|
|
10438
|
+
if (args.presetName === "typecheck-summary" && args.normalizedOutput.trim() === "") {
|
|
9283
10439
|
return "No type errors.";
|
|
9284
10440
|
}
|
|
10441
|
+
if (args.presetName === "lint-failures" && args.normalizedOutput.trim() === "") {
|
|
10442
|
+
return "No lint failures.";
|
|
10443
|
+
}
|
|
9285
10444
|
return null;
|
|
9286
10445
|
}
|
|
9287
10446
|
async function runExec(request) {
|
|
@@ -9293,10 +10452,11 @@ async function runExec(request) {
|
|
|
9293
10452
|
const shellPath = process.env.SHELL || "/bin/bash";
|
|
9294
10453
|
const commandPreview = buildCommandPreview(request);
|
|
9295
10454
|
const commandCwd = request.cwd ?? process.cwd();
|
|
10455
|
+
const scopedStatePath = getScopedTestStatusStatePath(commandCwd);
|
|
9296
10456
|
const isTestStatusPreset = request.presetName === "test-status";
|
|
9297
10457
|
const readCachedBaseline = isTestStatusPreset && (request.readCachedBaseline ?? true);
|
|
9298
10458
|
const writeCachedBaselineRequested = isTestStatusPreset && (request.writeCachedBaseline ?? (request.skipCacheWrite ? false : true));
|
|
9299
|
-
const previousCachedRun = readCachedBaseline ? tryReadCachedTestStatusRun() : null;
|
|
10459
|
+
const previousCachedRun = readCachedBaseline ? tryReadCachedTestStatusRun(scopedStatePath) : null;
|
|
9300
10460
|
if (request.config.runtime.verbose) {
|
|
9301
10461
|
process.stderr.write(
|
|
9302
10462
|
`${pc5.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
|
|
@@ -9360,6 +10520,10 @@ async function runExec(request) {
|
|
|
9360
10520
|
}
|
|
9361
10521
|
const exitCode = normalizeChildExitCode(childStatus, childSignal);
|
|
9362
10522
|
const capturedOutput = capture.render();
|
|
10523
|
+
const normalizedOutput = normalizeScriptWrapperOutput({
|
|
10524
|
+
commandPreview,
|
|
10525
|
+
capturedOutput
|
|
10526
|
+
});
|
|
9363
10527
|
const autoWatchDetected = !request.watch && looksLikeWatchStream(capturedOutput);
|
|
9364
10528
|
const useWatchFlow = Boolean(request.watch) || autoWatchDetected;
|
|
9365
10529
|
const shouldBuildTestStatusState = isTestStatusPreset && !useWatchFlow;
|
|
@@ -9385,7 +10549,7 @@ async function runExec(request) {
|
|
|
9385
10549
|
const execSuccessShortcut = useWatchFlow ? null : getExecSuccessShortcut({
|
|
9386
10550
|
presetName: request.presetName,
|
|
9387
10551
|
exitCode,
|
|
9388
|
-
|
|
10552
|
+
normalizedOutput
|
|
9389
10553
|
});
|
|
9390
10554
|
if (execSuccessShortcut && !request.dryRun) {
|
|
9391
10555
|
if (request.config.runtime.verbose) {
|
|
@@ -9411,15 +10575,15 @@ async function runExec(request) {
|
|
|
9411
10575
|
if (useWatchFlow) {
|
|
9412
10576
|
let output2 = await runWatch({
|
|
9413
10577
|
...request,
|
|
9414
|
-
stdin:
|
|
10578
|
+
stdin: normalizedOutput
|
|
9415
10579
|
});
|
|
9416
10580
|
if (isInsufficientSignalOutput(output2)) {
|
|
9417
10581
|
output2 = buildInsufficientSignalOutput({
|
|
9418
10582
|
presetName: request.presetName,
|
|
9419
|
-
originalLength: capture.getTotalChars(),
|
|
10583
|
+
originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
|
|
9420
10584
|
truncatedApplied: capture.wasTruncated(),
|
|
9421
10585
|
exitCode,
|
|
9422
|
-
recognizedRunner: detectTestRunner(
|
|
10586
|
+
recognizedRunner: detectTestRunner(normalizedOutput)
|
|
9423
10587
|
});
|
|
9424
10588
|
}
|
|
9425
10589
|
process.stdout.write(`${output2}
|
|
@@ -9458,7 +10622,7 @@ async function runExec(request) {
|
|
|
9458
10622
|
}) : null;
|
|
9459
10623
|
const result = await runSiftWithStats({
|
|
9460
10624
|
...request,
|
|
9461
|
-
stdin:
|
|
10625
|
+
stdin: normalizedOutput,
|
|
9462
10626
|
analysisContext: request.testStatusContext?.remainingMode && request.testStatusContext.remainingMode !== "none" && request.presetName === "test-status" ? [
|
|
9463
10627
|
request.analysisContext,
|
|
9464
10628
|
"Zoom context:",
|
|
@@ -9481,10 +10645,10 @@ async function runExec(request) {
|
|
|
9481
10645
|
if (isInsufficientSignalOutput(output)) {
|
|
9482
10646
|
output = buildInsufficientSignalOutput({
|
|
9483
10647
|
presetName: request.presetName,
|
|
9484
|
-
originalLength: capture.getTotalChars(),
|
|
10648
|
+
originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
|
|
9485
10649
|
truncatedApplied: capture.wasTruncated(),
|
|
9486
10650
|
exitCode,
|
|
9487
|
-
recognizedRunner: detectTestRunner(
|
|
10651
|
+
recognizedRunner: detectTestRunner(normalizedOutput)
|
|
9488
10652
|
});
|
|
9489
10653
|
}
|
|
9490
10654
|
if (request.diff && !request.dryRun && previousCachedRun && currentCachedRun) {
|
|
@@ -9499,7 +10663,7 @@ ${output}`;
|
|
|
9499
10663
|
}
|
|
9500
10664
|
if (currentCachedRun && shouldWriteCachedBaseline) {
|
|
9501
10665
|
try {
|
|
9502
|
-
writeCachedTestStatusRun(currentCachedRun);
|
|
10666
|
+
writeCachedTestStatusRun(currentCachedRun, scopedStatePath);
|
|
9503
10667
|
} catch (error) {
|
|
9504
10668
|
if (request.config.runtime.verbose) {
|
|
9505
10669
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
@@ -9511,10 +10675,10 @@ ${output}`;
|
|
|
9511
10675
|
} else if (isInsufficientSignalOutput(output)) {
|
|
9512
10676
|
output = buildInsufficientSignalOutput({
|
|
9513
10677
|
presetName: request.presetName,
|
|
9514
|
-
originalLength: capture.getTotalChars(),
|
|
10678
|
+
originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
|
|
9515
10679
|
truncatedApplied: capture.wasTruncated(),
|
|
9516
10680
|
exitCode,
|
|
9517
|
-
recognizedRunner: detectTestRunner(
|
|
10681
|
+
recognizedRunner: detectTestRunner(normalizedOutput)
|
|
9518
10682
|
});
|
|
9519
10683
|
}
|
|
9520
10684
|
process.stdout.write(`${output}
|
|
@@ -9535,7 +10699,7 @@ ${output}`;
|
|
|
9535
10699
|
|
|
9536
10700
|
// src/core/rerun.ts
|
|
9537
10701
|
async function runRerun(request) {
|
|
9538
|
-
const state = readCachedTestStatusRun();
|
|
10702
|
+
const state = readCachedTestStatusRun(getScopedTestStatusStatePath(process.cwd()));
|
|
9539
10703
|
if (!request.remaining) {
|
|
9540
10704
|
return runExec({
|
|
9541
10705
|
...request,
|
|
@@ -9625,6 +10789,7 @@ function getPreset(config, name) {
|
|
|
9625
10789
|
var require2 = createRequire(import.meta.url);
|
|
9626
10790
|
var pkg = require2("../package.json");
|
|
9627
10791
|
var defaultCliDeps = {
|
|
10792
|
+
installRuntimeSupport,
|
|
9628
10793
|
installAgent,
|
|
9629
10794
|
removeAgent,
|
|
9630
10795
|
showAgent,
|
|
@@ -9979,7 +11144,7 @@ function createCliApp(args = {}) {
|
|
|
9979
11144
|
});
|
|
9980
11145
|
});
|
|
9981
11146
|
applySharedOptions(
|
|
9982
|
-
cli.command("exec [question]", "Run a command and
|
|
11147
|
+
cli.command("exec [question]", "Run a command and turn noisy output into a smaller first pass for the model").allowUnknownOptions()
|
|
9983
11148
|
).usage("exec [question] [options] -- <program> [args...]").example('exec "what changed?" -- git diff').example("exec --preset test-status -- npm test").example("exec --preset test-status --diff -- npm test").example('exec --watch "summarize the stream" -- node watcher.js').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").option("--watch", "Treat the command output as a watch/change-summary stream").option("--diff", "Prepend material changes versus the previous matching test-status run").action(async (question, options) => {
|
|
9984
11149
|
if (question === "preset") {
|
|
9985
11150
|
throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
|
|
@@ -10258,6 +11423,14 @@ function createCliApp(args = {}) {
|
|
|
10258
11423
|
stdout.write(`${output}
|
|
10259
11424
|
`);
|
|
10260
11425
|
});
|
|
11426
|
+
cli.command("install [runtime]", "Interactive runtime installer for Codex and Claude").usage("install [runtime] [options]").example("install").example("install codex").example("install codex --scope global --yes").example("install all --scope local --yes").option("--scope <scope>", "Install scope: local | global").option("--yes", "Skip prompts when runtime and scope are already provided").action(async (runtime, options) => {
|
|
11427
|
+
process.exitCode = await deps.installRuntimeSupport({
|
|
11428
|
+
runtime: normalizeInstallRuntime(runtime),
|
|
11429
|
+
scope: normalizeInstallScope(options.scope),
|
|
11430
|
+
yes: Boolean(options.yes),
|
|
11431
|
+
version
|
|
11432
|
+
});
|
|
11433
|
+
});
|
|
10261
11434
|
cli.command("agent <action> [name]", "Agent commands: show | install | remove | status").usage("agent <show|install|remove|status> [name] [options]").example("agent show codex").example("agent show codex --raw").example("agent install codex").example("agent install claude --scope global").example("agent install codex --dry-run").example("agent install codex --dry-run --raw").example("agent status").example("agent remove codex --scope repo").option("--scope <scope>", "Install scope: repo | global").option("--dry-run", "Show a short plan without changing files").option("--raw", "Print the exact managed block or dry-run file content").option("--yes", "Skip confirmation prompts when writing").option("--path <path>", "Explicit target path for install or remove").action(async (action, name, options) => {
|
|
10262
11435
|
const scope = normalizeAgentScope(options.scope);
|
|
10263
11436
|
if (action === "show") {
|
|
@@ -10401,14 +11574,14 @@ function createCliApp(args = {}) {
|
|
|
10401
11574
|
{
|
|
10402
11575
|
title: ui.section("Quick start"),
|
|
10403
11576
|
body: [
|
|
10404
|
-
` ${ui.command("sift
|
|
11577
|
+
` ${ui.command("sift install")}${ui.note(" # choose agent-escalation, provider-assisted, or local-only")}`,
|
|
10405
11578
|
` ${ui.command("sift exec --preset test-status -- npm test")}`,
|
|
10406
11579
|
` ${ui.command("sift exec --preset test-status -- npm test")}${ui.note(" # stop here if standard already shows the main buckets")}`,
|
|
10407
11580
|
` ${ui.command("sift rerun")}${ui.note(" # rerun the cached full suite after a fix")}`,
|
|
10408
11581
|
` ${ui.command("sift rerun --remaining --detail focused")}${ui.note(" # zoom into what is still failing")}`,
|
|
10409
11582
|
` ${ui.command("sift rerun --remaining --detail verbose --show-raw")}`,
|
|
10410
|
-
` ${ui.command(
|
|
10411
|
-
` ${ui.command(
|
|
11583
|
+
` ${ui.command("sift config setup")}${ui.note(" # optional if you want provider-assisted fallback")}`,
|
|
11584
|
+
` ${ui.command("sift install codex --scope global --yes")}`,
|
|
10412
11585
|
` ${ui.command("sift agent install codex --dry-run")}`,
|
|
10413
11586
|
` ${ui.command("sift agent install codex --dry-run --raw")}`,
|
|
10414
11587
|
` ${ui.command("sift agent status")}`,
|