aienvmp 0.1.5 → 0.1.6
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 +6 -0
- package/README.md +6 -0
- package/ROADMAP.md +2 -3
- package/package.json +1 -1
- package/src/cli.js +2 -2
- package/src/commands/context.js +10 -0
- package/src/commands/handoff.js +10 -0
- package/src/commands/scan.js +1 -1
- package/src/inventory.js +88 -0
- package/src/manifest.js +11 -2
- package/src/render.js +19 -0
- package/src/shell.js +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
- Added optional `sync --deep` / `scan --deep` read-only global tool inventory.
|
|
6
|
+
- Kept the default scan lightweight while exposing deep inventory summaries to `AIENV.md`, `context`, `handoff`, and the dashboard.
|
|
7
|
+
- Added parsers for npm global packages, pipx tools, uv tools, and Homebrew package versions.
|
|
8
|
+
|
|
3
9
|
## 0.1.5
|
|
4
10
|
|
|
5
11
|
- Added machine-readable trust states for observed, planned, changed, review, verified, and stale environment facts.
|
package/README.md
CHANGED
package/ROADMAP.md
CHANGED
|
@@ -17,9 +17,8 @@
|
|
|
17
17
|
- pyenv, uv, conda
|
|
18
18
|
- mise, asdf
|
|
19
19
|
- Global tool inventory:
|
|
20
|
-
- `npm -g`
|
|
21
|
-
- `
|
|
22
|
-
- `uv tool list`
|
|
20
|
+
- richer summaries for `npm -g`, `pipx list`, `uv tool list`, and Homebrew
|
|
21
|
+
- optional `--deep` scanners for more toolchains
|
|
23
22
|
- Conflict detection:
|
|
24
23
|
- stale unresolved intents
|
|
25
24
|
- recent runtime changes without a fresh handoff
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -74,7 +74,7 @@ function printUsage() {
|
|
|
74
74
|
console.log(`aienvmp - AI-first env map + lightweight runtime SBOM
|
|
75
75
|
|
|
76
76
|
Usage:
|
|
77
|
-
aienvmp sync [--dir .] [--json] [--quiet]
|
|
77
|
+
aienvmp sync [--dir .] [--json] [--quiet] [--deep]
|
|
78
78
|
aienvmp context [--dir .] [--json]
|
|
79
79
|
aienvmp handoff [--dir .] [--json]
|
|
80
80
|
|
|
@@ -87,7 +87,7 @@ Common:
|
|
|
87
87
|
|
|
88
88
|
Advanced:
|
|
89
89
|
aienvmp init [--dir .]
|
|
90
|
-
aienvmp scan [--dir .]
|
|
90
|
+
aienvmp scan [--dir .] [--deep]
|
|
91
91
|
aienvmp intent [--dir .] --actor agent:codex --action "install pnpm"
|
|
92
92
|
aienvmp resolve [--dir .] --actor human:you --id <intent-id> [--status resolved|cancelled]
|
|
93
93
|
aienvmp record [--dir .] --actor agent:codex --summary "updated .nvmrc" [--target node] [--before 20] [--after 24]
|
package/src/commands/context.js
CHANGED
|
@@ -24,6 +24,7 @@ export async function contextWorkspace(args) {
|
|
|
24
24
|
runtimes: manifest.runtimes,
|
|
25
25
|
packageManagers: manifest.packageManagers,
|
|
26
26
|
containers: manifest.containers,
|
|
27
|
+
inventory: inventorySummary(manifest.inventory),
|
|
27
28
|
projectHints: manifest.projectHints,
|
|
28
29
|
warnings,
|
|
29
30
|
policy,
|
|
@@ -36,6 +37,15 @@ export async function contextWorkspace(args) {
|
|
|
36
37
|
console.log(renderContext(manifest, timeline, warnings, intents, policy));
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function inventorySummary(inventory = {}) {
|
|
41
|
+
const tools = inventory.tools || {};
|
|
42
|
+
return {
|
|
43
|
+
mode: inventory.mode || "basic",
|
|
44
|
+
enabled: inventory.enabled === true,
|
|
45
|
+
groups: Object.fromEntries(Object.entries(tools).map(([name, items]) => [name, items.length]))
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
function contextDecision(warnings, intents) {
|
|
40
50
|
const reviewRequired = warnings.length > 0 || intents.length > 0;
|
|
41
51
|
return {
|
package/src/commands/handoff.js
CHANGED
|
@@ -35,6 +35,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
35
35
|
python: manifest.runtimes?.python || manifest.runtimes?.python3 || "not detected",
|
|
36
36
|
docker: manifest.containers?.docker ? "available" : "not detected"
|
|
37
37
|
},
|
|
38
|
+
inventory: inventorySummary(manifest.inventory),
|
|
38
39
|
policy: {
|
|
39
40
|
node: policy.node || "not set",
|
|
40
41
|
python: policy.python || "not set",
|
|
@@ -55,3 +56,12 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
55
56
|
: "continue with project-local work; record intent before environment changes"
|
|
56
57
|
};
|
|
57
58
|
}
|
|
59
|
+
|
|
60
|
+
function inventorySummary(inventory = {}) {
|
|
61
|
+
const tools = inventory.tools || {};
|
|
62
|
+
return {
|
|
63
|
+
mode: inventory.mode || "basic",
|
|
64
|
+
enabled: inventory.enabled === true,
|
|
65
|
+
groups: Object.fromEntries(Object.entries(tools).map(([name, items]) => [name, items.length]))
|
|
66
|
+
};
|
|
67
|
+
}
|
package/src/commands/scan.js
CHANGED
|
@@ -11,7 +11,7 @@ export async function scanWorkspace(args) {
|
|
|
11
11
|
if (previous && await exists(currentPath)) {
|
|
12
12
|
await fs.copyFile(currentPath, previousManifestPath(dir));
|
|
13
13
|
}
|
|
14
|
-
const manifest = await buildManifest(dir);
|
|
14
|
+
const manifest = await buildManifest(dir, { deep: args.deep });
|
|
15
15
|
await writeJson(currentPath, manifest);
|
|
16
16
|
const changes = diffManifests(previous, manifest);
|
|
17
17
|
for (const change of changes) {
|
package/src/inventory.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { commandOutput } from "./shell.js";
|
|
2
|
+
|
|
3
|
+
const MAX_ITEMS = 40;
|
|
4
|
+
|
|
5
|
+
export async function scanGlobalInventory(options = {}) {
|
|
6
|
+
if (!options.deep) {
|
|
7
|
+
return {
|
|
8
|
+
mode: "basic",
|
|
9
|
+
enabled: false,
|
|
10
|
+
note: "Run `aienvmp sync --deep` to collect read-only global tool inventory."
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
mode: "deep",
|
|
16
|
+
enabled: true,
|
|
17
|
+
note: "Read-only global tool inventory. Use for AI awareness, not enforcement.",
|
|
18
|
+
tools: compact({
|
|
19
|
+
npmGlobal: await scanNpmGlobal(),
|
|
20
|
+
pipx: await scanPipx(),
|
|
21
|
+
uvTools: await scanUvTools(),
|
|
22
|
+
brew: await scanBrew()
|
|
23
|
+
})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function scanNpmGlobal() {
|
|
28
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
29
|
+
const raw = await commandOutput(command, ["list", "-g", "--depth=0", "--json"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
|
|
30
|
+
return parseNpmGlobal(raw);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function scanPipx() {
|
|
34
|
+
const raw = await commandOutput("pipx", ["list", "--json"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
|
|
35
|
+
return parsePipx(raw);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function scanUvTools() {
|
|
39
|
+
const raw = await commandOutput("uv", ["tool", "list"], { timeout: 5000 });
|
|
40
|
+
return parseNameVersionLines(raw);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function scanBrew() {
|
|
44
|
+
if (process.platform === "win32") return [];
|
|
45
|
+
const raw = await commandOutput("brew", ["list", "--versions"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
|
|
46
|
+
return parseNameVersionLines(raw);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseNpmGlobal(raw) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
return Object.entries(parsed.dependencies || {})
|
|
53
|
+
.slice(0, MAX_ITEMS)
|
|
54
|
+
.map(([name, value]) => ({ name, version: value.version || "unknown" }));
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parsePipx(raw) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(raw);
|
|
63
|
+
return Object.entries(parsed.venvs || {})
|
|
64
|
+
.slice(0, MAX_ITEMS)
|
|
65
|
+
.map(([name, value]) => ({
|
|
66
|
+
name,
|
|
67
|
+
version: value.metadata?.main_package?.package_version || "unknown"
|
|
68
|
+
}));
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseNameVersionLines(raw) {
|
|
75
|
+
return String(raw || "")
|
|
76
|
+
.split(/\r?\n/)
|
|
77
|
+
.map((line) => line.trim())
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.slice(0, MAX_ITEMS)
|
|
80
|
+
.map((line) => {
|
|
81
|
+
const [name, ...rest] = line.split(/\s+/);
|
|
82
|
+
return { name, version: rest.join(" ") || "unknown" };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function compact(obj) {
|
|
87
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => Array.isArray(value) && value.length));
|
|
88
|
+
}
|
package/src/manifest.js
CHANGED
|
@@ -4,8 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { commandOutput, commandVersion } from "./shell.js";
|
|
5
5
|
import { exists } from "./fsutil.js";
|
|
6
6
|
import { observedTrust } from "./trust.js";
|
|
7
|
+
import { scanGlobalInventory } from "./inventory.js";
|
|
7
8
|
|
|
8
|
-
export async function buildManifest(dir) {
|
|
9
|
+
export async function buildManifest(dir, options = {}) {
|
|
9
10
|
const now = new Date().toISOString();
|
|
10
11
|
const manifest = {
|
|
11
12
|
schemaName: "aienvmp.runtime-sbom",
|
|
@@ -13,7 +14,7 @@ export async function buildManifest(dir) {
|
|
|
13
14
|
generatedAt: now,
|
|
14
15
|
generatedBy: {
|
|
15
16
|
name: "aienvmp",
|
|
16
|
-
command: "aienvmp sync"
|
|
17
|
+
command: options.deep ? "aienvmp sync --deep" : "aienvmp sync"
|
|
17
18
|
},
|
|
18
19
|
trust: observedTrust(new Date(now)),
|
|
19
20
|
workspace: {
|
|
@@ -25,6 +26,7 @@ export async function buildManifest(dir) {
|
|
|
25
26
|
packageManagers: await scanPackageManagers(),
|
|
26
27
|
containers: await scanContainers(),
|
|
27
28
|
projectHints: await scanProjectHints(dir),
|
|
29
|
+
inventory: await scanGlobalInventory({ deep: options.deep }),
|
|
28
30
|
agentFiles: await scanAgentFiles(dir),
|
|
29
31
|
agentProtocol: {
|
|
30
32
|
sourceOfTruth: "AIENV.md",
|
|
@@ -69,6 +71,13 @@ export async function buildManifest(dir) {
|
|
|
69
71
|
docker: "docker --version",
|
|
70
72
|
compose: "docker compose version or docker-compose --version"
|
|
71
73
|
},
|
|
74
|
+
globalInventory: {
|
|
75
|
+
mode: "basic by default; deep only when requested",
|
|
76
|
+
npmGlobal: "npm list -g --depth=0 --json",
|
|
77
|
+
pipx: "pipx list --json",
|
|
78
|
+
uvTools: "uv tool list",
|
|
79
|
+
brew: "brew list --versions"
|
|
80
|
+
},
|
|
72
81
|
projectHints: [
|
|
73
82
|
".nvmrc",
|
|
74
83
|
".python-version",
|
package/src/render.js
CHANGED
|
@@ -28,6 +28,8 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
28
28
|
pushMap(lines, "Runtimes", manifest.runtimes);
|
|
29
29
|
pushMap(lines, "Package Managers", manifest.packageManagers);
|
|
30
30
|
pushMap(lines, "Containers", manifest.containers);
|
|
31
|
+
lines.push("## Global Tool Inventory", "");
|
|
32
|
+
lines.push(...inventoryLines(manifest.inventory), "");
|
|
31
33
|
lines.push("## Project Requirements And Hints", "");
|
|
32
34
|
pushMap(lines, "Detected", manifest.projectHints);
|
|
33
35
|
lines.push("## Drift And Warnings", "");
|
|
@@ -91,6 +93,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
91
93
|
`Node: ${manifest.runtimes.node || "not detected"}`,
|
|
92
94
|
`Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
93
95
|
`Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
|
|
96
|
+
`Inventory: ${manifest.inventory?.mode || "basic"}${manifest.inventory?.enabled ? " enabled" : " disabled"}`,
|
|
94
97
|
`Policy Node: ${policy.node || "not set"}`,
|
|
95
98
|
`Policy Python: ${policy.python || "not set"}`,
|
|
96
99
|
`Policy Package Manager: ${policy.packageManager || "not set"}`,
|
|
@@ -128,6 +131,7 @@ export function renderHandoff(handoff) {
|
|
|
128
131
|
`- Node: ${handoff.safeRuntime.node}`,
|
|
129
132
|
`- Python: ${handoff.safeRuntime.python}`,
|
|
130
133
|
`- Docker: ${handoff.safeRuntime.docker}`,
|
|
134
|
+
`- Inventory: ${handoff.inventory?.mode || "basic"}${handoff.inventory?.enabled ? " enabled" : " disabled"}`,
|
|
131
135
|
"",
|
|
132
136
|
"Open intents:",
|
|
133
137
|
...(handoff.openIntents.length ? handoff.openIntents.map((i) => `- ${i.actor}: ${i.action}${i.target ? ` (${i.target})` : ""}`) : ["- none"]),
|
|
@@ -203,6 +207,9 @@ const {manifest,timeline,warnings,intents,policy}=JSON.parse(document.getElement
|
|
|
203
207
|
function esc(s){return String(s).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"')}
|
|
204
208
|
const entries=o=>Object.entries(o||{});
|
|
205
209
|
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>';
|
|
210
|
+
const inventoryGroups=manifest.inventory?.tools||{};
|
|
211
|
+
const inventoryCount=Object.values(inventoryGroups).reduce((sum,items)=>sum+(Array.isArray(items)?items.length:0),0);
|
|
212
|
+
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>';
|
|
206
213
|
const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
|
|
207
214
|
const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
|
|
208
215
|
const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
|
|
@@ -246,6 +253,7 @@ document.getElementById('app').innerHTML=\`
|
|
|
246
253
|
\${card('Package Managers',\`<span class="pill">\${entries(manifest.packageManagers).length} found</span>\`,\`<table>\${rows(manifest.packageManagers)}</table>\`)}
|
|
247
254
|
\${card('Containers',manifest.containers?.docker?'<span class="pill">available</span>':'<span class="pill off">not detected</span>',\`<table>\${rows(manifest.containers)}</table>\`)}
|
|
248
255
|
\${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
|
|
256
|
+
\${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
|
|
249
257
|
</div>
|
|
250
258
|
<aside>
|
|
251
259
|
\${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
|
|
@@ -304,6 +312,17 @@ function contextLines(manifest, warnings, intents) {
|
|
|
304
312
|
];
|
|
305
313
|
}
|
|
306
314
|
|
|
315
|
+
function inventoryLines(inventory = {}) {
|
|
316
|
+
if (!inventory.enabled) return ["- Mode: basic", "- Deep global inventory is disabled. Run `aienvmp sync --deep` when needed."];
|
|
317
|
+
const groups = Object.entries(inventory.tools || {});
|
|
318
|
+
if (!groups.length) return ["- Mode: deep", "- No global tools detected by optional scanners."];
|
|
319
|
+
const lines = ["- Mode: deep"];
|
|
320
|
+
for (const [name, items] of groups) {
|
|
321
|
+
lines.push(`- ${name}: ${items.length} tools`);
|
|
322
|
+
}
|
|
323
|
+
return lines;
|
|
324
|
+
}
|
|
325
|
+
|
|
307
326
|
function policyLines(policy) {
|
|
308
327
|
const lines = [];
|
|
309
328
|
if (policy.node) lines.push(`- Node version policy: ${policy.node}`);
|
package/src/shell.js
CHANGED
|
@@ -15,10 +15,11 @@ export async function commandVersion(command, args = ["--version"]) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export async function commandOutput(command, args = []) {
|
|
18
|
+
export async function commandOutput(command, args = [], options = {}) {
|
|
19
19
|
try {
|
|
20
20
|
const { stdout } = await execFileAsync(command, args, {
|
|
21
|
-
timeout: 2500,
|
|
21
|
+
timeout: options.timeout || 2500,
|
|
22
|
+
maxBuffer: options.maxBuffer || 1024 * 1024,
|
|
22
23
|
windowsHide: true
|
|
23
24
|
});
|
|
24
25
|
return stdout.trim();
|