upstream-radar 0.32.0 → 0.33.1
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 +616 -43
- package/cordis.patch.yml +1 -0
- package/dist/src/cli.js +1123 -34
- 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-profile-check.d.ts +79 -0
- package/dist/src/dsh-profile-check.d.ts.map +1 -0
- package/dist/src/dsh-profile-check.js +600 -0
- package/dist/src/dsh-profile-check.js.map +1 -0
- 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 +340 -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 +16 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +15 -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/npm.d.ts.map +1 -1
- package/dist/src/npm.js +9 -1
- package/dist/src/npm.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 +27 -2
- package/dist/src/render.js.map +1 -1
- package/dist/src/scan.d.ts.map +1 -1
- package/dist/src/scan.js +39 -0
- package/dist/src/scan.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/upstream-observer.d.ts +224 -0
- package/dist/src/upstream-observer.d.ts.map +1 -0
- package/dist/src/upstream-observer.js +1342 -0
- package/dist/src/upstream-observer.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 +593 -40
- package/package.json +30 -3
- package/schemas/quickstart.schema.json +56 -0
- package/schemas/webhook.schema.json +53 -0
package/dist/src/cli.js
CHANGED
|
@@ -1,72 +1,453 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { access, readFile } from 'node:fs/promises';
|
|
5
|
-
import { resolve } from 'node:path';
|
|
4
|
+
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
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 { checkDshProfile, renderDshProfileCheck, renderDshProfileCheckSummary } from './dsh-profile-check.js';
|
|
12
|
+
import { createDemoReport, renderDemo } from './demo.js';
|
|
11
13
|
import { GitHubReleaseClient } from './github-release.js';
|
|
12
|
-
import {
|
|
14
|
+
import { parseNpmLockGraph, parsePnpmLockGraph } from './graph.js';
|
|
15
|
+
import { createRadarConfigFromDshProfile, createRadarConfigFromNpmLock, createRadarConfigFromPnpmLock, discoverDshProfiles, refreshRadarConfigFromConfiguredProfile, resolveDshProfileDirectory, writeDshPatch, writeRadarConfig } from './init.js';
|
|
13
16
|
import { parsePackageManifestSnapshot, parseRadarConfig } from './inventory.js';
|
|
14
17
|
import { inspectNpmPackage } from './npm.js';
|
|
15
18
|
import { NpmCandidateGraphClient } from './npm-candidate.js';
|
|
16
19
|
import { NpmReleaseClient } from './npm-release.js';
|
|
20
|
+
import { createNotificationPolicyMap, filterNotifiableRadarEvents } from './notification-policy.js';
|
|
17
21
|
import { OsvClient } from './osv.js';
|
|
18
22
|
import { verdictAtLeast } from './policy.js';
|
|
23
|
+
import { createQuickstartReport, renderQuickstartReport } from './quickstart.js';
|
|
24
|
+
import { GitHubAdvisoryClient } from './github-advisory.js';
|
|
19
25
|
import { emptyRadarState, pollRadar } from './radar.js';
|
|
20
26
|
import { evaluateRadarPolicy, RADAR_COMPATIBILITY_FAIL_THRESHOLDS, RADAR_FAIL_THRESHOLDS, renderRadarPolicy, } from './radar-policy.js';
|
|
21
27
|
import { renderRadarEvents } from './radar-render.js';
|
|
28
|
+
import { createRadarHistory, renderRadarHistory } from './radar-history.js';
|
|
22
29
|
import { loadRadarState, saveRadarState } from './radar-state.js';
|
|
23
|
-
import { createRadarStatus, renderRadarStatus } from './radar-status.js';
|
|
30
|
+
import { createRadarNext, createRadarStatus, renderRadarNext, renderRadarStatus } from './radar-status.js';
|
|
24
31
|
import { renderTextReport } from './render.js';
|
|
25
32
|
import { scanDirectory } from './scan.js';
|
|
33
|
+
import { CisaKevClient, EpssClient } from './threat-intel.js';
|
|
34
|
+
import { loadObservationState, parseObserverConfigText, renderObserverReport, runDshAgentCommand, runOpenAiCompatibleAgent, runObserver, saveObservationState, UpstreamObserverClient, } from './upstream-observer.js';
|
|
26
35
|
import { TOOL_VERSION } from './version.js';
|
|
36
|
+
import { eventsForRadarWebhookTarget, markRadarWebhookEventsDelivered, markRadarWebhookEventsDeliveredForRoute, normalizeRadarWebhookUrl, queueRadarWebhookEvents, queueRadarWebhookEventsForRoute, resolveRadarWebhookTargets, sendRadarWebhook, undeliveredRadarWebhookEvents, undeliveredRadarWebhookEventsForRoute, } from './webhook.js';
|
|
27
37
|
const VALID_THRESHOLDS = new Set(['warn', 'review', 'block', 'never']);
|
|
28
38
|
const VALID_RADAR_THRESHOLDS = new Set(RADAR_FAIL_THRESHOLDS);
|
|
29
39
|
const VALID_RADAR_COMPATIBILITY_THRESHOLDS = new Set(RADAR_COMPATIBILITY_FAIL_THRESHOLDS);
|
|
40
|
+
const VALID_NOTIFICATION_SEVERITIES = new Set(['info', 'low', 'medium', 'high', 'critical']);
|
|
30
41
|
function safeErrorMessage(value, maxLength = 2_048) {
|
|
31
42
|
return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, character => (`\\u${character.codePointAt(0)?.toString(16).padStart(4, '0') ?? '0000'}`)).slice(0, maxLength);
|
|
32
43
|
}
|
|
33
44
|
function shellQuote(value) {
|
|
34
45
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
35
46
|
}
|
|
47
|
+
function parseNotificationPolicyArguments(minimumSeverity, quietHoursValue) {
|
|
48
|
+
if (minimumSeverity === undefined && quietHoursValue === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (minimumSeverity !== undefined && !VALID_NOTIFICATION_SEVERITIES.has(minimumSeverity)) {
|
|
51
|
+
throw new Error('--minimum-severity must be info, low, medium, high or critical');
|
|
52
|
+
}
|
|
53
|
+
let quietHours;
|
|
54
|
+
if (quietHoursValue !== undefined) {
|
|
55
|
+
const comma = quietHoursValue.indexOf(',');
|
|
56
|
+
const interval = comma < 0 ? '' : quietHoursValue.slice(comma + 1);
|
|
57
|
+
const dash = interval.indexOf('-');
|
|
58
|
+
const timezone = comma < 1 ? '' : quietHoursValue.slice(0, comma);
|
|
59
|
+
const start = dash < 1 ? '' : interval.slice(0, dash);
|
|
60
|
+
const end = dash < 0 ? '' : interval.slice(dash + 1);
|
|
61
|
+
if (!/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(start)
|
|
62
|
+
|| !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(end)
|
|
63
|
+
|| start === end
|
|
64
|
+
|| timezone.length === 0) {
|
|
65
|
+
throw new Error('--quiet-hours must use <IANA timezone>,<HH:MM>-<HH:MM> with different start and end times');
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
new Intl.DateTimeFormat('en-US', { timeZone: timezone }).format();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error('--quiet-hours must start with a valid IANA timezone');
|
|
72
|
+
}
|
|
73
|
+
quietHours = { timezone, start, end };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
...(minimumSeverity === undefined ? {} : { minimumSeverity: minimumSeverity }),
|
|
77
|
+
...(quietHours === undefined ? {} : { quietHours }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function commandUsage(key) {
|
|
81
|
+
const help = {
|
|
82
|
+
setup: `Upstream Radar — install the exact Radar bundle into DSH and prepare monitoring
|
|
83
|
+
|
|
84
|
+
Usage:
|
|
85
|
+
upstream-radar setup [--profile <name>] [--start] [--project-name <name>]
|
|
86
|
+
|
|
87
|
+
What it does:
|
|
88
|
+
Installs Radar into the selected DSH profile, writes a reviewable config and
|
|
89
|
+
--patch overlay, then runs the local doctor check. By default it does not
|
|
90
|
+
start DSH or execute plugin business actions; --start opts into launching DSH
|
|
91
|
+
only after the doctor check passes.
|
|
92
|
+
|
|
93
|
+
Prerequisite:
|
|
94
|
+
Install DeepSeek Harness first and verify that \`dsh --help\` works.
|
|
95
|
+
|
|
96
|
+
Common options:
|
|
97
|
+
--profile <name> select a DSH profile (auto-selects the only candidate)
|
|
98
|
+
--project-name <name> name shown in incidents and Agent tasks
|
|
99
|
+
--workspace <path> project workspace used to route Agent follow-up
|
|
100
|
+
--repository <url> repository evidence passed to the Agent task
|
|
101
|
+
--webhook-url-env <name> env var containing this project's HTTPS webhook URL
|
|
102
|
+
--webhook-secret-env <name> optional env var for a Feishu/Lark V2 signing secret
|
|
103
|
+
--output <path> Radar config path (default: ./upstream-radar.config.json)
|
|
104
|
+
--dsh-patch <path> DSH overlay path (default: ./upstream-radar.dsh.yml)
|
|
105
|
+
--minimum-severity <level> minimum vulnerability notice level
|
|
106
|
+
--quiet-hours <tz,start-end> e.g. Asia/Shanghai,22:00-08:00
|
|
107
|
+
--start start DSH after the local wiring check passes
|
|
108
|
+
--no-install reuse an already installed Radar bundle
|
|
109
|
+
--no-dsh-patch use legacy environment-variable wiring
|
|
110
|
+
|
|
111
|
+
Notification controls:
|
|
112
|
+
Add an optional \`notificationPolicy\` block to a generated projects[] entry
|
|
113
|
+
to set \`minimumSeverity\` and timezone-aware \`quietHours\`. This changes only
|
|
114
|
+
delivery; active evidence and queued tasks remain durable.
|
|
115
|
+
|
|
116
|
+
Next:
|
|
117
|
+
Review the generated files, then run the printed doctor command and start DSH
|
|
118
|
+
with the printed --patch command. Pass --start only when you explicitly accept
|
|
119
|
+
starting DSH in the same invocation after the doctor check passes.
|
|
120
|
+
|
|
121
|
+
If the profile has no third-party plugin yet:
|
|
122
|
+
dsh plugin --profile <name> add <package>@<exact-version>
|
|
123
|
+
`,
|
|
124
|
+
init: `Upstream Radar — create a reviewable inventory
|
|
125
|
+
|
|
126
|
+
Usage:
|
|
127
|
+
upstream-radar init --profile <name> [options]
|
|
128
|
+
upstream-radar init --pnpm-lock <pnpm-lock.yaml> [options]
|
|
129
|
+
upstream-radar init --npm-lock <package-lock.json> [options]
|
|
130
|
+
|
|
131
|
+
What it does:
|
|
132
|
+
Reads a DSH profile or lockfile without executing plugin code. Lockfile mode
|
|
133
|
+
is the pre-install path; it writes a static graph for a later frozen check.
|
|
134
|
+
|
|
135
|
+
Common options:
|
|
136
|
+
--root <package>@<exact-version> explicit lockfile root when it cannot be inferred
|
|
137
|
+
--project-name <name> name shown in incidents
|
|
138
|
+
--webhook-url-env <name> env var containing this project's HTTPS webhook URL
|
|
139
|
+
--webhook-secret-env <name> optional env var for a Feishu/Lark V2 signing secret
|
|
140
|
+
--output <path> config path (default: ./upstream-radar.config.json)
|
|
141
|
+
--dsh-patch <path> write a self-contained DSH overlay (profile mode)
|
|
142
|
+
--minimum-severity <level> minimum vulnerability notice level
|
|
143
|
+
--quiet-hours <tz,start-end> e.g. Asia/Shanghai,22:00-08:00
|
|
144
|
+
--json print a compact machine-readable summary
|
|
145
|
+
--force replace an existing output file
|
|
146
|
+
|
|
147
|
+
Notification controls:
|
|
148
|
+
Add an optional \`notificationPolicy\` block to a generated projects[] entry
|
|
149
|
+
to set \`minimumSeverity\` and timezone-aware \`quietHours\`. This changes only
|
|
150
|
+
delivery; active evidence and queued tasks remain durable.
|
|
151
|
+
|
|
152
|
+
Project webhook routing:
|
|
153
|
+
Store only environment variable names in the config. Set the URL and optional
|
|
154
|
+
Feishu secret in the DSH process environment; each project's changed events
|
|
155
|
+
then use its own endpoint while the native DSH Agent route remains workspace-based.
|
|
156
|
+
|
|
157
|
+
Next:
|
|
158
|
+
upstream-radar radar check ./upstream-radar.config.json --frozen
|
|
159
|
+
`,
|
|
160
|
+
doctor: `Upstream Radar — verify local wiring without polling upstream sources
|
|
161
|
+
|
|
162
|
+
Usage:
|
|
163
|
+
upstream-radar doctor [config.json] [--profile <name>] [--patch <path>] [--json]
|
|
164
|
+
|
|
165
|
+
Checks:
|
|
166
|
+
config/state readability, Radar registration in DSH, overlay alignment,
|
|
167
|
+
dependency coverage, and local webhook configuration. It does not contact
|
|
168
|
+
OSV, npm, GitHub, DSH, or a model.
|
|
169
|
+
`,
|
|
170
|
+
scan: `Upstream Radar — inspect a local package directory without running it
|
|
171
|
+
|
|
172
|
+
Usage:
|
|
173
|
+
upstream-radar scan <directory> [--json] [--fail-on <warn|review|block|never>]
|
|
174
|
+
|
|
175
|
+
Use this for an unpacked plugin before installation. Lifecycle scripts, remote
|
|
176
|
+
shell patterns, unsafe symlinks, mutable dependencies, and DSH bundle metadata
|
|
177
|
+
are recorded as bounded evidence.
|
|
178
|
+
`,
|
|
179
|
+
inspect: `Upstream Radar — inspect one exact npm artifact before installation
|
|
180
|
+
|
|
181
|
+
Usage:
|
|
182
|
+
upstream-radar inspect npm:<package>@<exact-version> [--deep] [--json]
|
|
183
|
+
[--registry <https-url>] [--fail-on <warn|review|block|never>]
|
|
184
|
+
|
|
185
|
+
--deep downloads the exact tarball, verifies npm integrity/signatures and
|
|
186
|
+
provenance when available, resolves the dependency graph with scripts disabled,
|
|
187
|
+
and queries the implemented vulnerability checks. An empty finding list is not
|
|
188
|
+
a safety certificate; check the coverage verdict before admitting the package.
|
|
189
|
+
The default gate exits 2 for review or block; use --fail-on block when review
|
|
190
|
+
should remain visible without failing CI.
|
|
191
|
+
`,
|
|
192
|
+
observe: `Upstream Radar — watch DSH plugin repositories and packages for meaningful upstream changes
|
|
193
|
+
|
|
194
|
+
Usage:
|
|
195
|
+
upstream-radar observe <targets.yml> [--state <observations.json>]
|
|
196
|
+
[--report <report.md>] [--dsh-agent-command <executable>]
|
|
197
|
+
[--dsh-agent-arg <argument>] [--llm-env-file <path>] [--retry-pending] [--json]
|
|
198
|
+
|
|
199
|
+
The first run creates a baseline. Later runs compare source commits, published
|
|
200
|
+
npm metadata, package manifests, and an optional npm/pnpm dependency graph. A
|
|
201
|
+
DSH Agent is called only for meaningful changes. Safety: does not install packages, run lifecycle scripts, load plugin code, or invoke a shell.
|
|
202
|
+
|
|
203
|
+
The Agent executable receives one read-only task prompt on stdin and should
|
|
204
|
+
return one JSON conclusion on stdout. If it is not configured, the task stays
|
|
205
|
+
in observations.json for a later explicit retry. As a simpler alternative,
|
|
206
|
+
--llm-env-file reads an OpenAI-compatible issue-locator/.env-style file for
|
|
207
|
+
only the model call. It accepts ISSUE_LOCATOR_LLM_*, OPENAI_*, or MODEL/CODEX_MODEL
|
|
208
|
+
keys; it never writes the key or endpoint to observations.json.
|
|
209
|
+
If a ModelBest-style base URL ends in /llm/v1, a 404 also retries the known
|
|
210
|
+
/llm/openai/v1 path.
|
|
211
|
+
`,
|
|
212
|
+
graph: `Upstream Radar — read a lockfile into the canonical dependency graph
|
|
213
|
+
|
|
214
|
+
Usage:
|
|
215
|
+
upstream-radar graph pnpm-lock <pnpm-lock.yaml> [--root <package>@<exact-version>] [--json]
|
|
216
|
+
upstream-radar graph npm-lock <package-lock.json> [--root <package>@<exact-version>] [--json]
|
|
217
|
+
|
|
218
|
+
This command is offline and does not install packages, run lifecycle scripts,
|
|
219
|
+
load plugin code, or query vulnerability sources.
|
|
220
|
+
`,
|
|
221
|
+
'profile-check': `Upstream Radar — check one DSH profile before starting it
|
|
222
|
+
|
|
223
|
+
Usage:
|
|
224
|
+
upstream-radar profile-check [profile-directory] [--patch <path>] [--report <path>] [--summary] [--json]
|
|
225
|
+
|
|
226
|
+
Reads the profile package manifest, lockfile, node_modules package metadata,
|
|
227
|
+
pnpm release-age policy, and cordis.patch.yml. It reports loader rows that
|
|
228
|
+
refer to missing packages and duplicate loader ids. It never installs packages,
|
|
229
|
+
starts DSH, loads plugin code, contacts a vulnerability source, or invokes a
|
|
230
|
+
DSH Agent/model. When the directory is omitted, the only DSH profile with
|
|
231
|
+
third-party bundles is selected automatically.
|
|
232
|
+
`,
|
|
233
|
+
probe: `Upstream Radar — test whether a DSH bundle loads in disposable profiles
|
|
234
|
+
|
|
235
|
+
Usage:
|
|
236
|
+
upstream-radar probe dsh-load <package.tgz> [--dsh-version <exact-version>] [--json]
|
|
237
|
+
upstream-radar probe dsh-matrix <package.tgz> --dsh-version <v1>,<v2>,... [--json]
|
|
238
|
+
|
|
239
|
+
The probe is bounded and isolated. It is a compatibility/load check, not a
|
|
240
|
+
semantic safety review or a substitute for dependency monitoring.
|
|
241
|
+
`,
|
|
242
|
+
demo: `Upstream Radar — show the exact-path-to-DSH handoff without side effects
|
|
243
|
+
|
|
244
|
+
Usage:
|
|
245
|
+
upstream-radar demo [--json]
|
|
246
|
+
|
|
247
|
+
The demo is network-free and uses only a local fixture. It does not inspect your
|
|
248
|
+
repository, install a plugin, start DSH, or claim that its advisory is real.
|
|
249
|
+
`,
|
|
250
|
+
quickstart: `Upstream Radar — choose the smallest honest first-use path
|
|
251
|
+
|
|
252
|
+
Usage:
|
|
253
|
+
upstream-radar quickstart [directory] [--json]
|
|
254
|
+
|
|
255
|
+
What it does:
|
|
256
|
+
Inspects only the selected directory and local DSH profile metadata. It finds
|
|
257
|
+
an existing Radar config, one supported lockfile, or eligible DSH profiles,
|
|
258
|
+
then prints the next commands in the right order.
|
|
259
|
+
|
|
260
|
+
Safety:
|
|
261
|
+
This command never installs packages, starts DSH, queries vulnerability
|
|
262
|
+
sources, or executes plugin code. It does not choose between two lockfiles or
|
|
263
|
+
multiple DSH profiles on your behalf.
|
|
264
|
+
`,
|
|
265
|
+
benchmark: `Upstream Radar — run offline compatibility-rule contracts
|
|
266
|
+
|
|
267
|
+
Usage:
|
|
268
|
+
upstream-radar benchmark compatibility [--json]
|
|
269
|
+
`,
|
|
270
|
+
radar: `Upstream Radar — monitor a reviewed inventory
|
|
271
|
+
|
|
272
|
+
Usage:
|
|
273
|
+
upstream-radar radar check <config.json> [options]
|
|
274
|
+
upstream-radar radar watch <config.json> [options]
|
|
275
|
+
upstream-radar radar status <config.json> [options]
|
|
276
|
+
upstream-radar radar history <config.json> [options]
|
|
277
|
+
upstream-radar radar compare <config.json> <before.json> <candidate.json> [options]
|
|
278
|
+
|
|
279
|
+
Use 'radar status' and 'radar history' for local, no-network diagnosis. Use
|
|
280
|
+
--frozen for CI or any run that must not read a live DSH profile.
|
|
281
|
+
`,
|
|
282
|
+
'radar check': `Upstream Radar — run one vulnerability and compatibility check
|
|
283
|
+
|
|
284
|
+
Usage:
|
|
285
|
+
upstream-radar radar check <config.json> [--state <state.json>] [--frozen]
|
|
286
|
+
[--fail-on <severity>] [--fail-on-compatibility <never|breaking|any>]
|
|
287
|
+
[--webhook <https-url>] [--threat-intel] [--json]
|
|
288
|
+
|
|
289
|
+
One cycle queries the configured sources and persists changed incidents. Use
|
|
290
|
+
--frozen when the config is the reviewed graph you want to enforce.
|
|
291
|
+
|
|
292
|
+
--threat-intel adds CISA KEV and FIRST EPSS prioritization signals for matched
|
|
293
|
+
CVEs. It is opt-in here; the native DSH bundle enables it by default.
|
|
294
|
+
`,
|
|
295
|
+
'radar watch': `Upstream Radar — keep monitoring a reviewed inventory
|
|
296
|
+
|
|
297
|
+
Usage:
|
|
298
|
+
upstream-radar radar watch <config.json> [--interval <seconds>] [--once]
|
|
299
|
+
[--state <state.json>] [--frozen] [--fail-on <severity>]
|
|
300
|
+
[--fail-on-compatibility <never|breaking|any>] [--webhook <https-url>]
|
|
301
|
+
[--threat-intel]
|
|
302
|
+
|
|
303
|
+
Use --once in CI. A long-running watch keeps polling and should not be given a
|
|
304
|
+
policy gate that would make it exit on the first incident.
|
|
305
|
+
`,
|
|
306
|
+
'radar status': `Upstream Radar — see the local monitoring snapshot
|
|
307
|
+
|
|
308
|
+
Usage:
|
|
309
|
+
upstream-radar radar status <config.json> [--state <state.json>]
|
|
310
|
+
[--fail-on <severity>] [--fail-on-compatibility <never|breaking|any>] [--json]
|
|
311
|
+
|
|
312
|
+
This command never polls OSV, npm, GitHub, or DSH. It shows whether monitoring
|
|
313
|
+
has run, coverage, active incidents, queued Agent tasks, and the next useful
|
|
314
|
+
action.
|
|
315
|
+
`,
|
|
316
|
+
'radar next': `Upstream Radar — show the one next action for the highest-priority incident
|
|
317
|
+
|
|
318
|
+
Usage:
|
|
319
|
+
upstream-radar radar next <config.json> [--state <state.json>] [--json]
|
|
320
|
+
|
|
321
|
+
This command is read-only and does not poll upstream sources. It selects the
|
|
322
|
+
same first incident as 'radar status', then points to the pending DSH task,
|
|
323
|
+
verified analysis, or the next check command.
|
|
324
|
+
`,
|
|
325
|
+
'radar history': `Upstream Radar — inspect the durable transition ledger
|
|
326
|
+
|
|
327
|
+
Usage:
|
|
328
|
+
upstream-radar radar history <config.json> [--state <state.json>] [--limit <n>] [--json]
|
|
329
|
+
|
|
330
|
+
This command is local-only and shows new, updated, resolved, and source-health
|
|
331
|
+
transitions, including their exact affected paths.
|
|
332
|
+
`,
|
|
333
|
+
'radar compare': `Upstream Radar — compare one reviewed release candidate
|
|
334
|
+
|
|
335
|
+
Usage:
|
|
336
|
+
upstream-radar radar compare <config.json> <before.json> <candidate.json>
|
|
337
|
+
[--notes <release-notes.txt>] [--json]
|
|
338
|
+
|
|
339
|
+
The release notes are treated as untrusted evidence. This command does not
|
|
340
|
+
install or execute the candidate.
|
|
341
|
+
`,
|
|
342
|
+
task: `Upstream Radar — inspect or acknowledge the DSH analysis outbox
|
|
343
|
+
|
|
344
|
+
Usage:
|
|
345
|
+
upstream-radar task list <state.json> [--json]
|
|
346
|
+
upstream-radar task show <state.json> [task-id] [--json]
|
|
347
|
+
upstream-radar task ack <state.json> <task-id>
|
|
348
|
+
`,
|
|
349
|
+
analysis: `Upstream Radar — inspect verified DSH conclusions
|
|
350
|
+
|
|
351
|
+
Usage:
|
|
352
|
+
upstream-radar analysis list <state.json> [--json]
|
|
353
|
+
upstream-radar analysis show <state.json> [incident-id] [--json]
|
|
354
|
+
`,
|
|
355
|
+
mute: `Upstream Radar — pause delivery for one exact incident until a fixed expiry
|
|
356
|
+
|
|
357
|
+
Usage:
|
|
358
|
+
upstream-radar mute <state.json> <incident-id> --until <ISO-8601> [--force] [--json]
|
|
359
|
+
|
|
360
|
+
This pauses DSH and webhook delivery only. The active incident, dependency paths,
|
|
361
|
+
history, and status remain visible. The mute expires automatically. Critical and
|
|
362
|
+
malware incidents require --force.
|
|
363
|
+
`,
|
|
364
|
+
unmute: `Upstream Radar — resume delivery for one incident immediately
|
|
365
|
+
|
|
366
|
+
Usage:
|
|
367
|
+
upstream-radar unmute <state.json> <incident-id> [--json]
|
|
368
|
+
`,
|
|
369
|
+
triage: `Upstream Radar — record human follow-up for one active incident
|
|
370
|
+
|
|
371
|
+
Usage:
|
|
372
|
+
upstream-radar triage <state.json> <incident-id>
|
|
373
|
+
--status <open|in-progress|blocked|accepted-risk>
|
|
374
|
+
[--owner <name>] [--note <text>] [--due <ISO-8601>] [--json]
|
|
375
|
+
|
|
376
|
+
This records ownership and work context without resolving, hiding, or changing
|
|
377
|
+
the deterministic incident. A new event version requires a fresh follow-up.
|
|
378
|
+
Blocked and accepted-risk states require a note.
|
|
379
|
+
`,
|
|
380
|
+
};
|
|
381
|
+
return help[key];
|
|
382
|
+
}
|
|
383
|
+
function commandHelp(args) {
|
|
384
|
+
const helpIndex = args.findIndex(argument => argument === '--help' || argument === '-h');
|
|
385
|
+
if (helpIndex < 0)
|
|
386
|
+
return undefined;
|
|
387
|
+
const command = args[0];
|
|
388
|
+
if (command === undefined)
|
|
389
|
+
return undefined;
|
|
390
|
+
const nested = command === 'radar' || command === 'task' || command === 'analysis'
|
|
391
|
+
? args[1]
|
|
392
|
+
: undefined;
|
|
393
|
+
const key = nested === undefined ? command : `${command} ${nested}`;
|
|
394
|
+
return commandUsage(key) ?? commandUsage(command);
|
|
395
|
+
}
|
|
36
396
|
function usage() {
|
|
37
397
|
return `Upstream Radar — always-on dependency and compatibility monitoring for DSH plugins
|
|
38
398
|
|
|
39
399
|
Usage:
|
|
40
|
-
upstream-radar
|
|
400
|
+
upstream-radar <command> --help
|
|
401
|
+
upstream-radar setup [--profile <name>] [--start] [options]
|
|
41
402
|
upstream-radar init [--profile <name>] [options]
|
|
403
|
+
upstream-radar init --pnpm-lock <pnpm-lock.yaml> [--root <package>@<exact-version>] [options]
|
|
404
|
+
upstream-radar init --npm-lock <package-lock.json> [--root <package>@<exact-version>] [options]
|
|
42
405
|
upstream-radar doctor [config.json] [options]
|
|
43
406
|
upstream-radar scan <directory> [--json] [--fail-on <warn|review|block|never>]
|
|
44
407
|
upstream-radar inspect npm:<package>@<exact-version> [--deep] [--json] [--fail-on <warn|review|block|never>]
|
|
408
|
+
upstream-radar observe <targets.yml> [--state <observations.json>] [--report <report.md>] [--dsh-agent-command <executable>] [--dsh-agent-arg <argument>] [--llm-env-file <path>] [--retry-pending] [--json]
|
|
409
|
+
upstream-radar graph <npm-lock|pnpm-lock> <lockfile> [--root <package>@<exact-version>] [--json]
|
|
410
|
+
upstream-radar profile-check [profile-directory] [--patch <path>] [--report <path>] [--summary] [--json]
|
|
45
411
|
upstream-radar probe dsh-load <package.tgz> [--dsh-version <exact-version>] [--timeout <seconds>] [--keep-profile] [--json]
|
|
46
412
|
upstream-radar probe dsh-matrix <package.tgz> --dsh-version <v1>[,<v2>,...] [--timeout <seconds>] [--keep-profile] [--json]
|
|
413
|
+
upstream-radar demo [--json]
|
|
414
|
+
upstream-radar quickstart [directory] [--json]
|
|
47
415
|
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]
|
|
416
|
+
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]
|
|
417
|
+
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
418
|
upstream-radar radar status <config.json> [--state <state.json>] [--fail-on <severity>] [--fail-on-compatibility <never|breaking|any>] [--json]
|
|
419
|
+
upstream-radar radar next <config.json> [--state <state.json>] [--json]
|
|
420
|
+
upstream-radar radar history <config.json> [--state <state.json>] [--limit <n>] [--json]
|
|
51
421
|
upstream-radar radar compare <config.json> <before.json> <candidate.json> [--notes <release-notes.txt>] [--json]
|
|
52
422
|
upstream-radar task list <state.json> [--json]
|
|
53
423
|
upstream-radar task show <state.json> [task-id] [--json]
|
|
54
424
|
upstream-radar task ack <state.json> <task-id>
|
|
55
425
|
upstream-radar analysis list <state.json> [--json]
|
|
56
426
|
upstream-radar analysis show <state.json> [incident-id] [--json]
|
|
427
|
+
upstream-radar mute <state.json> <incident-id> --until <ISO-8601> [--force] [--json]
|
|
428
|
+
upstream-radar unmute <state.json> <incident-id> [--json]
|
|
429
|
+
upstream-radar triage <state.json> <incident-id> --status <open|in-progress|blocked|accepted-risk> [--owner <name>] [--note <text>] [--due <ISO-8601>] [--json]
|
|
57
430
|
upstream-radar version
|
|
58
431
|
|
|
59
432
|
Commands:
|
|
60
433
|
setup install the exact Radar bundle, generate DSH wiring, and run doctor
|
|
61
|
-
init discover third-party bundles in
|
|
434
|
+
init discover third-party bundles in DSH or initialize a reviewable lockfile inventory
|
|
62
435
|
doctor check local Radar/DSH wiring without polling upstream sources
|
|
63
436
|
scan bounded, read-only inspection of a local package directory
|
|
64
437
|
inspect fetch and verify the exact npm artifact before inspecting its contents
|
|
438
|
+
observe compare upstream DSH plugin repositories and route only meaningful changes to a DSH Agent
|
|
439
|
+
graph read a lockfile into the canonical dependency graph without installing packages
|
|
440
|
+
profile-check check a DSH profile's lockfile and patch rows without starting DSH
|
|
65
441
|
probe run a bounded DSH bundle-load check or version matrix in disposable profiles
|
|
442
|
+
demo show the exact-path-to-DSH handoff without network, DSH, or plugin installation
|
|
443
|
+
quickstart choose the smallest first-use path without changing the environment
|
|
66
444
|
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
|
|
445
|
+
radar monitor vulnerability changes, watch continuously, find the next action, inspect status/history, or assess a candidate compatibility change
|
|
68
446
|
task inspect or acknowledge the durable DSH analysis outbox
|
|
69
447
|
analysis inspect verified DSH conclusions stored in the Radar state
|
|
448
|
+
mute pause delivery for one incident with a bounded expiry
|
|
449
|
+
unmute resume delivery for one incident immediately
|
|
450
|
+
triage record owner, work status, and handoff note for one incident
|
|
70
451
|
|
|
71
452
|
Options:
|
|
72
453
|
--deep resolve the dependency graph with scripts disabled and ask npm to verify signatures/provenance
|
|
@@ -74,16 +455,29 @@ Options:
|
|
|
74
455
|
--no-deep-candidates skip bounded transitive dependency graph checks for upgrade candidates
|
|
75
456
|
--state <path> persistent radar state (default: <config.json>.state.json)
|
|
76
457
|
--osv-base-url <url> alternate HTTPS OSV API base URL
|
|
458
|
+
--no-github-advisories disable the independent GitHub Advisory Database check for radar check/watch
|
|
459
|
+
--threat-intel radar check/watch: add CISA KEV and FIRST EPSS signals for matched CVEs
|
|
460
|
+
--webhook <https-url> radar check/watch: POST changed events to an HTTPS endpoint
|
|
461
|
+
--webhook-url-env <name> init/setup: env var containing a project's HTTPS webhook URL
|
|
462
|
+
--webhook-secret-env <name> init/setup: optional Feishu/Lark V2 signing secret env var
|
|
77
463
|
--interval <seconds> watch interval from 300 to 86400 seconds (default: 1800)
|
|
464
|
+
--limit <n> radar history: show 1 to 1000 recent transitions (default: 20)
|
|
78
465
|
--once run one watch cycle and exit (useful for CI and demos)
|
|
79
466
|
--frozen radar check/watch: use the reviewed graph in config without reading a local DSH profile
|
|
80
467
|
--fail-on <value> scan/inspect verdict or radar severity: unknown|info|low|medium|high|critical|never
|
|
81
468
|
--fail-on-compatibility <value> CI gate: never|breaking|any (default: never)
|
|
82
469
|
--notes <path> release notes used as untrusted compatibility evidence
|
|
83
470
|
--profile <name> DSH profile for init or doctor (init auto-selects the only candidate when omitted)
|
|
471
|
+
--npm-lock <path> init: build a static inventory from an npm v2/v3 package-lock.json
|
|
472
|
+
--pnpm-lock <path> init: build a static inventory from a pnpm v6/v9 lockfile
|
|
473
|
+
--root <coordinate> init/graph lockfiles: override the root; otherwise read package.json beside the lockfile
|
|
84
474
|
--no-install setup: reuse an already installed upstream-radar bundle
|
|
85
475
|
--output <path> init output path (default: ./upstream-radar.config.json)
|
|
86
|
-
--dsh-patch <path> write a self-contained DSH --patch overlay (
|
|
476
|
+
--dsh-patch <path> write a self-contained DSH --patch overlay (setup default: ./upstream-radar.dsh.yml)
|
|
477
|
+
--minimum-severity <level> init/setup notification threshold: info|low|medium|high|critical
|
|
478
|
+
--quiet-hours <tz,start-end> init/setup window, e.g. Asia/Shanghai,22:00-08:00
|
|
479
|
+
--start setup: start DSH after the local wiring check passes
|
|
480
|
+
--no-dsh-patch setup: keep the legacy UPSTREAM_RADAR_* environment-variable wiring
|
|
87
481
|
--patch <path> DSH overlay to verify with doctor
|
|
88
482
|
--force allow init to replace an existing output file
|
|
89
483
|
--json emit the canonical JSON report
|
|
@@ -94,6 +488,140 @@ Exit codes:
|
|
|
94
488
|
2 configured policy threshold was reached
|
|
95
489
|
`;
|
|
96
490
|
}
|
|
491
|
+
function parseExactPackageCoordinate(value) {
|
|
492
|
+
const at = value.lastIndexOf('@');
|
|
493
|
+
if (at <= 0 || at === value.length - 1)
|
|
494
|
+
throw new Error(`--root must be an exact package coordinate: ${value}`);
|
|
495
|
+
const name = value.slice(0, at);
|
|
496
|
+
const version = value.slice(at + 1);
|
|
497
|
+
if (name.startsWith('@') && !name.includes('/'))
|
|
498
|
+
throw new Error(`--root must include a scoped package name: ${value}`);
|
|
499
|
+
return { name, version };
|
|
500
|
+
}
|
|
501
|
+
async function inferLockfileRoot(lockfile, kind) {
|
|
502
|
+
const manifestPath = join(dirname(resolve(lockfile)), 'package.json');
|
|
503
|
+
try {
|
|
504
|
+
const manifest = parsePackageManifestSnapshot(await readJson(manifestPath));
|
|
505
|
+
return { name: manifest.name, version: manifest.version };
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
if (error.code === 'ENOENT') {
|
|
509
|
+
throw new Error(`could not infer the ${kind} lockfile root from ${manifestPath}; pass --root <package>@<exact-version>`);
|
|
510
|
+
}
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function runGraph(args) {
|
|
515
|
+
const kind = args[0];
|
|
516
|
+
if (kind !== 'npm-lock' && kind !== 'pnpm-lock')
|
|
517
|
+
throw new Error('graph requires the npm-lock or pnpm-lock subcommand');
|
|
518
|
+
const lockfile = args[1];
|
|
519
|
+
if (lockfile === undefined || lockfile.startsWith('-'))
|
|
520
|
+
throw new Error(`graph ${kind} requires a lockfile path`);
|
|
521
|
+
let rootSpec;
|
|
522
|
+
let json = false;
|
|
523
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
524
|
+
const argument = args[index];
|
|
525
|
+
if (argument === '--json') {
|
|
526
|
+
json = true;
|
|
527
|
+
}
|
|
528
|
+
else if (argument === '--root') {
|
|
529
|
+
const value = args[index + 1];
|
|
530
|
+
if (value === undefined || value.startsWith('-'))
|
|
531
|
+
throw new Error('--root requires a value');
|
|
532
|
+
rootSpec = value;
|
|
533
|
+
index += 1;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
throw new Error(`unknown option for graph ${kind}: ${argument}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const packageRoot = rootSpec === undefined
|
|
540
|
+
? await inferLockfileRoot(lockfile, kind === 'npm-lock' ? 'npm' : 'pnpm')
|
|
541
|
+
: parseExactPackageCoordinate(rootSpec);
|
|
542
|
+
const graph = kind === 'npm-lock'
|
|
543
|
+
? parseNpmLockGraph(await readJson(lockfile), packageRoot)
|
|
544
|
+
: parsePnpmLockGraph(await readBoundedFile(lockfile, 16 * 1024 * 1024), packageRoot);
|
|
545
|
+
if (json) {
|
|
546
|
+
process.stdout.write(`${JSON.stringify(graph, null, 2)}\n`);
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
const nodes = new Map(graph.nodes.map(node => [node.id, node]));
|
|
550
|
+
const root = nodes.get(graph.rootNodeId);
|
|
551
|
+
const display = (id) => {
|
|
552
|
+
const node = nodes.get(id);
|
|
553
|
+
return node === undefined ? id : `${node.name}@${node.version}`;
|
|
554
|
+
};
|
|
555
|
+
const unresolved = graph.unresolved ?? [];
|
|
556
|
+
process.stdout.write([
|
|
557
|
+
`Dependency graph: ${root === undefined ? graph.rootNodeId : `${root.name}@${root.version}`}`,
|
|
558
|
+
`Source: ${kind} (read-only; no install, no plugin execution)`,
|
|
559
|
+
`Nodes: ${graph.nodes.length}`,
|
|
560
|
+
`Edges: ${graph.edges.length}`,
|
|
561
|
+
`Unresolved: ${unresolved.length}`,
|
|
562
|
+
`Digest: ${graph.digest ?? '(none)'}`,
|
|
563
|
+
'',
|
|
564
|
+
'Edges:',
|
|
565
|
+
...(graph.edges.length === 0 ? [' (none)'] : graph.edges.map(edge => ` ${display(edge.from)} -[${edge.kind}]-> ${display(edge.to)}`)),
|
|
566
|
+
...(unresolved.length === 0 ? [] : [
|
|
567
|
+
'',
|
|
568
|
+
'Unresolved dependencies:',
|
|
569
|
+
...unresolved.map(item => ` ${display(item.from)} -[${item.kind}]-> ${item.name} (${item.spec})`),
|
|
570
|
+
]),
|
|
571
|
+
].join('\n') + '\n');
|
|
572
|
+
return 0;
|
|
573
|
+
}
|
|
574
|
+
async function runDshProfileCheck(args) {
|
|
575
|
+
let profileDirectory;
|
|
576
|
+
let firstOption = 0;
|
|
577
|
+
if (args[0] !== undefined && !args[0].startsWith('-')) {
|
|
578
|
+
profileDirectory = args[0];
|
|
579
|
+
firstOption = 1;
|
|
580
|
+
}
|
|
581
|
+
let json = false;
|
|
582
|
+
let summary = false;
|
|
583
|
+
let reportPath;
|
|
584
|
+
let patchPath;
|
|
585
|
+
for (let index = firstOption; index < args.length; index += 1) {
|
|
586
|
+
const argument = args[index];
|
|
587
|
+
if (argument === '--json') {
|
|
588
|
+
json = true;
|
|
589
|
+
}
|
|
590
|
+
else if (argument === '--summary') {
|
|
591
|
+
summary = true;
|
|
592
|
+
}
|
|
593
|
+
else if (argument === '--patch' || argument === '--report') {
|
|
594
|
+
const value = args[index + 1];
|
|
595
|
+
if (value === undefined || value.startsWith('-'))
|
|
596
|
+
throw new Error(`${argument} requires a value`);
|
|
597
|
+
if (argument === '--patch')
|
|
598
|
+
patchPath = value;
|
|
599
|
+
else
|
|
600
|
+
reportPath = value;
|
|
601
|
+
index += 1;
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
throw new Error(`unknown option for profile-check: ${argument}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (profileDirectory === undefined) {
|
|
608
|
+
const profiles = await discoverDshProfiles();
|
|
609
|
+
if (profiles.length === 0) {
|
|
610
|
+
throw new Error('profile-check could not find a DSH profile with third-party bundles; pass <profile-directory> explicitly');
|
|
611
|
+
}
|
|
612
|
+
if (profiles.length > 1) {
|
|
613
|
+
throw new Error(`profile-check found multiple DSH profiles with third-party bundles (${profiles.join(', ')}); pass <profile-directory> explicitly`);
|
|
614
|
+
}
|
|
615
|
+
profileDirectory = resolveDshProfileDirectory(profiles[0]);
|
|
616
|
+
}
|
|
617
|
+
const report = await checkDshProfile({ profileDirectory, ...(patchPath === undefined ? {} : { patchFile: patchPath }) });
|
|
618
|
+
const jsonText = `${JSON.stringify(report, null, 2)}\n`;
|
|
619
|
+
if (reportPath !== undefined) {
|
|
620
|
+
await writeFile(resolve(reportPath), reportPath.endsWith('.json') ? jsonText : summary ? renderDshProfileCheckSummary(report) : renderDshProfileCheck(report));
|
|
621
|
+
}
|
|
622
|
+
process.stdout.write(json ? jsonText : summary ? renderDshProfileCheckSummary(report) : renderDshProfileCheck(report));
|
|
623
|
+
return report.status === 'blocked' ? 2 : 0;
|
|
624
|
+
}
|
|
97
625
|
async function runTask(args) {
|
|
98
626
|
const subcommand = args[0];
|
|
99
627
|
if (subcommand !== 'list' && subcommand !== 'show' && subcommand !== 'ack') {
|
|
@@ -162,6 +690,200 @@ async function runTask(args) {
|
|
|
162
690
|
process.stdout.write(`Acknowledged ${safeErrorMessage(taskId)}.\n`);
|
|
163
691
|
return 0;
|
|
164
692
|
}
|
|
693
|
+
const MAX_INCIDENT_MUTE_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
694
|
+
const VALID_INCIDENT_TRIAGE_STATUSES = new Set([
|
|
695
|
+
'open',
|
|
696
|
+
'in-progress',
|
|
697
|
+
'blocked',
|
|
698
|
+
'accepted-risk',
|
|
699
|
+
]);
|
|
700
|
+
function activeEventForIncident(state, incidentId) {
|
|
701
|
+
return [
|
|
702
|
+
...Object.values(state.activeVulnerabilities),
|
|
703
|
+
...Object.values(state.activeCompatibility),
|
|
704
|
+
...Object.values(state.activeSourceHealth ?? {}),
|
|
705
|
+
].map(item => item.event).find(event => event.incidentId === incidentId);
|
|
706
|
+
}
|
|
707
|
+
async function runIncidentMute(args) {
|
|
708
|
+
const statePath = args[0];
|
|
709
|
+
const incidentId = args[1];
|
|
710
|
+
if (statePath === undefined || statePath.startsWith('-'))
|
|
711
|
+
throw new Error('mute requires a state file');
|
|
712
|
+
if (incidentId === undefined || incidentId.startsWith('-'))
|
|
713
|
+
throw new Error('mute requires an incident id');
|
|
714
|
+
let untilValue;
|
|
715
|
+
let force = false;
|
|
716
|
+
let json = false;
|
|
717
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
718
|
+
const argument = args[index];
|
|
719
|
+
if (argument === undefined)
|
|
720
|
+
throw new Error('mute received an incomplete option');
|
|
721
|
+
if (argument === '--force')
|
|
722
|
+
force = true;
|
|
723
|
+
else if (argument === '--json')
|
|
724
|
+
json = true;
|
|
725
|
+
else if (argument === '--until') {
|
|
726
|
+
const value = args[index + 1];
|
|
727
|
+
if (value === undefined || value.startsWith('-'))
|
|
728
|
+
throw new Error('--until requires a value');
|
|
729
|
+
untilValue = value;
|
|
730
|
+
index += 1;
|
|
731
|
+
}
|
|
732
|
+
else if (argument.startsWith('-')) {
|
|
733
|
+
throw new Error(`unknown option for mute: ${argument}`);
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
throw new Error(`mute received unexpected argument: ${argument}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (untilValue === undefined)
|
|
740
|
+
throw new Error('mute requires --until <ISO-8601>');
|
|
741
|
+
const mutedUntilMs = Date.parse(untilValue);
|
|
742
|
+
const nowMs = Date.now();
|
|
743
|
+
if (!Number.isFinite(mutedUntilMs))
|
|
744
|
+
throw new Error('--until must be a valid ISO-8601 timestamp');
|
|
745
|
+
if (mutedUntilMs <= nowMs)
|
|
746
|
+
throw new Error('--until must be in the future');
|
|
747
|
+
if (mutedUntilMs > nowMs + MAX_INCIDENT_MUTE_MS)
|
|
748
|
+
throw new Error('--until cannot be more than 30 days away');
|
|
749
|
+
const state = await loadRadarState(statePath);
|
|
750
|
+
const event = activeEventForIncident(state, incidentId);
|
|
751
|
+
if (event === undefined)
|
|
752
|
+
throw new Error(`active incident not found: ${incidentId}`);
|
|
753
|
+
const forceRequired = event.kind === 'malware'
|
|
754
|
+
|| (event.kind === 'vulnerability' && event.advisory.severity === 'critical');
|
|
755
|
+
if (forceRequired && !force)
|
|
756
|
+
throw new Error('critical or malware incidents require --force to mute');
|
|
757
|
+
const mutedUntil = new Date(mutedUntilMs).toISOString();
|
|
758
|
+
await saveRadarState(statePath, {
|
|
759
|
+
...state,
|
|
760
|
+
incidentMutes: {
|
|
761
|
+
...(state.incidentMutes ?? {}),
|
|
762
|
+
[incidentId]: { eventId: event.id, mutedUntil },
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
const result = { incidentId, eventId: event.id, mutedUntil, forced: force };
|
|
766
|
+
process.stdout.write(json
|
|
767
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
768
|
+
: `Muted delivery for ${safeErrorMessage(incidentId)} until ${mutedUntil}. Active evidence and history remain visible.\n`);
|
|
769
|
+
return 0;
|
|
770
|
+
}
|
|
771
|
+
async function runIncidentUnmute(args) {
|
|
772
|
+
const statePath = args[0];
|
|
773
|
+
const incidentId = args[1];
|
|
774
|
+
if (statePath === undefined || statePath.startsWith('-'))
|
|
775
|
+
throw new Error('unmute requires a state file');
|
|
776
|
+
if (incidentId === undefined || incidentId.startsWith('-'))
|
|
777
|
+
throw new Error('unmute requires an incident id');
|
|
778
|
+
let json = false;
|
|
779
|
+
for (const argument of args.slice(2)) {
|
|
780
|
+
if (argument === '--json')
|
|
781
|
+
json = true;
|
|
782
|
+
else
|
|
783
|
+
throw new Error(`unknown option for unmute: ${argument}`);
|
|
784
|
+
}
|
|
785
|
+
const state = await loadRadarState(statePath);
|
|
786
|
+
if (state.incidentMutes?.[incidentId] === undefined)
|
|
787
|
+
throw new Error(`incident is not muted: ${incidentId}`);
|
|
788
|
+
const nextIncidentMutes = Object.fromEntries(Object.entries(state.incidentMutes).filter(([id]) => id !== incidentId));
|
|
789
|
+
await saveRadarState(statePath, { ...state, incidentMutes: nextIncidentMutes });
|
|
790
|
+
const result = { incidentId, unmuted: true };
|
|
791
|
+
process.stdout.write(json
|
|
792
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
793
|
+
: `Resumed delivery for ${safeErrorMessage(incidentId)}.\n`);
|
|
794
|
+
return 0;
|
|
795
|
+
}
|
|
796
|
+
async function runIncidentTriage(args) {
|
|
797
|
+
const statePath = args[0];
|
|
798
|
+
const incidentId = args[1];
|
|
799
|
+
if (statePath === undefined || statePath.startsWith('-'))
|
|
800
|
+
throw new Error('triage requires a state file');
|
|
801
|
+
if (incidentId === undefined || incidentId.startsWith('-'))
|
|
802
|
+
throw new Error('triage requires an incident id');
|
|
803
|
+
let statusValue;
|
|
804
|
+
let ownerValue;
|
|
805
|
+
let noteValue;
|
|
806
|
+
let dueValue;
|
|
807
|
+
let json = false;
|
|
808
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
809
|
+
const argument = args[index];
|
|
810
|
+
if (argument === undefined)
|
|
811
|
+
throw new Error('triage received an incomplete option');
|
|
812
|
+
if (argument === '--json')
|
|
813
|
+
json = true;
|
|
814
|
+
else if (argument === '--status' || argument === '--owner' || argument === '--note' || argument === '--due') {
|
|
815
|
+
const value = args[index + 1];
|
|
816
|
+
if (value === undefined || value.startsWith('-'))
|
|
817
|
+
throw new Error(`${argument} requires a value`);
|
|
818
|
+
if (argument === '--status')
|
|
819
|
+
statusValue = value;
|
|
820
|
+
else if (argument === '--owner')
|
|
821
|
+
ownerValue = value;
|
|
822
|
+
else if (argument === '--note')
|
|
823
|
+
noteValue = value;
|
|
824
|
+
else
|
|
825
|
+
dueValue = value;
|
|
826
|
+
index += 1;
|
|
827
|
+
}
|
|
828
|
+
else if (argument.startsWith('-')) {
|
|
829
|
+
throw new Error(`unknown option for triage: ${argument}`);
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
throw new Error(`triage received unexpected argument: ${argument}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (statusValue === undefined)
|
|
836
|
+
throw new Error('triage requires --status <open|in-progress|blocked|accepted-risk>');
|
|
837
|
+
if (!VALID_INCIDENT_TRIAGE_STATUSES.has(statusValue)) {
|
|
838
|
+
throw new Error(`invalid triage --status value: ${statusValue}`);
|
|
839
|
+
}
|
|
840
|
+
const status = statusValue;
|
|
841
|
+
const owner = ownerValue?.trim();
|
|
842
|
+
const note = noteValue?.trim();
|
|
843
|
+
if (ownerValue !== undefined && (owner === undefined || owner.length === 0))
|
|
844
|
+
throw new Error('--owner cannot be empty');
|
|
845
|
+
if (owner !== undefined && owner.length > 512)
|
|
846
|
+
throw new Error('--owner must be 512 characters or fewer');
|
|
847
|
+
if (noteValue !== undefined && (note === undefined || note.length === 0))
|
|
848
|
+
throw new Error('--note cannot be empty');
|
|
849
|
+
if (note !== undefined && note.length > 2_048)
|
|
850
|
+
throw new Error('--note must be 2048 characters or fewer');
|
|
851
|
+
const dueAtMs = dueValue === undefined ? undefined : Date.parse(dueValue);
|
|
852
|
+
if (dueValue !== undefined && !Number.isFinite(dueAtMs))
|
|
853
|
+
throw new Error('--due must be a valid ISO-8601 timestamp');
|
|
854
|
+
const state = await loadRadarState(statePath);
|
|
855
|
+
const event = activeEventForIncident(state, incidentId);
|
|
856
|
+
if (event === undefined)
|
|
857
|
+
throw new Error(`active incident not found: ${incidentId}`);
|
|
858
|
+
const previous = state.incidentTriage?.[incidentId];
|
|
859
|
+
const inherited = previous?.eventId === event.id ? previous : undefined;
|
|
860
|
+
const recordNote = note ?? inherited?.note;
|
|
861
|
+
if ((status === 'blocked' || status === 'accepted-risk') && recordNote === undefined) {
|
|
862
|
+
throw new Error(`triage status ${status} requires --note <text>`);
|
|
863
|
+
}
|
|
864
|
+
const recordOwner = owner ?? inherited?.owner;
|
|
865
|
+
const recordDueAt = dueAtMs === undefined ? inherited?.dueAt : new Date(dueAtMs).toISOString();
|
|
866
|
+
const record = {
|
|
867
|
+
eventId: event.id,
|
|
868
|
+
status,
|
|
869
|
+
updatedAt: new Date().toISOString(),
|
|
870
|
+
...(recordOwner === undefined ? {} : { owner: recordOwner }),
|
|
871
|
+
...(recordNote === undefined ? {} : { note: recordNote }),
|
|
872
|
+
...(recordDueAt === undefined ? {} : { dueAt: recordDueAt }),
|
|
873
|
+
};
|
|
874
|
+
await saveRadarState(statePath, {
|
|
875
|
+
...state,
|
|
876
|
+
incidentTriage: {
|
|
877
|
+
...(state.incidentTriage ?? {}),
|
|
878
|
+
[incidentId]: record,
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
const result = { incidentId, ...record };
|
|
882
|
+
process.stdout.write(json
|
|
883
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
884
|
+
: `Recorded ${status} follow-up for ${safeErrorMessage(incidentId)}${record.owner === undefined ? '' : ` (owner: ${safeErrorMessage(record.owner)})`}.\n`);
|
|
885
|
+
return 0;
|
|
886
|
+
}
|
|
165
887
|
async function runAnalysis(args) {
|
|
166
888
|
const subcommand = args[0];
|
|
167
889
|
if (subcommand !== 'list' && subcommand !== 'show')
|
|
@@ -258,6 +980,18 @@ async function runBenchmark(args) {
|
|
|
258
980
|
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderCompatibilityBenchmark(report));
|
|
259
981
|
return report.summary.failed === 0 ? 0 : 1;
|
|
260
982
|
}
|
|
983
|
+
function runDemo(args) {
|
|
984
|
+
let json = false;
|
|
985
|
+
for (const argument of args) {
|
|
986
|
+
if (argument === '--json')
|
|
987
|
+
json = true;
|
|
988
|
+
else
|
|
989
|
+
throw new Error(`unknown option for demo: ${argument}`);
|
|
990
|
+
}
|
|
991
|
+
const report = createDemoReport();
|
|
992
|
+
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderDemo(report));
|
|
993
|
+
return 0;
|
|
994
|
+
}
|
|
261
995
|
async function runProbe(args) {
|
|
262
996
|
const mode = args[0];
|
|
263
997
|
if (mode !== 'dsh-load' && mode !== 'dsh-matrix')
|
|
@@ -337,8 +1071,8 @@ async function fileExists(path) {
|
|
|
337
1071
|
}
|
|
338
1072
|
async function runRadar(args) {
|
|
339
1073
|
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');
|
|
1074
|
+
if (subcommand !== 'check' && subcommand !== 'watch' && subcommand !== 'status' && subcommand !== 'next' && subcommand !== 'history' && subcommand !== 'compare') {
|
|
1075
|
+
throw new Error('radar requires check, watch, status, next, history or compare');
|
|
342
1076
|
}
|
|
343
1077
|
const configPath = args[1];
|
|
344
1078
|
if (configPath === undefined || configPath.startsWith('-'))
|
|
@@ -348,11 +1082,16 @@ async function runRadar(args) {
|
|
|
348
1082
|
let statePath;
|
|
349
1083
|
let osvBaseUrl;
|
|
350
1084
|
let registry;
|
|
1085
|
+
let webhookUrl;
|
|
351
1086
|
let notesPath;
|
|
352
1087
|
let intervalSeconds = 1_800;
|
|
353
1088
|
let intervalProvided = false;
|
|
1089
|
+
let historyLimit = 20;
|
|
1090
|
+
let historyLimitProvided = false;
|
|
354
1091
|
let once = false;
|
|
355
1092
|
let deepCandidates = true;
|
|
1093
|
+
let githubAdvisories = true;
|
|
1094
|
+
let threatIntel = false;
|
|
356
1095
|
let frozen = false;
|
|
357
1096
|
let failOn = 'never';
|
|
358
1097
|
let failOnCompatibility = 'never';
|
|
@@ -367,11 +1106,17 @@ async function runRadar(args) {
|
|
|
367
1106
|
else if (argument === '--no-deep-candidates') {
|
|
368
1107
|
deepCandidates = false;
|
|
369
1108
|
}
|
|
1109
|
+
else if (argument === '--no-github-advisories') {
|
|
1110
|
+
githubAdvisories = false;
|
|
1111
|
+
}
|
|
1112
|
+
else if (argument === '--threat-intel') {
|
|
1113
|
+
threatIntel = true;
|
|
1114
|
+
}
|
|
370
1115
|
else if (argument === '--frozen') {
|
|
371
1116
|
frozen = true;
|
|
372
1117
|
}
|
|
373
|
-
else if (argument === '--state' || argument === '--osv-base-url' || argument === '--registry'
|
|
374
|
-
|| argument === '--notes' || argument === '--interval' || argument === '--fail-on'
|
|
1118
|
+
else if (argument === '--state' || argument === '--osv-base-url' || argument === '--registry' || argument === '--webhook'
|
|
1119
|
+
|| argument === '--notes' || argument === '--interval' || argument === '--limit' || argument === '--fail-on'
|
|
375
1120
|
|| argument === '--fail-on-compatibility') {
|
|
376
1121
|
const value = args[index + 1];
|
|
377
1122
|
if (value === undefined || value.startsWith('-'))
|
|
@@ -382,6 +1127,8 @@ async function runRadar(args) {
|
|
|
382
1127
|
osvBaseUrl = value;
|
|
383
1128
|
else if (argument === '--registry')
|
|
384
1129
|
registry = value;
|
|
1130
|
+
else if (argument === '--webhook')
|
|
1131
|
+
webhookUrl = normalizeRadarWebhookUrl(value);
|
|
385
1132
|
else if (argument === '--notes')
|
|
386
1133
|
notesPath = value;
|
|
387
1134
|
else if (argument === '--fail-on') {
|
|
@@ -396,6 +1143,14 @@ async function runRadar(args) {
|
|
|
396
1143
|
}
|
|
397
1144
|
failOnCompatibility = value;
|
|
398
1145
|
}
|
|
1146
|
+
else if (argument === '--limit') {
|
|
1147
|
+
historyLimitProvided = true;
|
|
1148
|
+
const parsed = Number(value);
|
|
1149
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 1_000) {
|
|
1150
|
+
throw new Error('--limit must be an integer between 1 and 1000');
|
|
1151
|
+
}
|
|
1152
|
+
historyLimit = parsed;
|
|
1153
|
+
}
|
|
399
1154
|
else {
|
|
400
1155
|
intervalProvided = true;
|
|
401
1156
|
const parsed = Number(value);
|
|
@@ -417,10 +1172,13 @@ async function runRadar(args) {
|
|
|
417
1172
|
const readConfigForPoll = async () => frozen
|
|
418
1173
|
? readConfig()
|
|
419
1174
|
: refreshRadarConfigFromConfiguredProfile(await readConfig());
|
|
420
|
-
if (subcommand === 'status') {
|
|
1175
|
+
if (subcommand === 'status' || subcommand === 'next') {
|
|
421
1176
|
if (positional.length > 0 || notesPath !== undefined || once || intervalProvided
|
|
422
|
-
|| osvBaseUrl !== undefined || registry !== undefined || !deepCandidates || frozen || statePath === ':memory:'
|
|
423
|
-
|
|
1177
|
+
|| historyLimitProvided || osvBaseUrl !== undefined || registry !== undefined || webhookUrl !== undefined || !deepCandidates || !githubAdvisories || threatIntel || frozen || statePath === ':memory:'
|
|
1178
|
+
|| (subcommand === 'next' && (failOn !== 'never' || failOnCompatibility !== 'never'))) {
|
|
1179
|
+
throw new Error(subcommand === 'next'
|
|
1180
|
+
? 'radar next only accepts --state and --json options'
|
|
1181
|
+
: 'radar status only accepts --state, --fail-on, --fail-on-compatibility and --json options');
|
|
424
1182
|
}
|
|
425
1183
|
const config = await readConfig();
|
|
426
1184
|
const stateFile = statePath ?? `${resolve(configPath)}.state.json`;
|
|
@@ -433,6 +1191,11 @@ async function runRadar(args) {
|
|
|
433
1191
|
});
|
|
434
1192
|
const policy = evaluateRadarPolicy(state, failOn, failOnCompatibility);
|
|
435
1193
|
const policyEnabled = failOn !== 'never' || failOnCompatibility !== 'never';
|
|
1194
|
+
if (subcommand === 'next') {
|
|
1195
|
+
const next = createRadarNext(report, state);
|
|
1196
|
+
process.stdout.write(json ? `${JSON.stringify(next, null, 2)}\n` : renderRadarNext(next));
|
|
1197
|
+
return 0;
|
|
1198
|
+
}
|
|
436
1199
|
if (json) {
|
|
437
1200
|
process.stdout.write(!policyEnabled
|
|
438
1201
|
? `${JSON.stringify(report, null, 2)}\n`
|
|
@@ -445,6 +1208,27 @@ async function runRadar(args) {
|
|
|
445
1208
|
return 1;
|
|
446
1209
|
return policy.status === 'fail' ? 2 : 0;
|
|
447
1210
|
}
|
|
1211
|
+
if (subcommand === 'history') {
|
|
1212
|
+
if (positional.length > 0 || notesPath !== undefined || once || intervalProvided
|
|
1213
|
+
|| osvBaseUrl !== undefined || registry !== undefined || webhookUrl !== undefined || !deepCandidates || !githubAdvisories || frozen
|
|
1214
|
+
|| failOn !== 'never' || failOnCompatibility !== 'never' || threatIntel) {
|
|
1215
|
+
throw new Error('radar history only accepts --state, --limit and --json options');
|
|
1216
|
+
}
|
|
1217
|
+
// Parse the config as an input check, but deliberately do not refresh it
|
|
1218
|
+
// or contact any upstream source. History is a local diagnosis command.
|
|
1219
|
+
await readConfig();
|
|
1220
|
+
const stateFile = statePath ?? `${resolve(configPath)}.state.json`;
|
|
1221
|
+
const stateExists = await fileExists(stateFile);
|
|
1222
|
+
const state = stateExists ? await loadRadarState(stateFile) : emptyRadarState();
|
|
1223
|
+
const report = createRadarHistory(state, {
|
|
1224
|
+
configFile: resolve(configPath),
|
|
1225
|
+
stateFile: resolve(stateFile),
|
|
1226
|
+
stateExists,
|
|
1227
|
+
limit: historyLimit,
|
|
1228
|
+
});
|
|
1229
|
+
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderRadarHistory(report));
|
|
1230
|
+
return 0;
|
|
1231
|
+
}
|
|
448
1232
|
const osv = new OsvClient({
|
|
449
1233
|
...(osvBaseUrl === undefined ? {} : { baseUrl: osvBaseUrl }),
|
|
450
1234
|
});
|
|
@@ -455,14 +1239,60 @@ async function runRadar(args) {
|
|
|
455
1239
|
? new NpmCandidateGraphClient({ ...(registry === undefined ? {} : { registry }) })
|
|
456
1240
|
: undefined;
|
|
457
1241
|
const releaseNotesSource = new GitHubReleaseClient();
|
|
1242
|
+
const githubAdvisorySource = githubAdvisories
|
|
1243
|
+
? new GitHubAdvisoryClient({ ...(process.env.GITHUB_TOKEN === undefined ? {} : { token: process.env.GITHUB_TOKEN }) })
|
|
1244
|
+
: undefined;
|
|
1245
|
+
const threatIntelSources = threatIntel
|
|
1246
|
+
? [
|
|
1247
|
+
{ name: 'cisa-kev', source: new CisaKevClient() },
|
|
1248
|
+
{ name: 'epss', source: new EpssClient() },
|
|
1249
|
+
]
|
|
1250
|
+
: [];
|
|
458
1251
|
const stateFile = statePath ?? `${resolve(configPath)}.state.json`;
|
|
1252
|
+
if (webhookUrl !== undefined && statePath === ':memory:') {
|
|
1253
|
+
throw new Error('radar --webhook requires a persistent --state file so successful deliveries can be remembered');
|
|
1254
|
+
}
|
|
1255
|
+
const feishuSecret = process.env.UPSTREAM_RADAR_FEISHU_SECRET?.trim() || undefined;
|
|
459
1256
|
const runCheck = async () => {
|
|
460
1257
|
const config = await readConfigForPoll();
|
|
1258
|
+
const webhookTargets = resolveRadarWebhookTargets(config.projects, {
|
|
1259
|
+
...(webhookUrl === undefined ? {} : { globalUrl: webhookUrl }),
|
|
1260
|
+
...(feishuSecret === undefined ? {} : { globalFeishuSecret: feishuSecret }),
|
|
1261
|
+
});
|
|
1262
|
+
if (statePath === ':memory:' && webhookTargets.length > 0) {
|
|
1263
|
+
throw new Error('radar webhook routes require a persistent --state file so successful deliveries can be remembered');
|
|
1264
|
+
}
|
|
461
1265
|
const state = statePath === ':memory:' ? emptyRadarState() : await loadRadarState(stateFile);
|
|
462
|
-
const
|
|
1266
|
+
const checkedAt = new Date();
|
|
1267
|
+
const result = await pollRadar(config.projects, state, osv, checkedAt, releases, releaseNotesSource, candidateGraphs, githubAdvisorySource === undefined ? [] : [{ name: 'github-advisories', source: githubAdvisorySource }], threatIntelSources);
|
|
463
1268
|
if (statePath !== ':memory:')
|
|
464
1269
|
await saveRadarState(stateFile, result.state);
|
|
465
|
-
|
|
1270
|
+
const notificationPolicies = createNotificationPolicyMap(config.projects);
|
|
1271
|
+
let nextState = result.state;
|
|
1272
|
+
for (const target of webhookTargets) {
|
|
1273
|
+
const targetEvents = eventsForRadarWebhookTarget(result.events, target);
|
|
1274
|
+
const isLegacyGlobal = webhookUrl !== undefined && target.projectIds === undefined;
|
|
1275
|
+
const queuedState = isLegacyGlobal
|
|
1276
|
+
? queueRadarWebhookEvents(nextState, target.endpointHash, targetEvents)
|
|
1277
|
+
: queueRadarWebhookEventsForRoute(nextState, target.endpointHash, targetEvents);
|
|
1278
|
+
if (statePath !== ':memory:')
|
|
1279
|
+
await saveRadarState(stateFile, queuedState);
|
|
1280
|
+
const pendingWebhookEvents = filterNotifiableRadarEvents(isLegacyGlobal
|
|
1281
|
+
? undeliveredRadarWebhookEvents(queuedState, target.endpointHash, targetEvents)
|
|
1282
|
+
: undeliveredRadarWebhookEventsForRoute(queuedState, target.endpointHash, targetEvents), notificationPolicies, checkedAt, queuedState);
|
|
1283
|
+
if (pendingWebhookEvents.length === 0) {
|
|
1284
|
+
nextState = queuedState;
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
const payload = await sendRadarWebhook(target.url, pendingWebhookEvents, target.feishuSecret === undefined ? {} : { feishuSecret: target.feishuSecret });
|
|
1288
|
+
const deliveredIds = new Set(payload.events.map(event => event.id));
|
|
1289
|
+
const deliveredEvents = pendingWebhookEvents.filter(event => deliveredIds.has(event.id));
|
|
1290
|
+
nextState = isLegacyGlobal
|
|
1291
|
+
? markRadarWebhookEventsDelivered(queuedState, target.endpointHash, deliveredEvents)
|
|
1292
|
+
: markRadarWebhookEventsDeliveredForRoute(queuedState, target.endpointHash, deliveredEvents);
|
|
1293
|
+
await saveRadarState(stateFile, nextState);
|
|
1294
|
+
}
|
|
1295
|
+
return { ...result, state: nextState };
|
|
466
1296
|
};
|
|
467
1297
|
const writeCheckResult = (result, compactJson = false) => {
|
|
468
1298
|
const policy = evaluateRadarPolicy(result.state, failOn, failOnCompatibility);
|
|
@@ -478,7 +1308,7 @@ async function runRadar(args) {
|
|
|
478
1308
|
return policy;
|
|
479
1309
|
};
|
|
480
1310
|
if (subcommand === 'check') {
|
|
481
|
-
if (positional.length > 0 || notesPath !== undefined || once || intervalProvided) {
|
|
1311
|
+
if (positional.length > 0 || notesPath !== undefined || once || intervalProvided || historyLimitProvided) {
|
|
482
1312
|
throw new Error('radar check received an option meant for watch or compare');
|
|
483
1313
|
}
|
|
484
1314
|
const result = await runCheck();
|
|
@@ -488,7 +1318,7 @@ async function runRadar(args) {
|
|
|
488
1318
|
return policy.status === 'fail' ? 2 : 0;
|
|
489
1319
|
}
|
|
490
1320
|
if (subcommand === 'watch') {
|
|
491
|
-
if (positional.length > 0 || notesPath !== undefined)
|
|
1321
|
+
if (positional.length > 0 || notesPath !== undefined || historyLimitProvided)
|
|
492
1322
|
throw new Error('radar watch received unexpected arguments');
|
|
493
1323
|
if ((failOn !== 'never' || failOnCompatibility !== 'never') && !once)
|
|
494
1324
|
throw new Error('radar watch requires --once when a policy gate is used');
|
|
@@ -537,7 +1367,7 @@ async function runRadar(args) {
|
|
|
537
1367
|
}
|
|
538
1368
|
return 0;
|
|
539
1369
|
}
|
|
540
|
-
if (statePath !== undefined || osvBaseUrl !== undefined || registry !== undefined || once || intervalProvided || !deepCandidates || frozen || failOn !== 'never' || failOnCompatibility !== 'never') {
|
|
1370
|
+
if (statePath !== undefined || osvBaseUrl !== undefined || registry !== undefined || webhookUrl !== undefined || once || intervalProvided || historyLimitProvided || !deepCandidates || !githubAdvisories || threatIntel || frozen || failOn !== 'never' || failOnCompatibility !== 'never') {
|
|
541
1371
|
throw new Error('radar compare does not accept check or watch options');
|
|
542
1372
|
}
|
|
543
1373
|
const config = await readConfig();
|
|
@@ -562,15 +1392,116 @@ async function runRadar(args) {
|
|
|
562
1392
|
: `${renderRadarEvents(events)}Prepared ${analysisTasks.length} DSH compatibility analysis task(s).\n`);
|
|
563
1393
|
return 0;
|
|
564
1394
|
}
|
|
1395
|
+
async function runObserve(args) {
|
|
1396
|
+
const targetsPath = args[0];
|
|
1397
|
+
if (targetsPath === undefined || targetsPath.startsWith('-'))
|
|
1398
|
+
throw new Error('observe requires a targets.yml file');
|
|
1399
|
+
let statePath = 'observations.json';
|
|
1400
|
+
let reportPath;
|
|
1401
|
+
let agentCommand;
|
|
1402
|
+
let agentArgs = [];
|
|
1403
|
+
let llmEnvFile;
|
|
1404
|
+
let registry;
|
|
1405
|
+
let retryPending = false;
|
|
1406
|
+
let json = false;
|
|
1407
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
1408
|
+
const argument = args[index];
|
|
1409
|
+
if (argument === '--json') {
|
|
1410
|
+
json = true;
|
|
1411
|
+
}
|
|
1412
|
+
else if (argument === '--retry-pending') {
|
|
1413
|
+
retryPending = true;
|
|
1414
|
+
}
|
|
1415
|
+
else if (argument === '--state' || argument === '--report' || argument === '--dsh-agent-command' || argument === '--dsh-agent-arg' || argument === '--llm-env-file' || argument === '--registry') {
|
|
1416
|
+
const value = args[index + 1];
|
|
1417
|
+
if (value === undefined || (value.startsWith('-') && argument !== '--dsh-agent-arg'))
|
|
1418
|
+
throw new Error(`${argument} requires a value`);
|
|
1419
|
+
if (argument === '--state')
|
|
1420
|
+
statePath = value;
|
|
1421
|
+
else if (argument === '--report')
|
|
1422
|
+
reportPath = value;
|
|
1423
|
+
else if (argument === '--dsh-agent-command')
|
|
1424
|
+
agentCommand = value;
|
|
1425
|
+
else if (argument === '--dsh-agent-arg')
|
|
1426
|
+
agentArgs.push(value);
|
|
1427
|
+
else if (argument === '--llm-env-file')
|
|
1428
|
+
llmEnvFile = value;
|
|
1429
|
+
else
|
|
1430
|
+
registry = value;
|
|
1431
|
+
index += 1;
|
|
1432
|
+
}
|
|
1433
|
+
else {
|
|
1434
|
+
throw new Error(`unknown option for observe: ${argument}`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const targetText = await readBoundedFile(targetsPath, 256 * 1024);
|
|
1438
|
+
const config = parseObserverConfigText(targetText);
|
|
1439
|
+
const previousState = await loadObservationState(statePath);
|
|
1440
|
+
const source = new UpstreamObserverClient({
|
|
1441
|
+
...(process.env.GITHUB_TOKEN === undefined ? {} : { githubToken: process.env.GITHUB_TOKEN }),
|
|
1442
|
+
...(registry === undefined ? {} : { registry }),
|
|
1443
|
+
});
|
|
1444
|
+
let agentOptions;
|
|
1445
|
+
if (agentCommand !== undefined) {
|
|
1446
|
+
agentOptions = {
|
|
1447
|
+
command: agentCommand,
|
|
1448
|
+
...(agentArgs.length === 0 ? {} : { args: agentArgs }),
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
if (agentCommand !== undefined && llmEnvFile !== undefined) {
|
|
1452
|
+
throw new Error('observe accepts either --dsh-agent-command or --llm-env-file, not both');
|
|
1453
|
+
}
|
|
1454
|
+
const result = await runObserver(config, previousState, {
|
|
1455
|
+
source,
|
|
1456
|
+
retryPending,
|
|
1457
|
+
...(agentOptions === undefined ? {} : {
|
|
1458
|
+
agent: (task, prompt) => runDshAgentCommand(task, prompt, agentOptions),
|
|
1459
|
+
}),
|
|
1460
|
+
...(llmEnvFile === undefined ? {} : {
|
|
1461
|
+
agent: (task, prompt) => runOpenAiCompatibleAgent(task, prompt, { envFile: resolve(llmEnvFile) }),
|
|
1462
|
+
}),
|
|
1463
|
+
});
|
|
1464
|
+
await saveObservationState(statePath, result.state);
|
|
1465
|
+
const reportJson = `${JSON.stringify(result.report, null, 2)}\n`;
|
|
1466
|
+
if (reportPath !== undefined) {
|
|
1467
|
+
await writeFile(resolve(reportPath), reportPath.endsWith('.json') ? reportJson : renderObserverReport(result.report));
|
|
1468
|
+
}
|
|
1469
|
+
process.stdout.write(json ? reportJson : renderObserverReport(result.report));
|
|
1470
|
+
if (result.report.errors.length > 0 || result.report.agent.failed > 0)
|
|
1471
|
+
return 1;
|
|
1472
|
+
return 0;
|
|
1473
|
+
}
|
|
1474
|
+
async function runQuickstart(args) {
|
|
1475
|
+
let directory = process.cwd();
|
|
1476
|
+
let json = false;
|
|
1477
|
+
let positional = false;
|
|
1478
|
+
for (const argument of args) {
|
|
1479
|
+
if (argument === '--json') {
|
|
1480
|
+
json = true;
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
if (argument.startsWith('-'))
|
|
1484
|
+
throw new Error(`unknown option for quickstart: ${argument}`);
|
|
1485
|
+
if (positional)
|
|
1486
|
+
throw new Error('quickstart accepts at most one directory');
|
|
1487
|
+
directory = argument;
|
|
1488
|
+
positional = true;
|
|
1489
|
+
}
|
|
1490
|
+
const report = await createQuickstartReport(directory);
|
|
1491
|
+
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderQuickstartReport(report));
|
|
1492
|
+
return 0;
|
|
1493
|
+
}
|
|
565
1494
|
async function runSetup(args) {
|
|
566
1495
|
let profile;
|
|
567
1496
|
let output = 'upstream-radar.config.json';
|
|
568
|
-
let patchFile;
|
|
1497
|
+
let patchFile = 'upstream-radar.dsh.yml';
|
|
569
1498
|
let noInstall = false;
|
|
1499
|
+
let startDsh = false;
|
|
570
1500
|
const initArgs = [];
|
|
571
1501
|
const valueOptions = new Set([
|
|
572
1502
|
'--profile', '--output', '--dsh-patch', '--project-id', '--project-name',
|
|
573
|
-
'--repository', '--workspace', '--channel', '--
|
|
1503
|
+
'--repository', '--workspace', '--channel', '--webhook-url-env', '--webhook-secret-env',
|
|
1504
|
+
'--registry', '--minimum-severity', '--quiet-hours',
|
|
574
1505
|
]);
|
|
575
1506
|
for (let index = 0; index < args.length; index += 1) {
|
|
576
1507
|
const argument = args[index];
|
|
@@ -578,6 +1509,14 @@ async function runSetup(args) {
|
|
|
578
1509
|
noInstall = true;
|
|
579
1510
|
continue;
|
|
580
1511
|
}
|
|
1512
|
+
if (argument === '--no-dsh-patch') {
|
|
1513
|
+
patchFile = undefined;
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
if (argument === '--start') {
|
|
1517
|
+
startDsh = true;
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
581
1520
|
if (argument === '--json')
|
|
582
1521
|
throw new Error('setup does not accept --json; use init --json');
|
|
583
1522
|
if (argument === '--force') {
|
|
@@ -592,9 +1531,10 @@ async function runSetup(args) {
|
|
|
592
1531
|
profile = value;
|
|
593
1532
|
else if (argument === '--output')
|
|
594
1533
|
output = value;
|
|
595
|
-
|
|
1534
|
+
if (argument === '--dsh-patch')
|
|
596
1535
|
patchFile = value;
|
|
597
|
-
|
|
1536
|
+
else
|
|
1537
|
+
initArgs.push(argument ?? '', value);
|
|
598
1538
|
index += 1;
|
|
599
1539
|
continue;
|
|
600
1540
|
}
|
|
@@ -604,7 +1544,7 @@ async function runSetup(args) {
|
|
|
604
1544
|
if (!noInstall && resolvedProfile === undefined) {
|
|
605
1545
|
const candidates = await discoverDshProfiles();
|
|
606
1546
|
if (candidates.length === 0) {
|
|
607
|
-
throw new Error('setup could not find a DSH profile with third-party bundles;
|
|
1547
|
+
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
1548
|
}
|
|
609
1549
|
if (candidates.length > 1) {
|
|
610
1550
|
throw new Error(`setup found multiple DSH profiles with third-party bundles (${candidates.join(', ')}); pass --profile <name>`);
|
|
@@ -614,6 +1554,12 @@ async function runSetup(args) {
|
|
|
614
1554
|
if (resolvedProfile !== undefined && profile === undefined) {
|
|
615
1555
|
initArgs.unshift('--profile', resolvedProfile);
|
|
616
1556
|
}
|
|
1557
|
+
// Setup is the beginner-facing path: make the generated DSH overlay the
|
|
1558
|
+
// default so a first run does not fall back to hidden environment variables.
|
|
1559
|
+
// `init` remains available when a user deliberately wants the legacy
|
|
1560
|
+
// environment-variable wiring instead.
|
|
1561
|
+
if (patchFile !== undefined)
|
|
1562
|
+
initArgs.push('--dsh-patch', patchFile);
|
|
617
1563
|
if (!noInstall) {
|
|
618
1564
|
if (resolvedProfile === undefined)
|
|
619
1565
|
throw new Error('setup could not select a DSH profile');
|
|
@@ -627,6 +1573,10 @@ async function runSetup(args) {
|
|
|
627
1573
|
stdio: 'inherit',
|
|
628
1574
|
});
|
|
629
1575
|
if (result.error !== undefined) {
|
|
1576
|
+
const spawnError = result.error;
|
|
1577
|
+
if (spawnError.code === 'ENOENT') {
|
|
1578
|
+
throw new Error('setup could not find the `dsh` command; install DeepSeek Harness, verify `dsh --help` works, then rerun setup');
|
|
1579
|
+
}
|
|
630
1580
|
throw new Error(`setup could not run ${dshCommand}: ${safeErrorMessage(result.error.message)}`);
|
|
631
1581
|
}
|
|
632
1582
|
if (result.status !== 0) {
|
|
@@ -650,17 +1600,47 @@ async function runSetup(args) {
|
|
|
650
1600
|
doctorOptions.patchFile = patchFile;
|
|
651
1601
|
const doctor = await createDoctorReport(doctorOptions);
|
|
652
1602
|
process.stdout.write(`\nLocal wiring check:\n${renderDoctorReport(doctor)}`);
|
|
653
|
-
|
|
1603
|
+
if (doctor.status === 'blocked')
|
|
1604
|
+
return 1;
|
|
1605
|
+
if (!startDsh)
|
|
1606
|
+
return 0;
|
|
1607
|
+
if (doctorProfile === undefined) {
|
|
1608
|
+
throw new Error('setup --start could not determine the DSH profile; pass --profile <name>');
|
|
1609
|
+
}
|
|
1610
|
+
const dshCommand = process.platform === 'win32' ? 'dsh.cmd' : 'dsh';
|
|
1611
|
+
const dshArgs = ['--profile', doctorProfile];
|
|
1612
|
+
const dshEnvironment = patchFile === undefined
|
|
1613
|
+
? { ...process.env, UPSTREAM_RADAR_CONFIG: outputPath, UPSTREAM_RADAR_STATE: statePath }
|
|
1614
|
+
: process.env;
|
|
1615
|
+
if (patchFile !== undefined)
|
|
1616
|
+
dshArgs.push('--patch', patchFile);
|
|
1617
|
+
process.stdout.write(`\nStarting DSH profile ${doctorProfile}; press Ctrl-C to stop.\n`);
|
|
1618
|
+
const result = spawnSync(dshCommand, dshArgs, { env: dshEnvironment, stdio: 'inherit' });
|
|
1619
|
+
if (result.error !== undefined) {
|
|
1620
|
+
const spawnError = result.error;
|
|
1621
|
+
if (spawnError.code === 'ENOENT') {
|
|
1622
|
+
throw new Error('setup --start could not find the `dsh` command; install DeepSeek Harness, verify `dsh --help` works, then rerun setup');
|
|
1623
|
+
}
|
|
1624
|
+
throw new Error(`setup --start could not run ${dshCommand}: ${safeErrorMessage(result.error.message)}`);
|
|
1625
|
+
}
|
|
1626
|
+
return result.status ?? 1;
|
|
654
1627
|
}
|
|
655
1628
|
async function runInit(args) {
|
|
656
1629
|
let profile;
|
|
1630
|
+
let npmLockPath;
|
|
1631
|
+
let pnpmLockPath;
|
|
1632
|
+
let rootSpec;
|
|
657
1633
|
let output = 'upstream-radar.config.json';
|
|
658
1634
|
let dshPatch;
|
|
659
1635
|
let projectId;
|
|
660
1636
|
let projectName;
|
|
661
1637
|
let repository;
|
|
662
1638
|
let workspace;
|
|
1639
|
+
let webhookUrlEnv;
|
|
1640
|
+
let webhookSecretEnv;
|
|
663
1641
|
let registry;
|
|
1642
|
+
let minimumSeverity;
|
|
1643
|
+
let quietHoursValue;
|
|
664
1644
|
let force = false;
|
|
665
1645
|
let json = false;
|
|
666
1646
|
const channels = [];
|
|
@@ -670,14 +1650,21 @@ async function runInit(args) {
|
|
|
670
1650
|
force = true;
|
|
671
1651
|
else if (argument === '--json')
|
|
672
1652
|
json = true;
|
|
673
|
-
else if (argument === '--profile' || argument === '--output' || argument === '--dsh-patch' || argument === '--project-id'
|
|
1653
|
+
else if (argument === '--profile' || argument === '--npm-lock' || argument === '--pnpm-lock' || argument === '--root' || argument === '--output' || argument === '--dsh-patch' || argument === '--project-id'
|
|
674
1654
|
|| argument === '--project-name' || argument === '--repository' || argument === '--workspace'
|
|
675
|
-
|| argument === '--channel' || argument === '--
|
|
1655
|
+
|| argument === '--channel' || argument === '--webhook-url-env' || argument === '--webhook-secret-env'
|
|
1656
|
+
|| argument === '--registry' || argument === '--minimum-severity' || argument === '--quiet-hours') {
|
|
676
1657
|
const value = args[index + 1];
|
|
677
1658
|
if (value === undefined || value.startsWith('-'))
|
|
678
1659
|
throw new Error(`${argument} requires a value`);
|
|
679
1660
|
if (argument === '--profile')
|
|
680
1661
|
profile = value;
|
|
1662
|
+
else if (argument === '--npm-lock')
|
|
1663
|
+
npmLockPath = value;
|
|
1664
|
+
else if (argument === '--pnpm-lock')
|
|
1665
|
+
pnpmLockPath = value;
|
|
1666
|
+
else if (argument === '--root')
|
|
1667
|
+
rootSpec = value;
|
|
681
1668
|
else if (argument === '--output')
|
|
682
1669
|
output = value;
|
|
683
1670
|
else if (argument === '--dsh-patch')
|
|
@@ -692,6 +1679,14 @@ async function runInit(args) {
|
|
|
692
1679
|
workspace = value;
|
|
693
1680
|
else if (argument === '--channel')
|
|
694
1681
|
channels.push(value);
|
|
1682
|
+
else if (argument === '--webhook-url-env')
|
|
1683
|
+
webhookUrlEnv = value;
|
|
1684
|
+
else if (argument === '--webhook-secret-env')
|
|
1685
|
+
webhookSecretEnv = value;
|
|
1686
|
+
else if (argument === '--minimum-severity')
|
|
1687
|
+
minimumSeverity = value;
|
|
1688
|
+
else if (argument === '--quiet-hours')
|
|
1689
|
+
quietHoursValue = value;
|
|
695
1690
|
else
|
|
696
1691
|
registry = value;
|
|
697
1692
|
index += 1;
|
|
@@ -700,6 +1695,68 @@ async function runInit(args) {
|
|
|
700
1695
|
throw new Error(`unknown option for init: ${argument}`);
|
|
701
1696
|
}
|
|
702
1697
|
}
|
|
1698
|
+
const notificationPolicy = parseNotificationPolicyArguments(minimumSeverity, quietHoursValue);
|
|
1699
|
+
if (npmLockPath !== undefined || pnpmLockPath !== undefined) {
|
|
1700
|
+
if (npmLockPath !== undefined && pnpmLockPath !== undefined) {
|
|
1701
|
+
throw new Error('init accepts only one of --npm-lock or --pnpm-lock');
|
|
1702
|
+
}
|
|
1703
|
+
const lockKind = npmLockPath === undefined ? 'pnpm-lock' : 'npm-lock';
|
|
1704
|
+
const lockfile = npmLockPath ?? pnpmLockPath;
|
|
1705
|
+
if (lockfile === undefined)
|
|
1706
|
+
throw new Error('init lockfile path is missing');
|
|
1707
|
+
if (profile !== undefined || dshPatch !== undefined || registry !== undefined) {
|
|
1708
|
+
throw new Error(`init --${lockKind} does not accept --profile, --dsh-patch or --registry`);
|
|
1709
|
+
}
|
|
1710
|
+
const root = rootSpec === undefined
|
|
1711
|
+
? await inferLockfileRoot(lockfile, lockKind === 'npm-lock' ? 'npm' : 'pnpm')
|
|
1712
|
+
: parseExactPackageCoordinate(rootSpec);
|
|
1713
|
+
const config = lockKind === 'npm-lock'
|
|
1714
|
+
? await createRadarConfigFromNpmLock({
|
|
1715
|
+
lockfile,
|
|
1716
|
+
root,
|
|
1717
|
+
...(projectId === undefined ? {} : { projectId }),
|
|
1718
|
+
...(projectName === undefined ? {} : { projectName }),
|
|
1719
|
+
...(repository === undefined ? {} : { repository }),
|
|
1720
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
1721
|
+
...(channels.length === 0 ? {} : { channels }),
|
|
1722
|
+
...(webhookUrlEnv === undefined ? {} : { webhookUrlEnv }),
|
|
1723
|
+
...(webhookSecretEnv === undefined ? {} : { webhookSecretEnv }),
|
|
1724
|
+
...(notificationPolicy === undefined ? {} : { notificationPolicy }),
|
|
1725
|
+
})
|
|
1726
|
+
: await createRadarConfigFromPnpmLock({
|
|
1727
|
+
lockfile,
|
|
1728
|
+
root,
|
|
1729
|
+
...(projectId === undefined ? {} : { projectId }),
|
|
1730
|
+
...(projectName === undefined ? {} : { projectName }),
|
|
1731
|
+
...(repository === undefined ? {} : { repository }),
|
|
1732
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
1733
|
+
...(channels.length === 0 ? {} : { channels }),
|
|
1734
|
+
...(webhookUrlEnv === undefined ? {} : { webhookUrlEnv }),
|
|
1735
|
+
...(webhookSecretEnv === undefined ? {} : { webhookSecretEnv }),
|
|
1736
|
+
...(notificationPolicy === undefined ? {} : { notificationPolicy }),
|
|
1737
|
+
});
|
|
1738
|
+
const outputPath = await writeRadarConfig(config, { output: resolve(output), force });
|
|
1739
|
+
const plugin = config.projects[0]?.plugins[0];
|
|
1740
|
+
if (plugin === undefined)
|
|
1741
|
+
throw new Error(`${lockKind} initialization produced no plugin`);
|
|
1742
|
+
if (json) {
|
|
1743
|
+
process.stdout.write(`${JSON.stringify({
|
|
1744
|
+
output: outputPath,
|
|
1745
|
+
source: plugin.graph.source,
|
|
1746
|
+
root: plugin.package,
|
|
1747
|
+
nodes: plugin.graph.nodes.length,
|
|
1748
|
+
edges: plugin.graph.edges.length,
|
|
1749
|
+
...(plugin.graph.unresolved === undefined ? {} : { unresolved: plugin.graph.unresolved.length }),
|
|
1750
|
+
}, null, 2)}\n`);
|
|
1751
|
+
}
|
|
1752
|
+
else {
|
|
1753
|
+
const unresolved = plugin.graph.unresolved?.length ?? 0;
|
|
1754
|
+
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`);
|
|
1755
|
+
}
|
|
1756
|
+
return 0;
|
|
1757
|
+
}
|
|
1758
|
+
if (rootSpec !== undefined)
|
|
1759
|
+
throw new Error('init --root is only valid with --npm-lock or --pnpm-lock');
|
|
703
1760
|
let autoSelected = false;
|
|
704
1761
|
let resolvedProfile = profile;
|
|
705
1762
|
if (resolvedProfile === undefined) {
|
|
@@ -742,6 +1799,9 @@ async function runInit(args) {
|
|
|
742
1799
|
...(repository === undefined ? {} : { repository }),
|
|
743
1800
|
...(workspace === undefined ? {} : { workspace }),
|
|
744
1801
|
...(channels.length === 0 ? {} : { channels }),
|
|
1802
|
+
...(webhookUrlEnv === undefined ? {} : { webhookUrlEnv }),
|
|
1803
|
+
...(webhookSecretEnv === undefined ? {} : { webhookSecretEnv }),
|
|
1804
|
+
...(notificationPolicy === undefined ? {} : { notificationPolicy }),
|
|
745
1805
|
...(registry === undefined ? {} : { registry }),
|
|
746
1806
|
});
|
|
747
1807
|
config.dshProfile = { name: resolvedProfile };
|
|
@@ -756,7 +1816,13 @@ async function runInit(args) {
|
|
|
756
1816
|
force,
|
|
757
1817
|
});
|
|
758
1818
|
const plugins = config.projects[0]?.plugins ?? [];
|
|
759
|
-
|
|
1819
|
+
// Keep the printed recovery command independent of the package manager that
|
|
1820
|
+
// happened to launch this copy. Node ships with npm/npx, while a user may
|
|
1821
|
+
// have entered setup through pnpm, yarn, or a global binary. The command is
|
|
1822
|
+
// deliberately exact-versioned so the doctor checks the same release that
|
|
1823
|
+
// generated the config.
|
|
1824
|
+
const doctorCommand = `npx --yes upstream-radar@${TOOL_VERSION} doctor ${shellQuote(outputPath)} --profile ${shellQuote(resolvedProfile)}${patchPath === undefined ? '' : ` --patch ${shellQuote(patchPath)}`}`;
|
|
1825
|
+
const statusCommand = `npx --yes upstream-radar@${TOOL_VERSION} radar status ${shellQuote(outputPath)} --state ${shellQuote(statePath)}`;
|
|
760
1826
|
const startCommand = patchPath === undefined
|
|
761
1827
|
? `dsh --profile ${shellQuote(resolvedProfile)}`
|
|
762
1828
|
: `dsh --profile ${shellQuote(resolvedProfile)} --patch ${shellQuote(patchPath)}`;
|
|
@@ -768,6 +1834,7 @@ async function runInit(args) {
|
|
|
768
1834
|
edges: plugin.graph.edges.length,
|
|
769
1835
|
...(plugin.graph.source === undefined ? {} : { source: plugin.graph.source }),
|
|
770
1836
|
...(plugin.graph.hostRuntime === undefined ? {} : { hostRuntimeNodes: plugin.graph.hostRuntime.resolvedNodes }),
|
|
1837
|
+
...(plugin.graph.hostRuntime?.package === undefined ? {} : { hostRuntimePackage: plugin.graph.hostRuntime.package }),
|
|
771
1838
|
...(plugin.graph.unresolved === undefined ? {} : { unresolved: plugin.graph.unresolved.length }),
|
|
772
1839
|
...(plugin.graph.unresolved === undefined ? {} : {
|
|
773
1840
|
requiredUnresolved: plugin.graph.unresolved.filter(item => item.kind !== 'optional').length,
|
|
@@ -784,13 +1851,14 @@ async function runInit(args) {
|
|
|
784
1851
|
const requiredUnresolved = plugin.graph.unresolved?.filter(item => item.kind !== 'optional').length ?? 0;
|
|
785
1852
|
const optionalUnresolved = unresolved - requiredUnresolved;
|
|
786
1853
|
const hostRuntime = plugin.graph.hostRuntime?.resolvedNodes ?? 0;
|
|
787
|
-
|
|
1854
|
+
const hostRuntimePackage = plugin.graph.hostRuntime?.package;
|
|
1855
|
+
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
1856
|
}
|
|
789
1857
|
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`);
|
|
1858
|
+
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
1859
|
}
|
|
792
1860
|
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`);
|
|
1861
|
+
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
1862
|
}
|
|
795
1863
|
}
|
|
796
1864
|
return 0;
|
|
@@ -842,6 +1910,11 @@ async function runDoctor(args) {
|
|
|
842
1910
|
return report.status === 'blocked' ? 1 : 0;
|
|
843
1911
|
}
|
|
844
1912
|
async function main(args) {
|
|
1913
|
+
const help = commandHelp(args);
|
|
1914
|
+
if (help !== undefined) {
|
|
1915
|
+
process.stdout.write(help);
|
|
1916
|
+
return 0;
|
|
1917
|
+
}
|
|
845
1918
|
const command = args[0];
|
|
846
1919
|
if (command === undefined || command === '--help' || command === '-h' || command === 'help') {
|
|
847
1920
|
process.stdout.write(usage());
|
|
@@ -851,14 +1924,24 @@ async function main(args) {
|
|
|
851
1924
|
process.stdout.write(`${TOOL_VERSION}\n`);
|
|
852
1925
|
return 0;
|
|
853
1926
|
}
|
|
1927
|
+
if (command === 'quickstart')
|
|
1928
|
+
return runQuickstart(args.slice(1));
|
|
854
1929
|
if (command === 'setup')
|
|
855
1930
|
return runSetup(args.slice(1));
|
|
856
1931
|
if (command === 'init')
|
|
857
1932
|
return runInit(args.slice(1));
|
|
858
1933
|
if (command === 'doctor')
|
|
859
1934
|
return runDoctor(args.slice(1));
|
|
1935
|
+
if (command === 'graph')
|
|
1936
|
+
return runGraph(args.slice(1));
|
|
1937
|
+
if (command === 'profile-check')
|
|
1938
|
+
return runDshProfileCheck(args.slice(1));
|
|
860
1939
|
if (command === 'probe')
|
|
861
1940
|
return runProbe(args.slice(1));
|
|
1941
|
+
if (command === 'demo')
|
|
1942
|
+
return runDemo(args.slice(1));
|
|
1943
|
+
if (command === 'observe')
|
|
1944
|
+
return runObserve(args.slice(1));
|
|
862
1945
|
if (command === 'benchmark')
|
|
863
1946
|
return runBenchmark(args.slice(1));
|
|
864
1947
|
if (command === 'radar')
|
|
@@ -867,6 +1950,12 @@ async function main(args) {
|
|
|
867
1950
|
return runTask(args.slice(1));
|
|
868
1951
|
if (command === 'analysis')
|
|
869
1952
|
return runAnalysis(args.slice(1));
|
|
1953
|
+
if (command === 'mute')
|
|
1954
|
+
return runIncidentMute(args.slice(1));
|
|
1955
|
+
if (command === 'unmute')
|
|
1956
|
+
return runIncidentUnmute(args.slice(1));
|
|
1957
|
+
if (command === 'triage')
|
|
1958
|
+
return runIncidentTriage(args.slice(1));
|
|
870
1959
|
if (command !== 'scan' && command !== 'inspect')
|
|
871
1960
|
throw new Error(`unknown command: ${command}`);
|
|
872
1961
|
const target = args[1];
|