@agentprojectcontext/apx 1.0.3
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/LICENSE +21 -0
- package/README.md +142 -0
- package/package.json +52 -0
- package/skills/apx/SKILL.md +77 -0
- package/src/cli/commands/a2a.js +66 -0
- package/src/cli/commands/agent.js +181 -0
- package/src/cli/commands/chat.js +84 -0
- package/src/cli/commands/command.js +42 -0
- package/src/cli/commands/config.js +56 -0
- package/src/cli/commands/daemon.js +148 -0
- package/src/cli/commands/exec.js +56 -0
- package/src/cli/commands/identity.js +146 -0
- package/src/cli/commands/init.js +23 -0
- package/src/cli/commands/mcp.js +147 -0
- package/src/cli/commands/memory.js +69 -0
- package/src/cli/commands/messages.js +61 -0
- package/src/cli/commands/plugins.js +23 -0
- package/src/cli/commands/project.js +124 -0
- package/src/cli/commands/routine.js +99 -0
- package/src/cli/commands/runtime.js +64 -0
- package/src/cli/commands/session.js +387 -0
- package/src/cli/commands/skills.js +153 -0
- package/src/cli/commands/telegram.js +48 -0
- package/src/cli/http.js +102 -0
- package/src/cli/index.js +481 -0
- package/src/cli/postinstall.js +25 -0
- package/src/core/apc-context-skill.md +150 -0
- package/src/core/apx-skill.md +78 -0
- package/src/core/config.js +129 -0
- package/src/core/identity.js +23 -0
- package/src/core/messages-store.js +421 -0
- package/src/core/parser.js +217 -0
- package/src/core/routines-store.js +144 -0
- package/src/core/scaffold.js +417 -0
- package/src/core/session-store.js +36 -0
- package/src/daemon/apc-runtime-context.js +123 -0
- package/src/daemon/api.js +946 -0
- package/src/daemon/compact.js +140 -0
- package/src/daemon/conversations.js +108 -0
- package/src/daemon/db.js +81 -0
- package/src/daemon/engines/anthropic.js +58 -0
- package/src/daemon/engines/gemini.js +55 -0
- package/src/daemon/engines/index.js +65 -0
- package/src/daemon/engines/mock.js +18 -0
- package/src/daemon/engines/ollama.js +66 -0
- package/src/daemon/engines/openai.js +58 -0
- package/src/daemon/env-detect.js +69 -0
- package/src/daemon/index.js +156 -0
- package/src/daemon/mcp-runner.js +218 -0
- package/src/daemon/mcp-sources.js +114 -0
- package/src/daemon/plugins/index.js +91 -0
- package/src/daemon/plugins/telegram.js +549 -0
- package/src/daemon/project-config.js +98 -0
- package/src/daemon/routines.js +211 -0
- package/src/daemon/runtimes/_spawn.js +44 -0
- package/src/daemon/runtimes/aider.js +32 -0
- package/src/daemon/runtimes/claude-code.js +60 -0
- package/src/daemon/runtimes/codex.js +30 -0
- package/src/daemon/runtimes/index.js +39 -0
- package/src/daemon/runtimes/opencode.js +28 -0
- package/src/daemon/smoke.js +54 -0
- package/src/daemon/super-agent-tools.js +539 -0
- package/src/daemon/super-agent.js +188 -0
- package/src/daemon/thinking.js +45 -0
- package/src/daemon/tool-call-parser.js +116 -0
- package/src/daemon/wakeup.js +92 -0
- package/src/mcp/index.js +220 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { http } from "../http.js";
|
|
2
|
+
import { resolveProjectId } from "./project.js";
|
|
3
|
+
|
|
4
|
+
// Channels that live in ~/.apx/messages/<channel>/ (global, cross-project).
|
|
5
|
+
// Everything else is project-scoped.
|
|
6
|
+
const GLOBAL_CHANNELS = new Set(["telegram", "direct", "whatsapp"]);
|
|
7
|
+
|
|
8
|
+
function isGlobalChannel(channel) {
|
|
9
|
+
return channel && GLOBAL_CHANNELS.has(channel);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function cmdMessagesTail(args) {
|
|
13
|
+
const channel = args.flags.channel && args.flags.channel !== true ? args.flags.channel : null;
|
|
14
|
+
const n = args.flags.n || args.flags.last || "50";
|
|
15
|
+
const isGlobal = args.flags.global || isGlobalChannel(channel);
|
|
16
|
+
|
|
17
|
+
if (isGlobal) {
|
|
18
|
+
// Read from global store — no project needed
|
|
19
|
+
const params = new URLSearchParams({ limit: String(n) });
|
|
20
|
+
if (channel) params.set("channel", channel);
|
|
21
|
+
const rows = await http.get(`/messages/global?${params}`);
|
|
22
|
+
if (rows.length === 0) {
|
|
23
|
+
console.log("(no messages)");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
for (const r of rows) {
|
|
27
|
+
const arrow = r.direction === "in" ? "←" : "→";
|
|
28
|
+
console.log(`${r.ts} ${arrow} ${(r.channel || channel || "").padEnd(10)} ${r.author || ""}: ${r.body}`);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Project-scoped
|
|
34
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
35
|
+
const params = new URLSearchParams({ limit: String(n) });
|
|
36
|
+
if (args.flags.agent && args.flags.agent !== true) params.set("agent", args.flags.agent);
|
|
37
|
+
if (channel) params.set("channel", channel);
|
|
38
|
+
const rows = await http.get(`/projects/${pid}/messages?${params}`);
|
|
39
|
+
if (rows.length === 0) {
|
|
40
|
+
console.log("(no messages)");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const r of rows.reverse()) {
|
|
44
|
+
const arrow = r.direction === "in" ? "←" : "→";
|
|
45
|
+
console.log(`${r.ts} ${arrow} ${r.channel.padEnd(8)} ${r.author || ""}: ${r.body}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function cmdMessagesSearch(args) {
|
|
50
|
+
const q = args._[0];
|
|
51
|
+
if (!q) throw new Error("apx messages search: missing <query>");
|
|
52
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
53
|
+
const rows = await http.get(`/projects/${pid}/messages/search?q=${encodeURIComponent(q)}`);
|
|
54
|
+
if (rows.length === 0) {
|
|
55
|
+
console.log("(no matches)");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const r of rows) {
|
|
59
|
+
console.log(`${r.ts} ${r.channel} ${r.author || ""}: ${r.body}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { http } from "../http.js";
|
|
2
|
+
|
|
3
|
+
export async function cmdPluginsList() {
|
|
4
|
+
const data = await http.get("/plugins");
|
|
5
|
+
const ids = Object.keys(data);
|
|
6
|
+
if (ids.length === 0) {
|
|
7
|
+
console.log("(no plugins loaded)");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
for (const id of ids) {
|
|
11
|
+
const s = data[id];
|
|
12
|
+
console.log(`# ${id}`);
|
|
13
|
+
console.log(JSON.stringify(s, null, 2));
|
|
14
|
+
console.log("");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function cmdPluginStatus(args) {
|
|
19
|
+
const id = args._[0];
|
|
20
|
+
if (!id) throw new Error("apx plugins status: missing <plugin-id>");
|
|
21
|
+
const s = await http.get(`/plugins/${id}/status`);
|
|
22
|
+
console.log(JSON.stringify(s, null, 2));
|
|
23
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { findApfRoot } from "../../core/parser.js";
|
|
3
|
+
import { http } from "../http.js";
|
|
4
|
+
|
|
5
|
+
function requireRoot() {
|
|
6
|
+
const root = findApfRoot();
|
|
7
|
+
if (!root) {
|
|
8
|
+
throw new Error("not inside an APC project (run `apx init` first)");
|
|
9
|
+
}
|
|
10
|
+
return root;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function cmdProjectAdd(args) {
|
|
14
|
+
const target = args._[0] || requireRoot();
|
|
15
|
+
const result = await http.post("/projects", { path: path.resolve(target) });
|
|
16
|
+
console.log(`Registered #${result.id}: ${result.path}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function cmdProjectList() {
|
|
20
|
+
const projects = await http.get("/projects");
|
|
21
|
+
if (projects.length === 0) {
|
|
22
|
+
console.log("(no projects registered — try `apx project add .`)");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.log("ID\tNAME\t\t\tAGENTS\tPATH");
|
|
26
|
+
for (const p of projects) {
|
|
27
|
+
console.log(`${p.id}\t${p.name}\t\t${p.agents}\t${p.path}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function cmdProjectRemove(args) {
|
|
32
|
+
const target = args._[0];
|
|
33
|
+
if (!target) throw new Error("apx project remove: missing <path|id>");
|
|
34
|
+
const projects = await http.get("/projects");
|
|
35
|
+
let id;
|
|
36
|
+
if (/^\d+$/.test(target)) id = parseInt(target, 10);
|
|
37
|
+
else {
|
|
38
|
+
const abs = path.resolve(target);
|
|
39
|
+
const found = projects.find((p) => p.path === abs);
|
|
40
|
+
if (!found) throw new Error(`not registered: ${target}`);
|
|
41
|
+
id = found.id;
|
|
42
|
+
}
|
|
43
|
+
await http.delete(`/projects/${id}`);
|
|
44
|
+
console.log(`Removed project #${id}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function cmdProjectRebuild(args) {
|
|
48
|
+
const target = args._[0];
|
|
49
|
+
let id;
|
|
50
|
+
if (target) {
|
|
51
|
+
if (/^\d+$/.test(target)) id = parseInt(target, 10);
|
|
52
|
+
else {
|
|
53
|
+
const abs = path.resolve(target);
|
|
54
|
+
const projects = await http.get("/projects");
|
|
55
|
+
const found = projects.find((p) => p.path === abs);
|
|
56
|
+
if (!found) throw new Error(`not registered: ${target}`);
|
|
57
|
+
id = found.id;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
const root = requireRoot();
|
|
61
|
+
const projects = await http.get("/projects");
|
|
62
|
+
const found = projects.find((p) => p.path === root);
|
|
63
|
+
if (!found) throw new Error(`current project not registered — run \`apx project add .\``);
|
|
64
|
+
id = found.id;
|
|
65
|
+
}
|
|
66
|
+
const result = await http.post(`/projects/${id}/rebuild`);
|
|
67
|
+
console.log(`Rebuilt project #${id}: ${result.agents} agents`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Resolve a project id from one of:
|
|
71
|
+
// numeric id ("2")
|
|
72
|
+
// absolute path ("/abs/path/to/project")
|
|
73
|
+
// relative path (resolved against cwd)
|
|
74
|
+
// project name ("APX testing sandbox")
|
|
75
|
+
// Falls back to walking up from cwd if `target` is null/undefined.
|
|
76
|
+
export async function resolveProjectId(target) {
|
|
77
|
+
if (target !== undefined && target !== null && target !== "") {
|
|
78
|
+
if (typeof target === "number" || /^\d+$/.test(String(target))) {
|
|
79
|
+
return parseInt(target, 10);
|
|
80
|
+
}
|
|
81
|
+
const projects = await http.get("/projects");
|
|
82
|
+
const tgt = String(target);
|
|
83
|
+
|
|
84
|
+
// exact path
|
|
85
|
+
const abs = path.resolve(tgt);
|
|
86
|
+
const byPath = projects.find((p) => p.path === abs);
|
|
87
|
+
if (byPath) return byPath.id;
|
|
88
|
+
|
|
89
|
+
// exact name
|
|
90
|
+
const byName = projects.find((p) => p.name === tgt);
|
|
91
|
+
if (byName) return byName.id;
|
|
92
|
+
|
|
93
|
+
// case-insensitive substring on name OR full path — friendly default
|
|
94
|
+
const tgtLow = tgt.toLowerCase();
|
|
95
|
+
const fuzzy = projects.filter(
|
|
96
|
+
(p) =>
|
|
97
|
+
p.name.toLowerCase().includes(tgtLow) ||
|
|
98
|
+
p.path.toLowerCase().includes(tgtLow)
|
|
99
|
+
);
|
|
100
|
+
if (fuzzy.length === 1) return fuzzy[0].id;
|
|
101
|
+
if (fuzzy.length > 1) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`--project "${tgt}" is ambiguous; matches: ${fuzzy.map((p) => `${p.id}/${p.name}`).join(", ")}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`--project "${tgt}" not found in registered projects`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// No override: walk up from cwd
|
|
110
|
+
const root = findApfRoot();
|
|
111
|
+
if (!root) {
|
|
112
|
+
const registered = await http.get("/projects").catch(() => []);
|
|
113
|
+
const hint = registered.length
|
|
114
|
+
? `registered projects: ${registered.map((p) => `"${p.name}" (id ${p.id})`).join(", ")}`
|
|
115
|
+
: "no projects registered yet — run `apx project add <path>` first";
|
|
116
|
+
throw new Error(`not inside an APC project. Use --project <name|id|path>. ${hint}`);
|
|
117
|
+
}
|
|
118
|
+
const projects = await http.get("/projects");
|
|
119
|
+
const found = projects.find((p) => p.path === root);
|
|
120
|
+
if (found) return found.id;
|
|
121
|
+
// auto-register
|
|
122
|
+
const result = await http.post("/projects", { path: root });
|
|
123
|
+
return result.id;
|
|
124
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { http } from "../http.js";
|
|
2
|
+
import { resolveProjectId } from "./project.js";
|
|
3
|
+
|
|
4
|
+
function parseSpec(args) {
|
|
5
|
+
// Build spec from --spec '<json>' or from --K=V pairs
|
|
6
|
+
if (args.flags.spec && args.flags.spec !== true) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(args.flags.spec);
|
|
9
|
+
} catch (e) {
|
|
10
|
+
throw new Error(`--spec is not valid JSON: ${e.message}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const spec = {};
|
|
14
|
+
for (const [k, v] of Object.entries(args.flags)) {
|
|
15
|
+
if (["name", "kind", "schedule", "spec", "verbose"].includes(k)) continue;
|
|
16
|
+
if (v === true) continue;
|
|
17
|
+
// try to JSON-parse the value (numbers, bools), else keep as string
|
|
18
|
+
let val = v;
|
|
19
|
+
try { val = JSON.parse(v); } catch {}
|
|
20
|
+
spec[k] = val;
|
|
21
|
+
}
|
|
22
|
+
return spec;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function cmdRoutineList(args = {}) {
|
|
26
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
27
|
+
const rows = await http.get(`/projects/${pid}/routines`);
|
|
28
|
+
if (rows.length === 0) {
|
|
29
|
+
console.log("(no routines)");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
console.log("NAME".padEnd(20) + " EN " + "KIND".padEnd(11) + " SCHEDULE".padEnd(20) + " NEXT_RUN".padEnd(22) + " LAST");
|
|
33
|
+
for (const r of rows) {
|
|
34
|
+
console.log(
|
|
35
|
+
r.name.padEnd(20) + " " +
|
|
36
|
+
(r.enabled ? "✓" : "✗").padEnd(2) + " " +
|
|
37
|
+
(r.kind || "?").padEnd(10) + " " +
|
|
38
|
+
(r.schedule || "?").padEnd(20) + " " +
|
|
39
|
+
(r.next_run_at || "—").padEnd(22) + " " +
|
|
40
|
+
(r.last_status === "ok" ? "✓ " : r.last_status === "error" ? "✗ " : " ") +
|
|
41
|
+
(r.last_run_at || "—")
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function cmdRoutineGet(args) {
|
|
47
|
+
const name = args._[0];
|
|
48
|
+
if (!name) throw new Error("apx routine get: missing <name>");
|
|
49
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
50
|
+
const r = await http.get(`/projects/${pid}/routines/${name}`);
|
|
51
|
+
console.log(JSON.stringify(r, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function cmdRoutineAdd(args) {
|
|
55
|
+
const name = args._[0];
|
|
56
|
+
if (!name) throw new Error("apx routine add: missing <name>");
|
|
57
|
+
const kind = args.flags.kind === true ? null : args.flags.kind;
|
|
58
|
+
const schedule = args.flags.schedule === true ? null : args.flags.schedule;
|
|
59
|
+
if (!kind || !schedule)
|
|
60
|
+
throw new Error("apx routine add: --kind and --schedule are required");
|
|
61
|
+
const spec = parseSpec(args);
|
|
62
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
63
|
+
const r = await http.post(`/projects/${pid}/routines`, { name, kind, schedule, spec });
|
|
64
|
+
console.log(`added routine "${r.name}" (${r.kind}, ${r.schedule}) → next ${r.next_run_at}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function cmdRoutineRemove(args) {
|
|
68
|
+
const name = args._[0];
|
|
69
|
+
if (!name) throw new Error("apx routine remove: missing <name>");
|
|
70
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
71
|
+
await http.delete(`/projects/${pid}/routines/${name}`);
|
|
72
|
+
console.log(`removed routine "${name}"`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function cmdRoutineEnable(args) {
|
|
76
|
+
await toggle(args, true);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function cmdRoutineDisable(args) {
|
|
80
|
+
await toggle(args, false);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function toggle(args, enabled) {
|
|
84
|
+
const name = args._[0];
|
|
85
|
+
if (!name) throw new Error(`apx routine ${enabled ? "enable" : "disable"}: missing <name>`);
|
|
86
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
87
|
+
await http.post(
|
|
88
|
+
`/projects/${pid}/routines/${name}/${enabled ? "enable" : "disable"}`
|
|
89
|
+
);
|
|
90
|
+
console.log(`${enabled ? "enabled" : "disabled"} routine "${name}"`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function cmdRoutineRun(args) {
|
|
94
|
+
const name = args._[0];
|
|
95
|
+
if (!name) throw new Error("apx routine run: missing <name>");
|
|
96
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
97
|
+
const r = await http.post(`/projects/${pid}/routines/${name}/run`);
|
|
98
|
+
console.log(JSON.stringify(r, null, 2));
|
|
99
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { http } from "../http.js";
|
|
2
|
+
import { resolveProjectId } from "./project.js";
|
|
3
|
+
|
|
4
|
+
export async function cmdRun(args) {
|
|
5
|
+
const slug = args._[0];
|
|
6
|
+
if (!slug) throw new Error("apx run: usage: apx run <agent> --runtime <id> \"<prompt>\"");
|
|
7
|
+
const runtime = args.flags.runtime === true ? null : args.flags.runtime;
|
|
8
|
+
if (!runtime) throw new Error("apx run: --runtime required (claude-code | codex | opencode | aider)");
|
|
9
|
+
|
|
10
|
+
let prompt = args._.slice(1).join(" ").trim();
|
|
11
|
+
if (!prompt || prompt === "-") {
|
|
12
|
+
const fs = await import("node:fs");
|
|
13
|
+
if (!process.stdin.isTTY) {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
const buf = Buffer.alloc(65536);
|
|
16
|
+
try {
|
|
17
|
+
while (true) {
|
|
18
|
+
const n = fs.readSync(0, buf, 0, buf.length);
|
|
19
|
+
if (!n) break;
|
|
20
|
+
chunks.push(buf.slice(0, n).toString("utf8"));
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
prompt = chunks.join("").trim();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!prompt) throw new Error("apx run: prompt is empty");
|
|
27
|
+
|
|
28
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
29
|
+
const timeoutMs = args.flags.timeout
|
|
30
|
+
? parseInt(args.flags.timeout, 10) * 1000
|
|
31
|
+
: undefined;
|
|
32
|
+
|
|
33
|
+
const result = await http.post(
|
|
34
|
+
`/projects/${pid}/agents/${slug}/runtime`,
|
|
35
|
+
{ runtime, prompt, timeoutMs }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (result.output) process.stdout.write(result.output + "\n");
|
|
39
|
+
if (process.stderr.isTTY || args.flags.verbose) {
|
|
40
|
+
process.stderr.write(`\n— ${runtime} | exit ${result.exit_code}`);
|
|
41
|
+
if (result.external_session_path) {
|
|
42
|
+
process.stderr.write(` | session: ${result.external_session_path}`);
|
|
43
|
+
}
|
|
44
|
+
process.stderr.write("\n");
|
|
45
|
+
}
|
|
46
|
+
process.exit(result.exit_code === 0 ? 0 : 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function cmdEnvDetect() {
|
|
50
|
+
const probes = await http.get("/env/detect");
|
|
51
|
+
const groups = { runtime: [], engine: [], tool: [] };
|
|
52
|
+
for (const p of probes) {
|
|
53
|
+
(groups[p.category] || groups.tool).push(p);
|
|
54
|
+
}
|
|
55
|
+
for (const [cat, items] of Object.entries(groups)) {
|
|
56
|
+
if (!items.length) continue;
|
|
57
|
+
console.log(`\n${cat.toUpperCase()}:`);
|
|
58
|
+
for (const p of items) {
|
|
59
|
+
const mark = p.installed ? "✓" : "·";
|
|
60
|
+
const ver = p.installed ? p.version : `(${p.reason || "not found"})`;
|
|
61
|
+
console.log(` ${mark} ${p.id.padEnd(14)} ${p.binary.padEnd(14)} ${ver}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|