aienvmp 0.1.25 → 0.1.27
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/CHANGELOG.md +12 -0
- package/README.md +5 -5
- package/package.json +1 -1
- package/src/commands/context.js +12 -38
- package/src/commands/handoff.js +2 -0
- package/src/commands/plan.js +2 -0
- package/src/decision.js +35 -0
- package/src/dependencies.js +114 -0
- package/src/manifest.js +7 -0
- package/src/render.js +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.27
|
|
4
|
+
|
|
5
|
+
- Added a read-only dependency snapshot to the manifest, context, AIENV.md, and dashboard.
|
|
6
|
+
- Captured npm and Python project dependencies from `package.json`, `requirements.txt`, and `pyproject.toml` without installing or resolving packages.
|
|
7
|
+
- Linked the env map and light SBOM story so AI agents can see runtime, dependency, and vulnerability context together.
|
|
8
|
+
|
|
9
|
+
## 0.1.26
|
|
10
|
+
|
|
11
|
+
- Shared one AI decision contract across `context --json`, `plan`, and `handoff`.
|
|
12
|
+
- Added decision mode and required command guidance to plan and handoff outputs.
|
|
13
|
+
- Reduced duplicated guidance so multiple AI agents receive the same environment-change rules.
|
|
14
|
+
|
|
3
15
|
## 0.1.25
|
|
4
16
|
|
|
5
17
|
- Added a more explicit AI decision contract to `context --json`.
|
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
`aienvmp` is an AI-first environment map for shared coding machines.
|
|
10
10
|
|
|
11
|
-
It helps Codex, Claude, Gemini, and humans avoid silent Node, Python, package manager, Docker, and security drift.
|
|
11
|
+
It helps Codex, Claude, Gemini, and humans avoid silent Node, Python, package manager, dependency, Docker, and security drift.
|
|
12
12
|
|
|
13
|
-
Core loop: scan once, give AI a
|
|
13
|
+
Core loop: scan once, give AI a shared decision contract, write a read-only action plan, and hand off safe next steps.
|
|
14
14
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
@@ -48,7 +48,7 @@ AIENV.md
|
|
|
48
48
|
.aienvmp/timeline.jsonl
|
|
49
49
|
.aienvmp/plan.json
|
|
50
50
|
.aienvmp/plan.md
|
|
51
|
-
.aienvmp/dashboard.html # includes plan, remediation, and environment cards
|
|
51
|
+
.aienvmp/dashboard.html # includes dependencies, plan, remediation, and environment cards
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Trust states are machine-readable: `observed`, `planned`, `changed`, `review`, `verified`, `stale`.
|
|
@@ -81,8 +81,8 @@ The dashboard shows which strict scopes are CI-ready before you enforce them.
|
|
|
81
81
|
aienvmp sync # update env map, light SBOM, ledger, dashboard
|
|
82
82
|
aienvmp context # AI preflight brief
|
|
83
83
|
aienvmp context --json # AI decision contract + actions + compact step summary
|
|
84
|
-
aienvmp plan # read-only AI action plan
|
|
85
|
-
aienvmp handoff # next-agent handoff summary
|
|
84
|
+
aienvmp plan # read-only AI action plan using the same decision contract
|
|
85
|
+
aienvmp handoff # next-agent handoff summary using the same decision contract
|
|
86
86
|
aienvmp intent # record a planned env change
|
|
87
87
|
aienvmp record # record what changed
|
|
88
88
|
aienvmp doctor --ci # strict CI check for all warnings
|
package/package.json
CHANGED
package/src/commands/context.js
CHANGED
|
@@ -6,6 +6,7 @@ import { renderContext } from "../render.js";
|
|
|
6
6
|
import { loadPolicy, policyWarnings } from "../policy.js";
|
|
7
7
|
import { recommendedActions } from "../actions.js";
|
|
8
8
|
import { buildPlan, compactStepSummary } from "./plan.js";
|
|
9
|
+
import { aiDecision } from "../decision.js";
|
|
9
10
|
|
|
10
11
|
export async function contextWorkspace(args) {
|
|
11
12
|
const dir = workspaceDir(args);
|
|
@@ -15,7 +16,7 @@ export async function contextWorkspace(args) {
|
|
|
15
16
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
16
17
|
const policy = await loadPolicy(dir);
|
|
17
18
|
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
18
|
-
const decision =
|
|
19
|
+
const decision = aiDecision(warnings, intents);
|
|
19
20
|
const actions = recommendedActions(manifest, { warnings, intents });
|
|
20
21
|
const stepSummary = compactStepSummary(buildPlan(manifest, warnings, intents, policy));
|
|
21
22
|
if (args.json) {
|
|
@@ -31,6 +32,7 @@ export async function contextWorkspace(args) {
|
|
|
31
32
|
packageManagers: manifest.packageManagers,
|
|
32
33
|
containers: manifest.containers,
|
|
33
34
|
inventory: inventorySummary(manifest.inventory),
|
|
35
|
+
dependencySnapshot: dependencySummary(manifest.dependencySnapshot),
|
|
34
36
|
security: securitySummary(manifest.security),
|
|
35
37
|
projectHints: manifest.projectHints,
|
|
36
38
|
warnings,
|
|
@@ -44,6 +46,15 @@ export async function contextWorkspace(args) {
|
|
|
44
46
|
console.log(renderContext(manifest, timeline, warnings, intents, policy, actions));
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
function dependencySummary(snapshot = {}) {
|
|
50
|
+
return {
|
|
51
|
+
mode: snapshot.mode || "snapshot",
|
|
52
|
+
enabled: snapshot.enabled === true,
|
|
53
|
+
summary: snapshot.summary || { ecosystems: [], manifests: 0, packages: 0 },
|
|
54
|
+
packages: (snapshot.packages || []).slice(0, 12)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
function securitySummary(security = {}) {
|
|
48
59
|
return {
|
|
49
60
|
mode: security.mode || "basic",
|
|
@@ -61,40 +72,3 @@ function inventorySummary(inventory = {}) {
|
|
|
61
72
|
groups: Object.fromEntries(Object.entries(tools).map(([name, items]) => [name, items.length]))
|
|
62
73
|
};
|
|
63
74
|
}
|
|
64
|
-
|
|
65
|
-
function contextDecision(warnings, intents) {
|
|
66
|
-
const warningCodes = warnings.map((warning) => warning.code);
|
|
67
|
-
const reviewRequired = warnings.length > 0 || intents.length > 0;
|
|
68
|
-
const canChangeEnvironmentWithoutReview = !reviewRequired;
|
|
69
|
-
const mode = reviewRequired ? "review-first" : "project-local-work";
|
|
70
|
-
return {
|
|
71
|
-
schemaVersion: 1,
|
|
72
|
-
mode,
|
|
73
|
-
canProceed: !reviewRequired,
|
|
74
|
-
canContinueProjectLocalWork: true,
|
|
75
|
-
canChangeEnvironmentWithoutReview,
|
|
76
|
-
safeForProjectLocalWork: warnings.length === 0,
|
|
77
|
-
reviewRequired,
|
|
78
|
-
warningCodes,
|
|
79
|
-
environmentChangeRequiresIntent: true,
|
|
80
|
-
globalEnvironmentChangesRequireUserApproval: true,
|
|
81
|
-
pendingIntentCount: intents.length,
|
|
82
|
-
mustNotDo: [
|
|
83
|
-
"do not change global runtimes without user approval",
|
|
84
|
-
"do not install or remove global package managers without user approval",
|
|
85
|
-
"do not change Docker daemon/context assumptions without user approval",
|
|
86
|
-
"do not ignore open intents or review-required warnings"
|
|
87
|
-
],
|
|
88
|
-
recommendedNextActions: warnings.length
|
|
89
|
-
? ["review warnings", "ask the user before environment changes", "record intent before changes"]
|
|
90
|
-
: ["continue with project-local work", "run aienvmp intent before environment changes"],
|
|
91
|
-
requiredCommands: {
|
|
92
|
-
beforeEnvironmentChange: "aienvmp intent --actor agent:id --action planned-change --target <runtime|package-manager|docker>",
|
|
93
|
-
refreshAfterChange: "aienvmp sync",
|
|
94
|
-
recordAfterChange: "aienvmp record --actor agent:id --summary what-changed",
|
|
95
|
-
handoff: "aienvmp handoff --record --actor agent:id",
|
|
96
|
-
reviewPlan: "aienvmp plan"
|
|
97
|
-
},
|
|
98
|
-
nextCommand: reviewRequired ? "aienvmp plan" : "continue project-local work; use aienvmp intent before environment changes"
|
|
99
|
-
};
|
|
100
|
-
}
|
package/src/commands/handoff.js
CHANGED
|
@@ -6,6 +6,7 @@ import { renderHandoff } from "../render.js";
|
|
|
6
6
|
import { openIntents, readJsonl, readTimeline } from "../timeline.js";
|
|
7
7
|
import { changedTrust } from "../trust.js";
|
|
8
8
|
import { recommendedActions } from "../actions.js";
|
|
9
|
+
import { aiDecision } from "../decision.js";
|
|
9
10
|
|
|
10
11
|
export async function handoffWorkspace(args) {
|
|
11
12
|
const dir = workspaceDir(args);
|
|
@@ -48,6 +49,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
48
49
|
status: reviewRequired ? "review-required" : "clear",
|
|
49
50
|
trust: manifest.trust || {},
|
|
50
51
|
schemaVersion: manifest.schemaVersion || 1,
|
|
52
|
+
decision: aiDecision(warnings, intents),
|
|
51
53
|
workspace: manifest.workspace,
|
|
52
54
|
safeRuntime: {
|
|
53
55
|
node: manifest.runtimes?.node || "not detected",
|
package/src/commands/plan.js
CHANGED
|
@@ -6,6 +6,7 @@ import { intentsPath, manifestPath, planJsonPath, planMdPath, timelinePath, work
|
|
|
6
6
|
import { renderPlan } from "../render.js";
|
|
7
7
|
import { recommendedActions } from "../actions.js";
|
|
8
8
|
import { openIntents, readJsonl, readTimeline } from "../timeline.js";
|
|
9
|
+
import { aiDecision } from "../decision.js";
|
|
9
10
|
|
|
10
11
|
export async function planWorkspace(args) {
|
|
11
12
|
const dir = workspaceDir(args);
|
|
@@ -39,6 +40,7 @@ export function buildPlan(manifest, warnings = [], intents = [], policy = {}) {
|
|
|
39
40
|
status,
|
|
40
41
|
workspace: manifest.workspace || {},
|
|
41
42
|
trust: manifest.trust || {},
|
|
43
|
+
decision: aiDecision(warnings, intents),
|
|
42
44
|
policy: {
|
|
43
45
|
node: policy.node || "not set",
|
|
44
46
|
python: policy.python || "not set",
|
package/src/decision.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function aiDecision(warnings = [], intents = []) {
|
|
2
|
+
const warningCodes = warnings.map((warning) => warning.code).filter(Boolean);
|
|
3
|
+
const reviewRequired = warnings.length > 0 || intents.length > 0;
|
|
4
|
+
const mode = reviewRequired ? "review-first" : "project-local-work";
|
|
5
|
+
return {
|
|
6
|
+
schemaVersion: 1,
|
|
7
|
+
mode,
|
|
8
|
+
canProceed: !reviewRequired,
|
|
9
|
+
canContinueProjectLocalWork: true,
|
|
10
|
+
canChangeEnvironmentWithoutReview: !reviewRequired,
|
|
11
|
+
safeForProjectLocalWork: warnings.length === 0,
|
|
12
|
+
reviewRequired,
|
|
13
|
+
warningCodes,
|
|
14
|
+
environmentChangeRequiresIntent: true,
|
|
15
|
+
globalEnvironmentChangesRequireUserApproval: true,
|
|
16
|
+
pendingIntentCount: intents.length,
|
|
17
|
+
mustNotDo: [
|
|
18
|
+
"do not change global runtimes without user approval",
|
|
19
|
+
"do not install or remove global package managers without user approval",
|
|
20
|
+
"do not change Docker daemon/context assumptions without user approval",
|
|
21
|
+
"do not ignore open intents or review-required warnings"
|
|
22
|
+
],
|
|
23
|
+
recommendedNextActions: reviewRequired
|
|
24
|
+
? ["review warnings and open intents", "ask the user before environment changes", "record intent before changes"]
|
|
25
|
+
: ["continue with project-local work", "run aienvmp intent before environment changes"],
|
|
26
|
+
requiredCommands: {
|
|
27
|
+
beforeEnvironmentChange: "aienvmp intent --actor agent:id --action planned-change --target <runtime|package-manager|docker>",
|
|
28
|
+
refreshAfterChange: "aienvmp sync",
|
|
29
|
+
recordAfterChange: "aienvmp record --actor agent:id --summary what-changed",
|
|
30
|
+
handoff: "aienvmp handoff --record --actor agent:id",
|
|
31
|
+
reviewPlan: "aienvmp plan"
|
|
32
|
+
},
|
|
33
|
+
nextCommand: reviewRequired ? "aienvmp plan" : "continue project-local work; use aienvmp intent before environment changes"
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { exists, readJson } from "./fsutil.js";
|
|
4
|
+
|
|
5
|
+
const NODE_GROUPS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
6
|
+
|
|
7
|
+
export async function scanDependencySnapshot(dir) {
|
|
8
|
+
const node = await scanNodeDependencies(dir);
|
|
9
|
+
const python = await scanPythonDependencies(dir);
|
|
10
|
+
const packages = [...node.packages, ...python.packages];
|
|
11
|
+
const manifests = [...node.manifests, ...python.manifests];
|
|
12
|
+
return {
|
|
13
|
+
mode: "snapshot",
|
|
14
|
+
enabled: true,
|
|
15
|
+
note: "Read-only dependency snapshot from project files. It does not install, update, or resolve packages.",
|
|
16
|
+
manifests,
|
|
17
|
+
summary: {
|
|
18
|
+
ecosystems: [...new Set(packages.map((pkg) => pkg.ecosystem))],
|
|
19
|
+
manifests: manifests.length,
|
|
20
|
+
packages: packages.length
|
|
21
|
+
},
|
|
22
|
+
packages: packages.slice(0, 80)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function scanNodeDependencies(dir) {
|
|
27
|
+
const file = path.join(dir, "package.json");
|
|
28
|
+
if (!(await exists(file))) return { manifests: [], packages: [] };
|
|
29
|
+
const json = await readJson(file, {});
|
|
30
|
+
const packages = [];
|
|
31
|
+
for (const group of NODE_GROUPS) {
|
|
32
|
+
for (const [name, version] of Object.entries(json[group] || {})) {
|
|
33
|
+
packages.push({
|
|
34
|
+
ecosystem: "npm",
|
|
35
|
+
manager: "npm",
|
|
36
|
+
manifest: "package.json",
|
|
37
|
+
group,
|
|
38
|
+
name,
|
|
39
|
+
version: String(version)
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { manifests: ["package.json"], packages };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function scanPythonDependencies(dir) {
|
|
47
|
+
const requirements = await scanRequirementsTxt(dir);
|
|
48
|
+
const pyproject = await scanPyproject(dir);
|
|
49
|
+
return {
|
|
50
|
+
manifests: [...requirements.manifests, ...pyproject.manifests],
|
|
51
|
+
packages: [...requirements.packages, ...pyproject.packages]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function scanRequirementsTxt(dir) {
|
|
56
|
+
const file = path.join(dir, "requirements.txt");
|
|
57
|
+
if (!(await exists(file))) return { manifests: [], packages: [] };
|
|
58
|
+
const raw = await fs.readFile(file, "utf8");
|
|
59
|
+
const packages = raw
|
|
60
|
+
.split(/\r?\n/)
|
|
61
|
+
.map((line) => line.trim())
|
|
62
|
+
.filter((line) => line && !line.startsWith("#") && !line.startsWith("-"))
|
|
63
|
+
.slice(0, 80)
|
|
64
|
+
.map((line) => {
|
|
65
|
+
const parsed = parseRequirementLine(line);
|
|
66
|
+
return {
|
|
67
|
+
ecosystem: "python",
|
|
68
|
+
manager: "pip",
|
|
69
|
+
manifest: "requirements.txt",
|
|
70
|
+
group: "requirements",
|
|
71
|
+
name: parsed.name,
|
|
72
|
+
version: parsed.version
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
return { manifests: ["requirements.txt"], packages };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function scanPyproject(dir) {
|
|
79
|
+
const file = path.join(dir, "pyproject.toml");
|
|
80
|
+
if (!(await exists(file))) return { manifests: [], packages: [] };
|
|
81
|
+
const raw = await fs.readFile(file, "utf8");
|
|
82
|
+
const packages = parsePyprojectDependencies(raw).slice(0, 80).map((line) => {
|
|
83
|
+
const parsed = parseRequirementLine(line);
|
|
84
|
+
return {
|
|
85
|
+
ecosystem: "python",
|
|
86
|
+
manager: "pyproject",
|
|
87
|
+
manifest: "pyproject.toml",
|
|
88
|
+
group: "project.dependencies",
|
|
89
|
+
name: parsed.name,
|
|
90
|
+
version: parsed.version
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
return { manifests: ["pyproject.toml"], packages };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parseRequirementLine(line) {
|
|
97
|
+
const cleaned = String(line).split("#")[0].trim();
|
|
98
|
+
const match = cleaned.match(/^([A-Za-z0-9_.-]+)\s*(.*)$/);
|
|
99
|
+
return {
|
|
100
|
+
name: match?.[1] || cleaned,
|
|
101
|
+
version: (match?.[2] || "unspecified").trim() || "unspecified"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function parsePyprojectDependencies(raw) {
|
|
106
|
+
const lines = [];
|
|
107
|
+
const match = String(raw).match(/dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
108
|
+
if (!match) return lines;
|
|
109
|
+
for (const item of match[1].split(/\r?\n/)) {
|
|
110
|
+
const cleaned = item.trim().replace(/,$/, "").replace(/^["']|["']$/g, "");
|
|
111
|
+
if (cleaned && !cleaned.startsWith("#")) lines.push(cleaned);
|
|
112
|
+
}
|
|
113
|
+
return lines;
|
|
114
|
+
}
|
package/src/manifest.js
CHANGED
|
@@ -6,6 +6,7 @@ import { exists } from "./fsutil.js";
|
|
|
6
6
|
import { observedTrust } from "./trust.js";
|
|
7
7
|
import { scanGlobalInventory } from "./inventory.js";
|
|
8
8
|
import { scanSecurity } from "./security.js";
|
|
9
|
+
import { scanDependencySnapshot } from "./dependencies.js";
|
|
9
10
|
|
|
10
11
|
export async function buildManifest(dir, options = {}) {
|
|
11
12
|
const now = new Date().toISOString();
|
|
@@ -27,6 +28,7 @@ export async function buildManifest(dir, options = {}) {
|
|
|
27
28
|
packageManagers: await scanPackageManagers(),
|
|
28
29
|
containers: await scanContainers(),
|
|
29
30
|
projectHints: await scanProjectHints(dir),
|
|
31
|
+
dependencySnapshot: await scanDependencySnapshot(dir),
|
|
30
32
|
inventory: await scanGlobalInventory({ deep: options.deep }),
|
|
31
33
|
security: await scanSecurity(dir, { security: options.security }),
|
|
32
34
|
agentFiles: await scanAgentFiles(dir),
|
|
@@ -84,6 +86,11 @@ export async function buildManifest(dir, options = {}) {
|
|
|
84
86
|
mode: "basic by default; vulnerability summaries only when requested",
|
|
85
87
|
npmAudit: "npm audit --json"
|
|
86
88
|
},
|
|
89
|
+
dependencySnapshot: {
|
|
90
|
+
mode: "read-only project file snapshot",
|
|
91
|
+
node: "package.json dependencies",
|
|
92
|
+
python: "requirements.txt and pyproject.toml dependencies"
|
|
93
|
+
},
|
|
87
94
|
projectHints: [
|
|
88
95
|
".nvmrc",
|
|
89
96
|
".python-version",
|
package/src/render.js
CHANGED
|
@@ -31,6 +31,8 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
31
31
|
pushMap(lines, "Containers", manifest.containers);
|
|
32
32
|
lines.push("## Global Tool Inventory", "");
|
|
33
33
|
lines.push(...inventoryLines(manifest.inventory), "");
|
|
34
|
+
lines.push("## Dependency Snapshot", "");
|
|
35
|
+
lines.push(...dependencyLines(manifest.dependencySnapshot), "");
|
|
34
36
|
lines.push("## Security Summary", "");
|
|
35
37
|
lines.push(...securityLines(manifest.security), "");
|
|
36
38
|
lines.push("## Project Requirements And Hints", "");
|
|
@@ -99,6 +101,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
99
101
|
`Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
100
102
|
`Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
|
|
101
103
|
`Inventory: ${manifest.inventory?.mode || "basic"}${manifest.inventory?.enabled ? " enabled" : " disabled"}`,
|
|
104
|
+
`Dependencies: ${manifest.dependencySnapshot?.summary?.packages || 0} packages across ${(manifest.dependencySnapshot?.summary?.ecosystems || []).join(", ") || "no ecosystems"}`,
|
|
102
105
|
`Security: ${manifest.security?.mode || "basic"}${manifest.security?.enabled ? ` enabled (${manifest.security.summary?.total || 0} vulnerabilities)` : " disabled"}`,
|
|
103
106
|
`Policy Node: ${policy.node || "not set"}`,
|
|
104
107
|
`Policy Python: ${policy.python || "not set"}`,
|
|
@@ -133,6 +136,7 @@ export function renderHandoff(handoff) {
|
|
|
133
136
|
"# AI Handoff",
|
|
134
137
|
"",
|
|
135
138
|
`Status: ${handoff.status}`,
|
|
139
|
+
`Decision: ${handoff.decision?.mode || handoff.status}`,
|
|
136
140
|
`Trust: ${handoff.trust?.state || "observed"} (not AI-verified)`,
|
|
137
141
|
`Schema: ${handoff.schemaVersion}`,
|
|
138
142
|
`Workspace: ${handoff.workspace?.path || "unknown"}`,
|
|
@@ -170,6 +174,7 @@ export function renderPlan(plan) {
|
|
|
170
174
|
"# AI Environment Plan",
|
|
171
175
|
"",
|
|
172
176
|
`Status: ${plan.status}`,
|
|
177
|
+
`Decision: ${plan.decision?.mode || plan.status}`,
|
|
173
178
|
`Generated: ${plan.generatedAt}`,
|
|
174
179
|
`Workspace: ${plan.workspace?.path || "unknown"}`,
|
|
175
180
|
"",
|
|
@@ -271,6 +276,10 @@ const rows=o=>entries(o).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${esc(
|
|
|
271
276
|
const inventoryGroups=manifest.inventory?.tools||{};
|
|
272
277
|
const inventoryCount=Object.values(inventoryGroups).reduce((sum,items)=>sum+(Array.isArray(items)?items.length:0),0);
|
|
273
278
|
const inventoryHtml=manifest.inventory?.enabled?('<table>'+Object.entries(inventoryGroups).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${Array.isArray(v)?v.length:0} tools</code></td></tr>\`).join('')+'</table>'):'<div class="okline">Deep global inventory is off. Run <code>aienvmp sync --deep</code> when an AI needs global tool awareness.</div>';
|
|
279
|
+
const deps=manifest.dependencySnapshot||{};
|
|
280
|
+
const depSummary=deps.summary||{ecosystems:[],manifests:0,packages:0};
|
|
281
|
+
const depPackages=deps.packages||[];
|
|
282
|
+
const depHtml=depPackages.length?'<table><tr><th>Packages</th><td><code>'+esc(depSummary.packages||0)+'</code></td></tr><tr><th>Ecosystems</th><td><code>'+esc((depSummary.ecosystems||[]).join(', ')||'none')+'</code></td></tr><tr><th>Manifests</th><td><code>'+esc((deps.manifests||[]).join(', ')||'none')+'</code></td></tr></table><div class="timeline">'+depPackages.slice(0,8).map(p=>\`<div class="event"><time>\${esc(p.ecosystem)}</time><div><b>\${esc(p.name)}</b> <code>\${esc(p.version)}</code><div class="path">\${esc(p.manifest)} / \${esc(p.group)}</div></div></div>\`).join('')+'</div>':'<div class="okline">No project dependency manifests detected.</div>';
|
|
274
283
|
const sec=manifest.security||{};
|
|
275
284
|
const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
|
|
276
285
|
const secPackages=sec.topPackages||[];
|
|
@@ -334,6 +343,7 @@ document.getElementById('app').innerHTML=\`
|
|
|
334
343
|
\${card('Containers',manifest.containers?.docker?'<span class="pill">available</span>':'<span class="pill off">not detected</span>',\`<table>\${rows(manifest.containers)}</table>\`)}
|
|
335
344
|
\${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
|
|
336
345
|
\${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
|
|
346
|
+
\${card('Dependency Snapshot','<span class="pill">'+(depSummary.packages||0)+' packages</span>',depHtml)}
|
|
337
347
|
\${card('Security Summary',sec.enabled?'<span class="pill warn">security</span>':'<span class="pill off">basic</span>',securityHtml)}
|
|
338
348
|
</div>
|
|
339
349
|
<aside>
|
|
@@ -414,6 +424,22 @@ function inventoryLines(inventory = {}) {
|
|
|
414
424
|
return lines;
|
|
415
425
|
}
|
|
416
426
|
|
|
427
|
+
function dependencyLines(snapshot = {}) {
|
|
428
|
+
const summary = snapshot.summary || {};
|
|
429
|
+
const packages = snapshot.packages || [];
|
|
430
|
+
const ecosystems = summary.ecosystems?.length ? summary.ecosystems.join(", ") : "none";
|
|
431
|
+
const lines = [
|
|
432
|
+
`- Mode: ${snapshot.mode || "snapshot"}`,
|
|
433
|
+
`- Manifests: ${(snapshot.manifests || []).join(", ") || "none"}`,
|
|
434
|
+
`- Ecosystems: ${ecosystems}`,
|
|
435
|
+
`- Packages: ${summary.packages || 0}`
|
|
436
|
+
];
|
|
437
|
+
for (const pkg of packages.slice(0, 10)) {
|
|
438
|
+
lines.push(`- ${pkg.ecosystem}/${pkg.name}: ${pkg.version} (${pkg.manifest})`);
|
|
439
|
+
}
|
|
440
|
+
return lines;
|
|
441
|
+
}
|
|
442
|
+
|
|
417
443
|
function securityLines(security = {}) {
|
|
418
444
|
if (!security.enabled) return ["- Mode: basic", "- Security scan is disabled. Run `aienvmp sync --security` when vulnerability context is needed."];
|
|
419
445
|
const summary = security.summary || {};
|