fallow 2.78.0 → 2.79.0
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 +41 -26
- package/package.json +22 -15
- package/scripts/verify-binary.js +69 -20
- package/scripts/verify-binary.test.js +136 -0
- package/skills/fallow/SKILL.md +5 -5
package/README.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# fallow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Deterministic codebase intelligence for TypeScript and JavaScript.**
|
|
4
|
+
|
|
5
|
+
Quality, risk, architecture, dependencies, duplication, and safe cleanup evidence for humans, CI, and agents.
|
|
4
6
|
|
|
5
7
|
[](https://github.com/fallow-rs/fallow/actions/workflows/ci.yml)
|
|
6
8
|
[](https://www.npmjs.com/package/fallow)
|
|
7
9
|
[](https://github.com/fallow-rs/fallow/blob/main/LICENSE)
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Fallow turns a JS/TS repository into a trusted quality report: health score, changed-code risk, hotspots, duplication, architecture issues, dependency hygiene, 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.
|
|
10
14
|
|
|
11
|
-
Static analysis is free and open source. Runtime
|
|
15
|
+
Static analysis is free and open source. An optional paid runtime layer (Fallow Runtime) adds production execution evidence. Rust-native, sub-second, 97 framework plugins, 5-41x faster than [knip](https://knip.dev) v5 (2-18x faster than knip v6), 8-29x faster than [jscpd](https://github.com/kucherenko/jscpd) for duplication detection, with no Node.js runtime dependency for analysis.
|
|
12
16
|
|
|
13
17
|
## Installation
|
|
14
18
|
|
|
@@ -36,31 +40,29 @@ import type { CheckOutput, FallowJsonOutput } from "fallow/types";
|
|
|
36
40
|
|
|
37
41
|
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.
|
|
38
42
|
|
|
39
|
-
##
|
|
43
|
+
## Quick start
|
|
40
44
|
|
|
41
45
|
```bash
|
|
42
|
-
fallow
|
|
43
|
-
fallow
|
|
44
|
-
fallow
|
|
45
|
-
fallow
|
|
46
|
-
fallow
|
|
47
|
-
fallow fix --dry-run
|
|
46
|
+
npx fallow audit # PR-style audit: verdict pass / warn / fail
|
|
47
|
+
npx fallow audit --format json # Machine-readable audit (for CI and agents)
|
|
48
|
+
npx fallow health --score # Quality score and grade
|
|
49
|
+
npx fallow # Full codebase analysis: health + duplication + cleanup
|
|
50
|
+
npx fallow dead-code # Cleanup-specific findings
|
|
51
|
+
npx fallow fix --dry-run # Preview automatic cleanup
|
|
48
52
|
```
|
|
49
53
|
|
|
50
|
-
## What
|
|
51
|
-
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
54
|
-
- **
|
|
55
|
-
- **
|
|
56
|
-
- **
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
59
|
-
- **
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- **Circular dependencies** -- import cycles in the module graph
|
|
63
|
-
- **Type-only dependencies** -- production deps only used via `import type`
|
|
54
|
+
## What Fallow reports
|
|
55
|
+
|
|
56
|
+
- **Quality score** -- compact health score with grade and trend delta when snapshot history is enabled
|
|
57
|
+
- **PR risk** -- changed-code analysis with pass / warn / fail verdict and per-finding attribution
|
|
58
|
+
- **Hotspots** -- functions, files, and packages combining complexity, churn, size, and coupling
|
|
59
|
+
- **Duplication** -- clone families across four detection modes (strict, mild, weak, semantic)
|
|
60
|
+
- **Architecture** -- circular dependencies, boundary violations, re-export chains
|
|
61
|
+
- **Dependency hygiene** -- unused, unlisted, unresolved, duplicate, and type-only deps; pnpm catalog and overrides
|
|
62
|
+
- **Cleanup opportunities** -- unused files, exports, types, enum members, class members, stale suppressions
|
|
63
|
+
- **Runtime intelligence (optional, paid)** -- hot paths, cold code, runtime-weighted health, stale flags
|
|
64
|
+
|
|
65
|
+
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.
|
|
64
66
|
|
|
65
67
|
## Code duplication
|
|
66
68
|
|
|
@@ -71,11 +73,24 @@ fallow dupes --threshold 5 # Fail CI if duplication exceeds 5%
|
|
|
71
73
|
fallow dupes --save-baseline # Save current duplication as baseline
|
|
72
74
|
```
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
Four detection modes (strict, mild, weak, semantic), clone family grouping with refactoring suggestions, baseline tracking, and cross-language TS/JS matching.
|
|
77
|
+
|
|
78
|
+
## Built for agents
|
|
79
|
+
|
|
80
|
+
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:
|
|
81
|
+
|
|
82
|
+
- Who imports this symbol?
|
|
83
|
+
- Why is this export considered used or unused?
|
|
84
|
+
- What changed in this PR?
|
|
85
|
+
- Which files are risky to touch?
|
|
86
|
+
- What duplicate siblings exist?
|
|
87
|
+
- What cleanup action is safest?
|
|
88
|
+
|
|
89
|
+
Every issue in `--format json` carries a machine-actionable `actions` array with an `auto_fixable` flag so agents can self-correct.
|
|
75
90
|
|
|
76
91
|
## Framework support
|
|
77
92
|
|
|
78
|
-
|
|
93
|
+
97 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`.
|
|
79
94
|
|
|
80
95
|
## Configuration
|
|
81
96
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fallow",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.79.0",
|
|
4
|
+
"description": "Deterministic codebase intelligence for TypeScript and JavaScript. Quality, risk, architecture, dependencies, duplication, and safe cleanup evidence for humans, CI, and agents. Optional runtime intelligence layer (Fallow Runtime) adds production execution evidence. Rust-native, sub-second, 96 framework plugins.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -17,14 +17,21 @@
|
|
|
17
17
|
"docs": "https://docs.fallow.tools"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
|
-
"
|
|
21
|
-
"
|
|
20
|
+
"code-intelligence",
|
|
21
|
+
"code-quality",
|
|
22
|
+
"pr-risk",
|
|
23
|
+
"static-analysis",
|
|
22
24
|
"code-duplication",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
+
"circular-dependencies",
|
|
26
|
+
"unused-code",
|
|
27
|
+
"complexity",
|
|
28
|
+
"architecture",
|
|
25
29
|
"typescript",
|
|
26
30
|
"javascript",
|
|
27
|
-
"
|
|
31
|
+
"analyzer",
|
|
32
|
+
"lint",
|
|
33
|
+
"lsp",
|
|
34
|
+
"mcp",
|
|
28
35
|
"agent-skills",
|
|
29
36
|
"tanstack-intent"
|
|
30
37
|
],
|
|
@@ -74,13 +81,13 @@
|
|
|
74
81
|
"@tanstack/intent": "0.0.41"
|
|
75
82
|
},
|
|
76
83
|
"optionalDependencies": {
|
|
77
|
-
"@fallow-cli/darwin-arm64": "2.
|
|
78
|
-
"@fallow-cli/darwin-x64": "2.
|
|
79
|
-
"@fallow-cli/linux-x64-gnu": "2.
|
|
80
|
-
"@fallow-cli/linux-arm64-gnu": "2.
|
|
81
|
-
"@fallow-cli/linux-x64-musl": "2.
|
|
82
|
-
"@fallow-cli/linux-arm64-musl": "2.
|
|
83
|
-
"@fallow-cli/win32-arm64-msvc": "2.
|
|
84
|
-
"@fallow-cli/win32-x64-msvc": "2.
|
|
84
|
+
"@fallow-cli/darwin-arm64": "2.79.0",
|
|
85
|
+
"@fallow-cli/darwin-x64": "2.79.0",
|
|
86
|
+
"@fallow-cli/linux-x64-gnu": "2.79.0",
|
|
87
|
+
"@fallow-cli/linux-arm64-gnu": "2.79.0",
|
|
88
|
+
"@fallow-cli/linux-x64-musl": "2.79.0",
|
|
89
|
+
"@fallow-cli/linux-arm64-musl": "2.79.0",
|
|
90
|
+
"@fallow-cli/win32-arm64-msvc": "2.79.0",
|
|
91
|
+
"@fallow-cli/win32-x64-msvc": "2.79.0"
|
|
85
92
|
}
|
|
86
93
|
}
|
package/scripts/verify-binary.js
CHANGED
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Verifies each platform binary against a .sig file shipped alongside it in
|
|
4
4
|
// the @fallow-cli/<platform> package, then cross-checks the binary bytes
|
|
5
|
-
// against
|
|
6
|
-
//
|
|
7
|
-
// `.github/scripts/sign-binary.mjs` using the workflow's
|
|
5
|
+
// against an expected SHA-256 digest. The .sig is produced at release time
|
|
6
|
+
// by `.github/scripts/sign-binary.mjs` using the workflow's
|
|
8
7
|
// ED25519_BINARY_SIGNING_PRIVATE_KEY secret. The matching public key (32 raw
|
|
9
8
|
// bytes) is embedded below and is identical to the value already trusted by
|
|
10
9
|
// the VS Code extension at editors/vscode/src/download.ts:19-22.
|
|
11
10
|
//
|
|
11
|
+
// SHA-256 digest source (in order of preference, refs #465 and #597):
|
|
12
|
+
// 1. `fallowDigests` field in the platform package's package.json, written
|
|
13
|
+
// at release time by the `npm-prep` job. This is the steady-state path:
|
|
14
|
+
// no network traffic, immune to GitHub API rate limits.
|
|
15
|
+
// 2. Fallback: GitHub Release asset digest via the public REST API. Kept
|
|
16
|
+
// for backwards compatibility with platform packages published before
|
|
17
|
+
// #597 that lack the embedded field. Shared-IP CI runners (Buildkite,
|
|
18
|
+
// pooled GHA runners) can exceed the 60 req/hr unauthenticated limit;
|
|
19
|
+
// that failure mode is what motivated #597.
|
|
20
|
+
//
|
|
12
21
|
// Triggered from scripts/postinstall.js and from the GitHub Action installer
|
|
13
22
|
// at action/scripts/install.sh. The escape hatch FALLOW_SKIP_BINARY_VERIFY=1
|
|
14
23
|
// is documented in SECURITY.md.
|
|
15
24
|
//
|
|
16
|
-
// No external dependencies: uses node:crypto and node:fs only.
|
|
25
|
+
// No external dependencies: uses node:crypto and node:fs only.
|
|
17
26
|
|
|
18
27
|
const crypto = require('node:crypto');
|
|
19
28
|
const fs = require('node:fs');
|
|
@@ -211,6 +220,31 @@ function platformPackageDir(pkg, resolveFrom) {
|
|
|
211
220
|
return { dir: path.dirname(manifestPath), manifestPath };
|
|
212
221
|
}
|
|
213
222
|
|
|
223
|
+
// Read the SHA-256 digest for a binary embedded in the platform package's
|
|
224
|
+
// package.json (written by the npm-prep job at release time as
|
|
225
|
+
// `fallowDigests[<filename>]`). Returns the normalized digest hex or null
|
|
226
|
+
// when the manifest does not exist, cannot be parsed, lacks the field, or
|
|
227
|
+
// the value is malformed. Refs #597.
|
|
228
|
+
function readEmbeddedDigest(manifestPath, binaryFileName) {
|
|
229
|
+
if (typeof manifestPath !== 'string' || manifestPath.length === 0) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
let manifest;
|
|
233
|
+
try {
|
|
234
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const digests = manifest.fallowDigests;
|
|
242
|
+
if (!digests || typeof digests !== 'object') {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return normalizeDigest(digests[binaryFileName]);
|
|
246
|
+
}
|
|
247
|
+
|
|
214
248
|
function binaryTargetsForPlatform(platformId) {
|
|
215
249
|
const isWindows = process.platform === 'win32';
|
|
216
250
|
const ext = isWindows ? '.exe' : '';
|
|
@@ -265,6 +299,9 @@ async function verifyInstalled(options) {
|
|
|
265
299
|
pkg = '<override>';
|
|
266
300
|
version = opts.version || '0.0.0';
|
|
267
301
|
platformId = opts.platformId || 'test-platform';
|
|
302
|
+
// Lets tests drop a package.json next to the binaries to exercise the
|
|
303
|
+
// embedded-digest path. readEmbeddedDigest returns null when missing.
|
|
304
|
+
manifestPath = path.join(dir, 'package.json');
|
|
268
305
|
} else {
|
|
269
306
|
if (process.platform !== 'linux') {
|
|
270
307
|
pkg = getPlatformPackage(process.platform, process.arch);
|
|
@@ -308,22 +345,33 @@ async function verifyInstalled(options) {
|
|
|
308
345
|
if (!result.ok) {
|
|
309
346
|
return { ...result, binary: binaryPath, package: pkg };
|
|
310
347
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
348
|
+
// Prefer the digest embedded in the platform package's package.json
|
|
349
|
+
// (written at release time by `npm-prep`). Falling back to the GitHub
|
|
350
|
+
// release API only when no embedded digest is present preserves
|
|
351
|
+
// backwards compatibility with platform packages published before #597.
|
|
352
|
+
// The shared-IP CI rate-limit failure mode that motivated #597 only
|
|
353
|
+
// affects the fallback path, so the steady-state install path is now
|
|
354
|
+
// fully offline.
|
|
355
|
+
let expectedDigest = manifestPath
|
|
356
|
+
? readEmbeddedDigest(manifestPath, target.binary)
|
|
357
|
+
: null;
|
|
358
|
+
if (!expectedDigest) {
|
|
359
|
+
try {
|
|
360
|
+
expectedDigest = await digestProvider({
|
|
361
|
+
assetName: target.asset,
|
|
362
|
+
binaryPath,
|
|
363
|
+
packageName: pkg,
|
|
364
|
+
version,
|
|
365
|
+
});
|
|
366
|
+
} catch (err) {
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
code: 'digest-unavailable',
|
|
370
|
+
message: `cannot load SHA-256 digest for ${target.asset}: ${err.message}`,
|
|
371
|
+
binary: binaryPath,
|
|
372
|
+
package: pkg,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
327
375
|
}
|
|
328
376
|
const digestResult = verifyDigestAt(binaryPath, expectedDigest);
|
|
329
377
|
if (!digestResult.ok) {
|
|
@@ -340,6 +388,7 @@ module.exports = {
|
|
|
340
388
|
_verifyWithKey,
|
|
341
389
|
fetchReleaseDigest,
|
|
342
390
|
normalizeDigest,
|
|
391
|
+
readEmbeddedDigest,
|
|
343
392
|
sha256Hex,
|
|
344
393
|
EMBEDDED_PUBLIC_KEY,
|
|
345
394
|
ED25519_SPKI_HEADER,
|
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
verifyInstalled,
|
|
13
13
|
sha256Hex,
|
|
14
14
|
normalizeDigest,
|
|
15
|
+
readEmbeddedDigest,
|
|
15
16
|
EMBEDDED_PUBLIC_KEY,
|
|
16
17
|
ED25519_SPKI_HEADER,
|
|
17
18
|
SKIP_ENV,
|
|
@@ -381,6 +382,141 @@ test('verifyInstalled honors FALLOW_SKIP_BINARY_VERIFY', async (t) => {
|
|
|
381
382
|
assert.equal(result.skipped, true);
|
|
382
383
|
});
|
|
383
384
|
|
|
385
|
+
function computeDigestsForDir(dir) {
|
|
386
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
387
|
+
const out = {};
|
|
388
|
+
for (const base of ['fallow', 'fallow-lsp', 'fallow-mcp']) {
|
|
389
|
+
const fileName = `${base}${ext}`;
|
|
390
|
+
const full = path.join(dir, fileName);
|
|
391
|
+
out[fileName] = 'sha256:' + crypto.createHash('sha256').update(fs.readFileSync(full)).digest('hex');
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function writeManifest(dir, body) {
|
|
397
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(body));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
test('readEmbeddedDigest returns null when manifest is missing', () => {
|
|
401
|
+
assert.equal(readEmbeddedDigest('/does/not/exist/package.json', 'fallow'), null);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('readEmbeddedDigest returns null when fallowDigests field is absent', (t) => {
|
|
405
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fallow-vbtest-emb-'));
|
|
406
|
+
t.after(() => cleanup(dir));
|
|
407
|
+
writeManifest(dir, { name: '@fallow-cli/x', version: '1.0.0' });
|
|
408
|
+
assert.equal(readEmbeddedDigest(path.join(dir, 'package.json'), 'fallow'), null);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('readEmbeddedDigest returns null when the per-binary entry is malformed', (t) => {
|
|
412
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fallow-vbtest-emb-'));
|
|
413
|
+
t.after(() => cleanup(dir));
|
|
414
|
+
writeManifest(dir, {
|
|
415
|
+
name: '@fallow-cli/x',
|
|
416
|
+
version: '1.0.0',
|
|
417
|
+
fallowDigests: { fallow: 'not-a-real-digest' },
|
|
418
|
+
});
|
|
419
|
+
assert.equal(readEmbeddedDigest(path.join(dir, 'package.json'), 'fallow'), null);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('readEmbeddedDigest returns normalized hex for a valid sha256: prefixed entry', (t) => {
|
|
423
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fallow-vbtest-emb-'));
|
|
424
|
+
t.after(() => cleanup(dir));
|
|
425
|
+
const hex = 'a'.repeat(64);
|
|
426
|
+
writeManifest(dir, {
|
|
427
|
+
name: '@fallow-cli/x',
|
|
428
|
+
version: '1.0.0',
|
|
429
|
+
fallowDigests: { fallow: 'sha256:' + hex },
|
|
430
|
+
});
|
|
431
|
+
assert.equal(readEmbeddedDigest(path.join(dir, 'package.json'), 'fallow'), hex);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('verifyInstalled uses the embedded digest without calling the provider', async (t) => {
|
|
435
|
+
const { privateKey, rawPub } = makeKeypair();
|
|
436
|
+
const dir = makePlatformDir(privateKey);
|
|
437
|
+
t.after(() => cleanup(dir));
|
|
438
|
+
writeManifest(dir, {
|
|
439
|
+
name: '@fallow-cli/x',
|
|
440
|
+
version: '1.0.0',
|
|
441
|
+
fallowDigests: computeDigestsForDir(dir),
|
|
442
|
+
});
|
|
443
|
+
let providerCalls = 0;
|
|
444
|
+
const result = await verifyInstalled({
|
|
445
|
+
dirOverride: dir,
|
|
446
|
+
verifyFn: (p) => _verifyWithKey(p, rawPub),
|
|
447
|
+
digestProvider: () => {
|
|
448
|
+
providerCalls += 1;
|
|
449
|
+
return Promise.reject(new Error('provider should not be called'));
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
assert.equal(result.ok, true, JSON.stringify(result));
|
|
453
|
+
assert.equal(providerCalls, 0);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('verifyInstalled falls back to the provider when the embedded digest is missing', async (t) => {
|
|
457
|
+
const { privateKey, rawPub } = makeKeypair();
|
|
458
|
+
const dir = makePlatformDir(privateKey);
|
|
459
|
+
t.after(() => cleanup(dir));
|
|
460
|
+
// No package.json at all; legacy platform package shape.
|
|
461
|
+
let providerCalls = 0;
|
|
462
|
+
const result = await verifyInstalled({
|
|
463
|
+
dirOverride: dir,
|
|
464
|
+
verifyFn: (p) => _verifyWithKey(p, rawPub),
|
|
465
|
+
digestProvider: ({ binaryPath }) => {
|
|
466
|
+
providerCalls += 1;
|
|
467
|
+
return Promise.resolve('sha256:' + crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex'));
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
assert.equal(result.ok, true);
|
|
471
|
+
assert.equal(providerCalls, 3);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('verifyInstalled falls back to the provider when fallowDigests is partial / malformed', async (t) => {
|
|
475
|
+
const { privateKey, rawPub } = makeKeypair();
|
|
476
|
+
const dir = makePlatformDir(privateKey);
|
|
477
|
+
t.after(() => cleanup(dir));
|
|
478
|
+
writeManifest(dir, {
|
|
479
|
+
name: '@fallow-cli/x',
|
|
480
|
+
version: '1.0.0',
|
|
481
|
+
fallowDigests: { fallow: 'not-a-real-digest' },
|
|
482
|
+
});
|
|
483
|
+
let providerCalls = 0;
|
|
484
|
+
const result = await verifyInstalled({
|
|
485
|
+
dirOverride: dir,
|
|
486
|
+
verifyFn: (p) => _verifyWithKey(p, rawPub),
|
|
487
|
+
digestProvider: ({ binaryPath }) => {
|
|
488
|
+
providerCalls += 1;
|
|
489
|
+
return Promise.resolve('sha256:' + crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex'));
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
assert.equal(result.ok, true);
|
|
493
|
+
// One call per binary because all three entries fail to normalize.
|
|
494
|
+
assert.equal(providerCalls, 3);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('verifyInstalled returns digest-mismatch when the embedded digest disagrees with the binary', async (t) => {
|
|
498
|
+
const { privateKey, rawPub } = makeKeypair();
|
|
499
|
+
const dir = makePlatformDir(privateKey);
|
|
500
|
+
t.after(() => cleanup(dir));
|
|
501
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
502
|
+
writeManifest(dir, {
|
|
503
|
+
name: '@fallow-cli/x',
|
|
504
|
+
version: '1.0.0',
|
|
505
|
+
fallowDigests: {
|
|
506
|
+
[`fallow${ext}`]: 'sha256:' + 'a'.repeat(64),
|
|
507
|
+
[`fallow-lsp${ext}`]: 'sha256:' + 'a'.repeat(64),
|
|
508
|
+
[`fallow-mcp${ext}`]: 'sha256:' + 'a'.repeat(64),
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
const result = await verifyInstalled({
|
|
512
|
+
dirOverride: dir,
|
|
513
|
+
verifyFn: (p) => _verifyWithKey(p, rawPub),
|
|
514
|
+
digestProvider: () => Promise.reject(new Error('should not be reached')),
|
|
515
|
+
});
|
|
516
|
+
assert.equal(result.ok, false);
|
|
517
|
+
assert.equal(result.code, 'digest-mismatch');
|
|
518
|
+
});
|
|
519
|
+
|
|
384
520
|
test('verifyInstalled ignores skip env when allowSkipEnv is false', async (t) => {
|
|
385
521
|
const previous = process.env[SKIP_ENV];
|
|
386
522
|
process.env[SKIP_ENV] = '1';
|
package/skills/fallow/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fallow
|
|
3
|
-
description: Codebase intelligence for JavaScript and TypeScript. Free static layer finds unused code (files, exports, types, dependencies), code duplication, circular dependencies, complexity hotspots, architecture boundary violations, and feature flag patterns. Runtime coverage merges production execution data into the same health report for hot-path review, cold-path deletion confidence, and stale-flag evidence - a single local capture is free, while continuous/cloud runtime monitoring is paid.
|
|
3
|
+
description: Codebase intelligence for JavaScript and TypeScript. Free static layer finds unused code (files, exports, types, dependencies), code duplication, circular dependencies, complexity hotspots, architecture boundary violations, and feature flag patterns. Runtime coverage merges production execution data into the same health report for hot-path review, cold-path deletion confidence, and stale-flag evidence - a single local capture is free, while continuous/cloud runtime monitoring is paid. 97 framework plugins, zero configuration, sub-second static analysis. Use when asked to analyze code health, find unused code, detect duplicates, check circular dependencies, audit complexity, check architecture boundaries, detect feature flags, clean up the codebase, auto-fix issues, merge runtime coverage, or run fallow.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: Bart Waardenburg
|
|
@@ -10,7 +10,7 @@ metadata:
|
|
|
10
10
|
|
|
11
11
|
# Fallow: codebase intelligence for JavaScript and TypeScript
|
|
12
12
|
|
|
13
|
-
Codebase intelligence for JavaScript and TypeScript. The free static layer finds unused code, circular dependencies, code duplication, complexity hotspots, architecture boundary violations, and feature flag patterns. Runtime coverage merges production execution data into the same `fallow health` report for hot-path review, cold-path deletion confidence, and stale-flag evidence: a single local capture is free, while continuous/cloud runtime monitoring is paid.
|
|
13
|
+
Codebase intelligence for JavaScript and TypeScript. The free static layer finds unused code, circular dependencies, code duplication, complexity hotspots, architecture boundary violations, and feature flag patterns. Runtime coverage merges production execution data into the same `fallow health` report for hot-path review, cold-path deletion confidence, and stale-flag evidence: a single local capture is free, while continuous/cloud runtime monitoring is paid. 97 framework plugins, zero configuration, sub-second static analysis.
|
|
14
14
|
|
|
15
15
|
## When to Use
|
|
16
16
|
|
|
@@ -218,7 +218,7 @@ fallow list --entry-points --format json --quiet
|
|
|
218
218
|
fallow list --plugins --format json --quiet
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
Shows detected entry points and active framework plugins (
|
|
221
|
+
Shows detected entry points and active framework plugins (97 built-in: Next.js, Vite, Ember, Jest, Storybook, Tailwind, PandaCSS, tap, tsd, etc.).
|
|
222
222
|
|
|
223
223
|
### Production-only analysis
|
|
224
224
|
|
|
@@ -323,7 +323,7 @@ When `--format json` is active and exit code is 2, errors are emitted as JSON on
|
|
|
323
323
|
|
|
324
324
|
## Configuration
|
|
325
325
|
|
|
326
|
-
Fallow reads config from project root: `.fallowrc.json` > `.fallowrc.jsonc` > `fallow.toml` > `.fallow.toml`. Both `.fallowrc.json` and `.fallowrc.jsonc` accept JSON-with-comments syntax (same parser); the `.jsonc` extension lets editors auto-detect JSONC syntax highlighting. Most projects work with zero configuration thanks to
|
|
326
|
+
Fallow reads config from project root: `.fallowrc.json` > `.fallowrc.jsonc` > `fallow.toml` > `.fallow.toml`. Both `.fallowrc.json` and `.fallowrc.jsonc` accept JSON-with-comments syntax (same parser); the `.jsonc` extension lets editors auto-detect JSONC syntax highlighting. Most projects work with zero configuration thanks to 97 auto-detecting framework plugins.
|
|
327
327
|
|
|
328
328
|
```jsonc
|
|
329
329
|
{
|
|
@@ -372,7 +372,7 @@ export const deprecatedHelper = () => {};
|
|
|
372
372
|
## Key Gotchas
|
|
373
373
|
|
|
374
374
|
- **`fix --yes` is required** in non-TTY (agent) environments. Without it, `fix` exits with code 2
|
|
375
|
-
- **Zero config by default.**
|
|
375
|
+
- **Zero config by default.** 97 framework plugins auto-detect, including tap and tsd test entry points. Don't create config unless customization is needed
|
|
376
376
|
- **Syntactic analysis only.** No TypeScript compiler, so fully dynamic `import(variable)` is not resolved
|
|
377
377
|
- **Function overloads are deduplicated.** TypeScript function overload signatures are merged into a single export (not reported as separate unused exports)
|
|
378
378
|
- **Re-export chains are resolved.** Exports through barrel files are tracked, not falsely flagged
|