repro-surgeon 0.1.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/CHANGELOG.md +15 -0
- package/CODE_OF_CONDUCT.md +7 -0
- package/CONTRIBUTING.md +31 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/SECURITY.md +15 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +165 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +177 -0
- package/dist/config.js.map +1 -0
- package/dist/dependencies.d.ts +37 -0
- package/dist/dependencies.js +237 -0
- package/dist/dependencies.js.map +1 -0
- package/dist/engine.d.ts +25 -0
- package/dist/engine.js +327 -0
- package/dist/engine.js.map +1 -0
- package/dist/export.d.ts +7 -0
- package/dist/export.js +520 -0
- package/dist/export.js.map +1 -0
- package/dist/graph.d.ts +3 -0
- package/dist/graph.js +247 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/oracle.d.ts +2 -0
- package/dist/oracle.js +81 -0
- package/dist/oracle.js.map +1 -0
- package/dist/protection.d.ts +2 -0
- package/dist/protection.js +6 -0
- package/dist/protection.js.map +1 -0
- package/dist/report.d.ts +5 -0
- package/dist/report.js +539 -0
- package/dist/report.js.map +1 -0
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +195 -0
- package/dist/runner.js.map +1 -0
- package/dist/snapshot.d.ts +11 -0
- package/dist/snapshot.js +220 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/state.d.ts +9 -0
- package/dist/state.js +143 -0
- package/dist/state.js.map +1 -0
- package/dist/transforms.d.ts +3 -0
- package/dist/transforms.js +188 -0
- package/dist/transforms.js.map +1 -0
- package/dist/types.d.ts +207 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/architecture.md +91 -0
- package/docs/assets/report.png +0 -0
- package/docs/configuration.md +73 -0
- package/docs/evidence/dependency-removal.json +109 -0
- package/docs/evidence/next-frameworks.json +271 -0
- package/docs/evidence/next-pages.json +152 -0
- package/docs/evidence/rounding.json +46 -0
- package/docs/report-surface.md +22 -0
- package/docs/troubleshooting.md +47 -0
- package/docs/validation.md +103 -0
- package/examples/next-container/README.md +7 -0
- package/examples/next-container/app/about/page.jsx +1 -0
- package/examples/next-container/app/case.css +5 -0
- package/examples/next-container/app/layout.jsx +3 -0
- package/examples/next-container/app/page.jsx +4 -0
- package/examples/next-container/lib/catalog.js +2 -0
- package/examples/next-container/next.config.mjs +1 -0
- package/examples/next-container/package-lock.json +952 -0
- package/examples/next-container/package.json +13 -0
- package/examples/next-container/repro-surgeon.json +37 -0
- package/examples/next-css-bom/README.md +7 -0
- package/examples/next-css-bom/app/about/page.jsx +1 -0
- package/examples/next-css-bom/app/case.css +3 -0
- package/examples/next-css-bom/app/layout.jsx +3 -0
- package/examples/next-css-bom/app/page.jsx +4 -0
- package/examples/next-css-bom/lib/catalog.js +2 -0
- package/examples/next-css-bom/next.config.mjs +1 -0
- package/examples/next-css-bom/package-lock.json +952 -0
- package/examples/next-css-bom/package.json +13 -0
- package/examples/next-css-bom/repro-surgeon.json +36 -0
- package/examples/next-pages/README.md +14 -0
- package/examples/next-pages/lib/unused-catalog.js +5 -0
- package/examples/next-pages/next.config.mjs +1 -0
- package/examples/next-pages/package-lock.json +952 -0
- package/examples/next-pages/package.json +11 -0
- package/examples/next-pages/pages/health.jsx +3 -0
- package/examples/next-pages/pages/index.jsx +9 -0
- package/examples/next-pages/repro-surgeon.json +18 -0
- package/examples/next-root-param/README.md +7 -0
- package/examples/next-root-param/app/[lang-country]/about/page.jsx +1 -0
- package/examples/next-root-param/app/[lang-country]/layout.tsx +4 -0
- package/examples/next-root-param/app/[lang-country]/page.tsx +3 -0
- package/examples/next-root-param/lib/catalog.js +2 -0
- package/examples/next-root-param/next-env.d.ts +2 -0
- package/examples/next-root-param/next.config.mjs +1 -0
- package/examples/next-root-param/package-lock.json +1016 -0
- package/examples/next-root-param/package.json +19 -0
- package/examples/next-root-param/repro-surgeon.json +37 -0
- package/examples/next-root-param/tsconfig.json +35 -0
- package/examples/rounding/README.md +16 -0
- package/examples/rounding/check.mjs +6 -0
- package/examples/rounding/config/store.json +7 -0
- package/examples/rounding/fixtures/invoice.json +11 -0
- package/examples/rounding/package-lock.json +7 -0
- package/examples/rounding/package.json +7 -0
- package/examples/rounding/repro-surgeon.json +12 -0
- package/examples/rounding/src/catalog.mjs +9 -0
- package/examples/rounding/src/shipping.mjs +7 -0
- package/examples/rounding/src/totals.mjs +11 -0
- package/package.json +29 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial public release of the complete local application-reduction workflow.
|
|
6
|
+
|
|
7
|
+
- Explicit argv commands and failure checks using exact exit codes, required output, and forbidden output.
|
|
8
|
+
- Repeated baseline, candidate and independent export checks; invalid executions never accepted.
|
|
9
|
+
- Hierarchical file removal, JavaScript/TypeScript/JSX syntax reductions, JSON reductions, and direct npm dependency removal with lock integrity checks.
|
|
10
|
+
- Immutable source snapshots, content-addressed checkpoints, cancellation and resumable search budgets.
|
|
11
|
+
- Single-package npm projects with a Next.js adapter.
|
|
12
|
+
- Standalone exported verifier and offline HTML/JSON evidence reports.
|
|
13
|
+
- Runnable examples, regression tests, and documented execution/privacy limits.
|
|
14
|
+
|
|
15
|
+
See [validation](docs/validation.md) for the exact release evidence and tested scope.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Code of conduct
|
|
2
|
+
|
|
3
|
+
Repro Surgeon welcomes contributors with different backgrounds and levels of experience. Treat people with respect, give specific constructive feedback, and discuss the work without attacking the person.
|
|
4
|
+
|
|
5
|
+
Harassment, threats, discriminatory language, sexual attention, disclosure of private information, and repeated disruption are unacceptable. Do not publish anyone's source, credentials or personal information without permission.
|
|
6
|
+
|
|
7
|
+
This policy applies to the project's issues, pull requests, discussions and other project spaces. Report concerns to **pavan.gupta.352@gmail.com**. The maintainer will review reports privately where possible, consider relevant context, and may remove content, restrict participation or ban participants. Decisions should be proportional to the conduct and protect the community's ability to collaborate.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
The most useful contribution is a case where the reduced export fails to preserve the configured observation, changes the original source, or cannot reproduce independently. Small licensed examples and precise failure checks help more than large log dumps.
|
|
4
|
+
|
|
5
|
+
## Run locally
|
|
6
|
+
|
|
7
|
+
Use Node.js 22.18 or newer and npm 10 or newer on Linux or macOS.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
git clone https://github.com/pavangupta352/repro-surgeon.git
|
|
11
|
+
cd repro-surgeon
|
|
12
|
+
npm ci
|
|
13
|
+
npm run check
|
|
14
|
+
node dist/cli.js --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Tests use Node's test runner with native TypeScript stripping. Type checking and the published JavaScript build use the pinned TypeScript compiler. Unit and subprocess tests do not need a registry dependency installation; the slower framework validation installs pinned packages separately.
|
|
18
|
+
|
|
19
|
+
## Make a change
|
|
20
|
+
|
|
21
|
+
Keep a pull request focused on an observable behavior. Add a regression that demonstrates a correctness bug before the fix, then run `npm run check`. For a new reduction, show both a preserved failure and a candidate that must be rejected. For execution changes, include cancellation, invalid setup, and process cleanup where relevant. For exported output, test the standalone verifier without this package installed.
|
|
22
|
+
|
|
23
|
+
The [architecture](docs/architecture.md) explains the acceptance contract. Never accept a candidate based only on an import graph or cached observation. Do not weaken the oracle to improve reduction numbers. Keep source and raw logs out of report HTML. Update the relevant documentation when behavior changes.
|
|
24
|
+
|
|
25
|
+
Run the rounding example through the packaged CLI before proposing release changes. UI changes need keyboard, narrow-screen, empty-state, and hostile-text checks. The generated report must keep working offline.
|
|
26
|
+
|
|
27
|
+
## Share a case
|
|
28
|
+
|
|
29
|
+
Include the tool, Node, npm and operating-system versions; the argv command; the required and forbidden diagnostic fragments; and the difference between expected and observed behavior. Review your reproduction and reports for credentials and private information before attaching them. A run directory can contain the original snapshot and raw logs; do not upload the whole directory by default.
|
|
30
|
+
|
|
31
|
+
Only contribute source you have the right to license under this repository's MIT license. Preserve existing notices. By submitting a contribution, you agree it may be distributed under that license. Be considerate of maintainers' time and follow the [code of conduct](CODE_OF_CONDUCT.md).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pavan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Repro Surgeon
|
|
2
|
+
|
|
3
|
+
**Shrink the project. Keep the failure. Hand over something runnable.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/pavangupta352/repro-surgeon/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
“Can you provide a minimal reproduction?” is often the hardest part of a bug report.
|
|
9
|
+
|
|
10
|
+
Repro Surgeon takes a failing npm application, removes source that isn't needed for your failure check, and exports a smaller project with its own verifier. It tests every accepted change and checks the export again with a fresh dependency installation. Your original project stays in place.
|
|
11
|
+
|
|
12
|
+
No account. No telemetry. Source and reports stay local.
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
*Actual bundled demonstration: 3,173 → 520 source bytes, 10 → 5 files, 115 evaluations. The assertion remains pinned. This is a small seeded example, not a framework benchmark. [Method and evidence](docs/validation.md).*
|
|
17
|
+
|
|
18
|
+
## Try it
|
|
19
|
+
|
|
20
|
+
Requires **Node.js 22.18+ and npm 10+**, on Linux or macOS.
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
git clone https://github.com/pavangupta352/repro-surgeon.git
|
|
24
|
+
cd repro-surgeon
|
|
25
|
+
npm ci
|
|
26
|
+
npm run build
|
|
27
|
+
|
|
28
|
+
node dist/cli.js reduce examples/rounding --out /tmp/rounding-repro
|
|
29
|
+
node /tmp/rounding-repro/repro/.repro/verify.mjs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Choose a new output directory for each run. Open `/tmp/rounding-repro/report.html` to inspect the result. The verifier exits successfully when the application's **expected failure** occurs; the original application command still exits with its configured failure code.
|
|
33
|
+
|
|
34
|
+
Install the packaged release for your own projects:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install --global repro-surgeon@0.1.0
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The same package is available from the [versioned GitHub release](https://github.com/pavangupta352/repro-surgeon/releases/tag/v0.1.0), with a checksum. You can also run `npx repro-surgeon@0.1.0 --help`.
|
|
41
|
+
|
|
42
|
+
## Reduce your application
|
|
43
|
+
|
|
44
|
+
Start from a project whose failure reproduces with a finite command and a committed npm lockfile.
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
repro-surgeon init ./my-app \
|
|
48
|
+
--match "the distinctive diagnostic from your failure" \
|
|
49
|
+
-- npm run build
|
|
50
|
+
|
|
51
|
+
repro-surgeon doctor ./my-app --json
|
|
52
|
+
repro-surgeon reduce ./my-app --out ./my-app-repro
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Replace the example diagnostic with text from your actual error. Use several `--match` fragments to identify it precisely, and `--forbid` to reject a known competing error. Pin your test harness or other essential source with `preserve` in `repro-surgeon.json`.
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"version": 1,
|
|
60
|
+
"command": ["npm", "run", "build"],
|
|
61
|
+
"oracle": {
|
|
62
|
+
"exitCode": 1,
|
|
63
|
+
"allOf": ["Parsing CSS source code failed", "Unexpected end of input"],
|
|
64
|
+
"noneOf": ["Module not found"]
|
|
65
|
+
},
|
|
66
|
+
"preserve": ["scripts/check.mjs"],
|
|
67
|
+
"budget": { "maxEvaluations": 200, "maxSeconds": 900 }
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The failure check is the contract. A broad phrase such as `failed` can match the wrong problem. Try a fixed control and a deliberately different failure before starting an expensive run. Repeated matching output does not establish identical semantic root cause.
|
|
72
|
+
|
|
73
|
+
## What you get
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
my-app-repro/
|
|
77
|
+
├── repro/ # Reduced application to inspect and share
|
|
78
|
+
│ ├── package.json
|
|
79
|
+
│ ├── package-lock.json
|
|
80
|
+
│ ├── … retained source
|
|
81
|
+
│ └── .repro/
|
|
82
|
+
│ ├── README.md # Runtime, command and verification instructions
|
|
83
|
+
│ ├── config.json
|
|
84
|
+
│ ├── LICENSE # License for the generated verifier code
|
|
85
|
+
│ ├── manifest.json # Source and metadata integrity records
|
|
86
|
+
│ └── verify.mjs # Runs without Repro Surgeon installed
|
|
87
|
+
├── report.html # Offline, searchable evidence
|
|
88
|
+
├── report.json # Machine-readable report
|
|
89
|
+
└── … private run state # Original snapshots and raw logs; keep private
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The report shows accepted, rejected and invalid trials, before/after metrics, remaining files, the exact failure check and the fresh verification outcome. Search and filters work offline.
|
|
93
|
+
|
|
94
|
+
**Review the `repro/` folder before sharing it.** Exclusions and review findings help locate sensitive material; they do not certify that a reproduction is safe to publish. The full run directory contains original source and logs.
|
|
95
|
+
|
|
96
|
+
## How the reduction works
|
|
97
|
+
|
|
98
|
+
1. **Establish the failure.** Three uncached baseline observations must match.
|
|
99
|
+
2. **Try smaller source.** Remove file groups and complements, syntax elements, JSON fields and direct npm dependencies. Keep retained dependency versions and integrity records fixed.
|
|
100
|
+
3. **Confirm every accepted candidate.** Require two matching executions. Timeouts, signals, installation failures, output overflow and spawn failures are invalid, never evidence of preservation.
|
|
101
|
+
4. **Export and verify independently.** Copy the result into a separate temporary directory, install dependencies afresh and check three times. The standalone verifier checks the exported file manifest as well.
|
|
102
|
+
|
|
103
|
+
The search seeks fewer source bytes, then fewer files, fewer declared dependencies and fewer total bytes. It finds the smallest candidate reached by the enabled passes within your budget; it does not promise a globally minimal program.
|
|
104
|
+
|
|
105
|
+
## Stop, resume and inspect
|
|
106
|
+
|
|
107
|
+
Press Ctrl-C to checkpoint the accepted snapshot and stop child processes.
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
repro-surgeon resume ./my-app-repro --max-evaluations 500 --max-seconds 1800
|
|
111
|
+
repro-surgeon verify ./my-app-repro/repro
|
|
112
|
+
repro-surgeon report ./my-app-repro
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Resume budgets are **new total limits**, including earlier search time and evaluations. After a normal search budget stop, the current best result still gets a separately bounded export verification. A baseline interrupted before calibration completes remains paused. Resume requires the same tool, Node, npm, platform and architecture. See [troubleshooting](docs/troubleshooting.md) for crash locks and incompatible environments.
|
|
116
|
+
|
|
117
|
+
## Supported scope
|
|
118
|
+
|
|
119
|
+
| Works with | Boundary |
|
|
120
|
+
|---|---|
|
|
121
|
+
| Single-package npm applications | `package-lock.json` version 2 or 3; public portable registry dependencies |
|
|
122
|
+
| Deterministic command and build failures | Explicit argv, exit code, required and forbidden literal text |
|
|
123
|
+
| Next.js application structure | App/Pages Router detection and conservative entrypoint protection |
|
|
124
|
+
| JavaScript, TypeScript, JSX, TSX and JSON | Syntax-aware source edits and JSON/JSONC field removal |
|
|
125
|
+
| Linux and macOS | Runtime/OS matrix in CI; Windows is not yet supported |
|
|
126
|
+
|
|
127
|
+
npm workspaces, pnpm/Yarn lockfiles, shrinkwrap, local/Git/private dependencies, browser recording, and managed service startup are outside this release. Existing syntax errors can be preserved by file reduction; malformed files are skipped by structural passes. [Full configuration](docs/configuration.md) · [Architecture](docs/architecture.md) · [Validation](docs/validation.md).
|
|
128
|
+
|
|
129
|
+
## Local execution, plainly
|
|
130
|
+
|
|
131
|
+
Commands execute with your host permissions. This is **not a security sandbox**. Run trusted source, or put the entire workflow inside your own isolated environment. Install scripts are disabled unless you enable them. Network access is required for uncached dependencies; your command may also use the network. Common credentials, environment files, generated output and symlinks are excluded. Only explicitly requested environment variables are inherited beyond operating-system essentials. [Security policy](SECURITY.md).
|
|
132
|
+
|
|
133
|
+
## Run the checks
|
|
134
|
+
|
|
135
|
+
From the repository root:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
npm ci
|
|
139
|
+
npm run check
|
|
140
|
+
npm run test:package
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The native suite does not fetch framework fixture dependencies. Run these optional integrations separately; they download public pinned packages and the Next.js checks take several minutes:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
node scripts/validate-dependency.mjs /tmp/repro-dependency-validation
|
|
147
|
+
node scripts/validate-pages.mjs /tmp/repro-pages-validation
|
|
148
|
+
node scripts/validate-next.mjs /tmp/repro-framework-validation
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Choose new output directories. The scripts check fixed and different-error controls, preserve the original source, and verify the reduced exports. [Measured results and limitations](docs/validation.md).
|
|
152
|
+
|
|
153
|
+
## Related work and contributions
|
|
154
|
+
|
|
155
|
+
This builds on [delta debugging](https://www.st.cs.uni-saarland.de/papers/tse2002/) and shares the goal of property-preserving reduction with [treereduce](https://github.com/langston-barrett/treereduce). [Replay](https://www.replay.io/debugging) addresses recorded execution. Repro Surgeon focuses on the path from an application source tree to an independently installable, verified reproduction. No comparative superiority is claimed.
|
|
156
|
+
|
|
157
|
+
Have a failure this cannot reduce reliably? A small licensed case with a precise failure check is especially useful. Read [CONTRIBUTING.md](CONTRIBUTING.md), open an [issue](https://github.com/pavangupta352/repro-surgeon/issues), or improve a reducer with a regression test.
|
|
158
|
+
|
|
159
|
+
Maintained by [Pavan](https://github.com/pavangupta352). [MIT licensed](LICENSE).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
Report a suspected vulnerability privately to **pavan.gupta.352@gmail.com**. Include the affected version, impact, and a small reproduction using non-sensitive data. Do not disclose credentials or another person's private application source. Please give the maintainer an opportunity to investigate before public disclosure.
|
|
4
|
+
|
|
5
|
+
The latest published release receives security fixes. This is a small open-source project; no response-time guarantee is offered.
|
|
6
|
+
|
|
7
|
+
## Execution boundary
|
|
8
|
+
|
|
9
|
+
Repro Surgeon runs the command you supply with your operating-system permissions. It is **not a security sandbox**. Use it only with projects and commands you trust, or run the whole workflow in your own appropriately isolated environment. A child process can access files and the network according to those host permissions even though the reducer itself never runs inside the original project.
|
|
10
|
+
|
|
11
|
+
Installation scripts are disabled by default. Enabling `execution.allowInstallScripts` permits package lifecycle code to run. Dependencies are fetched from the configured supported public registry locations. Repro Surgeon does not operate a source-upload service and includes no telemetry.
|
|
12
|
+
|
|
13
|
+
Run directories contain original source snapshots, reduced source, private logs, environment variable names and evidence. Keep them private. The tool excludes common credential files and scans retained content for review findings, but those checks are not complete secret detection. Review the exported source, commands, paths, diagnostic fragments, report and package metadata before sharing. The complete run directory is not the shareable artifact.
|
|
14
|
+
|
|
15
|
+
The standalone verifier is executable source. Recipients should inspect a reproduction before running it, just as they would inspect any unfamiliar project. A matching failure check establishes the configured observation; it does not prove that source is benign or that two failures have the same root cause.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseArgs, stripVTControlCharacters } from 'node:util';
|
|
5
|
+
import { loadConfig, parseConfig } from "./config.js";
|
|
6
|
+
import { runtimeInfo, validateNpmProject } from "./dependencies.js";
|
|
7
|
+
import { reduceProject, resumeProject } from "./engine.js";
|
|
8
|
+
import { exportReproduction, verifyReproduction } from "./export.js";
|
|
9
|
+
import { detectAdapter } from "./graph.js";
|
|
10
|
+
import { sanitizeReport, writeReport } from "./report.js";
|
|
11
|
+
import { inventoryProject, metrics } from "./snapshot.js";
|
|
12
|
+
import { loadCheckpoint } from "./state.js";
|
|
13
|
+
const help = `Repro Surgeon · smaller source, the failure intact
|
|
14
|
+
|
|
15
|
+
Usage
|
|
16
|
+
repro-surgeon init [project] --match "distinctive diagnostic" -- <command> [args]
|
|
17
|
+
repro-surgeon doctor [project] [--json]
|
|
18
|
+
repro-surgeon reduce [project] --out <run-directory> [--config <file>]
|
|
19
|
+
repro-surgeon resume <run-directory> [--max-evaluations <n>] [--max-seconds <n>]
|
|
20
|
+
repro-surgeon verify <exported-repro> [--json]
|
|
21
|
+
repro-surgeon report <run-directory> [--json]
|
|
22
|
+
|
|
23
|
+
Options
|
|
24
|
+
--match <text> Required output fragment; repeat for multiple signals
|
|
25
|
+
--forbid <text> Forbidden competing diagnostic; repeat as needed
|
|
26
|
+
--exit <number> Expected failure exit code (default 1)
|
|
27
|
+
--config <file> Configuration path (default project/repro-surgeon.json)
|
|
28
|
+
--out <directory> New run directory outside the source tree
|
|
29
|
+
--max-evaluations <n> Search evaluation budget
|
|
30
|
+
--max-seconds <n> Total search time budget
|
|
31
|
+
--json Machine-readable result; progress stays on stderr
|
|
32
|
+
--help, -h Show this help
|
|
33
|
+
--version, -v Show the installed version
|
|
34
|
+
|
|
35
|
+
Single-package npm projects · Node 22.18+ · Linux/macOS
|
|
36
|
+
Commands execute locally with your permissions. Review remaining source before sharing.
|
|
37
|
+
`;
|
|
38
|
+
export function createRunReport(state, snapshot) {
|
|
39
|
+
return sanitizeReport({
|
|
40
|
+
version: 1, id: state.id, name: state.config.name, createdAt: state.createdAt,
|
|
41
|
+
status: state.status, stopReason: state.stopReason,
|
|
42
|
+
command: state.config.command, oracle: state.config.oracle, runtime: state.runtime, adapter: state.adapter,
|
|
43
|
+
initial: state.initial, current: state.current, evaluations: state.evaluations, elapsedMs: state.elapsedMs,
|
|
44
|
+
baseline: { completed: state.baseline.filter(observation => observation.status === 'reproduced').length, required: state.config.runs.baseline },
|
|
45
|
+
trials: state.trials,
|
|
46
|
+
files: [...snapshot].sort(([a], [b]) => a.localeCompare(b, 'en')).map(([name, file]) => ({ path: name, bytes: file.content.length })),
|
|
47
|
+
excluded: state.excluded, warnings: state.warnings,
|
|
48
|
+
verification: state.verification,
|
|
49
|
+
reviewFindings: state.reviewFindings,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function terminal(message) { process.stderr.write(stripVTControlCharacters(message) + '\n'); }
|
|
53
|
+
async function main(args) {
|
|
54
|
+
const separator = args.indexOf('--');
|
|
55
|
+
const invocation = separator < 0 ? [] : args.slice(separator + 1);
|
|
56
|
+
const { values, positionals } = parseArgs({ args: separator < 0 ? args : args.slice(0, separator), strict: true, allowPositionals: true, options: {
|
|
57
|
+
help: { type: 'boolean', short: 'h' }, version: { type: 'boolean', short: 'v' }, json: { type: 'boolean' },
|
|
58
|
+
match: { type: 'string', multiple: true }, forbid: { type: 'string', multiple: true }, exit: { type: 'string' },
|
|
59
|
+
config: { type: 'string' }, out: { type: 'string' }, 'max-evaluations': { type: 'string' }, 'max-seconds': { type: 'string' },
|
|
60
|
+
} });
|
|
61
|
+
if (values.help || args.length === 0) {
|
|
62
|
+
process.stdout.write(help);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
if (values.version) {
|
|
66
|
+
process.stdout.write('0.1.0\n');
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
const [command, target, ...extra] = positionals;
|
|
70
|
+
if (extra.length)
|
|
71
|
+
throw new Error('Too many positional arguments. See --help.');
|
|
72
|
+
if (process.platform === 'win32')
|
|
73
|
+
throw new Error('Windows execution is not validated yet. Use Linux or macOS.');
|
|
74
|
+
const directory = path.resolve(target ?? process.cwd());
|
|
75
|
+
const print = (value, plain) => { process.stdout.write(values.json ? JSON.stringify(value, null, 2) + '\n' : plain + '\n'); };
|
|
76
|
+
if (command === 'init') {
|
|
77
|
+
const config = parseConfig({ name: path.basename(directory), command: invocation, oracle: { exitCode: values.exit === undefined ? 1 : Number(values.exit), allOf: values.match ?? [], noneOf: values.forbid ?? [] } });
|
|
78
|
+
const destination = path.resolve(values.config ?? path.join(directory, 'repro-surgeon.json'));
|
|
79
|
+
await readFile(path.join(directory, 'package.json'));
|
|
80
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
81
|
+
await writeFile(destination, JSON.stringify({ version: config.version, name: config.name, command: config.command, oracle: config.oracle, adapter: config.adapter }, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
82
|
+
print({ config: destination }, `Created ${destination}\nRun repro-surgeon doctor, then repro-surgeon reduce.`);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
if (invocation.length)
|
|
86
|
+
throw new Error('Arguments after -- are supported only by init. Store the command in your configuration.');
|
|
87
|
+
if (command === 'doctor') {
|
|
88
|
+
let config;
|
|
89
|
+
try {
|
|
90
|
+
config = await loadConfig(path.resolve(values.config ?? path.join(directory, 'repro-surgeon.json')));
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (values.config || error.code !== 'ENOENT')
|
|
94
|
+
throw error;
|
|
95
|
+
config = { include: [], exclude: [], adapter: 'auto' };
|
|
96
|
+
}
|
|
97
|
+
const inventory = await inventoryProject(directory, config);
|
|
98
|
+
validateNpmProject(inventory.snapshot);
|
|
99
|
+
const summary = { adapter: detectAdapter(inventory.snapshot, config.adapter), metrics: metrics(inventory.snapshot), runtime: await runtimeInfo(directory), excluded: inventory.excluded, warnings: inventory.warnings };
|
|
100
|
+
print(summary, `${summary.adapter.name} project · ${summary.metrics.files} files · ${summary.metrics.dependencies} dependencies\nInput checks passed. No project command was executed.\n${summary.excluded.length} paths excluded; inspect --json for details.`);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const interrupt = () => controller.abort();
|
|
105
|
+
process.once('SIGINT', interrupt);
|
|
106
|
+
process.once('SIGTERM', interrupt);
|
|
107
|
+
try {
|
|
108
|
+
if (command === 'verify') {
|
|
109
|
+
if (!target)
|
|
110
|
+
throw new Error('verify requires the exported reproduction directory.');
|
|
111
|
+
const verification = await verifyReproduction(directory, { signal: controller.signal });
|
|
112
|
+
print(verification, `${verification.status}: ${verification.reason}`);
|
|
113
|
+
return verification.status === 'verified' ? 0 : 1;
|
|
114
|
+
}
|
|
115
|
+
if (command === 'report') {
|
|
116
|
+
if (!target)
|
|
117
|
+
throw new Error('report requires a run directory.');
|
|
118
|
+
const { state, snapshot } = await loadCheckpoint(directory);
|
|
119
|
+
const report = createRunReport(state, snapshot);
|
|
120
|
+
await writeReport(report, directory);
|
|
121
|
+
print(report, `Report: ${path.join(directory, 'report.html')}`);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
if (command !== 'reduce' && command !== 'resume')
|
|
125
|
+
throw new Error(`Unknown command: ${command ?? '(none)'}. See --help.`);
|
|
126
|
+
if (command === 'resume' && !target)
|
|
127
|
+
throw new Error('resume requires a run directory.');
|
|
128
|
+
const onEvent = (event) => terminal(event.message);
|
|
129
|
+
let result;
|
|
130
|
+
let runRoot;
|
|
131
|
+
if (command === 'reduce') {
|
|
132
|
+
const config = await loadConfig(path.resolve(values.config ?? path.join(directory, 'repro-surgeon.json')));
|
|
133
|
+
if (values['max-evaluations'] !== undefined)
|
|
134
|
+
config.budget.maxEvaluations = Number(values['max-evaluations']);
|
|
135
|
+
if (values['max-seconds'] !== undefined)
|
|
136
|
+
config.budget.maxSeconds = Number(values['max-seconds']);
|
|
137
|
+
runRoot = path.resolve(values.out ?? path.join(path.dirname(directory), `${path.basename(directory)}-repro-${Date.now()}`));
|
|
138
|
+
result = await reduceProject({ sourceRoot: directory, runRoot, config, signal: controller.signal, onEvent });
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
runRoot = directory;
|
|
142
|
+
result = await resumeProject({ runRoot, signal: controller.signal, onEvent, ...(values['max-evaluations'] === undefined ? {} : { maxEvaluations: Number(values['max-evaluations']) }), ...(values['max-seconds'] === undefined ? {} : { maxSeconds: Number(values['max-seconds']) }) });
|
|
143
|
+
}
|
|
144
|
+
if (result.state.status === 'verifying') {
|
|
145
|
+
terminal('Checking the exported source with a fresh dependency installation…');
|
|
146
|
+
result.state = await exportReproduction(runRoot, result.state, result.snapshot, { signal: controller.signal });
|
|
147
|
+
}
|
|
148
|
+
const report = createRunReport(result.state, result.snapshot);
|
|
149
|
+
await writeReport(report, runRoot);
|
|
150
|
+
print(report, `${report.initial.files} → ${report.current.files} files · ${report.verification.status}\nReproduction: ${path.join(runRoot, 'repro')}\nReport: ${path.join(runRoot, 'report.html')}\nReview the remaining source before sharing.`);
|
|
151
|
+
if (controller.signal.aborted)
|
|
152
|
+
return 130;
|
|
153
|
+
return report.verification.status === 'verified' ? 0 : 1;
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
process.removeListener('SIGINT', interrupt);
|
|
157
|
+
process.removeListener('SIGTERM', interrupt);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
void main(process.argv.slice(2)).then(code => { process.exitCode = code; }).catch((error) => {
|
|
161
|
+
const message = error.code === 'EEXIST' ? 'Configuration already exists. Edit it or choose another --config path.' : error instanceof Error ? error.message : String(error);
|
|
162
|
+
terminal(`Repro Surgeon: ${message}`);
|
|
163
|
+
process.exitCode = 2;
|
|
164
|
+
});
|
|
165
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
const DEFAULTS = {
|
|
3
|
+
name: 'reproduction',
|
|
4
|
+
adapter: 'auto',
|
|
5
|
+
budget: { maxEvaluations: 200, maxSeconds: 900 },
|
|
6
|
+
runs: { baseline: 3, candidate: 2, final: 3 },
|
|
7
|
+
execution: {
|
|
8
|
+
timeoutMs: 60_000,
|
|
9
|
+
maxOutputBytes: 1_048_576,
|
|
10
|
+
installTimeoutMs: 120_000,
|
|
11
|
+
allowInstallScripts: false,
|
|
12
|
+
env: [],
|
|
13
|
+
},
|
|
14
|
+
reduce: { files: true, syntax: true, json: true, dependencies: true },
|
|
15
|
+
};
|
|
16
|
+
function record(value, path) {
|
|
17
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
18
|
+
throw new TypeError(`${path} must be an object`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function rejectUnknown(value, allowed, path) {
|
|
23
|
+
const allowedFields = new Set(allowed);
|
|
24
|
+
for (const key of Object.keys(value)) {
|
|
25
|
+
if (!allowedFields.has(key))
|
|
26
|
+
throw new TypeError(`Unknown field ${path}.${key}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function text(value, path) {
|
|
30
|
+
if (typeof value !== 'string' || value.trim().length === 0 || value.includes('\0')) {
|
|
31
|
+
throw new TypeError(`${path} must be a non-empty string without NUL bytes`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function commandArray(value) {
|
|
36
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
37
|
+
throw new TypeError('config.command must be a non-empty array of argv strings');
|
|
38
|
+
}
|
|
39
|
+
const command = value.map((item, index) => {
|
|
40
|
+
if (typeof item !== 'string' || item.includes('\0')) {
|
|
41
|
+
throw new TypeError(`config.command[${index}] must be a string without NUL bytes`);
|
|
42
|
+
}
|
|
43
|
+
return item;
|
|
44
|
+
});
|
|
45
|
+
if (command[0].trim().length === 0) {
|
|
46
|
+
throw new TypeError('config.command executable must be a non-empty string');
|
|
47
|
+
}
|
|
48
|
+
return command;
|
|
49
|
+
}
|
|
50
|
+
function stringArray(value, path, allowEmpty) {
|
|
51
|
+
if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) {
|
|
52
|
+
throw new TypeError(`${path} must be ${allowEmpty ? 'an' : 'a non-empty'} array of strings`);
|
|
53
|
+
}
|
|
54
|
+
return value.map((item, index) => text(item, `${path}[${index}]`));
|
|
55
|
+
}
|
|
56
|
+
function integer(value, path, minimum, maximum = Number.MAX_SAFE_INTEGER) {
|
|
57
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
58
|
+
throw new TypeError(`${path} must be a safe integer between ${minimum} and ${maximum}`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function boolean(value, path) {
|
|
63
|
+
if (typeof value !== 'boolean')
|
|
64
|
+
throw new TypeError(`${path} must be a boolean`);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function optionalRecord(parent, key) {
|
|
68
|
+
return parent[key] === undefined ? {} : record(parent[key], key);
|
|
69
|
+
}
|
|
70
|
+
function parseOracle(value) {
|
|
71
|
+
const input = record(value, 'oracle');
|
|
72
|
+
rejectUnknown(input, ['exitCode', 'allOf', 'noneOf'], 'oracle');
|
|
73
|
+
if (input.exitCode === undefined)
|
|
74
|
+
throw new TypeError('oracle.exitCode must be provided');
|
|
75
|
+
if (input.allOf === undefined)
|
|
76
|
+
throw new TypeError('oracle.allOf target must be provided');
|
|
77
|
+
return {
|
|
78
|
+
exitCode: integer(input.exitCode, 'oracle.exitCode', 0, 255),
|
|
79
|
+
allOf: stringArray(input.allOf, 'oracle.allOf target', false),
|
|
80
|
+
noneOf: input.noneOf === undefined ? [] : stringArray(input.noneOf, 'oracle.noneOf', true),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function parseConfig(value) {
|
|
84
|
+
const input = record(value, 'config');
|
|
85
|
+
rejectUnknown(input, ['version', 'name', 'command', 'oracle', 'adapter', 'budget', 'runs', 'execution', 'reduce', 'include', 'exclude', 'preserve'], 'config');
|
|
86
|
+
if (input.command === undefined)
|
|
87
|
+
throw new TypeError('config.command must be provided');
|
|
88
|
+
if (input.oracle === undefined)
|
|
89
|
+
throw new TypeError('config.oracle target must be provided');
|
|
90
|
+
if (input.version !== undefined && input.version !== 1)
|
|
91
|
+
throw new TypeError('config.version must be 1');
|
|
92
|
+
const budget = optionalRecord(input, 'budget');
|
|
93
|
+
rejectUnknown(budget, ['maxEvaluations', 'maxSeconds'], 'budget');
|
|
94
|
+
const runs = optionalRecord(input, 'runs');
|
|
95
|
+
rejectUnknown(runs, ['baseline', 'candidate', 'final'], 'runs');
|
|
96
|
+
const execution = optionalRecord(input, 'execution');
|
|
97
|
+
rejectUnknown(execution, ['timeoutMs', 'maxOutputBytes', 'installTimeoutMs', 'allowInstallScripts', 'env'], 'execution');
|
|
98
|
+
const reduce = optionalRecord(input, 'reduce');
|
|
99
|
+
rejectUnknown(reduce, ['files', 'syntax', 'json', 'dependencies'], 'reduce');
|
|
100
|
+
const adapter = input.adapter ?? DEFAULTS.adapter;
|
|
101
|
+
if (adapter !== 'auto' && adapter !== 'next' && adapter !== 'generic') {
|
|
102
|
+
throw new TypeError('config.adapter must be auto, next, or generic');
|
|
103
|
+
}
|
|
104
|
+
const environment = execution.env === undefined
|
|
105
|
+
? []
|
|
106
|
+
: stringArray(execution.env, 'execution.env', true);
|
|
107
|
+
for (const name of environment) {
|
|
108
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
109
|
+
throw new TypeError(`execution.env contains invalid environment variable name: ${name}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
version: 1,
|
|
114
|
+
name: input.name === undefined ? DEFAULTS.name : text(input.name, 'config.name'),
|
|
115
|
+
command: commandArray(input.command),
|
|
116
|
+
oracle: parseOracle(input.oracle),
|
|
117
|
+
adapter,
|
|
118
|
+
budget: {
|
|
119
|
+
maxEvaluations: budget.maxEvaluations === undefined
|
|
120
|
+
? DEFAULTS.budget.maxEvaluations
|
|
121
|
+
: integer(budget.maxEvaluations, 'budget.maxEvaluations', 1),
|
|
122
|
+
maxSeconds: budget.maxSeconds === undefined
|
|
123
|
+
? DEFAULTS.budget.maxSeconds
|
|
124
|
+
: integer(budget.maxSeconds, 'budget.maxSeconds', 1),
|
|
125
|
+
},
|
|
126
|
+
runs: {
|
|
127
|
+
baseline: runs.baseline === undefined
|
|
128
|
+
? DEFAULTS.runs.baseline
|
|
129
|
+
: integer(runs.baseline, 'runs.baseline', DEFAULTS.runs.baseline),
|
|
130
|
+
candidate: runs.candidate === undefined
|
|
131
|
+
? DEFAULTS.runs.candidate
|
|
132
|
+
: integer(runs.candidate, 'runs.candidate', DEFAULTS.runs.candidate),
|
|
133
|
+
final: runs.final === undefined
|
|
134
|
+
? DEFAULTS.runs.final
|
|
135
|
+
: integer(runs.final, 'runs.final', DEFAULTS.runs.final),
|
|
136
|
+
},
|
|
137
|
+
execution: {
|
|
138
|
+
timeoutMs: execution.timeoutMs === undefined
|
|
139
|
+
? DEFAULTS.execution.timeoutMs
|
|
140
|
+
: integer(execution.timeoutMs, 'execution.timeoutMs', 1),
|
|
141
|
+
maxOutputBytes: execution.maxOutputBytes === undefined
|
|
142
|
+
? DEFAULTS.execution.maxOutputBytes
|
|
143
|
+
: integer(execution.maxOutputBytes, 'execution.maxOutputBytes', 1),
|
|
144
|
+
installTimeoutMs: execution.installTimeoutMs === undefined
|
|
145
|
+
? DEFAULTS.execution.installTimeoutMs
|
|
146
|
+
: integer(execution.installTimeoutMs, 'execution.installTimeoutMs', 1),
|
|
147
|
+
allowInstallScripts: execution.allowInstallScripts === undefined
|
|
148
|
+
? DEFAULTS.execution.allowInstallScripts
|
|
149
|
+
: boolean(execution.allowInstallScripts, 'execution.allowInstallScripts'),
|
|
150
|
+
env: [...new Set(environment)],
|
|
151
|
+
},
|
|
152
|
+
reduce: {
|
|
153
|
+
files: reduce.files === undefined ? DEFAULTS.reduce.files : boolean(reduce.files, 'reduce.files'),
|
|
154
|
+
syntax: reduce.syntax === undefined ? DEFAULTS.reduce.syntax : boolean(reduce.syntax, 'reduce.syntax'),
|
|
155
|
+
json: reduce.json === undefined ? DEFAULTS.reduce.json : boolean(reduce.json, 'reduce.json'),
|
|
156
|
+
dependencies: reduce.dependencies === undefined
|
|
157
|
+
? DEFAULTS.reduce.dependencies
|
|
158
|
+
: boolean(reduce.dependencies, 'reduce.dependencies'),
|
|
159
|
+
},
|
|
160
|
+
include: input.include === undefined ? [] : stringArray(input.include, 'config.include', true),
|
|
161
|
+
exclude: input.exclude === undefined ? [] : stringArray(input.exclude, 'config.exclude', true),
|
|
162
|
+
preserve: input.preserve === undefined ? [] : stringArray(input.preserve, 'config.preserve', true),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export async function loadConfig(path) {
|
|
166
|
+
const source = await readFile(path, 'utf8');
|
|
167
|
+
let value;
|
|
168
|
+
try {
|
|
169
|
+
value = JSON.parse(source);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
+
throw new SyntaxError(`Invalid JSON in ${path}: ${message}`);
|
|
174
|
+
}
|
|
175
|
+
return parseConfig(value);
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=config.js.map
|