upstream-radar 0.32.0 → 0.33.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 +396 -43
- package/cordis.patch.yml +1 -0
- package/dist/src/cli.js +950 -33
- package/dist/src/cli.js.map +1 -1
- package/dist/src/compatibility.d.ts +3 -1
- package/dist/src/compatibility.d.ts.map +1 -1
- package/dist/src/compatibility.js +146 -12
- package/dist/src/compatibility.js.map +1 -1
- package/dist/src/demo.d.ts +13 -0
- package/dist/src/demo.d.ts.map +1 -0
- package/dist/src/demo.js +85 -0
- package/dist/src/demo.js.map +1 -0
- package/dist/src/doctor.d.ts.map +1 -1
- package/dist/src/doctor.js +62 -0
- package/dist/src/doctor.js.map +1 -1
- package/dist/src/dsh-analysis.d.ts.map +1 -1
- package/dist/src/dsh-analysis.js +2 -1
- package/dist/src/dsh-analysis.js.map +1 -1
- package/dist/src/dsh-plugin.d.ts +9 -3
- package/dist/src/dsh-plugin.d.ts.map +1 -1
- package/dist/src/dsh-plugin.js +87 -11
- package/dist/src/dsh-plugin.js.map +1 -1
- package/dist/src/dsh-runtime.d.ts +7 -0
- package/dist/src/dsh-runtime.d.ts.map +1 -1
- package/dist/src/dsh-runtime.js +21 -0
- package/dist/src/dsh-runtime.js.map +1 -1
- package/dist/src/github-advisory.d.ts +25 -0
- package/dist/src/github-advisory.d.ts.map +1 -0
- package/dist/src/github-advisory.js +335 -0
- package/dist/src/github-advisory.js.map +1 -0
- package/dist/src/graph.d.ts +2 -0
- package/dist/src/graph.d.ts.map +1 -1
- package/dist/src/graph.js +600 -4
- package/dist/src/graph.js.map +1 -1
- package/dist/src/index.d.ts +14 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +13 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/init.d.ts +44 -2
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/init.js +62 -5
- package/dist/src/init.js.map +1 -1
- package/dist/src/installed-graph.d.ts +3 -1
- package/dist/src/installed-graph.d.ts.map +1 -1
- package/dist/src/installed-graph.js +101 -2
- package/dist/src/installed-graph.js.map +1 -1
- package/dist/src/inventory.d.ts.map +1 -1
- package/dist/src/inventory.js +76 -2
- package/dist/src/inventory.js.map +1 -1
- package/dist/src/notification-policy.d.ts +20 -0
- package/dist/src/notification-policy.d.ts.map +1 -0
- package/dist/src/notification-policy.js +87 -0
- package/dist/src/notification-policy.js.map +1 -0
- package/dist/src/npm-release.d.ts.map +1 -1
- package/dist/src/npm-release.js +18 -7
- package/dist/src/npm-release.js.map +1 -1
- package/dist/src/osv.d.ts.map +1 -1
- package/dist/src/osv.js +1 -0
- package/dist/src/osv.js.map +1 -1
- package/dist/src/quickstart.d.ts +31 -0
- package/dist/src/quickstart.d.ts.map +1 -0
- package/dist/src/quickstart.js +266 -0
- package/dist/src/quickstart.js.map +1 -0
- package/dist/src/radar-history.d.ts +22 -0
- package/dist/src/radar-history.d.ts.map +1 -0
- package/dist/src/radar-history.js +80 -0
- package/dist/src/radar-history.js.map +1 -0
- package/dist/src/radar-render.d.ts.map +1 -1
- package/dist/src/radar-render.js +106 -1
- package/dist/src/radar-render.js.map +1 -1
- package/dist/src/radar-state.d.ts.map +1 -1
- package/dist/src/radar-state.js +264 -6
- package/dist/src/radar-state.js.map +1 -1
- package/dist/src/radar-status.d.ts +36 -1
- package/dist/src/radar-status.d.ts.map +1 -1
- package/dist/src/radar-status.js +199 -16
- package/dist/src/radar-status.js.map +1 -1
- package/dist/src/radar-types.d.ts +128 -3
- package/dist/src/radar-types.d.ts.map +1 -1
- package/dist/src/radar-types.js +4 -0
- package/dist/src/radar-types.js.map +1 -1
- package/dist/src/radar.d.ts +7 -1
- package/dist/src/radar.d.ts.map +1 -1
- package/dist/src/radar.js +432 -70
- package/dist/src/radar.js.map +1 -1
- package/dist/src/render.d.ts.map +1 -1
- package/dist/src/render.js +10 -0
- package/dist/src/render.js.map +1 -1
- package/dist/src/threat-intel.d.ts +46 -0
- package/dist/src/threat-intel.d.ts.map +1 -0
- package/dist/src/threat-intel.js +243 -0
- package/dist/src/threat-intel.js.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/vulnerability-priority.d.ts +16 -0
- package/dist/src/vulnerability-priority.d.ts.map +1 -0
- package/dist/src/vulnerability-priority.js +20 -0
- package/dist/src/vulnerability-priority.js.map +1 -0
- package/dist/src/webhook.d.ts +84 -0
- package/dist/src/webhook.d.ts.map +1 -0
- package/dist/src/webhook.js +432 -0
- package/dist/src/webhook.js.map +1 -0
- package/docs/README.zh-CN.md +394 -40
- package/package.json +22 -3
- package/schemas/quickstart.schema.json +56 -0
- package/schemas/webhook.schema.json +53 -0
package/dist/src/cli.js
CHANGED
|
@@ -2,71 +2,414 @@
|
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { access, readFile } from 'node:fs/promises';
|
|
5
|
-
import { resolve } from 'node:path';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { renderCompatibilityBenchmark, runCompatibilityBenchmark } from './compatibility-benchmark.js';
|
|
7
7
|
import { assessCompatibilityChange } from './compatibility.js';
|
|
8
8
|
import { probeDshLoad, probeDshLoadMatrix, renderDshLoadMatrix, renderDshLoadProbe } from './dsh-probe.js';
|
|
9
9
|
import { createAnalysisTask, renderAgentAnalysisPrompt } from './dsh-analysis.js';
|
|
10
10
|
import { createDoctorReport, renderDoctorReport } from './doctor.js';
|
|
11
|
+
import { createDemoReport, renderDemo } from './demo.js';
|
|
11
12
|
import { GitHubReleaseClient } from './github-release.js';
|
|
12
|
-
import {
|
|
13
|
+
import { parseNpmLockGraph, parsePnpmLockGraph } from './graph.js';
|
|
14
|
+
import { createRadarConfigFromDshProfile, createRadarConfigFromNpmLock, createRadarConfigFromPnpmLock, discoverDshProfiles, refreshRadarConfigFromConfiguredProfile, resolveDshProfileDirectory, writeDshPatch, writeRadarConfig } from './init.js';
|
|
13
15
|
import { parsePackageManifestSnapshot, parseRadarConfig } from './inventory.js';
|
|
14
16
|
import { inspectNpmPackage } from './npm.js';
|
|
15
17
|
import { NpmCandidateGraphClient } from './npm-candidate.js';
|
|
16
18
|
import { NpmReleaseClient } from './npm-release.js';
|
|
19
|
+
import { createNotificationPolicyMap, filterNotifiableRadarEvents } from './notification-policy.js';
|
|
17
20
|
import { OsvClient } from './osv.js';
|
|
18
21
|
import { verdictAtLeast } from './policy.js';
|
|
22
|
+
import { createQuickstartReport, renderQuickstartReport } from './quickstart.js';
|
|
23
|
+
import { GitHubAdvisoryClient } from './github-advisory.js';
|
|
19
24
|
import { emptyRadarState, pollRadar } from './radar.js';
|
|
20
25
|
import { evaluateRadarPolicy, RADAR_COMPATIBILITY_FAIL_THRESHOLDS, RADAR_FAIL_THRESHOLDS, renderRadarPolicy, } from './radar-policy.js';
|
|
21
26
|
import { renderRadarEvents } from './radar-render.js';
|
|
27
|
+
import { createRadarHistory, renderRadarHistory } from './radar-history.js';
|
|
22
28
|
import { loadRadarState, saveRadarState } from './radar-state.js';
|
|
23
|
-
import { createRadarStatus, renderRadarStatus } from './radar-status.js';
|
|
29
|
+
import { createRadarNext, createRadarStatus, renderRadarNext, renderRadarStatus } from './radar-status.js';
|
|
24
30
|
import { renderTextReport } from './render.js';
|
|
25
31
|
import { scanDirectory } from './scan.js';
|
|
32
|
+
import { CisaKevClient, EpssClient } from './threat-intel.js';
|
|
26
33
|
import { TOOL_VERSION } from './version.js';
|
|
34
|
+
import { eventsForRadarWebhookTarget, markRadarWebhookEventsDelivered, markRadarWebhookEventsDeliveredForRoute, normalizeRadarWebhookUrl, queueRadarWebhookEvents, queueRadarWebhookEventsForRoute, resolveRadarWebhookTargets, sendRadarWebhook, undeliveredRadarWebhookEvents, undeliveredRadarWebhookEventsForRoute, } from './webhook.js';
|
|
27
35
|
const VALID_THRESHOLDS = new Set(['warn', 'review', 'block', 'never']);
|
|
28
36
|
const VALID_RADAR_THRESHOLDS = new Set(RADAR_FAIL_THRESHOLDS);
|
|
29
37
|
const VALID_RADAR_COMPATIBILITY_THRESHOLDS = new Set(RADAR_COMPATIBILITY_FAIL_THRESHOLDS);
|
|
38
|
+
const VALID_NOTIFICATION_SEVERITIES = new Set(['info', 'low', 'medium', 'high', 'critical']);
|
|
30
39
|
function safeErrorMessage(value, maxLength = 2_048) {
|
|
31
40
|
return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, character => (`\\u${character.codePointAt(0)?.toString(16).padStart(4, '0') ?? '0000'}`)).slice(0, maxLength);
|
|
32
41
|
}
|
|
33
42
|
function shellQuote(value) {
|
|
34
43
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
35
44
|
}
|
|
45
|
+
function parseNotificationPolicyArguments(minimumSeverity, quietHoursValue) {
|
|
46
|
+
if (minimumSeverity === undefined && quietHoursValue === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (minimumSeverity !== undefined && !VALID_NOTIFICATION_SEVERITIES.has(minimumSeverity)) {
|
|
49
|
+
throw new Error('--minimum-severity must be info, low, medium, high or critical');
|
|
50
|
+
}
|
|
51
|
+
let quietHours;
|
|
52
|
+
if (quietHoursValue !== undefined) {
|
|
53
|
+
const comma = quietHoursValue.indexOf(',');
|
|
54
|
+
const interval = comma < 0 ? '' : quietHoursValue.slice(comma + 1);
|
|
55
|
+
const dash = interval.indexOf('-');
|
|
56
|
+
const timezone = comma < 1 ? '' : quietHoursValue.slice(0, comma);
|
|
57
|
+
const start = dash < 1 ? '' : interval.slice(0, dash);
|
|
58
|
+
const end = dash < 0 ? '' : interval.slice(dash + 1);
|
|
59
|
+
if (!/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(start)
|
|
60
|
+
|| !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(end)
|
|
61
|
+
|| start === end
|
|
62
|
+
|| timezone.length === 0) {
|
|
63
|
+
throw new Error('--quiet-hours must use <IANA timezone>,<HH:MM>-<HH:MM> with different start and end times');
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
new Intl.DateTimeFormat('en-US', { timeZone: timezone }).format();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Error('--quiet-hours must start with a valid IANA timezone');
|
|
70
|
+
}
|
|
71
|
+
quietHours = { timezone, start, end };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
...(minimumSeverity === undefined ? {} : { minimumSeverity: minimumSeverity }),
|
|
75
|
+
...(quietHours === undefined ? {} : { quietHours }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function commandUsage(key) {
|
|
79
|
+
const help = {
|
|
80
|
+
setup: `Upstream Radar — install the exact Radar bundle into DSH and prepare monitoring
|
|
81
|
+
|
|
82
|
+
Usage:
|
|
83
|
+
upstream-radar setup [--profile <name>] [--start] [--project-name <name>]
|
|
84
|
+
|
|
85
|
+
What it does:
|
|
86
|
+
Installs Radar into the selected DSH profile, writes a reviewable config and
|
|
87
|
+
--patch overlay, then runs the local doctor check. By default it does not
|
|
88
|
+
start DSH or execute plugin business actions; --start opts into launching DSH
|
|
89
|
+
only after the doctor check passes.
|
|
90
|
+
|
|
91
|
+
Prerequisite:
|
|
92
|
+
Install DeepSeek Harness first and verify that \`dsh --help\` works.
|
|
93
|
+
|
|
94
|
+
Common options:
|
|
95
|
+
--profile <name> select a DSH profile (auto-selects the only candidate)
|
|
96
|
+
--project-name <name> name shown in incidents and Agent tasks
|
|
97
|
+
--workspace <path> project workspace used to route Agent follow-up
|
|
98
|
+
--repository <url> repository evidence passed to the Agent task
|
|
99
|
+
--webhook-url-env <name> env var containing this project's HTTPS webhook URL
|
|
100
|
+
--webhook-secret-env <name> optional env var for a Feishu/Lark V2 signing secret
|
|
101
|
+
--output <path> Radar config path (default: ./upstream-radar.config.json)
|
|
102
|
+
--dsh-patch <path> DSH overlay path (default: ./upstream-radar.dsh.yml)
|
|
103
|
+
--minimum-severity <level> minimum vulnerability notice level
|
|
104
|
+
--quiet-hours <tz,start-end> e.g. Asia/Shanghai,22:00-08:00
|
|
105
|
+
--start start DSH after the local wiring check passes
|
|
106
|
+
--no-install reuse an already installed Radar bundle
|
|
107
|
+
--no-dsh-patch use legacy environment-variable wiring
|
|
108
|
+
|
|
109
|
+
Notification controls:
|
|
110
|
+
Add an optional \`notificationPolicy\` block to a generated projects[] entry
|
|
111
|
+
to set \`minimumSeverity\` and timezone-aware \`quietHours\`. This changes only
|
|
112
|
+
delivery; active evidence and queued tasks remain durable.
|
|
113
|
+
|
|
114
|
+
Next:
|
|
115
|
+
Review the generated files, then run the printed doctor command and start DSH
|
|
116
|
+
with the printed --patch command. Pass --start only when you explicitly accept
|
|
117
|
+
starting DSH in the same invocation after the doctor check passes.
|
|
118
|
+
|
|
119
|
+
If the profile has no third-party plugin yet:
|
|
120
|
+
dsh plugin --profile <name> add <package>@<exact-version>
|
|
121
|
+
`,
|
|
122
|
+
init: `Upstream Radar — create a reviewable inventory
|
|
123
|
+
|
|
124
|
+
Usage:
|
|
125
|
+
upstream-radar init --profile <name> [options]
|
|
126
|
+
upstream-radar init --pnpm-lock <pnpm-lock.yaml> [options]
|
|
127
|
+
upstream-radar init --npm-lock <package-lock.json> [options]
|
|
128
|
+
|
|
129
|
+
What it does:
|
|
130
|
+
Reads a DSH profile or lockfile without executing plugin code. Lockfile mode
|
|
131
|
+
is the pre-install path; it writes a static graph for a later frozen check.
|
|
132
|
+
|
|
133
|
+
Common options:
|
|
134
|
+
--root <package>@<exact-version> explicit lockfile root when it cannot be inferred
|
|
135
|
+
--project-name <name> name shown in incidents
|
|
136
|
+
--webhook-url-env <name> env var containing this project's HTTPS webhook URL
|
|
137
|
+
--webhook-secret-env <name> optional env var for a Feishu/Lark V2 signing secret
|
|
138
|
+
--output <path> config path (default: ./upstream-radar.config.json)
|
|
139
|
+
--dsh-patch <path> write a self-contained DSH overlay (profile mode)
|
|
140
|
+
--minimum-severity <level> minimum vulnerability notice level
|
|
141
|
+
--quiet-hours <tz,start-end> e.g. Asia/Shanghai,22:00-08:00
|
|
142
|
+
--json print a compact machine-readable summary
|
|
143
|
+
--force replace an existing output file
|
|
144
|
+
|
|
145
|
+
Notification controls:
|
|
146
|
+
Add an optional \`notificationPolicy\` block to a generated projects[] entry
|
|
147
|
+
to set \`minimumSeverity\` and timezone-aware \`quietHours\`. This changes only
|
|
148
|
+
delivery; active evidence and queued tasks remain durable.
|
|
149
|
+
|
|
150
|
+
Project webhook routing:
|
|
151
|
+
Store only environment variable names in the config. Set the URL and optional
|
|
152
|
+
Feishu secret in the DSH process environment; each project's changed events
|
|
153
|
+
then use its own endpoint while the native DSH Agent route remains workspace-based.
|
|
154
|
+
|
|
155
|
+
Next:
|
|
156
|
+
upstream-radar radar check ./upstream-radar.config.json --frozen
|
|
157
|
+
`,
|
|
158
|
+
doctor: `Upstream Radar — verify local wiring without polling upstream sources
|
|
159
|
+
|
|
160
|
+
Usage:
|
|
161
|
+
upstream-radar doctor [config.json] [--profile <name>] [--patch <path>] [--json]
|
|
162
|
+
|
|
163
|
+
Checks:
|
|
164
|
+
config/state readability, Radar registration in DSH, overlay alignment,
|
|
165
|
+
dependency coverage, and local webhook configuration. It does not contact
|
|
166
|
+
OSV, npm, GitHub, DSH, or a model.
|
|
167
|
+
`,
|
|
168
|
+
scan: `Upstream Radar — inspect a local package directory without running it
|
|
169
|
+
|
|
170
|
+
Usage:
|
|
171
|
+
upstream-radar scan <directory> [--json] [--fail-on <warn|review|block|never>]
|
|
172
|
+
|
|
173
|
+
Use this for an unpacked plugin before installation. Lifecycle scripts, remote
|
|
174
|
+
shell patterns, unsafe symlinks, mutable dependencies, and DSH bundle metadata
|
|
175
|
+
are recorded as bounded evidence.
|
|
176
|
+
`,
|
|
177
|
+
inspect: `Upstream Radar — inspect one exact npm artifact before installation
|
|
178
|
+
|
|
179
|
+
Usage:
|
|
180
|
+
upstream-radar inspect npm:<package>@<exact-version> [--deep] [--json]
|
|
181
|
+
[--registry <https-url>] [--fail-on <warn|review|block|never>]
|
|
182
|
+
|
|
183
|
+
--deep downloads the exact tarball, verifies npm integrity/signatures and
|
|
184
|
+
provenance when available, resolves the dependency graph with scripts disabled,
|
|
185
|
+
and queries the implemented vulnerability checks. An empty finding list is not
|
|
186
|
+
a safety certificate; check the coverage verdict before admitting the package.
|
|
187
|
+
The default gate exits 2 for review or block; use --fail-on block when review
|
|
188
|
+
should remain visible without failing CI.
|
|
189
|
+
`,
|
|
190
|
+
graph: `Upstream Radar — read a lockfile into the canonical dependency graph
|
|
191
|
+
|
|
192
|
+
Usage:
|
|
193
|
+
upstream-radar graph pnpm-lock <pnpm-lock.yaml> [--root <package>@<exact-version>] [--json]
|
|
194
|
+
upstream-radar graph npm-lock <package-lock.json> [--root <package>@<exact-version>] [--json]
|
|
195
|
+
|
|
196
|
+
This command is offline and does not install packages, run lifecycle scripts,
|
|
197
|
+
load plugin code, or query vulnerability sources.
|
|
198
|
+
`,
|
|
199
|
+
probe: `Upstream Radar — test whether a DSH bundle loads in disposable profiles
|
|
200
|
+
|
|
201
|
+
Usage:
|
|
202
|
+
upstream-radar probe dsh-load <package.tgz> [--dsh-version <exact-version>] [--json]
|
|
203
|
+
upstream-radar probe dsh-matrix <package.tgz> --dsh-version <v1>,<v2>,... [--json]
|
|
204
|
+
|
|
205
|
+
The probe is bounded and isolated. It is a compatibility/load check, not a
|
|
206
|
+
semantic safety review or a substitute for dependency monitoring.
|
|
207
|
+
`,
|
|
208
|
+
demo: `Upstream Radar — show the exact-path-to-DSH handoff without side effects
|
|
209
|
+
|
|
210
|
+
Usage:
|
|
211
|
+
upstream-radar demo [--json]
|
|
212
|
+
|
|
213
|
+
The demo is network-free and uses only a local fixture. It does not inspect your
|
|
214
|
+
repository, install a plugin, start DSH, or claim that its advisory is real.
|
|
215
|
+
`,
|
|
216
|
+
quickstart: `Upstream Radar — choose the smallest honest first-use path
|
|
217
|
+
|
|
218
|
+
Usage:
|
|
219
|
+
upstream-radar quickstart [directory] [--json]
|
|
220
|
+
|
|
221
|
+
What it does:
|
|
222
|
+
Inspects only the selected directory and local DSH profile metadata. It finds
|
|
223
|
+
an existing Radar config, one supported lockfile, or eligible DSH profiles,
|
|
224
|
+
then prints the next commands in the right order.
|
|
225
|
+
|
|
226
|
+
Safety:
|
|
227
|
+
This command never installs packages, starts DSH, queries vulnerability
|
|
228
|
+
sources, or executes plugin code. It does not choose between two lockfiles or
|
|
229
|
+
multiple DSH profiles on your behalf.
|
|
230
|
+
`,
|
|
231
|
+
benchmark: `Upstream Radar — run offline compatibility-rule contracts
|
|
232
|
+
|
|
233
|
+
Usage:
|
|
234
|
+
upstream-radar benchmark compatibility [--json]
|
|
235
|
+
`,
|
|
236
|
+
radar: `Upstream Radar — monitor a reviewed inventory
|
|
237
|
+
|
|
238
|
+
Usage:
|
|
239
|
+
upstream-radar radar check <config.json> [options]
|
|
240
|
+
upstream-radar radar watch <config.json> [options]
|
|
241
|
+
upstream-radar radar status <config.json> [options]
|
|
242
|
+
upstream-radar radar history <config.json> [options]
|
|
243
|
+
upstream-radar radar compare <config.json> <before.json> <candidate.json> [options]
|
|
244
|
+
|
|
245
|
+
Use 'radar status' and 'radar history' for local, no-network diagnosis. Use
|
|
246
|
+
--frozen for CI or any run that must not read a live DSH profile.
|
|
247
|
+
`,
|
|
248
|
+
'radar check': `Upstream Radar — run one vulnerability and compatibility check
|
|
249
|
+
|
|
250
|
+
Usage:
|
|
251
|
+
upstream-radar radar check <config.json> [--state <state.json>] [--frozen]
|
|
252
|
+
[--fail-on <severity>] [--fail-on-compatibility <never|breaking|any>]
|
|
253
|
+
[--webhook <https-url>] [--threat-intel] [--json]
|
|
254
|
+
|
|
255
|
+
One cycle queries the configured sources and persists changed incidents. Use
|
|
256
|
+
--frozen when the config is the reviewed graph you want to enforce.
|
|
257
|
+
|
|
258
|
+
--threat-intel adds CISA KEV and FIRST EPSS prioritization signals for matched
|
|
259
|
+
CVEs. It is opt-in here; the native DSH bundle enables it by default.
|
|
260
|
+
`,
|
|
261
|
+
'radar watch': `Upstream Radar — keep monitoring a reviewed inventory
|
|
262
|
+
|
|
263
|
+
Usage:
|
|
264
|
+
upstream-radar radar watch <config.json> [--interval <seconds>] [--once]
|
|
265
|
+
[--state <state.json>] [--frozen] [--fail-on <severity>]
|
|
266
|
+
[--fail-on-compatibility <never|breaking|any>] [--webhook <https-url>]
|
|
267
|
+
[--threat-intel]
|
|
268
|
+
|
|
269
|
+
Use --once in CI. A long-running watch keeps polling and should not be given a
|
|
270
|
+
policy gate that would make it exit on the first incident.
|
|
271
|
+
`,
|
|
272
|
+
'radar status': `Upstream Radar — see the local monitoring snapshot
|
|
273
|
+
|
|
274
|
+
Usage:
|
|
275
|
+
upstream-radar radar status <config.json> [--state <state.json>]
|
|
276
|
+
[--fail-on <severity>] [--fail-on-compatibility <never|breaking|any>] [--json]
|
|
277
|
+
|
|
278
|
+
This command never polls OSV, npm, GitHub, or DSH. It shows whether monitoring
|
|
279
|
+
has run, coverage, active incidents, queued Agent tasks, and the next useful
|
|
280
|
+
action.
|
|
281
|
+
`,
|
|
282
|
+
'radar next': `Upstream Radar — show the one next action for the highest-priority incident
|
|
283
|
+
|
|
284
|
+
Usage:
|
|
285
|
+
upstream-radar radar next <config.json> [--state <state.json>] [--json]
|
|
286
|
+
|
|
287
|
+
This command is read-only and does not poll upstream sources. It selects the
|
|
288
|
+
same first incident as 'radar status', then points to the pending DSH task,
|
|
289
|
+
verified analysis, or the next check command.
|
|
290
|
+
`,
|
|
291
|
+
'radar history': `Upstream Radar — inspect the durable transition ledger
|
|
292
|
+
|
|
293
|
+
Usage:
|
|
294
|
+
upstream-radar radar history <config.json> [--state <state.json>] [--limit <n>] [--json]
|
|
295
|
+
|
|
296
|
+
This command is local-only and shows new, updated, resolved, and source-health
|
|
297
|
+
transitions, including their exact affected paths.
|
|
298
|
+
`,
|
|
299
|
+
'radar compare': `Upstream Radar — compare one reviewed release candidate
|
|
300
|
+
|
|
301
|
+
Usage:
|
|
302
|
+
upstream-radar radar compare <config.json> <before.json> <candidate.json>
|
|
303
|
+
[--notes <release-notes.txt>] [--json]
|
|
304
|
+
|
|
305
|
+
The release notes are treated as untrusted evidence. This command does not
|
|
306
|
+
install or execute the candidate.
|
|
307
|
+
`,
|
|
308
|
+
task: `Upstream Radar — inspect or acknowledge the DSH analysis outbox
|
|
309
|
+
|
|
310
|
+
Usage:
|
|
311
|
+
upstream-radar task list <state.json> [--json]
|
|
312
|
+
upstream-radar task show <state.json> [task-id] [--json]
|
|
313
|
+
upstream-radar task ack <state.json> <task-id>
|
|
314
|
+
`,
|
|
315
|
+
analysis: `Upstream Radar — inspect verified DSH conclusions
|
|
316
|
+
|
|
317
|
+
Usage:
|
|
318
|
+
upstream-radar analysis list <state.json> [--json]
|
|
319
|
+
upstream-radar analysis show <state.json> [incident-id] [--json]
|
|
320
|
+
`,
|
|
321
|
+
mute: `Upstream Radar — pause delivery for one exact incident until a fixed expiry
|
|
322
|
+
|
|
323
|
+
Usage:
|
|
324
|
+
upstream-radar mute <state.json> <incident-id> --until <ISO-8601> [--force] [--json]
|
|
325
|
+
|
|
326
|
+
This pauses DSH and webhook delivery only. The active incident, dependency paths,
|
|
327
|
+
history, and status remain visible. The mute expires automatically. Critical and
|
|
328
|
+
malware incidents require --force.
|
|
329
|
+
`,
|
|
330
|
+
unmute: `Upstream Radar — resume delivery for one incident immediately
|
|
331
|
+
|
|
332
|
+
Usage:
|
|
333
|
+
upstream-radar unmute <state.json> <incident-id> [--json]
|
|
334
|
+
`,
|
|
335
|
+
triage: `Upstream Radar — record human follow-up for one active incident
|
|
336
|
+
|
|
337
|
+
Usage:
|
|
338
|
+
upstream-radar triage <state.json> <incident-id>
|
|
339
|
+
--status <open|in-progress|blocked|accepted-risk>
|
|
340
|
+
[--owner <name>] [--note <text>] [--due <ISO-8601>] [--json]
|
|
341
|
+
|
|
342
|
+
This records ownership and work context without resolving, hiding, or changing
|
|
343
|
+
the deterministic incident. A new event version requires a fresh follow-up.
|
|
344
|
+
Blocked and accepted-risk states require a note.
|
|
345
|
+
`,
|
|
346
|
+
};
|
|
347
|
+
return help[key];
|
|
348
|
+
}
|
|
349
|
+
function commandHelp(args) {
|
|
350
|
+
const helpIndex = args.findIndex(argument => argument === '--help' || argument === '-h');
|
|
351
|
+
if (helpIndex < 0)
|
|
352
|
+
return undefined;
|
|
353
|
+
const command = args[0];
|
|
354
|
+
if (command === undefined)
|
|
355
|
+
return undefined;
|
|
356
|
+
const nested = command === 'radar' || command === 'task' || command === 'analysis'
|
|
357
|
+
? args[1]
|
|
358
|
+
: undefined;
|
|
359
|
+
const key = nested === undefined ? command : `${command} ${nested}`;
|
|
360
|
+
return commandUsage(key) ?? commandUsage(command);
|
|
361
|
+
}
|
|
36
362
|
function usage() {
|
|
37
363
|
return `Upstream Radar — always-on dependency and compatibility monitoring for DSH plugins
|
|
38
364
|
|
|
39
365
|
Usage:
|
|
40
|
-
upstream-radar
|
|
366
|
+
upstream-radar <command> --help
|
|
367
|
+
upstream-radar setup [--profile <name>] [--start] [options]
|
|
41
368
|
upstream-radar init [--profile <name>] [options]
|
|
369
|
+
upstream-radar init --pnpm-lock <pnpm-lock.yaml> [--root <package>@<exact-version>] [options]
|
|
370
|
+
upstream-radar init --npm-lock <package-lock.json> [--root <package>@<exact-version>] [options]
|
|
42
371
|
upstream-radar doctor [config.json] [options]
|
|
43
372
|
upstream-radar scan <directory> [--json] [--fail-on <warn|review|block|never>]
|
|
44
373
|
upstream-radar inspect npm:<package>@<exact-version> [--deep] [--json] [--fail-on <warn|review|block|never>]
|
|
374
|
+
upstream-radar graph <npm-lock|pnpm-lock> <lockfile> [--root <package>@<exact-version>] [--json]
|
|
45
375
|
upstream-radar probe dsh-load <package.tgz> [--dsh-version <exact-version>] [--timeout <seconds>] [--keep-profile] [--json]
|
|
46
376
|
upstream-radar probe dsh-matrix <package.tgz> --dsh-version <v1>[,<v2>,...] [--timeout <seconds>] [--keep-profile] [--json]
|
|
377
|
+
upstream-radar demo [--json]
|
|
378
|
+
upstream-radar quickstart [directory] [--json]
|
|
47
379
|
upstream-radar benchmark compatibility [--json]
|
|
48
|
-
upstream-radar radar check <config.json> [--state <state.json>] [--frozen] [--fail-on <severity>] [--fail-on-compatibility <never|breaking|any>] [--json]
|
|
49
|
-
upstream-radar radar watch <config.json> [--state <state.json>] [--interval <seconds>] [--once] [--frozen] [--fail-on <severity>] [--fail-on-compatibility <never|breaking|any>] [--json]
|
|
380
|
+
upstream-radar radar check <config.json> [--state <state.json>] [--webhook <https-url>] [--threat-intel] [--frozen] [--fail-on <severity>] [--fail-on-compatibility <never|breaking|any>] [--json]
|
|
381
|
+
upstream-radar radar watch <config.json> [--state <state.json>] [--webhook <https-url>] [--threat-intel] [--interval <seconds>] [--once] [--frozen] [--fail-on <severity>] [--fail-on-compatibility <never|breaking|any>] [--json]
|
|
50
382
|
upstream-radar radar status <config.json> [--state <state.json>] [--fail-on <severity>] [--fail-on-compatibility <never|breaking|any>] [--json]
|
|
383
|
+
upstream-radar radar next <config.json> [--state <state.json>] [--json]
|
|
384
|
+
upstream-radar radar history <config.json> [--state <state.json>] [--limit <n>] [--json]
|
|
51
385
|
upstream-radar radar compare <config.json> <before.json> <candidate.json> [--notes <release-notes.txt>] [--json]
|
|
52
386
|
upstream-radar task list <state.json> [--json]
|
|
53
387
|
upstream-radar task show <state.json> [task-id] [--json]
|
|
54
388
|
upstream-radar task ack <state.json> <task-id>
|
|
55
389
|
upstream-radar analysis list <state.json> [--json]
|
|
56
390
|
upstream-radar analysis show <state.json> [incident-id] [--json]
|
|
391
|
+
upstream-radar mute <state.json> <incident-id> --until <ISO-8601> [--force] [--json]
|
|
392
|
+
upstream-radar unmute <state.json> <incident-id> [--json]
|
|
393
|
+
upstream-radar triage <state.json> <incident-id> --status <open|in-progress|blocked|accepted-risk> [--owner <name>] [--note <text>] [--due <ISO-8601>] [--json]
|
|
57
394
|
upstream-radar version
|
|
58
395
|
|
|
59
396
|
Commands:
|
|
60
397
|
setup install the exact Radar bundle, generate DSH wiring, and run doctor
|
|
61
|
-
init discover third-party bundles in
|
|
398
|
+
init discover third-party bundles in DSH or initialize a reviewable lockfile inventory
|
|
62
399
|
doctor check local Radar/DSH wiring without polling upstream sources
|
|
63
400
|
scan bounded, read-only inspection of a local package directory
|
|
64
401
|
inspect fetch and verify the exact npm artifact before inspecting its contents
|
|
402
|
+
graph read a lockfile into the canonical dependency graph without installing packages
|
|
65
403
|
probe run a bounded DSH bundle-load check or version matrix in disposable profiles
|
|
404
|
+
demo show the exact-path-to-DSH handoff without network, DSH, or plugin installation
|
|
405
|
+
quickstart choose the smallest first-use path without changing the environment
|
|
66
406
|
benchmark run offline compatibility-rule contracts without network or plugin execution
|
|
67
|
-
radar monitor vulnerability changes, watch continuously, inspect status, or assess a candidate compatibility change
|
|
407
|
+
radar monitor vulnerability changes, watch continuously, find the next action, inspect status/history, or assess a candidate compatibility change
|
|
68
408
|
task inspect or acknowledge the durable DSH analysis outbox
|
|
69
409
|
analysis inspect verified DSH conclusions stored in the Radar state
|
|
410
|
+
mute pause delivery for one incident with a bounded expiry
|
|
411
|
+
unmute resume delivery for one incident immediately
|
|
412
|
+
triage record owner, work status, and handoff note for one incident
|
|
70
413
|
|
|
71
414
|
Options:
|
|
72
415
|
--deep resolve the dependency graph with scripts disabled and ask npm to verify signatures/provenance
|
|
@@ -74,16 +417,29 @@ Options:
|
|
|
74
417
|
--no-deep-candidates skip bounded transitive dependency graph checks for upgrade candidates
|
|
75
418
|
--state <path> persistent radar state (default: <config.json>.state.json)
|
|
76
419
|
--osv-base-url <url> alternate HTTPS OSV API base URL
|
|
420
|
+
--no-github-advisories disable the independent GitHub Advisory Database check for radar check/watch
|
|
421
|
+
--threat-intel radar check/watch: add CISA KEV and FIRST EPSS signals for matched CVEs
|
|
422
|
+
--webhook <https-url> radar check/watch: POST changed events to an HTTPS endpoint
|
|
423
|
+
--webhook-url-env <name> init/setup: env var containing a project's HTTPS webhook URL
|
|
424
|
+
--webhook-secret-env <name> init/setup: optional Feishu/Lark V2 signing secret env var
|
|
77
425
|
--interval <seconds> watch interval from 300 to 86400 seconds (default: 1800)
|
|
426
|
+
--limit <n> radar history: show 1 to 1000 recent transitions (default: 20)
|
|
78
427
|
--once run one watch cycle and exit (useful for CI and demos)
|
|
79
428
|
--frozen radar check/watch: use the reviewed graph in config without reading a local DSH profile
|
|
80
429
|
--fail-on <value> scan/inspect verdict or radar severity: unknown|info|low|medium|high|critical|never
|
|
81
430
|
--fail-on-compatibility <value> CI gate: never|breaking|any (default: never)
|
|
82
431
|
--notes <path> release notes used as untrusted compatibility evidence
|
|
83
432
|
--profile <name> DSH profile for init or doctor (init auto-selects the only candidate when omitted)
|
|
433
|
+
--npm-lock <path> init: build a static inventory from an npm v2/v3 package-lock.json
|
|
434
|
+
--pnpm-lock <path> init: build a static inventory from a pnpm v6/v9 lockfile
|
|
435
|
+
--root <coordinate> init/graph lockfiles: override the root; otherwise read package.json beside the lockfile
|
|
84
436
|
--no-install setup: reuse an already installed upstream-radar bundle
|
|
85
437
|
--output <path> init output path (default: ./upstream-radar.config.json)
|
|
86
|
-
--dsh-patch <path> write a self-contained DSH --patch overlay (
|
|
438
|
+
--dsh-patch <path> write a self-contained DSH --patch overlay (setup default: ./upstream-radar.dsh.yml)
|
|
439
|
+
--minimum-severity <level> init/setup notification threshold: info|low|medium|high|critical
|
|
440
|
+
--quiet-hours <tz,start-end> init/setup window, e.g. Asia/Shanghai,22:00-08:00
|
|
441
|
+
--start setup: start DSH after the local wiring check passes
|
|
442
|
+
--no-dsh-patch setup: keep the legacy UPSTREAM_RADAR_* environment-variable wiring
|
|
87
443
|
--patch <path> DSH overlay to verify with doctor
|
|
88
444
|
--force allow init to replace an existing output file
|
|
89
445
|
--json emit the canonical JSON report
|
|
@@ -94,6 +450,89 @@ Exit codes:
|
|
|
94
450
|
2 configured policy threshold was reached
|
|
95
451
|
`;
|
|
96
452
|
}
|
|
453
|
+
function parseExactPackageCoordinate(value) {
|
|
454
|
+
const at = value.lastIndexOf('@');
|
|
455
|
+
if (at <= 0 || at === value.length - 1)
|
|
456
|
+
throw new Error(`--root must be an exact package coordinate: ${value}`);
|
|
457
|
+
const name = value.slice(0, at);
|
|
458
|
+
const version = value.slice(at + 1);
|
|
459
|
+
if (name.startsWith('@') && !name.includes('/'))
|
|
460
|
+
throw new Error(`--root must include a scoped package name: ${value}`);
|
|
461
|
+
return { name, version };
|
|
462
|
+
}
|
|
463
|
+
async function inferLockfileRoot(lockfile, kind) {
|
|
464
|
+
const manifestPath = join(dirname(resolve(lockfile)), 'package.json');
|
|
465
|
+
try {
|
|
466
|
+
const manifest = parsePackageManifestSnapshot(await readJson(manifestPath));
|
|
467
|
+
return { name: manifest.name, version: manifest.version };
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
if (error.code === 'ENOENT') {
|
|
471
|
+
throw new Error(`could not infer the ${kind} lockfile root from ${manifestPath}; pass --root <package>@<exact-version>`);
|
|
472
|
+
}
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function runGraph(args) {
|
|
477
|
+
const kind = args[0];
|
|
478
|
+
if (kind !== 'npm-lock' && kind !== 'pnpm-lock')
|
|
479
|
+
throw new Error('graph requires the npm-lock or pnpm-lock subcommand');
|
|
480
|
+
const lockfile = args[1];
|
|
481
|
+
if (lockfile === undefined || lockfile.startsWith('-'))
|
|
482
|
+
throw new Error(`graph ${kind} requires a lockfile path`);
|
|
483
|
+
let rootSpec;
|
|
484
|
+
let json = false;
|
|
485
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
486
|
+
const argument = args[index];
|
|
487
|
+
if (argument === '--json') {
|
|
488
|
+
json = true;
|
|
489
|
+
}
|
|
490
|
+
else if (argument === '--root') {
|
|
491
|
+
const value = args[index + 1];
|
|
492
|
+
if (value === undefined || value.startsWith('-'))
|
|
493
|
+
throw new Error('--root requires a value');
|
|
494
|
+
rootSpec = value;
|
|
495
|
+
index += 1;
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
throw new Error(`unknown option for graph ${kind}: ${argument}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const packageRoot = rootSpec === undefined
|
|
502
|
+
? await inferLockfileRoot(lockfile, kind === 'npm-lock' ? 'npm' : 'pnpm')
|
|
503
|
+
: parseExactPackageCoordinate(rootSpec);
|
|
504
|
+
const graph = kind === 'npm-lock'
|
|
505
|
+
? parseNpmLockGraph(await readJson(lockfile), packageRoot)
|
|
506
|
+
: parsePnpmLockGraph(await readBoundedFile(lockfile, 16 * 1024 * 1024), packageRoot);
|
|
507
|
+
if (json) {
|
|
508
|
+
process.stdout.write(`${JSON.stringify(graph, null, 2)}\n`);
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
511
|
+
const nodes = new Map(graph.nodes.map(node => [node.id, node]));
|
|
512
|
+
const root = nodes.get(graph.rootNodeId);
|
|
513
|
+
const display = (id) => {
|
|
514
|
+
const node = nodes.get(id);
|
|
515
|
+
return node === undefined ? id : `${node.name}@${node.version}`;
|
|
516
|
+
};
|
|
517
|
+
const unresolved = graph.unresolved ?? [];
|
|
518
|
+
process.stdout.write([
|
|
519
|
+
`Dependency graph: ${root === undefined ? graph.rootNodeId : `${root.name}@${root.version}`}`,
|
|
520
|
+
`Source: ${kind} (read-only; no install, no plugin execution)`,
|
|
521
|
+
`Nodes: ${graph.nodes.length}`,
|
|
522
|
+
`Edges: ${graph.edges.length}`,
|
|
523
|
+
`Unresolved: ${unresolved.length}`,
|
|
524
|
+
`Digest: ${graph.digest ?? '(none)'}`,
|
|
525
|
+
'',
|
|
526
|
+
'Edges:',
|
|
527
|
+
...(graph.edges.length === 0 ? [' (none)'] : graph.edges.map(edge => ` ${display(edge.from)} -[${edge.kind}]-> ${display(edge.to)}`)),
|
|
528
|
+
...(unresolved.length === 0 ? [] : [
|
|
529
|
+
'',
|
|
530
|
+
'Unresolved dependencies:',
|
|
531
|
+
...unresolved.map(item => ` ${display(item.from)} -[${item.kind}]-> ${item.name} (${item.spec})`),
|
|
532
|
+
]),
|
|
533
|
+
].join('\n') + '\n');
|
|
534
|
+
return 0;
|
|
535
|
+
}
|
|
97
536
|
async function runTask(args) {
|
|
98
537
|
const subcommand = args[0];
|
|
99
538
|
if (subcommand !== 'list' && subcommand !== 'show' && subcommand !== 'ack') {
|
|
@@ -162,6 +601,200 @@ async function runTask(args) {
|
|
|
162
601
|
process.stdout.write(`Acknowledged ${safeErrorMessage(taskId)}.\n`);
|
|
163
602
|
return 0;
|
|
164
603
|
}
|
|
604
|
+
const MAX_INCIDENT_MUTE_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
605
|
+
const VALID_INCIDENT_TRIAGE_STATUSES = new Set([
|
|
606
|
+
'open',
|
|
607
|
+
'in-progress',
|
|
608
|
+
'blocked',
|
|
609
|
+
'accepted-risk',
|
|
610
|
+
]);
|
|
611
|
+
function activeEventForIncident(state, incidentId) {
|
|
612
|
+
return [
|
|
613
|
+
...Object.values(state.activeVulnerabilities),
|
|
614
|
+
...Object.values(state.activeCompatibility),
|
|
615
|
+
...Object.values(state.activeSourceHealth ?? {}),
|
|
616
|
+
].map(item => item.event).find(event => event.incidentId === incidentId);
|
|
617
|
+
}
|
|
618
|
+
async function runIncidentMute(args) {
|
|
619
|
+
const statePath = args[0];
|
|
620
|
+
const incidentId = args[1];
|
|
621
|
+
if (statePath === undefined || statePath.startsWith('-'))
|
|
622
|
+
throw new Error('mute requires a state file');
|
|
623
|
+
if (incidentId === undefined || incidentId.startsWith('-'))
|
|
624
|
+
throw new Error('mute requires an incident id');
|
|
625
|
+
let untilValue;
|
|
626
|
+
let force = false;
|
|
627
|
+
let json = false;
|
|
628
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
629
|
+
const argument = args[index];
|
|
630
|
+
if (argument === undefined)
|
|
631
|
+
throw new Error('mute received an incomplete option');
|
|
632
|
+
if (argument === '--force')
|
|
633
|
+
force = true;
|
|
634
|
+
else if (argument === '--json')
|
|
635
|
+
json = true;
|
|
636
|
+
else if (argument === '--until') {
|
|
637
|
+
const value = args[index + 1];
|
|
638
|
+
if (value === undefined || value.startsWith('-'))
|
|
639
|
+
throw new Error('--until requires a value');
|
|
640
|
+
untilValue = value;
|
|
641
|
+
index += 1;
|
|
642
|
+
}
|
|
643
|
+
else if (argument.startsWith('-')) {
|
|
644
|
+
throw new Error(`unknown option for mute: ${argument}`);
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
throw new Error(`mute received unexpected argument: ${argument}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (untilValue === undefined)
|
|
651
|
+
throw new Error('mute requires --until <ISO-8601>');
|
|
652
|
+
const mutedUntilMs = Date.parse(untilValue);
|
|
653
|
+
const nowMs = Date.now();
|
|
654
|
+
if (!Number.isFinite(mutedUntilMs))
|
|
655
|
+
throw new Error('--until must be a valid ISO-8601 timestamp');
|
|
656
|
+
if (mutedUntilMs <= nowMs)
|
|
657
|
+
throw new Error('--until must be in the future');
|
|
658
|
+
if (mutedUntilMs > nowMs + MAX_INCIDENT_MUTE_MS)
|
|
659
|
+
throw new Error('--until cannot be more than 30 days away');
|
|
660
|
+
const state = await loadRadarState(statePath);
|
|
661
|
+
const event = activeEventForIncident(state, incidentId);
|
|
662
|
+
if (event === undefined)
|
|
663
|
+
throw new Error(`active incident not found: ${incidentId}`);
|
|
664
|
+
const forceRequired = event.kind === 'malware'
|
|
665
|
+
|| (event.kind === 'vulnerability' && event.advisory.severity === 'critical');
|
|
666
|
+
if (forceRequired && !force)
|
|
667
|
+
throw new Error('critical or malware incidents require --force to mute');
|
|
668
|
+
const mutedUntil = new Date(mutedUntilMs).toISOString();
|
|
669
|
+
await saveRadarState(statePath, {
|
|
670
|
+
...state,
|
|
671
|
+
incidentMutes: {
|
|
672
|
+
...(state.incidentMutes ?? {}),
|
|
673
|
+
[incidentId]: { eventId: event.id, mutedUntil },
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
const result = { incidentId, eventId: event.id, mutedUntil, forced: force };
|
|
677
|
+
process.stdout.write(json
|
|
678
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
679
|
+
: `Muted delivery for ${safeErrorMessage(incidentId)} until ${mutedUntil}. Active evidence and history remain visible.\n`);
|
|
680
|
+
return 0;
|
|
681
|
+
}
|
|
682
|
+
async function runIncidentUnmute(args) {
|
|
683
|
+
const statePath = args[0];
|
|
684
|
+
const incidentId = args[1];
|
|
685
|
+
if (statePath === undefined || statePath.startsWith('-'))
|
|
686
|
+
throw new Error('unmute requires a state file');
|
|
687
|
+
if (incidentId === undefined || incidentId.startsWith('-'))
|
|
688
|
+
throw new Error('unmute requires an incident id');
|
|
689
|
+
let json = false;
|
|
690
|
+
for (const argument of args.slice(2)) {
|
|
691
|
+
if (argument === '--json')
|
|
692
|
+
json = true;
|
|
693
|
+
else
|
|
694
|
+
throw new Error(`unknown option for unmute: ${argument}`);
|
|
695
|
+
}
|
|
696
|
+
const state = await loadRadarState(statePath);
|
|
697
|
+
if (state.incidentMutes?.[incidentId] === undefined)
|
|
698
|
+
throw new Error(`incident is not muted: ${incidentId}`);
|
|
699
|
+
const nextIncidentMutes = Object.fromEntries(Object.entries(state.incidentMutes).filter(([id]) => id !== incidentId));
|
|
700
|
+
await saveRadarState(statePath, { ...state, incidentMutes: nextIncidentMutes });
|
|
701
|
+
const result = { incidentId, unmuted: true };
|
|
702
|
+
process.stdout.write(json
|
|
703
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
704
|
+
: `Resumed delivery for ${safeErrorMessage(incidentId)}.\n`);
|
|
705
|
+
return 0;
|
|
706
|
+
}
|
|
707
|
+
async function runIncidentTriage(args) {
|
|
708
|
+
const statePath = args[0];
|
|
709
|
+
const incidentId = args[1];
|
|
710
|
+
if (statePath === undefined || statePath.startsWith('-'))
|
|
711
|
+
throw new Error('triage requires a state file');
|
|
712
|
+
if (incidentId === undefined || incidentId.startsWith('-'))
|
|
713
|
+
throw new Error('triage requires an incident id');
|
|
714
|
+
let statusValue;
|
|
715
|
+
let ownerValue;
|
|
716
|
+
let noteValue;
|
|
717
|
+
let dueValue;
|
|
718
|
+
let json = false;
|
|
719
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
720
|
+
const argument = args[index];
|
|
721
|
+
if (argument === undefined)
|
|
722
|
+
throw new Error('triage received an incomplete option');
|
|
723
|
+
if (argument === '--json')
|
|
724
|
+
json = true;
|
|
725
|
+
else if (argument === '--status' || argument === '--owner' || argument === '--note' || argument === '--due') {
|
|
726
|
+
const value = args[index + 1];
|
|
727
|
+
if (value === undefined || value.startsWith('-'))
|
|
728
|
+
throw new Error(`${argument} requires a value`);
|
|
729
|
+
if (argument === '--status')
|
|
730
|
+
statusValue = value;
|
|
731
|
+
else if (argument === '--owner')
|
|
732
|
+
ownerValue = value;
|
|
733
|
+
else if (argument === '--note')
|
|
734
|
+
noteValue = value;
|
|
735
|
+
else
|
|
736
|
+
dueValue = value;
|
|
737
|
+
index += 1;
|
|
738
|
+
}
|
|
739
|
+
else if (argument.startsWith('-')) {
|
|
740
|
+
throw new Error(`unknown option for triage: ${argument}`);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
throw new Error(`triage received unexpected argument: ${argument}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (statusValue === undefined)
|
|
747
|
+
throw new Error('triage requires --status <open|in-progress|blocked|accepted-risk>');
|
|
748
|
+
if (!VALID_INCIDENT_TRIAGE_STATUSES.has(statusValue)) {
|
|
749
|
+
throw new Error(`invalid triage --status value: ${statusValue}`);
|
|
750
|
+
}
|
|
751
|
+
const status = statusValue;
|
|
752
|
+
const owner = ownerValue?.trim();
|
|
753
|
+
const note = noteValue?.trim();
|
|
754
|
+
if (ownerValue !== undefined && (owner === undefined || owner.length === 0))
|
|
755
|
+
throw new Error('--owner cannot be empty');
|
|
756
|
+
if (owner !== undefined && owner.length > 512)
|
|
757
|
+
throw new Error('--owner must be 512 characters or fewer');
|
|
758
|
+
if (noteValue !== undefined && (note === undefined || note.length === 0))
|
|
759
|
+
throw new Error('--note cannot be empty');
|
|
760
|
+
if (note !== undefined && note.length > 2_048)
|
|
761
|
+
throw new Error('--note must be 2048 characters or fewer');
|
|
762
|
+
const dueAtMs = dueValue === undefined ? undefined : Date.parse(dueValue);
|
|
763
|
+
if (dueValue !== undefined && !Number.isFinite(dueAtMs))
|
|
764
|
+
throw new Error('--due must be a valid ISO-8601 timestamp');
|
|
765
|
+
const state = await loadRadarState(statePath);
|
|
766
|
+
const event = activeEventForIncident(state, incidentId);
|
|
767
|
+
if (event === undefined)
|
|
768
|
+
throw new Error(`active incident not found: ${incidentId}`);
|
|
769
|
+
const previous = state.incidentTriage?.[incidentId];
|
|
770
|
+
const inherited = previous?.eventId === event.id ? previous : undefined;
|
|
771
|
+
const recordNote = note ?? inherited?.note;
|
|
772
|
+
if ((status === 'blocked' || status === 'accepted-risk') && recordNote === undefined) {
|
|
773
|
+
throw new Error(`triage status ${status} requires --note <text>`);
|
|
774
|
+
}
|
|
775
|
+
const recordOwner = owner ?? inherited?.owner;
|
|
776
|
+
const recordDueAt = dueAtMs === undefined ? inherited?.dueAt : new Date(dueAtMs).toISOString();
|
|
777
|
+
const record = {
|
|
778
|
+
eventId: event.id,
|
|
779
|
+
status,
|
|
780
|
+
updatedAt: new Date().toISOString(),
|
|
781
|
+
...(recordOwner === undefined ? {} : { owner: recordOwner }),
|
|
782
|
+
...(recordNote === undefined ? {} : { note: recordNote }),
|
|
783
|
+
...(recordDueAt === undefined ? {} : { dueAt: recordDueAt }),
|
|
784
|
+
};
|
|
785
|
+
await saveRadarState(statePath, {
|
|
786
|
+
...state,
|
|
787
|
+
incidentTriage: {
|
|
788
|
+
...(state.incidentTriage ?? {}),
|
|
789
|
+
[incidentId]: record,
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
const result = { incidentId, ...record };
|
|
793
|
+
process.stdout.write(json
|
|
794
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
795
|
+
: `Recorded ${status} follow-up for ${safeErrorMessage(incidentId)}${record.owner === undefined ? '' : ` (owner: ${safeErrorMessage(record.owner)})`}.\n`);
|
|
796
|
+
return 0;
|
|
797
|
+
}
|
|
165
798
|
async function runAnalysis(args) {
|
|
166
799
|
const subcommand = args[0];
|
|
167
800
|
if (subcommand !== 'list' && subcommand !== 'show')
|
|
@@ -258,6 +891,18 @@ async function runBenchmark(args) {
|
|
|
258
891
|
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderCompatibilityBenchmark(report));
|
|
259
892
|
return report.summary.failed === 0 ? 0 : 1;
|
|
260
893
|
}
|
|
894
|
+
function runDemo(args) {
|
|
895
|
+
let json = false;
|
|
896
|
+
for (const argument of args) {
|
|
897
|
+
if (argument === '--json')
|
|
898
|
+
json = true;
|
|
899
|
+
else
|
|
900
|
+
throw new Error(`unknown option for demo: ${argument}`);
|
|
901
|
+
}
|
|
902
|
+
const report = createDemoReport();
|
|
903
|
+
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderDemo(report));
|
|
904
|
+
return 0;
|
|
905
|
+
}
|
|
261
906
|
async function runProbe(args) {
|
|
262
907
|
const mode = args[0];
|
|
263
908
|
if (mode !== 'dsh-load' && mode !== 'dsh-matrix')
|
|
@@ -337,8 +982,8 @@ async function fileExists(path) {
|
|
|
337
982
|
}
|
|
338
983
|
async function runRadar(args) {
|
|
339
984
|
const subcommand = args[0];
|
|
340
|
-
if (subcommand !== 'check' && subcommand !== 'watch' && subcommand !== 'status' && subcommand !== 'compare') {
|
|
341
|
-
throw new Error('radar requires check, watch, status or compare');
|
|
985
|
+
if (subcommand !== 'check' && subcommand !== 'watch' && subcommand !== 'status' && subcommand !== 'next' && subcommand !== 'history' && subcommand !== 'compare') {
|
|
986
|
+
throw new Error('radar requires check, watch, status, next, history or compare');
|
|
342
987
|
}
|
|
343
988
|
const configPath = args[1];
|
|
344
989
|
if (configPath === undefined || configPath.startsWith('-'))
|
|
@@ -348,11 +993,16 @@ async function runRadar(args) {
|
|
|
348
993
|
let statePath;
|
|
349
994
|
let osvBaseUrl;
|
|
350
995
|
let registry;
|
|
996
|
+
let webhookUrl;
|
|
351
997
|
let notesPath;
|
|
352
998
|
let intervalSeconds = 1_800;
|
|
353
999
|
let intervalProvided = false;
|
|
1000
|
+
let historyLimit = 20;
|
|
1001
|
+
let historyLimitProvided = false;
|
|
354
1002
|
let once = false;
|
|
355
1003
|
let deepCandidates = true;
|
|
1004
|
+
let githubAdvisories = true;
|
|
1005
|
+
let threatIntel = false;
|
|
356
1006
|
let frozen = false;
|
|
357
1007
|
let failOn = 'never';
|
|
358
1008
|
let failOnCompatibility = 'never';
|
|
@@ -367,11 +1017,17 @@ async function runRadar(args) {
|
|
|
367
1017
|
else if (argument === '--no-deep-candidates') {
|
|
368
1018
|
deepCandidates = false;
|
|
369
1019
|
}
|
|
1020
|
+
else if (argument === '--no-github-advisories') {
|
|
1021
|
+
githubAdvisories = false;
|
|
1022
|
+
}
|
|
1023
|
+
else if (argument === '--threat-intel') {
|
|
1024
|
+
threatIntel = true;
|
|
1025
|
+
}
|
|
370
1026
|
else if (argument === '--frozen') {
|
|
371
1027
|
frozen = true;
|
|
372
1028
|
}
|
|
373
|
-
else if (argument === '--state' || argument === '--osv-base-url' || argument === '--registry'
|
|
374
|
-
|| argument === '--notes' || argument === '--interval' || argument === '--fail-on'
|
|
1029
|
+
else if (argument === '--state' || argument === '--osv-base-url' || argument === '--registry' || argument === '--webhook'
|
|
1030
|
+
|| argument === '--notes' || argument === '--interval' || argument === '--limit' || argument === '--fail-on'
|
|
375
1031
|
|| argument === '--fail-on-compatibility') {
|
|
376
1032
|
const value = args[index + 1];
|
|
377
1033
|
if (value === undefined || value.startsWith('-'))
|
|
@@ -382,6 +1038,8 @@ async function runRadar(args) {
|
|
|
382
1038
|
osvBaseUrl = value;
|
|
383
1039
|
else if (argument === '--registry')
|
|
384
1040
|
registry = value;
|
|
1041
|
+
else if (argument === '--webhook')
|
|
1042
|
+
webhookUrl = normalizeRadarWebhookUrl(value);
|
|
385
1043
|
else if (argument === '--notes')
|
|
386
1044
|
notesPath = value;
|
|
387
1045
|
else if (argument === '--fail-on') {
|
|
@@ -396,6 +1054,14 @@ async function runRadar(args) {
|
|
|
396
1054
|
}
|
|
397
1055
|
failOnCompatibility = value;
|
|
398
1056
|
}
|
|
1057
|
+
else if (argument === '--limit') {
|
|
1058
|
+
historyLimitProvided = true;
|
|
1059
|
+
const parsed = Number(value);
|
|
1060
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 1_000) {
|
|
1061
|
+
throw new Error('--limit must be an integer between 1 and 1000');
|
|
1062
|
+
}
|
|
1063
|
+
historyLimit = parsed;
|
|
1064
|
+
}
|
|
399
1065
|
else {
|
|
400
1066
|
intervalProvided = true;
|
|
401
1067
|
const parsed = Number(value);
|
|
@@ -417,10 +1083,13 @@ async function runRadar(args) {
|
|
|
417
1083
|
const readConfigForPoll = async () => frozen
|
|
418
1084
|
? readConfig()
|
|
419
1085
|
: refreshRadarConfigFromConfiguredProfile(await readConfig());
|
|
420
|
-
if (subcommand === 'status') {
|
|
1086
|
+
if (subcommand === 'status' || subcommand === 'next') {
|
|
421
1087
|
if (positional.length > 0 || notesPath !== undefined || once || intervalProvided
|
|
422
|
-
|| osvBaseUrl !== undefined || registry !== undefined || !deepCandidates || frozen || statePath === ':memory:'
|
|
423
|
-
|
|
1088
|
+
|| historyLimitProvided || osvBaseUrl !== undefined || registry !== undefined || webhookUrl !== undefined || !deepCandidates || !githubAdvisories || threatIntel || frozen || statePath === ':memory:'
|
|
1089
|
+
|| (subcommand === 'next' && (failOn !== 'never' || failOnCompatibility !== 'never'))) {
|
|
1090
|
+
throw new Error(subcommand === 'next'
|
|
1091
|
+
? 'radar next only accepts --state and --json options'
|
|
1092
|
+
: 'radar status only accepts --state, --fail-on, --fail-on-compatibility and --json options');
|
|
424
1093
|
}
|
|
425
1094
|
const config = await readConfig();
|
|
426
1095
|
const stateFile = statePath ?? `${resolve(configPath)}.state.json`;
|
|
@@ -433,6 +1102,11 @@ async function runRadar(args) {
|
|
|
433
1102
|
});
|
|
434
1103
|
const policy = evaluateRadarPolicy(state, failOn, failOnCompatibility);
|
|
435
1104
|
const policyEnabled = failOn !== 'never' || failOnCompatibility !== 'never';
|
|
1105
|
+
if (subcommand === 'next') {
|
|
1106
|
+
const next = createRadarNext(report, state);
|
|
1107
|
+
process.stdout.write(json ? `${JSON.stringify(next, null, 2)}\n` : renderRadarNext(next));
|
|
1108
|
+
return 0;
|
|
1109
|
+
}
|
|
436
1110
|
if (json) {
|
|
437
1111
|
process.stdout.write(!policyEnabled
|
|
438
1112
|
? `${JSON.stringify(report, null, 2)}\n`
|
|
@@ -445,6 +1119,27 @@ async function runRadar(args) {
|
|
|
445
1119
|
return 1;
|
|
446
1120
|
return policy.status === 'fail' ? 2 : 0;
|
|
447
1121
|
}
|
|
1122
|
+
if (subcommand === 'history') {
|
|
1123
|
+
if (positional.length > 0 || notesPath !== undefined || once || intervalProvided
|
|
1124
|
+
|| osvBaseUrl !== undefined || registry !== undefined || webhookUrl !== undefined || !deepCandidates || !githubAdvisories || frozen
|
|
1125
|
+
|| failOn !== 'never' || failOnCompatibility !== 'never' || threatIntel) {
|
|
1126
|
+
throw new Error('radar history only accepts --state, --limit and --json options');
|
|
1127
|
+
}
|
|
1128
|
+
// Parse the config as an input check, but deliberately do not refresh it
|
|
1129
|
+
// or contact any upstream source. History is a local diagnosis command.
|
|
1130
|
+
await readConfig();
|
|
1131
|
+
const stateFile = statePath ?? `${resolve(configPath)}.state.json`;
|
|
1132
|
+
const stateExists = await fileExists(stateFile);
|
|
1133
|
+
const state = stateExists ? await loadRadarState(stateFile) : emptyRadarState();
|
|
1134
|
+
const report = createRadarHistory(state, {
|
|
1135
|
+
configFile: resolve(configPath),
|
|
1136
|
+
stateFile: resolve(stateFile),
|
|
1137
|
+
stateExists,
|
|
1138
|
+
limit: historyLimit,
|
|
1139
|
+
});
|
|
1140
|
+
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderRadarHistory(report));
|
|
1141
|
+
return 0;
|
|
1142
|
+
}
|
|
448
1143
|
const osv = new OsvClient({
|
|
449
1144
|
...(osvBaseUrl === undefined ? {} : { baseUrl: osvBaseUrl }),
|
|
450
1145
|
});
|
|
@@ -455,14 +1150,60 @@ async function runRadar(args) {
|
|
|
455
1150
|
? new NpmCandidateGraphClient({ ...(registry === undefined ? {} : { registry }) })
|
|
456
1151
|
: undefined;
|
|
457
1152
|
const releaseNotesSource = new GitHubReleaseClient();
|
|
1153
|
+
const githubAdvisorySource = githubAdvisories
|
|
1154
|
+
? new GitHubAdvisoryClient({ ...(process.env.GITHUB_TOKEN === undefined ? {} : { token: process.env.GITHUB_TOKEN }) })
|
|
1155
|
+
: undefined;
|
|
1156
|
+
const threatIntelSources = threatIntel
|
|
1157
|
+
? [
|
|
1158
|
+
{ name: 'cisa-kev', source: new CisaKevClient() },
|
|
1159
|
+
{ name: 'epss', source: new EpssClient() },
|
|
1160
|
+
]
|
|
1161
|
+
: [];
|
|
458
1162
|
const stateFile = statePath ?? `${resolve(configPath)}.state.json`;
|
|
1163
|
+
if (webhookUrl !== undefined && statePath === ':memory:') {
|
|
1164
|
+
throw new Error('radar --webhook requires a persistent --state file so successful deliveries can be remembered');
|
|
1165
|
+
}
|
|
1166
|
+
const feishuSecret = process.env.UPSTREAM_RADAR_FEISHU_SECRET?.trim() || undefined;
|
|
459
1167
|
const runCheck = async () => {
|
|
460
1168
|
const config = await readConfigForPoll();
|
|
1169
|
+
const webhookTargets = resolveRadarWebhookTargets(config.projects, {
|
|
1170
|
+
...(webhookUrl === undefined ? {} : { globalUrl: webhookUrl }),
|
|
1171
|
+
...(feishuSecret === undefined ? {} : { globalFeishuSecret: feishuSecret }),
|
|
1172
|
+
});
|
|
1173
|
+
if (statePath === ':memory:' && webhookTargets.length > 0) {
|
|
1174
|
+
throw new Error('radar webhook routes require a persistent --state file so successful deliveries can be remembered');
|
|
1175
|
+
}
|
|
461
1176
|
const state = statePath === ':memory:' ? emptyRadarState() : await loadRadarState(stateFile);
|
|
462
|
-
const
|
|
1177
|
+
const checkedAt = new Date();
|
|
1178
|
+
const result = await pollRadar(config.projects, state, osv, checkedAt, releases, releaseNotesSource, candidateGraphs, githubAdvisorySource === undefined ? [] : [{ name: 'github-advisories', source: githubAdvisorySource }], threatIntelSources);
|
|
463
1179
|
if (statePath !== ':memory:')
|
|
464
1180
|
await saveRadarState(stateFile, result.state);
|
|
465
|
-
|
|
1181
|
+
const notificationPolicies = createNotificationPolicyMap(config.projects);
|
|
1182
|
+
let nextState = result.state;
|
|
1183
|
+
for (const target of webhookTargets) {
|
|
1184
|
+
const targetEvents = eventsForRadarWebhookTarget(result.events, target);
|
|
1185
|
+
const isLegacyGlobal = webhookUrl !== undefined && target.projectIds === undefined;
|
|
1186
|
+
const queuedState = isLegacyGlobal
|
|
1187
|
+
? queueRadarWebhookEvents(nextState, target.endpointHash, targetEvents)
|
|
1188
|
+
: queueRadarWebhookEventsForRoute(nextState, target.endpointHash, targetEvents);
|
|
1189
|
+
if (statePath !== ':memory:')
|
|
1190
|
+
await saveRadarState(stateFile, queuedState);
|
|
1191
|
+
const pendingWebhookEvents = filterNotifiableRadarEvents(isLegacyGlobal
|
|
1192
|
+
? undeliveredRadarWebhookEvents(queuedState, target.endpointHash, targetEvents)
|
|
1193
|
+
: undeliveredRadarWebhookEventsForRoute(queuedState, target.endpointHash, targetEvents), notificationPolicies, checkedAt, queuedState);
|
|
1194
|
+
if (pendingWebhookEvents.length === 0) {
|
|
1195
|
+
nextState = queuedState;
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
const payload = await sendRadarWebhook(target.url, pendingWebhookEvents, target.feishuSecret === undefined ? {} : { feishuSecret: target.feishuSecret });
|
|
1199
|
+
const deliveredIds = new Set(payload.events.map(event => event.id));
|
|
1200
|
+
const deliveredEvents = pendingWebhookEvents.filter(event => deliveredIds.has(event.id));
|
|
1201
|
+
nextState = isLegacyGlobal
|
|
1202
|
+
? markRadarWebhookEventsDelivered(queuedState, target.endpointHash, deliveredEvents)
|
|
1203
|
+
: markRadarWebhookEventsDeliveredForRoute(queuedState, target.endpointHash, deliveredEvents);
|
|
1204
|
+
await saveRadarState(stateFile, nextState);
|
|
1205
|
+
}
|
|
1206
|
+
return { ...result, state: nextState };
|
|
466
1207
|
};
|
|
467
1208
|
const writeCheckResult = (result, compactJson = false) => {
|
|
468
1209
|
const policy = evaluateRadarPolicy(result.state, failOn, failOnCompatibility);
|
|
@@ -478,7 +1219,7 @@ async function runRadar(args) {
|
|
|
478
1219
|
return policy;
|
|
479
1220
|
};
|
|
480
1221
|
if (subcommand === 'check') {
|
|
481
|
-
if (positional.length > 0 || notesPath !== undefined || once || intervalProvided) {
|
|
1222
|
+
if (positional.length > 0 || notesPath !== undefined || once || intervalProvided || historyLimitProvided) {
|
|
482
1223
|
throw new Error('radar check received an option meant for watch or compare');
|
|
483
1224
|
}
|
|
484
1225
|
const result = await runCheck();
|
|
@@ -488,7 +1229,7 @@ async function runRadar(args) {
|
|
|
488
1229
|
return policy.status === 'fail' ? 2 : 0;
|
|
489
1230
|
}
|
|
490
1231
|
if (subcommand === 'watch') {
|
|
491
|
-
if (positional.length > 0 || notesPath !== undefined)
|
|
1232
|
+
if (positional.length > 0 || notesPath !== undefined || historyLimitProvided)
|
|
492
1233
|
throw new Error('radar watch received unexpected arguments');
|
|
493
1234
|
if ((failOn !== 'never' || failOnCompatibility !== 'never') && !once)
|
|
494
1235
|
throw new Error('radar watch requires --once when a policy gate is used');
|
|
@@ -537,7 +1278,7 @@ async function runRadar(args) {
|
|
|
537
1278
|
}
|
|
538
1279
|
return 0;
|
|
539
1280
|
}
|
|
540
|
-
if (statePath !== undefined || osvBaseUrl !== undefined || registry !== undefined || once || intervalProvided || !deepCandidates || frozen || failOn !== 'never' || failOnCompatibility !== 'never') {
|
|
1281
|
+
if (statePath !== undefined || osvBaseUrl !== undefined || registry !== undefined || webhookUrl !== undefined || once || intervalProvided || historyLimitProvided || !deepCandidates || !githubAdvisories || threatIntel || frozen || failOn !== 'never' || failOnCompatibility !== 'never') {
|
|
541
1282
|
throw new Error('radar compare does not accept check or watch options');
|
|
542
1283
|
}
|
|
543
1284
|
const config = await readConfig();
|
|
@@ -562,15 +1303,37 @@ async function runRadar(args) {
|
|
|
562
1303
|
: `${renderRadarEvents(events)}Prepared ${analysisTasks.length} DSH compatibility analysis task(s).\n`);
|
|
563
1304
|
return 0;
|
|
564
1305
|
}
|
|
1306
|
+
async function runQuickstart(args) {
|
|
1307
|
+
let directory = process.cwd();
|
|
1308
|
+
let json = false;
|
|
1309
|
+
let positional = false;
|
|
1310
|
+
for (const argument of args) {
|
|
1311
|
+
if (argument === '--json') {
|
|
1312
|
+
json = true;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (argument.startsWith('-'))
|
|
1316
|
+
throw new Error(`unknown option for quickstart: ${argument}`);
|
|
1317
|
+
if (positional)
|
|
1318
|
+
throw new Error('quickstart accepts at most one directory');
|
|
1319
|
+
directory = argument;
|
|
1320
|
+
positional = true;
|
|
1321
|
+
}
|
|
1322
|
+
const report = await createQuickstartReport(directory);
|
|
1323
|
+
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderQuickstartReport(report));
|
|
1324
|
+
return 0;
|
|
1325
|
+
}
|
|
565
1326
|
async function runSetup(args) {
|
|
566
1327
|
let profile;
|
|
567
1328
|
let output = 'upstream-radar.config.json';
|
|
568
|
-
let patchFile;
|
|
1329
|
+
let patchFile = 'upstream-radar.dsh.yml';
|
|
569
1330
|
let noInstall = false;
|
|
1331
|
+
let startDsh = false;
|
|
570
1332
|
const initArgs = [];
|
|
571
1333
|
const valueOptions = new Set([
|
|
572
1334
|
'--profile', '--output', '--dsh-patch', '--project-id', '--project-name',
|
|
573
|
-
'--repository', '--workspace', '--channel', '--
|
|
1335
|
+
'--repository', '--workspace', '--channel', '--webhook-url-env', '--webhook-secret-env',
|
|
1336
|
+
'--registry', '--minimum-severity', '--quiet-hours',
|
|
574
1337
|
]);
|
|
575
1338
|
for (let index = 0; index < args.length; index += 1) {
|
|
576
1339
|
const argument = args[index];
|
|
@@ -578,6 +1341,14 @@ async function runSetup(args) {
|
|
|
578
1341
|
noInstall = true;
|
|
579
1342
|
continue;
|
|
580
1343
|
}
|
|
1344
|
+
if (argument === '--no-dsh-patch') {
|
|
1345
|
+
patchFile = undefined;
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
if (argument === '--start') {
|
|
1349
|
+
startDsh = true;
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
581
1352
|
if (argument === '--json')
|
|
582
1353
|
throw new Error('setup does not accept --json; use init --json');
|
|
583
1354
|
if (argument === '--force') {
|
|
@@ -592,9 +1363,10 @@ async function runSetup(args) {
|
|
|
592
1363
|
profile = value;
|
|
593
1364
|
else if (argument === '--output')
|
|
594
1365
|
output = value;
|
|
595
|
-
|
|
1366
|
+
if (argument === '--dsh-patch')
|
|
596
1367
|
patchFile = value;
|
|
597
|
-
|
|
1368
|
+
else
|
|
1369
|
+
initArgs.push(argument ?? '', value);
|
|
598
1370
|
index += 1;
|
|
599
1371
|
continue;
|
|
600
1372
|
}
|
|
@@ -604,7 +1376,7 @@ async function runSetup(args) {
|
|
|
604
1376
|
if (!noInstall && resolvedProfile === undefined) {
|
|
605
1377
|
const candidates = await discoverDshProfiles();
|
|
606
1378
|
if (candidates.length === 0) {
|
|
607
|
-
throw new Error('setup could not find a DSH profile with third-party bundles;
|
|
1379
|
+
throw new Error('setup could not find a DSH profile with third-party bundles; install one with `dsh plugin --profile <name> add <package>@<exact-version>`, then rerun setup');
|
|
608
1380
|
}
|
|
609
1381
|
if (candidates.length > 1) {
|
|
610
1382
|
throw new Error(`setup found multiple DSH profiles with third-party bundles (${candidates.join(', ')}); pass --profile <name>`);
|
|
@@ -614,6 +1386,12 @@ async function runSetup(args) {
|
|
|
614
1386
|
if (resolvedProfile !== undefined && profile === undefined) {
|
|
615
1387
|
initArgs.unshift('--profile', resolvedProfile);
|
|
616
1388
|
}
|
|
1389
|
+
// Setup is the beginner-facing path: make the generated DSH overlay the
|
|
1390
|
+
// default so a first run does not fall back to hidden environment variables.
|
|
1391
|
+
// `init` remains available when a user deliberately wants the legacy
|
|
1392
|
+
// environment-variable wiring instead.
|
|
1393
|
+
if (patchFile !== undefined)
|
|
1394
|
+
initArgs.push('--dsh-patch', patchFile);
|
|
617
1395
|
if (!noInstall) {
|
|
618
1396
|
if (resolvedProfile === undefined)
|
|
619
1397
|
throw new Error('setup could not select a DSH profile');
|
|
@@ -627,6 +1405,10 @@ async function runSetup(args) {
|
|
|
627
1405
|
stdio: 'inherit',
|
|
628
1406
|
});
|
|
629
1407
|
if (result.error !== undefined) {
|
|
1408
|
+
const spawnError = result.error;
|
|
1409
|
+
if (spawnError.code === 'ENOENT') {
|
|
1410
|
+
throw new Error('setup could not find the `dsh` command; install DeepSeek Harness, verify `dsh --help` works, then rerun setup');
|
|
1411
|
+
}
|
|
630
1412
|
throw new Error(`setup could not run ${dshCommand}: ${safeErrorMessage(result.error.message)}`);
|
|
631
1413
|
}
|
|
632
1414
|
if (result.status !== 0) {
|
|
@@ -650,17 +1432,47 @@ async function runSetup(args) {
|
|
|
650
1432
|
doctorOptions.patchFile = patchFile;
|
|
651
1433
|
const doctor = await createDoctorReport(doctorOptions);
|
|
652
1434
|
process.stdout.write(`\nLocal wiring check:\n${renderDoctorReport(doctor)}`);
|
|
653
|
-
|
|
1435
|
+
if (doctor.status === 'blocked')
|
|
1436
|
+
return 1;
|
|
1437
|
+
if (!startDsh)
|
|
1438
|
+
return 0;
|
|
1439
|
+
if (doctorProfile === undefined) {
|
|
1440
|
+
throw new Error('setup --start could not determine the DSH profile; pass --profile <name>');
|
|
1441
|
+
}
|
|
1442
|
+
const dshCommand = process.platform === 'win32' ? 'dsh.cmd' : 'dsh';
|
|
1443
|
+
const dshArgs = ['--profile', doctorProfile];
|
|
1444
|
+
const dshEnvironment = patchFile === undefined
|
|
1445
|
+
? { ...process.env, UPSTREAM_RADAR_CONFIG: outputPath, UPSTREAM_RADAR_STATE: statePath }
|
|
1446
|
+
: process.env;
|
|
1447
|
+
if (patchFile !== undefined)
|
|
1448
|
+
dshArgs.push('--patch', patchFile);
|
|
1449
|
+
process.stdout.write(`\nStarting DSH profile ${doctorProfile}; press Ctrl-C to stop.\n`);
|
|
1450
|
+
const result = spawnSync(dshCommand, dshArgs, { env: dshEnvironment, stdio: 'inherit' });
|
|
1451
|
+
if (result.error !== undefined) {
|
|
1452
|
+
const spawnError = result.error;
|
|
1453
|
+
if (spawnError.code === 'ENOENT') {
|
|
1454
|
+
throw new Error('setup --start could not find the `dsh` command; install DeepSeek Harness, verify `dsh --help` works, then rerun setup');
|
|
1455
|
+
}
|
|
1456
|
+
throw new Error(`setup --start could not run ${dshCommand}: ${safeErrorMessage(result.error.message)}`);
|
|
1457
|
+
}
|
|
1458
|
+
return result.status ?? 1;
|
|
654
1459
|
}
|
|
655
1460
|
async function runInit(args) {
|
|
656
1461
|
let profile;
|
|
1462
|
+
let npmLockPath;
|
|
1463
|
+
let pnpmLockPath;
|
|
1464
|
+
let rootSpec;
|
|
657
1465
|
let output = 'upstream-radar.config.json';
|
|
658
1466
|
let dshPatch;
|
|
659
1467
|
let projectId;
|
|
660
1468
|
let projectName;
|
|
661
1469
|
let repository;
|
|
662
1470
|
let workspace;
|
|
1471
|
+
let webhookUrlEnv;
|
|
1472
|
+
let webhookSecretEnv;
|
|
663
1473
|
let registry;
|
|
1474
|
+
let minimumSeverity;
|
|
1475
|
+
let quietHoursValue;
|
|
664
1476
|
let force = false;
|
|
665
1477
|
let json = false;
|
|
666
1478
|
const channels = [];
|
|
@@ -670,14 +1482,21 @@ async function runInit(args) {
|
|
|
670
1482
|
force = true;
|
|
671
1483
|
else if (argument === '--json')
|
|
672
1484
|
json = true;
|
|
673
|
-
else if (argument === '--profile' || argument === '--output' || argument === '--dsh-patch' || argument === '--project-id'
|
|
1485
|
+
else if (argument === '--profile' || argument === '--npm-lock' || argument === '--pnpm-lock' || argument === '--root' || argument === '--output' || argument === '--dsh-patch' || argument === '--project-id'
|
|
674
1486
|
|| argument === '--project-name' || argument === '--repository' || argument === '--workspace'
|
|
675
|
-
|| argument === '--channel' || argument === '--
|
|
1487
|
+
|| argument === '--channel' || argument === '--webhook-url-env' || argument === '--webhook-secret-env'
|
|
1488
|
+
|| argument === '--registry' || argument === '--minimum-severity' || argument === '--quiet-hours') {
|
|
676
1489
|
const value = args[index + 1];
|
|
677
1490
|
if (value === undefined || value.startsWith('-'))
|
|
678
1491
|
throw new Error(`${argument} requires a value`);
|
|
679
1492
|
if (argument === '--profile')
|
|
680
1493
|
profile = value;
|
|
1494
|
+
else if (argument === '--npm-lock')
|
|
1495
|
+
npmLockPath = value;
|
|
1496
|
+
else if (argument === '--pnpm-lock')
|
|
1497
|
+
pnpmLockPath = value;
|
|
1498
|
+
else if (argument === '--root')
|
|
1499
|
+
rootSpec = value;
|
|
681
1500
|
else if (argument === '--output')
|
|
682
1501
|
output = value;
|
|
683
1502
|
else if (argument === '--dsh-patch')
|
|
@@ -692,6 +1511,14 @@ async function runInit(args) {
|
|
|
692
1511
|
workspace = value;
|
|
693
1512
|
else if (argument === '--channel')
|
|
694
1513
|
channels.push(value);
|
|
1514
|
+
else if (argument === '--webhook-url-env')
|
|
1515
|
+
webhookUrlEnv = value;
|
|
1516
|
+
else if (argument === '--webhook-secret-env')
|
|
1517
|
+
webhookSecretEnv = value;
|
|
1518
|
+
else if (argument === '--minimum-severity')
|
|
1519
|
+
minimumSeverity = value;
|
|
1520
|
+
else if (argument === '--quiet-hours')
|
|
1521
|
+
quietHoursValue = value;
|
|
695
1522
|
else
|
|
696
1523
|
registry = value;
|
|
697
1524
|
index += 1;
|
|
@@ -700,6 +1527,68 @@ async function runInit(args) {
|
|
|
700
1527
|
throw new Error(`unknown option for init: ${argument}`);
|
|
701
1528
|
}
|
|
702
1529
|
}
|
|
1530
|
+
const notificationPolicy = parseNotificationPolicyArguments(minimumSeverity, quietHoursValue);
|
|
1531
|
+
if (npmLockPath !== undefined || pnpmLockPath !== undefined) {
|
|
1532
|
+
if (npmLockPath !== undefined && pnpmLockPath !== undefined) {
|
|
1533
|
+
throw new Error('init accepts only one of --npm-lock or --pnpm-lock');
|
|
1534
|
+
}
|
|
1535
|
+
const lockKind = npmLockPath === undefined ? 'pnpm-lock' : 'npm-lock';
|
|
1536
|
+
const lockfile = npmLockPath ?? pnpmLockPath;
|
|
1537
|
+
if (lockfile === undefined)
|
|
1538
|
+
throw new Error('init lockfile path is missing');
|
|
1539
|
+
if (profile !== undefined || dshPatch !== undefined || registry !== undefined) {
|
|
1540
|
+
throw new Error(`init --${lockKind} does not accept --profile, --dsh-patch or --registry`);
|
|
1541
|
+
}
|
|
1542
|
+
const root = rootSpec === undefined
|
|
1543
|
+
? await inferLockfileRoot(lockfile, lockKind === 'npm-lock' ? 'npm' : 'pnpm')
|
|
1544
|
+
: parseExactPackageCoordinate(rootSpec);
|
|
1545
|
+
const config = lockKind === 'npm-lock'
|
|
1546
|
+
? await createRadarConfigFromNpmLock({
|
|
1547
|
+
lockfile,
|
|
1548
|
+
root,
|
|
1549
|
+
...(projectId === undefined ? {} : { projectId }),
|
|
1550
|
+
...(projectName === undefined ? {} : { projectName }),
|
|
1551
|
+
...(repository === undefined ? {} : { repository }),
|
|
1552
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
1553
|
+
...(channels.length === 0 ? {} : { channels }),
|
|
1554
|
+
...(webhookUrlEnv === undefined ? {} : { webhookUrlEnv }),
|
|
1555
|
+
...(webhookSecretEnv === undefined ? {} : { webhookSecretEnv }),
|
|
1556
|
+
...(notificationPolicy === undefined ? {} : { notificationPolicy }),
|
|
1557
|
+
})
|
|
1558
|
+
: await createRadarConfigFromPnpmLock({
|
|
1559
|
+
lockfile,
|
|
1560
|
+
root,
|
|
1561
|
+
...(projectId === undefined ? {} : { projectId }),
|
|
1562
|
+
...(projectName === undefined ? {} : { projectName }),
|
|
1563
|
+
...(repository === undefined ? {} : { repository }),
|
|
1564
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
1565
|
+
...(channels.length === 0 ? {} : { channels }),
|
|
1566
|
+
...(webhookUrlEnv === undefined ? {} : { webhookUrlEnv }),
|
|
1567
|
+
...(webhookSecretEnv === undefined ? {} : { webhookSecretEnv }),
|
|
1568
|
+
...(notificationPolicy === undefined ? {} : { notificationPolicy }),
|
|
1569
|
+
});
|
|
1570
|
+
const outputPath = await writeRadarConfig(config, { output: resolve(output), force });
|
|
1571
|
+
const plugin = config.projects[0]?.plugins[0];
|
|
1572
|
+
if (plugin === undefined)
|
|
1573
|
+
throw new Error(`${lockKind} initialization produced no plugin`);
|
|
1574
|
+
if (json) {
|
|
1575
|
+
process.stdout.write(`${JSON.stringify({
|
|
1576
|
+
output: outputPath,
|
|
1577
|
+
source: plugin.graph.source,
|
|
1578
|
+
root: plugin.package,
|
|
1579
|
+
nodes: plugin.graph.nodes.length,
|
|
1580
|
+
edges: plugin.graph.edges.length,
|
|
1581
|
+
...(plugin.graph.unresolved === undefined ? {} : { unresolved: plugin.graph.unresolved.length }),
|
|
1582
|
+
}, null, 2)}\n`);
|
|
1583
|
+
}
|
|
1584
|
+
else {
|
|
1585
|
+
const unresolved = plugin.graph.unresolved?.length ?? 0;
|
|
1586
|
+
process.stdout.write(`Created ${outputPath}\nSource: ${plugin.graph.source ?? lockKind}\nRoot: ${plugin.package.name}@${plugin.package.version}\nGraph: ${plugin.graph.nodes.length} nodes, ${plugin.graph.edges.length} edges, ${unresolved} unresolved\nNext: upstream-radar radar check ${shellQuote(outputPath)} --frozen\n`);
|
|
1587
|
+
}
|
|
1588
|
+
return 0;
|
|
1589
|
+
}
|
|
1590
|
+
if (rootSpec !== undefined)
|
|
1591
|
+
throw new Error('init --root is only valid with --npm-lock or --pnpm-lock');
|
|
703
1592
|
let autoSelected = false;
|
|
704
1593
|
let resolvedProfile = profile;
|
|
705
1594
|
if (resolvedProfile === undefined) {
|
|
@@ -742,6 +1631,9 @@ async function runInit(args) {
|
|
|
742
1631
|
...(repository === undefined ? {} : { repository }),
|
|
743
1632
|
...(workspace === undefined ? {} : { workspace }),
|
|
744
1633
|
...(channels.length === 0 ? {} : { channels }),
|
|
1634
|
+
...(webhookUrlEnv === undefined ? {} : { webhookUrlEnv }),
|
|
1635
|
+
...(webhookSecretEnv === undefined ? {} : { webhookSecretEnv }),
|
|
1636
|
+
...(notificationPolicy === undefined ? {} : { notificationPolicy }),
|
|
745
1637
|
...(registry === undefined ? {} : { registry }),
|
|
746
1638
|
});
|
|
747
1639
|
config.dshProfile = { name: resolvedProfile };
|
|
@@ -756,7 +1648,13 @@ async function runInit(args) {
|
|
|
756
1648
|
force,
|
|
757
1649
|
});
|
|
758
1650
|
const plugins = config.projects[0]?.plugins ?? [];
|
|
759
|
-
|
|
1651
|
+
// Keep the printed recovery command independent of the package manager that
|
|
1652
|
+
// happened to launch this copy. Node ships with npm/npx, while a user may
|
|
1653
|
+
// have entered setup through pnpm, yarn, or a global binary. The command is
|
|
1654
|
+
// deliberately exact-versioned so the doctor checks the same release that
|
|
1655
|
+
// generated the config.
|
|
1656
|
+
const doctorCommand = `npx --yes upstream-radar@${TOOL_VERSION} doctor ${shellQuote(outputPath)} --profile ${shellQuote(resolvedProfile)}${patchPath === undefined ? '' : ` --patch ${shellQuote(patchPath)}`}`;
|
|
1657
|
+
const statusCommand = `npx --yes upstream-radar@${TOOL_VERSION} radar status ${shellQuote(outputPath)} --state ${shellQuote(statePath)}`;
|
|
760
1658
|
const startCommand = patchPath === undefined
|
|
761
1659
|
? `dsh --profile ${shellQuote(resolvedProfile)}`
|
|
762
1660
|
: `dsh --profile ${shellQuote(resolvedProfile)} --patch ${shellQuote(patchPath)}`;
|
|
@@ -768,6 +1666,7 @@ async function runInit(args) {
|
|
|
768
1666
|
edges: plugin.graph.edges.length,
|
|
769
1667
|
...(plugin.graph.source === undefined ? {} : { source: plugin.graph.source }),
|
|
770
1668
|
...(plugin.graph.hostRuntime === undefined ? {} : { hostRuntimeNodes: plugin.graph.hostRuntime.resolvedNodes }),
|
|
1669
|
+
...(plugin.graph.hostRuntime?.package === undefined ? {} : { hostRuntimePackage: plugin.graph.hostRuntime.package }),
|
|
771
1670
|
...(plugin.graph.unresolved === undefined ? {} : { unresolved: plugin.graph.unresolved.length }),
|
|
772
1671
|
...(plugin.graph.unresolved === undefined ? {} : {
|
|
773
1672
|
requiredUnresolved: plugin.graph.unresolved.filter(item => item.kind !== 'optional').length,
|
|
@@ -784,13 +1683,14 @@ async function runInit(args) {
|
|
|
784
1683
|
const requiredUnresolved = plugin.graph.unresolved?.filter(item => item.kind !== 'optional').length ?? 0;
|
|
785
1684
|
const optionalUnresolved = unresolved - requiredUnresolved;
|
|
786
1685
|
const hostRuntime = plugin.graph.hostRuntime?.resolvedNodes ?? 0;
|
|
787
|
-
|
|
1686
|
+
const hostRuntimePackage = plugin.graph.hostRuntime?.package;
|
|
1687
|
+
process.stdout.write(` ${plugin.package.name}@${plugin.package.version} (${plugin.graph.nodes.length} dependency nodes${plugin.graph.source === undefined ? '' : `, ${plugin.graph.source}`}${hostRuntime === 0 ? '' : `, ${hostRuntime} DSH host`}${hostRuntimePackage === undefined ? '' : `, runtime ${hostRuntimePackage.name}@${hostRuntimePackage.version}`}${requiredUnresolved === 0 ? '' : `, ${requiredUnresolved} required unresolved`}${optionalUnresolved === 0 ? '' : `, ${optionalUnresolved} optional absent`})\n`);
|
|
788
1688
|
}
|
|
789
1689
|
if (patchPath === undefined) {
|
|
790
|
-
process.stdout.write(`\nReview the generated inventory, then verify the wiring:\n ${doctorCommand}\n\nStart DSH (keep it running):\n export UPSTREAM_RADAR_CONFIG=${shellQuote(outputPath)}\n export UPSTREAM_RADAR_STATE=${shellQuote(statePath)}\n ${startCommand}\n`);
|
|
1690
|
+
process.stdout.write(`\nReview the generated inventory, then verify the wiring:\n ${doctorCommand}\n\nStart DSH (keep it running):\n export UPSTREAM_RADAR_CONFIG=${shellQuote(outputPath)}\n export UPSTREAM_RADAR_STATE=${shellQuote(statePath)}\n ${startCommand}\n\nAfter the first check, inspect the local result:\n ${statusCommand}\n`);
|
|
791
1691
|
}
|
|
792
1692
|
else {
|
|
793
|
-
process.stdout.write(`Created ${patchPath}\n\nReview the generated inventory and DSH overlay, then verify the wiring:\n ${doctorCommand}\n\nStart DSH (keep it running):\n ${startCommand}\n`);
|
|
1693
|
+
process.stdout.write(`Created ${patchPath}\n\nReview the generated inventory and DSH overlay, then verify the wiring:\n ${doctorCommand}\n\nStart DSH (keep it running):\n ${startCommand}\n\nAfter the first check, inspect the local result:\n ${statusCommand}\n`);
|
|
794
1694
|
}
|
|
795
1695
|
}
|
|
796
1696
|
return 0;
|
|
@@ -842,6 +1742,11 @@ async function runDoctor(args) {
|
|
|
842
1742
|
return report.status === 'blocked' ? 1 : 0;
|
|
843
1743
|
}
|
|
844
1744
|
async function main(args) {
|
|
1745
|
+
const help = commandHelp(args);
|
|
1746
|
+
if (help !== undefined) {
|
|
1747
|
+
process.stdout.write(help);
|
|
1748
|
+
return 0;
|
|
1749
|
+
}
|
|
845
1750
|
const command = args[0];
|
|
846
1751
|
if (command === undefined || command === '--help' || command === '-h' || command === 'help') {
|
|
847
1752
|
process.stdout.write(usage());
|
|
@@ -851,14 +1756,20 @@ async function main(args) {
|
|
|
851
1756
|
process.stdout.write(`${TOOL_VERSION}\n`);
|
|
852
1757
|
return 0;
|
|
853
1758
|
}
|
|
1759
|
+
if (command === 'quickstart')
|
|
1760
|
+
return runQuickstart(args.slice(1));
|
|
854
1761
|
if (command === 'setup')
|
|
855
1762
|
return runSetup(args.slice(1));
|
|
856
1763
|
if (command === 'init')
|
|
857
1764
|
return runInit(args.slice(1));
|
|
858
1765
|
if (command === 'doctor')
|
|
859
1766
|
return runDoctor(args.slice(1));
|
|
1767
|
+
if (command === 'graph')
|
|
1768
|
+
return runGraph(args.slice(1));
|
|
860
1769
|
if (command === 'probe')
|
|
861
1770
|
return runProbe(args.slice(1));
|
|
1771
|
+
if (command === 'demo')
|
|
1772
|
+
return runDemo(args.slice(1));
|
|
862
1773
|
if (command === 'benchmark')
|
|
863
1774
|
return runBenchmark(args.slice(1));
|
|
864
1775
|
if (command === 'radar')
|
|
@@ -867,6 +1778,12 @@ async function main(args) {
|
|
|
867
1778
|
return runTask(args.slice(1));
|
|
868
1779
|
if (command === 'analysis')
|
|
869
1780
|
return runAnalysis(args.slice(1));
|
|
1781
|
+
if (command === 'mute')
|
|
1782
|
+
return runIncidentMute(args.slice(1));
|
|
1783
|
+
if (command === 'unmute')
|
|
1784
|
+
return runIncidentUnmute(args.slice(1));
|
|
1785
|
+
if (command === 'triage')
|
|
1786
|
+
return runIncidentTriage(args.slice(1));
|
|
870
1787
|
if (command !== 'scan' && command !== 'inspect')
|
|
871
1788
|
throw new Error(`unknown command: ${command}`);
|
|
872
1789
|
const target = args[1];
|