md4ai 0.9.1 → 0.9.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.
Files changed (2) hide show
  1. package/dist/index.bundled.js +403 -303
  2. package/package.json +1 -1
@@ -1198,6 +1198,122 @@ var init_scanner = __esm({
1198
1198
  }
1199
1199
  });
1200
1200
 
1201
+ // dist/output/html-generator.js
1202
+ function generateOfflineHtml(result, projectRoot) {
1203
+ const title = projectRoot.split("/").pop() ?? "MD4AI";
1204
+ const dataJson = JSON.stringify(result);
1205
+ const orphanItems = result.orphans.map((o) => `<li data-path="${escapeHtml(o.path)}">${escapeHtml(o.path)} <span class="meta">(${o.sizeBytes} bytes)</span></li>`).join("\n");
1206
+ const staleItems = result.staleFiles.map((s) => `<li data-path="${escapeHtml(s.path)}">${escapeHtml(s.path)} <span class="stale-meta">(${s.daysSinceModified} days)</span></li>`).join("\n");
1207
+ const skillRows = result.skills.map((s) => `<tr><td>${escapeHtml(s.name)}</td><td>${s.machineWide ? '<span class="check">\u2713</span>' : ""}</td><td>${s.projectSpecific ? '<span class="check">\u2713</span>' : ""}</td><td class="meta">${escapeHtml(s.source)}</td></tr>`).join("\n");
1208
+ const fileItems = result.graph.nodes.map((n) => `<li data-path="${escapeHtml(n.filePath)}" data-type="${n.type}">${escapeHtml(n.filePath)} <span class="meta">(${n.type})</span></li>`).join("\n");
1209
+ return `<!DOCTYPE html>
1210
+ <html lang="en">
1211
+ <head>
1212
+ <meta charset="UTF-8">
1213
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1214
+ <title>MD4AI \u2014 ${escapeHtml(title)}</title>
1215
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
1216
+ <style>
1217
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1218
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
1219
+ h1 { color: #38bdf8; margin-bottom: 0.5rem; }
1220
+ .subtitle { color: #94a3b8; margin-bottom: 2rem; }
1221
+ .section { background: #1e293b; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
1222
+ .section h2 { color: #38bdf8; margin-bottom: 1rem; font-size: 1.2rem; }
1223
+ .mermaid { background: #0f172a; border-radius: 8px; padding: 1rem; overflow-x: auto; }
1224
+ .badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.85rem; margin-left: 0.5rem; }
1225
+ .badge-orphan { background: #ef4444; color: white; }
1226
+ .badge-stale { background: #f59e0b; color: black; }
1227
+ table { width: 100%; border-collapse: collapse; }
1228
+ th, td { padding: 0.5rem 1rem; text-align: left; border-bottom: 1px solid #334155; }
1229
+ th { color: #94a3b8; font-weight: 600; }
1230
+ .check { color: #22c55e; }
1231
+ .meta { color: #94a3b8; }
1232
+ .stale-meta { color: #f59e0b; }
1233
+ .stats { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem; }
1234
+ .stat { text-align: center; }
1235
+ .stat-value { font-size: 2rem; font-weight: bold; color: #38bdf8; }
1236
+ .stat-label { color: #94a3b8; font-size: 0.85rem; }
1237
+ .file-list { list-style: none; }
1238
+ .file-list li { padding: 0.3rem 0; color: #cbd5e1; font-family: monospace; font-size: 0.9rem; }
1239
+ #search { width: 100%; padding: 0.75rem 1rem; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 1rem; margin-bottom: 1rem; }
1240
+ #search:focus { outline: none; border-color: #38bdf8; }
1241
+ @media print { body { background: white; color: black; } .section { border: 1px solid #ccc; } }
1242
+ </style>
1243
+ </head>
1244
+ <body>
1245
+ <h1>MD4AI \u2014 ${escapeHtml(title)}</h1>
1246
+ <p class="subtitle">Scanned: ${new Date(result.scannedAt).toLocaleString()} | Hash: ${result.dataHash.slice(0, 12)}</p>
1247
+
1248
+ <div class="stats">
1249
+ <div class="stat"><div class="stat-value">${result.graph.nodes.length}</div><div class="stat-label">Files</div></div>
1250
+ <div class="stat"><div class="stat-value">${result.graph.edges.length}</div><div class="stat-label">References</div></div>
1251
+ <div class="stat"><div class="stat-value">${result.orphans.length}</div><div class="stat-label">Orphans</div></div>
1252
+ <div class="stat"><div class="stat-value">${result.staleFiles.length}</div><div class="stat-label">Stale</div></div>
1253
+ <div class="stat"><div class="stat-value">${result.skills.length}</div><div class="stat-label">Skills</div></div>
1254
+ </div>
1255
+
1256
+ <input type="text" id="search" placeholder="Search files..." oninput="filterFiles(this.value)">
1257
+
1258
+ <div class="section">
1259
+ <h2>Dependency Graph</h2>
1260
+ <pre class="mermaid">${escapeHtml(result.graph.mermaid)}</pre>
1261
+ </div>
1262
+
1263
+ ${result.orphans.length > 0 ? `
1264
+ <div class="section">
1265
+ <h2>Orphan Files <span class="badge badge-orphan">${result.orphans.length}</span></h2>
1266
+ <p class="meta" style="margin-bottom: 1rem;">Files not reachable from any root configuration file.</p>
1267
+ <ul class="file-list" id="orphan-list">${orphanItems}</ul>
1268
+ </div>
1269
+ ` : ""}
1270
+
1271
+ ${result.staleFiles.length > 0 ? `
1272
+ <div class="section">
1273
+ <h2>Stale Files <span class="badge badge-stale">${result.staleFiles.length}</span></h2>
1274
+ <p class="meta" style="margin-bottom: 1rem;">Files not modified in over 90 days.</p>
1275
+ <ul class="file-list" id="stale-list">${staleItems}</ul>
1276
+ </div>
1277
+ ` : ""}
1278
+
1279
+ <div class="section">
1280
+ <h2>Skills Comparison</h2>
1281
+ <table>
1282
+ <thead><tr><th>Skill/Plugin</th><th>Entire Machine</th><th>Project Specific</th><th>Source</th></tr></thead>
1283
+ <tbody>${skillRows}</tbody>
1284
+ </table>
1285
+ </div>
1286
+
1287
+ <div class="section">
1288
+ <h2>All Files</h2>
1289
+ <ul class="file-list" id="all-files">${fileItems}</ul>
1290
+ </div>
1291
+
1292
+ <script>
1293
+ mermaid.initialize({ startOnLoad: true, theme: 'dark' });
1294
+
1295
+ function filterFiles(query) {
1296
+ var q = query.toLowerCase();
1297
+ document.querySelectorAll('.file-list li').forEach(function(li) {
1298
+ var path = li.getAttribute('data-path') || '';
1299
+ li.style.display = path.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
1300
+ });
1301
+ }
1302
+ </script>
1303
+
1304
+ <script type="application/json" id="scan-data">${dataJson}</script>
1305
+ </body>
1306
+ </html>`;
1307
+ }
1308
+ function escapeHtml(text) {
1309
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1310
+ }
1311
+ var init_html_generator = __esm({
1312
+ "dist/output/html-generator.js"() {
1313
+ "use strict";
1314
+ }
1315
+ });
1316
+
1201
1317
  // dist/check-update.js
1202
1318
  import chalk8 from "chalk";
1203
1319
  async function fetchLatest() {
@@ -1262,7 +1378,7 @@ var CURRENT_VERSION;
1262
1378
  var init_check_update = __esm({
1263
1379
  "dist/check-update.js"() {
1264
1380
  "use strict";
1265
- CURRENT_VERSION = true ? "0.9.1" : "0.0.0-dev";
1381
+ CURRENT_VERSION = true ? "0.9.3" : "0.0.0-dev";
1266
1382
  }
1267
1383
  });
1268
1384
 
@@ -1290,6 +1406,253 @@ var init_push_toolings = __esm({
1290
1406
  }
1291
1407
  });
1292
1408
 
1409
+ // dist/device-utils.js
1410
+ import { hostname as hostname2, platform as platform2 } from "node:os";
1411
+ function detectOs2() {
1412
+ const p = platform2();
1413
+ if (p === "win32")
1414
+ return "windows";
1415
+ if (p === "darwin")
1416
+ return "macos";
1417
+ if (p === "linux")
1418
+ return "linux";
1419
+ return "other";
1420
+ }
1421
+ function detectDeviceName() {
1422
+ const os = detectOs2();
1423
+ const host = hostname2().split(".")[0];
1424
+ const osLabel = os.charAt(0).toUpperCase() + os.slice(1);
1425
+ return `${host}-${osLabel}`;
1426
+ }
1427
+ async function resolveDeviceId(supabase, userId) {
1428
+ const deviceName = detectDeviceName();
1429
+ const osType = detectOs2();
1430
+ await supabase.from("devices").upsert({
1431
+ user_id: userId,
1432
+ device_name: deviceName,
1433
+ os_type: osType
1434
+ }, { onConflict: "user_id,device_name" });
1435
+ const { data, error } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", deviceName).single();
1436
+ if (error || !data) {
1437
+ throw new Error(`Failed to resolve device ID: ${error?.message ?? "not found"}`);
1438
+ }
1439
+ return data.id;
1440
+ }
1441
+ var init_device_utils = __esm({
1442
+ "dist/device-utils.js"() {
1443
+ "use strict";
1444
+ }
1445
+ });
1446
+
1447
+ // dist/commands/map.js
1448
+ var map_exports = {};
1449
+ __export(map_exports, {
1450
+ mapCommand: () => mapCommand
1451
+ });
1452
+ import { resolve as resolve3, basename } from "node:path";
1453
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
1454
+ import { existsSync as existsSync7 } from "node:fs";
1455
+ import chalk9 from "chalk";
1456
+ import { select as select3, input as input5, confirm as confirm2 } from "@inquirer/prompts";
1457
+ async function mapCommand(path, options) {
1458
+ await checkForUpdate();
1459
+ const projectRoot = resolve3(path ?? process.cwd());
1460
+ if (!existsSync7(projectRoot)) {
1461
+ console.error(chalk9.red(`Path not found: ${projectRoot}`));
1462
+ process.exit(1);
1463
+ }
1464
+ console.log(chalk9.blue(`Scanning: ${projectRoot}
1465
+ `));
1466
+ const result = await scanProject(projectRoot);
1467
+ console.log(` Files found: ${result.graph.nodes.length}`);
1468
+ console.log(` References: ${result.graph.edges.length}`);
1469
+ console.log(` Orphans: ${result.orphans.length}`);
1470
+ console.log(` Stale files: ${result.staleFiles.length}`);
1471
+ console.log(` Skills: ${result.skills.length}`);
1472
+ console.log(` Toolings: ${result.toolings.length}`);
1473
+ console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
1474
+ console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
1475
+ const outputDir = resolve3(projectRoot, "output");
1476
+ if (!existsSync7(outputDir)) {
1477
+ await mkdir2(outputDir, { recursive: true });
1478
+ }
1479
+ const htmlPath = resolve3(outputDir, "index.html");
1480
+ const html = generateOfflineHtml(result, projectRoot);
1481
+ await writeFile2(htmlPath, html, "utf-8");
1482
+ console.log(chalk9.green(`
1483
+ Local preview: ${htmlPath}`));
1484
+ if (!options.offline) {
1485
+ try {
1486
+ const { supabase } = await getAuthenticatedClient();
1487
+ const { data: devicePaths } = await supabase.from("device_paths").select("folder_id, device_name").eq("path", projectRoot);
1488
+ if (devicePaths?.length) {
1489
+ const { folder_id, device_name } = devicePaths[0];
1490
+ const { data: proposedFiles } = await supabase.from("folder_files").select("id, file_path, proposed_at").eq("folder_id", folder_id).eq("proposed_for_deletion", true);
1491
+ if (proposedFiles?.length) {
1492
+ const { checkbox } = await import("@inquirer/prompts");
1493
+ console.log(chalk9.yellow(`
1494
+ ${proposedFiles.length} file(s) proposed for deletion:
1495
+ `));
1496
+ const toDelete = await checkbox({
1497
+ message: "Select files to delete (space to toggle, enter to confirm)",
1498
+ choices: proposedFiles.map((f) => ({
1499
+ name: `${f.file_path}${f.proposed_at ? ` (proposed ${new Date(f.proposed_at).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric" })})` : ""}`,
1500
+ value: f
1501
+ }))
1502
+ });
1503
+ for (const file of toDelete) {
1504
+ const fullPath = resolve3(projectRoot, file.file_path);
1505
+ try {
1506
+ const { unlink } = await import("node:fs/promises");
1507
+ await unlink(fullPath);
1508
+ await supabase.from("folder_files").delete().eq("id", file.id);
1509
+ console.log(chalk9.green(` Deleted: ${file.file_path}`));
1510
+ } catch (err) {
1511
+ console.error(chalk9.red(` Failed to delete ${file.file_path}: ${err}`));
1512
+ }
1513
+ }
1514
+ const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
1515
+ if (keptIds.length > 0) {
1516
+ for (const id of keptIds) {
1517
+ await supabase.from("folder_files").update({ proposed_for_deletion: false, proposed_at: null, proposed_by: null }).eq("id", id);
1518
+ }
1519
+ console.log(chalk9.cyan(` Kept ${keptIds.length} file(s) \u2014 proposals cleared.`));
1520
+ }
1521
+ console.log("");
1522
+ }
1523
+ const { error } = await supabase.from("claude_folders").update({
1524
+ graph_json: result.graph,
1525
+ orphans_json: result.orphans,
1526
+ skills_table_json: result.skills,
1527
+ stale_files_json: result.staleFiles,
1528
+ env_manifest_json: result.envManifest,
1529
+ last_scanned: result.scannedAt,
1530
+ data_hash: result.dataHash
1531
+ }).eq("id", folder_id);
1532
+ if (error) {
1533
+ console.error(chalk9.yellow(`Sync warning: ${error.message}`));
1534
+ } else {
1535
+ await pushToolings(supabase, folder_id, result.toolings);
1536
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
1537
+ await saveState({
1538
+ lastFolderId: folder_id,
1539
+ lastDeviceName: device_name,
1540
+ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
1541
+ });
1542
+ console.log(chalk9.green("Synced to Supabase."));
1543
+ console.log(chalk9.cyan(`
1544
+ https://www.md4ai.com/project/${folder_id}
1545
+ `));
1546
+ const configFiles = await readClaudeConfigFiles(projectRoot);
1547
+ if (configFiles.length > 0) {
1548
+ for (const file of configFiles) {
1549
+ await supabase.from("folder_files").upsert({
1550
+ folder_id,
1551
+ file_path: file.filePath,
1552
+ content: file.content,
1553
+ size_bytes: file.sizeBytes,
1554
+ last_modified: file.lastModified
1555
+ }, { onConflict: "folder_id,file_path" });
1556
+ }
1557
+ console.log(chalk9.green(` Uploaded ${configFiles.length} config file(s).`));
1558
+ }
1559
+ }
1560
+ } else {
1561
+ console.log(chalk9.yellow("\nThis folder is not linked to a project on your dashboard."));
1562
+ const shouldLink = await confirm2({ message: "Would you like to link it now?" });
1563
+ if (!shouldLink) {
1564
+ console.log(chalk9.dim("Skipped \u2014 local preview still generated."));
1565
+ } else {
1566
+ const { supabase: sb, userId } = await getAuthenticatedClient();
1567
+ const { data: folders } = await sb.from("claude_folders").select("id, name").order("name");
1568
+ const choices = [
1569
+ { name: "+ Create a new project", value: "__new__" },
1570
+ ...(folders ?? []).map((f) => ({ name: f.name, value: f.id }))
1571
+ ];
1572
+ const chosen = await select3({
1573
+ message: "Link to which project?",
1574
+ choices
1575
+ });
1576
+ let folderId;
1577
+ if (chosen === "__new__") {
1578
+ const projectName = await input5({
1579
+ message: "Project name:",
1580
+ default: basename(projectRoot)
1581
+ });
1582
+ const { data: newFolder, error: createErr } = await sb.from("claude_folders").insert({ user_id: userId, name: projectName }).select("id").single();
1583
+ if (createErr || !newFolder) {
1584
+ console.error(chalk9.red(`Failed to create project: ${createErr?.message}`));
1585
+ return;
1586
+ }
1587
+ folderId = newFolder.id;
1588
+ console.log(chalk9.green(`Created project "${projectName}".`));
1589
+ } else {
1590
+ folderId = chosen;
1591
+ }
1592
+ const deviceName = detectDeviceName();
1593
+ const osType = detectOs2();
1594
+ await sb.from("devices").upsert({
1595
+ user_id: userId,
1596
+ device_name: deviceName,
1597
+ os_type: osType
1598
+ }, { onConflict: "user_id,device_name" });
1599
+ await sb.from("device_paths").upsert({
1600
+ user_id: userId,
1601
+ folder_id: folderId,
1602
+ device_name: deviceName,
1603
+ os_type: osType,
1604
+ path: projectRoot,
1605
+ last_synced: (/* @__PURE__ */ new Date()).toISOString()
1606
+ }, { onConflict: "folder_id,device_name" });
1607
+ await sb.from("claude_folders").update({
1608
+ graph_json: result.graph,
1609
+ orphans_json: result.orphans,
1610
+ skills_table_json: result.skills,
1611
+ stale_files_json: result.staleFiles,
1612
+ env_manifest_json: result.envManifest,
1613
+ last_scanned: result.scannedAt,
1614
+ data_hash: result.dataHash
1615
+ }).eq("id", folderId);
1616
+ await pushToolings(sb, folderId, result.toolings);
1617
+ const configFiles = await readClaudeConfigFiles(projectRoot);
1618
+ for (const file of configFiles) {
1619
+ await sb.from("folder_files").upsert({
1620
+ folder_id: folderId,
1621
+ file_path: file.filePath,
1622
+ content: file.content,
1623
+ size_bytes: file.sizeBytes,
1624
+ last_modified: file.lastModified
1625
+ }, { onConflict: "folder_id,file_path" });
1626
+ }
1627
+ await saveState({
1628
+ lastFolderId: folderId,
1629
+ lastDeviceName: deviceName,
1630
+ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
1631
+ });
1632
+ console.log(chalk9.green("\nLinked and synced."));
1633
+ console.log(chalk9.cyan(`
1634
+ https://www.md4ai.com/project/${folderId}
1635
+ `));
1636
+ }
1637
+ }
1638
+ } catch {
1639
+ console.log(chalk9.yellow("Not logged in \u2014 local preview only."));
1640
+ }
1641
+ }
1642
+ }
1643
+ var init_map = __esm({
1644
+ "dist/commands/map.js"() {
1645
+ "use strict";
1646
+ init_scanner();
1647
+ init_auth();
1648
+ init_config();
1649
+ init_html_generator();
1650
+ init_check_update();
1651
+ init_push_toolings();
1652
+ init_device_utils();
1653
+ }
1654
+ });
1655
+
1293
1656
  // dist/commands/sync.js
1294
1657
  var sync_exports = {};
1295
1658
  __export(sync_exports, {
@@ -1356,56 +1719,18 @@ async function syncCommand(options) {
1356
1719
  data_hash: result.dataHash
1357
1720
  }).eq("id", device.folder_id);
1358
1721
  await pushToolings(supabase, device.folder_id, result.toolings);
1359
- await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
1360
- await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
1361
- console.log(chalk12.green("Synced."));
1362
- }
1363
- }
1364
- var init_sync = __esm({
1365
- "dist/commands/sync.js"() {
1366
- "use strict";
1367
- init_auth();
1368
- init_config();
1369
- init_scanner();
1370
- init_push_toolings();
1371
- }
1372
- });
1373
-
1374
- // dist/device-utils.js
1375
- import { hostname as hostname2, platform as platform2 } from "node:os";
1376
- function detectOs2() {
1377
- const p = platform2();
1378
- if (p === "win32")
1379
- return "windows";
1380
- if (p === "darwin")
1381
- return "macos";
1382
- if (p === "linux")
1383
- return "linux";
1384
- return "other";
1385
- }
1386
- function detectDeviceName() {
1387
- const os = detectOs2();
1388
- const host = hostname2().split(".")[0];
1389
- const osLabel = os.charAt(0).toUpperCase() + os.slice(1);
1390
- return `${host}-${osLabel}`;
1391
- }
1392
- async function resolveDeviceId(supabase, userId) {
1393
- const deviceName = detectDeviceName();
1394
- const osType = detectOs2();
1395
- await supabase.from("devices").upsert({
1396
- user_id: userId,
1397
- device_name: deviceName,
1398
- os_type: osType
1399
- }, { onConflict: "user_id,device_name" });
1400
- const { data, error } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", deviceName).single();
1401
- if (error || !data) {
1402
- throw new Error(`Failed to resolve device ID: ${error?.message ?? "not found"}`);
1722
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
1723
+ await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
1724
+ console.log(chalk12.green("Synced."));
1403
1725
  }
1404
- return data.id;
1405
1726
  }
1406
- var init_device_utils = __esm({
1407
- "dist/device-utils.js"() {
1727
+ var init_sync = __esm({
1728
+ "dist/commands/sync.js"() {
1408
1729
  "use strict";
1730
+ init_auth();
1731
+ init_config();
1732
+ init_scanner();
1733
+ init_push_toolings();
1409
1734
  }
1410
1735
  });
1411
1736
 
@@ -2243,240 +2568,8 @@ ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
2243
2568
  }
2244
2569
  }
2245
2570
 
2246
- // dist/commands/map.js
2247
- init_scanner();
2248
- init_auth();
2249
- init_config();
2250
- import { resolve as resolve3 } from "node:path";
2251
- import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
2252
- import { existsSync as existsSync7 } from "node:fs";
2253
- import chalk9 from "chalk";
2254
-
2255
- // dist/output/html-generator.js
2256
- function generateOfflineHtml(result, projectRoot) {
2257
- const title = projectRoot.split("/").pop() ?? "MD4AI";
2258
- const dataJson = JSON.stringify(result);
2259
- const orphanItems = result.orphans.map((o) => `<li data-path="${escapeHtml(o.path)}">${escapeHtml(o.path)} <span class="meta">(${o.sizeBytes} bytes)</span></li>`).join("\n");
2260
- const staleItems = result.staleFiles.map((s) => `<li data-path="${escapeHtml(s.path)}">${escapeHtml(s.path)} <span class="stale-meta">(${s.daysSinceModified} days)</span></li>`).join("\n");
2261
- const skillRows = result.skills.map((s) => `<tr><td>${escapeHtml(s.name)}</td><td>${s.machineWide ? '<span class="check">\u2713</span>' : ""}</td><td>${s.projectSpecific ? '<span class="check">\u2713</span>' : ""}</td><td class="meta">${escapeHtml(s.source)}</td></tr>`).join("\n");
2262
- const fileItems = result.graph.nodes.map((n) => `<li data-path="${escapeHtml(n.filePath)}" data-type="${n.type}">${escapeHtml(n.filePath)} <span class="meta">(${n.type})</span></li>`).join("\n");
2263
- return `<!DOCTYPE html>
2264
- <html lang="en">
2265
- <head>
2266
- <meta charset="UTF-8">
2267
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
2268
- <title>MD4AI \u2014 ${escapeHtml(title)}</title>
2269
- <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
2270
- <style>
2271
- * { margin: 0; padding: 0; box-sizing: border-box; }
2272
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
2273
- h1 { color: #38bdf8; margin-bottom: 0.5rem; }
2274
- .subtitle { color: #94a3b8; margin-bottom: 2rem; }
2275
- .section { background: #1e293b; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
2276
- .section h2 { color: #38bdf8; margin-bottom: 1rem; font-size: 1.2rem; }
2277
- .mermaid { background: #0f172a; border-radius: 8px; padding: 1rem; overflow-x: auto; }
2278
- .badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.85rem; margin-left: 0.5rem; }
2279
- .badge-orphan { background: #ef4444; color: white; }
2280
- .badge-stale { background: #f59e0b; color: black; }
2281
- table { width: 100%; border-collapse: collapse; }
2282
- th, td { padding: 0.5rem 1rem; text-align: left; border-bottom: 1px solid #334155; }
2283
- th { color: #94a3b8; font-weight: 600; }
2284
- .check { color: #22c55e; }
2285
- .meta { color: #94a3b8; }
2286
- .stale-meta { color: #f59e0b; }
2287
- .stats { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem; }
2288
- .stat { text-align: center; }
2289
- .stat-value { font-size: 2rem; font-weight: bold; color: #38bdf8; }
2290
- .stat-label { color: #94a3b8; font-size: 0.85rem; }
2291
- .file-list { list-style: none; }
2292
- .file-list li { padding: 0.3rem 0; color: #cbd5e1; font-family: monospace; font-size: 0.9rem; }
2293
- #search { width: 100%; padding: 0.75rem 1rem; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 1rem; margin-bottom: 1rem; }
2294
- #search:focus { outline: none; border-color: #38bdf8; }
2295
- @media print { body { background: white; color: black; } .section { border: 1px solid #ccc; } }
2296
- </style>
2297
- </head>
2298
- <body>
2299
- <h1>MD4AI \u2014 ${escapeHtml(title)}</h1>
2300
- <p class="subtitle">Scanned: ${new Date(result.scannedAt).toLocaleString()} | Hash: ${result.dataHash.slice(0, 12)}</p>
2301
-
2302
- <div class="stats">
2303
- <div class="stat"><div class="stat-value">${result.graph.nodes.length}</div><div class="stat-label">Files</div></div>
2304
- <div class="stat"><div class="stat-value">${result.graph.edges.length}</div><div class="stat-label">References</div></div>
2305
- <div class="stat"><div class="stat-value">${result.orphans.length}</div><div class="stat-label">Orphans</div></div>
2306
- <div class="stat"><div class="stat-value">${result.staleFiles.length}</div><div class="stat-label">Stale</div></div>
2307
- <div class="stat"><div class="stat-value">${result.skills.length}</div><div class="stat-label">Skills</div></div>
2308
- </div>
2309
-
2310
- <input type="text" id="search" placeholder="Search files..." oninput="filterFiles(this.value)">
2311
-
2312
- <div class="section">
2313
- <h2>Dependency Graph</h2>
2314
- <pre class="mermaid">${escapeHtml(result.graph.mermaid)}</pre>
2315
- </div>
2316
-
2317
- ${result.orphans.length > 0 ? `
2318
- <div class="section">
2319
- <h2>Orphan Files <span class="badge badge-orphan">${result.orphans.length}</span></h2>
2320
- <p class="meta" style="margin-bottom: 1rem;">Files not reachable from any root configuration file.</p>
2321
- <ul class="file-list" id="orphan-list">${orphanItems}</ul>
2322
- </div>
2323
- ` : ""}
2324
-
2325
- ${result.staleFiles.length > 0 ? `
2326
- <div class="section">
2327
- <h2>Stale Files <span class="badge badge-stale">${result.staleFiles.length}</span></h2>
2328
- <p class="meta" style="margin-bottom: 1rem;">Files not modified in over 90 days.</p>
2329
- <ul class="file-list" id="stale-list">${staleItems}</ul>
2330
- </div>
2331
- ` : ""}
2332
-
2333
- <div class="section">
2334
- <h2>Skills Comparison</h2>
2335
- <table>
2336
- <thead><tr><th>Skill/Plugin</th><th>Entire Machine</th><th>Project Specific</th><th>Source</th></tr></thead>
2337
- <tbody>${skillRows}</tbody>
2338
- </table>
2339
- </div>
2340
-
2341
- <div class="section">
2342
- <h2>All Files</h2>
2343
- <ul class="file-list" id="all-files">${fileItems}</ul>
2344
- </div>
2345
-
2346
- <script>
2347
- mermaid.initialize({ startOnLoad: true, theme: 'dark' });
2348
-
2349
- function filterFiles(query) {
2350
- var q = query.toLowerCase();
2351
- document.querySelectorAll('.file-list li').forEach(function(li) {
2352
- var path = li.getAttribute('data-path') || '';
2353
- li.style.display = path.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
2354
- });
2355
- }
2356
- </script>
2357
-
2358
- <script type="application/json" id="scan-data">${dataJson}</script>
2359
- </body>
2360
- </html>`;
2361
- }
2362
- function escapeHtml(text) {
2363
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2364
- }
2365
-
2366
- // dist/commands/map.js
2367
- init_check_update();
2368
- init_push_toolings();
2369
- async function mapCommand(path, options) {
2370
- await checkForUpdate();
2371
- const projectRoot = resolve3(path ?? process.cwd());
2372
- if (!existsSync7(projectRoot)) {
2373
- console.error(chalk9.red(`Path not found: ${projectRoot}`));
2374
- process.exit(1);
2375
- }
2376
- console.log(chalk9.blue(`Scanning: ${projectRoot}
2377
- `));
2378
- const result = await scanProject(projectRoot);
2379
- console.log(` Files found: ${result.graph.nodes.length}`);
2380
- console.log(` References: ${result.graph.edges.length}`);
2381
- console.log(` Orphans: ${result.orphans.length}`);
2382
- console.log(` Stale files: ${result.staleFiles.length}`);
2383
- console.log(` Skills: ${result.skills.length}`);
2384
- console.log(` Toolings: ${result.toolings.length}`);
2385
- console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
2386
- console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
2387
- const outputDir = resolve3(projectRoot, "output");
2388
- if (!existsSync7(outputDir)) {
2389
- await mkdir2(outputDir, { recursive: true });
2390
- }
2391
- const htmlPath = resolve3(outputDir, "index.html");
2392
- const html = generateOfflineHtml(result, projectRoot);
2393
- await writeFile2(htmlPath, html, "utf-8");
2394
- console.log(chalk9.green(`
2395
- Local preview: ${htmlPath}`));
2396
- if (!options.offline) {
2397
- try {
2398
- const { supabase } = await getAuthenticatedClient();
2399
- const { data: devicePaths } = await supabase.from("device_paths").select("folder_id, device_name").eq("path", projectRoot);
2400
- if (devicePaths?.length) {
2401
- const { folder_id, device_name } = devicePaths[0];
2402
- const { data: proposedFiles } = await supabase.from("folder_files").select("id, file_path, proposed_at").eq("folder_id", folder_id).eq("proposed_for_deletion", true);
2403
- if (proposedFiles?.length) {
2404
- const { checkbox } = await import("@inquirer/prompts");
2405
- console.log(chalk9.yellow(`
2406
- ${proposedFiles.length} file(s) proposed for deletion:
2407
- `));
2408
- const toDelete = await checkbox({
2409
- message: "Select files to delete (space to toggle, enter to confirm)",
2410
- choices: proposedFiles.map((f) => ({
2411
- name: `${f.file_path}${f.proposed_at ? ` (proposed ${new Date(f.proposed_at).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric" })})` : ""}`,
2412
- value: f
2413
- }))
2414
- });
2415
- for (const file of toDelete) {
2416
- const fullPath = resolve3(projectRoot, file.file_path);
2417
- try {
2418
- const { unlink } = await import("node:fs/promises");
2419
- await unlink(fullPath);
2420
- await supabase.from("folder_files").delete().eq("id", file.id);
2421
- console.log(chalk9.green(` Deleted: ${file.file_path}`));
2422
- } catch (err) {
2423
- console.error(chalk9.red(` Failed to delete ${file.file_path}: ${err}`));
2424
- }
2425
- }
2426
- const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
2427
- if (keptIds.length > 0) {
2428
- for (const id of keptIds) {
2429
- await supabase.from("folder_files").update({ proposed_for_deletion: false, proposed_at: null, proposed_by: null }).eq("id", id);
2430
- }
2431
- console.log(chalk9.cyan(` Kept ${keptIds.length} file(s) \u2014 proposals cleared.`));
2432
- }
2433
- console.log("");
2434
- }
2435
- const { error } = await supabase.from("claude_folders").update({
2436
- graph_json: result.graph,
2437
- orphans_json: result.orphans,
2438
- skills_table_json: result.skills,
2439
- stale_files_json: result.staleFiles,
2440
- env_manifest_json: result.envManifest,
2441
- last_scanned: result.scannedAt,
2442
- data_hash: result.dataHash
2443
- }).eq("id", folder_id);
2444
- if (error) {
2445
- console.error(chalk9.yellow(`Sync warning: ${error.message}`));
2446
- } else {
2447
- await pushToolings(supabase, folder_id, result.toolings);
2448
- await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
2449
- await saveState({
2450
- lastFolderId: folder_id,
2451
- lastDeviceName: device_name,
2452
- lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
2453
- });
2454
- console.log(chalk9.green("Synced to Supabase."));
2455
- console.log(chalk9.cyan(`
2456
- https://www.md4ai.com/project/${folder_id}
2457
- `));
2458
- const configFiles = await readClaudeConfigFiles(projectRoot);
2459
- if (configFiles.length > 0) {
2460
- for (const file of configFiles) {
2461
- await supabase.from("folder_files").upsert({
2462
- folder_id,
2463
- file_path: file.filePath,
2464
- content: file.content,
2465
- size_bytes: file.sizeBytes,
2466
- last_modified: file.lastModified
2467
- }, { onConflict: "folder_id,file_path" });
2468
- }
2469
- console.log(chalk9.green(` Uploaded ${configFiles.length} config file(s).`));
2470
- }
2471
- }
2472
- } else {
2473
- console.log(chalk9.yellow("No device path matches this folder. Run: md4ai add-device\n(Local preview still generated.)"));
2474
- }
2475
- } catch {
2476
- console.log(chalk9.yellow("Not logged in \u2014 local preview only."));
2477
- }
2478
- }
2479
- }
2571
+ // dist/index.js
2572
+ init_map();
2480
2573
 
2481
2574
  // dist/commands/simulate.js
2482
2575
  init_dist();
@@ -2704,7 +2797,7 @@ import { readFile as readFile7, writeFile as writeFile4, mkdir as mkdir3 } from
2704
2797
  import { join as join11, dirname as dirname2 } from "node:path";
2705
2798
  import { existsSync as existsSync10 } from "node:fs";
2706
2799
  import chalk14 from "chalk";
2707
- import { confirm as confirm2, input as input5 } from "@inquirer/prompts";
2800
+ import { confirm as confirm3, input as input6 } from "@inquirer/prompts";
2708
2801
  async function importBundleCommand(zipPath) {
2709
2802
  if (!existsSync10(zipPath)) {
2710
2803
  console.error(chalk14.red(`File not found: ${zipPath}`));
@@ -2741,11 +2834,11 @@ Files to extract:`));
2741
2834
  for (const f of files) {
2742
2835
  console.log(` ${f.filePath}`);
2743
2836
  }
2744
- const targetDir = await input5({
2837
+ const targetDir = await input6({
2745
2838
  message: "Extract to directory:",
2746
2839
  default: process.cwd()
2747
2840
  });
2748
- const proceed = await confirm2({
2841
+ const proceed = await confirm3({
2749
2842
  message: `Extract ${files.length} file(s) to ${targetDir}?`
2750
2843
  });
2751
2844
  if (!proceed) {
@@ -3235,7 +3328,7 @@ function spawnPostUpdate() {
3235
3328
  child.on("exit", (code) => process.exit(code ?? 0));
3236
3329
  }
3237
3330
  async function postUpdateFlow() {
3238
- const { confirm: confirm3 } = await import("@inquirer/prompts");
3331
+ const { confirm: confirm4 } = await import("@inquirer/prompts");
3239
3332
  console.log(chalk20.green(`
3240
3333
  md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.
3241
3334
  `));
@@ -3243,7 +3336,7 @@ async function postUpdateFlow() {
3243
3336
  const isLoggedIn = !!creds?.accessToken && Date.now() < creds.expiresAt;
3244
3337
  if (!isLoggedIn) {
3245
3338
  console.log(chalk20.yellow(" You are not logged in.\n"));
3246
- const wantLogin = await confirm3({
3339
+ const wantLogin = await confirm4({
3247
3340
  message: "Log in now?",
3248
3341
  default: true
3249
3342
  });
@@ -3257,21 +3350,28 @@ async function postUpdateFlow() {
3257
3350
  return;
3258
3351
  }
3259
3352
  }
3260
- const state = await loadState();
3261
- const hasProject = !!state.lastFolderId;
3262
- if (hasProject) {
3263
- const wantScan = await confirm3({
3264
- message: "Rescan your project and refresh the dashboard?",
3265
- default: true
3266
- });
3267
- if (wantScan) {
3268
- console.log("");
3269
- const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
3270
- await syncCommand2({ all: false });
3271
- console.log("");
3272
- }
3353
+ const { select: selectPrompt } = await import("@inquirer/prompts");
3354
+ const cwd = process.cwd();
3355
+ const scanChoice = await selectPrompt({
3356
+ message: "What would you like to scan?",
3357
+ choices: [
3358
+ { name: `Current folder (${cwd})`, value: "cwd" },
3359
+ { name: "All linked projects on this device", value: "all" },
3360
+ { name: "Skip scanning", value: "skip" }
3361
+ ]
3362
+ });
3363
+ if (scanChoice === "cwd") {
3364
+ console.log("");
3365
+ const { mapCommand: mapCommand2 } = await Promise.resolve().then(() => (init_map(), map_exports));
3366
+ await mapCommand2(cwd, {});
3367
+ console.log("");
3368
+ } else if (scanChoice === "all") {
3369
+ console.log("");
3370
+ const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
3371
+ await syncCommand2({ all: true });
3372
+ console.log("");
3273
3373
  }
3274
- const wantWatch = await confirm3({
3374
+ const wantWatch = await confirm4({
3275
3375
  message: "Start monitoring MCP servers? (runs until you press Ctrl+C)",
3276
3376
  default: true
3277
3377
  });
@@ -3302,8 +3402,8 @@ async function updateCommand(options) {
3302
3402
  }
3303
3403
  console.log(chalk20.white(" Update available: ") + chalk20.dim(`v${CURRENT_VERSION}`) + chalk20.white(" \u2192 ") + chalk20.green.bold(`v${latest}
3304
3404
  `));
3305
- const { confirm: confirm3 } = await import("@inquirer/prompts");
3306
- const wantUpdate = await confirm3({
3405
+ const { confirm: confirm4 } = await import("@inquirer/prompts");
3406
+ const wantUpdate = await confirm4({
3307
3407
  message: `Install md4ai v${latest}?`,
3308
3408
  default: true
3309
3409
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {