aienvmp 0.1.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/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +18 -0
- package/LICENSE +134 -0
- package/README.md +298 -0
- package/ROADMAP.md +44 -0
- package/SECURITY.md +22 -0
- package/action.yml +42 -0
- package/bin/aienvmp.js +7 -0
- package/examples/github-action.yml +17 -0
- package/package.json +51 -0
- package/src/cli.js +81 -0
- package/src/commands/compile.js +41 -0
- package/src/commands/context.js +33 -0
- package/src/commands/dash.js +35 -0
- package/src/commands/diff.js +23 -0
- package/src/commands/doctor.js +34 -0
- package/src/commands/init.js +19 -0
- package/src/commands/intent.js +26 -0
- package/src/commands/record.js +26 -0
- package/src/commands/resolve.js +35 -0
- package/src/commands/scan.js +28 -0
- package/src/diff.js +23 -0
- package/src/doctor.js +38 -0
- package/src/fsutil.js +51 -0
- package/src/manifest.js +130 -0
- package/src/paths.js +33 -0
- package/src/policy.js +74 -0
- package/src/render.js +259 -0
- package/src/shell.js +33 -0
- package/src/timeline.js +46 -0
package/src/policy.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function loadPolicy(dir) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = await fs.readFile(path.join(dir, ".aienvmp", "policy.yml"), "utf8");
|
|
7
|
+
return parseSimplePolicy(raw);
|
|
8
|
+
} catch {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseSimplePolicy(raw) {
|
|
14
|
+
const policy = {};
|
|
15
|
+
for (const line of String(raw).split(/\r?\n/)) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
18
|
+
const match = trimmed.match(/^([A-Za-z][\w-]*)\s*:\s*(.+)$/);
|
|
19
|
+
if (!match) continue;
|
|
20
|
+
policy[normalizeKey(match[1])] = unquote(match[2].trim());
|
|
21
|
+
}
|
|
22
|
+
return policy;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function policyWarnings(manifest, policy = {}) {
|
|
26
|
+
const warnings = [];
|
|
27
|
+
if (policy.node) {
|
|
28
|
+
compareVersionPolicy(warnings, "node", policy.node, manifest.runtimes?.node, ".aienvmp/policy.yml");
|
|
29
|
+
compareVersionPolicy(warnings, ".nvmrc", policy.node, manifest.projectHints?.nvmrc, ".aienvmp/policy.yml");
|
|
30
|
+
}
|
|
31
|
+
if (policy.python) {
|
|
32
|
+
const py = manifest.runtimes?.python || manifest.runtimes?.python3;
|
|
33
|
+
compareVersionPolicy(warnings, "python", policy.python, py, ".aienvmp/policy.yml");
|
|
34
|
+
compareVersionPolicy(warnings, ".python-version", policy.python, manifest.projectHints?.pythonVersion, ".aienvmp/policy.yml");
|
|
35
|
+
}
|
|
36
|
+
if (policy.packageManager) {
|
|
37
|
+
const locks = lockManagers(manifest.projectHints || {});
|
|
38
|
+
if (locks.length && !locks.includes(policy.packageManager)) {
|
|
39
|
+
warnings.push({
|
|
40
|
+
code: "package-manager-policy-mismatch",
|
|
41
|
+
message: `Policy requires ${policy.packageManager}, but detected lockfile(s) for ${locks.join(", ")}.`
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return warnings;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compareVersionPolicy(warnings, target, expected, actual, source) {
|
|
49
|
+
if (!actual) return;
|
|
50
|
+
const expectedVersion = String(expected).replace(/^v/, "");
|
|
51
|
+
const actualVersion = String(actual).replace(/^v/, "");
|
|
52
|
+
if (!actualVersion.startsWith(expectedVersion)) {
|
|
53
|
+
warnings.push({
|
|
54
|
+
code: "policy-version-mismatch",
|
|
55
|
+
message: `${source} requires ${target} ${expectedVersion}, but detected ${actualVersion}.`
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function lockManagers(hints) {
|
|
61
|
+
const managers = [];
|
|
62
|
+
if (hints.packageLock) managers.push("npm");
|
|
63
|
+
if (hints.pnpmLock) managers.push("pnpm");
|
|
64
|
+
if (hints.yarnLock) managers.push("yarn");
|
|
65
|
+
return managers;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeKey(key) {
|
|
69
|
+
return key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function unquote(value) {
|
|
73
|
+
return value.replace(/^["']|["']$/g, "");
|
|
74
|
+
}
|
package/src/render.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
const markerBegin = "<!-- aienvmp:begin -->";
|
|
2
|
+
const markerEnd = "<!-- aienvmp:end -->";
|
|
3
|
+
|
|
4
|
+
export { markerBegin, markerEnd };
|
|
5
|
+
|
|
6
|
+
export function renderAIEnv(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
lines.push("# AI Environment Protocol", "");
|
|
9
|
+
lines.push("This workspace uses `aienvmp` as the shared environment source of truth for humans and AI agents.", "");
|
|
10
|
+
lines.push("## Read Me First", "");
|
|
11
|
+
lines.push("Before changing runtimes, package managers, Docker settings, or global packages:");
|
|
12
|
+
lines.push("1. Run `aienvmp context`.");
|
|
13
|
+
lines.push("2. Prefer project-local version files such as `.nvmrc`, `.python-version`, `mise.toml`, and `.tool-versions`.");
|
|
14
|
+
lines.push("3. Ask the user before changing global environment state.");
|
|
15
|
+
lines.push("4. Record planned environment changes with `aienvmp intent --actor <agent:id> --action <planned-change>`.");
|
|
16
|
+
lines.push("5. After environment changes, run `aienvmp scan && aienvmp compile`.");
|
|
17
|
+
lines.push("6. Record what changed with `aienvmp record --actor <agent:id> --summary <what-changed>`.", "");
|
|
18
|
+
lines.push("## Current Policy", "");
|
|
19
|
+
lines.push(...policyLines(policy));
|
|
20
|
+
lines.push("- Enforcement: non-blocking by default; warnings require review but do not lock the machine.");
|
|
21
|
+
lines.push("- Project-local dependency installs: allowed when required by the user task.", "");
|
|
22
|
+
lines.push("## AI Preflight Summary", "");
|
|
23
|
+
lines.push(...contextLines(manifest, warnings, intents), "");
|
|
24
|
+
lines.push("## Runtime Map", "");
|
|
25
|
+
pushMap(lines, "Runtimes", manifest.runtimes);
|
|
26
|
+
pushMap(lines, "Package Managers", manifest.packageManagers);
|
|
27
|
+
pushMap(lines, "Containers", manifest.containers);
|
|
28
|
+
lines.push("## Project Requirements And Hints", "");
|
|
29
|
+
pushMap(lines, "Detected", manifest.projectHints);
|
|
30
|
+
lines.push("## Drift And Warnings", "");
|
|
31
|
+
if (warnings.length) {
|
|
32
|
+
for (const warning of warnings) lines.push(`- ${warning.message}`);
|
|
33
|
+
} else {
|
|
34
|
+
lines.push("- No blocking environment warnings detected.");
|
|
35
|
+
}
|
|
36
|
+
lines.push("", "## Pending Agent Intents", "");
|
|
37
|
+
if (intents.length) {
|
|
38
|
+
for (const intent of intents.slice(-8).reverse()) {
|
|
39
|
+
lines.push(`- ${intent.at}: ${intent.actor} plans ${intent.action}${intent.target ? ` (${intent.target})` : ""}`);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
lines.push("- No pending agent intents recorded.");
|
|
43
|
+
}
|
|
44
|
+
lines.push("", "## Environment Ledger", "");
|
|
45
|
+
if (timeline.length) {
|
|
46
|
+
for (const item of timeline.slice(-8).reverse()) {
|
|
47
|
+
lines.push(`- ${item.at}: ${formatTimeline(item)}`);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
lines.push("- No previous environment changes recorded.");
|
|
51
|
+
}
|
|
52
|
+
lines.push("", "## Snapshot", "");
|
|
53
|
+
lines.push(`- Generated: ${manifest.generatedAt}`);
|
|
54
|
+
lines.push(`- Workspace: ${manifest.workspace.path}`);
|
|
55
|
+
lines.push(`- OS: ${manifest.os.platform} ${manifest.os.release} ${manifest.os.arch}`);
|
|
56
|
+
lines.push("");
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function renderAgentBlock(manifest) {
|
|
61
|
+
const node = manifest.runtimes.node || "not detected";
|
|
62
|
+
const python = manifest.runtimes.python || manifest.runtimes.python3 || "not detected";
|
|
63
|
+
const docker = manifest.containers.docker ? "available" : "not detected";
|
|
64
|
+
return `AI environment map is maintained by aienvmp.
|
|
65
|
+
|
|
66
|
+
Before changing runtimes, package managers, Docker, or global packages:
|
|
67
|
+
1. Read AIENV.md.
|
|
68
|
+
2. Run \`aienvmp context\`.
|
|
69
|
+
3. Ask the user before global environment changes.
|
|
70
|
+
4. Record planned changes with \`aienvmp intent\`.
|
|
71
|
+
5. After changes, run \`aienvmp scan && aienvmp compile\`.
|
|
72
|
+
6. Record what changed with \`aienvmp record\`.
|
|
73
|
+
|
|
74
|
+
Default detected tools:
|
|
75
|
+
- Node.js: ${node}
|
|
76
|
+
- Python: ${python}
|
|
77
|
+
- Docker: ${docker}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function renderContext(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
|
|
81
|
+
return [
|
|
82
|
+
"# AI Preflight Context",
|
|
83
|
+
"",
|
|
84
|
+
`Status: ${warnings.length ? "review-required" : "clear"}`,
|
|
85
|
+
`Workspace: ${manifest.workspace.path}`,
|
|
86
|
+
`Node: ${manifest.runtimes.node || "not detected"}`,
|
|
87
|
+
`Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
88
|
+
`Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
|
|
89
|
+
`Policy Node: ${policy.node || "not set"}`,
|
|
90
|
+
`Policy Python: ${policy.python || "not set"}`,
|
|
91
|
+
`Policy Package Manager: ${policy.packageManager || "not set"}`,
|
|
92
|
+
"",
|
|
93
|
+
"Must follow:",
|
|
94
|
+
"- Ask the user before global runtime, package manager, Docker, or global package changes.",
|
|
95
|
+
"- Treat policy mismatches as review-required, not as permission to break ongoing operations.",
|
|
96
|
+
"- Prefer project-local version files and local environments.",
|
|
97
|
+
"- Before planned env changes, run `aienvmp intent --actor <agent:id> --action <planned-change>`.",
|
|
98
|
+
"- After env changes, run `aienvmp scan && aienvmp compile`.",
|
|
99
|
+
"- Then run `aienvmp record --actor <agent:id> --summary <what-changed>`.",
|
|
100
|
+
"",
|
|
101
|
+
"Warnings:",
|
|
102
|
+
...(warnings.length ? warnings.map((w) => `- ${w.message}`) : ["- none"]),
|
|
103
|
+
"",
|
|
104
|
+
"Open intents:",
|
|
105
|
+
...(intents.length ? intents.slice(-5).reverse().map((i) => `- ${i.actor}: ${i.action}`) : ["- none"]),
|
|
106
|
+
"",
|
|
107
|
+
"Recent ledger:",
|
|
108
|
+
...(timeline.length ? timeline.slice(-5).reverse().map((t) => `- ${formatTimeline(t)}`) : ["- none"]),
|
|
109
|
+
""
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function renderDashboard(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
|
|
114
|
+
const data = JSON.stringify({ manifest, timeline, warnings, intents, policy });
|
|
115
|
+
return `<!doctype html>
|
|
116
|
+
<html lang="en">
|
|
117
|
+
<head>
|
|
118
|
+
<meta charset="utf-8">
|
|
119
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
120
|
+
<title>aienvmp dashboard</title>
|
|
121
|
+
<style>
|
|
122
|
+
:root{color-scheme:dark;--bg:#08110f;--panel:#0d1815;--panel2:#101e1a;--line:#214138;--line2:#172b26;--text:#eefcf5;--muted:#91aa9d;--green:#47e58d;--green2:#133d2a;--amber:#f4bf5f;--red:#ff6b6b;--code:#d7ffe9}
|
|
123
|
+
*{box-sizing:border-box}
|
|
124
|
+
body{margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;background:var(--bg);color:var(--text)}
|
|
125
|
+
body:before{content:"";position:fixed;inset:0;pointer-events:none;background:linear-gradient(180deg,rgba(71,229,141,.12),transparent 38%),radial-gradient(circle at 74% 0,rgba(244,191,95,.08),transparent 28%)}
|
|
126
|
+
.shell{position:relative;max-width:1180px;margin:0 auto;padding:26px 22px 36px}
|
|
127
|
+
header{border:1px solid var(--line);background:linear-gradient(135deg,rgba(16,30,26,.96),rgba(8,17,15,.94));border-radius:8px;padding:22px;display:grid;grid-template-columns:1fr auto;gap:18px;align-items:start}
|
|
128
|
+
.eyebrow{color:var(--green);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
|
|
129
|
+
h1,h2,h3,p{margin:0}h1{font-size:clamp(28px,4vw,46px);line-height:1.02;margin-top:8px;letter-spacing:0}h2{font-size:17px;letter-spacing:0}h3{font-size:13px;color:var(--muted);font-weight:600;letter-spacing:0}
|
|
130
|
+
.sub{color:var(--muted);margin-top:12px;max-width:680px;line-height:1.55}
|
|
131
|
+
.stamp{min-width:220px;border:1px solid var(--line2);background:#091310;border-radius:8px;padding:14px}
|
|
132
|
+
.stamp b{display:block;color:var(--green);font-size:24px;margin-bottom:3px}.stamp span{display:block;color:var(--muted);font-size:12px;overflow-wrap:anywhere}
|
|
133
|
+
.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:14px 0 18px}
|
|
134
|
+
.metric,.card{border:1px solid var(--line);background:rgba(13,24,21,.9);border-radius:8px}
|
|
135
|
+
.metric{padding:14px}.metric .num{font-size:28px;font-weight:800;color:var(--green);line-height:1}.metric .label{margin-top:7px;color:var(--muted);font-size:12px}
|
|
136
|
+
.layout{display:grid;grid-template-columns:1.35fr .9fr;gap:14px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
|
137
|
+
.card{padding:16px;min-width:0}.card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}
|
|
138
|
+
.pill{display:inline-flex;align-items:center;border:1px solid var(--line);background:var(--green2);color:var(--green);border-radius:999px;padding:4px 9px;font-size:12px;font-weight:700}
|
|
139
|
+
.pill.warn{background:rgba(244,191,95,.12);border-color:rgba(244,191,95,.35);color:var(--amber)}
|
|
140
|
+
.pill.off{background:#1c2421;color:var(--muted)}
|
|
141
|
+
table{width:100%;border-collapse:collapse}td,th{border-top:1px solid var(--line2);padding:10px 0;text-align:left;vertical-align:top}th{width:42%;color:var(--muted);font-weight:600}td{color:var(--text);overflow-wrap:anywhere}
|
|
142
|
+
code{color:var(--code);background:#0a2017;border:1px solid #17462f;padding:2px 6px;border-radius:5px}
|
|
143
|
+
.warnings{display:grid;gap:9px}.warning{border:1px solid rgba(244,191,95,.35);background:rgba(244,191,95,.08);border-radius:8px;padding:11px;color:#ffe3a9}
|
|
144
|
+
.okline{border:1px solid rgba(71,229,141,.32);background:rgba(71,229,141,.08);border-radius:8px;padding:12px;color:var(--green)}
|
|
145
|
+
.agents{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.agent{border:1px solid var(--line2);border-radius:8px;padding:10px;background:#0a1412}.agent strong{display:block}.agent span{color:var(--muted);font-size:12px}
|
|
146
|
+
.timeline{display:grid;gap:10px}.event{display:grid;grid-template-columns:108px 1fr;gap:12px;border-top:1px solid var(--line2);padding-top:10px}.event time{color:var(--muted);font-size:12px}.event b{color:var(--green)}
|
|
147
|
+
.path{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted);font-size:12px;overflow-wrap:anywhere}
|
|
148
|
+
@media (max-width:860px){header,.layout{grid-template-columns:1fr}.metrics{grid-template-columns:repeat(2,1fr)}.grid{grid-template-columns:1fr}.agents{grid-template-columns:1fr}}
|
|
149
|
+
@media (max-width:520px){.shell{padding:14px}.metrics{grid-template-columns:1fr}.event{grid-template-columns:1fr}h1{font-size:32px}}
|
|
150
|
+
</style>
|
|
151
|
+
</head>
|
|
152
|
+
<body>
|
|
153
|
+
<main class="shell" id="app"></main>
|
|
154
|
+
<script type="application/json" id="data">${escapeHtml(data)}</script>
|
|
155
|
+
<script>
|
|
156
|
+
const {manifest,timeline,warnings,intents,policy}=JSON.parse(document.getElementById('data').textContent);
|
|
157
|
+
function esc(s){return String(s).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"')}
|
|
158
|
+
const entries=o=>Object.entries(o||{});
|
|
159
|
+
const rows=o=>entries(o).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${esc(String(v))}</code></td></tr>\`).join('')||'<tr><td colspan="2">None detected</td></tr>';
|
|
160
|
+
const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
|
|
161
|
+
const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
|
|
162
|
+
const totalTools=entries(manifest.runtimes).length+entries(manifest.packageManagers).length+entries(manifest.containers).length;
|
|
163
|
+
const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
|
|
164
|
+
const agentCards=Object.entries(agentNames).map(([key,label])=>\`<div class="agent"><strong>\${label}</strong><span>\${manifest.agentFiles?.[key]?'connected':'not connected'}</span></div>\`).join('');
|
|
165
|
+
const warnHtml=warnings.length?'<div class="warnings">'+warnings.map(w=>\`<div class="warning">\${esc(w.message)}</div>\`).join('')+'</div>':'<div class="okline">No blocking environment warnings detected.</div>';
|
|
166
|
+
const timelineHtml=timeline.length?'<div class="timeline">'+timeline.slice(-8).reverse().map(t=>\`<div class="event"><time>\${esc(t.at.replace('T',' ').slice(0,16))}</time><div><b>\${esc(t.actor||'system')}</b> \${esc(timelineLabel(t))}</div></div>\`).join('')+'</div>':'<div class="okline">No previous environment changes recorded.</div>';
|
|
167
|
+
const intentsHtml=intents.length?'<div class="timeline">'+intents.slice(-6).reverse().map(i=>\`<div class="event"><time>\${esc(i.at.replace('T',' ').slice(0,16))}</time><div><b>\${esc(i.actor)}</b> plans \${esc(i.action)}</div></div>\`).join('')+'</div>':'<div class="okline">No pending agent intents recorded.</div>';
|
|
168
|
+
const policyHtml=entries(policy).length?\`<table>\${rows(policy)}</table>\`:'<div class="okline">No explicit version policy set.</div>';
|
|
169
|
+
const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
|
|
170
|
+
document.getElementById('app').innerHTML=\`
|
|
171
|
+
<header>
|
|
172
|
+
<div>
|
|
173
|
+
<div class="eyebrow">aienvmp dashboard</div>
|
|
174
|
+
<h1>AI environment map</h1>
|
|
175
|
+
<p class="sub">An AI-first environment map and change ledger for agents that share one development machine.</p>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="stamp"><b>\${warnings.length?'review':'clear'}</b><span>\${esc(manifest.workspace.name)}</span><span>\${esc(manifest.generatedAt)}</span></div>
|
|
178
|
+
</header>
|
|
179
|
+
<section class="metrics">
|
|
180
|
+
<div class="metric"><div class="num">\${entries(manifest.runtimes).length}</div><div class="label">runtimes</div></div>
|
|
181
|
+
<div class="metric"><div class="num">\${entries(manifest.packageManagers).length}</div><div class="label">package managers</div></div>
|
|
182
|
+
<div class="metric"><div class="num">\${warnings.length}</div><div class="label">warnings</div></div>
|
|
183
|
+
<div class="metric"><div class="num">\${intents.length}</div><div class="label">open intents</div></div>
|
|
184
|
+
</section>
|
|
185
|
+
<section class="layout">
|
|
186
|
+
<div class="grid">
|
|
187
|
+
\${card('Runtimes',\`<span class="pill">\${entries(manifest.runtimes).length} found</span>\`,\`<table>\${rows(manifest.runtimes)}</table>\`)}
|
|
188
|
+
\${card('Package Managers',\`<span class="pill">\${entries(manifest.packageManagers).length} found</span>\`,\`<table>\${rows(manifest.packageManagers)}</table>\`)}
|
|
189
|
+
\${card('Containers',manifest.containers?.docker?'<span class="pill">available</span>':'<span class="pill off">not detected</span>',\`<table>\${rows(manifest.containers)}</table>\`)}
|
|
190
|
+
\${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
|
|
191
|
+
</div>
|
|
192
|
+
<aside>
|
|
193
|
+
\${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
|
|
194
|
+
<div style="height:14px"></div>
|
|
195
|
+
\${card('Version Policy','<span class="pill">'+entries(policy).length+' rules</span>',policyHtml)}
|
|
196
|
+
<div style="height:14px"></div>
|
|
197
|
+
\${card('Agent Intents','<span class="pill">'+intents.length+' open</span>',intentsHtml)}
|
|
198
|
+
<div style="height:14px"></div>
|
|
199
|
+
\${card('Agent Integration','<span class="pill">'+totalTools+' tools</span>','<div class="agents">'+agentCards+'</div>')}
|
|
200
|
+
<div style="height:14px"></div>
|
|
201
|
+
\${card('Snapshot','',\`<table><tr><th>OS</th><td>\${esc(manifest.os.platform)} \${esc(manifest.os.release)} \${esc(manifest.os.arch)}</td></tr><tr><th>Shell</th><td>\${esc(manifest.os.shell||'unknown')}</td></tr><tr><th>Workspace</th><td><div class="path">\${esc(manifest.workspace.path)}</div></td></tr></table>\`)}
|
|
202
|
+
</aside>
|
|
203
|
+
</section>
|
|
204
|
+
<section style="margin-top:14px">\${card('Environment Ledger','',timelineHtml)}</section>
|
|
205
|
+
\`;
|
|
206
|
+
</script>
|
|
207
|
+
</main>
|
|
208
|
+
</body>
|
|
209
|
+
</html>`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function pushMap(lines, title, obj = {}) {
|
|
213
|
+
lines.push(`### ${title}`, "");
|
|
214
|
+
const entries = Object.entries(obj);
|
|
215
|
+
if (!entries.length) {
|
|
216
|
+
lines.push("- None detected.", "");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
for (const [key, value] of entries) lines.push(`- ${key}: ${value}`);
|
|
220
|
+
lines.push("");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function formatChange(change) {
|
|
224
|
+
if (change.type === "changed") return `${change.scope} ${change.key} changed ${change.before} -> ${change.after}`;
|
|
225
|
+
return `${change.scope} ${change.key} ${change.type} ${change.after ?? change.before}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function formatTimeline(item) {
|
|
229
|
+
if (item.change) return `${item.actor || "system"}: ${formatChange(item.change)}`;
|
|
230
|
+
const details = [item.target, item.before && item.after ? `${item.before} -> ${item.after}` : "", item.evidence ? `evidence: ${item.evidence}` : ""]
|
|
231
|
+
.filter(Boolean)
|
|
232
|
+
.join("; ");
|
|
233
|
+
return `${item.actor || "unknown"}: ${item.summary || item.action || item.type}${details ? ` (${details})` : ""}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function contextLines(manifest, warnings, intents) {
|
|
237
|
+
return [
|
|
238
|
+
`- Status: ${warnings.length ? "review-required" : "clear"}`,
|
|
239
|
+
`- Node: ${manifest.runtimes.node || "not detected"}`,
|
|
240
|
+
`- Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
241
|
+
`- Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
|
|
242
|
+
`- Open intents: ${intents.length}`
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function policyLines(policy) {
|
|
247
|
+
const lines = [];
|
|
248
|
+
if (policy.node) lines.push(`- Node version policy: ${policy.node}`);
|
|
249
|
+
if (policy.python) lines.push(`- Python version policy: ${policy.python}`);
|
|
250
|
+
if (policy.packageManager) lines.push(`- Package manager policy: ${policy.packageManager}`);
|
|
251
|
+
lines.push(`- Global installs: ${policy.globalInstalls || "ask-first"}`);
|
|
252
|
+
lines.push(`- Runtime changes: ${policy.runtimeChanges || "ask-first"}`);
|
|
253
|
+
lines.push("- Docker daemon/context changes: ask first.");
|
|
254
|
+
return lines;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function escapeHtml(value) {
|
|
258
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
259
|
+
}
|
package/src/shell.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
export async function commandVersion(command, args = ["--version"]) {
|
|
7
|
+
try {
|
|
8
|
+
const { stdout, stderr } = await execFileAsync(command, args, {
|
|
9
|
+
timeout: 2500,
|
|
10
|
+
windowsHide: true
|
|
11
|
+
});
|
|
12
|
+
return firstVersion(`${stdout}\n${stderr}`);
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function commandOutput(command, args = []) {
|
|
19
|
+
try {
|
|
20
|
+
const { stdout } = await execFileAsync(command, args, {
|
|
21
|
+
timeout: 2500,
|
|
22
|
+
windowsHide: true
|
|
23
|
+
});
|
|
24
|
+
return stdout.trim();
|
|
25
|
+
} catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function firstVersion(text) {
|
|
31
|
+
const match = String(text).match(/(?:v)?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
|
|
32
|
+
return match ? match[1] : null;
|
|
33
|
+
}
|
package/src/timeline.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
export async function readTimeline(file) {
|
|
4
|
+
return readJsonl(file);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function readJsonl(file) {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await fs.readFile(file, "utf8");
|
|
10
|
+
return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function openIntents(events = []) {
|
|
17
|
+
const byID = new Map();
|
|
18
|
+
for (const event of events) {
|
|
19
|
+
if (event.type === "intent-resolved") {
|
|
20
|
+
const key = event.ref || event.id;
|
|
21
|
+
if (key && byID.has(key)) {
|
|
22
|
+
const current = byID.get(key);
|
|
23
|
+
current.status = event.status || "resolved";
|
|
24
|
+
current.resolvedAt = event.at;
|
|
25
|
+
current.resolvedBy = event.actor;
|
|
26
|
+
current.resolution = event.reason || "";
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (event.type === "intent" || !event.type) {
|
|
31
|
+
const id = event.id || intentID(event);
|
|
32
|
+
byID.set(id, { ...event, id, type: "intent", status: event.status || "open" });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [...byID.values()].filter((intent) => intent.status === "open");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function intentID(intent) {
|
|
39
|
+
return `${intent.at || ""}:${intent.actor || ""}:${intent.action || ""}:${intent.target || ""}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function newIntentID(now = new Date()) {
|
|
43
|
+
const time = now.getTime().toString(36);
|
|
44
|
+
const entropy = Math.random().toString(36).slice(2, 8);
|
|
45
|
+
return `int_${time}_${entropy}`;
|
|
46
|
+
}
|