activate-agentmd 2.4.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.
Files changed (44) hide show
  1. package/README.md +158 -0
  2. package/bin/agentmd.js +186 -0
  3. package/package.json +55 -0
  4. package/src/commands/agents.js +44 -0
  5. package/src/commands/analytics.js +128 -0
  6. package/src/commands/auth.js +144 -0
  7. package/src/commands/ci.js +296 -0
  8. package/src/commands/extract.js +254 -0
  9. package/src/commands/info.js +105 -0
  10. package/src/commands/init.js +166 -0
  11. package/src/commands/install.js +216 -0
  12. package/src/commands/link.js +206 -0
  13. package/src/commands/list.js +99 -0
  14. package/src/commands/outdated.js +92 -0
  15. package/src/commands/remove.js +72 -0
  16. package/src/commands/review.js +293 -0
  17. package/src/commands/search.js +110 -0
  18. package/src/commands/sync.js +245 -0
  19. package/src/commands/telemetry.js +82 -0
  20. package/src/commands/test.js +226 -0
  21. package/src/commands/update.js +115 -0
  22. package/src/commands/validate.js +125 -0
  23. package/src/config/license-public-keys.json +17 -0
  24. package/src/detect.json +452 -0
  25. package/src/lib/agents.js +301 -0
  26. package/src/lib/analytics.js +137 -0
  27. package/src/lib/coordinates.js +103 -0
  28. package/src/lib/credentials.js +115 -0
  29. package/src/lib/detect.js +352 -0
  30. package/src/lib/diff.js +142 -0
  31. package/src/lib/enterprise.js +140 -0
  32. package/src/lib/fetcher.js +174 -0
  33. package/src/lib/invocation.js +47 -0
  34. package/src/lib/license.js +234 -0
  35. package/src/lib/manifest.js +191 -0
  36. package/src/lib/patterns.js +135 -0
  37. package/src/lib/pro.js +149 -0
  38. package/src/lib/ranking.js +287 -0
  39. package/src/lib/registry.js +207 -0
  40. package/src/lib/star.js +105 -0
  41. package/src/lib/status.js +75 -0
  42. package/src/lib/telemetry.js +169 -0
  43. package/src/lib/versions.js +190 -0
  44. package/src/registry.json +33907 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * install.js
3
+ *
4
+ * Handles: agentmd install <model/category[/preset]>
5
+ *
6
+ * claude/Security/owasp → install one preset
7
+ * claude/Security/ → install every preset in the category
8
+ * claude/ → refused (see below)
9
+ *
10
+ * Whole-model installs are refused on purpose. A model has ~287 presets and
11
+ * roughly 2 MB of instructions; dropping all of it into a project would blow
12
+ * past any agent's usable context and make the rules contradict each other.
13
+ * Categories are the largest unit that stays coherent.
14
+ */
15
+
16
+ "use strict";
17
+
18
+ const fs = require("fs");
19
+ const path = require("path");
20
+ const pc = require("picocolors");
21
+ const { listModels, getPreset, getCategoryPresets, listCategories } = require("../lib/registry");
22
+ const { fetchText } = require("../lib/fetcher");
23
+ const {
24
+ readManifest,
25
+ writeManifest,
26
+ ensurePresetsDir,
27
+ upsertPreset,
28
+ isInstalled,
29
+ findPreset,
30
+ resolvePresetPath,
31
+ presetFilename,
32
+ checksum,
33
+ PRESETS_DIR,
34
+ } = require("../lib/manifest");
35
+ const { parseCoordinates, suggest } = require("../lib/coordinates");
36
+ const { cmd } = require("../lib/invocation");
37
+
38
+ /** How the user invoked us — "npx activate-agentmd" or "agentmd". */
39
+ const A = cmd();
40
+
41
+ const ISSUES_URL = "https://github.com/Aaditya1273/Agent.md/issues";
42
+
43
+ /**
44
+ * A package with no substance. The registry has served one before
45
+ * (Community/maintainers was a 0-byte file, and in three models nothing but a
46
+ * frontmatter block), and writing it looks like a successful install while
47
+ * giving the agent no guidance at all.
48
+ */
49
+ function isEmptyPackage(content) {
50
+ if (typeof content !== "string") return true;
51
+ // Strip a leading frontmatter block before judging emptiness.
52
+ const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---/, "");
53
+ return body.trim().length === 0;
54
+ }
55
+
56
+ /** Fetch one preset, write it, record it. Returns "installed" | "skipped" | "failed". */
57
+ async function installOne(entry, cwd, manifest, opts) {
58
+ ensurePresetsDir(cwd);
59
+
60
+ if (!opts.force && isInstalled(manifest, entry.model, entry.category, entry.preset)) {
61
+ console.log(pc.dim(` skip ${entry.id} ${pc.dim("(already installed — use --force to overwrite)")}`));
62
+ return "skipped";
63
+ }
64
+
65
+ process.stdout.write(pc.cyan(` ↓ ${entry.id} …`));
66
+
67
+ let content;
68
+ try {
69
+ content = await fetchText(entry.url);
70
+ } catch (err) {
71
+ process.stdout.write("\n");
72
+ console.error(pc.red(` ✗ ${entry.id} — ${err.message}`));
73
+ return "failed";
74
+ }
75
+
76
+ // An empty package is worse than a missing one: the install looks like it
77
+ // succeeded and the agent silently gets no guidance. Treat it as a failure
78
+ // rather than writing a 0-byte file.
79
+ if (isEmptyPackage(content)) {
80
+ process.stdout.write("\n");
81
+ console.error(pc.red(` ✗ ${entry.id} — the registry returned an empty package`));
82
+ console.error(pc.dim(` Nothing was written. Please report it: ${ISSUES_URL}`));
83
+ return "failed";
84
+ }
85
+
86
+ const filename = presetFilename(entry.model, entry.category, entry.preset);
87
+
88
+ // A package installed before filenames included the model lives at the old
89
+ // path. Remove it so the upgrade doesn't leave an orphan behind.
90
+ const previous = findPreset(manifest, entry.id);
91
+ if (previous && previous.file && previous.file !== `${PRESETS_DIR}/${filename}`) {
92
+ const stale = path.resolve(cwd, previous.file);
93
+ if (stale.startsWith(path.resolve(cwd, PRESETS_DIR) + path.sep) && fs.existsSync(stale)) {
94
+ fs.unlinkSync(stale);
95
+ }
96
+ }
97
+
98
+ fs.writeFileSync(resolvePresetPath(cwd, filename), content, "utf-8");
99
+
100
+ upsertPreset(manifest, {
101
+ ...entry,
102
+ checksum: checksum(content),
103
+ file: `${PRESETS_DIR}/${filename}`,
104
+ });
105
+
106
+ process.stdout.write(" " + pc.green("✓") + "\n");
107
+ return "installed";
108
+ }
109
+
110
+ /**
111
+ * Install a resolved list of registry entries, printing progress.
112
+ * Shared by `install` and `init` so a 15-package `init` prints one report
113
+ * rather than fifteen.
114
+ */
115
+ async function installEntries(entries, opts = {}) {
116
+ const cwd = process.cwd();
117
+ const manifest = readManifest(cwd);
118
+
119
+ const tally = { installed: 0, skipped: 0, failed: 0 };
120
+ for (const entry of entries) {
121
+ tally[await installOne(entry, cwd, manifest, opts)]++;
122
+ }
123
+
124
+ if (tally.installed > 0) writeManifest(cwd, manifest);
125
+
126
+ console.log("");
127
+ if (tally.installed > 0) {
128
+ console.log(pc.green(`✓ ${tally.installed} package${tally.installed !== 1 ? "s" : ""} installed`) + pc.dim(` → ${PRESETS_DIR}/`));
129
+ }
130
+ if (tally.skipped > 0) console.log(pc.dim(` ${tally.skipped} already installed`));
131
+ if (tally.failed > 0) console.log(pc.red(` ${tally.failed} failed`));
132
+
133
+ if (tally.installed > 0) {
134
+ console.log("");
135
+ console.log(pc.bold("Next:"));
136
+ console.log(pc.dim(` ${A} link wire these into CLAUDE.md / AGENTS.md / .cursor/rules`));
137
+ console.log(pc.dim(` ${A} list --installed see what this project has`));
138
+ console.log("");
139
+ }
140
+
141
+ if (tally.failed > 0) process.exitCode = 1;
142
+ return tally;
143
+ }
144
+
145
+ async function install(args) {
146
+ const positional = (args || []).filter((a) => !a.startsWith("--"));
147
+ const opts = { force: (args || []).includes("--force") };
148
+
149
+ if (positional.length === 0) {
150
+ console.error(pc.red(`Usage: ${A} install <model/category[/preset]>`));
151
+ console.error(pc.dim(` ${A} install claude/Security/owasp install one preset`));
152
+ console.error(pc.dim(` ${A} install claude/Security/ install the whole category`));
153
+ console.error(pc.dim(`\n Not sure what you want? Run '${A} init' to get recommendations.`));
154
+ process.exit(1);
155
+ }
156
+
157
+ // One or many ids: `install claude/Security/owasp claude/AI/spec-driven`.
158
+ // Passing several used to install only the first and say nothing.
159
+ const resolve = (raw) => {
160
+ const coords = parseCoordinates(raw);
161
+
162
+ if (!coords.model || !listModels().includes(coords.model)) {
163
+ console.error(pc.red(`Unknown model: "${coords.raw.split("/")[0] || coords.raw}"`));
164
+ console.error(pc.dim(` Available: ${listModels().join(", ")}`));
165
+ process.exit(1);
166
+ }
167
+
168
+ if (!coords.category) {
169
+ console.error(pc.yellow(`⚠ "${coords.model}" has ${listCategories(coords.model).length} categories and hundreds of presets.`));
170
+ console.error(pc.dim(" Installing all of them would exceed any agent's usable context, so pick a category.\n"));
171
+ console.error(pc.dim(` See categories: ${A} list ${coords.model}`));
172
+ console.error(pc.dim(` Install one: ${A} install ${coords.model}/Security/owasp`));
173
+ console.error(pc.dim(` Get suggestions: ${A} init`));
174
+ process.exit(1);
175
+ }
176
+
177
+ if (coords.preset) {
178
+ const entry = getPreset(coords.model, coords.category, coords.preset);
179
+ if (!entry) {
180
+ console.error(pc.red(`Package not found: ${coords.model}/${coords.category}/${coords.preset}`));
181
+ const near = suggest(coords.model, coords.preset);
182
+ if (near.length) {
183
+ console.error(pc.dim("\n Did you mean:"));
184
+ for (const n of near) console.error(pc.dim(` ${A} install ${n.id}`));
185
+ }
186
+ console.error(pc.dim(`\n Search: ${A} search ${coords.preset}`));
187
+ console.error(pc.dim(` Browse: ${A} list ${coords.model}/${coords.category}`));
188
+ process.exit(1);
189
+ }
190
+ return [entry];
191
+ } else {
192
+ const all = getCategoryPresets(coords.model, coords.category);
193
+ if (all.length === 0) {
194
+ console.error(pc.red(`Category not found: ${coords.model}/${coords.category}`));
195
+ console.error(pc.dim(`\n Available categories in ${coords.model}:`));
196
+ console.error(pc.dim(" " + listCategories(coords.model).join(", ")));
197
+ process.exit(1);
198
+ }
199
+ }
200
+
201
+ return all;
202
+ };
203
+
204
+ const toInstall = [];
205
+ for (const raw of positional) for (const e of resolve(raw)) if (!toInstall.some((x) => x.id === e.id)) toInstall.push(e);
206
+
207
+ console.log(
208
+ toInstall.length > 1
209
+ ? pc.bold(`\nInstalling ${pc.cyan(toInstall.length + " packages")}${positional.length === 1 ? ` from ${pc.white(positional[0].replace(/\/$/, ""))}` : ""}\n`)
210
+ : pc.bold(`\nInstalling ${pc.cyan(toInstall[0].id)}\n`)
211
+ );
212
+
213
+ await installEntries(toInstall, opts);
214
+ }
215
+
216
+ module.exports = { install, installEntries, isEmptyPackage };
@@ -0,0 +1,206 @@
1
+ /**
2
+ * link.js
3
+ *
4
+ * Handles: agentmd link [--agent=claude,cursor,…] [--dry]
5
+ *
6
+ * Downloading a preset is only half the job — the agent has to be told to
7
+ * read it. `link` writes the installed package list into each agent's own
8
+ * config convention, inside a delimited block so it can be regenerated
9
+ * without clobbering anything hand-written.
10
+ */
11
+
12
+ "use strict";
13
+
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+ const pc = require("picocolors");
17
+ const { TARGETS, REFERENCE_DIRS, applyToFile, planAlwaysOn, referenceRules } = require("../lib/agents");
18
+ const { findWorkspaces, scanOne, RECOMMENDATIONS } = require("../lib/detect");
19
+ const { readManifest, resolvePresetPath, resolveInProject } = require("../lib/manifest");
20
+ const { detectVersions } = require("../lib/versions");
21
+ const { cmd } = require("../lib/invocation");
22
+
23
+ /** How the user invoked us — "npx activate-agentmd" or "agentmd". */
24
+ const A = cmd();
25
+
26
+ function flagValue(args, name) {
27
+ const hit = (args || []).find((a) => a.startsWith(`--${name}=`));
28
+ return hit ? hit.slice(name.length + 3) : null;
29
+ }
30
+
31
+ /** Signs a tool is in use, checked only when no agent file exists yet. */
32
+ const ENV_SIGNALS = [
33
+ { agent: "cursor", via: ".cursor/", test: (cwd) => fs.existsSync(path.join(cwd, ".cursor")) },
34
+ { agent: "claude", via: ".claude/", test: (cwd) => fs.existsSync(path.join(cwd, ".claude")) },
35
+ { agent: "claude", via: "claude on PATH", test: (cwd, env) => onPath("claude", env) },
36
+ { agent: "gemini", via: ".gemini/", test: (cwd) => fs.existsSync(path.join(cwd, ".gemini")) },
37
+ { agent: "copilot", via: ".github/copilot-instructions.md", test: (cwd) => fs.existsSync(path.join(cwd, ".github", "copilot-instructions.md")) },
38
+ ];
39
+
40
+ /** Is `bin` somewhere on PATH? Windows binaries carry an extension. */
41
+ function onPath(bin, env = process.env) {
42
+ const dirs = String(env.PATH || env.Path || "").split(path.delimiter).filter(Boolean);
43
+ const names = process.platform === "win32" ? [`${bin}.exe`, `${bin}.cmd`, `${bin}.bat`, bin] : [bin];
44
+ return dirs.some((d) => names.some((n) => fs.existsSync(path.join(d, n))));
45
+ }
46
+
47
+ /**
48
+ * Which agent configs to write, and why.
49
+ *
50
+ * 1. `--agent=` wins.
51
+ * 2. Every convention already present in the project (an existing CLAUDE.md,
52
+ * .cursor/rules/agentmd.mdc, …) is regenerated.
53
+ * 3. Otherwise the environment decides: a `.cursor/` dir means Cursor, a
54
+ * `.claude/` dir or the `claude` binary means Claude Code, and so on.
55
+ * 4. When nothing is detectable, both CLAUDE.md and AGENTS.md are written —
56
+ * the two files the most agents read — and the output says so. Writing
57
+ * only AGENTS.md left Claude Code and Cursor users thinking nothing happened.
58
+ */
59
+ function resolveTargets(cwd, requested, env = process.env) {
60
+ if (requested) {
61
+ const names = requested.split(",").map((s) => s.trim()).filter(Boolean);
62
+ if (names.includes("all")) return { names: Object.keys(TARGETS), reason: "--agent=all" };
63
+ const unknown = names.filter((n) => !Object.prototype.hasOwnProperty.call(TARGETS, n));
64
+ if (unknown.length) {
65
+ console.error(pc.red(`Unknown agent: ${unknown.join(", ")}`));
66
+ console.error(pc.dim(` Available: ${Object.keys(TARGETS).join(", ")}, all`));
67
+ process.exit(1);
68
+ }
69
+ return { names, reason: "--agent" };
70
+ }
71
+
72
+ const present = Object.keys(TARGETS).filter((name) => fs.existsSync(path.join(cwd, TARGETS[name].file)));
73
+ if (present.length > 0) return { names: present, reason: `found ${present.map((n) => TARGETS[n].file).join(", ")}` };
74
+
75
+ const detected = [];
76
+ const via = [];
77
+ for (const sig of ENV_SIGNALS) {
78
+ if (detected.includes(sig.agent) || !sig.test(cwd, env)) continue;
79
+ detected.push(sig.agent);
80
+ via.push(sig.via);
81
+ }
82
+ if (detected.length > 0) return { names: detected, reason: `detected ${via.join(", ")}` };
83
+
84
+ return { names: ["claude", "agents"], reason: "no agent detected — writing the two files most agents read" };
85
+ }
86
+
87
+ function link(args) {
88
+ args = args || [];
89
+ const dry = args.includes("--dry");
90
+ const cwd = process.cwd();
91
+ const manifest = readManifest(cwd);
92
+
93
+ if (manifest.presets.length === 0) {
94
+ console.log(pc.yellow("\nNo packages installed — nothing to link."));
95
+ console.log(pc.dim(` Run '${A} init' to get recommendations for this project.\n`));
96
+ return;
97
+ }
98
+
99
+ const { names, reason } = resolveTargets(cwd, flagValue(args, "agent"));
100
+ // Version pins are re-read on every link so a bumped dependency updates
101
+ // the agent file the next time someone links.
102
+ const pins = detectVersions(cwd);
103
+
104
+ console.log(
105
+ pc.bold(`\nLinking ${pc.cyan(manifest.presets.length)} package${manifest.presets.length === 1 ? "" : "s"}${pins.length ? ` + ${pc.cyan(pins.length)} version pin${pins.length === 1 ? "" : "s"}` : ""} into ${names.length} agent config${names.length === 1 ? "" : "s"}\n`)
106
+ );
107
+ for (const p of pins) console.log(` ${pc.dim("pin")} ${pc.white(`${p.label} ${p.version}`.padEnd(22))} ${pc.dim(p.via)}`);
108
+ if (pins.length) console.log("");
109
+ console.log(pc.dim(` targets: ${names.map((n) => TARGETS[n].file).join(", ")} (${reason})`));
110
+
111
+ /**
112
+ * A preset's text, with the project's own override winning. Drop a file at
113
+ * .agentmd/overrides/<preset-file> to replace a registry standard for the
114
+ * whole project; a workspace-level overrides/ dir replaces it for that
115
+ * workspace only (see below).
116
+ */
117
+ const overridesAt = (dir) => (p) => {
118
+ const o = path.join(dir, ".agentmd", "overrides", path.basename(p.file));
119
+ return fs.existsSync(o) ? fs.readFileSync(o, "utf-8") : null;
120
+ };
121
+ const rootOverride = overridesAt(cwd);
122
+ const readPreset = (p) => {
123
+ const o = rootOverride(p);
124
+ if (o !== null) return o;
125
+ try { return fs.readFileSync(resolvePresetPath(cwd, path.basename(p.file)), "utf-8"); } catch { return null; }
126
+ };
127
+
128
+ const plan = planAlwaysOn(manifest.presets, pins, readPreset);
129
+ console.log(pc.dim(` always-on: ~${plan.used} of ${plan.budget} tokens · ${plan.core.length} package${plan.core.length === 1 ? "" : "s"} in core · ${plan.demoted.length} on demand\n`));
130
+
131
+ for (const name of names) {
132
+ const target = TARGETS[name];
133
+ const filePath = resolveInProject(cwd, target.file);
134
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : null;
135
+ const next = applyToFile(target, existing, manifest.presets, pins, { readPreset });
136
+
137
+ if (existing === next) {
138
+ console.log(` ${pc.dim("=")} ${pc.white(target.file.padEnd(38))} ${pc.dim("already up to date")}`);
139
+ } else if (dry) {
140
+ console.log(` ${pc.yellow("~")} ${pc.white(target.file.padEnd(38))} ${pc.dim(existing ? "would update" : "would create")}`);
141
+ } else {
142
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
143
+ fs.writeFileSync(filePath, next, "utf-8");
144
+ console.log(` ${pc.green("✓")} ${pc.white(target.file.padEnd(38))} ${pc.dim(existing ? "updated" : "created")} ${pc.dim("— " + target.label)}`);
145
+ }
146
+
147
+ // On-demand references in the target's own convention. Stale files from
148
+ // removed packages are deleted so the directory mirrors the manifest.
149
+ const ref = REFERENCE_DIRS[name];
150
+ if (!ref || dry) continue;
151
+ const rules = referenceRules(manifest.presets, readPreset, name);
152
+ const dir = resolveInProject(cwd, ref.dir);
153
+ fs.mkdirSync(dir, { recursive: true });
154
+ const keep = new Set(rules.map((r) => path.basename(r.file)));
155
+ for (const r of rules) fs.writeFileSync(resolveInProject(cwd, r.file), r.content, "utf-8");
156
+ let removed = 0;
157
+ for (const f of fs.readdirSync(dir)) {
158
+ if (ref.pattern.test(f) && !keep.has(f)) { fs.unlinkSync(path.join(dir, f)); removed++; }
159
+ }
160
+ const shown = `${ref.dir.split(path.sep).join("/")}/agentmd-*`;
161
+ console.log(` ${pc.green("✓")} ${pc.white(shown.padEnd(38))} ${pc.dim(`${rules.length} on-demand reference${rules.length === 1 ? "" : "s"}${removed ? `, ${removed} stale removed` : ""}`)}`);
162
+ }
163
+
164
+ // Nested workspaces: a monorepo package gets its own file carrying only the
165
+ // standards its stack selects, so apps/web reads the Next.js core and
166
+ // services/api the Go one. Agents read the nearest file, so this overrides
167
+ // the root for everything under the workspace.
168
+ if (!dry) {
169
+ for (const ws of findWorkspaces(cwd).filter((w) => w !== ".")) {
170
+ const found = scanOne(path.join(cwd, ws));
171
+ const wanted = new Set([...found.keys()].flatMap((sig) => RECOMMENDATIONS[sig] || []));
172
+ const wsOverride = overridesAt(path.join(cwd, ws));
173
+ const relevant = manifest.presets.filter((p) => wanted.has(`${p.category}/${p.preset}`) || wsOverride(p) !== null);
174
+ if (!relevant.length) continue;
175
+
176
+ const up = path.posix.relative(ws.split(path.sep).join("/"), ".") || ".";
177
+ const scoped = relevant.map((p) => ({
178
+ ...p,
179
+ file: wsOverride(p) !== null ? `.agentmd/overrides/${path.basename(p.file)}` : path.posix.join(up, p.file.split(path.sep).join("/")),
180
+ }));
181
+ const wsRead = (p) => wsOverride(p) ?? readPreset(p);
182
+
183
+ for (const name of names) {
184
+ const target = TARGETS[name];
185
+ if (target.format !== "markdown" || name === "copilot") continue; // Copilot reads the root file only
186
+ const filePath = resolveInProject(cwd, path.join(ws, target.file));
187
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : null;
188
+ const next = applyToFile(target, existing, scoped, [], { readPreset: wsRead, scope: ws.split(path.sep).join("/") });
189
+ if (existing === next) continue;
190
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
191
+ fs.writeFileSync(filePath, next, "utf-8");
192
+ console.log(` ${pc.green("✓")} ${pc.white(path.posix.join(ws.split(path.sep).join("/"), target.file).padEnd(38))} ${pc.dim(`${existing ? "updated" : "created"} — workspace, ${scoped.length} package${scoped.length === 1 ? "" : "s"}`)}`);
193
+ }
194
+ }
195
+ }
196
+
197
+ console.log("");
198
+ if (dry) {
199
+ console.log(pc.dim(` --dry: nothing written. Run '${A} link' to apply.\n`));
200
+ } else {
201
+ console.log(pc.dim(" Your agent will pick these up on its next run."));
202
+ console.log(pc.dim(` Add another target with: ${A} link --agent=cursor,gemini\n`));
203
+ }
204
+ }
205
+
206
+ module.exports = { link, resolveTargets };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * list.js
3
+ *
4
+ * Handles: agentmd list [model[/category]] [--installed]
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const pc = require("picocolors");
10
+ const { listModels, listCategories, getCategoryPresets, getAllPresets, getPreset } = require("../lib/registry");
11
+ const { parseCoordinates } = require("../lib/coordinates");
12
+ const { readManifest } = require("../lib/manifest");
13
+ const { cmd } = require("../lib/invocation");
14
+
15
+ /** How the user invoked us — "npx activate-agentmd" or "agentmd". */
16
+ const A = cmd();
17
+
18
+ function listInstalled() {
19
+ const manifest = readManifest(process.cwd());
20
+
21
+ if (manifest.presets.length === 0) {
22
+ console.log(pc.yellow("\nNo packages installed in this project."));
23
+ console.log(pc.dim(` Run '${A} init' to get recommendations.\n`));
24
+ return;
25
+ }
26
+
27
+ console.log(pc.bold(`\n${manifest.presets.length} package${manifest.presets.length === 1 ? "" : "s"} installed\n`));
28
+ const width = Math.max(...manifest.presets.map((p) => p.id.length));
29
+ for (const p of manifest.presets) {
30
+ const stale = getPreset(p.model, p.category, p.preset) ? "" : pc.red(" (not in registry)");
31
+ console.log(` ${pc.white(p.id.padEnd(width))} ${pc.dim("v" + (p.version || "?"))}${stale}`);
32
+ }
33
+ console.log("");
34
+ console.log(pc.dim(` Check for updates: ${A} outdated`));
35
+ console.log(pc.dim(` Wire into agents: ${A} link\n`));
36
+ }
37
+
38
+ function list(args) {
39
+ args = args || [];
40
+ if (args.includes("--installed") || args.includes("-i")) return listInstalled();
41
+
42
+ const raw = args.find((a) => !a.startsWith("--")) || "";
43
+
44
+ if (!raw) {
45
+ console.log(pc.bold("\nModels in the registry:\n"));
46
+ for (const model of listModels()) {
47
+ const categories = listCategories(model);
48
+ const total = getAllPresets(model).length;
49
+ console.log(` ${pc.cyan(model.padEnd(10))} ${pc.dim(`${categories.length} categories · ${total} packages`)}`);
50
+ }
51
+ console.log("");
52
+ console.log(pc.dim(` Categories: ${A} list claude`));
53
+ console.log(pc.dim(` This project: ${A} list --installed`));
54
+ console.log(pc.dim(` Recommendations: ${A} init\n`));
55
+ return;
56
+ }
57
+
58
+ const coords = parseCoordinates(raw);
59
+
60
+ if (!listModels().includes(coords.model)) {
61
+ console.error(pc.red(`Unknown model: "${coords.model}"`));
62
+ console.error(pc.dim(` Available: ${listModels().join(", ")}`));
63
+ process.exit(1);
64
+ }
65
+
66
+ if (!coords.category) {
67
+ const categories = listCategories(coords.model);
68
+ console.log(pc.bold(`\nCategories in ${pc.cyan(coords.model)}:\n`));
69
+ for (const category of categories) {
70
+ const count = getCategoryPresets(coords.model, category).length;
71
+ console.log(` ${pc.white(category.padEnd(16))} ${pc.dim(count + " package" + (count === 1 ? "" : "s"))}`);
72
+ }
73
+ console.log("");
74
+ console.log(pc.dim(` See a category: ${A} list ${coords.model}/Security`));
75
+ console.log(pc.dim(` Install one: ${A} install ${coords.model}/Security/owasp\n`));
76
+ return;
77
+ }
78
+
79
+ const entries = getCategoryPresets(coords.model, coords.category);
80
+ if (entries.length === 0) {
81
+ console.error(pc.red(`Category not found: ${coords.model}/${coords.category}`));
82
+ console.error(pc.dim(`\n Available in ${coords.model}:`));
83
+ console.error(pc.dim(" " + listCategories(coords.model).join(", ")));
84
+ process.exit(1);
85
+ }
86
+
87
+ console.log(pc.bold(`\n${entries.length} packages in ${pc.cyan(entries[0].model + "/" + entries[0].category)}\n`));
88
+ const width = Math.max(...entries.map((e) => e.preset.length));
89
+ for (const entry of entries) {
90
+ const description = entry.description ? pc.dim(entry.description.slice(0, 72)) : "";
91
+ console.log(` ${pc.white(entry.preset.padEnd(width))} ${description}`);
92
+ }
93
+ console.log("");
94
+ console.log(pc.dim(` Details: ${A} info ${entries[0].id}`));
95
+ console.log(pc.dim(` Install one: ${A} install ${entries[0].id}`));
96
+ console.log(pc.dim(` Install all: ${A} install ${entries[0].model}/${entries[0].category}/\n`));
97
+ }
98
+
99
+ module.exports = { list };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * outdated.js
3
+ *
4
+ * Handles: agentmd outdated
5
+ *
6
+ * Reports which installed packages have moved on in the registry, which you
7
+ * have edited locally, and which are both (a conflict `update` won't silently
8
+ * resolve). Read-only — it never writes.
9
+ *
10
+ * Exits 1 when anything needs attention, so CI can gate on it.
11
+ */
12
+
13
+ "use strict";
14
+
15
+ const pc = require("picocolors");
16
+ const { readManifest } = require("../lib/manifest");
17
+ const { checkAll } = require("../lib/status");
18
+ const { cmd } = require("../lib/invocation");
19
+
20
+ /** How the user invoked us — "npx activate-agentmd" or "agentmd". */
21
+ const A = cmd();
22
+
23
+ const PRESENTATION = {
24
+ outdated: { mark: pc.yellow("↑"), note: "update available" },
25
+ modified: { mark: pc.cyan("✎"), note: "edited locally" },
26
+ conflict: { mark: pc.red("!"), note: "edited locally AND updated upstream" },
27
+ missing: { mark: pc.red("?"), note: "file missing from disk" },
28
+ removed: { mark: pc.red("✗"), note: "no longer in the registry" },
29
+ failed: { mark: pc.red("✗"), note: "check failed" },
30
+ };
31
+
32
+ async function outdated() {
33
+ const cwd = process.cwd();
34
+ const manifest = readManifest(cwd);
35
+
36
+ if (manifest.presets.length === 0) {
37
+ console.log(pc.yellow("\nNo packages installed."));
38
+ console.log(pc.dim(` Run '${A} init' to get recommendations for this project.\n`));
39
+ return;
40
+ }
41
+
42
+ // Extracted (local/) packages have no registry counterpart; they are yours.
43
+ const local = manifest.presets.filter((p) => p.source === "local");
44
+ const registryPresets = manifest.presets.filter((p) => p.source !== "local");
45
+ for (const p of local) console.log(` ${pc.dim("·")} ${pc.white(p.id)} ${pc.dim("local — managed by extract")}`);
46
+ if (registryPresets.length === 0) {
47
+ console.log(pc.dim("\nNo registry packages to check.\n"));
48
+ return;
49
+ }
50
+
51
+ console.log(pc.dim(`\nChecking ${registryPresets.length} package${registryPresets.length === 1 ? "" : "s"} against the registry…\n`));
52
+
53
+ const results = await checkAll(cwd, registryPresets);
54
+ const notable = results.filter((r) => r.state !== "current");
55
+
56
+ if (notable.length === 0) {
57
+ console.log(pc.green(`✓ All ${results.length} packages are up to date.\n`));
58
+ return;
59
+ }
60
+
61
+ const width = Math.max(...notable.map((r) => r.entry.id.length));
62
+ for (const result of notable) {
63
+ const p = PRESENTATION[result.state];
64
+ const detail = result.error ? `${p.note}: ${result.error}` : p.note;
65
+ console.log(` ${p.mark} ${pc.white(result.entry.id.padEnd(width))} ${pc.dim(detail)}`);
66
+ }
67
+
68
+ const counts = notable.reduce((acc, r) => ({ ...acc, [r.state]: (acc[r.state] || 0) + 1 }), {});
69
+ console.log("");
70
+ console.log(
71
+ pc.bold(
72
+ Object.entries(counts)
73
+ .map(([state, n]) => `${n} ${state}`)
74
+ .join(", ")
75
+ ) + pc.dim(`, ${results.length - notable.length} current`)
76
+ );
77
+
78
+ if (counts.outdated || counts.missing) {
79
+ console.log(pc.dim(`\n Apply updates: ${A} update`));
80
+ }
81
+ if (counts.conflict) {
82
+ console.log(pc.dim(` Conflicts are left alone. Review the diff, then: ${A} update --force`));
83
+ }
84
+ if (counts.removed) {
85
+ console.log(pc.dim(` Removed packages: ${A} remove <id>`));
86
+ }
87
+ console.log("");
88
+
89
+ process.exitCode = 1;
90
+ }
91
+
92
+ module.exports = { outdated };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * remove.js
3
+ *
4
+ * Handles: agentmd remove <model/category/preset>
5
+ *
6
+ * Deletes the installed file and drops the manifest entry. Only ever touches
7
+ * paths recorded in the manifest and resolved back inside .agentmd/presets/.
8
+ */
9
+
10
+ "use strict";
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const pc = require("picocolors");
15
+ const { cmd } = require("../lib/invocation");
16
+
17
+ /** How the user invoked us — "npx activate-agentmd" or "agentmd". */
18
+ const A = cmd();
19
+ const {
20
+ readManifest,
21
+ writeManifest,
22
+ removePreset,
23
+ findPreset,
24
+ getPresetsDir,
25
+ } = require("../lib/manifest");
26
+
27
+ function remove(args) {
28
+ const positional = (args || []).filter((a) => !a.startsWith("--"));
29
+ if (positional.length === 0) {
30
+ console.error(pc.red(`Usage: ${A} remove <model/category/preset>`));
31
+ console.error(pc.dim(` Example: ${A} remove claude/Security/owasp`));
32
+ console.error(pc.dim(` See installed packages: ${A} list --installed`));
33
+ process.exit(1);
34
+ }
35
+
36
+ const cwd = process.cwd();
37
+ const manifest = readManifest(cwd);
38
+ const id = positional[0].replace(/^\/+|\/+$/g, "");
39
+
40
+ const entry = findPreset(manifest, id);
41
+ if (!entry) {
42
+ console.error(pc.red(`Not installed: ${id}`));
43
+ if (manifest.presets.length > 0) {
44
+ console.error(pc.dim("\n This project has:"));
45
+ for (const p of manifest.presets.slice(0, 10)) console.error(pc.dim(` ${p.id}`));
46
+ if (manifest.presets.length > 10) console.error(pc.dim(` …and ${manifest.presets.length - 10} more`));
47
+ } else {
48
+ console.error(pc.dim(" No packages are installed in this project."));
49
+ }
50
+ process.exit(1);
51
+ }
52
+
53
+ // Re-derive the path from the presets dir rather than trusting the manifest
54
+ // string, so a tampered manifest can't point rm at something outside it.
55
+ const presetsDir = path.resolve(getPresetsDir(cwd));
56
+ const filePath = path.resolve(cwd, entry.file);
57
+ if (!filePath.startsWith(presetsDir + path.sep)) {
58
+ console.error(pc.red(`Refusing to delete ${entry.file} — it is outside .agentmd/presets/`));
59
+ process.exit(1);
60
+ }
61
+
62
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
63
+ removePreset(manifest, entry.id);
64
+ writeManifest(cwd, manifest);
65
+
66
+ console.log("");
67
+ console.log(pc.green(`✓ Removed ${pc.cyan(entry.id)}`) + pc.dim(` ${entry.file}`));
68
+ console.log(pc.dim(` ${manifest.presets.length} package${manifest.presets.length === 1 ? "" : "s"} remain`));
69
+ console.log(pc.dim(` Run '${A} link' to update your agent config files.\n`));
70
+ }
71
+
72
+ module.exports = { remove };