safeinstall-cli 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 +34 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/SUPPORT.md +42 -0
- package/dist/async.js +26 -0
- package/dist/check-flow.js +160 -0
- package/dist/cli-options.js +15 -0
- package/dist/cli.js +97 -0
- package/dist/config.js +129 -0
- package/dist/disk-cache.js +67 -0
- package/dist/evaluations.js +27 -0
- package/dist/init-flow.js +55 -0
- package/dist/install-flow.js +274 -0
- package/dist/output.js +93 -0
- package/dist/package-managers.js +98 -0
- package/dist/policy.js +83 -0
- package/dist/project-discovery.js +90 -0
- package/dist/project-installs/npm.js +93 -0
- package/dist/project-installs/pnpm.js +166 -0
- package/dist/project-installs/shared.js +62 -0
- package/dist/project-installs/types.js +2 -0
- package/dist/project-installs.js +60 -0
- package/dist/project-state.js +101 -0
- package/dist/registry.js +239 -0
- package/dist/signals.js +55 -0
- package/dist/specs.js +185 -0
- package/dist/types.js +2 -0
- package/package.json +60 -0
- package/safeinstall.config.example.json +21 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-03-31
|
|
4
|
+
|
|
5
|
+
First public release. Open source under MIT license.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Local CLI wrapper for `npm`, `pnpm`, and `bun`
|
|
10
|
+
- Policy enforcement for release age, lifecycle scripts, untrusted sources, and trust downgrades
|
|
11
|
+
- Lockfile-aware project installs for `pnpm install`, `npm install`, and `npm ci`
|
|
12
|
+
- JSON output for CI and automation
|
|
13
|
+
- `safeinstall init` starter config generation
|
|
14
|
+
- Monorepo-aware package and lockfile discovery for npm and pnpm
|
|
15
|
+
- End-to-end CLI regression coverage and local-repo QA against real projects
|
|
16
|
+
- Configurable registry URL for private mirrors (Verdaccio, Artifactory)
|
|
17
|
+
- On-disk cache for exact-version registry metadata and publish timestamps
|
|
18
|
+
- Graceful SIGINT/SIGTERM shutdown with proper exit codes
|
|
19
|
+
- `--help` / `-h` and `--version` / `-v` flags
|
|
20
|
+
|
|
21
|
+
### Hardened
|
|
22
|
+
|
|
23
|
+
- Fail-closed behavior for stale, missing, or ambiguous lockfiles
|
|
24
|
+
- Explicit blocking for ambiguous workspace-targeting flags
|
|
25
|
+
- Package-manager mismatch blocking when `package.json` declares a different manager
|
|
26
|
+
- Local workspace and file-based project references handled as local sources instead of external supply-chain inputs
|
|
27
|
+
- Abbreviated registry metadata for faster fetches
|
|
28
|
+
- Concurrency-limited registry requests
|
|
29
|
+
|
|
30
|
+
### Notes
|
|
31
|
+
|
|
32
|
+
- Package name: `safeinstall-cli` (npm) — command: `safeinstall`
|
|
33
|
+
- `bun install` remains manifest-based for project installs in this release
|
|
34
|
+
- Policy checks apply to direct dependencies only in this release
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SafeInstall Contributors
|
|
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,305 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<strong>SafeInstall</strong>
|
|
3
|
+
<br />
|
|
4
|
+
<em>Stop risky package installs before they run.</em>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/safeinstall-cli"><img src="https://img.shields.io/npm/v/safeinstall-cli?style=flat-square&color=22c55e" alt="npm version" /></a>
|
|
9
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-22c55e?style=flat-square" alt="license" /></a>
|
|
10
|
+
<a href="https://github.com/Mickdownunder/SafeInstall"><img src="https://img.shields.io/github/stars/Mickdownunder/SafeInstall?style=flat-square" alt="stars" /></a>
|
|
11
|
+
<a href="https://github.com/Mickdownunder/SafeInstall"><img src="https://img.shields.io/badge/TypeScript-strict-blue?style=flat-square" alt="TypeScript" /></a>
|
|
12
|
+
<a href="https://safeinstall.dev"><img src="https://img.shields.io/badge/docs-safeinstall.dev-22c55e?style=flat-square" alt="docs" /></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
Local-first CLI wrapper for <strong>npm</strong>, <strong>pnpm</strong>, and <strong>bun</strong>.<br />
|
|
17
|
+
Policy runs first. Then your package manager. Not the other way around.<br /><br />
|
|
18
|
+
Open source · MIT licensed · Free forever
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Why SafeInstall
|
|
24
|
+
|
|
25
|
+
AI coding tools suggest packages in seconds. They don't check publish dates. They don't read install scripts. They don't verify the source. You type "yes" and move on.
|
|
26
|
+
|
|
27
|
+
SafeInstall is the gate between suggestion and execution.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
$ safeinstall pnpm add compromised-pkg@9.9.9
|
|
31
|
+
|
|
32
|
+
Install blocked.
|
|
33
|
+
- compromised-pkg@9.9.9
|
|
34
|
+
Blocked: release too new (published 3 hours ago; minimum is 72 hours).
|
|
35
|
+
Blocked: install script present (has postinstall).
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
No dashboard. No account. No cloud. One command prefix — policy runs locally, blocks by default, then invokes the real tool.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g safeinstall-cli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
> Node.js >=20 · macOS, Linux, Windows · Command: `safeinstall`
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
safeinstall init # create safeinstall.config.json
|
|
54
|
+
safeinstall pnpm add axios # policy runs, then pnpm
|
|
55
|
+
safeinstall npm install # lockfile-aware project install
|
|
56
|
+
safeinstall check # audit direct deps against policy
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## How it works
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
┌─────────────────────┐ ┌──────────────┐ ┌─────────────────┐
|
|
65
|
+
│ safeinstall pnpm │ ──▶ │ Resolve & │ ──▶ │ Policy check │
|
|
66
|
+
│ add axios │ │ fetch meta │ │ (age, scripts, │
|
|
67
|
+
└─────────────────────┘ └──────────────┘ │ sources, ...) │
|
|
68
|
+
└────────┬────────┘
|
|
69
|
+
│
|
|
70
|
+
┌─────────▼─────────┐
|
|
71
|
+
pass → │ Invoke pnpm add │
|
|
72
|
+
fail → │ Exit 2 (blocked) │
|
|
73
|
+
└───────────────────┘
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
1. Resolves what would be installed
|
|
77
|
+
2. Fetches registry metadata (publish time, declared scripts)
|
|
78
|
+
3. Evaluates policy rules
|
|
79
|
+
4. Blocks (exit 2) or invokes the real package manager
|
|
80
|
+
|
|
81
|
+
No registry proxy. No tarball rewriting. No cloud dependency.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Policy defaults
|
|
86
|
+
|
|
87
|
+
| Rule | Default | Block message |
|
|
88
|
+
|:---|:---|:---|
|
|
89
|
+
| **Release age** | 72 hours minimum | `Blocked: release too new` |
|
|
90
|
+
| **Lifecycle scripts** | preinstall, install, postinstall blocked | `Blocked: install script present` |
|
|
91
|
+
| **Source types** | registry, workspace, file, directory allowed | `Blocked: untrusted source` |
|
|
92
|
+
| **Trust downgrade** | Detects registry→git/url or new scripts on update | `Blocked: trust level dropped` |
|
|
93
|
+
|
|
94
|
+
All rules are configurable. Ambiguous or incomplete metadata **blocks instead of allowing**.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Ad-hoc installs
|
|
102
|
+
safeinstall pnpm add axios
|
|
103
|
+
safeinstall npm install react@19.2.0
|
|
104
|
+
safeinstall bun add zod
|
|
105
|
+
|
|
106
|
+
# Project installs (lockfile-aware for npm/pnpm)
|
|
107
|
+
safeinstall pnpm install
|
|
108
|
+
safeinstall npm ci
|
|
109
|
+
|
|
110
|
+
# Monorepo — target one package
|
|
111
|
+
safeinstall pnpm -C packages/app install
|
|
112
|
+
safeinstall npm --prefix packages/app ci
|
|
113
|
+
|
|
114
|
+
# Utilities
|
|
115
|
+
safeinstall check # direct dependency audit
|
|
116
|
+
safeinstall check --json # machine-readable
|
|
117
|
+
safeinstall init # create starter config
|
|
118
|
+
safeinstall init --force # overwrite existing config
|
|
119
|
+
safeinstall --help
|
|
120
|
+
safeinstall --version
|
|
121
|
+
|
|
122
|
+
# JSON output (CI/automation)
|
|
123
|
+
safeinstall --json pnpm add axios
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Project installs
|
|
127
|
+
|
|
128
|
+
For `pnpm install` and `npm install` / `npm ci`, dependency versions come from the lockfile — not loose ranges in `package.json`.
|
|
129
|
+
|
|
130
|
+
- `pnpm-lock.yaml` for pnpm
|
|
131
|
+
- `package-lock.json` or `npm-shrinkwrap.json` for npm
|
|
132
|
+
- Stale, missing, or mismatched lockfile entries **fail closed**
|
|
133
|
+
- If `packageManager` is set in `package.json`, using a different CLI is blocked
|
|
134
|
+
- Workspace-targeting flags (`--filter`, `--workspace`) are blocked — use `-C` or `--prefix`
|
|
135
|
+
- `bun install` uses manifest-oriented analysis (full lockfile parity not yet implemented)
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Configuration
|
|
140
|
+
|
|
141
|
+
Optional `safeinstall.config.json` — discovered by walking upward from the project directory.
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"minimumReleaseAgeHours": 72,
|
|
146
|
+
"registryUrl": "https://registry.npmjs.org",
|
|
147
|
+
"allowedScripts": {
|
|
148
|
+
"esbuild": ["postinstall"]
|
|
149
|
+
},
|
|
150
|
+
"allowedSources": ["registry", "workspace", "file", "directory"],
|
|
151
|
+
"allowedPackages": [],
|
|
152
|
+
"ciMode": false,
|
|
153
|
+
"packageManagerDefaults": {
|
|
154
|
+
"npm": { "ignoreScripts": true },
|
|
155
|
+
"pnpm": { "ignoreScripts": true },
|
|
156
|
+
"bun": { "ignoreScripts": true }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
| Field | Purpose |
|
|
162
|
+
|:---|:---|
|
|
163
|
+
| `minimumReleaseAgeHours` | Minimum age in hours for registry versions |
|
|
164
|
+
| `registryUrl` | npm-compatible registry URL for metadata (mirrors, Artifactory, Verdaccio) |
|
|
165
|
+
| `allowedScripts` | Per-package lifecycle script exceptions |
|
|
166
|
+
| `allowedSources` | Permitted source types |
|
|
167
|
+
| `allowedPackages` | Names that skip policy entirely (with warning) |
|
|
168
|
+
| `ciMode` | Reserved for future CI-specific behavior |
|
|
169
|
+
| `packageManagerDefaults` | Per-manager flags forwarded to the tool |
|
|
170
|
+
|
|
171
|
+
Run `safeinstall init` to generate a starter config.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Exit codes
|
|
176
|
+
|
|
177
|
+
| Code | Meaning |
|
|
178
|
+
|:---|:---|
|
|
179
|
+
| `0` | Allowed / check passed |
|
|
180
|
+
| `1` | Runtime or config error |
|
|
181
|
+
| `2` | Blocked by policy |
|
|
182
|
+
|
|
183
|
+
Use exit code 2 like any other failing step in a CI pipeline.
|
|
184
|
+
|
|
185
|
+
## JSON output
|
|
186
|
+
|
|
187
|
+
Pass `--json` anywhere in the command. Structured output goes to stdout.
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
safeinstall --json pnpm add axios
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Fields: `command`, `commandString`, `packageManager`, `decision`, `summary`, `reasons`, `warnings`, `affectedPackages`, `exitCode`, `exitCodeMeaning`. Allowed installs include `execution.stdout` and `execution.stderr`.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Examples
|
|
198
|
+
|
|
199
|
+
<details>
|
|
200
|
+
<summary><strong>Fresh release blocked</strong></summary>
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
$ safeinstall pnpm add axios
|
|
204
|
+
Using config: built-in defaults
|
|
205
|
+
Install blocked.
|
|
206
|
+
- axios@1.14.0
|
|
207
|
+
Blocked: release too new (axios@1.14.0 is 6 hours old; minimum is 72 hours).
|
|
208
|
+
Suggestion: Retry later or lower minimumReleaseAgeHours if this package is intentionally urgent.
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
</details>
|
|
212
|
+
|
|
213
|
+
<details>
|
|
214
|
+
<summary><strong>Git source blocked</strong></summary>
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
$ safeinstall npm install github:axios/axios
|
|
218
|
+
Using config: built-in defaults
|
|
219
|
+
Install blocked.
|
|
220
|
+
- github:axios/axios
|
|
221
|
+
Blocked: untrusted source (git).
|
|
222
|
+
Suggestion: Use a registry release or allow this source intentionally.
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
</details>
|
|
226
|
+
|
|
227
|
+
<details>
|
|
228
|
+
<summary><strong>Package manager mismatch</strong></summary>
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
$ safeinstall npm install
|
|
232
|
+
Using config: built-in defaults
|
|
233
|
+
Install blocked.
|
|
234
|
+
- Project install blocked: package.json declares pnpm as packageManager, but this command uses npm.
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
</details>
|
|
238
|
+
|
|
239
|
+
<details>
|
|
240
|
+
<summary><strong>Stale lockfile</strong></summary>
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
$ safeinstall pnpm install
|
|
244
|
+
Using config: built-in defaults
|
|
245
|
+
Install blocked.
|
|
246
|
+
- Project install blocked: axios is declared in package.json but missing from pnpm-lock.yaml.
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
</details>
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Limitations
|
|
254
|
+
|
|
255
|
+
- **Not a CVE scanner** — pair with `npm audit` or Snyk for vulnerability data
|
|
256
|
+
- **Transitive dependencies** not fully evaluated yet
|
|
257
|
+
- **`peerDependencies`** not evaluated unless also declared as direct dependencies
|
|
258
|
+
- **Trust downgrade detection** requires prior install state in `node_modules`
|
|
259
|
+
- **`bun install`** uses manifest-only analysis (lockfile parity not yet implemented)
|
|
260
|
+
- **`safeinstall check`** evaluates direct dependencies only
|
|
261
|
+
- Ambiguous metadata blocks instead of guessing — by design
|
|
262
|
+
|
|
263
|
+
## What it does not do
|
|
264
|
+
|
|
265
|
+
- Vulnerability scanning or CVE databases
|
|
266
|
+
- Registry proxying or tarball rewriting
|
|
267
|
+
- Malware detection or provenance attestation
|
|
268
|
+
- Selective lifecycle script execution (forwards `--ignore-scripts` by default)
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Works with
|
|
273
|
+
|
|
274
|
+
SafeInstall works with any tool that runs package manager commands — including AI coding assistants:
|
|
275
|
+
|
|
276
|
+
**Cursor** · **GitHub Copilot** · **Cline** · **Claude Code** · **Windsurf** · **Aider** · **Devin** · **Continue**
|
|
277
|
+
|
|
278
|
+
Just prefix your install commands with `safeinstall`. Same workflow, one safety layer.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Contributing
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
pnpm install
|
|
286
|
+
pnpm typecheck
|
|
287
|
+
pnpm test
|
|
288
|
+
pnpm build
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Issues and PRs welcome. Author merges at own discretion — this is a solo-maintained project.
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
MIT — see [LICENSE](./LICENSE).
|
|
296
|
+
|
|
297
|
+
## Disclaimer
|
|
298
|
+
|
|
299
|
+
SafeInstall is provided as-is under the MIT license. It is a policy tool that enforces configurable rules on package installs. It does not guarantee the safety of any package, does not detect all supply-chain attacks, and does not replace professional security review. Use at your own risk. The authors are not liable for any damages arising from the use of this software.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
<p align="center">
|
|
304
|
+
<a href="https://safeinstall.dev">safeinstall.dev</a> · <a href="https://www.npmjs.com/package/safeinstall">npm</a> · <a href="https://github.com/Mickdownunder/SafeInstall">GitHub</a>
|
|
305
|
+
</p>
|
package/SUPPORT.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Support
|
|
2
|
+
|
|
3
|
+
SafeInstall is a solo-maintained open-source project. Issues and PRs are welcome — the author reviews and merges at own discretion.
|
|
4
|
+
|
|
5
|
+
SafeInstall is designed to fail closed when project metadata is stale, inconsistent, or ambiguous.
|
|
6
|
+
|
|
7
|
+
## Before Reporting a Problem
|
|
8
|
+
|
|
9
|
+
Run these commands in the affected project:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
safeinstall --json npm install
|
|
13
|
+
safeinstall --json npm ci
|
|
14
|
+
safeinstall --json pnpm install
|
|
15
|
+
safeinstall check --json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Use the command that matches the package manager and workflow you expected to use.
|
|
19
|
+
|
|
20
|
+
## Include This Information
|
|
21
|
+
|
|
22
|
+
- SafeInstall version (`safeinstall --version`)
|
|
23
|
+
- Node.js version
|
|
24
|
+
- Package manager and version
|
|
25
|
+
- Exact SafeInstall command
|
|
26
|
+
- Exact JSON output
|
|
27
|
+
- Relevant `packageManager` field from `package.json`
|
|
28
|
+
- Whether the project uses `package-lock.json`, `npm-shrinkwrap.json`, or `pnpm-lock.yaml`
|
|
29
|
+
- Redacted `safeinstall.config.json` if one exists
|
|
30
|
+
|
|
31
|
+
## Expected Support Boundary
|
|
32
|
+
|
|
33
|
+
- SafeInstall supports direct dependency policy checks
|
|
34
|
+
- SafeInstall supports lockfile-aware project installs for npm and pnpm
|
|
35
|
+
- SafeInstall intentionally blocks ambiguous workspace-targeting commands
|
|
36
|
+
- SafeInstall intentionally blocks when lockfile state is incomplete or conflicting
|
|
37
|
+
|
|
38
|
+
## Known Limits In 0.1.0
|
|
39
|
+
|
|
40
|
+
- No transitive dependency policy
|
|
41
|
+
- No bun lockfile-aware project install analysis
|
|
42
|
+
- No selective lifecycle-script execution even when scripts are allowlisted
|
package/dist/async.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapConcurrent = mapConcurrent;
|
|
4
|
+
async function mapConcurrent(values, concurrency, mapper) {
|
|
5
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
6
|
+
throw new Error(`Concurrency must be a positive integer. Received: ${concurrency}.`);
|
|
7
|
+
}
|
|
8
|
+
if (values.length === 0) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const results = new Array(values.length);
|
|
12
|
+
let nextIndex = 0;
|
|
13
|
+
async function runWorker() {
|
|
14
|
+
while (true) {
|
|
15
|
+
const currentIndex = nextIndex;
|
|
16
|
+
nextIndex += 1;
|
|
17
|
+
if (currentIndex >= values.length) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
results[currentIndex] = await mapper(values[currentIndex], currentIndex);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const workerCount = Math.min(concurrency, values.length);
|
|
24
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
25
|
+
return results;
|
|
26
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCheckFlow = runCheckFlow;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const evaluations_1 = require("./evaluations");
|
|
6
|
+
const output_1 = require("./output");
|
|
7
|
+
const project_state_1 = require("./project-state");
|
|
8
|
+
const project_discovery_1 = require("./project-discovery");
|
|
9
|
+
const project_installs_1 = require("./project-installs");
|
|
10
|
+
const registry_1 = require("./registry");
|
|
11
|
+
const signals_1 = require("./signals");
|
|
12
|
+
const specs_1 = require("./specs");
|
|
13
|
+
function configLabel(configPath) {
|
|
14
|
+
return configPath ?? "built-in defaults";
|
|
15
|
+
}
|
|
16
|
+
function createProjectIssueReason(message) {
|
|
17
|
+
if (message.includes("both pnpm-lock.yaml and an npm lockfile exist")) {
|
|
18
|
+
return {
|
|
19
|
+
code: "ambiguous-lockfiles",
|
|
20
|
+
message,
|
|
21
|
+
suggestion: "Set packageManager in package.json or remove the stale lockfile so SafeInstall can choose one source of truth."
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (message.includes("required for safeinstall")) {
|
|
25
|
+
return {
|
|
26
|
+
code: "lockfile-required",
|
|
27
|
+
message,
|
|
28
|
+
suggestion: "Create or refresh the lockfile before relying on SafeInstall for project-level checks."
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (message.includes("specifier")) {
|
|
32
|
+
return {
|
|
33
|
+
code: "lockfile-specifier-mismatch",
|
|
34
|
+
message,
|
|
35
|
+
suggestion: "Regenerate the lockfile so package.json and the lockfile match."
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
code: "check-blocked",
|
|
40
|
+
message,
|
|
41
|
+
suggestion: "Fix the project metadata inconsistency and run safeinstall check again."
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function createAffectedPackage(evaluation) {
|
|
45
|
+
return {
|
|
46
|
+
name: evaluation.requested.name,
|
|
47
|
+
requested: evaluation.requested.raw,
|
|
48
|
+
sourceType: evaluation.requested.sourceType,
|
|
49
|
+
resolvedVersion: evaluation.resolvedRegistryPackage?.resolvedVersion,
|
|
50
|
+
reasons: evaluation.blockedReasons,
|
|
51
|
+
warnings: evaluation.warnings
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function runCheckFlow(cwd, argv, options = {}) {
|
|
55
|
+
(0, signals_1.throwIfAborted)(options.signal);
|
|
56
|
+
const invocation = await (0, project_discovery_1.resolveInvocationContext)(cwd, []);
|
|
57
|
+
const { config, path } = await (0, config_1.loadConfig)(invocation.effectiveCwd);
|
|
58
|
+
const projectTargets = await (0, project_installs_1.inferProjectInstallTargetsForCheck)(invocation.effectiveCwd, invocation.packageDir);
|
|
59
|
+
const commandString = (0, output_1.formatCommand)("safeinstall", argv);
|
|
60
|
+
if (!invocation.packageDir) {
|
|
61
|
+
return {
|
|
62
|
+
mode: "check",
|
|
63
|
+
decision: "block",
|
|
64
|
+
exitCode: 2,
|
|
65
|
+
exitCodeMeaning: "Check was blocked because the current directory does not map to a package.",
|
|
66
|
+
command: argv,
|
|
67
|
+
commandString,
|
|
68
|
+
configPath: path,
|
|
69
|
+
configLabel: configLabel(path),
|
|
70
|
+
reasons: [
|
|
71
|
+
{
|
|
72
|
+
code: "package-root-not-found",
|
|
73
|
+
message: "Check blocked: the current directory does not map to a package.json-backed project.",
|
|
74
|
+
suggestion: "Run SafeInstall from a package directory."
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
summary: "Check blocked.",
|
|
78
|
+
warnings: [],
|
|
79
|
+
affectedPackages: []
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (projectTargets?.issues.length) {
|
|
83
|
+
return {
|
|
84
|
+
mode: "check",
|
|
85
|
+
decision: "block",
|
|
86
|
+
exitCode: 2,
|
|
87
|
+
exitCodeMeaning: "Check was blocked because project metadata was incomplete or inconsistent.",
|
|
88
|
+
command: argv,
|
|
89
|
+
commandString,
|
|
90
|
+
configPath: path,
|
|
91
|
+
configLabel: configLabel(path),
|
|
92
|
+
reasons: projectTargets.issues.map(createProjectIssueReason),
|
|
93
|
+
summary: "Check blocked.",
|
|
94
|
+
warnings: [],
|
|
95
|
+
affectedPackages: []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const requestedPackages = projectTargets
|
|
99
|
+
? projectTargets.targets.map((target) => target.requested)
|
|
100
|
+
: Object.entries(await (0, project_state_1.loadManifestDependencies)(invocation.packageDir ?? invocation.effectiveCwd)).map(([name, spec]) => (0, specs_1.parseManifestDependency)(name, spec));
|
|
101
|
+
if (requestedPackages.length === 0) {
|
|
102
|
+
return {
|
|
103
|
+
mode: "check",
|
|
104
|
+
decision: "allow",
|
|
105
|
+
exitCode: 0,
|
|
106
|
+
exitCodeMeaning: "Check passed; there were no direct dependencies to evaluate.",
|
|
107
|
+
command: argv,
|
|
108
|
+
commandString,
|
|
109
|
+
configPath: path,
|
|
110
|
+
configLabel: configLabel(path),
|
|
111
|
+
reasons: [],
|
|
112
|
+
summary: "Check skipped: package.json has no direct dependencies.",
|
|
113
|
+
warnings: [],
|
|
114
|
+
affectedPackages: []
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const registryClient = new registry_1.RegistryClient({
|
|
118
|
+
registryUrl: config.registryUrl,
|
|
119
|
+
signal: options.signal
|
|
120
|
+
});
|
|
121
|
+
const evaluations = await (0, evaluations_1.evaluateRequestedPackages)(invocation.packageDir ?? invocation.effectiveCwd, requestedPackages, registryClient, config);
|
|
122
|
+
const blocked = evaluations.filter((evaluation) => evaluation.blockedReasons.length > 0);
|
|
123
|
+
const warnings = evaluations.flatMap((evaluation) => evaluation.warnings);
|
|
124
|
+
if (blocked.length > 0) {
|
|
125
|
+
return {
|
|
126
|
+
mode: "check",
|
|
127
|
+
decision: "block",
|
|
128
|
+
exitCode: 2,
|
|
129
|
+
exitCodeMeaning: "Check found dependencies that violate the current policy.",
|
|
130
|
+
command: argv,
|
|
131
|
+
commandString,
|
|
132
|
+
configPath: path,
|
|
133
|
+
configLabel: configLabel(path),
|
|
134
|
+
reasons: blocked.flatMap((evaluation) => evaluation.blockedReasons),
|
|
135
|
+
summary: "Check blocked.",
|
|
136
|
+
warnings,
|
|
137
|
+
affectedPackages: blocked.map(createAffectedPackage)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
mode: "check",
|
|
142
|
+
decision: "allow",
|
|
143
|
+
exitCode: 0,
|
|
144
|
+
exitCodeMeaning: "Check passed with no direct dependency policy violations.",
|
|
145
|
+
command: argv,
|
|
146
|
+
commandString,
|
|
147
|
+
configPath: path,
|
|
148
|
+
configLabel: configLabel(path),
|
|
149
|
+
reasons: [],
|
|
150
|
+
summary: "Check passed: no direct dependency policy violations found.",
|
|
151
|
+
warnings,
|
|
152
|
+
affectedPackages: requestedPackages.map((requested) => ({
|
|
153
|
+
name: requested.name,
|
|
154
|
+
requested: requested.raw,
|
|
155
|
+
sourceType: requested.sourceType,
|
|
156
|
+
reasons: [],
|
|
157
|
+
warnings: []
|
|
158
|
+
}))
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCliOptions = parseCliOptions;
|
|
4
|
+
function parseCliOptions(argv) {
|
|
5
|
+
let json = false;
|
|
6
|
+
const args = [];
|
|
7
|
+
for (const token of argv) {
|
|
8
|
+
if (token === "--json") {
|
|
9
|
+
json = true;
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
args.push(token);
|
|
13
|
+
}
|
|
14
|
+
return { args, json };
|
|
15
|
+
}
|