fallow 3.4.2 → 3.5.1
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 +40 -81
- package/bin/fallow-lsp +4 -1
- package/bin/fallow-mcp +4 -1
- package/capabilities.json +14 -3
- package/package.json +10 -10
- package/scripts/lazy-verify.js +4 -1
- package/scripts/lazy-verify.test.js +6 -11
- package/scripts/run-binary.js +10 -2
- package/scripts/run-binary.test.js +71 -0
- package/scripts/verify-binary.js +9 -5
- package/scripts/verify-binary.test.js +16 -22
- package/skills/fallow/SKILL.md +6 -4
- package/skills/fallow/references/cli-reference.md +19 -15
- package/skills/fallow/references/mcp.md +1 -1
- package/skills/fallow/references/patterns.md +6 -6
- package/types/output-contract.d.ts +249 -141
package/README.md
CHANGED
|
@@ -2,98 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
**Codebase intelligence for TypeScript and JavaScript.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
One binary finds unused code, circular dependencies, duplication, complexity hotspots, boundary violations, and design-system styling drift. An optional paid layer, Fallow Runtime, adds production execution evidence. No AI inside the analyzer, and no TypeScript compiler or Node.js runtime needed for static analysis: runs are deterministic, with typed output contracts and traceable explanations.
|
|
6
6
|
|
|
7
7
|
[](https://github.com/fallow-rs/fallow/actions/workflows/ci.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/fallow)
|
|
9
9
|
[](https://github.com/fallow-rs/fallow/blob/main/LICENSE)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
It helps you answer: what changed, what got riskier, what should be reviewed, what should be refactored, and what can be safely removed. No AI inside the analyzer. Fallow produces deterministic findings, typed output contracts, and traceable explanations that downstream tools can trust.
|
|
14
|
-
|
|
15
|
-
Static analysis is open source. An optional runtime layer adds production execution evidence. Rust-native, sub-second, 123 framework plugins, no Node.js runtime dependency for analysis. fallow's edge is doing more in one pass: dead code, duplication, dependencies, complexity, architecture, styling consistency, and security candidates in a single tool. For head-to-head timings against [knip](https://knip.dev) and [jscpd](https://github.com/kucherenko/jscpd), see the [benchmarks](https://github.com/fallow-rs/fallow/tree/main/benchmarks): fallow is faster than knip on smaller projects, knip is faster on several larger repos, and jscpd's Rust rewrite is faster for raw duplication scanning.
|
|
16
|
-
|
|
17
|
-
## Installation
|
|
11
|
+
## Install
|
|
18
12
|
|
|
19
13
|
```bash
|
|
20
|
-
npm install --save-dev fallow # or: pnpm add -D fallow / yarn add -D fallow
|
|
14
|
+
npm install --save-dev fallow # or: pnpm add -D fallow / yarn add -D fallow
|
|
21
15
|
```
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
The package also ships a version-matched Agent Skill under `skills/fallow`.
|
|
26
|
-
For tools that need CLI and issue-surface metadata without spawning the binary,
|
|
27
|
-
`fallow/capabilities.json` mirrors `fallow schema` for the installed version.
|
|
28
|
-
TanStack Intent discovers it from `node_modules` automatically:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npx @tanstack/intent list
|
|
32
|
-
npx @tanstack/intent load fallow#fallow
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
For one-off CLI use without project-local skill discovery, run `npx fallow`.
|
|
36
|
-
|
|
37
|
-
Parsing fallow's JSON output in TypeScript? Import the typed shapes:
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
import type { CheckOutput, FallowJsonOutput } from "fallow/types";
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
The types are generated from the same schema as the VS Code extension and pin to the CLI version you install. See [docs.fallow.tools](https://docs.fallow.tools) for the full output contract.
|
|
17
|
+
This installs the `fallow` CLI plus the `fallow-lsp` and `fallow-mcp` launchers, so editor and agent integrations resolve the project-local binary instead of whatever happens to be on `PATH`. For one-off use, run `npx fallow` without installing. Other channels (cargo, Docker, prebuilt binaries) are covered in the [installation guide](https://docs.fallow.tools/installation).
|
|
44
18
|
|
|
45
19
|
## Quick start
|
|
46
20
|
|
|
47
21
|
```bash
|
|
48
|
-
npx fallow
|
|
49
|
-
npx fallow audit
|
|
50
|
-
npx fallow health --score #
|
|
51
|
-
npx fallow
|
|
52
|
-
npx fallow dead-code # Cleanup-specific findings
|
|
22
|
+
npx fallow # Full pipeline: dead code + duplication + health
|
|
23
|
+
npx fallow audit # Gate only what a PR changed: verdict pass/warn/fail
|
|
24
|
+
npx fallow health --score # 0 to 100 health score with a letter grade
|
|
25
|
+
npx fallow dupes # Duplication; modes strict, mild (default), weak, semantic
|
|
53
26
|
npx fallow fix --dry-run # Preview automatic cleanup
|
|
54
27
|
```
|
|
55
28
|
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
- **Quality score** -- compact health score with grade and trend delta when snapshot history is enabled
|
|
59
|
-
- **PR risk** -- changed-code analysis with pass / warn / fail verdict and per-finding attribution
|
|
60
|
-
- **Hotspots** -- functions, files, and packages combining complexity, churn, size, and coupling
|
|
61
|
-
- **Duplication** -- clone families across four detection modes (strict, mild, weak, semantic)
|
|
62
|
-
- **Design-system styling** -- CSS, Sass/Less, CSS Modules, Tailwind/shadcn/CVA, StyleX/PandaCSS, vanilla-extract, styled-components, and Emotion consistency signals
|
|
63
|
-
- **Architecture** -- circular dependencies, boundary violations, re-export chains
|
|
64
|
-
- **Dependency hygiene** -- unused, unlisted, unresolved, duplicate, and type-only deps; pnpm catalog and overrides
|
|
65
|
-
- **Cleanup opportunities** -- unused files, exports, types, enum members, class members, stale suppressions
|
|
66
|
-
- **Runtime intelligence (optional)** -- hot paths, cold code, runtime-weighted health, stale flags
|
|
29
|
+
## Output and exit codes
|
|
67
30
|
|
|
68
|
-
|
|
31
|
+
Add `--format json --quiet` to any command for one typed JSON document on stdout. Exit code 1 means findings, not failure; 0 is clean (or an audit pass or warn verdict); 2 is a validation or runtime error, reported as a JSON error envelope rather than a stack trace.
|
|
69
32
|
|
|
70
|
-
|
|
33
|
+
Parsing the output in TypeScript? Import the typed shapes, version-pinned to the CLI you install:
|
|
71
34
|
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
fallow dupes --mode semantic # Catch clones with renamed variables
|
|
75
|
-
fallow dupes --threshold 5 # Fail CI if duplication exceeds 5%
|
|
76
|
-
fallow dupes --save-baseline # Save current duplication as baseline
|
|
35
|
+
```ts
|
|
36
|
+
import type { CheckOutput, FallowJsonOutput } from "fallow/types";
|
|
77
37
|
```
|
|
78
38
|
|
|
79
|
-
|
|
39
|
+
Every issue carries an `actions[]` array with an `auto_fixable` flag, so scripts and agents know which findings they can hand to `fallow fix`. The full contract lives at [docs.fallow.tools](https://docs.fallow.tools).
|
|
80
40
|
|
|
81
|
-
##
|
|
41
|
+
## What fallow reports
|
|
82
42
|
|
|
83
|
-
|
|
43
|
+
- Unused files, exports, types, enum and class members, and dependencies
|
|
44
|
+
- Circular dependencies and re-export cycles
|
|
45
|
+
- Code duplication as clone families, across four detection modes
|
|
46
|
+
- Complexity hotspots and a 0 to 100 health score
|
|
47
|
+
- Architecture boundary violations, with zero-config presets
|
|
48
|
+
- Design-system styling drift for CSS and CSS-in-JS (Sass/Less, CSS Modules, Tailwind, styled-components, Emotion, and more)
|
|
49
|
+
- A changed-file PR gate with per-finding attribution (`fallow audit`)
|
|
50
|
+
- Optional runtime intelligence: hot paths, cold code, runtime-weighted health, stale flags (licensed Fallow Runtime; a single local coverage capture is free)
|
|
84
51
|
|
|
85
|
-
-
|
|
86
|
-
- Why is this export considered used or unused?
|
|
87
|
-
- What changed in this PR?
|
|
88
|
-
- Which files are risky to touch?
|
|
89
|
-
- What duplicate siblings exist?
|
|
90
|
-
- What cleanup action is safest?
|
|
52
|
+
For head-to-head timings against [knip](https://knip.dev) and [jscpd](https://github.com/kucherenko/jscpd), see [BENCHMARKS.md](https://github.com/fallow-rs/fallow/blob/main/BENCHMARKS.md): fallow is faster than knip on smaller projects, knip is faster on several larger repos, and jscpd's Rust rewrite is faster at raw duplication scanning.
|
|
91
53
|
|
|
92
|
-
|
|
54
|
+
## Built for agents
|
|
93
55
|
|
|
94
|
-
|
|
56
|
+
Agents get structured repo truth instead of inferring everything from grep: who imports a symbol, why an export counts as used, what a PR changed, which cleanup action is safest.
|
|
95
57
|
|
|
96
|
-
|
|
58
|
+
The bundled `fallow-mcp` server lives in `node_modules/.bin/` when installed as a devDependency, so launch it through your package manager's runner:
|
|
97
59
|
|
|
98
60
|
```json
|
|
99
61
|
{
|
|
@@ -106,15 +68,22 @@ Agents that speak MCP can launch the bundled `fallow-mcp` server. Installed as a
|
|
|
106
68
|
}
|
|
107
69
|
```
|
|
108
70
|
|
|
109
|
-
Swap `npx` for `pnpm exec`
|
|
71
|
+
Swap `npx` for `pnpm exec` or `yarn` to match your package manager; a globally installed `fallow-mcp` works as `"command": "fallow-mcp"` directly. See the [MCP integration guide](https://docs.fallow.tools/integrations/mcp).
|
|
72
|
+
|
|
73
|
+
The package also ships a version-matched agent skill under `skills/fallow`, and `fallow/capabilities.json` mirrors `fallow schema` for tools that need CLI and issue-surface metadata without spawning the binary. TanStack Intent discovers both from `node_modules`:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx @tanstack/intent list
|
|
77
|
+
npx @tanstack/intent load fallow#fallow
|
|
78
|
+
```
|
|
110
79
|
|
|
111
80
|
## Framework support
|
|
112
81
|
|
|
113
|
-
|
|
82
|
+
Over 100 built-in framework plugins covering Next.js, Nuxt, Remix, Qwik, SvelteKit, Gatsby, Astro, Angular, NestJS, AdonisJS, Ember, Expo Router, Vite, Webpack, Vitest, Jest, Playwright, Cypress, Storybook, ESLint, TypeScript, Tailwind, UnoCSS, Prisma, Drizzle, Convex, Turborepo, Hardhat, and more. Entry points are auto-detected from `package.json`, so the first run needs no configuration.
|
|
114
83
|
|
|
115
84
|
## Configuration
|
|
116
85
|
|
|
117
|
-
|
|
86
|
+
Works out of the box. To customize, let [`fallow recommend`](https://docs.fallow.tools/cli/recommend) propose a config from the detected stack (read-only; `--format json` returns the full decision set for agents), run `fallow init`, or create a config file in your project root:
|
|
118
87
|
|
|
119
88
|
```jsonc
|
|
120
89
|
// .fallowrc.json
|
|
@@ -130,23 +99,13 @@ Create a config file in your project root, or run `fallow init`:
|
|
|
130
99
|
}
|
|
131
100
|
```
|
|
132
101
|
|
|
133
|
-
`$schema` gives editors autocomplete and validation and has no effect on
|
|
134
|
-
analysis. As an npm package, `fallow` always ships a version-aligned schema at
|
|
135
|
-
`./node_modules/fallow/schema.json`, which `fallow init` and
|
|
136
|
-
`fallow recommend` point at by default: offline, no editor trust prompt. If
|
|
137
|
-
you install fallow another way (cargo, homebrew, a bare binary) with no
|
|
138
|
-
`node_modules/fallow` to point at, use the remote fallback instead:
|
|
139
|
-
`https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json`. Some
|
|
140
|
-
editors, including VS Code, refuse to load a remote schema URL until you
|
|
141
|
-
explicitly trust the domain.
|
|
142
|
-
|
|
143
|
-
Also supports TOML (`fallow init --toml` creates `fallow.toml`).
|
|
102
|
+
`$schema` gives editors autocomplete and validation and has no effect on analysis. The npm package ships a version-aligned schema at `./node_modules/fallow/schema.json`, so validation works offline with no editor trust prompt. TOML works too: `fallow init --toml` creates `fallow.toml`. Full reference: [configuration overview](https://docs.fallow.tools/configuration/overview).
|
|
144
103
|
|
|
145
104
|
## Documentation
|
|
146
105
|
|
|
147
|
-
- [
|
|
106
|
+
- [docs.fallow.tools](https://docs.fallow.tools)
|
|
148
107
|
- [GitHub repository](https://github.com/fallow-rs/fallow)
|
|
149
|
-
- [Plugin
|
|
108
|
+
- [Plugin authoring guide](https://github.com/fallow-rs/fallow/blob/main/docs/plugin-authoring.md)
|
|
150
109
|
|
|
151
110
|
## License
|
|
152
111
|
|
package/bin/fallow-lsp
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Launcher shim: the platform packages ship a single multicall `fallow`
|
|
4
|
+
// binary, so `fallow-lsp` spawns `fallow lsp-server` with the same stdio,
|
|
5
|
+
// signal, and exit-code forwarding as a bare `fallow` invocation.
|
|
3
6
|
const { runBinary } = require('../scripts/run-binary');
|
|
4
|
-
runBinary('fallow-
|
|
7
|
+
runBinary('fallow', { prependArgs: ['lsp-server'] });
|
package/bin/fallow-mcp
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Launcher shim: the platform packages ship a single multicall `fallow`
|
|
4
|
+
// binary, so `fallow-mcp` spawns `fallow mcp-server` with the same stdio,
|
|
5
|
+
// signal, and exit-code forwarding as a bare `fallow` invocation.
|
|
3
6
|
const { runBinary } = require('../scripts/run-binary');
|
|
4
|
-
runBinary('fallow-
|
|
7
|
+
runBinary('fallow', { prependArgs: ['mcp-server'] });
|
package/capabilities.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fallow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"manifest_version": "1",
|
|
5
5
|
"description": "Codebase analyzer for TypeScript/JavaScript: unused code, circular dependencies, code duplication, complexity hotspots, and architecture boundary violations",
|
|
6
6
|
"global_flags": [
|
|
@@ -311,7 +311,7 @@
|
|
|
311
311
|
"name": "--save-regression-baseline",
|
|
312
312
|
"type": "string",
|
|
313
313
|
"required": false,
|
|
314
|
-
"description": "Save the current issue counts as a regression baseline"
|
|
314
|
+
"description": "Save the current issue counts as a regression baseline. Omit PATH to update regression.baseline in the discovered fallow config, or create .fallowrc.json when none exists. Provide PATH to write a standalone file"
|
|
315
315
|
},
|
|
316
316
|
{
|
|
317
317
|
"name": "--only",
|
|
@@ -854,6 +854,16 @@
|
|
|
854
854
|
"true",
|
|
855
855
|
"false"
|
|
856
856
|
]
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
"name": "--churn",
|
|
860
|
+
"type": "bool",
|
|
861
|
+
"required": false,
|
|
862
|
+
"description": "OPT-IN: attach target-level git churn evidence from the health hotspot subsystem. Default off to avoid git-history latency",
|
|
863
|
+
"possible_values": [
|
|
864
|
+
"true",
|
|
865
|
+
"false"
|
|
866
|
+
]
|
|
857
867
|
}
|
|
858
868
|
]
|
|
859
869
|
},
|
|
@@ -6380,7 +6390,8 @@
|
|
|
6380
6390
|
"cli_command": "fallow inspect --format json --quiet",
|
|
6381
6391
|
"key_params": [
|
|
6382
6392
|
"target",
|
|
6383
|
-
"production"
|
|
6393
|
+
"production",
|
|
6394
|
+
"include_churn"
|
|
6384
6395
|
],
|
|
6385
6396
|
"license": "free",
|
|
6386
6397
|
"license_note": null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fallow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"description": "Codebase intelligence for TypeScript and JavaScript. Free static analysis of code and styles, optional paid runtime intelligence (Fallow Runtime). Quality, risk, architecture, dependencies, duplication, and design-system drift for humans, CI, and the agents writing your code. Zero-config framework support.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -84,16 +84,16 @@
|
|
|
84
84
|
"detect-libc": "2.1.2"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
|
-
"@tanstack/intent": "0.3.
|
|
87
|
+
"@tanstack/intent": "0.3.5"
|
|
88
88
|
},
|
|
89
89
|
"optionalDependencies": {
|
|
90
|
-
"@fallow-cli/darwin-arm64": "3.
|
|
91
|
-
"@fallow-cli/darwin-x64": "3.
|
|
92
|
-
"@fallow-cli/linux-x64-gnu": "3.
|
|
93
|
-
"@fallow-cli/linux-arm64-gnu": "3.
|
|
94
|
-
"@fallow-cli/linux-x64-musl": "3.
|
|
95
|
-
"@fallow-cli/linux-arm64-musl": "3.
|
|
96
|
-
"@fallow-cli/win32-arm64-msvc": "3.
|
|
97
|
-
"@fallow-cli/win32-x64-msvc": "3.
|
|
90
|
+
"@fallow-cli/darwin-arm64": "3.5.1",
|
|
91
|
+
"@fallow-cli/darwin-x64": "3.5.1",
|
|
92
|
+
"@fallow-cli/linux-x64-gnu": "3.5.1",
|
|
93
|
+
"@fallow-cli/linux-arm64-gnu": "3.5.1",
|
|
94
|
+
"@fallow-cli/linux-x64-musl": "3.5.1",
|
|
95
|
+
"@fallow-cli/linux-arm64-musl": "3.5.1",
|
|
96
|
+
"@fallow-cli/win32-arm64-msvc": "3.5.1",
|
|
97
|
+
"@fallow-cli/win32-x64-msvc": "3.5.1"
|
|
98
98
|
}
|
|
99
99
|
}
|
package/scripts/lazy-verify.js
CHANGED
|
@@ -66,8 +66,11 @@ function emitVerifyLog(env, payload) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function binaryTargetsForPlatform(platform) {
|
|
69
|
+
// Platform packages ship one binary: the multicall `fallow`. The bundled
|
|
70
|
+
// lsp/mcp launchers spawn `fallow lsp-server` / `fallow mcp-server`, so there
|
|
71
|
+
// is only one binary to sentinel-track and verify.
|
|
69
72
|
const ext = platform === "win32" ? ".exe" : "";
|
|
70
|
-
return [`fallow${ext}
|
|
73
|
+
return [`fallow${ext}`];
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
function statMtimeMs(absPath) {
|
|
@@ -28,7 +28,8 @@ function ext() {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function binaryNames() {
|
|
31
|
-
|
|
31
|
+
// Platform packages ship a single multicall `fallow` binary.
|
|
32
|
+
return [`fallow${ext()}`];
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function computeDigestsForDir(dir) {
|
|
@@ -116,7 +117,7 @@ test("ensureVerified verifies on cache miss and writes the sentinel", (t) => {
|
|
|
116
117
|
assert.equal(sentinel.schemaVersion, SENTINEL_SCHEMA_VERSION);
|
|
117
118
|
assert.equal(sentinel.packageVersion, "2.81.0");
|
|
118
119
|
assert.equal(sentinel.packageName, "@fallow-cli/test-platform");
|
|
119
|
-
assert.equal(Object.keys(sentinel.binaries).length,
|
|
120
|
+
assert.equal(Object.keys(sentinel.binaries).length, 1);
|
|
120
121
|
});
|
|
121
122
|
|
|
122
123
|
test("ensureVerified returns cached:true on a valid sentinel", (t) => {
|
|
@@ -164,7 +165,7 @@ test("ensureVerified invalidates sentinel on mtime drift", (t) => {
|
|
|
164
165
|
);
|
|
165
166
|
assert.equal(result.ok, true);
|
|
166
167
|
assert.equal(result.cached, false);
|
|
167
|
-
assert.equal(verifyCallCount,
|
|
168
|
+
assert.equal(verifyCallCount, 1, "verify should rerun for the single binary");
|
|
168
169
|
});
|
|
169
170
|
|
|
170
171
|
test("ensureVerified invalidates sentinel on packageVersion drift", (t) => {
|
|
@@ -215,12 +216,6 @@ test("ensureVerified invalidates sentinel on packageName drift", (t) => {
|
|
|
215
216
|
packageName: "@fallow-cli/wrong-name",
|
|
216
217
|
binaries: {
|
|
217
218
|
[`fallow${ext()}`]: { mtimeMs: fs.statSync(path.join(dir, `fallow${ext()}`)).mtimeMs },
|
|
218
|
-
[`fallow-lsp${ext()}`]: {
|
|
219
|
-
mtimeMs: fs.statSync(path.join(dir, `fallow-lsp${ext()}`)).mtimeMs,
|
|
220
|
-
},
|
|
221
|
-
[`fallow-mcp${ext()}`]: {
|
|
222
|
-
mtimeMs: fs.statSync(path.join(dir, `fallow-mcp${ext()}`)).mtimeMs,
|
|
223
|
-
},
|
|
224
219
|
},
|
|
225
220
|
}),
|
|
226
221
|
);
|
|
@@ -266,13 +261,13 @@ test("ensureVerified invalidates sentinel on schemaVersion drift", (t) => {
|
|
|
266
261
|
test("ensureVerified returns sig-invalid on a tampered signature", (t) => {
|
|
267
262
|
_resetWarningState();
|
|
268
263
|
const { privateKey, rawPub } = makeKeypair();
|
|
269
|
-
const dir = mkPlatformDir(privateKey, { corruptSigFor: `fallow
|
|
264
|
+
const dir = mkPlatformDir(privateKey, { corruptSigFor: `fallow${ext()}` });
|
|
270
265
|
t.after(() => cleanup(dir));
|
|
271
266
|
|
|
272
267
|
const result = ensureVerified(baseInput(dir, (p) => _verifyWithKey(p, rawPub)));
|
|
273
268
|
assert.equal(result.ok, false);
|
|
274
269
|
assert.equal(result.code, "sig-invalid");
|
|
275
|
-
assert.match(result.binary, /fallow
|
|
270
|
+
assert.match(result.binary, /fallow/);
|
|
276
271
|
// Sentinel must NOT have been written on failure
|
|
277
272
|
assert.equal(fs.existsSync(path.join(dir, SENTINEL_FILENAME)), false);
|
|
278
273
|
});
|
package/scripts/run-binary.js
CHANGED
|
@@ -146,7 +146,15 @@ function guardBrokenStdout() {
|
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
// Run the resolved platform binary. `options.prependArgs` (default none) is a
|
|
150
|
+
// list of leading arguments inserted before the process argv, used by the
|
|
151
|
+
// `fallow-lsp` / `fallow-mcp` launcher shims to spawn the multicall binary as
|
|
152
|
+
// `fallow lsp-server` / `fallow mcp-server`. Because the platform packages ship
|
|
153
|
+
// a single `fallow` binary, those shims pass `binaryBaseName = "fallow"` and
|
|
154
|
+
// the subcommand via `prependArgs`; signal, exit-code, and version-line
|
|
155
|
+
// handling stay identical to a bare `fallow` invocation.
|
|
156
|
+
function runBinary(binaryBaseName, options = {}) {
|
|
157
|
+
const prependArgs = Array.isArray(options.prependArgs) ? options.prependArgs : [];
|
|
150
158
|
guardBrokenStdout();
|
|
151
159
|
const { pkg, manifestPath, platformPkgDir } = resolvePlatformPaths();
|
|
152
160
|
const resolvedVersion = readResolvedVersion(manifestPath);
|
|
@@ -166,7 +174,7 @@ function runBinary(binaryBaseName) {
|
|
|
166
174
|
}
|
|
167
175
|
|
|
168
176
|
try {
|
|
169
|
-
execFileSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
|
|
177
|
+
execFileSync(binaryPath, [...prependArgs, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
170
178
|
} catch (e) {
|
|
171
179
|
if (e.status === undefined) throw e;
|
|
172
180
|
if (e.status === null) {
|
|
@@ -1,10 +1,61 @@
|
|
|
1
1
|
const test = require("node:test");
|
|
2
2
|
const assert = require("node:assert/strict");
|
|
3
|
+
const fs = require("node:fs");
|
|
3
4
|
const os = require("node:os");
|
|
4
5
|
const { spawnSync } = require("node:child_process");
|
|
5
6
|
const path = require("node:path");
|
|
6
7
|
|
|
7
8
|
const RUN_BINARY = path.join(__dirname, "run-binary.js");
|
|
9
|
+
const BIN_DIR = path.join(__dirname, "..", "bin");
|
|
10
|
+
|
|
11
|
+
function currentPlatformPackage() {
|
|
12
|
+
const { getPlatformPackage } = require("./platform-package");
|
|
13
|
+
if (process.platform !== "linux") {
|
|
14
|
+
return getPlatformPackage(process.platform, process.arch);
|
|
15
|
+
}
|
|
16
|
+
let libcFamily;
|
|
17
|
+
try {
|
|
18
|
+
libcFamily = require("detect-libc").familySync();
|
|
19
|
+
} catch {
|
|
20
|
+
libcFamily = undefined;
|
|
21
|
+
}
|
|
22
|
+
return getPlatformPackage(process.platform, process.arch, libcFamily);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runLauncher(t, launcher, args) {
|
|
26
|
+
const work = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-launcher-"));
|
|
27
|
+
t.after(() => fs.rmSync(work, { recursive: true, force: true }));
|
|
28
|
+
|
|
29
|
+
const pkg = currentPlatformPackage();
|
|
30
|
+
assert.ok(pkg, "the test host must map to a supported platform package");
|
|
31
|
+
const pkgDir = path.join(work, "node_modules", ...pkg.split("/"));
|
|
32
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
path.join(pkgDir, "package.json"),
|
|
35
|
+
JSON.stringify({ name: pkg, version: "3.5.0" }),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const binary = path.join(pkgDir, "fallow");
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
binary,
|
|
41
|
+
"#!/usr/bin/env node\n" +
|
|
42
|
+
'require("node:fs").writeFileSync(process.env.FALLOW_TEST_ARGS, JSON.stringify(process.argv.slice(2)));\n',
|
|
43
|
+
);
|
|
44
|
+
fs.chmodSync(binary, 0o755);
|
|
45
|
+
|
|
46
|
+
const argsFile = path.join(work, "args.json");
|
|
47
|
+
const result = spawnSync(process.execPath, [path.join(BIN_DIR, launcher), ...args], {
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
env: {
|
|
50
|
+
...process.env,
|
|
51
|
+
NODE_PATH: path.join(work, "node_modules"),
|
|
52
|
+
FALLOW_SKIP_BINARY_VERIFY: "1",
|
|
53
|
+
FALLOW_TEST_ARGS: argsFile,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
assert.equal(result.status, 0, result.stderr);
|
|
57
|
+
return JSON.parse(fs.readFileSync(argsFile, "utf8"));
|
|
58
|
+
}
|
|
8
59
|
|
|
9
60
|
// Run a child that installs guardBrokenStdout, then emits a synthetic stdout
|
|
10
61
|
// 'error' with the given code. Node delivers a broken-pipe failure as exactly
|
|
@@ -91,3 +142,23 @@ test("exitCodeForChildFailure preserves status codes and maps signal deaths", ()
|
|
|
91
142
|
assert.equal(exitCodeForChildFailure({ status: null, signal: undefined }), 1);
|
|
92
143
|
assert.equal(exitCodeForChildFailure({ status: null, signal: "NOT_A_SIGNAL" }), 1);
|
|
93
144
|
});
|
|
145
|
+
|
|
146
|
+
test(
|
|
147
|
+
"fallow-lsp executes the multicall binary with the lsp-server subcommand",
|
|
148
|
+
{ skip: process.platform === "win32" },
|
|
149
|
+
(t) => {
|
|
150
|
+
assert.deepEqual(runLauncher(t, "fallow-lsp", ["--stdio"]), ["lsp-server", "--stdio"]);
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
test(
|
|
155
|
+
"fallow-mcp executes the multicall binary with the mcp-server subcommand",
|
|
156
|
+
{ skip: process.platform === "win32" },
|
|
157
|
+
(t) => {
|
|
158
|
+
assert.deepEqual(runLauncher(t, "fallow-mcp", ["--transport", "stdio"]), [
|
|
159
|
+
"mcp-server",
|
|
160
|
+
"--transport",
|
|
161
|
+
"stdio",
|
|
162
|
+
]);
|
|
163
|
+
},
|
|
164
|
+
);
|
package/scripts/verify-binary.js
CHANGED
|
@@ -293,11 +293,15 @@ function binaryTargetsForPlatform(platformId) {
|
|
|
293
293
|
// and tests can synthesize a Windows verify without running on Windows.
|
|
294
294
|
const isWindows = typeof platformId === "string" && platformId.startsWith("win32");
|
|
295
295
|
const ext = isWindows ? ".exe" : "";
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
296
|
+
// Platform packages ship ONE binary now: the multicall `fallow` (CLI + LSP +
|
|
297
|
+
// MCP in one engine). The bundled `fallow-lsp` / `fallow-mcp` launchers live
|
|
298
|
+
// in the wrapper and spawn `fallow lsp-server` / `fallow mcp-server`. The
|
|
299
|
+
// authoritative digest is the `fallowDigests.fallow` field the release
|
|
300
|
+
// pipeline embeds for every bundled package; the `asset` below is only the
|
|
301
|
+
// legacy GitHub-release fallback for pre-embedded-digest packages, which
|
|
302
|
+
// never shipped this bundled binary, so it is inert for bundled installs and
|
|
303
|
+
// fails closed (digest-mismatch) in the impossible case it is ever reached.
|
|
304
|
+
return [{ binary: `fallow${ext}`, asset: `fallow-${platformId}${ext}` }];
|
|
301
305
|
}
|
|
302
306
|
|
|
303
307
|
function isSkipRequested() {
|
|
@@ -243,7 +243,7 @@ function makePlatformDir(privateKey, options) {
|
|
|
243
243
|
const opts = options || {};
|
|
244
244
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
|
|
245
245
|
const ext = process.platform === "win32" ? ".exe" : "";
|
|
246
|
-
for (const base of ["fallow"
|
|
246
|
+
for (const base of ["fallow"]) {
|
|
247
247
|
const binaryPath = path.join(dir, `${base}${ext}`);
|
|
248
248
|
const content = Buffer.from(`mock ${base} contents`);
|
|
249
249
|
fs.writeFileSync(binaryPath, content);
|
|
@@ -349,7 +349,7 @@ test("verifyInstalled resolves a global npm install from the fallow package dire
|
|
|
349
349
|
);
|
|
350
350
|
|
|
351
351
|
const ext = process.platform === "win32" ? ".exe" : "";
|
|
352
|
-
for (const base of ["fallow"
|
|
352
|
+
for (const base of ["fallow"]) {
|
|
353
353
|
const binaryPath = path.join(platformDir, `${base}${ext}`);
|
|
354
354
|
const content = Buffer.from(`global install ${base}`);
|
|
355
355
|
fs.writeFileSync(binaryPath, content);
|
|
@@ -367,9 +367,9 @@ test("verifyInstalled resolves a global npm install from the fallow package dire
|
|
|
367
367
|
assert.equal(result.version, "9.9.9");
|
|
368
368
|
});
|
|
369
369
|
|
|
370
|
-
test("verifyInstalled with dirOverride fails
|
|
370
|
+
test("verifyInstalled with dirOverride fails on a bad signature", async (t) => {
|
|
371
371
|
const { privateKey, rawPub } = makeKeypair();
|
|
372
|
-
const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow
|
|
372
|
+
const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow" });
|
|
373
373
|
t.after(() => cleanup(dir));
|
|
374
374
|
const result = await verifyInstalled({
|
|
375
375
|
dirOverride: dir,
|
|
@@ -378,12 +378,12 @@ test("verifyInstalled with dirOverride fails fast on the first bad signature", a
|
|
|
378
378
|
});
|
|
379
379
|
assert.equal(result.ok, false);
|
|
380
380
|
assert.equal(result.code, "sig-invalid");
|
|
381
|
-
assert.match(result.binary, /fallow
|
|
381
|
+
assert.match(result.binary, /fallow/);
|
|
382
382
|
});
|
|
383
383
|
|
|
384
384
|
test("verifyInstalled with dirOverride reports sig-missing when a .sig is absent", async (t) => {
|
|
385
385
|
const { privateKey, rawPub } = makeKeypair();
|
|
386
|
-
const dir = makePlatformDir(privateKey, { skipSigFor: "fallow
|
|
386
|
+
const dir = makePlatformDir(privateKey, { skipSigFor: "fallow" });
|
|
387
387
|
t.after(() => cleanup(dir));
|
|
388
388
|
const result = await verifyInstalled({
|
|
389
389
|
dirOverride: dir,
|
|
@@ -392,13 +392,13 @@ test("verifyInstalled with dirOverride reports sig-missing when a .sig is absent
|
|
|
392
392
|
});
|
|
393
393
|
assert.equal(result.ok, false);
|
|
394
394
|
assert.equal(result.code, "sig-missing");
|
|
395
|
-
assert.match(result.binary, /fallow
|
|
395
|
+
assert.match(result.binary, /fallow/);
|
|
396
396
|
});
|
|
397
397
|
|
|
398
398
|
test("verifyInstalled threads the resolved version into the sig-missing message", async (t) => {
|
|
399
399
|
const { privateKey, rawPub } = makeKeypair();
|
|
400
|
-
const preDir = makePlatformDir(privateKey, { skipSigFor: "fallow
|
|
401
|
-
const eraDir = makePlatformDir(privateKey, { skipSigFor: "fallow
|
|
400
|
+
const preDir = makePlatformDir(privateKey, { skipSigFor: "fallow" });
|
|
401
|
+
const eraDir = makePlatformDir(privateKey, { skipSigFor: "fallow" });
|
|
402
402
|
t.after(() => {
|
|
403
403
|
cleanup(preDir);
|
|
404
404
|
cleanup(eraDir);
|
|
@@ -463,7 +463,7 @@ test("verifyInstalled honors FALLOW_SKIP_BINARY_VERIFY", async (t) => {
|
|
|
463
463
|
function computeDigestsForDir(dir) {
|
|
464
464
|
const ext = process.platform === "win32" ? ".exe" : "";
|
|
465
465
|
const out = {};
|
|
466
|
-
for (const base of ["fallow"
|
|
466
|
+
for (const base of ["fallow"]) {
|
|
467
467
|
const fileName = `${base}${ext}`;
|
|
468
468
|
const full = path.join(dir, fileName);
|
|
469
469
|
out[fileName] =
|
|
@@ -549,7 +549,7 @@ test("verifyInstalled falls back to the provider when the embedded digest is mis
|
|
|
549
549
|
},
|
|
550
550
|
});
|
|
551
551
|
assert.equal(result.ok, true);
|
|
552
|
-
assert.equal(providerCalls,
|
|
552
|
+
assert.equal(providerCalls, 1);
|
|
553
553
|
});
|
|
554
554
|
|
|
555
555
|
test("verifyInstalled falls back to the provider when fallowDigests is partial / malformed", async (t) => {
|
|
@@ -573,8 +573,8 @@ test("verifyInstalled falls back to the provider when fallowDigests is partial /
|
|
|
573
573
|
},
|
|
574
574
|
});
|
|
575
575
|
assert.equal(result.ok, true);
|
|
576
|
-
// One call
|
|
577
|
-
assert.equal(providerCalls,
|
|
576
|
+
// One call for the single shipped binary because its entry fails to normalize.
|
|
577
|
+
assert.equal(providerCalls, 1);
|
|
578
578
|
});
|
|
579
579
|
|
|
580
580
|
test("verifyInstalled returns digest-mismatch when the embedded digest disagrees with the binary", async (t) => {
|
|
@@ -587,8 +587,6 @@ test("verifyInstalled returns digest-mismatch when the embedded digest disagrees
|
|
|
587
587
|
version: "1.0.0",
|
|
588
588
|
fallowDigests: {
|
|
589
589
|
[`fallow${ext}`]: "sha256:" + "a".repeat(64),
|
|
590
|
-
[`fallow-lsp${ext}`]: "sha256:" + "a".repeat(64),
|
|
591
|
-
[`fallow-mcp${ext}`]: "sha256:" + "a".repeat(64),
|
|
592
590
|
},
|
|
593
591
|
});
|
|
594
592
|
const result = await verifyInstalled({
|
|
@@ -644,9 +642,9 @@ test("verifyInstalledSync with embedded digests returns ok end-to-end", (t) => {
|
|
|
644
642
|
assert.equal(result.package, "<override>");
|
|
645
643
|
});
|
|
646
644
|
|
|
647
|
-
test("verifyInstalledSync fails
|
|
645
|
+
test("verifyInstalledSync fails on a bad signature", (t) => {
|
|
648
646
|
const { privateKey, rawPub } = makeKeypair();
|
|
649
|
-
const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow
|
|
647
|
+
const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow" });
|
|
650
648
|
t.after(() => cleanup(dir));
|
|
651
649
|
writeManifest(dir, {
|
|
652
650
|
name: "@fallow-cli/x",
|
|
@@ -659,7 +657,7 @@ test("verifyInstalledSync fails fast on first bad signature", (t) => {
|
|
|
659
657
|
});
|
|
660
658
|
assert.equal(result.ok, false);
|
|
661
659
|
assert.equal(result.code, "sig-invalid");
|
|
662
|
-
assert.match(result.binary, /fallow
|
|
660
|
+
assert.match(result.binary, /fallow/);
|
|
663
661
|
});
|
|
664
662
|
|
|
665
663
|
test("verifyInstalledSync reports digest-mismatch when bytes diverge from embedded digest", (t) => {
|
|
@@ -671,11 +669,7 @@ test("verifyInstalledSync reports digest-mismatch when bytes diverge from embedd
|
|
|
671
669
|
version: "9.9.9",
|
|
672
670
|
fallowDigests: {
|
|
673
671
|
fallow: "sha256:" + "a".repeat(64),
|
|
674
|
-
"fallow-lsp": "sha256:" + "a".repeat(64),
|
|
675
|
-
"fallow-mcp": "sha256:" + "a".repeat(64),
|
|
676
672
|
"fallow.exe": "sha256:" + "a".repeat(64),
|
|
677
|
-
"fallow-lsp.exe": "sha256:" + "a".repeat(64),
|
|
678
|
-
"fallow-mcp.exe": "sha256:" + "a".repeat(64),
|
|
679
673
|
},
|
|
680
674
|
});
|
|
681
675
|
const result = verifyInstalledSync({
|