dme-agent 7.4.1 → 7.4.2

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/src/install.mjs CHANGED
@@ -1,17 +1,34 @@
1
1
  /**
2
- * DME bootstrap installer — copies manifesto, system prompt, skills, rules
3
- * into every major agent CLI surface.
2
+ * DME bootstrap installer — detect-only by default, AGENTS.md conflict prompts.
4
3
  */
5
4
 
6
5
  import fs from "node:fs";
7
6
  import path from "node:path";
8
7
  import { fileURLToPath } from "node:url";
9
- import { filterTargets } from "./targets.mjs";
10
- import { ok, fail, skip, info, warn, step, hr, box, table } from "./ui.mjs";
8
+ import { resolveInstallTargets, getDetectedTargets, formatScanReport } from "./detect.mjs";
9
+ import { getTargets } from "./targets.mjs";
10
+ import {
11
+ ok,
12
+ fail,
13
+ skip,
14
+ info,
15
+ warn,
16
+ step,
17
+ hr,
18
+ box,
19
+ table,
20
+ progressBar,
21
+ asciiLogo,
22
+ c,
23
+ } from "./ui.mjs";
24
+ import { agentsConflictPromptInline, multiSelect, printScanTable } from "./tui.mjs";
11
25
 
12
26
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
27
  const PKG_ROOT = path.resolve(__dirname, "..");
14
28
  const ASSETS = path.join(PKG_ROOT, "assets");
29
+ const VERSION = "7.4.2";
30
+ const MARKER_BEGIN = "<!-- DME-AGENT-7.4 BEGIN -->";
31
+ const MARKER_END = "<!-- DME-AGENT-7.4 END -->";
15
32
 
16
33
  const SKILL_NAMES = [
17
34
  "dme-design",
@@ -33,45 +50,34 @@ function readAsset(...parts) {
33
50
  return fs.readFileSync(p, "utf8");
34
51
  }
35
52
 
36
- function writeFile(filePath, content, { appendMarker } = {}) {
53
+ function escapeRe(s) {
54
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
55
+ }
56
+
57
+ function writeRaw(filePath, content) {
37
58
  ensureDir(path.dirname(filePath));
38
- if (appendMarker && fs.existsSync(filePath)) {
39
- const existing = fs.readFileSync(filePath, "utf8");
40
- if (existing.includes(appendMarker)) {
41
- // replace block between markers
42
- const re = new RegExp(
43
- `${escapeRe(appendMarker)}[\\s\\S]*?${escapeRe(appendMarker.replace("BEGIN", "END"))}`,
44
- "m"
45
- );
46
- const endMarker = appendMarker.replace("BEGIN", "END");
47
- const block = `${appendMarker}\n${content.trim()}\n${endMarker}\n`;
48
- if (re.test(existing)) {
49
- fs.writeFileSync(filePath, existing.replace(re, block.trimEnd() + "\n"), "utf8");
50
- } else {
51
- fs.writeFileSync(
52
- filePath,
53
- existing.trimEnd() + "\n\n" + block,
54
- "utf8"
55
- );
56
- }
57
- return "updated";
58
- }
59
- const endMarker = appendMarker.replace("BEGIN", "END");
60
- fs.writeFileSync(
61
- filePath,
62
- existing.trimEnd() +
63
- "\n\n" +
64
- `${appendMarker}\n${content.trim()}\n${endMarker}\n`,
65
- "utf8"
66
- );
67
- return "appended";
68
- }
69
59
  fs.writeFileSync(filePath, content, "utf8");
70
- return fs.existsSync(filePath) ? "written" : "written";
60
+ return "written";
71
61
  }
72
62
 
73
- function escapeRe(s) {
74
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
63
+ function mergeBlock(filePath, content) {
64
+ ensureDir(path.dirname(filePath));
65
+ const block = `${MARKER_BEGIN}\n${content.trim()}\n${MARKER_END}\n`;
66
+ if (!fs.existsSync(filePath)) {
67
+ fs.writeFileSync(filePath, block, "utf8");
68
+ return "created";
69
+ }
70
+ const existing = fs.readFileSync(filePath, "utf8");
71
+ const re = new RegExp(
72
+ `${escapeRe(MARKER_BEGIN)}[\\s\\S]*?${escapeRe(MARKER_END)}\\n?`,
73
+ "m"
74
+ );
75
+ if (re.test(existing)) {
76
+ fs.writeFileSync(filePath, existing.replace(re, block), "utf8");
77
+ return "updated";
78
+ }
79
+ fs.writeFileSync(filePath, existing.trimEnd() + "\n\n" + block, "utf8");
80
+ return "appended";
75
81
  }
76
82
 
77
83
  function copySkill(skillName, destRoot) {
@@ -92,15 +98,22 @@ function systemPrompt() {
92
98
  );
93
99
  }
94
100
 
101
+ function agentsDoctrine() {
102
+ return (
103
+ readAsset("agent", "AGENTS.md") ||
104
+ readAsset("agent", "PROMPT-COMPACT.md") ||
105
+ systemPrompt()
106
+ );
107
+ }
108
+
95
109
  function compactPrompt() {
96
- return readAsset("agent", "PROMPT-COMPACT.md") || systemPrompt();
110
+ return readAsset("agent", "PROMPT-COMPACT.md") || agentsDoctrine();
97
111
  }
98
112
 
99
113
  function manifesto() {
100
114
  return (
101
115
  readAsset("manifesto", "DME-Agent-v7.4.md") ||
102
- readAsset("manifesto", "MANIFESTO.md") ||
103
- systemPrompt()
116
+ agentsDoctrine()
104
117
  );
105
118
  }
106
119
 
@@ -110,7 +123,7 @@ function agentProfile() {
110
123
 
111
124
  function cursorRule() {
112
125
  return `---
113
- description: DME Agent 7.4 — Neominimal Identity Engine. Apply on any UI/UX/frontend work.
126
+ description: DME Agent 7.4.2 — Neominimal Identity Engine. Apply on any UI/UX/frontend work.
114
127
  globs:
115
128
  - "**/*.{tsx,jsx,vue,svelte,css,html}"
116
129
  - "**/components/**"
@@ -119,38 +132,185 @@ globs:
119
132
  alwaysApply: false
120
133
  ---
121
134
 
122
- ${compactPrompt()}
135
+ ${agentsDoctrine()}
123
136
  `;
124
137
  }
125
138
 
126
- function rulesSnippet() {
127
- return compactPrompt();
139
+ function isAgentsLikePath(filePath) {
140
+ const base = path.basename(filePath).toLowerCase();
141
+ return (
142
+ base === "agents.md" ||
143
+ base === "claude.md" ||
144
+ base === "gemini.md" ||
145
+ base === "conventions.md" ||
146
+ base === "copilot-instructions.md" ||
147
+ base === ".cursorrules" ||
148
+ base === ".windsurfrules"
149
+ );
128
150
  }
129
151
 
130
152
  /**
131
- * @param {{ targets?: string[], force?: boolean, dryRun?: boolean, cwdProject?: boolean }} opts
153
+ * Resolve policy for an existing AGENTS-like file.
154
+ * @returns {Promise<"merge"|"overwrite"|"skip">}
155
+ */
156
+ async function resolveAgentsPolicy(filePath, opts) {
157
+ if (opts.agentsPolicy && opts.agentsPolicy !== "ask") {
158
+ return opts.agentsPolicy;
159
+ }
160
+ if (opts.force) return "merge";
161
+ if (opts.dryRun) return "merge";
162
+ if (!fs.existsSync(filePath)) return "overwrite"; // new file — write full doctrine
163
+
164
+ const existing = fs.readFileSync(filePath, "utf8");
165
+ // already only DME? safe to overwrite
166
+ if (
167
+ existing.includes("DME Agent 7.4") &&
168
+ existing.includes("Neominimal Identity") &&
169
+ existing.length < 12000 &&
170
+ !existing.includes(MARKER_BEGIN)
171
+ ) {
172
+ // might be previous full DME — overwrite with stronger version
173
+ return "overwrite";
174
+ }
175
+
176
+ if (opts.quiet) return "merge";
177
+
178
+ return agentsConflictPromptInline(filePath);
179
+ }
180
+
181
+ async function writeAgentsFile(filePath, doctrine, opts, row) {
182
+ if (opts.dryRun) {
183
+ row.files.push(`[dry] agents → ${filePath}`);
184
+ return;
185
+ }
186
+
187
+ if (!fs.existsSync(filePath)) {
188
+ writeRaw(filePath, doctrine);
189
+ row.files.push(`${filePath} (created)`);
190
+ console.log(ok(`AGENTS created ${filePath}`));
191
+ return;
192
+ }
193
+
194
+ const policy = await resolveAgentsPolicy(filePath, opts);
195
+ if (policy === "skip") {
196
+ row.files.push(`${filePath} (skipped)`);
197
+ console.log(skip(`AGENTS skipped ${filePath}`));
198
+ return;
199
+ }
200
+ if (policy === "overwrite") {
201
+ writeRaw(filePath, doctrine);
202
+ row.files.push(`${filePath} (overwritten)`);
203
+ console.log(warn(`AGENTS overwrite ${filePath}`));
204
+ return;
205
+ }
206
+ // merge
207
+ const mode = mergeBlock(filePath, doctrine);
208
+ row.files.push(`${filePath} (${mode})`);
209
+ console.log(ok(`AGENTS ${mode.padEnd(9)} ${filePath}`));
210
+ }
211
+
212
+ /**
213
+ * @param {{
214
+ * targets?: string[],
215
+ * force?: boolean,
216
+ * dryRun?: boolean,
217
+ * cwdProject?: boolean,
218
+ * agentsPolicy?: "ask"|"merge"|"overwrite"|"skip",
219
+ * interactive?: boolean,
220
+ * quiet?: boolean,
221
+ * pick?: boolean,
222
+ * }} opts
132
223
  */
133
224
  export async function installDme(opts = {}) {
134
- const targets = filterTargets(opts.targets);
135
- const results = [];
136
- const total = 6;
225
+ const interactive = opts.interactive !== false && process.stdin.isTTY;
226
+ let targetIds = opts.targets;
227
+
228
+ // Default = detected only
229
+ if (!targetIds || targetIds.length === 0) {
230
+ targetIds = ["detected"];
231
+ }
232
+
233
+ let targets = resolveInstallTargets(targetIds, { force: opts.force });
234
+
235
+ // Interactive multi-select among detected
236
+ if (
237
+ interactive &&
238
+ opts.pick !== false &&
239
+ (targetIds.includes("detected") || targetIds.length === 0) &&
240
+ !opts.quiet
241
+ ) {
242
+ const detected = getDetectedTargets();
243
+ if (detected.length === 0) {
244
+ console.log(asciiLogo(VERSION));
245
+ console.log(
246
+ fail("No AI CLIs detected on this machine.")
247
+ );
248
+ console.log(
249
+ info("Run with --targets all or --targets claude,cursor to force.")
250
+ );
251
+ console.log(info("Or: dme scan to see the detection report."));
252
+ console.log("");
253
+ return { ok: false, results: [], reason: "none-detected" };
254
+ }
255
+
256
+ const selected = await multiSelect(
257
+ "Select CLIs to install DME into",
258
+ detected.map((t) => ({
259
+ id: t.id,
260
+ label: t.name,
261
+ detail: t.agentsMd ? path.basename(t.agentsMd) : "",
262
+ checked: true,
263
+ }))
264
+ );
137
265
 
138
- console.log(step(1, total, "Reading DME 7.4 assets…"));
266
+ if (selected.length === 0) {
267
+ console.log(warn("Nothing selected. Abort."));
268
+ return { ok: false, results: [], reason: "none-selected" };
269
+ }
270
+ targets = resolveInstallTargets(selected, { force: true });
271
+ }
272
+
273
+ if (targets.length === 0) {
274
+ console.log(fail("No targets to install."));
275
+ console.log(info("dme scan — see detection"));
276
+ console.log(info("dme install --targets all — force every surface"));
277
+ return { ok: false, results: [] };
278
+ }
279
+
280
+ const results = [];
281
+ const total = 5;
282
+ const doctrine = agentsDoctrine();
139
283
  const sys = systemPrompt();
140
- const compact = compactPrompt();
141
284
  const full = manifesto();
142
285
  const profile = agentProfile();
286
+ const compact = compactPrompt();
143
287
 
144
- if (!sys || sys.length < 100) {
145
- console.log(fail("System prompt asset missing or empty."));
288
+ console.log(asciiLogo(VERSION));
289
+ console.log(hr());
290
+ console.log(step(1, total, "Loading DME doctrine…"));
291
+ if (!doctrine || doctrine.length < 80) {
292
+ console.log(fail("AGENTS.md asset missing."));
146
293
  return { ok: false, results };
147
294
  }
148
- console.log(ok(`Manifesto ${full.length} chars · system ${sys.length} chars`));
295
+ console.log(
296
+ ok(
297
+ `doctrine ${doctrine.length} chars · manifesto ${full.length} · system ${sys.length}`
298
+ )
299
+ );
300
+ console.log(
301
+ info(
302
+ `targets: ${targets.map((t) => t.id).join(", ")} ${c.dim}(detected-only by default)${c.reset}`
303
+ )
304
+ );
149
305
  console.log(hr());
150
306
 
151
- console.log(step(2, total, `Installing into ${targets.length} surfaces…`));
152
-
307
+ console.log(step(2, total, `Installing ${targets.length} surface(s)…`));
308
+ let i = 0;
153
309
  for (const t of targets) {
310
+ i++;
311
+ console.log(
312
+ `\n ${c.brightMagenta}${c.bold}▸ ${t.name}${c.reset} ${c.dim}${t.id}${c.reset} ${progressBar(i, targets.length, 16)}`
313
+ );
154
314
  const row = { id: t.id, name: t.name, files: [], errors: [] };
155
315
  try {
156
316
  // skills
@@ -164,6 +324,7 @@ export async function installDme(opts = {}) {
164
324
  const dest = copySkill(skill, skillDir);
165
325
  if (dest) row.files.push(dest);
166
326
  }
327
+ console.log(ok(`skills → ${skillDir}`));
167
328
  }
168
329
 
169
330
  // agent profiles
@@ -172,82 +333,68 @@ export async function installDme(opts = {}) {
172
333
  row.files.push(`[dry] agent → ${af}`);
173
334
  continue;
174
335
  }
175
- const content =
176
- af.endsWith(".mdc") || af.includes("rules")
177
- ? cursorRule()
178
- : profile.includes("name: dme")
179
- ? profile
180
- : profile;
181
- writeFile(af, af.includes("cursor") && af.endsWith(".md") ? profile : content);
182
- // for agent md files use profile; for CLAUDE_DME use full system
183
336
  if (af.includes("CLAUDE_DME") || af.includes("instructions")) {
184
- writeFile(af, sys);
185
- } else if (af.endsWith("dme-v7.4.md")) {
186
- writeFile(af, profile);
337
+ writeRaw(af, sys);
338
+ } else {
339
+ writeRaw(af, profile);
187
340
  }
188
341
  row.files.push(af);
189
342
  }
190
343
 
191
- // rules append or write dedicated
344
+ // Canonical AGENTS.md for this CLI
345
+ if (t.agentsMd) {
346
+ await writeAgentsFile(t.agentsMd, doctrine, opts, row);
347
+ }
348
+
349
+ // Other rules files
192
350
  for (const rf of t.rulesFiles) {
193
351
  if (opts.dryRun) {
194
352
  row.files.push(`[dry] rules → ${rf}`);
195
353
  continue;
196
354
  }
355
+ // skip if same as agentsMd (already handled)
356
+ if (t.agentsMd && path.resolve(rf) === path.resolve(t.agentsMd)) {
357
+ continue;
358
+ }
359
+
197
360
  ensureDir(path.dirname(rf));
198
361
  const base = path.basename(rf).toLowerCase();
199
362
 
200
363
  if (base === "dme-agent.mdc" || rf.endsWith(".mdc")) {
201
- writeFile(rf, cursorRule());
364
+ writeRaw(rf, cursorRule());
202
365
  row.files.push(rf);
203
366
  continue;
204
367
  }
205
368
 
206
- if (
207
- base === "claude.md" ||
208
- base === "agents.md" ||
209
- base === "agents.md" ||
210
- base === ".cursorrules" ||
211
- base === ".windsurfrules" ||
212
- base === "gemini.md" ||
213
- base === "copilot-instructions.md" ||
214
- base === "dme-conventions.md" ||
215
- base.endsWith("agents.md")
216
- ) {
217
- // append DME block to shared rules so we don't wipe user content
218
- const marker = "<!-- DME-AGENT-7.4 BEGIN -->";
219
- writeFile(rf, rulesSnippet(), { appendMarker: marker });
220
- row.files.push(rf + " (merged)");
369
+ if (isAgentsLikePath(rf)) {
370
+ await writeAgentsFile(rf, doctrine, opts, row);
221
371
  continue;
222
372
  }
223
373
 
224
374
  if (base.includes("dme") || base.includes("conventions")) {
225
- writeFile(rf, sys);
375
+ writeRaw(rf, sys);
226
376
  row.files.push(rf);
227
377
  continue;
228
378
  }
229
379
 
230
- writeFile(rf, compact);
380
+ writeRaw(rf, compact);
231
381
  row.files.push(rf);
232
382
  }
233
383
 
234
- // always drop full manifesto into target root if root exists or is under home config
384
+ // manifesto + system in CLI root
235
385
  if (!opts.dryRun && t.root) {
236
386
  try {
237
387
  ensureDir(t.root);
238
- const manPath = path.join(t.root, "DME-Agent-v7.4.md");
239
- writeFile(manPath, full);
240
- row.files.push(manPath);
241
- const sysPath = path.join(t.root, "DME-SYSTEM.md");
242
- writeFile(sysPath, sys);
243
- row.files.push(sysPath);
388
+ writeRaw(path.join(t.root, "DME-Agent-v7.4.md"), full);
389
+ writeRaw(path.join(t.root, "DME-SYSTEM.md"), sys);
390
+ row.files.push(path.join(t.root, "DME-Agent-v7.4.md"));
244
391
  } catch {
245
- /* ignore optional root */
392
+ /* ignore */
246
393
  }
247
394
  }
248
395
 
249
396
  results.push({ ...row, status: "ok" });
250
- console.log(ok(`${t.name.padEnd(28)} ${row.files.length} paths`));
397
+ console.log(ok(`${t.name} done — ${row.files.length} paths`));
251
398
  } catch (e) {
252
399
  results.push({ ...row, status: "error", error: String(e?.message || e) });
253
400
  console.log(fail(`${t.name}: ${e?.message || e}`));
@@ -255,32 +402,17 @@ export async function installDme(opts = {}) {
255
402
  }
256
403
 
257
404
  console.log(hr());
258
- console.log(step(3, total, "Project-local install (cwd)…"));
405
+ console.log(step(3, total, "Project-local (cwd)…"));
259
406
  if (opts.cwdProject !== false) {
260
407
  try {
261
- const cwd = process.cwd();
262
- const localRoots = [
263
- path.join(cwd, ".grok", "skills"),
264
- path.join(cwd, ".claude", "skills"),
265
- path.join(cwd, ".cursor", "skills"),
266
- path.join(cwd, ".agents", "skills"),
267
- ];
268
- for (const lr of localRoots) {
269
- if (opts.dryRun) continue;
270
- ensureDir(lr);
271
- for (const skill of SKILL_NAMES) copySkill(skill, lr);
272
- }
273
- if (!opts.dryRun) {
274
- writeFile(path.join(cwd, "AGENTS.dme.md"), compact);
275
- writeFile(path.join(cwd, ".dme", "DME-Agent-v7.4.md"), full);
276
- writeFile(path.join(cwd, ".dme", "SYSTEM.md"), sys);
277
- // merge into AGENTS.md if present or create pointer
278
- const agentsMd = path.join(cwd, "AGENTS.md");
279
- writeFile(agentsMd, compact, {
280
- appendMarker: "<!-- DME-AGENT-7.4 BEGIN -->",
281
- });
282
- }
283
- console.log(ok(`Project skills + AGENTS.md in ${cwd}`));
408
+ await installProjectLocal({
409
+ opts,
410
+ doctrine,
411
+ sys,
412
+ full,
413
+ compact,
414
+ targets,
415
+ });
284
416
  } catch (e) {
285
417
  console.log(warn(`Project install: ${e?.message || e}`));
286
418
  }
@@ -289,78 +421,128 @@ export async function installDme(opts = {}) {
289
421
  }
290
422
 
291
423
  console.log(hr());
292
- console.log(step(4, total, "Obsidian-ready manifesto…"));
293
- if (!opts.dryRun) {
294
- const obs = path.join(process.cwd(), "obsidian");
295
- ensureDir(obs);
296
- writeFile(path.join(obs, "DME-Agent-v7.4.md"), full);
297
- console.log(ok(path.join(obs, "DME-Agent-v7.4.md")));
298
- }
299
-
300
- console.log(step(5, total, "Summary"));
424
+ console.log(step(4, total, "Summary"));
301
425
  const okN = results.filter((r) => r.status === "ok").length;
302
426
  const errN = results.filter((r) => r.status === "error").length;
303
427
  console.log(
304
428
  table([
305
- ["version", "7.4.1"],
429
+ ["version", VERSION],
306
430
  ["targets ok", String(okN)],
307
431
  ["errors", String(errN)],
308
- ["skills", SKILL_NAMES.join(", ")],
432
+ ["mode", targetIds.includes("all") ? "all" : "detected"],
433
+ ["skills", String(SKILL_NAMES.length)],
309
434
  ])
310
435
  );
311
436
 
312
437
  console.log(hr());
313
- console.log(step(6, total, "Next"));
438
+ console.log(step(5, total, "Next"));
314
439
  console.log(
315
- box("DME is live", [
316
- "Restart Claude / Cursor / Zed / Grok sessions",
317
- "Slash: /dme-design /dme-dashboard /dme-landing",
318
- "System: ask the agent to follow DME 7.4",
319
- "Full doctrine: DME-Agent-v7.4.md in each root",
320
- ])
440
+ box(
441
+ "DME is live",
442
+ [
443
+ "Restart agent sessions (Claude / Cursor / Grok…)",
444
+ "AGENTS.md is in each CLI’s correct path",
445
+ "Skills: /dme-design /dme-dashboard /dme-landing /dme-ux",
446
+ "dme doctor · dme scan · dme uninstall",
447
+ ],
448
+ "green"
449
+ )
321
450
  );
322
451
  console.log("");
323
- console.log(info("dme doctor — verify install"));
324
- console.log(info("dme update — re-sync latest assets"));
325
- console.log(info("dme uninstall — remove DME blocks (safe merge)"));
326
- console.log("");
327
452
 
328
453
  return { ok: errN === 0, results };
329
454
  }
330
455
 
456
+ async function installProjectLocal({ opts, doctrine, sys, full, compact, targets }) {
457
+ const cwd = process.cwd();
458
+ // Only drop project skill folders for CLIs that were actually installed
459
+ const skillRoots = new Set();
460
+ for (const t of targets) {
461
+ if (t.id === "claude") skillRoots.add(path.join(cwd, ".claude", "skills"));
462
+ if (t.id === "cursor") skillRoots.add(path.join(cwd, ".cursor", "skills"));
463
+ if (t.id === "grok") skillRoots.add(path.join(cwd, ".grok", "skills"));
464
+ if (t.id === "codex") skillRoots.add(path.join(cwd, ".codex", "skills"));
465
+ if (t.id === "zed") skillRoots.add(path.join(cwd, ".agents", "skills"));
466
+ }
467
+ // always allow generic project skills
468
+ skillRoots.add(path.join(cwd, ".agents", "skills"));
469
+
470
+ if (!opts.dryRun) {
471
+ for (const lr of skillRoots) {
472
+ ensureDir(lr);
473
+ for (const skill of SKILL_NAMES) copySkill(skill, lr);
474
+ }
475
+ writeRaw(path.join(cwd, ".dme", "DME-Agent-v7.4.md"), full);
476
+ writeRaw(path.join(cwd, ".dme", "SYSTEM.md"), sys);
477
+ writeRaw(path.join(cwd, ".dme", "AGENTS.md"), doctrine);
478
+ writeRaw(path.join(cwd, "AGENTS.dme.md"), doctrine);
479
+
480
+ const agentsMd = path.join(cwd, "AGENTS.md");
481
+ const row = { files: [] };
482
+ await writeAgentsFile(agentsMd, doctrine, opts, row);
483
+ console.log(ok(`project → ${cwd}`));
484
+ } else {
485
+ console.log(skip(`[dry] project → ${cwd}`));
486
+ }
487
+ }
488
+
331
489
  export function doctor() {
332
- const targets = filterTargets(["all"]);
333
- console.log(step(1, 1, "Scanning DME install…"));
490
+ console.log(asciiLogo(VERSION));
491
+ console.log(step(1, 1, "Scanning DME install health…"));
334
492
  console.log(hr());
335
- for (const t of targets) {
493
+ const report = formatScanReport();
494
+ for (const row of report) {
495
+ const t = getTargets().find((x) => x.id === row.id);
496
+ if (!t) continue;
336
497
  const hits = [];
498
+ if (row.detected) hits.push("cli-present");
337
499
  for (const skillDir of t.skillDirs) {
338
500
  for (const skill of SKILL_NAMES) {
339
- const p = path.join(skillDir, skill, "SKILL.md");
340
- if (fs.existsSync(p)) hits.push(skill);
501
+ if (fs.existsSync(path.join(skillDir, skill, "SKILL.md"))) hits.push(skill);
341
502
  }
342
503
  }
343
- for (const af of t.agentFiles) {
344
- if (fs.existsSync(af)) hits.push(path.basename(af));
345
- }
346
- for (const rf of t.rulesFiles) {
347
- if (fs.existsSync(rf)) {
348
- const txt = fs.readFileSync(rf, "utf8");
349
- if (txt.includes("DME") || txt.includes("7.4")) hits.push(path.basename(rf));
350
- }
504
+ if (t.agentsMd && fs.existsSync(t.agentsMd)) {
505
+ const txt = fs.readFileSync(t.agentsMd, "utf8");
506
+ if (txt.includes("DME") || txt.includes("7.4")) hits.push("agents.md");
507
+ else hits.push("agents.md(no-dme)");
351
508
  }
352
509
  const man = path.join(t.root, "DME-Agent-v7.4.md");
353
510
  if (fs.existsSync(man)) hits.push("manifesto");
354
511
 
355
- if (hits.length) console.log(ok(`${t.name.padEnd(28)} ${hits.length} hits`));
356
- else console.log(skip(`${t.name.padEnd(28)} not installed`));
512
+ if (hits.length && row.detected) {
513
+ console.log(ok(`${t.name.padEnd(24)} ${hits.length} hits ${c.dim}${hits.slice(0, 5).join(", ")}${c.reset}`));
514
+ } else if (row.detected) {
515
+ console.log(warn(`${t.name.padEnd(24)} detected but DME not installed`));
516
+ } else {
517
+ console.log(skip(`${t.name.padEnd(24)} CLI not on this PC`));
518
+ }
357
519
  }
358
520
  console.log("");
359
521
  }
360
522
 
361
- export function uninstall() {
362
- console.log(warn("Uninstall removes DME-named files and <!-- DME-AGENT-7.4 --> blocks."));
363
- const targets = filterTargets(["all"]);
523
+ export function scan() {
524
+ printScanTable(formatScanReport());
525
+ const n = getDetectedTargets().length;
526
+ console.log(
527
+ info(
528
+ `${c.brightGreen}${n}${c.reset} CLI(s) detected — install will target only these by default`
529
+ )
530
+ );
531
+ console.log("");
532
+ }
533
+
534
+ export function uninstall(opts = {}) {
535
+ console.log(asciiLogo(VERSION));
536
+ console.log(warn("Removing DME files & <!-- DME-AGENT-7.4 --> blocks…"));
537
+ console.log(hr());
538
+
539
+ // Only uninstall from detected unless --all
540
+ const targets = opts.all
541
+ ? getTargets()
542
+ : getDetectedTargets().length
543
+ ? getDetectedTargets()
544
+ : getTargets();
545
+
364
546
  for (const t of targets) {
365
547
  for (const skillDir of t.skillDirs) {
366
548
  for (const skill of SKILL_NAMES) {
@@ -372,12 +554,12 @@ export function uninstall() {
372
554
  }
373
555
  }
374
556
  for (const af of t.agentFiles) {
375
- if (fs.existsSync(af) && path.basename(af).includes("dme")) {
557
+ if (fs.existsSync(af) && path.basename(af).toLowerCase().includes("dme")) {
376
558
  fs.unlinkSync(af);
377
559
  console.log(ok(`removed ${af}`));
378
560
  }
379
561
  }
380
- for (const rf of t.rulesFiles) {
562
+ for (const rf of [...t.rulesFiles, t.agentsMd].filter(Boolean)) {
381
563
  if (!fs.existsSync(rf)) continue;
382
564
  const base = path.basename(rf).toLowerCase();
383
565
  if (base.includes("dme") || base.endsWith(".mdc")) {
@@ -386,17 +568,20 @@ export function uninstall() {
386
568
  continue;
387
569
  }
388
570
  let txt = fs.readFileSync(rf, "utf8");
389
- const re =
390
- /<!-- DME-AGENT-7\.4 BEGIN -->[\s\S]*?<!-- DME-AGENT-7\.4 END -->\n?/g;
571
+ const re = new RegExp(
572
+ `${escapeRe(MARKER_BEGIN)}[\\s\\S]*?${escapeRe(MARKER_END)}\\n?`,
573
+ "g"
574
+ );
391
575
  if (re.test(txt)) {
392
576
  txt = txt.replace(re, "");
393
577
  fs.writeFileSync(rf, txt, "utf8");
394
578
  console.log(ok(`stripped DME block from ${rf}`));
395
579
  }
396
580
  }
397
- const man = path.join(t.root, "DME-Agent-v7.4.md");
398
- const sys = path.join(t.root, "DME-SYSTEM.md");
399
- for (const p of [man, sys]) {
581
+ for (const p of [
582
+ path.join(t.root, "DME-Agent-v7.4.md"),
583
+ path.join(t.root, "DME-SYSTEM.md"),
584
+ ]) {
400
585
  if (fs.existsSync(p)) {
401
586
  fs.unlinkSync(p);
402
587
  console.log(ok(`removed ${p}`));