@bilalimamoglu/sift 0.4.2 → 0.4.4
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 +202 -75
- package/dist/cli.js +97 -36
- package/dist/index.js +35 -10
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,22 +1,45 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="assets/brand/sift-logo-minimal-teal-default.svg" alt="sift logo" width="220" />
|
|
4
|
+
|
|
1
5
|
# sift
|
|
2
6
|
|
|
7
|
+
### Turn noisy command output into actionable diagnoses for your coding agent
|
|
8
|
+
|
|
9
|
+
**Benchmark-backed test triage - Heuristic-first reductions - Agent-ready terminal workflows**
|
|
10
|
+
|
|
3
11
|
[](https://www.npmjs.com/package/@bilalimamoglu/sift)
|
|
4
12
|
[](LICENSE)
|
|
5
13
|
[](https://github.com/bilalimamoglu/sift/actions/workflows/ci.yml)
|
|
14
|
+
[](https://nodejs.org/)
|
|
6
15
|
|
|
7
|
-
|
|
16
|
+
<br />
|
|
17
|
+
|
|
18
|
+
### Get Started
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @bilalimamoglu/sift
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
<sub>Works with pytest, vitest, jest, tsc, ESLint, webpack, Cargo, terraform, npm audit, and more.</sub>
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
---
|
|
8
29
|
|
|
9
|
-
|
|
30
|
+
## Why Sift?
|
|
31
|
+
|
|
32
|
+
When an agent hits noisy output, it burns budget reading logs instead of fixing the problem.
|
|
33
|
+
|
|
34
|
+
`sift` sits in front of that output and reduces it into a small, actionable first pass. Your agent reads the diagnosis, not the wall of text.
|
|
35
|
+
|
|
36
|
+
Turn 13,000 lines of test output into 2 root causes.
|
|
10
37
|
|
|
11
38
|
<p align="center">
|
|
12
39
|
<img src="assets/readme/test-status-demo.gif" alt="sift turning a pytest failure wall into a short diagnosis" width="960" />
|
|
13
40
|
</p>
|
|
14
41
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
128 test failures. 13,000 lines of logs. The agent reads all of it.
|
|
18
|
-
|
|
19
|
-
With `sift`, it reads this instead:
|
|
42
|
+
With `sift`, the same run becomes:
|
|
20
43
|
|
|
21
44
|
```text
|
|
22
45
|
- Tests did not pass.
|
|
@@ -30,20 +53,118 @@ With `sift`, it reads this instead:
|
|
|
30
53
|
- Decision: stop and act.
|
|
31
54
|
```
|
|
32
55
|
|
|
33
|
-
|
|
56
|
+
In the largest benchmark fixture, sift compressed 198,026 raw output tokens to 129. That is what the agent reads instead of the full log.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Benchmark Results
|
|
61
|
+
|
|
62
|
+
The output reduction above measures a single command's raw output. The table below measures the full end-to-end debug session: how many tokens, tool calls, and seconds the agent spends to reach the same diagnosis.
|
|
63
|
+
|
|
64
|
+
Real debug loop on a 640-test Python backend with 124 repeated setup errors, 3 contract failures, and 511 passing tests:
|
|
65
|
+
|
|
66
|
+
| Metric | Without sift | With sift | Reduction |
|
|
67
|
+
|--------|-------------:|----------:|----------:|
|
|
68
|
+
| Tokens | 52,944 | 20,049 | 62% fewer |
|
|
69
|
+
| Tool calls | 40.8 | 12 | 71% fewer |
|
|
70
|
+
| Wall-clock time | 244s | 85s | 65% faster |
|
|
71
|
+
| Commands | 15.5 | 6 | 61% fewer |
|
|
72
|
+
| Diagnosis | Same | Same | Same outcome |
|
|
73
|
+
|
|
74
|
+
Same diagnosis, less agent thrash.
|
|
75
|
+
|
|
76
|
+
Methodology and caveats: [BENCHMARK_NOTES.md](BENCHMARK_NOTES.md)
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## How It Works
|
|
81
|
+
|
|
82
|
+
`sift` keeps the explanation simple:
|
|
83
|
+
|
|
84
|
+
1. **Capture output.** Run the noisy command or accept already-existing piped output.
|
|
85
|
+
2. **Run local heuristics.** Detect known failure shapes first so common cases stay cheap and deterministic.
|
|
86
|
+
3. **Return the diagnosis.** When heuristics are confident, `sift` gives the agent the root cause, anchor, and next step.
|
|
87
|
+
4. **Fall back only when needed.** If heuristics are not enough, `sift` uses a cheaper model instead of spending your main agent budget.
|
|
88
|
+
|
|
89
|
+
Your agent spends tokens fixing, not reading.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Key Features
|
|
94
|
+
|
|
95
|
+
<table>
|
|
96
|
+
<tr>
|
|
97
|
+
<td width="33%" valign="top">
|
|
98
|
+
|
|
99
|
+
### Test Failure Triage
|
|
100
|
+
Collapse repeated pytest, vitest, and jest failures into a short diagnosis with root-cause buckets, anchors, and fix hints.
|
|
101
|
+
|
|
102
|
+
</td>
|
|
103
|
+
<td width="33%" valign="top">
|
|
104
|
+
|
|
105
|
+
### Typecheck and Lint Reduction
|
|
106
|
+
Group noisy `tsc` and ESLint output into the few issues that actually matter instead of dumping the whole log back into the model.
|
|
107
|
+
|
|
108
|
+
</td>
|
|
109
|
+
<td width="33%" valign="top">
|
|
110
|
+
|
|
111
|
+
### Build Failure Extraction
|
|
112
|
+
Pull out the first concrete error from webpack, esbuild/Vite, Cargo, Go, GCC/Clang, and similar build output.
|
|
113
|
+
|
|
114
|
+
</td>
|
|
115
|
+
</tr>
|
|
116
|
+
<tr>
|
|
117
|
+
<td width="33%" valign="top">
|
|
118
|
+
|
|
119
|
+
### Audit and Infra Risk
|
|
120
|
+
Surface high-impact `npm audit` findings and destructive `terraform plan` signals without making the agent read everything.
|
|
121
|
+
|
|
122
|
+
</td>
|
|
123
|
+
<td width="33%" valign="top">
|
|
124
|
+
|
|
125
|
+
### Heuristic-First by Default
|
|
126
|
+
Every built-in preset tries local parsing first. When the heuristic handles the output, no provider call is needed.
|
|
127
|
+
|
|
128
|
+
</td>
|
|
129
|
+
<td width="33%" valign="top">
|
|
130
|
+
|
|
131
|
+
### Agent and Automation Friendly
|
|
132
|
+
Use `sift` in Codex, Claude, CI, hooks, or shell scripts so downstream tooling gets short, structured answers instead of raw noise.
|
|
133
|
+
|
|
134
|
+
</td>
|
|
135
|
+
</tr>
|
|
136
|
+
</table>
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Setup and Agent Integration
|
|
141
|
+
|
|
142
|
+
Most built-in presets run entirely on local heuristics with no API key needed. For presets that fall back to a model (`diff-summary`, `log-errors`, or when heuristics are not confident enough), sift supports OpenAI-compatible and OpenRouter-compatible endpoints.
|
|
143
|
+
|
|
144
|
+
Set up the provider first, then install the managed instruction block for the agent you want to steer:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
sift config setup
|
|
148
|
+
sift doctor
|
|
149
|
+
sift agent install codex
|
|
150
|
+
sift agent install claude
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
You can also preview, inspect, or remove those blocks:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
sift agent show codex
|
|
157
|
+
sift agent status
|
|
158
|
+
sift agent remove codex
|
|
159
|
+
```
|
|
34
160
|
|
|
35
|
-
|
|
161
|
+
Command-first details live in [docs/cli-reference.md](docs/cli-reference.md).
|
|
36
162
|
|
|
37
|
-
|
|
163
|
+
---
|
|
38
164
|
|
|
39
|
-
|
|
40
|
-
- **Lint output** → grouped by rule, no model call
|
|
41
|
-
- **Build failures** → first real error from webpack, esbuild/Vite, Cargo, Go, GCC/Clang
|
|
42
|
-
- **`npm audit`** → high/critical vulnerabilities only, no model call
|
|
43
|
-
- **`terraform plan`** → destructive risk detection, no model call
|
|
44
|
-
- **Diffs and logs** → compressed through a cheaper model before reaching your agent
|
|
165
|
+
## Quick Start
|
|
45
166
|
|
|
46
|
-
|
|
167
|
+
### 1. Install
|
|
47
168
|
|
|
48
169
|
```bash
|
|
49
170
|
npm install -g @bilalimamoglu/sift
|
|
@@ -51,109 +172,115 @@ npm install -g @bilalimamoglu/sift
|
|
|
51
172
|
|
|
52
173
|
Requires Node.js 20+.
|
|
53
174
|
|
|
54
|
-
|
|
175
|
+
### 2. Run Sift in front of a noisy command
|
|
55
176
|
|
|
56
177
|
```bash
|
|
57
178
|
sift exec --preset test-status -- pytest -q
|
|
58
|
-
sift exec --preset test-status -- npx vitest run
|
|
59
|
-
sift exec --preset test-status -- npx jest
|
|
60
179
|
```
|
|
61
180
|
|
|
62
|
-
Other
|
|
181
|
+
Other common entry points:
|
|
63
182
|
|
|
64
183
|
```bash
|
|
65
|
-
sift exec --preset
|
|
66
|
-
sift exec --preset
|
|
67
|
-
sift exec --preset build-failure -- npm run build
|
|
68
|
-
sift exec --preset audit-critical -- npm audit
|
|
69
|
-
sift exec --preset infra-risk -- terraform plan
|
|
184
|
+
sift exec --preset test-status -- npx vitest run
|
|
185
|
+
sift exec --preset test-status -- npx jest
|
|
70
186
|
sift exec "what changed?" -- git diff
|
|
71
187
|
```
|
|
72
188
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
`sift` sits between a noisy command and a coding agent.
|
|
189
|
+
### 3. Zoom only if needed
|
|
76
190
|
|
|
77
|
-
|
|
78
|
-
2. Run local heuristics for known failure shapes.
|
|
79
|
-
3. If heuristics are confident, return the diagnosis. No model call.
|
|
80
|
-
4. If not, call a cheaper model — not your agent's.
|
|
191
|
+
Think of the workflow like this:
|
|
81
192
|
|
|
82
|
-
|
|
193
|
+
- `standard` = map
|
|
194
|
+
- `focused` = zoom
|
|
195
|
+
- raw traceback = last resort
|
|
83
196
|
|
|
84
|
-
|
|
197
|
+
```bash
|
|
198
|
+
sift rerun
|
|
199
|
+
sift rerun --remaining --detail focused
|
|
200
|
+
```
|
|
85
201
|
|
|
86
|
-
|
|
202
|
+
If `standard` already gives you the root cause, anchor, and fix, stop there and act.
|
|
87
203
|
|
|
88
|
-
|
|
204
|
+
---
|
|
89
205
|
|
|
90
|
-
|
|
91
|
-
|--------|-------------|
|
|
92
|
-
| `test-status` | Groups pytest, vitest, jest failures into root-cause buckets with anchors and fix suggestions. 30+ failure patterns. |
|
|
93
|
-
| `typecheck-summary` | Parses `tsc` output, groups by error code, returns max 5 bullets. No model call. |
|
|
94
|
-
| `lint-failures` | Parses ESLint output, groups by rule, detects fixable hints. No model call. |
|
|
95
|
-
| `build-failure` | Extracts first concrete error from webpack, esbuild/Vite, Cargo, Go, GCC/Clang, `tsc --build`. Falls back to model for unsupported formats. |
|
|
96
|
-
| `audit-critical` | Extracts high/critical vulnerabilities from `npm audit`. No model call. |
|
|
97
|
-
| `infra-risk` | Detects destructive signals in `terraform plan`. No model call. |
|
|
98
|
-
| `diff-summary` | Summarizes changes and risks in diff output. |
|
|
99
|
-
| `log-errors` | Extracts top error signals from log output. |
|
|
206
|
+
## Presets
|
|
100
207
|
|
|
101
|
-
|
|
208
|
+
| Preset | What it does | Needs provider? |
|
|
209
|
+
|--------|--------------|:---------------:|
|
|
210
|
+
| `test-status` | Groups pytest, vitest, and jest failures into root-cause buckets with anchors and fix suggestions. | No |
|
|
211
|
+
| `typecheck-summary` | Parses `tsc` output and groups issues by error code. | No |
|
|
212
|
+
| `lint-failures` | Parses ESLint output and groups failures by rule. | No |
|
|
213
|
+
| `build-failure` | Extracts the first concrete build error from common toolchains. | Fallback only |
|
|
214
|
+
| `audit-critical` | Pulls high and critical `npm audit` findings. | No |
|
|
215
|
+
| `infra-risk` | Detects destructive signals in `terraform plan`. | No |
|
|
216
|
+
| `diff-summary` | Summarizes change sets and likely risks in diff output. | Yes |
|
|
217
|
+
| `log-errors` | Extracts the strongest error signals from noisy logs. | Fallback only |
|
|
102
218
|
|
|
103
|
-
|
|
219
|
+
When output already exists in a pipeline, use pipe mode instead of `exec`:
|
|
104
220
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
| Wall-clock time | 244s | 85s | 65% faster |
|
|
110
|
-
| Commands | 15.5 | 6 | 61% fewer |
|
|
111
|
-
| Diagnosis | Same | Same | — |
|
|
221
|
+
```bash
|
|
222
|
+
pytest -q 2>&1 | sift preset test-status
|
|
223
|
+
npm audit 2>&1 | sift preset audit-critical
|
|
224
|
+
```
|
|
112
225
|
|
|
113
|
-
|
|
226
|
+
---
|
|
114
227
|
|
|
115
|
-
## Test
|
|
228
|
+
## Test Debugging Workflow
|
|
116
229
|
|
|
117
|
-
|
|
118
|
-
- `standard` = map
|
|
119
|
-
- `focused` = zoom
|
|
120
|
-
- raw traceback = last resort
|
|
230
|
+
For noisy test failures, start with the `test-status` preset and let `standard` be the default stop point.
|
|
121
231
|
|
|
122
232
|
```bash
|
|
123
233
|
sift exec --preset test-status -- <test command>
|
|
124
234
|
sift rerun
|
|
125
235
|
sift rerun --remaining --detail focused
|
|
236
|
+
sift rerun --remaining --detail verbose --show-raw
|
|
126
237
|
```
|
|
127
238
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
`sift rerun --remaining` narrows automatically for cached `pytest` runs. For `vitest` and `jest`, it reruns the full command and keeps diagnosis focused on what still fails.
|
|
239
|
+
Useful rules of thumb:
|
|
131
240
|
|
|
132
|
-
|
|
241
|
+
- If `standard` ends with `Decision: stop and act`, go read source and fix the issue.
|
|
242
|
+
- Use `sift rerun` after a change to refresh the same test command at `standard`.
|
|
243
|
+
- Use `sift rerun --remaining` to zoom into what still fails after the first pass.
|
|
244
|
+
- Treat raw traceback as the last resort, not the starting point.
|
|
133
245
|
|
|
134
|
-
|
|
246
|
+
For machine branching or automation, `test-status` also supports diagnose JSON:
|
|
135
247
|
|
|
136
248
|
```bash
|
|
137
|
-
sift
|
|
138
|
-
sift
|
|
249
|
+
sift exec --preset test-status --goal diagnose --format json -- pytest -q
|
|
250
|
+
sift rerun --goal diagnose --format json
|
|
139
251
|
```
|
|
140
252
|
|
|
141
|
-
|
|
253
|
+
---
|
|
142
254
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
sift
|
|
146
|
-
|
|
255
|
+
## Limitations
|
|
256
|
+
|
|
257
|
+
- sift adds the most value when output is long, repetitive, and shaped by a small number of root causes. For short, obvious failures it may not save much.
|
|
258
|
+
- The deepest local heuristic coverage is in test debugging (pytest, vitest, jest). Other presets have solid heuristics but less depth.
|
|
259
|
+
- sift does not help with interactive or TUI-based commands.
|
|
260
|
+
- When heuristics cannot explain the output confidently, sift falls back to a provider. If no provider is configured, it returns what the heuristics could extract and signals that raw output may still be needed.
|
|
147
261
|
|
|
148
|
-
|
|
262
|
+
---
|
|
149
263
|
|
|
150
264
|
## Docs
|
|
151
265
|
|
|
152
266
|
- CLI reference: [docs/cli-reference.md](docs/cli-reference.md)
|
|
153
267
|
- Worked examples: [docs/examples](docs/examples)
|
|
154
268
|
- Benchmark methodology: [BENCHMARK_NOTES.md](BENCHMARK_NOTES.md)
|
|
269
|
+
- Contributing and development notes: [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
155
270
|
- Release notes: [release-notes](release-notes)
|
|
156
271
|
|
|
272
|
+
---
|
|
273
|
+
|
|
157
274
|
## License
|
|
158
275
|
|
|
159
276
|
MIT
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
<div align="center">
|
|
281
|
+
|
|
282
|
+
Built for agent-first terminal workflows.
|
|
283
|
+
|
|
284
|
+
[Report Bug](https://github.com/bilalimamoglu/sift/issues) | [Request Feature](https://github.com/bilalimamoglu/sift/issues)
|
|
285
|
+
|
|
286
|
+
</div>
|
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
|
|
|
@@ -472,7 +494,7 @@ function resolveConfig(options = {}) {
|
|
|
472
494
|
}
|
|
473
495
|
|
|
474
496
|
// src/config/write.ts
|
|
475
|
-
import
|
|
497
|
+
import fs3 from "fs";
|
|
476
498
|
import path3 from "path";
|
|
477
499
|
import YAML2 from "yaml";
|
|
478
500
|
function writeExampleConfig(options = {}) {
|
|
@@ -480,34 +502,41 @@ function writeExampleConfig(options = {}) {
|
|
|
480
502
|
throw new Error("Use either --path <path> or --global, not both.");
|
|
481
503
|
}
|
|
482
504
|
const resolved = options.global ? getDefaultGlobalConfigPath() : path3.resolve(options.targetPath ?? DEFAULT_CONFIG_FILENAME);
|
|
483
|
-
if (
|
|
505
|
+
if (fs3.existsSync(resolved)) {
|
|
484
506
|
throw new Error(`Config file already exists at ${resolved}`);
|
|
485
507
|
}
|
|
486
508
|
const yaml = YAML2.stringify(defaultConfig);
|
|
487
|
-
|
|
488
|
-
|
|
509
|
+
fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
|
|
510
|
+
fs3.writeFileSync(resolved, yaml, {
|
|
511
|
+
encoding: "utf8",
|
|
512
|
+
mode: 384
|
|
513
|
+
});
|
|
514
|
+
try {
|
|
515
|
+
fs3.chmodSync(resolved, 384);
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
489
518
|
return resolved;
|
|
490
519
|
}
|
|
491
520
|
function writeConfigFile(options) {
|
|
492
521
|
const resolved = path3.resolve(options.targetPath);
|
|
493
|
-
if (!options.overwrite &&
|
|
522
|
+
if (!options.overwrite && fs3.existsSync(resolved)) {
|
|
494
523
|
throw new Error(`Config file already exists at ${resolved}`);
|
|
495
524
|
}
|
|
496
525
|
const yaml = YAML2.stringify(options.config);
|
|
497
|
-
|
|
498
|
-
|
|
526
|
+
fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
|
|
527
|
+
fs3.writeFileSync(resolved, yaml, {
|
|
499
528
|
encoding: "utf8",
|
|
500
529
|
mode: 384
|
|
501
530
|
});
|
|
502
531
|
try {
|
|
503
|
-
|
|
532
|
+
fs3.chmodSync(resolved, 384);
|
|
504
533
|
} catch {
|
|
505
534
|
}
|
|
506
535
|
return resolved;
|
|
507
536
|
}
|
|
508
537
|
|
|
509
538
|
// src/config/editable.ts
|
|
510
|
-
import
|
|
539
|
+
import fs4 from "fs";
|
|
511
540
|
import path4 from "path";
|
|
512
541
|
function isRecord2(value) {
|
|
513
542
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -520,7 +549,7 @@ function resolveEditableConfigPath(explicitPath) {
|
|
|
520
549
|
}
|
|
521
550
|
function loadEditableConfig(explicitPath) {
|
|
522
551
|
const resolvedPath = resolveEditableConfigPath(explicitPath);
|
|
523
|
-
const existed =
|
|
552
|
+
const existed = fs4.existsSync(resolvedPath);
|
|
524
553
|
const rawConfig = existed ? loadRawConfig(resolvedPath) : {};
|
|
525
554
|
const config = siftConfigSchema.parse(
|
|
526
555
|
mergeDefined(defaultConfig, isRecord2(rawConfig) ? rawConfig : {})
|
|
@@ -1157,7 +1186,7 @@ function configUse(provider, configPath, env = process.env) {
|
|
|
1157
1186
|
}
|
|
1158
1187
|
|
|
1159
1188
|
// src/commands/agent.ts
|
|
1160
|
-
import
|
|
1189
|
+
import fs5 from "fs";
|
|
1161
1190
|
import os2 from "os";
|
|
1162
1191
|
import path6 from "path";
|
|
1163
1192
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
@@ -1787,28 +1816,49 @@ function joinAroundRemoval(before, after, eol) {
|
|
|
1787
1816
|
return `${left}${eol}${eol}${right}`;
|
|
1788
1817
|
}
|
|
1789
1818
|
function readOptionalFile(targetPath) {
|
|
1790
|
-
if (!
|
|
1819
|
+
if (!fs5.existsSync(targetPath)) {
|
|
1791
1820
|
return void 0;
|
|
1792
1821
|
}
|
|
1793
|
-
const stats =
|
|
1822
|
+
const stats = fs5.statSync(targetPath);
|
|
1794
1823
|
if (!stats.isFile()) {
|
|
1795
1824
|
throw new Error(`${targetPath} exists but is not a file.`);
|
|
1796
1825
|
}
|
|
1797
|
-
return
|
|
1826
|
+
return fs5.readFileSync(targetPath, "utf8");
|
|
1798
1827
|
}
|
|
1799
1828
|
function writeTextFileAtomic(targetPath, content) {
|
|
1800
|
-
|
|
1829
|
+
fs5.mkdirSync(path6.dirname(targetPath), { recursive: true });
|
|
1801
1830
|
const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
1802
|
-
|
|
1803
|
-
|
|
1831
|
+
fs5.writeFileSync(tempPath, content, "utf8");
|
|
1832
|
+
fs5.renameSync(tempPath, targetPath);
|
|
1804
1833
|
}
|
|
1805
1834
|
function escapeRegExp(value) {
|
|
1806
1835
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1807
1836
|
}
|
|
1808
1837
|
|
|
1809
1838
|
// src/commands/doctor.ts
|
|
1839
|
+
var PLACEHOLDER_API_KEYS = [
|
|
1840
|
+
"YOUR_API_KEY",
|
|
1841
|
+
"your_api_key",
|
|
1842
|
+
"your-api-key",
|
|
1843
|
+
"sk-xxx",
|
|
1844
|
+
"sk-placeholder",
|
|
1845
|
+
"CHANGE_ME",
|
|
1846
|
+
"change_me",
|
|
1847
|
+
"TODO",
|
|
1848
|
+
"todo",
|
|
1849
|
+
"xxx",
|
|
1850
|
+
"XXX"
|
|
1851
|
+
];
|
|
1852
|
+
function isPlaceholderApiKey(key) {
|
|
1853
|
+
if (!key) return false;
|
|
1854
|
+
return PLACEHOLDER_API_KEYS.includes(key.trim());
|
|
1855
|
+
}
|
|
1856
|
+
function isRealApiKey(key) {
|
|
1857
|
+
return Boolean(key) && !isPlaceholderApiKey(key);
|
|
1858
|
+
}
|
|
1810
1859
|
function runDoctor(config, configPath) {
|
|
1811
1860
|
const ui = createPresentation(Boolean(process.stdout.isTTY));
|
|
1861
|
+
const apiKeyStatus = isRealApiKey(config.provider.apiKey) ? "set" : isPlaceholderApiKey(config.provider.apiKey) ? "placeholder (not a real key)" : "not set";
|
|
1812
1862
|
const lines = [
|
|
1813
1863
|
"sift doctor",
|
|
1814
1864
|
"A quick check for your local setup.",
|
|
@@ -1817,7 +1867,7 @@ function runDoctor(config, configPath) {
|
|
|
1817
1867
|
ui.labelValue("provider", config.provider.provider),
|
|
1818
1868
|
ui.labelValue("model", config.provider.model),
|
|
1819
1869
|
ui.labelValue("baseUrl", config.provider.baseUrl),
|
|
1820
|
-
ui.labelValue("apiKey",
|
|
1870
|
+
ui.labelValue("apiKey", apiKeyStatus),
|
|
1821
1871
|
ui.labelValue("maxCaptureChars", String(config.input.maxCaptureChars)),
|
|
1822
1872
|
ui.labelValue("maxInputChars", String(config.input.maxInputChars)),
|
|
1823
1873
|
ui.labelValue("rawFallback", String(config.runtime.rawFallback))
|
|
@@ -1831,8 +1881,12 @@ function runDoctor(config, configPath) {
|
|
|
1831
1881
|
if (!config.provider.model) {
|
|
1832
1882
|
problems.push("Missing provider.model");
|
|
1833
1883
|
}
|
|
1834
|
-
if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible" || config.provider.provider === "openrouter") && !config.provider.apiKey) {
|
|
1835
|
-
|
|
1884
|
+
if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible" || config.provider.provider === "openrouter") && !isRealApiKey(config.provider.apiKey)) {
|
|
1885
|
+
if (isPlaceholderApiKey(config.provider.apiKey)) {
|
|
1886
|
+
problems.push(`provider.apiKey looks like a placeholder: "${config.provider.apiKey}"`);
|
|
1887
|
+
} else {
|
|
1888
|
+
problems.push("Missing provider.apiKey");
|
|
1889
|
+
}
|
|
1836
1890
|
problems.push(
|
|
1837
1891
|
`Set one of: ${getProviderApiKeyEnvNames(
|
|
1838
1892
|
config.provider.provider,
|
|
@@ -1951,6 +2005,7 @@ var OpenAIProvider = class {
|
|
|
1951
2005
|
signal: controller.signal,
|
|
1952
2006
|
headers: {
|
|
1953
2007
|
"content-type": "application/json",
|
|
2008
|
+
connection: "close",
|
|
1954
2009
|
...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
|
|
1955
2010
|
},
|
|
1956
2011
|
body: JSON.stringify({
|
|
@@ -2051,6 +2106,7 @@ var OpenAICompatibleProvider = class {
|
|
|
2051
2106
|
signal: controller.signal,
|
|
2052
2107
|
headers: {
|
|
2053
2108
|
"content-type": "application/json",
|
|
2109
|
+
connection: "close",
|
|
2054
2110
|
...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
|
|
2055
2111
|
},
|
|
2056
2112
|
body: JSON.stringify({
|
|
@@ -8233,7 +8289,7 @@ function emitStatsFooter(args) {
|
|
|
8233
8289
|
}
|
|
8234
8290
|
|
|
8235
8291
|
// src/core/testStatusState.ts
|
|
8236
|
-
import
|
|
8292
|
+
import fs6 from "fs";
|
|
8237
8293
|
import path7 from "path";
|
|
8238
8294
|
import { z as z3 } from "zod";
|
|
8239
8295
|
var detailSchema = z3.enum(["standard", "focused", "verbose"]);
|
|
@@ -8683,7 +8739,7 @@ function migrateCachedTestStatusRun(state) {
|
|
|
8683
8739
|
function readCachedTestStatusRun(statePath = getDefaultTestStatusStatePath()) {
|
|
8684
8740
|
let raw = "";
|
|
8685
8741
|
try {
|
|
8686
|
-
raw =
|
|
8742
|
+
raw = fs6.readFileSync(statePath, "utf8");
|
|
8687
8743
|
} catch (error) {
|
|
8688
8744
|
if (error.code === "ENOENT") {
|
|
8689
8745
|
throw new MissingCachedTestStatusRunError();
|
|
@@ -8704,10 +8760,10 @@ function tryReadCachedTestStatusRun(statePath = getDefaultTestStatusStatePath())
|
|
|
8704
8760
|
}
|
|
8705
8761
|
}
|
|
8706
8762
|
function writeCachedTestStatusRun(state, statePath = getDefaultTestStatusStatePath()) {
|
|
8707
|
-
|
|
8763
|
+
fs6.mkdirSync(path7.dirname(statePath), {
|
|
8708
8764
|
recursive: true
|
|
8709
8765
|
});
|
|
8710
|
-
|
|
8766
|
+
fs6.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}
|
|
8711
8767
|
`, "utf8");
|
|
8712
8768
|
}
|
|
8713
8769
|
function getNextEscalationDetail(detail) {
|
|
@@ -8869,7 +8925,8 @@ function resolveEscalationDetail(state, requested, showRaw = false) {
|
|
|
8869
8925
|
return nextDetail;
|
|
8870
8926
|
}
|
|
8871
8927
|
async function runEscalate(request) {
|
|
8872
|
-
const
|
|
8928
|
+
const scopedStatePath = getScopedTestStatusStatePath(process.cwd());
|
|
8929
|
+
const state = readCachedTestStatusRun(scopedStatePath);
|
|
8873
8930
|
const detail = resolveEscalationDetail(state, request.detail, request.showRaw);
|
|
8874
8931
|
if (request.verbose) {
|
|
8875
8932
|
process.stderr.write(
|
|
@@ -8917,10 +8974,13 @@ async function runEscalate(request) {
|
|
|
8917
8974
|
quiet: Boolean(request.quiet)
|
|
8918
8975
|
});
|
|
8919
8976
|
try {
|
|
8920
|
-
writeCachedTestStatusRun(
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8977
|
+
writeCachedTestStatusRun(
|
|
8978
|
+
{
|
|
8979
|
+
...state,
|
|
8980
|
+
detail
|
|
8981
|
+
},
|
|
8982
|
+
scopedStatePath
|
|
8983
|
+
);
|
|
8924
8984
|
} catch (error) {
|
|
8925
8985
|
if (request.verbose) {
|
|
8926
8986
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
@@ -9261,10 +9321,11 @@ async function runExec(request) {
|
|
|
9261
9321
|
const shellPath = process.env.SHELL || "/bin/bash";
|
|
9262
9322
|
const commandPreview = buildCommandPreview(request);
|
|
9263
9323
|
const commandCwd = request.cwd ?? process.cwd();
|
|
9324
|
+
const scopedStatePath = getScopedTestStatusStatePath(commandCwd);
|
|
9264
9325
|
const isTestStatusPreset = request.presetName === "test-status";
|
|
9265
9326
|
const readCachedBaseline = isTestStatusPreset && (request.readCachedBaseline ?? true);
|
|
9266
9327
|
const writeCachedBaselineRequested = isTestStatusPreset && (request.writeCachedBaseline ?? (request.skipCacheWrite ? false : true));
|
|
9267
|
-
const previousCachedRun = readCachedBaseline ? tryReadCachedTestStatusRun() : null;
|
|
9328
|
+
const previousCachedRun = readCachedBaseline ? tryReadCachedTestStatusRun(scopedStatePath) : null;
|
|
9268
9329
|
if (request.config.runtime.verbose) {
|
|
9269
9330
|
process.stderr.write(
|
|
9270
9331
|
`${pc5.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
|
|
@@ -9467,7 +9528,7 @@ ${output}`;
|
|
|
9467
9528
|
}
|
|
9468
9529
|
if (currentCachedRun && shouldWriteCachedBaseline) {
|
|
9469
9530
|
try {
|
|
9470
|
-
writeCachedTestStatusRun(currentCachedRun);
|
|
9531
|
+
writeCachedTestStatusRun(currentCachedRun, scopedStatePath);
|
|
9471
9532
|
} catch (error) {
|
|
9472
9533
|
if (request.config.runtime.verbose) {
|
|
9473
9534
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
@@ -9503,7 +9564,7 @@ ${output}`;
|
|
|
9503
9564
|
|
|
9504
9565
|
// src/core/rerun.ts
|
|
9505
9566
|
async function runRerun(request) {
|
|
9506
|
-
const state = readCachedTestStatusRun();
|
|
9567
|
+
const state = readCachedTestStatusRun(getScopedTestStatusStatePath(process.cwd()));
|
|
9507
9568
|
if (!request.remaining) {
|
|
9508
9569
|
return runExec({
|
|
9509
9570
|
...request,
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,10 @@ import { constants as osConstants } from "os";
|
|
|
4
4
|
import pc3 from "picocolors";
|
|
5
5
|
|
|
6
6
|
// src/constants.ts
|
|
7
|
+
import fs from "fs";
|
|
7
8
|
import os from "os";
|
|
8
9
|
import path from "path";
|
|
10
|
+
import crypto from "crypto";
|
|
9
11
|
function getDefaultGlobalConfigPath(homeDir = os.homedir()) {
|
|
10
12
|
return path.join(homeDir, ".config", "sift", "config.yaml");
|
|
11
13
|
}
|
|
@@ -15,6 +17,26 @@ function getDefaultGlobalStateDir(homeDir = os.homedir()) {
|
|
|
15
17
|
function getDefaultTestStatusStatePath(homeDir = os.homedir()) {
|
|
16
18
|
return path.join(getDefaultGlobalStateDir(homeDir), "last-test-status.json");
|
|
17
19
|
}
|
|
20
|
+
function getDefaultScopedTestStatusStateDir(homeDir = os.homedir()) {
|
|
21
|
+
return path.join(getDefaultGlobalStateDir(homeDir), "test-status", "by-cwd");
|
|
22
|
+
}
|
|
23
|
+
function getScopedTestStatusStatePath(cwd, homeDir = os.homedir()) {
|
|
24
|
+
const normalizedCwd = normalizeScopedCacheCwd(cwd);
|
|
25
|
+
const baseName = slugCachePathSegment(path.basename(normalizedCwd)) || "root";
|
|
26
|
+
const shortHash = crypto.createHash("sha256").update(normalizedCwd).digest("hex").slice(0, 10);
|
|
27
|
+
return path.join(getDefaultScopedTestStatusStateDir(homeDir), `${baseName}-${shortHash}.json`);
|
|
28
|
+
}
|
|
29
|
+
function slugCachePathSegment(value) {
|
|
30
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
31
|
+
}
|
|
32
|
+
function normalizeScopedCacheCwd(cwd) {
|
|
33
|
+
const absoluteCwd = path.resolve(cwd);
|
|
34
|
+
try {
|
|
35
|
+
return fs.realpathSync.native(absoluteCwd);
|
|
36
|
+
} catch {
|
|
37
|
+
return absoluteCwd;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
18
40
|
function getDefaultConfigSearchPaths() {
|
|
19
41
|
return [
|
|
20
42
|
path.resolve(process.cwd(), "sift.config.yaml"),
|
|
@@ -4741,6 +4763,7 @@ var OpenAIProvider = class {
|
|
|
4741
4763
|
signal: controller.signal,
|
|
4742
4764
|
headers: {
|
|
4743
4765
|
"content-type": "application/json",
|
|
4766
|
+
connection: "close",
|
|
4744
4767
|
...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
|
|
4745
4768
|
},
|
|
4746
4769
|
body: JSON.stringify({
|
|
@@ -4841,6 +4864,7 @@ var OpenAICompatibleProvider = class {
|
|
|
4841
4864
|
signal: controller.signal,
|
|
4842
4865
|
headers: {
|
|
4843
4866
|
"content-type": "application/json",
|
|
4867
|
+
connection: "close",
|
|
4844
4868
|
...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
|
|
4845
4869
|
},
|
|
4846
4870
|
body: JSON.stringify({
|
|
@@ -6413,7 +6437,7 @@ function emitStatsFooter(args) {
|
|
|
6413
6437
|
}
|
|
6414
6438
|
|
|
6415
6439
|
// src/core/testStatusState.ts
|
|
6416
|
-
import
|
|
6440
|
+
import fs2 from "fs";
|
|
6417
6441
|
import path2 from "path";
|
|
6418
6442
|
import { z as z2 } from "zod";
|
|
6419
6443
|
var detailSchema = z2.enum(["standard", "focused", "verbose"]);
|
|
@@ -6863,7 +6887,7 @@ function migrateCachedTestStatusRun(state) {
|
|
|
6863
6887
|
function readCachedTestStatusRun(statePath = getDefaultTestStatusStatePath()) {
|
|
6864
6888
|
let raw = "";
|
|
6865
6889
|
try {
|
|
6866
|
-
raw =
|
|
6890
|
+
raw = fs2.readFileSync(statePath, "utf8");
|
|
6867
6891
|
} catch (error) {
|
|
6868
6892
|
if (error.code === "ENOENT") {
|
|
6869
6893
|
throw new MissingCachedTestStatusRunError();
|
|
@@ -6884,10 +6908,10 @@ function tryReadCachedTestStatusRun(statePath = getDefaultTestStatusStatePath())
|
|
|
6884
6908
|
}
|
|
6885
6909
|
}
|
|
6886
6910
|
function writeCachedTestStatusRun(state, statePath = getDefaultTestStatusStatePath()) {
|
|
6887
|
-
|
|
6911
|
+
fs2.mkdirSync(path2.dirname(statePath), {
|
|
6888
6912
|
recursive: true
|
|
6889
6913
|
});
|
|
6890
|
-
|
|
6914
|
+
fs2.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}
|
|
6891
6915
|
`, "utf8");
|
|
6892
6916
|
}
|
|
6893
6917
|
function buildTargetDelta(args) {
|
|
@@ -7273,10 +7297,11 @@ async function runExec(request) {
|
|
|
7273
7297
|
const shellPath = process.env.SHELL || "/bin/bash";
|
|
7274
7298
|
const commandPreview = buildCommandPreview(request);
|
|
7275
7299
|
const commandCwd = request.cwd ?? process.cwd();
|
|
7300
|
+
const scopedStatePath = getScopedTestStatusStatePath(commandCwd);
|
|
7276
7301
|
const isTestStatusPreset = request.presetName === "test-status";
|
|
7277
7302
|
const readCachedBaseline = isTestStatusPreset && (request.readCachedBaseline ?? true);
|
|
7278
7303
|
const writeCachedBaselineRequested = isTestStatusPreset && (request.writeCachedBaseline ?? (request.skipCacheWrite ? false : true));
|
|
7279
|
-
const previousCachedRun = readCachedBaseline ? tryReadCachedTestStatusRun() : null;
|
|
7304
|
+
const previousCachedRun = readCachedBaseline ? tryReadCachedTestStatusRun(scopedStatePath) : null;
|
|
7280
7305
|
if (request.config.runtime.verbose) {
|
|
7281
7306
|
process.stderr.write(
|
|
7282
7307
|
`${pc3.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
|
|
@@ -7479,7 +7504,7 @@ ${output}`;
|
|
|
7479
7504
|
}
|
|
7480
7505
|
if (currentCachedRun && shouldWriteCachedBaseline) {
|
|
7481
7506
|
try {
|
|
7482
|
-
writeCachedTestStatusRun(currentCachedRun);
|
|
7507
|
+
writeCachedTestStatusRun(currentCachedRun, scopedStatePath);
|
|
7483
7508
|
} catch (error) {
|
|
7484
7509
|
if (request.config.runtime.verbose) {
|
|
7485
7510
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
@@ -7585,19 +7610,19 @@ var defaultConfig = {
|
|
|
7585
7610
|
};
|
|
7586
7611
|
|
|
7587
7612
|
// src/config/load.ts
|
|
7588
|
-
import
|
|
7613
|
+
import fs3 from "fs";
|
|
7589
7614
|
import path3 from "path";
|
|
7590
7615
|
import YAML from "yaml";
|
|
7591
7616
|
function findConfigPath(explicitPath) {
|
|
7592
7617
|
if (explicitPath) {
|
|
7593
7618
|
const resolved = path3.resolve(explicitPath);
|
|
7594
|
-
if (!
|
|
7619
|
+
if (!fs3.existsSync(resolved)) {
|
|
7595
7620
|
throw new Error(`Config file not found: ${resolved}`);
|
|
7596
7621
|
}
|
|
7597
7622
|
return resolved;
|
|
7598
7623
|
}
|
|
7599
7624
|
for (const candidate of getDefaultConfigSearchPaths()) {
|
|
7600
|
-
if (
|
|
7625
|
+
if (fs3.existsSync(candidate)) {
|
|
7601
7626
|
return candidate;
|
|
7602
7627
|
}
|
|
7603
7628
|
}
|
|
@@ -7608,7 +7633,7 @@ function loadRawConfig(explicitPath) {
|
|
|
7608
7633
|
if (!configPath) {
|
|
7609
7634
|
return {};
|
|
7610
7635
|
}
|
|
7611
|
-
const content =
|
|
7636
|
+
const content = fs3.readFileSync(configPath, "utf8");
|
|
7612
7637
|
return YAML.parse(content) ?? {};
|
|
7613
7638
|
}
|
|
7614
7639
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bilalimamoglu/sift",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Agent-first command-output reduction layer for agents, CI, and automation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,8 +25,10 @@
|
|
|
25
25
|
"bench:report": "node --import tsx scripts/bench/generate-progress-report.ts",
|
|
26
26
|
"dev": "tsx src/cli.ts",
|
|
27
27
|
"typecheck": "tsc --noEmit",
|
|
28
|
-
"test": "vitest run",
|
|
29
|
-
"test:
|
|
28
|
+
"test": "vitest run --config vitest.config.ts",
|
|
29
|
+
"test:smoke": "vitest run --config vitest.config.ts test/*.smoke.test.ts",
|
|
30
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts test/*.e2e.test.ts",
|
|
31
|
+
"test:coverage": "vitest run --config vitest.config.ts --coverage --exclude=\"test/**/*.smoke.test.ts\" --exclude=\"test/**/*.e2e.test.ts\"",
|
|
30
32
|
"test:watch": "vitest",
|
|
31
33
|
"prepublishOnly": "npm run typecheck && npm run test:coverage && npm run build"
|
|
32
34
|
},
|