itworksbut 0.5.0 → 0.6.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 +25 -31
- package/bin/itworksbut.js +9 -0
- package/package.json +1 -1
- package/src/checks/dependencies/outdated-packages.js +297 -0
- package/src/checks/index.js +2 -0
- package/src/cli/output.js +1 -0
- package/src/cli/parseArgs.js +3 -1
- package/src/core/checkResults.js +53 -0
- package/src/core/scanner.js +33 -4
- package/src/reporters/consoleReporter.js +42 -1
- package/src/reporters/jsonReporter.js +3 -0
- package/src/reporters/markdownReport.js +203 -0
- package/src/utils/packageManager.js +28 -0
package/README.md
CHANGED
|
@@ -6,7 +6,19 @@ It focuses on common "it works, but..." risks often found in AI-generated or rus
|
|
|
6
6
|
|
|
7
7
|
For every finding, ItWorksBut gives you a copy-ready fix prompt you can paste into your coding agent. It does not just say what is wrong; it tells your AI exactly what to inspect, what to change, and what not to leak.
|
|
8
8
|
|
|
9
|
-
It
|
|
9
|
+
It mostly reads files and reports findings. It does not send telemetry. The outdated-package check may invoke your local package manager, and the CLI only writes files when you explicitly ask for `todo.md` with `--todo` or `report.md` with `--report`.
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Options](#options)
|
|
16
|
+
- [Terminal Experience](#terminal-experience)
|
|
17
|
+
- [GitHub Actions](#github-actions)
|
|
18
|
+
- [Configuration](#configuration)
|
|
19
|
+
- [Example Output](#example-output)
|
|
20
|
+
- [What It Detects](#what-it-detects)
|
|
21
|
+
- [What It Does Not Guarantee](#what-it-does-not-guarantee)
|
|
10
22
|
|
|
11
23
|
## Installation
|
|
12
24
|
|
|
@@ -41,6 +53,7 @@ itworksbut scan --fail-on high
|
|
|
41
53
|
itworksbut scan --json
|
|
42
54
|
itworksbut scan --sarif > itworksbut.sarif
|
|
43
55
|
itworksbut scan --todo
|
|
56
|
+
itworksbut scan --report
|
|
44
57
|
itworksbut scan --config itworksbut.config.json
|
|
45
58
|
itworksbut scan --verbose
|
|
46
59
|
itworksbut --version
|
|
@@ -58,6 +71,7 @@ itworksbut scan [options]
|
|
|
58
71
|
- `--json`: Print machine-readable JSON only. No banner, colors, spinner, table, or extra text.
|
|
59
72
|
- `--sarif`: Print SARIF JSON for GitHub Code Scanning. No banner, colors, spinner, table, or extra text.
|
|
60
73
|
- `--todo`: Write an AI-ready `todo.md` into the scanned project with prioritized findings, fix prompts, and acceptance criteria.
|
|
74
|
+
- `--report`: Write a Markdown `report.md` into the current working directory.
|
|
61
75
|
- `--verbose`: Include scanner warnings and extra metadata in console output.
|
|
62
76
|
- `--quiet`: Print only the summary.
|
|
63
77
|
- `--no-color`: Disable colored output.
|
|
@@ -97,6 +111,14 @@ itworksbut scan --todo
|
|
|
97
111
|
|
|
98
112
|
This writes `todo.md` to the scanned project. The file is ordered by severity and includes agent rules, exact fix prompts, locations, recommendations, and final verification checkboxes.
|
|
99
113
|
|
|
114
|
+
To create a Markdown scan report:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
itworksbut scan --report
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This writes `report.md` to the current working directory with check statuses, summaries, details, and recommendations.
|
|
121
|
+
|
|
100
122
|
## GitHub Actions
|
|
101
123
|
|
|
102
124
|
```yaml
|
|
@@ -166,38 +188,10 @@ release/**
|
|
|
166
188
|
## Example Output
|
|
167
189
|
|
|
168
190
|

|
|
169
|
-
✖ CRITICAL It works, but your .env is tracked.
|
|
170
|
-
✔ Check: env.env-file-tracked
|
|
171
|
-
📁 File: .env
|
|
172
|
-
🤔 Why: .env appears to be tracked by git. Secrets may be exposed.
|
|
173
|
-
🤖 prompt: You are a senior security engineer working in this repository. Fix the ItWorksBut finding env.env-file-tracked at .env. Treat this as a concrete finding. Problem: .env appears to be tracked by git. Secrets may be exposed. Required change: Remove tracked env files from git, add safe examples such as .env.example, and make sure any exposed credentials are treated as compromised. Do not print, log, or preserve raw secret values; use placeholders only. Keep existing behavior intact where possible, add or update focused tests when useful, and do not silence the check unless the underlying risk is actually fixed.
|
|
174
|
-
|
|
175
|
-
▲ HIGH It works, but your SQL query is one template string away from pain.
|
|
176
|
-
✔ Check: database.raw-sql-interpolation
|
|
177
|
-
📁 File: src/db.js:12
|
|
178
|
-
🤔 Why: Possible SQL injection risk: raw SQL appears to be built with template string interpolation.
|
|
179
|
-
🤖 prompt: You are a senior security engineer working in this repository. Fix the ItWorksBut finding database.raw-sql-interpolation at src/db.js:12. This finding is heuristic, so inspect the code first and only change behavior when the risk is real. Problem: Possible SQL injection risk: raw SQL appears to be built with template string interpolation. Required change: Replace SQL string interpolation or concatenation with parameterized queries, prepared statements, or a safe ORM query builder. Keep existing behavior intact where possible, add or update focused tests when useful, and do not silence the check unless the underlying risk is actually fixed.
|
|
180
|
-
|
|
181
|
-
SUMMARY
|
|
182
|
-
|
|
183
|
-
- ship status: DO NOT SHIP
|
|
184
|
-
- Fix the red stuff before production.
|
|
185
|
-
- total findings: 2
|
|
186
|
-
- critical: 1
|
|
187
|
-
- high: 1
|
|
188
|
-
- medium: 0
|
|
189
|
-
- low: 0
|
|
190
|
-
- info: 0
|
|
191
|
-
- fail-on: high
|
|
192
|
-
- exit decision: 1
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Secret-like findings never print the full secret value. Findings report the file, line, and secret type where possible.
|
|
197
191
|
|
|
198
192
|
## What It Detects
|
|
199
193
|
|
|
200
|
-
The baseline includes
|
|
194
|
+
The baseline includes 51 modular checks:
|
|
201
195
|
|
|
202
196
|
- `git.gitignore-missing`
|
|
203
197
|
- `git.gitignore-incomplete`
|
|
@@ -211,6 +205,7 @@ The baseline includes 50 modular checks:
|
|
|
211
205
|
- `dependencies.multiple-lockfiles`
|
|
212
206
|
- `dependencies.install-scripts-risk`
|
|
213
207
|
- `dependencies.audit-script-missing`
|
|
208
|
+
- `dependencies.outdated-packages`
|
|
214
209
|
- `package.scripts-missing`
|
|
215
210
|
- `ci.no-ci-config`
|
|
216
211
|
- `ci.npm-install-instead-of-npm-ci`
|
|
@@ -257,4 +252,3 @@ Each check is a plain ESM module with an `id`, metadata, and async `run(context)
|
|
|
257
252
|
ItWorksBut is a static heuristic scanner, not a pentest, SAST replacement, dependency vulnerability database, or runtime security monitor. Findings intentionally use wording such as "possible", "potential", and "appears to" when a check is heuristic.
|
|
258
253
|
|
|
259
254
|
Use it as a CI guardrail for common project hygiene and security mistakes. For production systems, combine it with code review, tests, dependency scanning, secrets scanning, threat modeling, and focused security assessment.
|
|
260
|
-
```
|
package/bin/itworksbut.js
CHANGED
|
@@ -10,6 +10,7 @@ import { reportConsole } from '../src/reporters/consoleReporter.js';
|
|
|
10
10
|
import { reportJson } from '../src/reporters/jsonReporter.js';
|
|
11
11
|
import { reportSarif } from '../src/reporters/sarifReporter.js';
|
|
12
12
|
import { writeTodoReport } from '../src/reporters/todoReporter.js';
|
|
13
|
+
import { writeMarkdownReport } from '../src/reporters/markdownReport.js';
|
|
13
14
|
|
|
14
15
|
async function main() {
|
|
15
16
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -59,6 +60,14 @@ async function main() {
|
|
|
59
60
|
reportConsole(result, args);
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
if (args.report) {
|
|
64
|
+
const report = await writeMarkdownReport(result);
|
|
65
|
+
if (!args.json && !args.sarif) {
|
|
66
|
+
const verb = report.overwritten ? 'Overwrote' : 'Wrote';
|
|
67
|
+
process.stdout.write(`${verb} scan report: ${report.filePath}\n`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
return getExitCode(result.findings, result.config.failOn);
|
|
63
72
|
}
|
|
64
73
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { detectOutdatedPackageManager, getOutdatedCommand } from "../../utils/packageManager.js";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const CHECK_ID = "dependencies.outdated-packages";
|
|
7
|
+
const CHECK_TITLE = "Outdated packages";
|
|
8
|
+
const COMMAND_TIMEOUT_MS = 30_000;
|
|
9
|
+
const COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
id: CHECK_ID,
|
|
13
|
+
title: CHECK_TITLE,
|
|
14
|
+
category: "dependencies",
|
|
15
|
+
severity: "info",
|
|
16
|
+
tags: ["dependencies", "maintenance"],
|
|
17
|
+
run: async (context) => {
|
|
18
|
+
const result = await runOutdatedPackagesCheck(context);
|
|
19
|
+
return { findings: [], result };
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function runOutdatedPackagesCheck(context, options = {}) {
|
|
24
|
+
const detected = detectOutdatedPackageManager({
|
|
25
|
+
packageJson: context.packageJson,
|
|
26
|
+
files: context.allFiles
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (detected.status === "skip") {
|
|
30
|
+
return checkResult({
|
|
31
|
+
status: "skip",
|
|
32
|
+
summary: detected.summary,
|
|
33
|
+
metadata: { reason: "missing-package-json" }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const commandResult = await runOutdatedCommand(detected.manager, context.rootPath, options);
|
|
38
|
+
if (isMissingCommand(commandResult)) {
|
|
39
|
+
return checkResult({
|
|
40
|
+
status: "fail",
|
|
41
|
+
summary: `${detected.manager} is not installed or could not be found.`,
|
|
42
|
+
details: [
|
|
43
|
+
{
|
|
44
|
+
message: `${detected.manager} is not installed or could not be found.`,
|
|
45
|
+
command: renderCommand(commandResult),
|
|
46
|
+
exitCode: commandResult.exitCode
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
metadata: { packageManager: detected.manager }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parsed = parseOutdatedOutput(detected.manager, commandResult.stdout, context.packageJson);
|
|
54
|
+
if (!parsed.ok) {
|
|
55
|
+
if (!hasCommandFailure(commandResult) && isEmptyOutput(commandResult.stdout)) {
|
|
56
|
+
return checkResult({
|
|
57
|
+
status: "pass",
|
|
58
|
+
summary: "all dependencies are up to date",
|
|
59
|
+
details: [],
|
|
60
|
+
metadata: { packageManager: detected.manager }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return checkResult({
|
|
65
|
+
status: "fail",
|
|
66
|
+
summary: `${detected.manager} outdated returned output that could not be parsed.`,
|
|
67
|
+
details: [
|
|
68
|
+
{
|
|
69
|
+
message: parsed.error,
|
|
70
|
+
command: renderCommand(commandResult),
|
|
71
|
+
exitCode: commandResult.exitCode,
|
|
72
|
+
stderr: trimText(commandResult.stderr)
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
metadata: { packageManager: detected.manager }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if ((hasCommandFailure(commandResult) || hasEmptyExitOneFailure(commandResult)) && parsed.packages.length === 0) {
|
|
80
|
+
return checkResult({
|
|
81
|
+
status: "fail",
|
|
82
|
+
summary: `${detected.manager} outdated failed.`,
|
|
83
|
+
details: [
|
|
84
|
+
{
|
|
85
|
+
message: trimText(commandResult.stderr) || "Command exited with code " + commandResult.exitCode + ".",
|
|
86
|
+
command: renderCommand(commandResult),
|
|
87
|
+
exitCode: commandResult.exitCode
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
metadata: { packageManager: detected.manager }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (parsed.packages.length === 0) {
|
|
95
|
+
return checkResult({
|
|
96
|
+
status: "pass",
|
|
97
|
+
summary: "all dependencies are up to date",
|
|
98
|
+
details: [],
|
|
99
|
+
metadata: { packageManager: detected.manager }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return checkResult({
|
|
104
|
+
status: "warn",
|
|
105
|
+
summary: `${parsed.packages.length} ${parsed.packages.length === 1 ? "package" : "packages"} outdated`,
|
|
106
|
+
details: parsed.packages,
|
|
107
|
+
metadata: { packageManager: detected.manager }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runOutdatedCommand(manager, rootPath, options = {}) {
|
|
112
|
+
const runCommand = options.runCommand || execCommand;
|
|
113
|
+
const { command, args } = getOutdatedCommand(manager);
|
|
114
|
+
return await runCommand(command, args, { cwd: rootPath });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function parseOutdatedOutput(manager, stdout, packageJson = {}) {
|
|
118
|
+
const output = String(stdout || "").trim();
|
|
119
|
+
if (!output) return { ok: true, packages: [] };
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (manager === "yarn") {
|
|
123
|
+
return { ok: true, packages: parseYarnOutput(output, packageJson) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const parsed = JSON.parse(output);
|
|
127
|
+
return { ok: true, packages: normalizeOutdatedData(parsed, packageJson) };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
packages: [],
|
|
132
|
+
error: error instanceof Error ? error.message : String(error)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function normalizeOutdatedData(data, packageJson = {}) {
|
|
138
|
+
if (!data || typeof data !== "object") return [];
|
|
139
|
+
|
|
140
|
+
if (Array.isArray(data)) {
|
|
141
|
+
return data
|
|
142
|
+
.map((entry) => normalizePackageEntry(entry.name || entry.package, entry, packageJson))
|
|
143
|
+
.filter(Boolean);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return Object.entries(data)
|
|
147
|
+
.map(([name, entry]) => normalizePackageEntry(name, entry, packageJson))
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function execCommand(command, args, options) {
|
|
152
|
+
try {
|
|
153
|
+
const result = await execFileAsync(command, args, {
|
|
154
|
+
cwd: options.cwd,
|
|
155
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
156
|
+
maxBuffer: COMMAND_MAX_BUFFER
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
command,
|
|
160
|
+
args,
|
|
161
|
+
stdout: result.stdout || "",
|
|
162
|
+
stderr: result.stderr || "",
|
|
163
|
+
exitCode: 0
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return {
|
|
167
|
+
command,
|
|
168
|
+
args,
|
|
169
|
+
stdout: error?.stdout || "",
|
|
170
|
+
stderr: error?.stderr || error?.message || "",
|
|
171
|
+
exitCode: error?.code ?? null,
|
|
172
|
+
signal: error?.signal,
|
|
173
|
+
error
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseYarnOutput(output, packageJson) {
|
|
179
|
+
const directJson = tryJson(output);
|
|
180
|
+
if (directJson.ok) {
|
|
181
|
+
if (directJson.value?.type === "table") return parseYarnTable(directJson.value.data, packageJson);
|
|
182
|
+
return normalizeOutdatedData(directJson.value, packageJson);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const records = output
|
|
186
|
+
.split(/\r?\n/)
|
|
187
|
+
.map((line) => line.trim())
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.map((line) => tryJson(line))
|
|
190
|
+
.filter((record) => record.ok)
|
|
191
|
+
.map((record) => record.value);
|
|
192
|
+
|
|
193
|
+
const tableRecord = records.find((record) => record?.type === "table" && record.data);
|
|
194
|
+
if (tableRecord) return parseYarnTable(tableRecord.data, packageJson);
|
|
195
|
+
|
|
196
|
+
const packageRecords = records
|
|
197
|
+
.filter((record) => record?.data && typeof record.data === "object")
|
|
198
|
+
.map((record) => record.data);
|
|
199
|
+
if (packageRecords.length > 0) return normalizeOutdatedData(packageRecords, packageJson);
|
|
200
|
+
|
|
201
|
+
throw new Error("No parseable Yarn outdated JSON records were found.");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseYarnTable(data, packageJson) {
|
|
205
|
+
const head = Array.isArray(data?.head) ? data.head.map((value) => String(value).toLowerCase()) : [];
|
|
206
|
+
const body = Array.isArray(data?.body) ? data.body : [];
|
|
207
|
+
|
|
208
|
+
return body.map((row) => {
|
|
209
|
+
const entry = {
|
|
210
|
+
name: row[indexOf(head, "package", 0)],
|
|
211
|
+
current: row[indexOf(head, "current", 1)],
|
|
212
|
+
wanted: row[indexOf(head, "wanted", 2)],
|
|
213
|
+
latest: row[indexOf(head, "latest", 3)],
|
|
214
|
+
type: row[indexOf(head, "package type", 4)]
|
|
215
|
+
};
|
|
216
|
+
return normalizePackageEntry(entry.name, entry, packageJson);
|
|
217
|
+
}).filter(Boolean);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizePackageEntry(name, entry, packageJson) {
|
|
221
|
+
if (!name || !entry || typeof entry !== "object") return null;
|
|
222
|
+
|
|
223
|
+
const current = stringValue(entry.current ?? entry.installed ?? entry.version);
|
|
224
|
+
const wanted = stringValue(entry.wanted ?? entry.latest);
|
|
225
|
+
const latest = stringValue(entry.latest ?? entry.wanted);
|
|
226
|
+
|
|
227
|
+
if (!current && !wanted && !latest) return null;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
name: String(name),
|
|
231
|
+
current: current || "unknown",
|
|
232
|
+
wanted: wanted || "unknown",
|
|
233
|
+
latest: latest || "unknown",
|
|
234
|
+
type: stringValue(entry.type ?? entry.dependencyType ?? entry.packageType) || inferDependencyType(name, packageJson)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function inferDependencyType(name, packageJson = {}) {
|
|
239
|
+
if (Object.hasOwn(packageJson.dependencies || {}, name)) return "dependencies";
|
|
240
|
+
if (Object.hasOwn(packageJson.devDependencies || {}, name)) return "devDependencies";
|
|
241
|
+
if (Object.hasOwn(packageJson.peerDependencies || {}, name)) return "peerDependencies";
|
|
242
|
+
if (Object.hasOwn(packageJson.optionalDependencies || {}, name)) return "optionalDependencies";
|
|
243
|
+
return "dependencies";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function checkResult(result) {
|
|
247
|
+
return {
|
|
248
|
+
id: CHECK_ID,
|
|
249
|
+
title: CHECK_TITLE,
|
|
250
|
+
category: "dependencies",
|
|
251
|
+
details: [],
|
|
252
|
+
...result
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function hasCommandFailure(result) {
|
|
257
|
+
return result.exitCode !== 0 && result.exitCode !== 1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function hasEmptyExitOneFailure(result) {
|
|
261
|
+
return result.exitCode === 1 && isEmptyOutput(result.stdout) && !isEmptyOutput(result.stderr);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isMissingCommand(result) {
|
|
265
|
+
return result.exitCode === "ENOENT" || result.error?.code === "ENOENT";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isEmptyOutput(value) {
|
|
269
|
+
return String(value || "").trim() === "";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function renderCommand(result) {
|
|
273
|
+
return [result.command, ...(result.args || [])].filter(Boolean).join(" ");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function trimText(value) {
|
|
277
|
+
const normalized = String(value || "").trim();
|
|
278
|
+
return normalized.length > 1000 ? `${normalized.slice(0, 1000)}...` : normalized;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function tryJson(value) {
|
|
282
|
+
try {
|
|
283
|
+
return { ok: true, value: JSON.parse(value) };
|
|
284
|
+
} catch {
|
|
285
|
+
return { ok: false, value: null };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function indexOf(head, name, fallback) {
|
|
290
|
+
const index = head.indexOf(name);
|
|
291
|
+
return index === -1 ? fallback : index;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function stringValue(value) {
|
|
295
|
+
if (value === undefined || value === null || value === "") return "";
|
|
296
|
+
return String(value);
|
|
297
|
+
}
|
package/src/checks/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import lockfileMissing from "./dependencies/lockfile-missing.js";
|
|
|
10
10
|
import multipleLockfiles from "./dependencies/multiple-lockfiles.js";
|
|
11
11
|
import installScriptsRisk from "./dependencies/install-scripts-risk.js";
|
|
12
12
|
import auditScriptMissing from "./dependencies/audit-script-missing.js";
|
|
13
|
+
import outdatedPackages from "./dependencies/outdated-packages.js";
|
|
13
14
|
import packageScriptsMissing from "./package/scripts-missing.js";
|
|
14
15
|
import noCiConfig from "./ci/no-ci-config.js";
|
|
15
16
|
import npmInstallInsteadOfNpmCi from "./ci/npm-install-instead-of-npm-ci.js";
|
|
@@ -62,6 +63,7 @@ export default [
|
|
|
62
63
|
multipleLockfiles,
|
|
63
64
|
installScriptsRisk,
|
|
64
65
|
auditScriptMissing,
|
|
66
|
+
outdatedPackages,
|
|
65
67
|
packageScriptsMissing,
|
|
66
68
|
noCiConfig,
|
|
67
69
|
npmInstallInsteadOfNpmCi,
|
package/src/cli/output.js
CHANGED
|
@@ -14,6 +14,7 @@ Options:
|
|
|
14
14
|
--json Print machine-readable JSON.
|
|
15
15
|
--sarif Print SARIF for GitHub Code Scanning.
|
|
16
16
|
--todo Write an AI-ready todo.md to the scanned project.
|
|
17
|
+
--report Write a Markdown scan report.md to the current directory.
|
|
17
18
|
--no-color Disable color styling.
|
|
18
19
|
--no-banner Disable the intro banner.
|
|
19
20
|
--quiet Print only the summary.
|
package/src/cli/parseArgs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const FLAG_WITH_VALUE = new Set(["--fail-on", "--config", "--path"]);
|
|
2
|
-
const BOOLEAN_FLAGS = new Set(["--json", "--sarif", "--todo", "--verbose", "--help", "-h", "--version", "-v", "--no-color", "--no-banner", "--quiet"]);
|
|
2
|
+
const BOOLEAN_FLAGS = new Set(["--json", "--sarif", "--todo", "--report", "--verbose", "--help", "-h", "--version", "-v", "--no-color", "--no-banner", "--quiet"]);
|
|
3
3
|
|
|
4
4
|
export function parseArgs(argv) {
|
|
5
5
|
const args = {
|
|
@@ -10,6 +10,7 @@ export function parseArgs(argv) {
|
|
|
10
10
|
json: false,
|
|
11
11
|
sarif: false,
|
|
12
12
|
todo: false,
|
|
13
|
+
report: false,
|
|
13
14
|
verbose: false,
|
|
14
15
|
noColor: false,
|
|
15
16
|
noBanner: false,
|
|
@@ -48,6 +49,7 @@ export function parseArgs(argv) {
|
|
|
48
49
|
if (token === "--json") args.json = true;
|
|
49
50
|
if (token === "--sarif") args.sarif = true;
|
|
50
51
|
if (token === "--todo") args.todo = true;
|
|
52
|
+
if (token === "--report") args.report = true;
|
|
51
53
|
if (token === "--verbose") args.verbose = true;
|
|
52
54
|
if (token === "--no-color") args.noColor = true;
|
|
53
55
|
if (token === "--no-banner") args.noBanner = true;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const CHECK_STATUSES = ["pass", "warn", "fail", "skip"];
|
|
2
|
+
|
|
3
|
+
export function normalizeCheckResult(check, value = {}, findings = []) {
|
|
4
|
+
const status = normalizeStatus(value.status || deriveStatusFromFindings(findings));
|
|
5
|
+
const details = Array.isArray(value.details) ? value.details : detailsFromFindings(findings);
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
id: value.id || check.id,
|
|
9
|
+
title: value.title || check.title,
|
|
10
|
+
category: value.category || check.category,
|
|
11
|
+
status,
|
|
12
|
+
summary: value.summary || defaultSummary(status, findings),
|
|
13
|
+
details,
|
|
14
|
+
metadata: value.metadata || undefined
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function deriveStatusFromFindings(findings = []) {
|
|
19
|
+
if (!findings.length) return "pass";
|
|
20
|
+
if (findings.some((finding) => finding.severity === "critical" || finding.severity === "high")) return "fail";
|
|
21
|
+
return "warn";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function countByStatus(checks = []) {
|
|
25
|
+
return Object.fromEntries(
|
|
26
|
+
CHECK_STATUSES.map((status) => [status, checks.filter((check) => check.status === status).length])
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeStatus(value) {
|
|
31
|
+
const normalized = String(value || "pass").toLowerCase();
|
|
32
|
+
return CHECK_STATUSES.includes(normalized) ? normalized : "fail";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function defaultSummary(status, findings) {
|
|
36
|
+
if (status === "pass") return "No issues found.";
|
|
37
|
+
if (status === "skip") return "Skipped.";
|
|
38
|
+
if (!findings.length) return status === "fail" ? "Check failed." : "Check needs review.";
|
|
39
|
+
|
|
40
|
+
const count = findings.length;
|
|
41
|
+
const noun = count === 1 ? "finding" : "findings";
|
|
42
|
+
return `${count} ${noun} reported.`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function detailsFromFindings(findings) {
|
|
46
|
+
return findings.map((finding) => ({
|
|
47
|
+
message: finding.message,
|
|
48
|
+
file: finding.file,
|
|
49
|
+
line: finding.line,
|
|
50
|
+
severity: finding.severity,
|
|
51
|
+
recommendation: finding.recommendation
|
|
52
|
+
}));
|
|
53
|
+
}
|
package/src/core/scanner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import checks from "../checks/index.js";
|
|
2
2
|
import { createContext } from "./context.js";
|
|
3
|
+
import { normalizeCheckResult } from "./checkResults.js";
|
|
3
4
|
import { normalizeFinding, severityRank } from "./findings.js";
|
|
4
5
|
import { packageInfo } from "./packageInfo.js";
|
|
5
6
|
|
|
@@ -7,28 +8,54 @@ export async function scanProject(options = {}) {
|
|
|
7
8
|
const startedAt = new Date();
|
|
8
9
|
const context = await createContext(options);
|
|
9
10
|
const findings = [];
|
|
11
|
+
const checkResults = [];
|
|
10
12
|
const warnings = [];
|
|
11
13
|
|
|
12
14
|
for (const check of checks) {
|
|
13
|
-
if (context.config.checks[check.id] === false)
|
|
15
|
+
if (context.config.checks[check.id] === false) {
|
|
16
|
+
checkResults.push(normalizeCheckResult(check, {
|
|
17
|
+
status: "skip",
|
|
18
|
+
summary: "Disabled by configuration."
|
|
19
|
+
}));
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
14
22
|
|
|
15
23
|
try {
|
|
16
|
-
const
|
|
24
|
+
const rawResult = await check.run(context);
|
|
25
|
+
const checkFindings = Array.isArray(rawResult) ? rawResult : rawResult?.findings;
|
|
26
|
+
const explicitCheckResult = Array.isArray(rawResult) ? null : rawResult?.result || rawResult?.checkResult;
|
|
27
|
+
|
|
17
28
|
if (!Array.isArray(checkFindings)) {
|
|
18
29
|
warnings.push({
|
|
19
30
|
checkId: check.id,
|
|
20
31
|
message: "Check returned a non-array result and was ignored."
|
|
21
32
|
});
|
|
33
|
+
checkResults.push(normalizeCheckResult(check, {
|
|
34
|
+
status: "fail",
|
|
35
|
+
summary: "Check returned an invalid result.",
|
|
36
|
+
details: [{ message: "Check returned a non-array result and was ignored." }]
|
|
37
|
+
}));
|
|
22
38
|
continue;
|
|
23
39
|
}
|
|
40
|
+
|
|
41
|
+
const normalizedFindings = [];
|
|
24
42
|
for (const finding of checkFindings) {
|
|
25
|
-
|
|
43
|
+
const normalizedFinding = normalizeFinding(check, finding);
|
|
44
|
+
normalizedFindings.push(normalizedFinding);
|
|
45
|
+
findings.push(normalizedFinding);
|
|
26
46
|
}
|
|
47
|
+
checkResults.push(normalizeCheckResult(check, explicitCheckResult || {}, normalizedFindings));
|
|
27
48
|
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28
50
|
warnings.push({
|
|
29
51
|
checkId: check.id,
|
|
30
|
-
message
|
|
52
|
+
message
|
|
31
53
|
});
|
|
54
|
+
checkResults.push(normalizeCheckResult(check, {
|
|
55
|
+
status: "fail",
|
|
56
|
+
summary: message,
|
|
57
|
+
details: [{ message }]
|
|
58
|
+
}));
|
|
32
59
|
}
|
|
33
60
|
}
|
|
34
61
|
|
|
@@ -40,12 +67,14 @@ export async function scanProject(options = {}) {
|
|
|
40
67
|
|
|
41
68
|
return {
|
|
42
69
|
findings,
|
|
70
|
+
checks: checkResults,
|
|
43
71
|
warnings,
|
|
44
72
|
config: context.config,
|
|
45
73
|
meta: {
|
|
46
74
|
tool: "ItWorksBut",
|
|
47
75
|
version: packageInfo.version,
|
|
48
76
|
rootPath: context.rootPath,
|
|
77
|
+
packageName: context.packageJson?.name,
|
|
49
78
|
packageManager: context.packageManager,
|
|
50
79
|
gitAvailable: context.gitAvailable,
|
|
51
80
|
filesScanned: context.allFiles.length,
|
|
@@ -12,15 +12,17 @@ import {
|
|
|
12
12
|
|
|
13
13
|
export function reportConsole(result, options = {}) {
|
|
14
14
|
const { findings, warnings, config, meta } = result;
|
|
15
|
+
const checks = result.checks || [];
|
|
15
16
|
const counts = countBySeverity(findings);
|
|
16
17
|
const colors = getChalk(options);
|
|
17
18
|
const rich = isFancyOutputEnabled(options);
|
|
19
|
+
const hasReviewCheck = checks.some(check => check.status === 'warn' || check.status === 'fail');
|
|
18
20
|
|
|
19
21
|
if (!options.quiet && !rich) {
|
|
20
22
|
process.stdout.write(`${colors.bold('ItWorksBut receipts')}\n\n`);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
if (!options.quiet && findings.length === 0) {
|
|
25
|
+
if (!options.quiet && findings.length === 0 && !hasReviewCheck) {
|
|
24
26
|
process.stdout.write(
|
|
25
27
|
`${colors.green ? colors.green('Suspiciously clean. No findings.') : 'Suspiciously clean. No findings.'}\n\n`,
|
|
26
28
|
);
|
|
@@ -36,6 +38,10 @@ export function reportConsole(result, options = {}) {
|
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
if (!options.quiet) {
|
|
42
|
+
writeOutdatedPackagesCheck(checks, options);
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
if (options.verbose && warnings.length > 0) {
|
|
40
46
|
process.stdout.write('WARNINGS\n');
|
|
41
47
|
for (const warning of warnings) {
|
|
@@ -55,6 +61,41 @@ export function reportConsole(result, options = {}) {
|
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
function writeOutdatedPackagesCheck(checks, options) {
|
|
65
|
+
const check = checks.find(candidate => candidate.id === 'dependencies.outdated-packages');
|
|
66
|
+
if (!check) return;
|
|
67
|
+
|
|
68
|
+
const colors = getChalk(options);
|
|
69
|
+
const title = colors.bold('Outdated packages');
|
|
70
|
+
|
|
71
|
+
if (check.status === 'pass') {
|
|
72
|
+
process.stdout.write(`${colors.green('✓')} ${title}: ${check.summary}\n\n`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (check.status === 'skip') {
|
|
77
|
+
process.stdout.write(`- ${title}: ${check.summary}\n\n`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (check.status === 'fail') {
|
|
82
|
+
process.stdout.write(`${colors.red('✖')} ${title}: ${check.summary}\n\n`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
process.stdout.write(`${colors.yellow('⚠')} ${title}: ${check.summary}\n`);
|
|
87
|
+
const packages = check.details.filter(detail => detail?.name).slice(0, 10);
|
|
88
|
+
for (const detail of packages) {
|
|
89
|
+
process.stdout.write(
|
|
90
|
+
` - ${detail.name}: ${detail.current} → ${detail.wanted} wanted, ${detail.latest} latest\n`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (check.details.length > packages.length) {
|
|
94
|
+
process.stdout.write(` - and ${check.details.length - packages.length} more\n`);
|
|
95
|
+
}
|
|
96
|
+
process.stdout.write('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
58
99
|
function writeFinding(finding, options) {
|
|
59
100
|
const colors = getChalk(options);
|
|
60
101
|
const severity = formatSeverity(finding.severity, options);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { countBySeverity, getExitCode } from "../core/findings.js";
|
|
2
|
+
import { countByStatus } from "../core/checkResults.js";
|
|
2
3
|
|
|
3
4
|
export function reportJson(result) {
|
|
4
5
|
return {
|
|
@@ -8,9 +9,11 @@ export function reportJson(result) {
|
|
|
8
9
|
summary: {
|
|
9
10
|
total: result.findings.length,
|
|
10
11
|
bySeverity: countBySeverity(result.findings),
|
|
12
|
+
byStatus: countByStatus(result.checks || []),
|
|
11
13
|
failOn: result.config.failOn,
|
|
12
14
|
exitCode: getExitCode(result.findings, result.config.failOn)
|
|
13
15
|
},
|
|
16
|
+
checks: result.checks || [],
|
|
14
17
|
findings: result.findings,
|
|
15
18
|
warnings: result.warnings
|
|
16
19
|
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { countByStatus } from "../core/checkResults.js";
|
|
4
|
+
|
|
5
|
+
const ANSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
6
|
+
|
|
7
|
+
export async function writeMarkdownReport(result, options = {}) {
|
|
8
|
+
const filePath = options.filePath || path.join(options.directoryPath || process.cwd(), "report.md");
|
|
9
|
+
let overwritten = false;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(filePath);
|
|
13
|
+
overwritten = true;
|
|
14
|
+
} catch {
|
|
15
|
+
overwritten = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
await fs.writeFile(filePath, reportMarkdown(result), "utf8");
|
|
19
|
+
return { filePath, overwritten };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function reportMarkdown(result) {
|
|
23
|
+
const checks = result.checks || checksFromFindings(result.findings || []);
|
|
24
|
+
const counts = countByStatus(checks);
|
|
25
|
+
const projectName = result.meta?.packageName || path.basename(result.meta?.rootPath || "") || "unknown";
|
|
26
|
+
const generatedAt = formatTimestamp(result.meta?.completedAt || new Date());
|
|
27
|
+
|
|
28
|
+
return stripAnsi(`${[
|
|
29
|
+
"# ItWorksBut Scan Report",
|
|
30
|
+
"",
|
|
31
|
+
`Generated: ${generatedAt}`,
|
|
32
|
+
"",
|
|
33
|
+
`Project: ${projectName}`,
|
|
34
|
+
`Path: ${result.meta?.rootPath || "unknown"}`,
|
|
35
|
+
"",
|
|
36
|
+
"## Summary",
|
|
37
|
+
"",
|
|
38
|
+
"| Status | Count |",
|
|
39
|
+
"|---|---:|",
|
|
40
|
+
`| Pass | ${counts.pass} |`,
|
|
41
|
+
`| Warn | ${counts.warn} |`,
|
|
42
|
+
`| Fail | ${counts.fail} |`,
|
|
43
|
+
`| Skip | ${counts.skip} |`,
|
|
44
|
+
"",
|
|
45
|
+
"## Checks",
|
|
46
|
+
"",
|
|
47
|
+
renderChecks(checks),
|
|
48
|
+
renderScannerWarnings(result.warnings || []),
|
|
49
|
+
renderRecommendations(checks, result.findings || []),
|
|
50
|
+
].filter((line) => line !== null && line !== undefined).join("\n")}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderChecks(checks) {
|
|
54
|
+
if (!checks.length) return "No checks were recorded.\n";
|
|
55
|
+
|
|
56
|
+
return checks.map(renderCheck).join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderCheck(check) {
|
|
60
|
+
return [
|
|
61
|
+
`### ${check.title || check.id}`,
|
|
62
|
+
"",
|
|
63
|
+
`Status: ${check.status || "unknown"}`,
|
|
64
|
+
"",
|
|
65
|
+
"Summary:",
|
|
66
|
+
check.summary || "No summary available.",
|
|
67
|
+
"",
|
|
68
|
+
"Details:",
|
|
69
|
+
renderDetails(check),
|
|
70
|
+
""
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderDetails(check) {
|
|
75
|
+
const details = Array.isArray(check.details) ? check.details : [];
|
|
76
|
+
if (!details.length) return "None.";
|
|
77
|
+
|
|
78
|
+
if (isOutdatedPackageCheck(check)) {
|
|
79
|
+
return renderOutdatedPackageTable(details);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return details.map(renderDetailBullet).join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderOutdatedPackageTable(details) {
|
|
86
|
+
const packageDetails = details.filter((detail) => detail?.name);
|
|
87
|
+
if (!packageDetails.length) return details.map(renderDetailBullet).join("\n");
|
|
88
|
+
|
|
89
|
+
return [
|
|
90
|
+
"| Package | Current | Wanted | Latest | Type |",
|
|
91
|
+
"|---|---:|---:|---:|---|",
|
|
92
|
+
...packageDetails.map((detail) => (
|
|
93
|
+
`| ${escapeTable(detail.name)} | ${escapeTable(detail.current)} | ${escapeTable(detail.wanted)} | ${escapeTable(detail.latest)} | ${escapeTable(detail.type)} |`
|
|
94
|
+
))
|
|
95
|
+
].join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderDetailBullet(detail) {
|
|
99
|
+
if (typeof detail === "string") return `- ${detail}`;
|
|
100
|
+
if (!detail || typeof detail !== "object") return `- ${String(detail)}`;
|
|
101
|
+
|
|
102
|
+
if (detail.message) return `- ${detail.message}`;
|
|
103
|
+
return `- ${Object.entries(detail).map(([key, value]) => `${key}: ${String(value)}`).join(", ")}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderScannerWarnings(warnings) {
|
|
107
|
+
if (!warnings.length) return "";
|
|
108
|
+
|
|
109
|
+
return [
|
|
110
|
+
"## Scanner Warnings",
|
|
111
|
+
"",
|
|
112
|
+
...warnings.map((warning) => `- ${warning.checkId}: ${warning.message}`),
|
|
113
|
+
""
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderRecommendations(checks, findings) {
|
|
118
|
+
const needsReview = checks.some((check) => check.status === "warn" || check.status === "fail");
|
|
119
|
+
if (!needsReview && findings.length === 0) return "";
|
|
120
|
+
|
|
121
|
+
const recommendations = [];
|
|
122
|
+
if (checks.some((check) => isOutdatedPackageCheck(check) && (check.status === "warn" || check.status === "fail"))) {
|
|
123
|
+
recommendations.push("Update outdated packages carefully.");
|
|
124
|
+
recommendations.push("Review major-version upgrades manually.");
|
|
125
|
+
recommendations.push("Run tests after dependency updates.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const finding of findings) {
|
|
129
|
+
if (finding.recommendation) recommendations.push(finding.recommendation);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (checks.some((check) => check.status === "fail")) {
|
|
133
|
+
recommendations.push("Review failed checks and scanner warnings before shipping.");
|
|
134
|
+
}
|
|
135
|
+
if (recommendations.length === 0) {
|
|
136
|
+
recommendations.push("Review warning and failure details before shipping.");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return [
|
|
140
|
+
"## Recommendations",
|
|
141
|
+
"",
|
|
142
|
+
...unique(recommendations).map((recommendation) => `- ${recommendation}`),
|
|
143
|
+
""
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function checksFromFindings(findings) {
|
|
148
|
+
const byCheck = new Map();
|
|
149
|
+
for (const finding of findings) {
|
|
150
|
+
const existing = byCheck.get(finding.checkId) || {
|
|
151
|
+
id: finding.checkId,
|
|
152
|
+
title: finding.title,
|
|
153
|
+
category: finding.category,
|
|
154
|
+
status: finding.severity === "critical" || finding.severity === "high" ? "fail" : "warn",
|
|
155
|
+
summary: "Finding reported.",
|
|
156
|
+
details: []
|
|
157
|
+
};
|
|
158
|
+
existing.details.push({
|
|
159
|
+
message: finding.message,
|
|
160
|
+
file: finding.file,
|
|
161
|
+
line: finding.line,
|
|
162
|
+
severity: finding.severity
|
|
163
|
+
});
|
|
164
|
+
byCheck.set(finding.checkId, existing);
|
|
165
|
+
}
|
|
166
|
+
return [...byCheck.values()];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isOutdatedPackageCheck(check) {
|
|
170
|
+
return check.id === "dependencies.outdated-packages" || check.title === "Outdated packages";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatTimestamp(value) {
|
|
174
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
175
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
176
|
+
|
|
177
|
+
const pad = (number) => String(number).padStart(2, "0");
|
|
178
|
+
return [
|
|
179
|
+
date.getFullYear(),
|
|
180
|
+
"-",
|
|
181
|
+
pad(date.getMonth() + 1),
|
|
182
|
+
"-",
|
|
183
|
+
pad(date.getDate()),
|
|
184
|
+
" ",
|
|
185
|
+
pad(date.getHours()),
|
|
186
|
+
":",
|
|
187
|
+
pad(date.getMinutes()),
|
|
188
|
+
":",
|
|
189
|
+
pad(date.getSeconds())
|
|
190
|
+
].join("");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function escapeTable(value) {
|
|
194
|
+
return stripAnsi(String(value ?? "unknown")).replace(/\|/g, "\\|");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function stripAnsi(value) {
|
|
198
|
+
return String(value).replace(ANSI_PATTERN, "");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function unique(values) {
|
|
202
|
+
return [...new Set(values.filter(Boolean))];
|
|
203
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function detectOutdatedPackageManager({ packageJson, files = [] } = {}) {
|
|
2
|
+
if (!packageJson) {
|
|
3
|
+
return {
|
|
4
|
+
status: "skip",
|
|
5
|
+
manager: null,
|
|
6
|
+
summary: "skipped, no package.json found"
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (files.includes("pnpm-lock.yaml")) return packageManager("pnpm");
|
|
11
|
+
if (files.includes("yarn.lock")) return packageManager("yarn");
|
|
12
|
+
if (files.includes("package-lock.json")) return packageManager("npm");
|
|
13
|
+
return packageManager("npm");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getOutdatedCommand(manager) {
|
|
17
|
+
if (manager === "pnpm") return { command: "pnpm", args: ["outdated", "--json"] };
|
|
18
|
+
if (manager === "yarn") return { command: "yarn", args: ["outdated", "--json"] };
|
|
19
|
+
return { command: "npm", args: ["outdated", "--json"] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function packageManager(manager) {
|
|
23
|
+
return {
|
|
24
|
+
status: "run",
|
|
25
|
+
manager,
|
|
26
|
+
...getOutdatedCommand(manager)
|
|
27
|
+
};
|
|
28
|
+
}
|