aienvmp 0.1.46 → 0.1.48

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/BUGFIXES.md CHANGED
@@ -64,6 +64,18 @@ Short record of bugs, fixes, and follow-up checks.
64
64
  - Fix: `lightSbom.riskSummary` and preflight `sbomRisk` now provide a compact risk score, signals, review targets, and advisory next steps.
65
65
  - Verification: tests cover risk scoring, top-risk severity fallback, scanner-off guidance, status/context exposure, dashboard rendering, and recommended actions.
66
66
 
67
+ ### Light SBOM was only nested inside larger artifacts
68
+
69
+ - Issue: CI tools and AI agents that only need dependency/SBOM context had to read the full manifest or context payload.
70
+ - Fix: `aienvmp sbom --json` and `.aienvmp/sbom.json` now expose a standalone light SBOM artifact.
71
+ - Verification: tests cover standalone SBOM construction, writing, sync output, schema metadata, and dashboard linking.
72
+
73
+ ### Light SBOM needed a lightweight standard export
74
+
75
+ - Issue: the standalone SBOM was AI-friendly, but less convenient for tools expecting CycloneDX-shaped data.
76
+ - Fix: `aienvmp sbom --format cyclonedx-lite` and `.aienvmp/sbom.cdx.json` now export project-manifest packages in a CycloneDX-compatible shape with explicit limitations.
77
+ - Verification: tests cover component mapping, vulnerability hints, sync output, schema metadata, and dashboard links.
78
+
67
79
  ## Template
68
80
 
69
81
  ### Title
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.48
4
+
5
+ - Added `aienvmp sbom --format cyclonedx-lite` for a lightweight CycloneDX-compatible export.
6
+ - Added `.aienvmp/sbom.cdx.json` writing through the default `sync` flow.
7
+ - Added CycloneDX-lite artifact paths to status/preflight outputs and schema metadata.
8
+ - Added dashboard links for `sbom.cdx.json` beside the native light SBOM artifact.
9
+ - Added README guidance for the CycloneDX-lite command and output.
10
+ - Included explicit metadata limitations so consumers know no install or dependency resolver was run.
11
+ - Added regression tests for CycloneDX-lite component mapping, vulnerability hints, sync output, schema contract, and dashboard rendering.
12
+
13
+ ## 0.1.47
14
+
15
+ - Added `aienvmp sbom` for standalone AI-readable light SBOM output without deep manifest parsing.
16
+ - Added `.aienvmp/sbom.json` writing through `aienvmp sbom --write` and the default `sync` flow.
17
+ - Added the SBOM artifact path to the shared preflight artifacts map.
18
+ - Added SBOM output metadata to `aienvmp schema --json`.
19
+ - Added a dashboard Light SBOM Artifact card linking to `sbom.json`.
20
+ - Updated README outputs and commands with the standalone SBOM artifact.
21
+ - Added regression tests for SBOM artifact building, writing, sync output, schema contract, and dashboard rendering.
22
+
3
23
  ## 0.1.46
4
24
 
5
25
  - Added `lightSbom.riskSummary` with compact risk level, score, scanner state, signals, review targets, and next commands.
package/README.md CHANGED
@@ -35,6 +35,8 @@ Warnings are advisory by default. Use `doctor --strict <scope>` only when you wa
35
35
  AIENV.md # Markdown env map for AI agents
36
36
  .aienvmp/status.json # first AI read: clear/review, next command, nextAgent hint
37
37
  .aienvmp/manifest.json # runtime map + light SBOM
38
+ .aienvmp/sbom.json # standalone AI-readable light SBOM
39
+ .aienvmp/sbom.cdx.json # CycloneDX-lite export from project manifests
38
40
  .aienvmp/intents.jsonl # planned env changes
39
41
  .aienvmp/timeline.jsonl # append-only change ledger
40
42
  .aienvmp/plan.md # read-only action plan
@@ -72,6 +74,8 @@ npx aienvmp snippet gemini
72
74
  aienvmp sync # update env map, status, ledger, dashboard
73
75
  aienvmp status --write # refresh compact AI status
74
76
  aienvmp context --json # AI decision contract
77
+ aienvmp sbom --json # standalone light SBOM
78
+ aienvmp sbom --format cyclonedx-lite --json
75
79
  aienvmp schema --json # stable output contract for AI/CI consumers
76
80
  aienvmp plan --write # read-only action plan
77
81
  aienvmp handoff --record # next-agent summary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -15,6 +15,7 @@ import { planWorkspace } from "./commands/plan.js";
15
15
  import { statusWorkspace } from "./commands/status.js";
16
16
  import { schemaWorkspace } from "./commands/schema.js";
17
17
  import { checkpointWorkspace } from "./commands/checkpoint.js";
18
+ import { sbomWorkspace } from "./commands/sbom.js";
18
19
  import { readFileSync } from "node:fs";
19
20
 
20
21
  const commands = new Map([
@@ -34,7 +35,8 @@ const commands = new Map([
34
35
  ["plan", planWorkspace],
35
36
  ["status", statusWorkspace],
36
37
  ["schema", schemaWorkspace],
37
- ["checkpoint", checkpointWorkspace]
38
+ ["checkpoint", checkpointWorkspace],
39
+ ["sbom", sbomWorkspace]
38
40
  ]);
39
41
 
40
42
  const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
@@ -112,6 +114,7 @@ Usage:
112
114
  aienvmp handoff [--dir .] [--json] [--record --actor agent:id]
113
115
  aienvmp checkpoint [--dir .] --actor agent:id --summary "what changed" [--target dependency] [--json]
114
116
  aienvmp plan [--dir .] [--json] [--write]
117
+ aienvmp sbom [--dir .] [--json] [--write]
115
118
  aienvmp schema [--json]
116
119
 
117
120
  Common:
@@ -121,6 +124,7 @@ Common:
121
124
  aienvmp handoff print the next-agent handoff summary
122
125
  aienvmp checkpoint record, sync, status, and handoff after an env change
123
126
  aienvmp plan print a read-only AI environment action plan
127
+ aienvmp sbom print/write standalone light SBOM artifact
124
128
  aienvmp schema print the stable AI-readable output contract
125
129
  aienvmp snippet print an AGENTS.md pointer snippet
126
130
  aienvmp dash regenerate/open the lightweight dashboard
@@ -136,6 +140,7 @@ Advanced:
136
140
  aienvmp compile [--dir .]
137
141
  aienvmp diff [--dir .]
138
142
  aienvmp doctor [--dir .] [--json] [--ci] [--strict security|policy|coordination|all]
143
+ aienvmp sbom [--dir .] [--json] [--write]
139
144
  aienvmp dash [--dir .] [--open]
140
145
  `);
141
146
  }
@@ -0,0 +1,143 @@
1
+ import { readJson, writeJson } from "../fsutil.js";
2
+ import { cyclonedxSbomPath, manifestPath, sbomJsonPath, workspaceDir } from "../paths.js";
3
+
4
+ export async function sbomWorkspace(args = {}) {
5
+ const dir = workspaceDir(args);
6
+ const manifest = await readJson(manifestPath(dir));
7
+ if (!manifest) throw new Error("missing manifest; run `aienvmp sync` first");
8
+ const format = normalizeFormat(args.format);
9
+ const sbom = format === "cyclonedx-lite" ? buildCycloneDxLite(manifest) : buildSbomArtifact(manifest);
10
+ const artifact = args.write ? await writeSbomArtifact(dir, sbom, format) : "";
11
+ const output = artifact ? { ...sbom, artifact } : sbom;
12
+ if (args.json || args.write || args.quiet) {
13
+ if (args.json) console.log(JSON.stringify(output, null, 2));
14
+ } else {
15
+ console.log(`sbom: ${sbom.riskSummary.level}/${sbom.riskSummary.score}`);
16
+ console.log(`packages: ${sbom.summary.packages || 0}`);
17
+ console.log(`vulnerabilities: ${sbom.summary.vulnerabilities || 0}`);
18
+ console.log(`next: ${sbom.riskSummary.next}`);
19
+ }
20
+ return output;
21
+ }
22
+
23
+ export function buildSbomArtifact(manifest = {}) {
24
+ const lightSbom = manifest.lightSbom || {};
25
+ return {
26
+ schemaVersion: 1,
27
+ schemaName: "aienvmp.light-sbom",
28
+ generatedAt: manifest.generatedAt || "",
29
+ workspace: manifest.workspace || {},
30
+ mode: lightSbom.mode || "light-sbom",
31
+ source: lightSbom.source || {},
32
+ confidence: lightSbom.confidence || {},
33
+ limitations: lightSbom.limitations || [],
34
+ summary: lightSbom.summary || { packages: 0, vulnerabilities: 0 },
35
+ riskSummary: lightSbom.riskSummary || { level: "clear", score: 0, signals: [], commands: [] },
36
+ topRisk: (lightSbom.topRisk || []).slice(0, 20),
37
+ packageManagerPolicy: lightSbom.packageManagerPolicy || {},
38
+ dependencyChangeHints: (lightSbom.dependencyChangeHints || []).slice(0, 20),
39
+ aiUse: {
40
+ purpose: "Standalone AI-readable light SBOM artifact.",
41
+ readBefore: "Dependency changes, vulnerability remediation, release review, or shared AI handoff.",
42
+ nextCommand: lightSbom.riskSummary?.commands?.[0] || "aienvmp context --json",
43
+ rule: "Use as a lightweight planning map; verify security claims with dedicated scanners."
44
+ }
45
+ };
46
+ }
47
+
48
+ export async function writeSbomArtifact(dir, sbom) {
49
+ const out = sbom.bomFormat === "CycloneDX" ? cyclonedxSbomPath(dir) : sbomJsonPath(dir);
50
+ await writeJson(out, sbom);
51
+ return out;
52
+ }
53
+
54
+ export function buildCycloneDxLite(manifest = {}) {
55
+ const snapshot = manifest.dependencySnapshot || {};
56
+ const packages = snapshot.packages || [];
57
+ const lightSbom = manifest.lightSbom || {};
58
+ return {
59
+ bomFormat: "CycloneDX",
60
+ specVersion: "1.6",
61
+ serialNumber: `urn:uuid:aienvmp-${hashText(`${manifest.workspace?.path || ""}:${manifest.generatedAt || ""}`)}`,
62
+ version: 1,
63
+ metadata: {
64
+ timestamp: manifest.generatedAt || "",
65
+ tools: {
66
+ components: [{
67
+ type: "application",
68
+ name: "aienvmp",
69
+ version: manifest.generatedBy?.version || "unknown"
70
+ }]
71
+ },
72
+ component: {
73
+ type: "application",
74
+ name: manifest.workspace?.name || "workspace",
75
+ bomRef: "workspace"
76
+ },
77
+ properties: [
78
+ { name: "aienvmp:format", value: "cyclonedx-lite" },
79
+ { name: "aienvmp:source", value: lightSbom.source?.dependencies || "project manifests" },
80
+ { name: "aienvmp:confidence:transitiveDependencies", value: lightSbom.confidence?.transitiveDependencies || "not-resolved" },
81
+ { name: "aienvmp:risk:level", value: lightSbom.riskSummary?.level || "clear" },
82
+ { name: "aienvmp:risk:score", value: String(lightSbom.riskSummary?.score || 0) }
83
+ ]
84
+ },
85
+ components: packages.slice(0, 200).map(cycloneComponent),
86
+ vulnerabilities: (lightSbom.topRisk || []).slice(0, 50).map(cycloneVulnerability),
87
+ properties: [
88
+ { name: "aienvmp:limitation", value: "Light SBOM from project manifests only; no install or dependency resolver was run." },
89
+ { name: "aienvmp:verifyWith", value: "CycloneDX, Syft, Trivy, npm audit, pip-audit, or another dedicated scanner before security claims." }
90
+ ]
91
+ };
92
+ }
93
+
94
+ function cycloneComponent(pkg = {}) {
95
+ const version = String(pkg.version || "unspecified");
96
+ return {
97
+ type: "library",
98
+ name: pkg.name || "unknown",
99
+ version,
100
+ purl: packageUrl(pkg, version),
101
+ bomRef: `${pkg.ecosystem || "pkg"}:${pkg.name || "unknown"}@${version}`,
102
+ properties: [
103
+ { name: "aienvmp:ecosystem", value: pkg.ecosystem || "unknown" },
104
+ { name: "aienvmp:manager", value: pkg.manager || "unknown" },
105
+ { name: "aienvmp:manifest", value: pkg.manifest || "" },
106
+ { name: "aienvmp:group", value: pkg.group || "" }
107
+ ]
108
+ };
109
+ }
110
+
111
+ function cycloneVulnerability(pkg = {}) {
112
+ return {
113
+ id: pkg.name || "unknown",
114
+ source: { name: "aienvmp-light-sbom" },
115
+ ratings: [{ severity: pkg.severity || "unknown" }],
116
+ affects: [{
117
+ ref: `${pkg.ecosystem || "pkg"}:${pkg.name || "unknown"}@${pkg.version || "unspecified"}`
118
+ }],
119
+ properties: [
120
+ { name: "aienvmp:priority", value: pkg.priority || "low" },
121
+ { name: "aienvmp:score", value: String(pkg.score || 0) },
122
+ { name: "aienvmp:directDependency", value: String(pkg.directDependency === true) },
123
+ { name: "aienvmp:manifest", value: pkg.manifest || "" }
124
+ ]
125
+ };
126
+ }
127
+
128
+ function packageUrl(pkg = {}, version = "") {
129
+ const type = pkg.ecosystem === "python" ? "pypi" : "npm";
130
+ return `pkg:${type}/${encodeURIComponent(pkg.name || "unknown")}@${encodeURIComponent(version)}`;
131
+ }
132
+
133
+ function normalizeFormat(format = "") {
134
+ const value = String(format || "aienvmp").toLowerCase();
135
+ if (["cyclonedx", "cyclonedx-lite", "cdx"].includes(value)) return "cyclonedx-lite";
136
+ return "aienvmp";
137
+ }
138
+
139
+ function hashText(text = "") {
140
+ let hash = 0;
141
+ for (const ch of String(text)) hash = ((hash << 5) - hash + ch.charCodeAt(0)) >>> 0;
142
+ return `${hash.toString(16).padStart(8, "0")}-0000-4000-8000-000000000000`;
143
+ }
@@ -3,6 +3,7 @@ import { scanWorkspace } from "./scan.js";
3
3
  import { compileWorkspace } from "./compile.js";
4
4
  import { dashWorkspace } from "./dash.js";
5
5
  import { statusWorkspace } from "./status.js";
6
+ import { sbomWorkspace } from "./sbom.js";
6
7
 
7
8
  export async function syncWorkspace(args) {
8
9
  const quiet = args.quiet || args.json;
@@ -13,6 +14,8 @@ export async function syncWorkspace(args) {
13
14
  const compiled = await compileWorkspace(next);
14
15
  const dashboard = await dashWorkspace(next);
15
16
  const status = await statusWorkspace({ ...next, json: false, write: true, quiet: true });
17
+ const sbom = await sbomWorkspace({ ...next, json: false, write: true, quiet: true });
18
+ const cyclonedx = await sbomWorkspace({ ...next, json: false, write: true, quiet: true, format: "cyclonedx-lite" });
16
19
 
17
20
  const result = {
18
21
  status: "ok",
@@ -21,6 +24,8 @@ export async function syncWorkspace(args) {
21
24
  manifest: scanned.manifest,
22
25
  timeline: scanned.timeline,
23
26
  status: status.artifact,
27
+ sbom: sbom.artifact,
28
+ cyclonedx: cyclonedx.artifact,
24
29
  dashboard: dashboard.dashboard
25
30
  },
26
31
  changes: scanned.changes,
package/src/contract.js CHANGED
@@ -31,6 +31,16 @@ export function schemaContract() {
31
31
  manifest: {
32
32
  file: ".aienvmp/manifest.json",
33
33
  rootFields: ["schemaVersion", "workspace", "runtimes", "packageManagers", "dependencySnapshot", "lightSbom", "security", "trust"]
34
+ },
35
+ sbom: {
36
+ file: ".aienvmp/sbom.json",
37
+ command: "aienvmp sbom --json",
38
+ rootFields: ["schemaVersion", "schemaName", "workspace", "summary", "riskSummary", "topRisk", "packageManagerPolicy", "dependencyChangeHints"]
39
+ },
40
+ cyclonedxLite: {
41
+ file: ".aienvmp/sbom.cdx.json",
42
+ command: "aienvmp sbom --format cyclonedx-lite --json",
43
+ rootFields: ["bomFormat", "specVersion", "metadata", "components", "vulnerabilities", "properties"]
34
44
  }
35
45
  },
36
46
  compatibility: {
package/src/paths.js CHANGED
@@ -16,6 +16,14 @@ export function statusJsonPath(dir) {
16
16
  return path.join(stateDir(dir), "status.json");
17
17
  }
18
18
 
19
+ export function sbomJsonPath(dir) {
20
+ return path.join(stateDir(dir), "sbom.json");
21
+ }
22
+
23
+ export function cyclonedxSbomPath(dir) {
24
+ return path.join(stateDir(dir), "sbom.cdx.json");
25
+ }
26
+
19
27
  export function previousManifestPath(dir) {
20
28
  return path.join(stateDir(dir), "manifest.previous.json");
21
29
  }
package/src/preflight.js CHANGED
@@ -353,6 +353,8 @@ export function preflightArtifacts() {
353
353
  status: ".aienvmp/status.json",
354
354
  envMap: "AIENV.md",
355
355
  manifest: ".aienvmp/manifest.json",
356
+ sbom: ".aienvmp/sbom.json",
357
+ cyclonedx: ".aienvmp/sbom.cdx.json",
356
358
  dashboard: ".aienvmp/dashboard.html",
357
359
  planJson: ".aienvmp/plan.json",
358
360
  planMarkdown: ".aienvmp/plan.md",
package/src/render.js CHANGED
@@ -442,6 +442,7 @@ const actions=manifest.recommendedActions||[];
442
442
  const actionsHtml=actions.length?'<div class="timeline">'+actions.slice(0,6).map(a=>\`<div class="event"><time>\${esc(a.priority)}</time><div><b>\${esc(a.category)}</b> \${esc(a.summary)}\${a.command?\`<div class="path">\${esc(a.command)}</div>\`:''}</div></div>\`).join('')+'</div>':'<div class="okline">No recommended actions. Continue project-local work.</div>';
443
443
  const plan=manifest.planArtifacts||{};
444
444
  const planHtml=plan.markdown||plan.json?\`<table><tr><th>Markdown</th><td>\${plan.markdown?'<a href="plan.md">plan.md</a>':'not written'}</td></tr><tr><th>JSON</th><td>\${plan.json?'<a href="plan.json">plan.json</a>':'not written'}</td></tr></table>\`:'<div class="okline">No plan artifacts yet. Run <code>aienvmp plan --write</code>.</div>';
445
+ const sbomArtifactHtml='<table><tr><th>JSON</th><td><a href="sbom.json">sbom.json</a></td></tr><tr><th>CDX Lite</th><td><a href="sbom.cdx.json">sbom.cdx.json</a></td></tr><tr><th>Command</th><td><code>aienvmp sbom --write</code></td></tr></table>';
445
446
  const remediation=manifest.planRemediation||[];
446
447
  const remediationFix=r=>r.fixVersions?.length?\`fix \${r.fixVersions.join(', ')}\`:(r.fixAvailable?'fix available':'review required');
447
448
  const remediationRefs=r=>r.advisories?.length?\` - \${r.advisories.join(', ')}\`:'';
@@ -530,6 +531,8 @@ document.getElementById('app').innerHTML=\`
530
531
  <div style="height:14px"></div>
531
532
  \${card('AI Plan Artifacts',plan.markdown||plan.json?'<span class="pill">written</span>':'<span class="pill off">not written</span>',planHtml)}
532
533
  <div style="height:14px"></div>
534
+ \${card('Light SBOM Artifact','<span class="pill">json</span>',sbomArtifactHtml)}
535
+ <div style="height:14px"></div>
533
536
  \${card('Remediation Steps',remediation.length?'<span class="pill warn">'+remediation.length+' items</span>':'<span class="pill off">none</span>',remediationHtml)}
534
537
  <div style="height:14px"></div>
535
538
  \${card('Environment Steps',envSteps.length?'<span class="pill warn">'+envSteps.length+' items</span>':'<span class="pill off">none</span>',envStepsHtml)}