upstream-radar 0.33.12 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -1020
- package/dist/src/cli.js +365 -19
- package/dist/src/cli.js.map +1 -1
- package/dist/src/dependency-index.d.ts +76 -0
- package/dist/src/dependency-index.d.ts.map +1 -0
- package/dist/src/dependency-index.js +199 -0
- package/dist/src/dependency-index.js.map +1 -0
- package/dist/src/dsh-case.d.ts +50 -0
- package/dist/src/dsh-case.d.ts.map +1 -0
- package/dist/src/dsh-case.js +88 -0
- package/dist/src/dsh-case.js.map +1 -0
- package/dist/src/dsh-review.d.ts +46 -0
- package/dist/src/dsh-review.d.ts.map +1 -0
- package/dist/src/dsh-review.js +209 -0
- package/dist/src/dsh-review.js.map +1 -0
- package/dist/src/graph.d.ts +5 -1
- package/dist/src/graph.d.ts.map +1 -1
- package/dist/src/graph.js +56 -10
- package/dist/src/graph.js.map +1 -1
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/npm.d.ts +11 -0
- package/dist/src/npm.d.ts.map +1 -1
- package/dist/src/npm.js +122 -12
- package/dist/src/npm.js.map +1 -1
- package/dist/src/render.d.ts.map +1 -1
- package/dist/src/render.js +19 -0
- package/dist/src/render.js.map +1 -1
- package/dist/src/scan.d.ts.map +1 -1
- package/dist/src/scan.js +41 -0
- package/dist/src/scan.js.map +1 -1
- package/dist/src/types.d.ts +10 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/upstream-alignment.d.ts +67 -0
- package/dist/src/upstream-alignment.d.ts.map +1 -0
- package/dist/src/upstream-alignment.js +263 -0
- package/dist/src/upstream-alignment.js.map +1 -0
- package/dist/src/upstream-observer.d.ts +70 -0
- package/dist/src/upstream-observer.d.ts.map +1 -1
- package/dist/src/upstream-observer.js +387 -22
- package/dist/src/upstream-observer.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/docs/README.zh-CN.md +114 -22
- package/examples/cases/dsh-web-ui-issue-71/before/cordis.patch.yml +4 -0
- package/examples/cases/dsh-web-ui-issue-71/before/node_modules/@linxin666/dsh-client-ui-skin-center/cordis.patch.yml +1 -0
- package/examples/cases/dsh-web-ui-issue-71/before/node_modules/@linxin666/dsh-client-ui-skin-center/package.json +5 -0
- package/examples/cases/dsh-web-ui-issue-71/before/node_modules/@linxin666/dsh-web-ui-all/cordis.patch.yml +1 -0
- package/examples/cases/dsh-web-ui-issue-71/before/node_modules/@linxin666/dsh-web-ui-all/package.json +5 -0
- package/examples/cases/dsh-web-ui-issue-71/before/package.json +12 -0
- package/examples/cases/dsh-web-ui-issue-71/before/pnpm-lock.yaml +21 -0
- package/examples/cases/dsh-web-ui-issue-71/before/pnpm-workspace.yaml +7 -0
- package/examples/cases/dsh-web-ui-issue-71/fixed/cordis.patch.yml +3 -0
- package/examples/cases/dsh-web-ui-issue-71/fixed/node_modules/@linxin666/dsh-client-ui-skin-center/cordis.patch.yml +1 -0
- package/examples/cases/dsh-web-ui-issue-71/fixed/node_modules/@linxin666/dsh-client-ui-skin-center/package.json +5 -0
- package/examples/cases/dsh-web-ui-issue-71/fixed/node_modules/@linxin666/dsh-web-ui-all/cordis.patch.yml +1 -0
- package/examples/cases/dsh-web-ui-issue-71/fixed/node_modules/@linxin666/dsh-web-ui-all/package.json +5 -0
- package/examples/cases/dsh-web-ui-issue-71/fixed/package.json +12 -0
- package/examples/cases/dsh-web-ui-issue-71/fixed/pnpm-lock.yaml +18 -0
- package/examples/cases/dsh-web-ui-issue-71/fixed/pnpm-workspace.yaml +8 -0
- package/examples/cases/dsh-web-ui-issue-71/manual-add/cordis.patch.yml +4 -0
- package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-client-ui-skin-center/cordis.patch.yml +1 -0
- package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-client-ui-skin-center/package.json +5 -0
- package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-client-ui-skin-qq98/cordis.patch.yml +3 -0
- package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-client-ui-skin-qq98/package.json +5 -0
- package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-web-ui-all/cordis.patch.yml +1 -0
- package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-web-ui-all/package.json +5 -0
- package/examples/cases/dsh-web-ui-issue-71/manual-add/package.json +13 -0
- package/examples/cases/dsh-web-ui-issue-71/manual-add/pnpm-lock.yaml +21 -0
- package/examples/cases/dsh-web-ui-issue-71/manual-add/pnpm-workspace.yaml +5 -0
- package/package.json +2 -1
- package/schemas/upstream-downstream-ir.schema.json +87 -0
package/dist/src/cli.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
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, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { access, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
5
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
|
+
import { renderDshPluginReview, reviewDshPlugin } from './dsh-review.js';
|
|
9
10
|
import { createAnalysisTask, renderAgentAnalysisPrompt } from './dsh-analysis.js';
|
|
11
|
+
import { createDshCaseReport, renderDshCase } from './dsh-case.js';
|
|
10
12
|
import { createDoctorReport, renderDoctorReport } from './doctor.js';
|
|
11
13
|
import { checkDshProfile, renderDshProfileCheck, renderDshProfileCheckSummary } from './dsh-profile-check.js';
|
|
12
14
|
import { createDemoReport, renderDemo } from './demo.js';
|
|
13
15
|
import { GitHubReleaseClient } from './github-release.js';
|
|
14
16
|
import { parseNpmLockGraph, parsePnpmLockGraph } from './graph.js';
|
|
17
|
+
import { buildReverseDependencyIndex, findReverseDependencyEntry, parseReverseDependencyObservations } from './dependency-index.js';
|
|
15
18
|
import { createRadarConfigFromDshProfile, createRadarConfigFromNpmLock, createRadarConfigFromPnpmLock, discoverDshProfiles, refreshRadarConfigFromConfiguredProfile, resolveDshProfileDirectory, writeDshPatch, writeRadarConfig } from './init.js';
|
|
16
19
|
import { parsePackageManifestSnapshot, parseRadarConfig } from './inventory.js';
|
|
17
20
|
import { inspectNpmPackage } from './npm.js';
|
|
@@ -32,7 +35,7 @@ import { renderTextReport } from './render.js';
|
|
|
32
35
|
import { materializeGitHubRepository, parseGitHubRepositoryUrl } from './repository.js';
|
|
33
36
|
import { scanDirectory } from './scan.js';
|
|
34
37
|
import { CisaKevClient, EpssClient } from './threat-intel.js';
|
|
35
|
-
import { loadObservationState, OBSERVER_TARGETS_SCHEMA, parseObserverConfig, parseObserverConfigText, renderObserverReport, runDshAgentCommand, runOpenAiCompatibleAgent, runObserver, saveObservationState, UpstreamObserverClient, } from './upstream-observer.js';
|
|
38
|
+
import { loadObservationState, OBSERVER_TARGETS_SCHEMA, parseObserverConfig, parseObserverConfigText, observerExitCode, renderObserverReport, runDshAgentCommand, runOpenAiCompatibleAgent, runObserver, saveObservationState, UpstreamObserverClient, } from './upstream-observer.js';
|
|
36
39
|
import { TOOL_VERSION } from './version.js';
|
|
37
40
|
import { eventsForRadarWebhookTarget, markRadarWebhookEventsDelivered, markRadarWebhookEventsDeliveredForRoute, normalizeRadarWebhookUrl, queueRadarWebhookEvents, queueRadarWebhookEventsForRoute, resolveRadarWebhookTargets, sendRadarWebhook, undeliveredRadarWebhookEvents, undeliveredRadarWebhookEventsForRoute, } from './webhook.js';
|
|
38
41
|
const VALID_THRESHOLDS = new Set(['warn', 'review', 'block', 'never']);
|
|
@@ -198,7 +201,8 @@ should remain visible without failing CI.
|
|
|
198
201
|
|
|
199
202
|
Usage:
|
|
200
203
|
upstream-radar observe <targets.yml|github-url> [--state <observations.json>]
|
|
201
|
-
[--report <report.md>] [--dsh-
|
|
204
|
+
[--report <report.md>] [--dsh-version <v1>,<v2>,...]
|
|
205
|
+
[--dsh-agent-command <executable>]
|
|
202
206
|
[--dsh-agent-arg <argument>] [--llm-env-file <path>] [--retry-pending]
|
|
203
207
|
[--ecosystem <dsh|codex|pi>] [--id <id>] [--package <name>]
|
|
204
208
|
[--package-path <path>] [--lockfile <path>] [--lockfile-type <npm|pnpm>]
|
|
@@ -206,30 +210,48 @@ Usage:
|
|
|
206
210
|
|
|
207
211
|
The first run creates a baseline. Later runs compare source commits, published
|
|
208
212
|
npm metadata, package manifests, and an optional npm/pnpm dependency graph. A
|
|
209
|
-
|
|
213
|
+
meaningful change with a published npm version also triggers a deep review of
|
|
214
|
+
that exact artifact: Radar resolves its reachable dependency graph with npm
|
|
215
|
+
scripts disabled, checks known vulnerabilities, and records install-time
|
|
216
|
+
scripts and incomplete coverage. If exact DSH versions are supplied, the same
|
|
217
|
+
changed artifact is loaded in disposable profiles and the compatibility matrix
|
|
218
|
+
is added to the same report. A DSH Agent is called only for meaningful
|
|
219
|
+
changes. Safety: Radar does not install the observed plugin, run its lifecycle
|
|
220
|
+
scripts, load plugin code, or invoke a shell.
|
|
210
221
|
|
|
211
222
|
When the target is an HTTPS GitHub repository URL, Radar builds one target in
|
|
212
|
-
memory and does not require a targets.yml file.
|
|
213
|
-
|
|
214
|
-
the
|
|
223
|
+
memory and does not require a targets.yml file. For a DSH target with no
|
|
224
|
+
--package-path, it looks for one DSH bundle or one @deepseek-ai/dsh package up
|
|
225
|
+
to three directory levels deep; pass --package-path when the repository is
|
|
226
|
+
ambiguous. --lockfile remains available when you want to pin the graph file.
|
|
215
227
|
|
|
216
228
|
The Agent executable receives one read-only task prompt on stdin and should
|
|
217
|
-
return one JSON conclusion on stdout. If it is not configured
|
|
218
|
-
|
|
229
|
+
return one JSON conclusion on stdout. If it is not configured or temporarily
|
|
230
|
+
unavailable, the static observation still succeeds and the task stays in
|
|
231
|
+
observations.json for a later explicit retry. Source observation errors still
|
|
232
|
+
return a non-zero exit code. As a simpler alternative,
|
|
219
233
|
--llm-env-file reads an OpenAI-compatible issue-locator/.env-style file for
|
|
220
234
|
only the model call. It accepts ISSUE_LOCATOR_LLM_*, OPENAI_*, or MODEL/CODEX_MODEL
|
|
221
235
|
keys; it never writes the key or endpoint to observations.json.
|
|
222
236
|
If a ModelBest-style base URL ends in /llm/v1, a 404 also retries the known
|
|
223
237
|
/llm/openai/v1 path.
|
|
238
|
+
|
|
239
|
+
The exact npm artifact review is independent of the DSH Agent/model. It runs
|
|
240
|
+
when a meaningful change has a published package, and its dependency findings
|
|
241
|
+
remain in the report even when no model is configured or the model endpoint is
|
|
242
|
+
unavailable.
|
|
224
243
|
`,
|
|
225
244
|
graph: `Upstream Radar — read a lockfile into the canonical dependency graph
|
|
226
245
|
|
|
227
246
|
Usage:
|
|
228
247
|
upstream-radar graph pnpm-lock <pnpm-lock.yaml> [--root <package>@<exact-version>] [--json]
|
|
229
248
|
upstream-radar graph npm-lock <package-lock.json> [--root <package>@<exact-version>] [--json]
|
|
249
|
+
upstream-radar graph reverse <reports-directory> [--package <name>@<exact-version>] [--output <index.json>] [--json]
|
|
230
250
|
|
|
231
251
|
This command is offline and does not install packages, run lifecycle scripts,
|
|
232
|
-
load plugin code, or query vulnerability sources.
|
|
252
|
+
load plugin code, or query vulnerability sources. The reverse form reads saved
|
|
253
|
+
scan, exact-review, or Radar config JSON and builds a deterministic
|
|
254
|
+
dependency -> affected-plugin index.
|
|
233
255
|
`,
|
|
234
256
|
'profile-check': `Upstream Radar — check one DSH profile before starting it
|
|
235
257
|
|
|
@@ -251,6 +273,18 @@ Usage:
|
|
|
251
273
|
|
|
252
274
|
The probe is bounded and isolated. It is a compatibility/load check, not a
|
|
253
275
|
semantic safety review or a substitute for dependency monitoring.
|
|
276
|
+
`,
|
|
277
|
+
review: `Upstream Radar — review one exact published DSH plugin in one command
|
|
278
|
+
|
|
279
|
+
Usage:
|
|
280
|
+
upstream-radar review dsh-plugin [npm:]<package>@<exact-version>
|
|
281
|
+
--dsh-version <v1>,<v2>,... [--registry <https-url>] [--timeout <seconds>] [--json]
|
|
282
|
+
|
|
283
|
+
This combines exact npm artifact/dependency inspection with a bounded DSH load
|
|
284
|
+
matrix. It packs with lifecycle scripts disabled, uses a temporary profile for
|
|
285
|
+
each requested DSH version, and prints one result that an author can act on.
|
|
286
|
+
It does not execute plugin code or business actions and does not call an LLM.
|
|
287
|
+
The default command reports findings without turning them into an extra gate.
|
|
254
288
|
`,
|
|
255
289
|
demo: `Upstream Radar — show the exact-path-to-DSH handoff without side effects
|
|
256
290
|
|
|
@@ -259,6 +293,16 @@ Usage:
|
|
|
259
293
|
|
|
260
294
|
The demo is network-free and uses only a local fixture. It does not inspect your
|
|
261
295
|
repository, install a plugin, start DSH, or claim that its advisory is real.
|
|
296
|
+
`,
|
|
297
|
+
case: `Upstream Radar — replay a real DSH profile failure and its repair
|
|
298
|
+
|
|
299
|
+
Usage:
|
|
300
|
+
upstream-radar case dsh-web-ui [--json]
|
|
301
|
+
|
|
302
|
+
This network-free case checks a broken DSH web-ui profile, the tempting manual
|
|
303
|
+
package workaround, and the corrected bundled-carrier layout. It reads only
|
|
304
|
+
packaged fixtures; it does not install packages, start DSH, execute plugin code,
|
|
305
|
+
or call an Agent/model.
|
|
262
306
|
`,
|
|
263
307
|
quickstart: `Upstream Radar — choose the smallest honest first-use path
|
|
264
308
|
|
|
@@ -418,12 +462,15 @@ Usage:
|
|
|
418
462
|
upstream-radar doctor [config.json] [options]
|
|
419
463
|
upstream-radar scan <directory-or-github-url> [--json] [--fail-on <warn|review|block|never>]
|
|
420
464
|
upstream-radar inspect [npm:]<package>@<exact-version> [--deep] [--json] [--fail-on <warn|review|block|never>]
|
|
421
|
-
upstream-radar observe <targets.yml|github-url> [--state <observations.json>] [--report <report.md>] [--dsh-agent-command <executable>] [--dsh-agent-arg <argument>] [--llm-env-file <path>] [--retry-pending] [--ecosystem <dsh|codex|pi>] [--id <id>] [--package <name>] [--package-path <path>] [--lockfile <path>] [--lockfile-type <npm|pnpm>] [--ref <branch>] [--json]
|
|
465
|
+
upstream-radar observe <targets.yml|github-url> [--state <observations.json>] [--report <report.md>] [--dsh-version <v1>,<v2>,...] [--dsh-agent-command <executable>] [--dsh-agent-arg <argument>] [--llm-env-file <path>] [--retry-pending] [--ecosystem <dsh|codex|pi>] [--id <id>] [--package <name>] [--package-path <path>] [--lockfile <path>] [--lockfile-type <npm|pnpm>] [--ref <branch>] [--json]
|
|
422
466
|
upstream-radar graph <npm-lock|pnpm-lock> <lockfile> [--root <package>@<exact-version>] [--json]
|
|
467
|
+
upstream-radar graph reverse <reports-directory> [--package <name>@<exact-version>] [--output <index.json>] [--json]
|
|
423
468
|
upstream-radar profile-check [profile-directory] [--patch <path>] [--report <path>] [--summary] [--json]
|
|
424
469
|
upstream-radar probe dsh-load <package.tgz> [--dsh-version <exact-version>] [--timeout <seconds>] [--keep-profile] [--json]
|
|
425
470
|
upstream-radar probe dsh-matrix <package.tgz> --dsh-version <v1>[,<v2>,...] [--timeout <seconds>] [--keep-profile] [--json]
|
|
471
|
+
upstream-radar review dsh-plugin [npm:]<package>@<exact-version> --dsh-version <v1>,<v2>,... [--json]
|
|
426
472
|
upstream-radar demo [--json]
|
|
473
|
+
upstream-radar case dsh-web-ui [--json]
|
|
427
474
|
upstream-radar quickstart [directory] [--json]
|
|
428
475
|
upstream-radar benchmark compatibility [--json]
|
|
429
476
|
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]
|
|
@@ -449,10 +496,12 @@ Commands:
|
|
|
449
496
|
scan bounded, read-only inspection of a local directory or public GitHub repository
|
|
450
497
|
inspect fetch and verify the exact npm artifact before inspecting its contents
|
|
451
498
|
observe compare upstream DSH plugin repositories and route only meaningful changes to a DSH Agent
|
|
452
|
-
graph read
|
|
499
|
+
graph read lockfiles or build the dependency -> affected-plugin index without installing packages
|
|
453
500
|
profile-check check a DSH profile's lockfile and patch rows without starting DSH
|
|
454
501
|
probe run a bounded DSH bundle-load check or version matrix in disposable profiles
|
|
502
|
+
review combine exact npm dependency review and DSH load compatibility in one command
|
|
455
503
|
demo show the exact-path-to-DSH handoff without network, DSH, or plugin installation
|
|
504
|
+
case replay a real DSH profile failure and its repair without side effects
|
|
456
505
|
quickstart choose the smallest first-use path without changing the environment
|
|
457
506
|
benchmark run offline compatibility-rule contracts without network or plugin execution
|
|
458
507
|
radar monitor vulnerability changes, watch continuously, find the next action, inspect status/history, or assess a candidate compatibility change
|
|
@@ -524,10 +573,141 @@ async function inferLockfileRoot(lockfile, kind) {
|
|
|
524
573
|
throw error;
|
|
525
574
|
}
|
|
526
575
|
}
|
|
576
|
+
const MAX_REVERSE_INDEX_FILES = 2_000;
|
|
577
|
+
const MAX_REVERSE_INDEX_DEPTH = 8;
|
|
578
|
+
async function collectReverseIndexFiles(path, depth = 0) {
|
|
579
|
+
if (depth > MAX_REVERSE_INDEX_DEPTH)
|
|
580
|
+
throw new Error(`reverse index input exceeds the ${MAX_REVERSE_INDEX_DEPTH}-level directory limit: ${path}`);
|
|
581
|
+
const metadata = await stat(resolve(path));
|
|
582
|
+
if (metadata.isFile())
|
|
583
|
+
return path.endsWith('.json') ? [resolve(path)] : [];
|
|
584
|
+
if (!metadata.isDirectory())
|
|
585
|
+
return [];
|
|
586
|
+
const files = [];
|
|
587
|
+
const entries = await readdir(resolve(path), { withFileTypes: true });
|
|
588
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
589
|
+
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
590
|
+
continue;
|
|
591
|
+
const child = join(path, entry.name);
|
|
592
|
+
if (entry.isDirectory())
|
|
593
|
+
files.push(...await collectReverseIndexFiles(child, depth + 1));
|
|
594
|
+
else if (entry.isFile() && entry.name.endsWith('.json'))
|
|
595
|
+
files.push(resolve(child));
|
|
596
|
+
if (files.length > MAX_REVERSE_INDEX_FILES)
|
|
597
|
+
throw new Error(`reverse index input exceeds the ${MAX_REVERSE_INDEX_FILES}-file limit`);
|
|
598
|
+
}
|
|
599
|
+
return files;
|
|
600
|
+
}
|
|
601
|
+
function renderReverseDependencyIndex(index, packageFilter) {
|
|
602
|
+
const selected = packageFilter === undefined
|
|
603
|
+
? undefined
|
|
604
|
+
: findReverseDependencyEntry(index, { ecosystem: 'npm', ...packageFilter });
|
|
605
|
+
const dependencies = selected === undefined && packageFilter !== undefined
|
|
606
|
+
? []
|
|
607
|
+
: selected === undefined
|
|
608
|
+
? [...index.dependencies].sort((left, right) => (right.dependents.length - left.dependents.length
|
|
609
|
+
|| `${left.dependency.name}@${left.dependency.version}`.localeCompare(`${right.dependency.name}@${right.dependency.version}`))).slice(0, 20)
|
|
610
|
+
: [selected];
|
|
611
|
+
const lines = [
|
|
612
|
+
'Upstream Radar — reverse dependency index',
|
|
613
|
+
`Observations: ${index.observations}`,
|
|
614
|
+
`Plugins: ${index.plugins.length}`,
|
|
615
|
+
`Dependencies: ${index.dependencies.length}`,
|
|
616
|
+
`Coverage: ${index.coverage.completeObservations} complete, ${index.coverage.incompleteObservations} incomplete, ${index.coverage.unresolvedEdges} unresolved edge(s)`,
|
|
617
|
+
`Input files: ${index.inputs.files} (${index.inputs.loadedFiles} loaded, ${index.inputs.skipped.length} skipped)`,
|
|
618
|
+
];
|
|
619
|
+
if (packageFilter !== undefined)
|
|
620
|
+
lines.push(`Query: ${packageFilter.name}@${packageFilter.version}`);
|
|
621
|
+
lines.push('', dependencies.length === 0
|
|
622
|
+
? (packageFilter === undefined ? 'Dependencies:\n (none)' : 'Dependents:\n (none)')
|
|
623
|
+
: `${packageFilter === undefined ? 'Top dependencies by affected plugin count' : 'Dependents'}:`);
|
|
624
|
+
for (const dependency of dependencies) {
|
|
625
|
+
if (packageFilter === undefined)
|
|
626
|
+
lines.push(` ${dependency.dependency.name}@${dependency.dependency.version} ← ${dependency.dependents.length} plugin(s)`);
|
|
627
|
+
for (const dependent of dependency.dependents) {
|
|
628
|
+
const project = dependent.project === undefined ? '' : ` [${dependent.project.name}]`;
|
|
629
|
+
lines.push(` ${dependent.plugin.name}@${dependent.plugin.version}${project} (${dependent.coverage})`);
|
|
630
|
+
for (const path of dependent.paths.slice(0, 2))
|
|
631
|
+
lines.push(` ${path.nodes.join(' → ')}`);
|
|
632
|
+
if (dependent.paths.length > 2)
|
|
633
|
+
lines.push(` ... ${dependent.paths.length - 2} more path(s)`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (index.inputs.skipped.length > 0) {
|
|
637
|
+
lines.push('', 'Skipped input files:');
|
|
638
|
+
for (const item of index.inputs.skipped.slice(0, 20))
|
|
639
|
+
lines.push(` ${item.source}: ${item.reason}`);
|
|
640
|
+
}
|
|
641
|
+
return `${lines.join('\n')}\n`;
|
|
642
|
+
}
|
|
643
|
+
async function runReverseDependencyIndex(args) {
|
|
644
|
+
const input = args[0];
|
|
645
|
+
if (input === undefined || input.startsWith('-'))
|
|
646
|
+
throw new Error('graph reverse requires a reports directory or JSON file');
|
|
647
|
+
let json = false;
|
|
648
|
+
let output;
|
|
649
|
+
let packageFilter;
|
|
650
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
651
|
+
const argument = args[index];
|
|
652
|
+
if (argument === '--json')
|
|
653
|
+
json = true;
|
|
654
|
+
else if (argument === '--output') {
|
|
655
|
+
const value = args[index + 1];
|
|
656
|
+
if (value === undefined || value.startsWith('-'))
|
|
657
|
+
throw new Error('--output requires a path');
|
|
658
|
+
output = value;
|
|
659
|
+
index += 1;
|
|
660
|
+
}
|
|
661
|
+
else if (argument === '--package') {
|
|
662
|
+
const value = args[index + 1];
|
|
663
|
+
if (value === undefined || value.startsWith('-'))
|
|
664
|
+
throw new Error('--package requires an exact package coordinate');
|
|
665
|
+
packageFilter = parseExactPackageCoordinate(value);
|
|
666
|
+
index += 1;
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
throw new Error(`unknown option for graph reverse: ${argument}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const files = await collectReverseIndexFiles(input);
|
|
673
|
+
if (files.length === 0)
|
|
674
|
+
throw new Error(`graph reverse found no JSON reports under ${input}`);
|
|
675
|
+
const observations = [];
|
|
676
|
+
const skipped = [];
|
|
677
|
+
for (const file of files) {
|
|
678
|
+
try {
|
|
679
|
+
observations.push(...parseReverseDependencyObservations(await readJson(file), file));
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
skipped.push({ source: file, reason: safeErrorMessage(error instanceof Error ? error.message : String(error)) });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (observations.length === 0)
|
|
686
|
+
throw new Error(`graph reverse could not load a supported graph from ${files.length} JSON file(s)`);
|
|
687
|
+
const index = buildReverseDependencyIndex(observations, {
|
|
688
|
+
inputs: { files: files.length, loadedFiles: files.length - skipped.length, skipped },
|
|
689
|
+
});
|
|
690
|
+
if (output !== undefined)
|
|
691
|
+
await writeFile(resolve(output), `${JSON.stringify(index, null, 2)}\n`);
|
|
692
|
+
if (json) {
|
|
693
|
+
if (packageFilter === undefined)
|
|
694
|
+
process.stdout.write(`${JSON.stringify(index, null, 2)}\n`);
|
|
695
|
+
else
|
|
696
|
+
process.stdout.write(`${JSON.stringify({ ...index, dependencies: index.dependencies.filter(item => item.dependency.name === packageFilter.name && item.dependency.version === packageFilter.version) }, null, 2)}\n`);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
process.stdout.write(renderReverseDependencyIndex(index, packageFilter));
|
|
700
|
+
if (output !== undefined)
|
|
701
|
+
process.stdout.write(`Index written to ${resolve(output)}\n`);
|
|
702
|
+
}
|
|
703
|
+
return 0;
|
|
704
|
+
}
|
|
527
705
|
async function runGraph(args) {
|
|
528
706
|
const kind = args[0];
|
|
707
|
+
if (kind === 'reverse')
|
|
708
|
+
return runReverseDependencyIndex(args.slice(1));
|
|
529
709
|
if (kind !== 'npm-lock' && kind !== 'pnpm-lock')
|
|
530
|
-
throw new Error('graph requires the npm-lock
|
|
710
|
+
throw new Error('graph requires the npm-lock, pnpm-lock, or reverse subcommand');
|
|
531
711
|
const lockfile = args[1];
|
|
532
712
|
if (lockfile === undefined || lockfile.startsWith('-'))
|
|
533
713
|
throw new Error(`graph ${kind} requires a lockfile path`);
|
|
@@ -1005,6 +1185,21 @@ function runDemo(args) {
|
|
|
1005
1185
|
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderDemo(report));
|
|
1006
1186
|
return 0;
|
|
1007
1187
|
}
|
|
1188
|
+
async function runDshCase(args) {
|
|
1189
|
+
const caseId = args[0];
|
|
1190
|
+
if (caseId !== 'dsh-web-ui')
|
|
1191
|
+
throw new Error('case requires dsh-web-ui');
|
|
1192
|
+
let json = false;
|
|
1193
|
+
for (const argument of args.slice(1)) {
|
|
1194
|
+
if (argument === '--json')
|
|
1195
|
+
json = true;
|
|
1196
|
+
else
|
|
1197
|
+
throw new Error(`unknown option for case: ${argument}`);
|
|
1198
|
+
}
|
|
1199
|
+
const report = await createDshCaseReport();
|
|
1200
|
+
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderDshCase(report));
|
|
1201
|
+
return 0;
|
|
1202
|
+
}
|
|
1008
1203
|
async function runProbe(args) {
|
|
1009
1204
|
const mode = args[0];
|
|
1010
1205
|
if (mode !== 'dsh-load' && mode !== 'dsh-matrix')
|
|
@@ -1071,6 +1266,57 @@ async function runProbe(args) {
|
|
|
1071
1266
|
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderDshLoadMatrix(report));
|
|
1072
1267
|
return report.result === 'compatible' ? 0 : report.result === 'incompatible' ? 2 : 1;
|
|
1073
1268
|
}
|
|
1269
|
+
async function runDshPluginReview(args) {
|
|
1270
|
+
if (args[0] !== 'dsh-plugin')
|
|
1271
|
+
throw new Error('review requires dsh-plugin');
|
|
1272
|
+
const target = args[1];
|
|
1273
|
+
if (target === undefined || target.startsWith('-'))
|
|
1274
|
+
throw new Error('review dsh-plugin requires an exact npm package');
|
|
1275
|
+
const dshVersions = [];
|
|
1276
|
+
let registry;
|
|
1277
|
+
let timeoutSeconds = 120;
|
|
1278
|
+
let json = false;
|
|
1279
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
1280
|
+
const argument = args[index];
|
|
1281
|
+
if (argument === '--json') {
|
|
1282
|
+
json = true;
|
|
1283
|
+
}
|
|
1284
|
+
else if (argument === '--dsh-version' || argument === '--registry' || argument === '--timeout') {
|
|
1285
|
+
const value = args[index + 1];
|
|
1286
|
+
if (value === undefined || value.startsWith('-'))
|
|
1287
|
+
throw new Error(`${argument} requires a value`);
|
|
1288
|
+
if (argument === '--dsh-version') {
|
|
1289
|
+
dshVersions.push(...value.split(',').map(item => item.trim()).filter(item => item !== ''));
|
|
1290
|
+
}
|
|
1291
|
+
else if (argument === '--registry') {
|
|
1292
|
+
registry = value;
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
const parsed = Number(value);
|
|
1296
|
+
if (!Number.isSafeInteger(parsed) || parsed < 30 || parsed > 600) {
|
|
1297
|
+
throw new Error('--timeout must be an integer between 30 and 600 seconds');
|
|
1298
|
+
}
|
|
1299
|
+
timeoutSeconds = parsed;
|
|
1300
|
+
}
|
|
1301
|
+
index += 1;
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
throw new Error(`unknown option for review dsh-plugin: ${argument}`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (dshVersions.length < 2)
|
|
1308
|
+
throw new Error('review dsh-plugin requires at least two exact DSH versions in --dsh-version');
|
|
1309
|
+
const report = await reviewDshPlugin(target, {
|
|
1310
|
+
dshVersions,
|
|
1311
|
+
...(registry === undefined ? {} : { registry }),
|
|
1312
|
+
timeoutMs: timeoutSeconds * 1_000,
|
|
1313
|
+
});
|
|
1314
|
+
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderDshPluginReview(report));
|
|
1315
|
+
// This command is intentionally report-first: a review result is useful even
|
|
1316
|
+
// when it says review, block, incompatible, or unknown. CI callers can gate
|
|
1317
|
+
// on the JSON status without hiding the evidence from the author.
|
|
1318
|
+
return 0;
|
|
1319
|
+
}
|
|
1074
1320
|
async function fileExists(path) {
|
|
1075
1321
|
try {
|
|
1076
1322
|
await access(resolve(path));
|
|
@@ -1405,6 +1651,95 @@ async function runRadar(args) {
|
|
|
1405
1651
|
: `${renderRadarEvents(events)}Prepared ${analysisTasks.length} DSH compatibility analysis task(s).\n`);
|
|
1406
1652
|
return 0;
|
|
1407
1653
|
}
|
|
1654
|
+
function summarizeObserverDshCompatibility(report) {
|
|
1655
|
+
return {
|
|
1656
|
+
result: report.result,
|
|
1657
|
+
versions: report.dshVersions.slice(0, 8),
|
|
1658
|
+
summary: { ...report.summary },
|
|
1659
|
+
reports: report.reports.slice(0, 8).map(item => ({
|
|
1660
|
+
dshVersion: item.dshVersion,
|
|
1661
|
+
result: item.result,
|
|
1662
|
+
reason: item.reason.slice(0, 2_048),
|
|
1663
|
+
})),
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
function summarizeObserverArtifactReview(spec, report, dshCompatibility) {
|
|
1667
|
+
const npm = report.evidence.npm;
|
|
1668
|
+
const audit = npm?.dependencyAudit;
|
|
1669
|
+
const installScriptDetails = (audit?.installScriptDetails ?? []).slice(0, 32).map(detail => ({
|
|
1670
|
+
package: detail.package.slice(0, 512),
|
|
1671
|
+
scripts: detail.scripts.slice(0, 16).map(script => ({
|
|
1672
|
+
name: script.name,
|
|
1673
|
+
command: script.command.slice(0, 4_096),
|
|
1674
|
+
})),
|
|
1675
|
+
}));
|
|
1676
|
+
return {
|
|
1677
|
+
spec,
|
|
1678
|
+
verdict: report.verdict,
|
|
1679
|
+
riskVerdict: report.riskVerdict,
|
|
1680
|
+
coverageVerdict: report.coverageVerdict,
|
|
1681
|
+
artifactIntegrity: report.coverage.artifactIntegrity,
|
|
1682
|
+
registrySignature: npm?.registrySignature.status ?? report.coverage.registrySignature,
|
|
1683
|
+
provenance: npm?.provenance.status ?? report.coverage.provenance,
|
|
1684
|
+
dependencyResolution: report.coverage.dependencyResolution,
|
|
1685
|
+
dependencyAuditStatus: audit?.status ?? 'not-run',
|
|
1686
|
+
...(audit?.resolutionMode === undefined ? {} : { resolutionMode: audit.resolutionMode }),
|
|
1687
|
+
...(audit?.graphDigest === undefined ? {} : { graphDigest: audit.graphDigest }),
|
|
1688
|
+
packages: audit?.packages ?? null,
|
|
1689
|
+
unresolved: audit?.graph?.unresolved?.length ?? 0,
|
|
1690
|
+
vulnerabilities: audit?.vulnerabilities ?? null,
|
|
1691
|
+
installScriptPackages: (audit?.installScriptPackages ?? []).slice(0, 32),
|
|
1692
|
+
installScriptDetails,
|
|
1693
|
+
findings: report.findings.slice(0, 32).map(finding => ({
|
|
1694
|
+
code: finding.code,
|
|
1695
|
+
severity: finding.severity,
|
|
1696
|
+
summary: finding.summary.slice(0, 2_048),
|
|
1697
|
+
detail: finding.detail.slice(0, 4_096),
|
|
1698
|
+
...(finding.remediation === undefined ? {} : { remediation: finding.remediation.slice(0, 4_096) }),
|
|
1699
|
+
})),
|
|
1700
|
+
...(dshCompatibility === undefined ? {} : { dshCompatibility: summarizeObserverDshCompatibility(dshCompatibility) }),
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
async function reviewObserverArtifact(spec, target, registry) {
|
|
1704
|
+
const inspection = await inspectNpmPackage(spec, {
|
|
1705
|
+
deep: true,
|
|
1706
|
+
...(registry === undefined ? {} : { registry }),
|
|
1707
|
+
});
|
|
1708
|
+
if (target.dshVersions === undefined)
|
|
1709
|
+
return summarizeObserverArtifactReview(spec, inspection);
|
|
1710
|
+
try {
|
|
1711
|
+
const combined = await reviewDshPlugin(spec, {
|
|
1712
|
+
dshVersions: target.dshVersions,
|
|
1713
|
+
...(registry === undefined ? {} : { registry }),
|
|
1714
|
+
inspect: async () => inspection,
|
|
1715
|
+
});
|
|
1716
|
+
const summary = summarizeObserverArtifactReview(spec, combined.inspection, combined.compatibility);
|
|
1717
|
+
if (combined.artifact.matched)
|
|
1718
|
+
return summary;
|
|
1719
|
+
return {
|
|
1720
|
+
...summary,
|
|
1721
|
+
verdict: 'review',
|
|
1722
|
+
riskVerdict: 'review',
|
|
1723
|
+
coverageVerdict: 'incomplete',
|
|
1724
|
+
findings: [
|
|
1725
|
+
...summary.findings,
|
|
1726
|
+
{
|
|
1727
|
+
code: 'inspection-probe-artifact-mismatch',
|
|
1728
|
+
severity: 'high',
|
|
1729
|
+
summary: 'The artifact inspected for dependency evidence differs from the artifact loaded by the DSH probe.',
|
|
1730
|
+
detail: 'Repeat the review against one exact registry artifact before treating the compatibility result as evidence for the inspected bytes.',
|
|
1731
|
+
remediation: 'Pin the exact package version and registry, then rerun the observer review until the artifact digests match.',
|
|
1732
|
+
},
|
|
1733
|
+
].slice(0, 32),
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
catch (error) {
|
|
1737
|
+
return {
|
|
1738
|
+
...summarizeObserverArtifactReview(spec, inspection),
|
|
1739
|
+
error: safeErrorMessage(error instanceof Error ? error.message : String(error)),
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1408
1743
|
async function runObserve(args) {
|
|
1409
1744
|
const targetsPath = args[0];
|
|
1410
1745
|
if (targetsPath === undefined || targetsPath.startsWith('-'))
|
|
@@ -1424,6 +1759,7 @@ async function runObserve(args) {
|
|
|
1424
1759
|
let inlineLockfile;
|
|
1425
1760
|
let inlineLockfileType;
|
|
1426
1761
|
let inlineRef;
|
|
1762
|
+
let inlineDshVersions;
|
|
1427
1763
|
let inlineOptionsUsed = false;
|
|
1428
1764
|
for (let index = 1; index < args.length; index += 1) {
|
|
1429
1765
|
const argument = args[index];
|
|
@@ -1433,7 +1769,7 @@ async function runObserve(args) {
|
|
|
1433
1769
|
else if (argument === '--retry-pending') {
|
|
1434
1770
|
retryPending = true;
|
|
1435
1771
|
}
|
|
1436
|
-
else if (argument === '--ecosystem' || argument === '--id' || argument === '--package' || argument === '--package-path' || argument === '--lockfile' || argument === '--lockfile-type' || argument === '--ref' || argument === '--state' || argument === '--report' || argument === '--dsh-agent-command' || argument === '--dsh-agent-arg' || argument === '--llm-env-file' || argument === '--registry') {
|
|
1772
|
+
else if (argument === '--ecosystem' || argument === '--id' || argument === '--package' || argument === '--package-path' || argument === '--lockfile' || argument === '--lockfile-type' || argument === '--ref' || argument === '--dsh-version' || argument === '--state' || argument === '--report' || argument === '--dsh-agent-command' || argument === '--dsh-agent-arg' || argument === '--llm-env-file' || argument === '--registry') {
|
|
1437
1773
|
const value = args[index + 1];
|
|
1438
1774
|
if (value === undefined || (value.startsWith('-') && argument !== '--dsh-agent-arg'))
|
|
1439
1775
|
throw new Error(`${argument} requires a value`);
|
|
@@ -1451,6 +1787,8 @@ async function runObserve(args) {
|
|
|
1451
1787
|
inlineLockfileType = value;
|
|
1452
1788
|
else if (argument === '--ref')
|
|
1453
1789
|
inlineRef = value;
|
|
1790
|
+
else if (argument === '--dsh-version')
|
|
1791
|
+
inlineDshVersions = value.split(',').map(item => item.trim()).filter(item => item !== '');
|
|
1454
1792
|
else if (argument === '--state')
|
|
1455
1793
|
statePath = value;
|
|
1456
1794
|
else if (argument === '--report')
|
|
@@ -1485,9 +1823,10 @@ async function runObserve(args) {
|
|
|
1485
1823
|
repository: `${githubTarget.owner}/${githubTarget.repository}`,
|
|
1486
1824
|
ref: inlineRef ?? 'main',
|
|
1487
1825
|
packageName: inlinePackage,
|
|
1488
|
-
packagePath: inlinePackagePath
|
|
1826
|
+
...(inlinePackagePath === undefined ? {} : { packagePath: inlinePackagePath }),
|
|
1489
1827
|
...(inlineLockfile === undefined ? {} : { lockfile: inlineLockfile }),
|
|
1490
1828
|
...(inlineLockfileType === undefined ? {} : { lockfileType: inlineLockfileType }),
|
|
1829
|
+
...(inlineDshVersions === undefined ? {} : { dshVersions: inlineDshVersions }),
|
|
1491
1830
|
}],
|
|
1492
1831
|
});
|
|
1493
1832
|
}
|
|
@@ -1495,7 +1834,7 @@ async function runObserve(args) {
|
|
|
1495
1834
|
if (/^https?:\/\//i.test(targetsPath))
|
|
1496
1835
|
throw new Error('observe URL must be an HTTPS GitHub repository URL');
|
|
1497
1836
|
if (inlineOptionsUsed)
|
|
1498
|
-
throw new Error('--ecosystem, --id, --package, --package-path, --lockfile, --lockfile-type and --
|
|
1837
|
+
throw new Error('--ecosystem, --id, --package, --package-path, --lockfile, --lockfile-type, --ref and --dsh-version require a GitHub URL target');
|
|
1499
1838
|
const targetText = await readBoundedFile(targetsPath, 256 * 1024);
|
|
1500
1839
|
config = parseObserverConfigText(targetText);
|
|
1501
1840
|
}
|
|
@@ -1517,6 +1856,7 @@ async function runObserve(args) {
|
|
|
1517
1856
|
const result = await runObserver(config, previousState, {
|
|
1518
1857
|
source,
|
|
1519
1858
|
retryPending,
|
|
1859
|
+
artifactReviewer: async (spec, target) => reviewObserverArtifact(spec, target, registry),
|
|
1520
1860
|
...(agentOptions === undefined ? {} : {
|
|
1521
1861
|
agent: (task, prompt) => runDshAgentCommand(task, prompt, agentOptions),
|
|
1522
1862
|
}),
|
|
@@ -1530,9 +1870,11 @@ async function runObserve(args) {
|
|
|
1530
1870
|
await writeFile(resolve(reportPath), reportPath.endsWith('.json') ? reportJson : renderObserverReport(result.report));
|
|
1531
1871
|
}
|
|
1532
1872
|
process.stdout.write(json ? reportJson : renderObserverReport(result.report));
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1873
|
+
const observerExit = observerExitCode(result.report);
|
|
1874
|
+
if (result.report.agent.failed > 0) {
|
|
1875
|
+
process.stderr.write('upstream-radar: static observation completed; DSH Agent/model analysis remains pending. See the report and rerun with --retry-pending.\n');
|
|
1876
|
+
}
|
|
1877
|
+
return observerExit;
|
|
1536
1878
|
}
|
|
1537
1879
|
async function runQuickstart(args) {
|
|
1538
1880
|
let directory = process.cwd();
|
|
@@ -2001,8 +2343,12 @@ async function main(args) {
|
|
|
2001
2343
|
return runDshProfileCheck(args.slice(1));
|
|
2002
2344
|
if (command === 'probe')
|
|
2003
2345
|
return runProbe(args.slice(1));
|
|
2346
|
+
if (command === 'review')
|
|
2347
|
+
return runDshPluginReview(args.slice(1));
|
|
2004
2348
|
if (command === 'demo')
|
|
2005
2349
|
return runDemo(args.slice(1));
|
|
2350
|
+
if (command === 'case')
|
|
2351
|
+
return runDshCase(args.slice(1));
|
|
2006
2352
|
if (command === 'observe')
|
|
2007
2353
|
return runObserve(args.slice(1));
|
|
2008
2354
|
if (command === 'benchmark')
|