md4ai 0.9.2 → 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 +407 -384
  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.2" : "0.0.0-dev";
1381
+ CURRENT_VERSION = true ? "0.9.3" : "0.0.0-dev";
1266
1382
  }
1267
1383
  });
1268
1384
 
@@ -1328,6 +1444,215 @@ var init_device_utils = __esm({
1328
1444
  }
1329
1445
  });
1330
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
+
1331
1656
  // dist/commands/sync.js
1332
1657
  var sync_exports = {};
1333
1658
  __export(sync_exports, {
@@ -2176,385 +2501,76 @@ async function addDeviceCommand() {
2176
2501
  });
2177
2502
  const suggested = suggestDeviceName();
2178
2503
  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;");
2504
+ message: "Device name:",
2505
+ default: suggested
2506
+ });
2507
+ const osType = await select2({
2508
+ message: "OS type:",
2509
+ choices: [
2510
+ { name: "Windows", value: "windows" },
2511
+ { name: "macOS", value: "macos" },
2512
+ { name: "Ubuntu", value: "ubuntu" },
2513
+ { name: "Linux", value: "linux" },
2514
+ { name: "Other", value: "other" }
2515
+ ],
2516
+ default: detectOs()
2517
+ });
2518
+ const description = await input4({ message: "Description (optional):" });
2519
+ const { error } = await supabase.from("device_paths").insert({
2520
+ user_id: userId,
2521
+ folder_id: folderId,
2522
+ device_name: deviceName,
2523
+ os_type: osType,
2524
+ path: localPath,
2525
+ description: description || null
2526
+ });
2527
+ if (error) {
2528
+ console.error(chalk6.red(`Failed to add device: ${error.message}`));
2529
+ process.exit(1);
2530
+ }
2531
+ console.log(chalk6.green(`
2532
+ Device "${deviceName}" added to folder.`));
2364
2533
  }
2365
2534
 
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}`));
2535
+ // dist/commands/list-devices.js
2536
+ init_auth();
2537
+ import chalk7 from "chalk";
2538
+ async function listDevicesCommand() {
2539
+ const { supabase } = await getAuthenticatedClient();
2540
+ const { data: devices, error } = await supabase.from("device_paths").select(`
2541
+ id, device_name, os_type, path, last_synced, description,
2542
+ claude_folders!inner ( name )
2543
+ `).order("device_name");
2544
+ if (error) {
2545
+ console.error(chalk7.red(`Failed to list devices: ${error.message}`));
2376
2546
  process.exit(1);
2377
2547
  }
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 });
2548
+ if (!devices?.length) {
2549
+ console.log(chalk7.yellow("No devices found. Run: md4ai add-device"));
2550
+ return;
2392
2551
  }
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."));
2552
+ const grouped = /* @__PURE__ */ new Map();
2553
+ for (const d of devices) {
2554
+ const list = grouped.get(d.device_name) ?? [];
2555
+ list.push(d);
2556
+ grouped.set(d.device_name, list);
2557
+ }
2558
+ for (const [deviceName, entries] of grouped) {
2559
+ const first = entries[0];
2560
+ console.log(chalk7.bold(`
2561
+ ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
2562
+ for (const entry of entries) {
2563
+ const folderName = entry.claude_folders?.name ?? "unknown";
2564
+ const synced = entry.last_synced ? new Date(entry.last_synced).toLocaleString() : "never";
2565
+ console.log(` ${chalk7.cyan(folderName)} \u2192 ${entry.path}`);
2566
+ console.log(` Last synced: ${synced}`);
2554
2567
  }
2555
2568
  }
2556
2569
  }
2557
2570
 
2571
+ // dist/index.js
2572
+ init_map();
2573
+
2558
2574
  // dist/commands/simulate.js
2559
2575
  init_dist();
2560
2576
  import { join as join9 } from "node:path";
@@ -3334,19 +3350,26 @@ async function postUpdateFlow() {
3334
3350
  return;
3335
3351
  }
3336
3352
  }
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
- }
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("");
3350
3373
  }
3351
3374
  const wantWatch = await confirm4({
3352
3375
  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.3",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {