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 CHANGED
@@ -2,98 +2,60 @@
2
2
 
3
3
  **Codebase intelligence for TypeScript and JavaScript.**
4
4
 
5
- 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.
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
  [![CI](https://github.com/fallow-rs/fallow/actions/workflows/ci.yml/badge.svg)](https://github.com/fallow-rs/fallow/actions/workflows/ci.yml)
8
8
  [![npm](https://img.shields.io/npm/v/fallow.svg)](https://www.npmjs.com/package/fallow)
9
9
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/fallow-rs/fallow/blob/main/LICENSE)
10
10
 
11
- Fallow turns a frontend repository into a trusted quality report: health score, changed-code risk, hotspots, duplication, architecture issues, dependency hygiene, styling consistency, and cleanup opportunities.
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 / bun add -d fallow
14
+ npm install --save-dev fallow # or: pnpm add -D fallow / yarn add -D fallow
21
15
  ```
22
16
 
23
- Installs the `fallow` CLI plus the companion `fallow-lsp` and `fallow-mcp` binaries in your project.
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 audit # PR-style audit: verdict pass / warn / fail
49
- npx fallow audit --format json # Machine-readable audit (for CI and agents)
50
- npx fallow health --score # Quality score and grade
51
- npx fallow # Full codebase analysis: health + duplication + cleanup
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
- ## What Fallow reports
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
- Cleanup opportunities are findings that look safe to review for removal because no graph evidence supports keeping them. Dead code is one category of cleanup, not the product identity.
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
- ## Code duplication
33
+ Parsing the output in TypeScript? Import the typed shapes, version-pinned to the CLI you install:
71
34
 
72
- ```bash
73
- fallow dupes # Default: mild mode
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
- Four detection modes (strict, mild, weak, semantic), clone family grouping with refactoring suggestions, baseline tracking, and cross-language TS/JS matching.
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
- ## Built for agents
41
+ ## What fallow reports
82
42
 
83
- Fallow gives AI agents structured repo truth instead of forcing them to infer everything from grep. Agents call the CLI or the MCP server to answer:
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
- - Who imports this symbol?
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
- Every issue in `--format json` carries a machine-actionable `actions` array with an `auto_fixable` flag so agents can self-correct.
54
+ ## Built for agents
93
55
 
94
- ### MCP server
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
- Agents that speak MCP can launch the bundled `fallow-mcp` server. Installed as a devDependency, the binary lives in `node_modules/.bin/` and is not on your `PATH`, so launch it through your package manager's runner:
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` / `yarn` / `bunx` to match your package manager. If `fallow-mcp` is installed globally (on your `PATH`), `"command": "fallow-mcp"` works directly. See the [MCP integration guide](https://docs.fallow.tools/integrations/mcp).
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
- 123 built-in 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 many more. Auto-detected from your `package.json`.
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
- Create a config file in your project root, or run `fallow init`:
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
- - [Full documentation](https://docs.fallow.tools)
106
+ - [docs.fallow.tools](https://docs.fallow.tools)
148
107
  - [GitHub repository](https://github.com/fallow-rs/fallow)
149
- - [Plugin Authoring Guide](https://github.com/fallow-rs/fallow/blob/main/docs/plugin-authoring.md)
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-lsp');
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-mcp');
7
+ runBinary('fallow', { prependArgs: ['mcp-server'] });
package/capabilities.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow",
3
- "version": "3.4.2",
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.4.2",
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.2"
87
+ "@tanstack/intent": "0.3.5"
88
88
  },
89
89
  "optionalDependencies": {
90
- "@fallow-cli/darwin-arm64": "3.4.2",
91
- "@fallow-cli/darwin-x64": "3.4.2",
92
- "@fallow-cli/linux-x64-gnu": "3.4.2",
93
- "@fallow-cli/linux-arm64-gnu": "3.4.2",
94
- "@fallow-cli/linux-x64-musl": "3.4.2",
95
- "@fallow-cli/linux-arm64-musl": "3.4.2",
96
- "@fallow-cli/win32-arm64-msvc": "3.4.2",
97
- "@fallow-cli/win32-x64-msvc": "3.4.2"
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
  }
@@ -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}`, `fallow-lsp${ext}`, `fallow-mcp${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
- return [`fallow${ext()}`, `fallow-lsp${ext()}`, `fallow-mcp${ext()}`];
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, 3);
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, 3, "verify should rerun for all three binaries");
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-lsp${ext()}` });
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-lsp/);
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
  });
@@ -146,7 +146,15 @@ function guardBrokenStdout() {
146
146
  });
147
147
  }
148
148
 
149
- function runBinary(binaryBaseName) {
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
+ );
@@ -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
- return [
297
- { binary: `fallow${ext}`, asset: `fallow-${platformId}${ext}` },
298
- { binary: `fallow-lsp${ext}`, asset: `fallow-lsp-${platformId}${ext}` },
299
- { binary: `fallow-mcp${ext}`, asset: `fallow-mcp-${platformId}${ext}` },
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", "fallow-lsp", "fallow-mcp"]) {
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", "fallow-lsp", "fallow-mcp"]) {
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 fast on the first bad signature", async (t) => {
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-lsp" });
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-lsp/);
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-mcp" });
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-mcp/);
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-mcp" });
401
- const eraDir = makePlatformDir(privateKey, { skipSigFor: "fallow-mcp" });
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", "fallow-lsp", "fallow-mcp"]) {
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, 3);
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 per binary because all three entries fail to normalize.
577
- assert.equal(providerCalls, 3);
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 fast on first bad signature", (t) => {
645
+ test("verifyInstalledSync fails on a bad signature", (t) => {
648
646
  const { privateKey, rawPub } = makeKeypair();
649
- const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow-lsp" });
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-lsp/);
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({