md4ai 0.9.2 → 0.9.4

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 +442 -398
  2. package/package.json +1 -1
@@ -1149,22 +1149,43 @@ async function readClaudeConfigFiles(projectRoot) {
1149
1149
  ".claude/skills/*.md"
1150
1150
  ];
1151
1151
  const files = [];
1152
+ const seen = /* @__PURE__ */ new Set();
1153
+ async function addFile(fullPath) {
1154
+ const relPath = relative4(projectRoot, fullPath);
1155
+ if (seen.has(relPath))
1156
+ return;
1157
+ seen.add(relPath);
1158
+ try {
1159
+ const content = await readFile10(fullPath, "utf-8");
1160
+ const fileStat = await stat(fullPath);
1161
+ const lastMod = getGitLastModified(fullPath, projectRoot);
1162
+ files.push({ filePath: relPath, content, sizeBytes: fileStat.size, lastModified: lastMod });
1163
+ } catch {
1164
+ }
1165
+ }
1152
1166
  for (const pattern of configPatterns) {
1153
1167
  for await (const fullPath of glob2(join14(projectRoot, pattern))) {
1154
1168
  if (!existsSync13(fullPath))
1155
1169
  continue;
1156
- try {
1157
- const content = await readFile10(fullPath, "utf-8");
1158
- const fileStat = await stat(fullPath);
1159
- const lastMod = getGitLastModified(fullPath, projectRoot);
1160
- files.push({
1161
- filePath: relative4(projectRoot, fullPath),
1162
- content,
1163
- sizeBytes: fileStat.size,
1164
- lastModified: lastMod
1165
- });
1166
- } catch {
1170
+ await addFile(fullPath);
1171
+ }
1172
+ }
1173
+ const pkgPath = join14(projectRoot, "package.json");
1174
+ if (existsSync13(pkgPath)) {
1175
+ try {
1176
+ const pkgContent = await readFile10(pkgPath, "utf-8");
1177
+ const pkg = JSON.parse(pkgContent);
1178
+ const preflightCmd = pkg.scripts?.preflight;
1179
+ if (preflightCmd) {
1180
+ const match = preflightCmd.match(/(?:bash\s+|sh\s+|\.\/)?(\S+\.(?:sh|bash|ts|js|mjs))/);
1181
+ if (match) {
1182
+ const scriptPath = join14(projectRoot, match[1]);
1183
+ if (existsSync13(scriptPath)) {
1184
+ await addFile(scriptPath);
1185
+ }
1186
+ }
1167
1187
  }
1188
+ } catch {
1168
1189
  }
1169
1190
  }
1170
1191
  return files;
@@ -1198,6 +1219,122 @@ var init_scanner = __esm({
1198
1219
  }
1199
1220
  });
1200
1221
 
1222
+ // dist/output/html-generator.js
1223
+ function generateOfflineHtml(result, projectRoot) {
1224
+ const title = projectRoot.split("/").pop() ?? "MD4AI";
1225
+ const dataJson = JSON.stringify(result);
1226
+ 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");
1227
+ 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");
1228
+ 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");
1229
+ 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");
1230
+ return `<!DOCTYPE html>
1231
+ <html lang="en">
1232
+ <head>
1233
+ <meta charset="UTF-8">
1234
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1235
+ <title>MD4AI \u2014 ${escapeHtml(title)}</title>
1236
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
1237
+ <style>
1238
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1239
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
1240
+ h1 { color: #38bdf8; margin-bottom: 0.5rem; }
1241
+ .subtitle { color: #94a3b8; margin-bottom: 2rem; }
1242
+ .section { background: #1e293b; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
1243
+ .section h2 { color: #38bdf8; margin-bottom: 1rem; font-size: 1.2rem; }
1244
+ .mermaid { background: #0f172a; border-radius: 8px; padding: 1rem; overflow-x: auto; }
1245
+ .badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.85rem; margin-left: 0.5rem; }
1246
+ .badge-orphan { background: #ef4444; color: white; }
1247
+ .badge-stale { background: #f59e0b; color: black; }
1248
+ table { width: 100%; border-collapse: collapse; }
1249
+ th, td { padding: 0.5rem 1rem; text-align: left; border-bottom: 1px solid #334155; }
1250
+ th { color: #94a3b8; font-weight: 600; }
1251
+ .check { color: #22c55e; }
1252
+ .meta { color: #94a3b8; }
1253
+ .stale-meta { color: #f59e0b; }
1254
+ .stats { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem; }
1255
+ .stat { text-align: center; }
1256
+ .stat-value { font-size: 2rem; font-weight: bold; color: #38bdf8; }
1257
+ .stat-label { color: #94a3b8; font-size: 0.85rem; }
1258
+ .file-list { list-style: none; }
1259
+ .file-list li { padding: 0.3rem 0; color: #cbd5e1; font-family: monospace; font-size: 0.9rem; }
1260
+ #search { width: 100%; padding: 0.75rem 1rem; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 1rem; margin-bottom: 1rem; }
1261
+ #search:focus { outline: none; border-color: #38bdf8; }
1262
+ @media print { body { background: white; color: black; } .section { border: 1px solid #ccc; } }
1263
+ </style>
1264
+ </head>
1265
+ <body>
1266
+ <h1>MD4AI \u2014 ${escapeHtml(title)}</h1>
1267
+ <p class="subtitle">Scanned: ${new Date(result.scannedAt).toLocaleString()} | Hash: ${result.dataHash.slice(0, 12)}</p>
1268
+
1269
+ <div class="stats">
1270
+ <div class="stat"><div class="stat-value">${result.graph.nodes.length}</div><div class="stat-label">Files</div></div>
1271
+ <div class="stat"><div class="stat-value">${result.graph.edges.length}</div><div class="stat-label">References</div></div>
1272
+ <div class="stat"><div class="stat-value">${result.orphans.length}</div><div class="stat-label">Orphans</div></div>
1273
+ <div class="stat"><div class="stat-value">${result.staleFiles.length}</div><div class="stat-label">Stale</div></div>
1274
+ <div class="stat"><div class="stat-value">${result.skills.length}</div><div class="stat-label">Skills</div></div>
1275
+ </div>
1276
+
1277
+ <input type="text" id="search" placeholder="Search files..." oninput="filterFiles(this.value)">
1278
+
1279
+ <div class="section">
1280
+ <h2>Dependency Graph</h2>
1281
+ <pre class="mermaid">${escapeHtml(result.graph.mermaid)}</pre>
1282
+ </div>
1283
+
1284
+ ${result.orphans.length > 0 ? `
1285
+ <div class="section">
1286
+ <h2>Orphan Files <span class="badge badge-orphan">${result.orphans.length}</span></h2>
1287
+ <p class="meta" style="margin-bottom: 1rem;">Files not reachable from any root configuration file.</p>
1288
+ <ul class="file-list" id="orphan-list">${orphanItems}</ul>
1289
+ </div>
1290
+ ` : ""}
1291
+
1292
+ ${result.staleFiles.length > 0 ? `
1293
+ <div class="section">
1294
+ <h2>Stale Files <span class="badge badge-stale">${result.staleFiles.length}</span></h2>
1295
+ <p class="meta" style="margin-bottom: 1rem;">Files not modified in over 90 days.</p>
1296
+ <ul class="file-list" id="stale-list">${staleItems}</ul>
1297
+ </div>
1298
+ ` : ""}
1299
+
1300
+ <div class="section">
1301
+ <h2>Skills Comparison</h2>
1302
+ <table>
1303
+ <thead><tr><th>Skill/Plugin</th><th>Entire Machine</th><th>Project Specific</th><th>Source</th></tr></thead>
1304
+ <tbody>${skillRows}</tbody>
1305
+ </table>
1306
+ </div>
1307
+
1308
+ <div class="section">
1309
+ <h2>All Files</h2>
1310
+ <ul class="file-list" id="all-files">${fileItems}</ul>
1311
+ </div>
1312
+
1313
+ <script>
1314
+ mermaid.initialize({ startOnLoad: true, theme: 'dark' });
1315
+
1316
+ function filterFiles(query) {
1317
+ var q = query.toLowerCase();
1318
+ document.querySelectorAll('.file-list li').forEach(function(li) {
1319
+ var path = li.getAttribute('data-path') || '';
1320
+ li.style.display = path.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
1321
+ });
1322
+ }
1323
+ </script>
1324
+
1325
+ <script type="application/json" id="scan-data">${dataJson}</script>
1326
+ </body>
1327
+ </html>`;
1328
+ }
1329
+ function escapeHtml(text) {
1330
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1331
+ }
1332
+ var init_html_generator = __esm({
1333
+ "dist/output/html-generator.js"() {
1334
+ "use strict";
1335
+ }
1336
+ });
1337
+
1201
1338
  // dist/check-update.js
1202
1339
  import chalk8 from "chalk";
1203
1340
  async function fetchLatest() {
@@ -1262,7 +1399,7 @@ var CURRENT_VERSION;
1262
1399
  var init_check_update = __esm({
1263
1400
  "dist/check-update.js"() {
1264
1401
  "use strict";
1265
- CURRENT_VERSION = true ? "0.9.2" : "0.0.0-dev";
1402
+ CURRENT_VERSION = true ? "0.9.4" : "0.0.0-dev";
1266
1403
  }
1267
1404
  });
1268
1405
 
@@ -1320,11 +1457,220 @@ async function resolveDeviceId(supabase, userId) {
1320
1457
  if (error || !data) {
1321
1458
  throw new Error(`Failed to resolve device ID: ${error?.message ?? "not found"}`);
1322
1459
  }
1323
- return data.id;
1460
+ return data.id;
1461
+ }
1462
+ var init_device_utils = __esm({
1463
+ "dist/device-utils.js"() {
1464
+ "use strict";
1465
+ }
1466
+ });
1467
+
1468
+ // dist/commands/map.js
1469
+ var map_exports = {};
1470
+ __export(map_exports, {
1471
+ mapCommand: () => mapCommand
1472
+ });
1473
+ import { resolve as resolve3, basename } from "node:path";
1474
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
1475
+ import { existsSync as existsSync7 } from "node:fs";
1476
+ import chalk9 from "chalk";
1477
+ import { select as select3, input as input5, confirm as confirm2 } from "@inquirer/prompts";
1478
+ async function mapCommand(path, options) {
1479
+ await checkForUpdate();
1480
+ const projectRoot = resolve3(path ?? process.cwd());
1481
+ if (!existsSync7(projectRoot)) {
1482
+ console.error(chalk9.red(`Path not found: ${projectRoot}`));
1483
+ process.exit(1);
1484
+ }
1485
+ console.log(chalk9.blue(`Scanning: ${projectRoot}
1486
+ `));
1487
+ const result = await scanProject(projectRoot);
1488
+ console.log(` Files found: ${result.graph.nodes.length}`);
1489
+ console.log(` References: ${result.graph.edges.length}`);
1490
+ console.log(` Orphans: ${result.orphans.length}`);
1491
+ console.log(` Stale files: ${result.staleFiles.length}`);
1492
+ console.log(` Skills: ${result.skills.length}`);
1493
+ console.log(` Toolings: ${result.toolings.length}`);
1494
+ console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
1495
+ console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
1496
+ const outputDir = resolve3(projectRoot, "output");
1497
+ if (!existsSync7(outputDir)) {
1498
+ await mkdir2(outputDir, { recursive: true });
1499
+ }
1500
+ const htmlPath = resolve3(outputDir, "index.html");
1501
+ const html = generateOfflineHtml(result, projectRoot);
1502
+ await writeFile2(htmlPath, html, "utf-8");
1503
+ console.log(chalk9.green(`
1504
+ Local preview: ${htmlPath}`));
1505
+ if (!options.offline) {
1506
+ try {
1507
+ const { supabase } = await getAuthenticatedClient();
1508
+ const { data: devicePaths } = await supabase.from("device_paths").select("folder_id, device_name").eq("path", projectRoot);
1509
+ if (devicePaths?.length) {
1510
+ const { folder_id, device_name } = devicePaths[0];
1511
+ const { data: proposedFiles } = await supabase.from("folder_files").select("id, file_path, proposed_at").eq("folder_id", folder_id).eq("proposed_for_deletion", true);
1512
+ if (proposedFiles?.length) {
1513
+ const { checkbox } = await import("@inquirer/prompts");
1514
+ console.log(chalk9.yellow(`
1515
+ ${proposedFiles.length} file(s) proposed for deletion:
1516
+ `));
1517
+ const toDelete = await checkbox({
1518
+ message: "Select files to delete (space to toggle, enter to confirm)",
1519
+ choices: proposedFiles.map((f) => ({
1520
+ name: `${f.file_path}${f.proposed_at ? ` (proposed ${new Date(f.proposed_at).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric" })})` : ""}`,
1521
+ value: f
1522
+ }))
1523
+ });
1524
+ for (const file of toDelete) {
1525
+ const fullPath = resolve3(projectRoot, file.file_path);
1526
+ try {
1527
+ const { unlink } = await import("node:fs/promises");
1528
+ await unlink(fullPath);
1529
+ await supabase.from("folder_files").delete().eq("id", file.id);
1530
+ console.log(chalk9.green(` Deleted: ${file.file_path}`));
1531
+ } catch (err) {
1532
+ console.error(chalk9.red(` Failed to delete ${file.file_path}: ${err}`));
1533
+ }
1534
+ }
1535
+ const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
1536
+ if (keptIds.length > 0) {
1537
+ for (const id of keptIds) {
1538
+ await supabase.from("folder_files").update({ proposed_for_deletion: false, proposed_at: null, proposed_by: null }).eq("id", id);
1539
+ }
1540
+ console.log(chalk9.cyan(` Kept ${keptIds.length} file(s) \u2014 proposals cleared.`));
1541
+ }
1542
+ console.log("");
1543
+ }
1544
+ const { error } = await supabase.from("claude_folders").update({
1545
+ graph_json: result.graph,
1546
+ orphans_json: result.orphans,
1547
+ skills_table_json: result.skills,
1548
+ stale_files_json: result.staleFiles,
1549
+ env_manifest_json: result.envManifest,
1550
+ last_scanned: result.scannedAt,
1551
+ data_hash: result.dataHash
1552
+ }).eq("id", folder_id);
1553
+ if (error) {
1554
+ console.error(chalk9.yellow(`Sync warning: ${error.message}`));
1555
+ } else {
1556
+ await pushToolings(supabase, folder_id, result.toolings);
1557
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
1558
+ await saveState({
1559
+ lastFolderId: folder_id,
1560
+ lastDeviceName: device_name,
1561
+ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
1562
+ });
1563
+ console.log(chalk9.green("Synced to Supabase."));
1564
+ console.log(chalk9.cyan(`
1565
+ https://www.md4ai.com/project/${folder_id}
1566
+ `));
1567
+ const configFiles = await readClaudeConfigFiles(projectRoot);
1568
+ if (configFiles.length > 0) {
1569
+ for (const file of configFiles) {
1570
+ await supabase.from("folder_files").upsert({
1571
+ folder_id,
1572
+ file_path: file.filePath,
1573
+ content: file.content,
1574
+ size_bytes: file.sizeBytes,
1575
+ last_modified: file.lastModified
1576
+ }, { onConflict: "folder_id,file_path" });
1577
+ }
1578
+ console.log(chalk9.green(` Uploaded ${configFiles.length} config file(s).`));
1579
+ }
1580
+ }
1581
+ } else {
1582
+ console.log(chalk9.yellow("\nThis folder is not linked to a project on your dashboard."));
1583
+ const shouldLink = await confirm2({ message: "Would you like to link it now?" });
1584
+ if (!shouldLink) {
1585
+ console.log(chalk9.dim("Skipped \u2014 local preview still generated."));
1586
+ } else {
1587
+ const { supabase: sb, userId } = await getAuthenticatedClient();
1588
+ const { data: folders } = await sb.from("claude_folders").select("id, name").order("name");
1589
+ const choices = [
1590
+ { name: "+ Create a new project", value: "__new__" },
1591
+ ...(folders ?? []).map((f) => ({ name: f.name, value: f.id }))
1592
+ ];
1593
+ const chosen = await select3({
1594
+ message: "Link to which project?",
1595
+ choices
1596
+ });
1597
+ let folderId;
1598
+ if (chosen === "__new__") {
1599
+ const projectName = await input5({
1600
+ message: "Project name:",
1601
+ default: basename(projectRoot)
1602
+ });
1603
+ const { data: newFolder, error: createErr } = await sb.from("claude_folders").insert({ user_id: userId, name: projectName }).select("id").single();
1604
+ if (createErr || !newFolder) {
1605
+ console.error(chalk9.red(`Failed to create project: ${createErr?.message}`));
1606
+ return;
1607
+ }
1608
+ folderId = newFolder.id;
1609
+ console.log(chalk9.green(`Created project "${projectName}".`));
1610
+ } else {
1611
+ folderId = chosen;
1612
+ }
1613
+ const deviceName = detectDeviceName();
1614
+ const osType = detectOs2();
1615
+ await sb.from("devices").upsert({
1616
+ user_id: userId,
1617
+ device_name: deviceName,
1618
+ os_type: osType
1619
+ }, { onConflict: "user_id,device_name" });
1620
+ await sb.from("device_paths").upsert({
1621
+ user_id: userId,
1622
+ folder_id: folderId,
1623
+ device_name: deviceName,
1624
+ os_type: osType,
1625
+ path: projectRoot,
1626
+ last_synced: (/* @__PURE__ */ new Date()).toISOString()
1627
+ }, { onConflict: "folder_id,device_name" });
1628
+ await sb.from("claude_folders").update({
1629
+ graph_json: result.graph,
1630
+ orphans_json: result.orphans,
1631
+ skills_table_json: result.skills,
1632
+ stale_files_json: result.staleFiles,
1633
+ env_manifest_json: result.envManifest,
1634
+ last_scanned: result.scannedAt,
1635
+ data_hash: result.dataHash
1636
+ }).eq("id", folderId);
1637
+ await pushToolings(sb, folderId, result.toolings);
1638
+ const configFiles = await readClaudeConfigFiles(projectRoot);
1639
+ for (const file of configFiles) {
1640
+ await sb.from("folder_files").upsert({
1641
+ folder_id: folderId,
1642
+ file_path: file.filePath,
1643
+ content: file.content,
1644
+ size_bytes: file.sizeBytes,
1645
+ last_modified: file.lastModified
1646
+ }, { onConflict: "folder_id,file_path" });
1647
+ }
1648
+ await saveState({
1649
+ lastFolderId: folderId,
1650
+ lastDeviceName: deviceName,
1651
+ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
1652
+ });
1653
+ console.log(chalk9.green("\nLinked and synced."));
1654
+ console.log(chalk9.cyan(`
1655
+ https://www.md4ai.com/project/${folderId}
1656
+ `));
1657
+ }
1658
+ }
1659
+ } catch {
1660
+ console.log(chalk9.yellow("Not logged in \u2014 local preview only."));
1661
+ }
1662
+ }
1324
1663
  }
1325
- var init_device_utils = __esm({
1326
- "dist/device-utils.js"() {
1664
+ var init_map = __esm({
1665
+ "dist/commands/map.js"() {
1327
1666
  "use strict";
1667
+ init_scanner();
1668
+ init_auth();
1669
+ init_config();
1670
+ init_html_generator();
1671
+ init_check_update();
1672
+ init_push_toolings();
1673
+ init_device_utils();
1328
1674
  }
1329
1675
  });
1330
1676
 
@@ -2176,385 +2522,76 @@ async function addDeviceCommand() {
2176
2522
  });
2177
2523
  const suggested = suggestDeviceName();
2178
2524
  const deviceName = await input4({
2179
- message: "Device name:",
2180
- default: suggested
2181
- });
2182
- const osType = await select2({
2183
- message: "OS type:",
2184
- choices: [
2185
- { name: "Windows", value: "windows" },
2186
- { name: "macOS", value: "macos" },
2187
- { name: "Ubuntu", value: "ubuntu" },
2188
- { name: "Linux", value: "linux" },
2189
- { name: "Other", value: "other" }
2190
- ],
2191
- default: detectOs()
2192
- });
2193
- const description = await input4({ message: "Description (optional):" });
2194
- const { error } = await supabase.from("device_paths").insert({
2195
- user_id: userId,
2196
- folder_id: folderId,
2197
- device_name: deviceName,
2198
- os_type: osType,
2199
- path: localPath,
2200
- description: description || null
2201
- });
2202
- if (error) {
2203
- console.error(chalk6.red(`Failed to add device: ${error.message}`));
2204
- process.exit(1);
2205
- }
2206
- console.log(chalk6.green(`
2207
- Device "${deviceName}" added to folder.`));
2208
- }
2209
-
2210
- // dist/commands/list-devices.js
2211
- init_auth();
2212
- import chalk7 from "chalk";
2213
- async function listDevicesCommand() {
2214
- const { supabase } = await getAuthenticatedClient();
2215
- const { data: devices, error } = await supabase.from("device_paths").select(`
2216
- id, device_name, os_type, path, last_synced, description,
2217
- claude_folders!inner ( name )
2218
- `).order("device_name");
2219
- if (error) {
2220
- console.error(chalk7.red(`Failed to list devices: ${error.message}`));
2221
- process.exit(1);
2222
- }
2223
- if (!devices?.length) {
2224
- console.log(chalk7.yellow("No devices found. Run: md4ai add-device"));
2225
- return;
2226
- }
2227
- const grouped = /* @__PURE__ */ new Map();
2228
- for (const d of devices) {
2229
- const list = grouped.get(d.device_name) ?? [];
2230
- list.push(d);
2231
- grouped.set(d.device_name, list);
2232
- }
2233
- for (const [deviceName, entries] of grouped) {
2234
- const first = entries[0];
2235
- console.log(chalk7.bold(`
2236
- ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
2237
- for (const entry of entries) {
2238
- const folderName = entry.claude_folders?.name ?? "unknown";
2239
- const synced = entry.last_synced ? new Date(entry.last_synced).toLocaleString() : "never";
2240
- console.log(` ${chalk7.cyan(folderName)} \u2192 ${entry.path}`);
2241
- console.log(` Last synced: ${synced}`);
2242
- }
2243
- }
2244
- }
2245
-
2246
- // dist/commands/map.js
2247
- init_scanner();
2248
- init_auth();
2249
- init_config();
2250
- import { resolve as resolve3, basename } 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;");
2525
+ message: "Device name:",
2526
+ default: suggested
2527
+ });
2528
+ const osType = await select2({
2529
+ message: "OS type:",
2530
+ choices: [
2531
+ { name: "Windows", value: "windows" },
2532
+ { name: "macOS", value: "macos" },
2533
+ { name: "Ubuntu", value: "ubuntu" },
2534
+ { name: "Linux", value: "linux" },
2535
+ { name: "Other", value: "other" }
2536
+ ],
2537
+ default: detectOs()
2538
+ });
2539
+ const description = await input4({ message: "Description (optional):" });
2540
+ const { error } = await supabase.from("device_paths").insert({
2541
+ user_id: userId,
2542
+ folder_id: folderId,
2543
+ device_name: deviceName,
2544
+ os_type: osType,
2545
+ path: localPath,
2546
+ description: description || null
2547
+ });
2548
+ if (error) {
2549
+ console.error(chalk6.red(`Failed to add device: ${error.message}`));
2550
+ process.exit(1);
2551
+ }
2552
+ console.log(chalk6.green(`
2553
+ Device "${deviceName}" added to folder.`));
2364
2554
  }
2365
2555
 
2366
- // dist/commands/map.js
2367
- init_check_update();
2368
- init_push_toolings();
2369
- init_device_utils();
2370
- import { select as select3, input as input5, confirm as confirm2 } from "@inquirer/prompts";
2371
- async function mapCommand(path, options) {
2372
- await checkForUpdate();
2373
- const projectRoot = resolve3(path ?? process.cwd());
2374
- if (!existsSync7(projectRoot)) {
2375
- console.error(chalk9.red(`Path not found: ${projectRoot}`));
2556
+ // dist/commands/list-devices.js
2557
+ init_auth();
2558
+ import chalk7 from "chalk";
2559
+ async function listDevicesCommand() {
2560
+ const { supabase } = await getAuthenticatedClient();
2561
+ const { data: devices, error } = await supabase.from("device_paths").select(`
2562
+ id, device_name, os_type, path, last_synced, description,
2563
+ claude_folders!inner ( name )
2564
+ `).order("device_name");
2565
+ if (error) {
2566
+ console.error(chalk7.red(`Failed to list devices: ${error.message}`));
2376
2567
  process.exit(1);
2377
2568
  }
2378
- console.log(chalk9.blue(`Scanning: ${projectRoot}
2379
- `));
2380
- const result = await scanProject(projectRoot);
2381
- console.log(` Files found: ${result.graph.nodes.length}`);
2382
- console.log(` References: ${result.graph.edges.length}`);
2383
- console.log(` Orphans: ${result.orphans.length}`);
2384
- console.log(` Stale files: ${result.staleFiles.length}`);
2385
- console.log(` Skills: ${result.skills.length}`);
2386
- console.log(` Toolings: ${result.toolings.length}`);
2387
- console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
2388
- console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
2389
- const outputDir = resolve3(projectRoot, "output");
2390
- if (!existsSync7(outputDir)) {
2391
- await mkdir2(outputDir, { recursive: true });
2569
+ if (!devices?.length) {
2570
+ console.log(chalk7.yellow("No devices found. Run: md4ai add-device"));
2571
+ return;
2392
2572
  }
2393
- const htmlPath = resolve3(outputDir, "index.html");
2394
- const html = generateOfflineHtml(result, projectRoot);
2395
- await writeFile2(htmlPath, html, "utf-8");
2396
- console.log(chalk9.green(`
2397
- Local preview: ${htmlPath}`));
2398
- if (!options.offline) {
2399
- try {
2400
- const { supabase } = await getAuthenticatedClient();
2401
- const { data: devicePaths } = await supabase.from("device_paths").select("folder_id, device_name").eq("path", projectRoot);
2402
- if (devicePaths?.length) {
2403
- const { folder_id, device_name } = devicePaths[0];
2404
- const { data: proposedFiles } = await supabase.from("folder_files").select("id, file_path, proposed_at").eq("folder_id", folder_id).eq("proposed_for_deletion", true);
2405
- if (proposedFiles?.length) {
2406
- const { checkbox } = await import("@inquirer/prompts");
2407
- console.log(chalk9.yellow(`
2408
- ${proposedFiles.length} file(s) proposed for deletion:
2409
- `));
2410
- const toDelete = await checkbox({
2411
- message: "Select files to delete (space to toggle, enter to confirm)",
2412
- choices: proposedFiles.map((f) => ({
2413
- name: `${f.file_path}${f.proposed_at ? ` (proposed ${new Date(f.proposed_at).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric" })})` : ""}`,
2414
- value: f
2415
- }))
2416
- });
2417
- for (const file of toDelete) {
2418
- const fullPath = resolve3(projectRoot, file.file_path);
2419
- try {
2420
- const { unlink } = await import("node:fs/promises");
2421
- await unlink(fullPath);
2422
- await supabase.from("folder_files").delete().eq("id", file.id);
2423
- console.log(chalk9.green(` Deleted: ${file.file_path}`));
2424
- } catch (err) {
2425
- console.error(chalk9.red(` Failed to delete ${file.file_path}: ${err}`));
2426
- }
2427
- }
2428
- const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
2429
- if (keptIds.length > 0) {
2430
- for (const id of keptIds) {
2431
- await supabase.from("folder_files").update({ proposed_for_deletion: false, proposed_at: null, proposed_by: null }).eq("id", id);
2432
- }
2433
- console.log(chalk9.cyan(` Kept ${keptIds.length} file(s) \u2014 proposals cleared.`));
2434
- }
2435
- console.log("");
2436
- }
2437
- const { error } = await supabase.from("claude_folders").update({
2438
- graph_json: result.graph,
2439
- orphans_json: result.orphans,
2440
- skills_table_json: result.skills,
2441
- stale_files_json: result.staleFiles,
2442
- env_manifest_json: result.envManifest,
2443
- last_scanned: result.scannedAt,
2444
- data_hash: result.dataHash
2445
- }).eq("id", folder_id);
2446
- if (error) {
2447
- console.error(chalk9.yellow(`Sync warning: ${error.message}`));
2448
- } else {
2449
- await pushToolings(supabase, folder_id, result.toolings);
2450
- await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
2451
- await saveState({
2452
- lastFolderId: folder_id,
2453
- lastDeviceName: device_name,
2454
- lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
2455
- });
2456
- console.log(chalk9.green("Synced to Supabase."));
2457
- console.log(chalk9.cyan(`
2458
- https://www.md4ai.com/project/${folder_id}
2459
- `));
2460
- const configFiles = await readClaudeConfigFiles(projectRoot);
2461
- if (configFiles.length > 0) {
2462
- for (const file of configFiles) {
2463
- await supabase.from("folder_files").upsert({
2464
- folder_id,
2465
- file_path: file.filePath,
2466
- content: file.content,
2467
- size_bytes: file.sizeBytes,
2468
- last_modified: file.lastModified
2469
- }, { onConflict: "folder_id,file_path" });
2470
- }
2471
- console.log(chalk9.green(` Uploaded ${configFiles.length} config file(s).`));
2472
- }
2473
- }
2474
- } else {
2475
- console.log(chalk9.yellow("\nThis folder is not linked to a project on your dashboard."));
2476
- const shouldLink = await confirm2({ message: "Would you like to link it now?" });
2477
- if (!shouldLink) {
2478
- console.log(chalk9.dim("Skipped \u2014 local preview still generated."));
2479
- } else {
2480
- const { supabase: sb, userId } = await getAuthenticatedClient();
2481
- const { data: folders } = await sb.from("claude_folders").select("id, name").order("name");
2482
- const choices = [
2483
- { name: "+ Create a new project", value: "__new__" },
2484
- ...(folders ?? []).map((f) => ({ name: f.name, value: f.id }))
2485
- ];
2486
- const chosen = await select3({
2487
- message: "Link to which project?",
2488
- choices
2489
- });
2490
- let folderId;
2491
- if (chosen === "__new__") {
2492
- const projectName = await input5({
2493
- message: "Project name:",
2494
- default: basename(projectRoot)
2495
- });
2496
- const { data: newFolder, error: createErr } = await sb.from("claude_folders").insert({ user_id: userId, name: projectName }).select("id").single();
2497
- if (createErr || !newFolder) {
2498
- console.error(chalk9.red(`Failed to create project: ${createErr?.message}`));
2499
- return;
2500
- }
2501
- folderId = newFolder.id;
2502
- console.log(chalk9.green(`Created project "${projectName}".`));
2503
- } else {
2504
- folderId = chosen;
2505
- }
2506
- const deviceName = detectDeviceName();
2507
- const osType = detectOs2();
2508
- await sb.from("devices").upsert({
2509
- user_id: userId,
2510
- device_name: deviceName,
2511
- os_type: osType
2512
- }, { onConflict: "user_id,device_name" });
2513
- await sb.from("device_paths").upsert({
2514
- user_id: userId,
2515
- folder_id: folderId,
2516
- device_name: deviceName,
2517
- os_type: osType,
2518
- path: projectRoot,
2519
- last_synced: (/* @__PURE__ */ new Date()).toISOString()
2520
- }, { onConflict: "folder_id,device_name" });
2521
- await sb.from("claude_folders").update({
2522
- graph_json: result.graph,
2523
- orphans_json: result.orphans,
2524
- skills_table_json: result.skills,
2525
- stale_files_json: result.staleFiles,
2526
- env_manifest_json: result.envManifest,
2527
- last_scanned: result.scannedAt,
2528
- data_hash: result.dataHash
2529
- }).eq("id", folderId);
2530
- await pushToolings(sb, folderId, result.toolings);
2531
- const configFiles = await readClaudeConfigFiles(projectRoot);
2532
- for (const file of configFiles) {
2533
- await sb.from("folder_files").upsert({
2534
- folder_id: folderId,
2535
- file_path: file.filePath,
2536
- content: file.content,
2537
- size_bytes: file.sizeBytes,
2538
- last_modified: file.lastModified
2539
- }, { onConflict: "folder_id,file_path" });
2540
- }
2541
- await saveState({
2542
- lastFolderId: folderId,
2543
- lastDeviceName: deviceName,
2544
- lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
2545
- });
2546
- console.log(chalk9.green("\nLinked and synced."));
2547
- console.log(chalk9.cyan(`
2548
- https://www.md4ai.com/project/${folderId}
2549
- `));
2550
- }
2551
- }
2552
- } catch {
2553
- console.log(chalk9.yellow("Not logged in \u2014 local preview only."));
2573
+ const grouped = /* @__PURE__ */ new Map();
2574
+ for (const d of devices) {
2575
+ const list = grouped.get(d.device_name) ?? [];
2576
+ list.push(d);
2577
+ grouped.set(d.device_name, list);
2578
+ }
2579
+ for (const [deviceName, entries] of grouped) {
2580
+ const first = entries[0];
2581
+ console.log(chalk7.bold(`
2582
+ ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
2583
+ for (const entry of entries) {
2584
+ const folderName = entry.claude_folders?.name ?? "unknown";
2585
+ const synced = entry.last_synced ? new Date(entry.last_synced).toLocaleString() : "never";
2586
+ console.log(` ${chalk7.cyan(folderName)} \u2192 ${entry.path}`);
2587
+ console.log(` Last synced: ${synced}`);
2554
2588
  }
2555
2589
  }
2556
2590
  }
2557
2591
 
2592
+ // dist/index.js
2593
+ init_map();
2594
+
2558
2595
  // dist/commands/simulate.js
2559
2596
  init_dist();
2560
2597
  import { join as join9 } from "node:path";
@@ -3334,19 +3371,26 @@ async function postUpdateFlow() {
3334
3371
  return;
3335
3372
  }
3336
3373
  }
3337
- const state = await loadState();
3338
- const hasProject = !!state.lastFolderId;
3339
- if (hasProject) {
3340
- const wantScan = await confirm4({
3341
- message: "Rescan your project and refresh the dashboard?",
3342
- default: true
3343
- });
3344
- if (wantScan) {
3345
- console.log("");
3346
- const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
3347
- await syncCommand2({ all: false });
3348
- console.log("");
3349
- }
3374
+ const { select: selectPrompt } = await import("@inquirer/prompts");
3375
+ const cwd = process.cwd();
3376
+ const scanChoice = await selectPrompt({
3377
+ message: "What would you like to scan?",
3378
+ choices: [
3379
+ { name: `Current folder (${cwd})`, value: "cwd" },
3380
+ { name: "All linked projects on this device", value: "all" },
3381
+ { name: "Skip scanning", value: "skip" }
3382
+ ]
3383
+ });
3384
+ if (scanChoice === "cwd") {
3385
+ console.log("");
3386
+ const { mapCommand: mapCommand2 } = await Promise.resolve().then(() => (init_map(), map_exports));
3387
+ await mapCommand2(cwd, {});
3388
+ console.log("");
3389
+ } else if (scanChoice === "all") {
3390
+ console.log("");
3391
+ const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
3392
+ await syncCommand2({ all: true });
3393
+ console.log("");
3350
3394
  }
3351
3395
  const wantWatch = await confirm4({
3352
3396
  message: "Start monitoring MCP servers? (runs until you press Ctrl+C)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {