md4ai 0.10.4 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.bundled.js +688 -272
- package/package.json +2 -2
package/dist/index.bundled.js
CHANGED
|
@@ -73,7 +73,7 @@ var CURRENT_VERSION;
|
|
|
73
73
|
var init_check_update = __esm({
|
|
74
74
|
"dist/check-update.js"() {
|
|
75
75
|
"use strict";
|
|
76
|
-
CURRENT_VERSION = true ? "0.
|
|
76
|
+
CURRENT_VERSION = true ? "0.12.0" : "0.0.0-dev";
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
79
|
|
|
@@ -1479,7 +1479,61 @@ function computeDrift(variables, apps, localPresence, workflowRefs) {
|
|
|
1479
1479
|
workflowBrokenRefs: workflowBrokenRefs.sort()
|
|
1480
1480
|
};
|
|
1481
1481
|
}
|
|
1482
|
-
|
|
1482
|
+
function computeDopplerDrift(doppler2, manifest) {
|
|
1483
|
+
const dopplerNotDeployed = [];
|
|
1484
|
+
const notInDoppler = [];
|
|
1485
|
+
const allDopplerSecrets = /* @__PURE__ */ new Set();
|
|
1486
|
+
for (const config2 of doppler2.configs) {
|
|
1487
|
+
for (const name of config2.secretNames)
|
|
1488
|
+
allDopplerSecrets.add(name);
|
|
1489
|
+
}
|
|
1490
|
+
for (const config2 of doppler2.configs) {
|
|
1491
|
+
const targets = CONFIG_TARGET_MAP[config2.name.toLowerCase()];
|
|
1492
|
+
if (!targets)
|
|
1493
|
+
continue;
|
|
1494
|
+
for (const secretName of config2.secretNames) {
|
|
1495
|
+
const manifestVar = manifest.variables.find((v) => v.name === secretName);
|
|
1496
|
+
if (targets.local && manifestVar) {
|
|
1497
|
+
for (const [app, status] of Object.entries(manifestVar.localStatus)) {
|
|
1498
|
+
if (status === "missing") {
|
|
1499
|
+
dopplerNotDeployed.push({
|
|
1500
|
+
variable: secretName,
|
|
1501
|
+
config: config2.name,
|
|
1502
|
+
missingFrom: "local",
|
|
1503
|
+
app
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (targets.vercel && manifestVar?.vercelStatus) {
|
|
1509
|
+
for (const [proj, status] of Object.entries(manifestVar.vercelStatus)) {
|
|
1510
|
+
if (status === "missing") {
|
|
1511
|
+
dopplerNotDeployed.push({
|
|
1512
|
+
variable: secretName,
|
|
1513
|
+
config: config2.name,
|
|
1514
|
+
missingFrom: "vercel",
|
|
1515
|
+
app: proj
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (targets.github && manifestVar?.githubStatus === "missing") {
|
|
1521
|
+
dopplerNotDeployed.push({
|
|
1522
|
+
variable: secretName,
|
|
1523
|
+
config: config2.name,
|
|
1524
|
+
missingFrom: "github"
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
for (const v of manifest.variables) {
|
|
1530
|
+
if (!allDopplerSecrets.has(v.name)) {
|
|
1531
|
+
notInDoppler.push({ variable: v.name, foundIn: "manifest" });
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return { dopplerNotDeployed, notInDoppler };
|
|
1535
|
+
}
|
|
1536
|
+
var execFileAsync, MANIFEST_PATH, CONFIG_TARGET_MAP;
|
|
1483
1537
|
var init_env_manifest_scanner = __esm({
|
|
1484
1538
|
"dist/scanner/env-manifest-scanner.js"() {
|
|
1485
1539
|
"use strict";
|
|
@@ -1488,6 +1542,15 @@ var init_env_manifest_scanner = __esm({
|
|
|
1488
1542
|
init_fetch_env_vars();
|
|
1489
1543
|
execFileAsync = promisify(execFile);
|
|
1490
1544
|
MANIFEST_PATH = "docs/reference/env-manifest.md";
|
|
1545
|
+
CONFIG_TARGET_MAP = {
|
|
1546
|
+
dev: { local: true, vercel: false, github: false },
|
|
1547
|
+
development: { local: true, vercel: false, github: false },
|
|
1548
|
+
stg: { local: false, vercel: true, github: false },
|
|
1549
|
+
staging: { local: false, vercel: true, github: false },
|
|
1550
|
+
prd: { local: false, vercel: true, github: true },
|
|
1551
|
+
prod: { local: false, vercel: true, github: true },
|
|
1552
|
+
production: { local: false, vercel: true, github: true }
|
|
1553
|
+
};
|
|
1491
1554
|
}
|
|
1492
1555
|
});
|
|
1493
1556
|
|
|
@@ -1591,19 +1654,226 @@ var init_marketplace_scanner = __esm({
|
|
|
1591
1654
|
}
|
|
1592
1655
|
});
|
|
1593
1656
|
|
|
1657
|
+
// dist/doppler/auth.js
|
|
1658
|
+
async function resolveDopplerToken() {
|
|
1659
|
+
const creds = await loadCredentials();
|
|
1660
|
+
if (creds?.dopplerToken) {
|
|
1661
|
+
const { join: join17 } = await import("node:path");
|
|
1662
|
+
const { homedir: homedir10 } = await import("node:os");
|
|
1663
|
+
return {
|
|
1664
|
+
token: creds.dopplerToken,
|
|
1665
|
+
sourcePath: join17(homedir10(), ".md4ai", "credentials.json")
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
return null;
|
|
1669
|
+
}
|
|
1670
|
+
var init_auth3 = __esm({
|
|
1671
|
+
"dist/doppler/auth.js"() {
|
|
1672
|
+
"use strict";
|
|
1673
|
+
init_config();
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
// dist/doppler/api.js
|
|
1678
|
+
async function dopplerFetch(url, token, retryCount = 0) {
|
|
1679
|
+
const res = await fetch(url, {
|
|
1680
|
+
headers: {
|
|
1681
|
+
Authorization: `Bearer ${token}`,
|
|
1682
|
+
Accept: "application/json"
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
if (res.status === 429 && retryCount < MAX_RETRIES) {
|
|
1686
|
+
const retryAfter = parseInt(res.headers.get("retry-after") ?? "2", 10);
|
|
1687
|
+
await new Promise((r) => setTimeout(r, retryAfter * 1e3));
|
|
1688
|
+
return dopplerFetch(url, token, retryCount + 1);
|
|
1689
|
+
}
|
|
1690
|
+
if (!res.ok) {
|
|
1691
|
+
const isAuth = res.status === 401 || res.status === 403;
|
|
1692
|
+
const body = await res.text().catch(() => "");
|
|
1693
|
+
throw new DopplerApiError(`Doppler API ${res.status}: ${body || res.statusText}`, res.status, isAuth);
|
|
1694
|
+
}
|
|
1695
|
+
return res.json();
|
|
1696
|
+
}
|
|
1697
|
+
async function fetchDopplerConfigs(projectSlug, token) {
|
|
1698
|
+
const data = await dopplerFetch(`https://api.doppler.com/v3/configs?project=${encodeURIComponent(projectSlug)}`, token);
|
|
1699
|
+
return data.configs;
|
|
1700
|
+
}
|
|
1701
|
+
async function fetchDopplerSecretNames(projectSlug, configName, token) {
|
|
1702
|
+
const data = await dopplerFetch(`https://api.doppler.com/v3/configs/config/secrets?project=${encodeURIComponent(projectSlug)}&config=${encodeURIComponent(configName)}`, token);
|
|
1703
|
+
return Object.keys(data.secrets);
|
|
1704
|
+
}
|
|
1705
|
+
async function validateDopplerToken(token) {
|
|
1706
|
+
try {
|
|
1707
|
+
const data = await dopplerFetch("https://api.doppler.com/v3/projects", token);
|
|
1708
|
+
return { valid: true, projectCount: data.projects.length };
|
|
1709
|
+
} catch (err) {
|
|
1710
|
+
if (err instanceof DopplerApiError && err.isAuthError) {
|
|
1711
|
+
return { valid: false, projectCount: 0 };
|
|
1712
|
+
}
|
|
1713
|
+
throw err;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
async function fetchDopplerProjects(token) {
|
|
1717
|
+
const data = await dopplerFetch("https://api.doppler.com/v3/projects", token);
|
|
1718
|
+
return data.projects.map((p) => ({ slug: p.slug, name: p.name }));
|
|
1719
|
+
}
|
|
1720
|
+
var DopplerApiError, MAX_RETRIES;
|
|
1721
|
+
var init_api = __esm({
|
|
1722
|
+
"dist/doppler/api.js"() {
|
|
1723
|
+
"use strict";
|
|
1724
|
+
DopplerApiError = class extends Error {
|
|
1725
|
+
status;
|
|
1726
|
+
isAuthError;
|
|
1727
|
+
constructor(message, status, isAuthError) {
|
|
1728
|
+
super(message);
|
|
1729
|
+
this.status = status;
|
|
1730
|
+
this.isAuthError = isAuthError;
|
|
1731
|
+
this.name = "DopplerApiError";
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
MAX_RETRIES = 2;
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
// dist/scanner/doppler-scanner.js
|
|
1739
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
1740
|
+
import { join as join11, basename } from "node:path";
|
|
1741
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
1742
|
+
import chalk10 from "chalk";
|
|
1743
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
1744
|
+
async function parseDopplerYaml(projectRoot) {
|
|
1745
|
+
const yamlPath = join11(projectRoot, "doppler.yaml");
|
|
1746
|
+
if (!existsSync6(yamlPath))
|
|
1747
|
+
return null;
|
|
1748
|
+
try {
|
|
1749
|
+
const content = await readFile9(yamlPath, "utf-8");
|
|
1750
|
+
const match = content.match(/^project:\s*["']?([a-z0-9_-]+)["']?\s*$/m);
|
|
1751
|
+
return match?.[1] ?? null;
|
|
1752
|
+
} catch {
|
|
1753
|
+
return null;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
async function scanDoppler(projectRoot, folderId) {
|
|
1757
|
+
const tokenResult = await resolveDopplerToken();
|
|
1758
|
+
if (!tokenResult) {
|
|
1759
|
+
console.log(chalk10.dim(' Doppler: no token configured. Run "md4ai doppler connect" to set up.'));
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
let projectSlug = await parseDopplerYaml(projectRoot);
|
|
1763
|
+
let matchedVia = "doppler-yaml";
|
|
1764
|
+
if (!projectSlug && folderId) {
|
|
1765
|
+
const state = await loadState();
|
|
1766
|
+
projectSlug = state.dopplerProjects?.[folderId] ?? null;
|
|
1767
|
+
if (projectSlug)
|
|
1768
|
+
matchedVia = "manual";
|
|
1769
|
+
}
|
|
1770
|
+
if (!projectSlug) {
|
|
1771
|
+
if (!folderId || !process.stdin.isTTY) {
|
|
1772
|
+
console.log(chalk10.dim(' Doppler: token configured but no project linked. Run "md4ai doppler set-project <slug>" to link one.'));
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
const projects = await fetchDopplerProjects(tokenResult.token);
|
|
1776
|
+
if (projects.length === 0) {
|
|
1777
|
+
console.log(chalk10.yellow(" Doppler: no projects accessible with this token."));
|
|
1778
|
+
return null;
|
|
1779
|
+
}
|
|
1780
|
+
const projectName = basename(projectRoot);
|
|
1781
|
+
if (projects.length === 1) {
|
|
1782
|
+
projectSlug = projects[0].slug;
|
|
1783
|
+
console.log(chalk10.green(` Doppler: using "${projectSlug}" (only accessible project).`));
|
|
1784
|
+
} else {
|
|
1785
|
+
const autoMatch = projects.find((p) => p.slug.toLowerCase() === projectName.toLowerCase());
|
|
1786
|
+
if (autoMatch) {
|
|
1787
|
+
projectSlug = autoMatch.slug;
|
|
1788
|
+
console.log(chalk10.green(` Doppler: auto-matched project "${projectSlug}" from project name.`));
|
|
1789
|
+
} else {
|
|
1790
|
+
projectSlug = await select3({
|
|
1791
|
+
message: `Which Doppler project holds secrets for "${projectName}"?`,
|
|
1792
|
+
choices: projects.map((p) => ({
|
|
1793
|
+
name: `${p.slug}${p.name !== p.slug ? ` (${p.name})` : ""}`,
|
|
1794
|
+
value: p.slug
|
|
1795
|
+
}))
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
const state = await loadState();
|
|
1800
|
+
const dopplerProjects = state.dopplerProjects ?? {};
|
|
1801
|
+
dopplerProjects[folderId] = projectSlug;
|
|
1802
|
+
await saveState({ dopplerProjects });
|
|
1803
|
+
matchedVia = "manual";
|
|
1804
|
+
console.log(chalk10.dim(` Doppler: saved "${projectSlug}" for this project. To change: md4ai doppler set-project <slug>`));
|
|
1805
|
+
}
|
|
1806
|
+
console.log(chalk10.dim(` Doppler: checking project "${projectSlug}" (via ${matchedVia})...`));
|
|
1807
|
+
try {
|
|
1808
|
+
const apiConfigs = await fetchDopplerConfigs(projectSlug, tokenResult.token);
|
|
1809
|
+
const CONCURRENCY = 4;
|
|
1810
|
+
const configs = [];
|
|
1811
|
+
for (let i = 0; i < apiConfigs.length; i += CONCURRENCY) {
|
|
1812
|
+
const batch = apiConfigs.slice(i, i + CONCURRENCY);
|
|
1813
|
+
const results = await Promise.all(batch.map(async (cfg) => {
|
|
1814
|
+
try {
|
|
1815
|
+
const secretNames = await fetchDopplerSecretNames(projectSlug, cfg.name, tokenResult.token);
|
|
1816
|
+
return {
|
|
1817
|
+
name: cfg.name,
|
|
1818
|
+
environment: cfg.environment,
|
|
1819
|
+
secretNames
|
|
1820
|
+
};
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
if (err instanceof DopplerApiError && err.status === 429) {
|
|
1823
|
+
console.log(chalk10.yellow(` Doppler: rate limited on config "${cfg.name}", skipping remaining.`));
|
|
1824
|
+
return null;
|
|
1825
|
+
}
|
|
1826
|
+
console.log(chalk10.dim(` Doppler: failed to fetch config "${cfg.name}": ${err instanceof Error ? err.message : String(err)}`));
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1829
|
+
}));
|
|
1830
|
+
for (const r of results) {
|
|
1831
|
+
if (r)
|
|
1832
|
+
configs.push(r);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
console.log(chalk10.dim(` Doppler: ${configs.length} config(s), ${configs.reduce((n, c) => n + c.secretNames.length, 0)} total secrets`));
|
|
1836
|
+
return {
|
|
1837
|
+
project: projectSlug,
|
|
1838
|
+
configs,
|
|
1839
|
+
matchedVia,
|
|
1840
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1841
|
+
};
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
if (err instanceof DopplerApiError) {
|
|
1844
|
+
if (err.isAuthError) {
|
|
1845
|
+
console.log(chalk10.yellow(' Doppler: token invalid or expired. Run "md4ai doppler connect" to reconfigure.'));
|
|
1846
|
+
} else {
|
|
1847
|
+
console.log(chalk10.yellow(` Doppler: API error \u2014 ${err.message}`));
|
|
1848
|
+
}
|
|
1849
|
+
} else {
|
|
1850
|
+
console.log(chalk10.yellow(` Doppler: ${err instanceof Error ? err.message : String(err)}`));
|
|
1851
|
+
}
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
var init_doppler_scanner = __esm({
|
|
1856
|
+
"dist/scanner/doppler-scanner.js"() {
|
|
1857
|
+
"use strict";
|
|
1858
|
+
init_auth3();
|
|
1859
|
+
init_api();
|
|
1860
|
+
init_config();
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1594
1864
|
// dist/scanner/index.js
|
|
1595
1865
|
import { readdir as readdir3 } from "node:fs/promises";
|
|
1596
|
-
import { join as
|
|
1597
|
-
import { existsSync as
|
|
1866
|
+
import { join as join12, relative as relative3 } from "node:path";
|
|
1867
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1598
1868
|
import { homedir as homedir7 } from "node:os";
|
|
1599
1869
|
import { createHash } from "node:crypto";
|
|
1600
|
-
async function scanProject(projectRoot) {
|
|
1870
|
+
async function scanProject(projectRoot, folderId) {
|
|
1601
1871
|
const allFiles = await discoverFiles(projectRoot);
|
|
1602
1872
|
const rootFiles = identifyRoots(allFiles, projectRoot);
|
|
1603
1873
|
const allRefs = [];
|
|
1604
1874
|
const allBrokenRefs = [];
|
|
1605
1875
|
for (const file of allFiles) {
|
|
1606
|
-
const fullPath = file.startsWith("/") ? file :
|
|
1876
|
+
const fullPath = file.startsWith("/") ? file : join12(projectRoot, file);
|
|
1607
1877
|
try {
|
|
1608
1878
|
const { refs, brokenRefs: brokenRefs2 } = await parseFileReferences(fullPath, projectRoot);
|
|
1609
1879
|
allRefs.push(...refs);
|
|
@@ -1618,6 +1888,12 @@ async function scanProject(projectRoot) {
|
|
|
1618
1888
|
const toolings = await detectToolings(projectRoot);
|
|
1619
1889
|
const envManifest = await scanEnvManifest(projectRoot);
|
|
1620
1890
|
const marketplacePlugins = await scanMarketplacePlugins();
|
|
1891
|
+
const doppler2 = await scanDoppler(projectRoot, folderId);
|
|
1892
|
+
if (doppler2 && envManifest) {
|
|
1893
|
+
const dopplerDrift = computeDopplerDrift(doppler2, envManifest);
|
|
1894
|
+
envManifest.drift.dopplerNotDeployed = dopplerDrift.dopplerNotDeployed;
|
|
1895
|
+
envManifest.drift.notInDoppler = dopplerDrift.notInDoppler;
|
|
1896
|
+
}
|
|
1621
1897
|
const depthMap = /* @__PURE__ */ new Map();
|
|
1622
1898
|
const queue = [...rootFiles];
|
|
1623
1899
|
for (const r of queue)
|
|
@@ -1638,7 +1914,7 @@ async function scanProject(projectRoot) {
|
|
|
1638
1914
|
depth: depthMap.get(br.from) ?? 999
|
|
1639
1915
|
}));
|
|
1640
1916
|
brokenRefs.sort((a, b) => a.depth - b.depth || a.from.localeCompare(b.from));
|
|
1641
|
-
const scanData = JSON.stringify({ graph, orphans, brokenRefs, skills, staleFiles, toolings, envManifest, marketplacePlugins });
|
|
1917
|
+
const scanData = JSON.stringify({ graph, orphans, brokenRefs, skills, staleFiles, toolings, envManifest, marketplacePlugins, doppler: doppler2 });
|
|
1642
1918
|
const dataHash = createHash("sha256").update(scanData).digest("hex");
|
|
1643
1919
|
return {
|
|
1644
1920
|
graph,
|
|
@@ -1648,6 +1924,7 @@ async function scanProject(projectRoot) {
|
|
|
1648
1924
|
staleFiles,
|
|
1649
1925
|
toolings,
|
|
1650
1926
|
envManifest,
|
|
1927
|
+
doppler: doppler2,
|
|
1651
1928
|
marketplacePlugins,
|
|
1652
1929
|
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1653
1930
|
dataHash
|
|
@@ -1655,18 +1932,18 @@ async function scanProject(projectRoot) {
|
|
|
1655
1932
|
}
|
|
1656
1933
|
async function discoverFiles(projectRoot) {
|
|
1657
1934
|
const files = [];
|
|
1658
|
-
const claudeDir =
|
|
1659
|
-
if (
|
|
1935
|
+
const claudeDir = join12(projectRoot, ".claude");
|
|
1936
|
+
if (existsSync7(claudeDir)) {
|
|
1660
1937
|
await walkDir(claudeDir, projectRoot, files);
|
|
1661
1938
|
}
|
|
1662
|
-
if (
|
|
1939
|
+
if (existsSync7(join12(projectRoot, "CLAUDE.md"))) {
|
|
1663
1940
|
files.push("CLAUDE.md");
|
|
1664
1941
|
}
|
|
1665
|
-
if (
|
|
1942
|
+
if (existsSync7(join12(projectRoot, "skills.md"))) {
|
|
1666
1943
|
files.push("skills.md");
|
|
1667
1944
|
}
|
|
1668
|
-
const plansDir =
|
|
1669
|
-
if (
|
|
1945
|
+
const plansDir = join12(projectRoot, "docs", "plans");
|
|
1946
|
+
if (existsSync7(plansDir)) {
|
|
1670
1947
|
await walkDir(plansDir, projectRoot, files);
|
|
1671
1948
|
}
|
|
1672
1949
|
return [...new Set(files)];
|
|
@@ -1674,7 +1951,7 @@ async function discoverFiles(projectRoot) {
|
|
|
1674
1951
|
async function walkDir(dir, projectRoot, files) {
|
|
1675
1952
|
const entries = await readdir3(dir, { withFileTypes: true });
|
|
1676
1953
|
for (const entry of entries) {
|
|
1677
|
-
const fullPath =
|
|
1954
|
+
const fullPath = join12(dir, entry.name);
|
|
1678
1955
|
if (entry.isDirectory()) {
|
|
1679
1956
|
if (["node_modules", ".git", ".turbo", "cache", "session-env"].includes(entry.name))
|
|
1680
1957
|
continue;
|
|
@@ -1694,15 +1971,15 @@ function identifyRoots(allFiles, projectRoot) {
|
|
|
1694
1971
|
}
|
|
1695
1972
|
for (const globalFile of GLOBAL_ROOT_FILES) {
|
|
1696
1973
|
const expanded = globalFile.replace("~", homedir7());
|
|
1697
|
-
if (
|
|
1974
|
+
if (existsSync7(expanded)) {
|
|
1698
1975
|
roots.push(globalFile);
|
|
1699
1976
|
}
|
|
1700
1977
|
}
|
|
1701
1978
|
return roots;
|
|
1702
1979
|
}
|
|
1703
1980
|
async function readClaudeConfigFiles(projectRoot, graphFilePaths) {
|
|
1704
|
-
const { readFile:
|
|
1705
|
-
const { existsSync:
|
|
1981
|
+
const { readFile: readFile14, stat: stat2 } = await import("node:fs/promises");
|
|
1982
|
+
const { existsSync: existsSync15 } = await import("node:fs");
|
|
1706
1983
|
const filePaths = graphFilePaths ?? await discoverFiles(projectRoot);
|
|
1707
1984
|
const files = [];
|
|
1708
1985
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1712,11 +1989,11 @@ async function readClaudeConfigFiles(projectRoot, graphFilePaths) {
|
|
|
1712
1989
|
if (relPath.startsWith("~") || relPath.startsWith("/"))
|
|
1713
1990
|
continue;
|
|
1714
1991
|
seen.add(relPath);
|
|
1715
|
-
const fullPath =
|
|
1716
|
-
if (!
|
|
1992
|
+
const fullPath = join12(projectRoot, relPath);
|
|
1993
|
+
if (!existsSync15(fullPath))
|
|
1717
1994
|
continue;
|
|
1718
1995
|
try {
|
|
1719
|
-
const content = await
|
|
1996
|
+
const content = await readFile14(fullPath, "utf-8");
|
|
1720
1997
|
const fileStat = await stat2(fullPath);
|
|
1721
1998
|
const lastMod = getGitLastModified(fullPath, projectRoot);
|
|
1722
1999
|
files.push({ filePath: relPath, content, sizeBytes: fileStat.size, lastModified: lastMod });
|
|
@@ -1729,7 +2006,7 @@ function detectStaleFiles(allFiles, projectRoot) {
|
|
|
1729
2006
|
const stale = [];
|
|
1730
2007
|
const now = Date.now();
|
|
1731
2008
|
for (const file of allFiles) {
|
|
1732
|
-
const fullPath =
|
|
2009
|
+
const fullPath = join12(projectRoot, file);
|
|
1733
2010
|
const lastMod = getGitLastModified(fullPath, projectRoot);
|
|
1734
2011
|
if (lastMod) {
|
|
1735
2012
|
const days = Math.floor((now - new Date(lastMod).getTime()) / (1e3 * 60 * 60 * 24));
|
|
@@ -1752,6 +2029,7 @@ var init_scanner = __esm({
|
|
|
1752
2029
|
init_tooling_detector();
|
|
1753
2030
|
init_env_manifest_scanner();
|
|
1754
2031
|
init_marketplace_scanner();
|
|
2032
|
+
init_doppler_scanner();
|
|
1755
2033
|
}
|
|
1756
2034
|
});
|
|
1757
2035
|
|
|
@@ -1896,7 +2174,7 @@ var init_push_toolings = __esm({
|
|
|
1896
2174
|
});
|
|
1897
2175
|
|
|
1898
2176
|
// dist/commands/push-health-results.js
|
|
1899
|
-
import
|
|
2177
|
+
import chalk11 from "chalk";
|
|
1900
2178
|
async function pushHealthResults(supabase, folderId, manifest) {
|
|
1901
2179
|
const { data: checks } = await supabase.from("env_health_checks").select("id, check_type, check_config").eq("folder_id", folderId).eq("enabled", true);
|
|
1902
2180
|
if (!checks?.length)
|
|
@@ -1964,9 +2242,9 @@ async function pushHealthResults(supabase, folderId, manifest) {
|
|
|
1964
2242
|
if (results.length > 0) {
|
|
1965
2243
|
const { error } = await supabase.from("env_health_results").insert(results);
|
|
1966
2244
|
if (error) {
|
|
1967
|
-
console.error(
|
|
2245
|
+
console.error(chalk11.yellow(`Health results warning: ${error.message}`));
|
|
1968
2246
|
} else {
|
|
1969
|
-
console.log(
|
|
2247
|
+
console.log(chalk11.green(` Updated ${results.length} health check result(s).`));
|
|
1970
2248
|
const checkIds = results.map((r) => r.check_id);
|
|
1971
2249
|
const { data: allResults } = await supabase.from("env_health_results").select("id, check_id, ran_at").in("check_id", checkIds).order("ran_at", { ascending: false });
|
|
1972
2250
|
if (allResults) {
|
|
@@ -2035,21 +2313,30 @@ var map_exports = {};
|
|
|
2035
2313
|
__export(map_exports, {
|
|
2036
2314
|
mapCommand: () => mapCommand
|
|
2037
2315
|
});
|
|
2038
|
-
import { resolve as resolve4, basename } from "node:path";
|
|
2316
|
+
import { resolve as resolve4, basename as basename2 } from "node:path";
|
|
2039
2317
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
2040
|
-
import { existsSync as
|
|
2041
|
-
import
|
|
2042
|
-
import { select as
|
|
2318
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
2319
|
+
import chalk12 from "chalk";
|
|
2320
|
+
import { select as select4, input as input5, confirm as confirm2 } from "@inquirer/prompts";
|
|
2043
2321
|
async function mapCommand(path, options) {
|
|
2044
2322
|
await checkForUpdate();
|
|
2045
2323
|
const projectRoot = resolve4(path ?? process.cwd());
|
|
2046
|
-
if (!
|
|
2047
|
-
console.error(
|
|
2324
|
+
if (!existsSync8(projectRoot)) {
|
|
2325
|
+
console.error(chalk12.red(`Path not found: ${projectRoot}`));
|
|
2048
2326
|
process.exit(1);
|
|
2049
2327
|
}
|
|
2050
|
-
console.log(
|
|
2328
|
+
console.log(chalk12.blue(`Scanning: ${projectRoot}
|
|
2051
2329
|
`));
|
|
2052
|
-
|
|
2330
|
+
let earlyFolderId;
|
|
2331
|
+
if (!options.offline) {
|
|
2332
|
+
try {
|
|
2333
|
+
const { supabase: earlyDb } = await getAuthenticatedClient();
|
|
2334
|
+
const { data: dp } = await earlyDb.from("device_paths").select("folder_id").eq("path", projectRoot).maybeSingle();
|
|
2335
|
+
earlyFolderId = dp?.folder_id ?? void 0;
|
|
2336
|
+
} catch {
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
const result = await scanProject(projectRoot, earlyFolderId);
|
|
2053
2340
|
console.log(` Files found: ${result.graph.nodes.length}`);
|
|
2054
2341
|
console.log(` References: ${result.graph.edges.length}`);
|
|
2055
2342
|
console.log(` Broken refs: ${result.brokenRefs.length}`);
|
|
@@ -2058,26 +2345,27 @@ async function mapCommand(path, options) {
|
|
|
2058
2345
|
console.log(` Skills: ${result.skills.length}`);
|
|
2059
2346
|
console.log(` Toolings: ${result.toolings.length}`);
|
|
2060
2347
|
console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
|
|
2348
|
+
console.log(` Doppler: ${result.doppler ? `${result.doppler.configs.length} config(s) \u2014 ${result.doppler.project}` : "see above"}`);
|
|
2061
2349
|
console.log(` Plugins: ${result.marketplacePlugins.length} (${result.marketplacePlugins.reduce((n, p) => n + p.skills.length, 0)} skills)`);
|
|
2062
2350
|
console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
|
|
2063
2351
|
if (result.brokenRefs.length > 0) {
|
|
2064
|
-
console.log(
|
|
2352
|
+
console.log(chalk12.red(`
|
|
2065
2353
|
Warning: ${result.brokenRefs.length} broken reference(s) found:`));
|
|
2066
2354
|
for (const br of result.brokenRefs.slice(0, 5)) {
|
|
2067
|
-
console.log(
|
|
2355
|
+
console.log(chalk12.red(` ${br.from} -> ${br.to}`));
|
|
2068
2356
|
}
|
|
2069
2357
|
if (result.brokenRefs.length > 5) {
|
|
2070
|
-
console.log(
|
|
2358
|
+
console.log(chalk12.red(` ... and ${result.brokenRefs.length - 5} more`));
|
|
2071
2359
|
}
|
|
2072
2360
|
}
|
|
2073
2361
|
const outputDir = resolve4(projectRoot, "output");
|
|
2074
|
-
if (!
|
|
2362
|
+
if (!existsSync8(outputDir)) {
|
|
2075
2363
|
await mkdir2(outputDir, { recursive: true });
|
|
2076
2364
|
}
|
|
2077
2365
|
const htmlPath = resolve4(outputDir, "index.html");
|
|
2078
2366
|
const html = generateOfflineHtml(result, projectRoot);
|
|
2079
2367
|
await writeFile2(htmlPath, html, "utf-8");
|
|
2080
|
-
console.log(
|
|
2368
|
+
console.log(chalk12.green(`
|
|
2081
2369
|
Local preview: ${htmlPath}`));
|
|
2082
2370
|
if (!options.offline) {
|
|
2083
2371
|
try {
|
|
@@ -2088,7 +2376,7 @@ Local preview: ${htmlPath}`));
|
|
|
2088
2376
|
const { data: proposedFiles } = await supabase.from("folder_files").select("id, file_path, proposed_at").eq("folder_id", folder_id).eq("proposed_for_deletion", true);
|
|
2089
2377
|
if (proposedFiles?.length && process.stdin.isTTY) {
|
|
2090
2378
|
const { checkbox } = await import("@inquirer/prompts");
|
|
2091
|
-
console.log(
|
|
2379
|
+
console.log(chalk12.yellow(`
|
|
2092
2380
|
${proposedFiles.length} file(s) proposed for deletion:
|
|
2093
2381
|
`));
|
|
2094
2382
|
const toDelete = await checkbox({
|
|
@@ -2102,16 +2390,16 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2102
2390
|
for (const file of toDelete) {
|
|
2103
2391
|
const fullPath = resolve4(projectRoot, file.file_path);
|
|
2104
2392
|
if (!fullPath.startsWith(resolvedRoot + "/")) {
|
|
2105
|
-
console.error(
|
|
2393
|
+
console.error(chalk12.red(` Blocked path traversal: ${file.file_path}`));
|
|
2106
2394
|
continue;
|
|
2107
2395
|
}
|
|
2108
2396
|
try {
|
|
2109
2397
|
const { unlink } = await import("node:fs/promises");
|
|
2110
2398
|
await unlink(fullPath);
|
|
2111
2399
|
await supabase.from("folder_files").delete().eq("id", file.id);
|
|
2112
|
-
console.log(
|
|
2400
|
+
console.log(chalk12.green(` Deleted: ${file.file_path}`));
|
|
2113
2401
|
} catch (err) {
|
|
2114
|
-
console.error(
|
|
2402
|
+
console.error(chalk12.red(` Failed to delete ${file.file_path}: ${err instanceof Error ? err.message : String(err)}`));
|
|
2115
2403
|
}
|
|
2116
2404
|
}
|
|
2117
2405
|
const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
|
|
@@ -2119,11 +2407,11 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2119
2407
|
for (const id of keptIds) {
|
|
2120
2408
|
await supabase.from("folder_files").update({ proposed_for_deletion: false, proposed_at: null, proposed_by: null }).eq("id", id);
|
|
2121
2409
|
}
|
|
2122
|
-
console.log(
|
|
2410
|
+
console.log(chalk12.cyan(` Kept ${keptIds.length} file(s) \u2014 proposals cleared.`));
|
|
2123
2411
|
}
|
|
2124
2412
|
console.log("");
|
|
2125
2413
|
} else if (proposedFiles?.length) {
|
|
2126
|
-
console.log(
|
|
2414
|
+
console.log(chalk12.dim(` ${proposedFiles.length} file(s) proposed for deletion \u2014 skipped (non-interactive mode).`));
|
|
2127
2415
|
}
|
|
2128
2416
|
let storedManifest = null;
|
|
2129
2417
|
if (!result.envManifest) {
|
|
@@ -2145,9 +2433,12 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2145
2433
|
if (result.envManifest) {
|
|
2146
2434
|
updatePayload.env_manifest_json = result.envManifest;
|
|
2147
2435
|
}
|
|
2436
|
+
if (result.doppler) {
|
|
2437
|
+
updatePayload.doppler_json = result.doppler;
|
|
2438
|
+
}
|
|
2148
2439
|
const { error } = await supabase.from("claude_folders").update(updatePayload).eq("id", folder_id);
|
|
2149
2440
|
if (error) {
|
|
2150
|
-
console.error(
|
|
2441
|
+
console.error(chalk12.yellow(`Sync warning: ${error.message}`));
|
|
2151
2442
|
} else {
|
|
2152
2443
|
await pushToolings(supabase, folder_id, result.toolings);
|
|
2153
2444
|
const manifestForHealth = result.envManifest ?? storedManifest;
|
|
@@ -2160,8 +2451,8 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2160
2451
|
lastDeviceName: device_name,
|
|
2161
2452
|
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2162
2453
|
});
|
|
2163
|
-
console.log(
|
|
2164
|
-
console.log(
|
|
2454
|
+
console.log(chalk12.green("Synced to Supabase."));
|
|
2455
|
+
console.log(chalk12.cyan(`
|
|
2165
2456
|
https://www.md4ai.com/project/${folder_id}
|
|
2166
2457
|
`));
|
|
2167
2458
|
const graphPaths = result.graph.nodes.map((n) => n.filePath);
|
|
@@ -2176,18 +2467,18 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2176
2467
|
last_modified: file.lastModified
|
|
2177
2468
|
}, { onConflict: "folder_id,file_path" });
|
|
2178
2469
|
}
|
|
2179
|
-
console.log(
|
|
2470
|
+
console.log(chalk12.green(` Uploaded ${configFiles.length} config file(s).`));
|
|
2180
2471
|
}
|
|
2181
2472
|
}
|
|
2182
2473
|
} else {
|
|
2183
|
-
console.log(
|
|
2474
|
+
console.log(chalk12.yellow("\nThis folder is not linked to a project on your dashboard."));
|
|
2184
2475
|
if (!process.stdin.isTTY) {
|
|
2185
|
-
console.log(
|
|
2476
|
+
console.log(chalk12.dim("Non-interactive mode \u2014 skipping link prompt."));
|
|
2186
2477
|
return;
|
|
2187
2478
|
}
|
|
2188
2479
|
const shouldLink = await confirm2({ message: "Would you like to link it now?" });
|
|
2189
2480
|
if (!shouldLink) {
|
|
2190
|
-
console.log(
|
|
2481
|
+
console.log(chalk12.dim("Skipped \u2014 local preview still generated."));
|
|
2191
2482
|
} else {
|
|
2192
2483
|
const { supabase: sb, userId } = await getAuthenticatedClient();
|
|
2193
2484
|
const { data: folders } = await sb.from("claude_folders").select("id, name").order("name");
|
|
@@ -2195,7 +2486,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2195
2486
|
{ name: "+ Create a new project", value: "__new__" },
|
|
2196
2487
|
...(folders ?? []).map((f) => ({ name: f.name, value: f.id }))
|
|
2197
2488
|
];
|
|
2198
|
-
const chosen = await
|
|
2489
|
+
const chosen = await select4({
|
|
2199
2490
|
message: "Link to which project?",
|
|
2200
2491
|
choices
|
|
2201
2492
|
});
|
|
@@ -2203,15 +2494,15 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2203
2494
|
if (chosen === "__new__") {
|
|
2204
2495
|
const projectName = await input5({
|
|
2205
2496
|
message: "Project name:",
|
|
2206
|
-
default:
|
|
2497
|
+
default: basename2(projectRoot)
|
|
2207
2498
|
});
|
|
2208
2499
|
const { data: newFolder, error: createErr } = await sb.from("claude_folders").insert({ user_id: userId, name: projectName }).select("id").single();
|
|
2209
2500
|
if (createErr || !newFolder) {
|
|
2210
|
-
console.error(
|
|
2501
|
+
console.error(chalk12.red(`Failed to create project: ${createErr?.message}`));
|
|
2211
2502
|
return;
|
|
2212
2503
|
}
|
|
2213
2504
|
folderId = newFolder.id;
|
|
2214
|
-
console.log(
|
|
2505
|
+
console.log(chalk12.green(`Created project "${projectName}".`));
|
|
2215
2506
|
} else {
|
|
2216
2507
|
folderId = chosen;
|
|
2217
2508
|
}
|
|
@@ -2237,6 +2528,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2237
2528
|
skills_table_json: result.skills,
|
|
2238
2529
|
stale_files_json: result.staleFiles,
|
|
2239
2530
|
env_manifest_json: result.envManifest,
|
|
2531
|
+
doppler_json: result.doppler,
|
|
2240
2532
|
marketplace_plugins_json: result.marketplacePlugins,
|
|
2241
2533
|
last_scanned: result.scannedAt,
|
|
2242
2534
|
data_hash: result.dataHash
|
|
@@ -2261,14 +2553,14 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2261
2553
|
lastDeviceName: deviceName,
|
|
2262
2554
|
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2263
2555
|
});
|
|
2264
|
-
console.log(
|
|
2265
|
-
console.log(
|
|
2556
|
+
console.log(chalk12.green("\nLinked and synced."));
|
|
2557
|
+
console.log(chalk12.cyan(`
|
|
2266
2558
|
https://www.md4ai.com/project/${folderId}
|
|
2267
2559
|
`));
|
|
2268
2560
|
}
|
|
2269
2561
|
}
|
|
2270
2562
|
} catch {
|
|
2271
|
-
console.log(
|
|
2563
|
+
console.log(chalk12.yellow("Not logged in \u2014 local preview only."));
|
|
2272
2564
|
}
|
|
2273
2565
|
}
|
|
2274
2566
|
}
|
|
@@ -2292,29 +2584,29 @@ __export(sync_exports, {
|
|
|
2292
2584
|
syncCommand: () => syncCommand
|
|
2293
2585
|
});
|
|
2294
2586
|
import { resolve as resolve5 } from "node:path";
|
|
2295
|
-
import { existsSync as
|
|
2296
|
-
import
|
|
2587
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
2588
|
+
import chalk15 from "chalk";
|
|
2297
2589
|
function isValidProjectPath(p) {
|
|
2298
2590
|
const resolved = resolve5(p);
|
|
2299
|
-
return resolved.startsWith("/") && !resolved.includes("..") &&
|
|
2591
|
+
return resolved.startsWith("/") && !resolved.includes("..") && existsSync11(resolved);
|
|
2300
2592
|
}
|
|
2301
2593
|
async function syncCommand(options) {
|
|
2302
2594
|
const { supabase } = await getAuthenticatedClient();
|
|
2303
2595
|
if (options.all) {
|
|
2304
2596
|
const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path");
|
|
2305
2597
|
if (error || !devices?.length) {
|
|
2306
|
-
console.error(
|
|
2598
|
+
console.error(chalk15.red("No devices found."));
|
|
2307
2599
|
process.exit(1);
|
|
2308
2600
|
}
|
|
2309
2601
|
for (const device of devices) {
|
|
2310
2602
|
if (!isValidProjectPath(device.path)) {
|
|
2311
|
-
console.error(
|
|
2603
|
+
console.error(chalk15.red(` Skipping invalid path: ${device.path}`));
|
|
2312
2604
|
continue;
|
|
2313
2605
|
}
|
|
2314
|
-
console.log(
|
|
2606
|
+
console.log(chalk15.blue(`Syncing: ${device.path}`));
|
|
2315
2607
|
const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
|
|
2316
2608
|
if (proposedAll?.length) {
|
|
2317
|
-
console.log(
|
|
2609
|
+
console.log(chalk15.yellow(` ${proposedAll.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
|
|
2318
2610
|
}
|
|
2319
2611
|
try {
|
|
2320
2612
|
const result = await scanProject(device.path);
|
|
@@ -2330,32 +2622,32 @@ async function syncCommand(options) {
|
|
|
2330
2622
|
}).eq("id", device.folder_id);
|
|
2331
2623
|
await pushToolings(supabase, device.folder_id, result.toolings);
|
|
2332
2624
|
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
|
|
2333
|
-
console.log(
|
|
2625
|
+
console.log(chalk15.green(` Done: ${device.device_name}`));
|
|
2334
2626
|
} catch (err) {
|
|
2335
|
-
console.error(
|
|
2627
|
+
console.error(chalk15.red(` Failed: ${device.path}: ${err}`));
|
|
2336
2628
|
}
|
|
2337
2629
|
}
|
|
2338
2630
|
await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2339
|
-
console.log(
|
|
2631
|
+
console.log(chalk15.green("\nAll devices synced."));
|
|
2340
2632
|
} else {
|
|
2341
2633
|
const state = await loadState();
|
|
2342
2634
|
if (!state.lastFolderId) {
|
|
2343
|
-
console.error(
|
|
2635
|
+
console.error(chalk15.yellow("No recent sync. Use: md4ai sync --all, or md4ai scan <path> first."));
|
|
2344
2636
|
process.exit(1);
|
|
2345
2637
|
}
|
|
2346
2638
|
const { data: device } = await supabase.from("device_paths").select("folder_id, device_name, path").eq("folder_id", state.lastFolderId).eq("device_name", state.lastDeviceName).single();
|
|
2347
2639
|
if (!device) {
|
|
2348
|
-
console.error(
|
|
2640
|
+
console.error(chalk15.red("Could not find last synced device/folder."));
|
|
2349
2641
|
process.exit(1);
|
|
2350
2642
|
}
|
|
2351
2643
|
if (!isValidProjectPath(device.path)) {
|
|
2352
|
-
console.error(
|
|
2644
|
+
console.error(chalk15.red(`Invalid project path: ${device.path}`));
|
|
2353
2645
|
process.exit(1);
|
|
2354
2646
|
}
|
|
2355
|
-
console.log(
|
|
2647
|
+
console.log(chalk15.blue(`Syncing: ${device.path}`));
|
|
2356
2648
|
const { data: proposedSingle } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
|
|
2357
2649
|
if (proposedSingle?.length) {
|
|
2358
|
-
console.log(
|
|
2650
|
+
console.log(chalk15.yellow(` ${proposedSingle.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
|
|
2359
2651
|
}
|
|
2360
2652
|
const result = await scanProject(device.path);
|
|
2361
2653
|
await supabase.from("claude_folders").update({
|
|
@@ -2370,7 +2662,7 @@ async function syncCommand(options) {
|
|
|
2370
2662
|
await pushToolings(supabase, device.folder_id, result.toolings);
|
|
2371
2663
|
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
|
|
2372
2664
|
await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2373
|
-
console.log(
|
|
2665
|
+
console.log(chalk15.green("Synced."));
|
|
2374
2666
|
}
|
|
2375
2667
|
}
|
|
2376
2668
|
var init_sync = __esm({
|
|
@@ -2384,16 +2676,16 @@ var init_sync = __esm({
|
|
|
2384
2676
|
});
|
|
2385
2677
|
|
|
2386
2678
|
// dist/mcp/read-configs.js
|
|
2387
|
-
import { readFile as
|
|
2388
|
-
import { join as
|
|
2679
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
2680
|
+
import { join as join15 } from "node:path";
|
|
2389
2681
|
import { homedir as homedir9 } from "node:os";
|
|
2390
|
-
import { existsSync as
|
|
2682
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
2391
2683
|
import { readdir as readdir4 } from "node:fs/promises";
|
|
2392
2684
|
async function readJsonSafe(path) {
|
|
2393
2685
|
try {
|
|
2394
|
-
if (!
|
|
2686
|
+
if (!existsSync13(path))
|
|
2395
2687
|
return null;
|
|
2396
|
-
const raw = await
|
|
2688
|
+
const raw = await readFile12(path, "utf-8");
|
|
2397
2689
|
return JSON.parse(raw);
|
|
2398
2690
|
} catch {
|
|
2399
2691
|
return null;
|
|
@@ -2455,50 +2747,50 @@ function parseFlatConfig(data, source) {
|
|
|
2455
2747
|
async function readAllMcpConfigs() {
|
|
2456
2748
|
const home = homedir9();
|
|
2457
2749
|
const entries = [];
|
|
2458
|
-
const userConfig = await readJsonSafe(
|
|
2750
|
+
const userConfig = await readJsonSafe(join15(home, ".claude.json"));
|
|
2459
2751
|
entries.push(...parseServers(userConfig, "global"));
|
|
2460
|
-
const globalMcp = await readJsonSafe(
|
|
2752
|
+
const globalMcp = await readJsonSafe(join15(home, ".claude", "mcp.json"));
|
|
2461
2753
|
entries.push(...parseServers(globalMcp, "global"));
|
|
2462
|
-
const cwdMcp = await readJsonSafe(
|
|
2754
|
+
const cwdMcp = await readJsonSafe(join15(process.cwd(), ".mcp.json"));
|
|
2463
2755
|
entries.push(...parseServers(cwdMcp, "project"));
|
|
2464
|
-
const pluginsBase =
|
|
2465
|
-
if (
|
|
2756
|
+
const pluginsBase = join15(home, ".claude", "plugins", "marketplaces");
|
|
2757
|
+
if (existsSync13(pluginsBase)) {
|
|
2466
2758
|
try {
|
|
2467
2759
|
const marketplaces = await readdir4(pluginsBase, { withFileTypes: true });
|
|
2468
2760
|
for (const mp of marketplaces) {
|
|
2469
2761
|
if (!mp.isDirectory())
|
|
2470
2762
|
continue;
|
|
2471
|
-
const extDir =
|
|
2472
|
-
if (!
|
|
2763
|
+
const extDir = join15(pluginsBase, mp.name, "external_plugins");
|
|
2764
|
+
if (!existsSync13(extDir))
|
|
2473
2765
|
continue;
|
|
2474
2766
|
const plugins = await readdir4(extDir, { withFileTypes: true });
|
|
2475
2767
|
for (const plugin of plugins) {
|
|
2476
2768
|
if (!plugin.isDirectory())
|
|
2477
2769
|
continue;
|
|
2478
|
-
const pluginMcp = await readJsonSafe(
|
|
2770
|
+
const pluginMcp = await readJsonSafe(join15(extDir, plugin.name, ".mcp.json"));
|
|
2479
2771
|
entries.push(...parseServers(pluginMcp, "plugin"));
|
|
2480
2772
|
}
|
|
2481
2773
|
}
|
|
2482
2774
|
} catch {
|
|
2483
2775
|
}
|
|
2484
2776
|
}
|
|
2485
|
-
const cacheBase =
|
|
2486
|
-
if (
|
|
2777
|
+
const cacheBase = join15(home, ".claude", "plugins", "cache");
|
|
2778
|
+
if (existsSync13(cacheBase)) {
|
|
2487
2779
|
try {
|
|
2488
2780
|
const registries = await readdir4(cacheBase, { withFileTypes: true });
|
|
2489
2781
|
for (const reg of registries) {
|
|
2490
2782
|
if (!reg.isDirectory())
|
|
2491
2783
|
continue;
|
|
2492
|
-
const regDir =
|
|
2784
|
+
const regDir = join15(cacheBase, reg.name);
|
|
2493
2785
|
const plugins = await readdir4(regDir, { withFileTypes: true });
|
|
2494
2786
|
for (const plugin of plugins) {
|
|
2495
2787
|
if (!plugin.isDirectory())
|
|
2496
2788
|
continue;
|
|
2497
|
-
const versionDirs = await readdir4(
|
|
2789
|
+
const versionDirs = await readdir4(join15(regDir, plugin.name), { withFileTypes: true });
|
|
2498
2790
|
for (const ver of versionDirs) {
|
|
2499
2791
|
if (!ver.isDirectory())
|
|
2500
2792
|
continue;
|
|
2501
|
-
const mcpPath =
|
|
2793
|
+
const mcpPath = join15(regDir, plugin.name, ver.name, ".mcp.json");
|
|
2502
2794
|
const flatData = await readJsonSafe(mcpPath);
|
|
2503
2795
|
if (flatData) {
|
|
2504
2796
|
entries.push(...parseFlatConfig(flatData, "plugin"));
|
|
@@ -2648,7 +2940,7 @@ var mcp_watch_exports = {};
|
|
|
2648
2940
|
__export(mcp_watch_exports, {
|
|
2649
2941
|
mcpWatchCommand: () => mcpWatchCommand
|
|
2650
2942
|
});
|
|
2651
|
-
import
|
|
2943
|
+
import chalk21 from "chalk";
|
|
2652
2944
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2653
2945
|
import { createHash as createHash2 } from "node:crypto";
|
|
2654
2946
|
function detectTty() {
|
|
@@ -2802,20 +3094,20 @@ function buildRows(configs, httpResults, cdpStatus) {
|
|
|
2802
3094
|
}
|
|
2803
3095
|
function printTable(rows, deviceName, watcherPid, cdpResult) {
|
|
2804
3096
|
process.stdout.write("\x1B[3J\x1B[2J\x1B[H");
|
|
2805
|
-
console.log(
|
|
2806
|
-
MCP Monitor v${CURRENT_VERSION} \u2014 ${deviceName}`) +
|
|
2807
|
-
console.log(
|
|
3097
|
+
console.log(chalk21.bold.cyan(`
|
|
3098
|
+
MCP Monitor v${CURRENT_VERSION} \u2014 ${deviceName}`) + chalk21.dim(` (PID ${watcherPid})`));
|
|
3099
|
+
console.log(chalk21.dim(` ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 refreshes every 30s \xB7 Ctrl+C to stop
|
|
2808
3100
|
`));
|
|
2809
3101
|
if (cdpResult) {
|
|
2810
3102
|
if (cdpResult.status === "reachable") {
|
|
2811
3103
|
const browserInfo = cdpResult.browser ? ` (${cdpResult.browser})` : "";
|
|
2812
|
-
console.log(
|
|
2813
|
-
console.log(
|
|
3104
|
+
console.log(chalk21.green(" \u2714 Chrome browser connected") + chalk21.dim(browserInfo));
|
|
3105
|
+
console.log(chalk21.dim(" Claude Code can control Chrome via DevTools Protocol on localhost:9222"));
|
|
2814
3106
|
} else {
|
|
2815
|
-
console.log(
|
|
2816
|
-
console.log(
|
|
2817
|
-
console.log(
|
|
2818
|
-
console.log(
|
|
3107
|
+
console.log(chalk21.red(" \u2717 Chrome browser not connected"));
|
|
3108
|
+
console.log(chalk21.dim(" Claude Code cannot reach Chrome. To fix:"));
|
|
3109
|
+
console.log(chalk21.dim(" 1. Open Chrome with: google-chrome --remote-debugging-port=9222"));
|
|
3110
|
+
console.log(chalk21.dim(" 2. Or add --remote-debugging-port=9222 to your Chrome shortcut"));
|
|
2819
3111
|
}
|
|
2820
3112
|
console.log("");
|
|
2821
3113
|
}
|
|
@@ -2836,41 +3128,41 @@ function printTable(rows, deviceName, watcherPid, cdpResult) {
|
|
|
2836
3128
|
sessionNum++;
|
|
2837
3129
|
const totalMem = servers.reduce((sum, s) => sum + (s.memory_mb ?? 0), 0);
|
|
2838
3130
|
const label = byTty.size === 1 ? "Claude Code session" : `Claude Code session ${sessionNum}`;
|
|
2839
|
-
console.log(
|
|
3131
|
+
console.log(chalk21.green(` ${label}`) + chalk21.dim(` \u2014 ${servers.length} server${servers.length !== 1 ? "s" : ""} \xB7 ${totalMem} MB \xB7 terminal ${tty}`));
|
|
2840
3132
|
for (const s of servers) {
|
|
2841
3133
|
const uptime = formatUptime(s.uptime_seconds ?? 0);
|
|
2842
|
-
const source =
|
|
2843
|
-
console.log(` ${
|
|
3134
|
+
const source = chalk21.dim(`[${s.config_source}]`);
|
|
3135
|
+
console.log(` ${chalk21.green("\u25CF")} ${s.server_name.padEnd(20)} ${source} ${chalk21.dim((s.package_name ?? "").padEnd(25))} ${String(s.memory_mb ?? 0).padStart(4)} MB ${uptime}`);
|
|
2844
3136
|
}
|
|
2845
3137
|
console.log("");
|
|
2846
3138
|
}
|
|
2847
3139
|
if (byTty.size > 1) {
|
|
2848
|
-
console.log(
|
|
3140
|
+
console.log(chalk21.dim(" Each open Claude Code window runs its own set of MCP servers.\n"));
|
|
2849
3141
|
}
|
|
2850
3142
|
}
|
|
2851
3143
|
if (runningHttp.length > 0) {
|
|
2852
|
-
console.log(
|
|
3144
|
+
console.log(chalk21.blue(` Remote Services (${runningHttp.length})`) + chalk21.dim(" \u2014 HTTP endpoints reachable"));
|
|
2853
3145
|
for (const s of runningHttp) {
|
|
2854
|
-
console.log(` ${
|
|
3146
|
+
console.log(` ${chalk21.blue("\u25CF")} ${s.server_name.padEnd(20)} ${chalk21.dim((s.http_url ?? "").padEnd(30))}`);
|
|
2855
3147
|
}
|
|
2856
3148
|
console.log("");
|
|
2857
3149
|
}
|
|
2858
3150
|
if (stopped.length > 0 || errored.length > 0) {
|
|
2859
3151
|
const notRunning = [...stopped, ...errored];
|
|
2860
|
-
console.log(
|
|
3152
|
+
console.log(chalk21.yellow(` Not Running (${notRunning.length})`) + chalk21.dim(" \u2014 configured but no process detected"));
|
|
2861
3153
|
for (const s of notRunning) {
|
|
2862
|
-
const icon = s.status === "error" ?
|
|
2863
|
-
const source =
|
|
2864
|
-
const detail = s.error_detail ?
|
|
3154
|
+
const icon = s.status === "error" ? chalk21.red("\u2717") : chalk21.yellow("\u25CB");
|
|
3155
|
+
const source = chalk21.dim(`[${s.config_source}]`);
|
|
3156
|
+
const detail = s.error_detail ? chalk21.dim(` \u2014 ${s.error_detail}`) : "";
|
|
2865
3157
|
console.log(` ${icon} ${s.server_name.padEnd(20)} ${source}${detail}`);
|
|
2866
3158
|
}
|
|
2867
3159
|
console.log("");
|
|
2868
3160
|
}
|
|
2869
3161
|
if (rows.length === 0) {
|
|
2870
|
-
console.log(
|
|
2871
|
-
console.log(
|
|
3162
|
+
console.log(chalk21.yellow(" No MCP servers configured."));
|
|
3163
|
+
console.log(chalk21.dim(" Configure servers in ~/.claude/mcp.json or .mcp.json\n"));
|
|
2872
3164
|
}
|
|
2873
|
-
console.log(
|
|
3165
|
+
console.log(chalk21.bgYellow.black.bold(" \u26A0 DO NOT CLOSE THIS WINDOW \u2014 it feeds live data to the dashboard \u26A0 "));
|
|
2874
3166
|
console.log("");
|
|
2875
3167
|
}
|
|
2876
3168
|
function formatUptime(seconds) {
|
|
@@ -2899,7 +3191,7 @@ async function mcpWatchCommand() {
|
|
|
2899
3191
|
const { data: existingWatchers } = await supabase.from("mcp_watchers").select("pid, tty, cli_version, started_at").eq("device_id", deviceId);
|
|
2900
3192
|
if (existingWatchers && existingWatchers.length > 0) {
|
|
2901
3193
|
console.log("");
|
|
2902
|
-
console.log(
|
|
3194
|
+
console.log(chalk21.yellow(` Replacing ${existingWatchers.length} existing watcher${existingWatchers.length !== 1 ? "s" : ""} on this device...`));
|
|
2903
3195
|
for (const w of existingWatchers) {
|
|
2904
3196
|
try {
|
|
2905
3197
|
if (typeof w.pid === "number" && w.pid > 1 && w.pid !== process.pid) {
|
|
@@ -2910,15 +3202,15 @@ async function mcpWatchCommand() {
|
|
|
2910
3202
|
}
|
|
2911
3203
|
await supabase.from("mcp_watchers").delete().eq("device_id", deviceId);
|
|
2912
3204
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
2913
|
-
console.log(
|
|
3205
|
+
console.log(chalk21.dim(" Previous watcher stopped.\n"));
|
|
2914
3206
|
}
|
|
2915
3207
|
process.stdout.write(`\x1B]0;MCP mon\x07`);
|
|
2916
|
-
console.log(
|
|
3208
|
+
console.log(chalk21.blue(`Starting MCP monitor for ${deviceName}...`));
|
|
2917
3209
|
console.log("");
|
|
2918
|
-
console.log(
|
|
2919
|
-
console.log(
|
|
3210
|
+
console.log(chalk21.dim(" View this in the online dashboard at:"));
|
|
3211
|
+
console.log(chalk21.cyan(` https://www.md4ai.com/device/${deviceId}`));
|
|
2920
3212
|
console.log("");
|
|
2921
|
-
console.log(
|
|
3213
|
+
console.log(chalk21.yellow(" Please note, closing this window will stop ALL watch reports."));
|
|
2922
3214
|
console.log("");
|
|
2923
3215
|
await supabase.from("mcp_watchers").upsert({
|
|
2924
3216
|
device_id: deviceId,
|
|
@@ -2948,7 +3240,7 @@ async function mcpWatchCommand() {
|
|
|
2948
3240
|
return;
|
|
2949
3241
|
}
|
|
2950
3242
|
if (deleteError) {
|
|
2951
|
-
console.error(
|
|
3243
|
+
console.error(chalk21.red(` [debug] Failed to delete old status: ${deleteError.message}`));
|
|
2952
3244
|
}
|
|
2953
3245
|
if (rows.length > 0) {
|
|
2954
3246
|
const { error: insertError } = await supabase.from("mcp_server_status").insert(rows.map((row) => ({
|
|
@@ -2961,7 +3253,7 @@ async function mcpWatchCommand() {
|
|
|
2961
3253
|
return;
|
|
2962
3254
|
}
|
|
2963
3255
|
if (insertError) {
|
|
2964
|
-
console.error(
|
|
3256
|
+
console.error(chalk21.red(` [debug] Failed to write MCP status: ${insertError.message}`));
|
|
2965
3257
|
}
|
|
2966
3258
|
}
|
|
2967
3259
|
const { error: heartbeatError } = await supabase.from("mcp_watchers").update({ last_heartbeat: now }).eq("device_id", deviceId).eq("pid", myPid);
|
|
@@ -2979,11 +3271,11 @@ async function mcpWatchCommand() {
|
|
|
2979
3271
|
return;
|
|
2980
3272
|
}
|
|
2981
3273
|
jwtRefreshAttempted = true;
|
|
2982
|
-
console.log(
|
|
3274
|
+
console.log(chalk21.yellow("\n Session token expired \u2014 attempting to refresh..."));
|
|
2983
3275
|
const refreshed = await refreshSession();
|
|
2984
3276
|
if (refreshed) {
|
|
2985
3277
|
supabase = refreshed.supabase;
|
|
2986
|
-
console.log(
|
|
3278
|
+
console.log(chalk21.green(" Session refreshed successfully. Resuming monitoring.\n"));
|
|
2987
3279
|
jwtRefreshAttempted = false;
|
|
2988
3280
|
await supabase.from("mcp_watchers").upsert({
|
|
2989
3281
|
device_id: deviceId,
|
|
@@ -3001,14 +3293,14 @@ async function mcpWatchCommand() {
|
|
|
3001
3293
|
function printSessionExpired() {
|
|
3002
3294
|
process.stdout.write("\x1B[3J\x1B[2J\x1B[H");
|
|
3003
3295
|
console.log("");
|
|
3004
|
-
console.log(
|
|
3296
|
+
console.log(chalk21.bgRed.white.bold(" \u2717 SESSION EXPIRED \u2014 this watcher is no longer sending data to the dashboard \u2717 "));
|
|
3005
3297
|
console.log("");
|
|
3006
|
-
console.log(
|
|
3007
|
-
console.log(
|
|
3298
|
+
console.log(chalk21.yellow(" Your authentication token has expired and could not be refreshed."));
|
|
3299
|
+
console.log(chalk21.yellow(' The dashboard will show "No watchers" because this process can no longer update it.'));
|
|
3008
3300
|
console.log("");
|
|
3009
|
-
console.log(
|
|
3010
|
-
console.log(
|
|
3011
|
-
console.log(
|
|
3301
|
+
console.log(chalk21.white(" To fix this:"));
|
|
3302
|
+
console.log(chalk21.cyan(" 1. Run: md4ai login"));
|
|
3303
|
+
console.log(chalk21.cyan(" 2. Then restart the watcher: md4ai mcp-watch"));
|
|
3012
3304
|
console.log("");
|
|
3013
3305
|
}
|
|
3014
3306
|
let interval;
|
|
@@ -3051,7 +3343,7 @@ async function mcpWatchCommand() {
|
|
|
3051
3343
|
clearInterval(interval);
|
|
3052
3344
|
clearInterval(envInterval);
|
|
3053
3345
|
await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).eq("pid", myPid);
|
|
3054
|
-
console.log(
|
|
3346
|
+
console.log(chalk21.dim("\nMCP monitor stopped."));
|
|
3055
3347
|
process.exit(0);
|
|
3056
3348
|
};
|
|
3057
3349
|
process.on("SIGINT", () => {
|
|
@@ -3359,71 +3651,71 @@ init_map();
|
|
|
3359
3651
|
|
|
3360
3652
|
// dist/commands/simulate.js
|
|
3361
3653
|
init_dist();
|
|
3362
|
-
import { join as
|
|
3363
|
-
import { existsSync as
|
|
3654
|
+
import { join as join13 } from "node:path";
|
|
3655
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
3364
3656
|
import { homedir as homedir8 } from "node:os";
|
|
3365
|
-
import
|
|
3657
|
+
import chalk13 from "chalk";
|
|
3366
3658
|
async function simulateCommand(prompt) {
|
|
3367
3659
|
const projectRoot = process.cwd();
|
|
3368
|
-
console.log(
|
|
3660
|
+
console.log(chalk13.blue(`Simulating prompt: "${prompt}"
|
|
3369
3661
|
`));
|
|
3370
|
-
console.log(
|
|
3371
|
-
const globalClaude =
|
|
3372
|
-
if (
|
|
3373
|
-
console.log(
|
|
3662
|
+
console.log(chalk13.dim("Files Claude would load:\n"));
|
|
3663
|
+
const globalClaude = join13(homedir8(), ".claude", "CLAUDE.md");
|
|
3664
|
+
if (existsSync9(globalClaude)) {
|
|
3665
|
+
console.log(chalk13.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
|
|
3374
3666
|
}
|
|
3375
3667
|
for (const rootFile of ROOT_FILES) {
|
|
3376
|
-
const fullPath =
|
|
3377
|
-
if (
|
|
3378
|
-
console.log(
|
|
3668
|
+
const fullPath = join13(projectRoot, rootFile);
|
|
3669
|
+
if (existsSync9(fullPath)) {
|
|
3670
|
+
console.log(chalk13.green(` \u2713 ${rootFile} (project root)`));
|
|
3379
3671
|
}
|
|
3380
3672
|
}
|
|
3381
3673
|
const settingsFiles = [
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3674
|
+
join13(homedir8(), ".claude", "settings.json"),
|
|
3675
|
+
join13(homedir8(), ".claude", "settings.local.json"),
|
|
3676
|
+
join13(projectRoot, ".claude", "settings.json"),
|
|
3677
|
+
join13(projectRoot, ".claude", "settings.local.json")
|
|
3386
3678
|
];
|
|
3387
3679
|
for (const sf of settingsFiles) {
|
|
3388
|
-
if (
|
|
3680
|
+
if (existsSync9(sf)) {
|
|
3389
3681
|
const display = sf.startsWith(homedir8()) ? sf.replace(homedir8(), "~") : sf.replace(projectRoot + "/", "");
|
|
3390
|
-
console.log(
|
|
3682
|
+
console.log(chalk13.green(` \u2713 ${display} (settings)`));
|
|
3391
3683
|
}
|
|
3392
3684
|
}
|
|
3393
3685
|
const words = prompt.split(/\s+/);
|
|
3394
3686
|
for (const word of words) {
|
|
3395
3687
|
const cleaned = word.replace(/['"]/g, "");
|
|
3396
|
-
const candidatePath =
|
|
3397
|
-
if (
|
|
3398
|
-
console.log(
|
|
3688
|
+
const candidatePath = join13(projectRoot, cleaned);
|
|
3689
|
+
if (existsSync9(candidatePath) && cleaned.includes("/")) {
|
|
3690
|
+
console.log(chalk13.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
|
|
3399
3691
|
}
|
|
3400
3692
|
}
|
|
3401
|
-
console.log(
|
|
3693
|
+
console.log(chalk13.dim("\nNote: This is an approximation. Actual file loading depends on Claude Code internals."));
|
|
3402
3694
|
}
|
|
3403
3695
|
|
|
3404
3696
|
// dist/commands/print.js
|
|
3405
|
-
import { join as
|
|
3406
|
-
import { readFile as
|
|
3407
|
-
import { existsSync as
|
|
3408
|
-
import
|
|
3697
|
+
import { join as join14 } from "node:path";
|
|
3698
|
+
import { readFile as readFile10, writeFile as writeFile3 } from "node:fs/promises";
|
|
3699
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
3700
|
+
import chalk14 from "chalk";
|
|
3409
3701
|
async function printCommand(title) {
|
|
3410
3702
|
const projectRoot = process.cwd();
|
|
3411
|
-
const scanDataPath =
|
|
3412
|
-
if (!
|
|
3413
|
-
console.error(
|
|
3703
|
+
const scanDataPath = join14(projectRoot, "output", "index.html");
|
|
3704
|
+
if (!existsSync10(scanDataPath)) {
|
|
3705
|
+
console.error(chalk14.red("No scan data found. Run: md4ai scan"));
|
|
3414
3706
|
process.exit(1);
|
|
3415
3707
|
}
|
|
3416
|
-
const html = await
|
|
3708
|
+
const html = await readFile10(scanDataPath, "utf-8");
|
|
3417
3709
|
const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
|
|
3418
3710
|
if (!match) {
|
|
3419
|
-
console.error(
|
|
3711
|
+
console.error(chalk14.red("Could not extract scan data from output/index.html"));
|
|
3420
3712
|
process.exit(1);
|
|
3421
3713
|
}
|
|
3422
3714
|
const result = JSON.parse(match[1]);
|
|
3423
3715
|
const printHtml = generatePrintHtml(result, title);
|
|
3424
|
-
const outputPath =
|
|
3716
|
+
const outputPath = join14(projectRoot, "output", `print-${Date.now()}.html`);
|
|
3425
3717
|
await writeFile3(outputPath, printHtml, "utf-8");
|
|
3426
|
-
console.log(
|
|
3718
|
+
console.log(chalk14.green(`Print-ready wall sheet: ${outputPath}`));
|
|
3427
3719
|
}
|
|
3428
3720
|
function escapeHtml2(text) {
|
|
3429
3721
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -3489,7 +3781,7 @@ init_scanner();
|
|
|
3489
3781
|
init_push_toolings();
|
|
3490
3782
|
init_device_utils();
|
|
3491
3783
|
import { resolve as resolve6 } from "node:path";
|
|
3492
|
-
import
|
|
3784
|
+
import chalk16 from "chalk";
|
|
3493
3785
|
async function linkCommand(projectId) {
|
|
3494
3786
|
const { supabase, userId } = await getAuthenticatedClient();
|
|
3495
3787
|
const cwd = resolve6(process.cwd());
|
|
@@ -3497,11 +3789,11 @@ async function linkCommand(projectId) {
|
|
|
3497
3789
|
const osType = detectOs2();
|
|
3498
3790
|
const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
|
|
3499
3791
|
if (folderErr || !folder) {
|
|
3500
|
-
console.error(
|
|
3501
|
-
console.error(
|
|
3792
|
+
console.error(chalk16.red("Project not found, or you do not have access."));
|
|
3793
|
+
console.error(chalk16.yellow("Check the project ID in the MD4AI web dashboard."));
|
|
3502
3794
|
process.exit(1);
|
|
3503
3795
|
}
|
|
3504
|
-
console.log(
|
|
3796
|
+
console.log(chalk16.blue(`
|
|
3505
3797
|
Linking "${folder.name}" to this device...
|
|
3506
3798
|
`));
|
|
3507
3799
|
console.log(` Project: ${folder.name}`);
|
|
@@ -3525,19 +3817,20 @@ Linking "${folder.name}" to this device...
|
|
|
3525
3817
|
path: cwd
|
|
3526
3818
|
});
|
|
3527
3819
|
if (pathErr) {
|
|
3528
|
-
console.error(
|
|
3820
|
+
console.error(chalk16.red(`Failed to link: ${pathErr.message}`));
|
|
3529
3821
|
process.exit(1);
|
|
3530
3822
|
}
|
|
3531
3823
|
}
|
|
3532
|
-
console.log(
|
|
3533
|
-
console.log(
|
|
3534
|
-
const result = await scanProject(cwd);
|
|
3824
|
+
console.log(chalk16.green("\nLinked successfully."));
|
|
3825
|
+
console.log(chalk16.blue("\nRunning initial scan...\n"));
|
|
3826
|
+
const result = await scanProject(cwd, folder.id);
|
|
3535
3827
|
console.log(` Files: ${result.graph.nodes.length}`);
|
|
3536
3828
|
console.log(` References:${result.graph.edges.length}`);
|
|
3537
3829
|
console.log(` Orphans: ${result.orphans.length}`);
|
|
3538
3830
|
console.log(` Skills: ${result.skills.length}`);
|
|
3539
3831
|
console.log(` Toolings: ${result.toolings.length}`);
|
|
3540
3832
|
console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
|
|
3833
|
+
console.log(` Doppler: ${result.doppler ? `${result.doppler.configs.length} config(s) \u2014 ${result.doppler.project}` : "see above"}`);
|
|
3541
3834
|
console.log(` Plugins: ${result.marketplacePlugins.length} (${result.marketplacePlugins.reduce((n, p) => n + p.skills.length, 0)} skills)`);
|
|
3542
3835
|
const { error: scanErr } = await supabase.from("claude_folders").update({
|
|
3543
3836
|
graph_json: result.graph,
|
|
@@ -3546,12 +3839,13 @@ Linking "${folder.name}" to this device...
|
|
|
3546
3839
|
skills_table_json: result.skills,
|
|
3547
3840
|
stale_files_json: result.staleFiles,
|
|
3548
3841
|
env_manifest_json: result.envManifest,
|
|
3842
|
+
doppler_json: result.doppler,
|
|
3549
3843
|
marketplace_plugins_json: result.marketplacePlugins,
|
|
3550
3844
|
last_scanned: result.scannedAt,
|
|
3551
3845
|
data_hash: result.dataHash
|
|
3552
3846
|
}).eq("id", folder.id);
|
|
3553
3847
|
if (scanErr) {
|
|
3554
|
-
console.error(
|
|
3848
|
+
console.error(chalk16.yellow(`Scan upload warning: ${scanErr.message}`));
|
|
3555
3849
|
}
|
|
3556
3850
|
await pushToolings(supabase, folder.id, result.toolings);
|
|
3557
3851
|
const graphPaths = result.graph.nodes.map((n) => n.filePath);
|
|
@@ -3566,7 +3860,7 @@ Linking "${folder.name}" to this device...
|
|
|
3566
3860
|
last_modified: file.lastModified
|
|
3567
3861
|
}, { onConflict: "folder_id,file_path" });
|
|
3568
3862
|
}
|
|
3569
|
-
console.log(
|
|
3863
|
+
console.log(chalk16.green(` Uploaded ${configFiles.length} config file(s).`));
|
|
3570
3864
|
}
|
|
3571
3865
|
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder.id).eq("device_name", deviceName);
|
|
3572
3866
|
await saveState({
|
|
@@ -3575,40 +3869,40 @@ Linking "${folder.name}" to this device...
|
|
|
3575
3869
|
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3576
3870
|
});
|
|
3577
3871
|
const projectUrl = `https://www.md4ai.com/project/${folder.id}`;
|
|
3578
|
-
console.log(
|
|
3579
|
-
console.log(
|
|
3872
|
+
console.log(chalk16.green("\nDone! Project linked and scanned."));
|
|
3873
|
+
console.log(chalk16.cyan(`
|
|
3580
3874
|
${projectUrl}
|
|
3581
3875
|
`));
|
|
3582
|
-
console.log(
|
|
3876
|
+
console.log(chalk16.grey('Run "md4ai scan" to rescan at any time.'));
|
|
3583
3877
|
}
|
|
3584
3878
|
|
|
3585
3879
|
// dist/commands/import-bundle.js
|
|
3586
|
-
import { readFile as
|
|
3880
|
+
import { readFile as readFile11, writeFile as writeFile4, mkdir as mkdir3, stat as fsStat } from "node:fs/promises";
|
|
3587
3881
|
import { dirname as dirname3, resolve as resolve7 } from "node:path";
|
|
3588
|
-
import { existsSync as
|
|
3589
|
-
import
|
|
3882
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
3883
|
+
import chalk17 from "chalk";
|
|
3590
3884
|
import { confirm as confirm3, input as input6 } from "@inquirer/prompts";
|
|
3591
3885
|
var MAX_BUNDLE_SIZE_BYTES = 50 * 1024 * 1024;
|
|
3592
3886
|
async function importBundleCommand(zipPath) {
|
|
3593
|
-
if (!
|
|
3594
|
-
console.error(
|
|
3887
|
+
if (!existsSync12(zipPath)) {
|
|
3888
|
+
console.error(chalk17.red(`File not found: ${zipPath}`));
|
|
3595
3889
|
process.exit(1);
|
|
3596
3890
|
}
|
|
3597
3891
|
const fileStat = await fsStat(zipPath);
|
|
3598
3892
|
if (fileStat.size > MAX_BUNDLE_SIZE_BYTES) {
|
|
3599
|
-
console.error(
|
|
3893
|
+
console.error(chalk17.red(`Bundle too large (${Math.round(fileStat.size / 1024 / 1024)} MB). Maximum is 50 MB.`));
|
|
3600
3894
|
process.exit(1);
|
|
3601
3895
|
}
|
|
3602
3896
|
const JSZip = (await import("jszip")).default;
|
|
3603
|
-
const zipData = await
|
|
3897
|
+
const zipData = await readFile11(zipPath);
|
|
3604
3898
|
const zip = await JSZip.loadAsync(zipData);
|
|
3605
3899
|
const manifestFile = zip.file("manifest.json");
|
|
3606
3900
|
if (!manifestFile) {
|
|
3607
|
-
console.error(
|
|
3901
|
+
console.error(chalk17.red("Invalid bundle: missing manifest.json"));
|
|
3608
3902
|
process.exit(1);
|
|
3609
3903
|
}
|
|
3610
3904
|
const manifest = JSON.parse(await manifestFile.async("string"));
|
|
3611
|
-
console.log(
|
|
3905
|
+
console.log(chalk17.blue(`
|
|
3612
3906
|
Bundle: ${manifest.folderName}`));
|
|
3613
3907
|
console.log(` Exported by: ${manifest.ownerEmail}`);
|
|
3614
3908
|
console.log(` Exported at: ${manifest.exportedAt}`);
|
|
@@ -3622,10 +3916,10 @@ Bundle: ${manifest.folderName}`));
|
|
|
3622
3916
|
}
|
|
3623
3917
|
}
|
|
3624
3918
|
if (files.length === 0) {
|
|
3625
|
-
console.log(
|
|
3919
|
+
console.log(chalk17.yellow("No Claude config files found in bundle."));
|
|
3626
3920
|
return;
|
|
3627
3921
|
}
|
|
3628
|
-
console.log(
|
|
3922
|
+
console.log(chalk17.blue(`
|
|
3629
3923
|
Files to extract:`));
|
|
3630
3924
|
for (const f of files) {
|
|
3631
3925
|
console.log(` ${f.filePath}`);
|
|
@@ -3638,39 +3932,39 @@ Files to extract:`));
|
|
|
3638
3932
|
message: `Extract ${files.length} file(s) to ${targetDir}?`
|
|
3639
3933
|
});
|
|
3640
3934
|
if (!proceed) {
|
|
3641
|
-
console.log(
|
|
3935
|
+
console.log(chalk17.yellow("Cancelled."));
|
|
3642
3936
|
return;
|
|
3643
3937
|
}
|
|
3644
3938
|
const resolvedTarget = resolve7(targetDir);
|
|
3645
3939
|
for (const file of files) {
|
|
3646
3940
|
const fullPath = resolve7(targetDir, file.filePath);
|
|
3647
3941
|
if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
|
|
3648
|
-
console.error(
|
|
3942
|
+
console.error(chalk17.red(` Blocked path traversal: ${file.filePath}`));
|
|
3649
3943
|
continue;
|
|
3650
3944
|
}
|
|
3651
3945
|
const dir = dirname3(fullPath);
|
|
3652
|
-
if (!
|
|
3946
|
+
if (!existsSync12(dir)) {
|
|
3653
3947
|
await mkdir3(dir, { recursive: true });
|
|
3654
3948
|
}
|
|
3655
3949
|
await writeFile4(fullPath, file.content, "utf-8");
|
|
3656
|
-
console.log(
|
|
3950
|
+
console.log(chalk17.green(` \u2713 ${file.filePath}`));
|
|
3657
3951
|
}
|
|
3658
|
-
console.log(
|
|
3952
|
+
console.log(chalk17.green(`
|
|
3659
3953
|
Done! ${files.length} file(s) extracted to ${targetDir}`));
|
|
3660
3954
|
}
|
|
3661
3955
|
|
|
3662
3956
|
// dist/commands/admin-update-tool.js
|
|
3663
3957
|
init_auth();
|
|
3664
|
-
import
|
|
3958
|
+
import chalk18 from "chalk";
|
|
3665
3959
|
var VALID_CATEGORIES = ["framework", "runtime", "cli", "mcp", "package", "database", "other"];
|
|
3666
3960
|
async function adminUpdateToolCommand(options) {
|
|
3667
3961
|
const { supabase } = await getAuthenticatedClient();
|
|
3668
3962
|
if (!options.name) {
|
|
3669
|
-
console.error(
|
|
3963
|
+
console.error(chalk18.red("--name is required."));
|
|
3670
3964
|
process.exit(1);
|
|
3671
3965
|
}
|
|
3672
3966
|
if (options.category && !VALID_CATEGORIES.includes(options.category)) {
|
|
3673
|
-
console.error(
|
|
3967
|
+
console.error(chalk18.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
|
|
3674
3968
|
process.exit(1);
|
|
3675
3969
|
}
|
|
3676
3970
|
const { data: existing } = await supabase.from("tools_registry").select("id, name, display_name, category").eq("name", options.name).maybeSingle();
|
|
@@ -3692,13 +3986,13 @@ async function adminUpdateToolCommand(options) {
|
|
|
3692
3986
|
updates.notes = options.notes;
|
|
3693
3987
|
const { error } = await supabase.from("tools_registry").update(updates).eq("id", existing.id);
|
|
3694
3988
|
if (error) {
|
|
3695
|
-
console.error(
|
|
3989
|
+
console.error(chalk18.red(`Failed to update: ${error.message}`));
|
|
3696
3990
|
process.exit(1);
|
|
3697
3991
|
}
|
|
3698
|
-
console.log(
|
|
3992
|
+
console.log(chalk18.green(`Updated "${existing.display_name}" in the registry.`));
|
|
3699
3993
|
} else {
|
|
3700
3994
|
if (!options.display || !options.category) {
|
|
3701
|
-
console.error(
|
|
3995
|
+
console.error(chalk18.red("New tools require --display and --category."));
|
|
3702
3996
|
process.exit(1);
|
|
3703
3997
|
}
|
|
3704
3998
|
const { error } = await supabase.from("tools_registry").insert({
|
|
@@ -3712,25 +4006,25 @@ async function adminUpdateToolCommand(options) {
|
|
|
3712
4006
|
notes: options.notes ?? null
|
|
3713
4007
|
});
|
|
3714
4008
|
if (error) {
|
|
3715
|
-
console.error(
|
|
4009
|
+
console.error(chalk18.red(`Failed to create: ${error.message}`));
|
|
3716
4010
|
process.exit(1);
|
|
3717
4011
|
}
|
|
3718
|
-
console.log(
|
|
4012
|
+
console.log(chalk18.green(`Added "${options.display}" to the registry.`));
|
|
3719
4013
|
}
|
|
3720
4014
|
}
|
|
3721
4015
|
|
|
3722
4016
|
// dist/commands/admin-list-tools.js
|
|
3723
4017
|
init_auth();
|
|
3724
|
-
import
|
|
4018
|
+
import chalk19 from "chalk";
|
|
3725
4019
|
async function adminListToolsCommand() {
|
|
3726
4020
|
const { supabase } = await getAuthenticatedClient();
|
|
3727
4021
|
const { data: tools, error } = await supabase.from("tools_registry").select("*").order("category").order("display_name");
|
|
3728
4022
|
if (error) {
|
|
3729
|
-
console.error(
|
|
4023
|
+
console.error(chalk19.red(`Failed to fetch tools: ${error.message}`));
|
|
3730
4024
|
process.exit(1);
|
|
3731
4025
|
}
|
|
3732
4026
|
if (!tools?.length) {
|
|
3733
|
-
console.log(
|
|
4027
|
+
console.log(chalk19.yellow("No tools in the registry."));
|
|
3734
4028
|
return;
|
|
3735
4029
|
}
|
|
3736
4030
|
const nameW = Math.max(16, ...tools.map((t) => t.display_name.length));
|
|
@@ -3744,7 +4038,7 @@ async function adminListToolsCommand() {
|
|
|
3744
4038
|
"Beta".padEnd(betaW),
|
|
3745
4039
|
"Last Checked"
|
|
3746
4040
|
].join(" ");
|
|
3747
|
-
console.log(
|
|
4041
|
+
console.log(chalk19.bold(header));
|
|
3748
4042
|
console.log("\u2500".repeat(header.length));
|
|
3749
4043
|
for (const tool of tools) {
|
|
3750
4044
|
const lastChecked = tool.updated_at ? formatRelative(new Date(tool.updated_at)) : "\u2014";
|
|
@@ -3757,7 +4051,7 @@ async function adminListToolsCommand() {
|
|
|
3757
4051
|
].join(" ");
|
|
3758
4052
|
console.log(row);
|
|
3759
4053
|
}
|
|
3760
|
-
console.log(
|
|
4054
|
+
console.log(chalk19.grey(`
|
|
3761
4055
|
${tools.length} tool(s) in registry.`));
|
|
3762
4056
|
}
|
|
3763
4057
|
function formatRelative(date) {
|
|
@@ -3775,7 +4069,7 @@ function formatRelative(date) {
|
|
|
3775
4069
|
|
|
3776
4070
|
// dist/commands/admin-fetch-versions.js
|
|
3777
4071
|
init_auth();
|
|
3778
|
-
import
|
|
4072
|
+
import chalk20 from "chalk";
|
|
3779
4073
|
|
|
3780
4074
|
// dist/commands/version-sources.js
|
|
3781
4075
|
var VERSION_SOURCES = {
|
|
@@ -3804,11 +4098,11 @@ async function adminFetchVersionsCommand() {
|
|
|
3804
4098
|
const { supabase } = await getAuthenticatedClient();
|
|
3805
4099
|
const { data: tools, error } = await supabase.from("tools_registry").select("*").order("display_name");
|
|
3806
4100
|
if (error) {
|
|
3807
|
-
console.error(
|
|
4101
|
+
console.error(chalk20.red(`Failed to fetch tools: ${error.message}`));
|
|
3808
4102
|
process.exit(1);
|
|
3809
4103
|
}
|
|
3810
4104
|
if (!tools?.length) {
|
|
3811
|
-
console.log(
|
|
4105
|
+
console.log(chalk20.yellow("No tools in the registry."));
|
|
3812
4106
|
}
|
|
3813
4107
|
const { data: allProjectToolings } = await supabase.from("project_toolings").select("tool_name, detection_source").is("tool_id", null);
|
|
3814
4108
|
const registeredNames = new Set((tools ?? []).map((t) => t.name));
|
|
@@ -3819,7 +4113,7 @@ async function adminFetchVersionsCommand() {
|
|
|
3819
4113
|
}
|
|
3820
4114
|
}
|
|
3821
4115
|
if (unregisteredNames.size > 0) {
|
|
3822
|
-
console.log(
|
|
4116
|
+
console.log(chalk20.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
|
|
3823
4117
|
`));
|
|
3824
4118
|
for (const name of unregisteredNames) {
|
|
3825
4119
|
const displayName = name;
|
|
@@ -3837,10 +4131,10 @@ async function adminFetchVersionsCommand() {
|
|
|
3837
4131
|
}
|
|
3838
4132
|
}
|
|
3839
4133
|
if (!tools?.length) {
|
|
3840
|
-
console.log(
|
|
4134
|
+
console.log(chalk20.yellow("No tools to fetch."));
|
|
3841
4135
|
return;
|
|
3842
4136
|
}
|
|
3843
|
-
console.log(
|
|
4137
|
+
console.log(chalk20.bold(`Fetching versions for ${tools.length} tool(s)...
|
|
3844
4138
|
`));
|
|
3845
4139
|
const results = await Promise.all(tools.map(async (tool) => {
|
|
3846
4140
|
const source = VERSION_SOURCES[tool.name] ?? { type: "npm", package: tool.name };
|
|
@@ -3879,10 +4173,10 @@ async function adminFetchVersionsCommand() {
|
|
|
3879
4173
|
"Beta".padEnd(betaW),
|
|
3880
4174
|
"Status".padEnd(statusW)
|
|
3881
4175
|
].join(" ");
|
|
3882
|
-
console.log(
|
|
4176
|
+
console.log(chalk20.bold(header));
|
|
3883
4177
|
console.log("\u2500".repeat(header.length));
|
|
3884
4178
|
for (const result of results) {
|
|
3885
|
-
const statusColour = result.status === "updated" ?
|
|
4179
|
+
const statusColour = result.status === "updated" ? chalk20.green : result.status === "unchanged" ? chalk20.grey : result.status === "failed" ? chalk20.red : chalk20.yellow;
|
|
3886
4180
|
const row = [
|
|
3887
4181
|
result.displayName.padEnd(nameW),
|
|
3888
4182
|
(result.stable ?? "\u2014").padEnd(stableW),
|
|
@@ -3895,8 +4189,8 @@ async function adminFetchVersionsCommand() {
|
|
|
3895
4189
|
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
3896
4190
|
const failed = results.filter((r) => r.status === "failed").length;
|
|
3897
4191
|
const noSource = results.filter((r) => r.status === "no source").length;
|
|
3898
|
-
console.log(
|
|
3899
|
-
${results.length} tool(s): `) +
|
|
4192
|
+
console.log(chalk20.grey(`
|
|
4193
|
+
${results.length} tool(s): `) + chalk20.green(`${updated} updated`) + ", " + chalk20.grey(`${unchanged} unchanged`) + ", " + chalk20.red(`${failed} failed`) + ", " + chalk20.yellow(`${noSource} no source`));
|
|
3900
4194
|
}
|
|
3901
4195
|
async function fetchVersions(source) {
|
|
3902
4196
|
const controller = new AbortController();
|
|
@@ -3954,10 +4248,10 @@ async function fetchGitHubVersions(repo, signal) {
|
|
|
3954
4248
|
init_mcp_watch();
|
|
3955
4249
|
|
|
3956
4250
|
// dist/commands/init-manifest.js
|
|
3957
|
-
import { resolve as resolve8, join as
|
|
3958
|
-
import { readFile as
|
|
3959
|
-
import { existsSync as
|
|
3960
|
-
import
|
|
4251
|
+
import { resolve as resolve8, join as join16, relative as relative4, dirname as dirname4 } from "node:path";
|
|
4252
|
+
import { readFile as readFile13, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
|
|
4253
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
4254
|
+
import chalk22 from "chalk";
|
|
3961
4255
|
var SECRET_PATTERNS = [
|
|
3962
4256
|
/_KEY$/i,
|
|
3963
4257
|
/_SECRET$/i,
|
|
@@ -3973,8 +4267,8 @@ function isLikelySecret(name) {
|
|
|
3973
4267
|
async function discoverEnvFiles(projectRoot) {
|
|
3974
4268
|
const apps = [];
|
|
3975
4269
|
for (const envName of [".env", ".env.local", ".env.example"]) {
|
|
3976
|
-
const envPath =
|
|
3977
|
-
if (
|
|
4270
|
+
const envPath = join16(projectRoot, envName);
|
|
4271
|
+
if (existsSync14(envPath)) {
|
|
3978
4272
|
const vars = await extractVarNames(envPath);
|
|
3979
4273
|
if (vars.length > 0) {
|
|
3980
4274
|
apps.push({ name: "root", envFilePath: envName, vars });
|
|
@@ -3984,12 +4278,12 @@ async function discoverEnvFiles(projectRoot) {
|
|
|
3984
4278
|
}
|
|
3985
4279
|
const subdirs = ["web", "cli", "api", "app", "server", "packages"];
|
|
3986
4280
|
for (const sub of subdirs) {
|
|
3987
|
-
const subDir =
|
|
3988
|
-
if (!
|
|
4281
|
+
const subDir = join16(projectRoot, sub);
|
|
4282
|
+
if (!existsSync14(subDir))
|
|
3989
4283
|
continue;
|
|
3990
4284
|
for (const envName of [".env.local", ".env", ".env.example"]) {
|
|
3991
|
-
const envPath =
|
|
3992
|
-
if (
|
|
4285
|
+
const envPath = join16(subDir, envName);
|
|
4286
|
+
if (existsSync14(envPath)) {
|
|
3993
4287
|
const vars = await extractVarNames(envPath);
|
|
3994
4288
|
if (vars.length > 0) {
|
|
3995
4289
|
apps.push({
|
|
@@ -4005,7 +4299,7 @@ async function discoverEnvFiles(projectRoot) {
|
|
|
4005
4299
|
return apps;
|
|
4006
4300
|
}
|
|
4007
4301
|
async function extractVarNames(envPath) {
|
|
4008
|
-
const content = await
|
|
4302
|
+
const content = await readFile13(envPath, "utf-8");
|
|
4009
4303
|
const vars = [];
|
|
4010
4304
|
for (const line of content.split("\n")) {
|
|
4011
4305
|
const trimmed = line.trim();
|
|
@@ -4050,38 +4344,38 @@ function generateManifest(apps) {
|
|
|
4050
4344
|
}
|
|
4051
4345
|
async function initManifestCommand() {
|
|
4052
4346
|
const projectRoot = resolve8(process.cwd());
|
|
4053
|
-
const manifestPath =
|
|
4054
|
-
if (
|
|
4055
|
-
console.log(
|
|
4056
|
-
console.log(
|
|
4347
|
+
const manifestPath = join16(projectRoot, "docs", "reference", "env-manifest.md");
|
|
4348
|
+
if (existsSync14(manifestPath)) {
|
|
4349
|
+
console.log(chalk22.yellow(`Manifest already exists: ${relative4(projectRoot, manifestPath)}`));
|
|
4350
|
+
console.log(chalk22.dim("Edit it directly to make changes."));
|
|
4057
4351
|
return;
|
|
4058
4352
|
}
|
|
4059
|
-
console.log(
|
|
4353
|
+
console.log(chalk22.blue("Scanning for .env files...\n"));
|
|
4060
4354
|
const apps = await discoverEnvFiles(projectRoot);
|
|
4061
4355
|
if (apps.length === 0) {
|
|
4062
|
-
console.log(
|
|
4063
|
-
console.log(
|
|
4356
|
+
console.log(chalk22.yellow("No .env files found. Create a manifest manually at:"));
|
|
4357
|
+
console.log(chalk22.cyan(` docs/reference/env-manifest.md`));
|
|
4064
4358
|
return;
|
|
4065
4359
|
}
|
|
4066
4360
|
for (const app of apps) {
|
|
4067
|
-
console.log(` ${
|
|
4361
|
+
console.log(` ${chalk22.green(app.name)}: ${app.envFilePath} (${app.vars.length} vars)`);
|
|
4068
4362
|
}
|
|
4069
4363
|
const content = generateManifest(apps);
|
|
4070
4364
|
const dir = dirname4(manifestPath);
|
|
4071
|
-
if (!
|
|
4365
|
+
if (!existsSync14(dir)) {
|
|
4072
4366
|
await mkdir4(dir, { recursive: true });
|
|
4073
4367
|
}
|
|
4074
4368
|
await writeFile5(manifestPath, content, "utf-8");
|
|
4075
|
-
console.log(
|
|
4369
|
+
console.log(chalk22.green(`
|
|
4076
4370
|
Manifest created: ${relative4(projectRoot, manifestPath)}`));
|
|
4077
|
-
console.log(
|
|
4078
|
-
console.log(
|
|
4371
|
+
console.log(chalk22.dim("Review and edit the file \u2014 it is your source of truth."));
|
|
4372
|
+
console.log(chalk22.dim("Then run `md4ai scan` to verify against your environments."));
|
|
4079
4373
|
}
|
|
4080
4374
|
|
|
4081
4375
|
// dist/commands/update.js
|
|
4082
4376
|
init_check_update();
|
|
4083
4377
|
init_config();
|
|
4084
|
-
import
|
|
4378
|
+
import chalk23 from "chalk";
|
|
4085
4379
|
import { execFileSync as execFileSync6, spawn } from "node:child_process";
|
|
4086
4380
|
async function fetchLatestVersion() {
|
|
4087
4381
|
try {
|
|
@@ -4130,13 +4424,13 @@ function spawnPostUpdate() {
|
|
|
4130
4424
|
}
|
|
4131
4425
|
async function postUpdateFlow() {
|
|
4132
4426
|
const { confirm: confirm4 } = await import("@inquirer/prompts");
|
|
4133
|
-
console.log(
|
|
4427
|
+
console.log(chalk23.green(`
|
|
4134
4428
|
md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.
|
|
4135
4429
|
`));
|
|
4136
4430
|
const creds = await loadCredentials();
|
|
4137
4431
|
const isLoggedIn = !!creds?.accessToken && Date.now() < creds.expiresAt;
|
|
4138
4432
|
if (!isLoggedIn) {
|
|
4139
|
-
console.log(
|
|
4433
|
+
console.log(chalk23.yellow(" You are not logged in.\n"));
|
|
4140
4434
|
const wantLogin = await confirm4({
|
|
4141
4435
|
message: "Log in now?",
|
|
4142
4436
|
default: true
|
|
@@ -4147,7 +4441,7 @@ async function postUpdateFlow() {
|
|
|
4147
4441
|
await loginCommand2();
|
|
4148
4442
|
console.log("");
|
|
4149
4443
|
} else {
|
|
4150
|
-
console.log(
|
|
4444
|
+
console.log(chalk23.dim("\nRun md4ai login when you're ready.\n"));
|
|
4151
4445
|
return;
|
|
4152
4446
|
}
|
|
4153
4447
|
}
|
|
@@ -4181,7 +4475,7 @@ async function postUpdateFlow() {
|
|
|
4181
4475
|
const { mcpWatchCommand: mcpWatchCommand2 } = await Promise.resolve().then(() => (init_mcp_watch(), mcp_watch_exports));
|
|
4182
4476
|
await mcpWatchCommand2();
|
|
4183
4477
|
} else {
|
|
4184
|
-
console.log(
|
|
4478
|
+
console.log(chalk23.dim("\nYou can start monitoring later with: md4ai mcp-watch\n"));
|
|
4185
4479
|
}
|
|
4186
4480
|
}
|
|
4187
4481
|
async function updateCommand(options) {
|
|
@@ -4189,19 +4483,19 @@ async function updateCommand(options) {
|
|
|
4189
4483
|
await postUpdateFlow();
|
|
4190
4484
|
return;
|
|
4191
4485
|
}
|
|
4192
|
-
console.log(
|
|
4486
|
+
console.log(chalk23.blue(`
|
|
4193
4487
|
Current version: v${CURRENT_VERSION}`));
|
|
4194
|
-
console.log(
|
|
4488
|
+
console.log(chalk23.dim(" Checking for updates...\n"));
|
|
4195
4489
|
const latest = await fetchLatestVersion();
|
|
4196
4490
|
if (!latest) {
|
|
4197
|
-
console.log(
|
|
4491
|
+
console.log(chalk23.yellow(" Could not reach the npm registry. Check your internet connection."));
|
|
4198
4492
|
process.exit(1);
|
|
4199
4493
|
}
|
|
4200
4494
|
if (!isNewer2(latest, CURRENT_VERSION)) {
|
|
4201
4495
|
await postUpdateFlow();
|
|
4202
4496
|
return;
|
|
4203
4497
|
}
|
|
4204
|
-
console.log(
|
|
4498
|
+
console.log(chalk23.white(" Update available: ") + chalk23.dim(`v${CURRENT_VERSION}`) + chalk23.white(" \u2192 ") + chalk23.green.bold(`v${latest}
|
|
4205
4499
|
`));
|
|
4206
4500
|
const { confirm: confirm4 } = await import("@inquirer/prompts");
|
|
4207
4501
|
const wantUpdate = await confirm4({
|
|
@@ -4209,17 +4503,17 @@ async function updateCommand(options) {
|
|
|
4209
4503
|
default: true
|
|
4210
4504
|
});
|
|
4211
4505
|
if (!wantUpdate) {
|
|
4212
|
-
console.log(
|
|
4506
|
+
console.log(chalk23.dim("\nUpdate skipped.\n"));
|
|
4213
4507
|
return;
|
|
4214
4508
|
}
|
|
4215
|
-
console.log(
|
|
4509
|
+
console.log(chalk23.blue("\n Installing...\n"));
|
|
4216
4510
|
const ok = runNpmInstall();
|
|
4217
4511
|
if (!ok) {
|
|
4218
|
-
console.log(
|
|
4219
|
-
console.log(
|
|
4512
|
+
console.log(chalk23.red("\n npm install failed. Try running manually:"));
|
|
4513
|
+
console.log(chalk23.cyan(" npm install -g md4ai\n"));
|
|
4220
4514
|
process.exit(1);
|
|
4221
4515
|
}
|
|
4222
|
-
console.log(
|
|
4516
|
+
console.log(chalk23.green("\n Updated successfully.\n"));
|
|
4223
4517
|
spawnPostUpdate();
|
|
4224
4518
|
}
|
|
4225
4519
|
|
|
@@ -4227,7 +4521,7 @@ async function updateCommand(options) {
|
|
|
4227
4521
|
init_check_update();
|
|
4228
4522
|
init_map();
|
|
4229
4523
|
init_mcp_watch();
|
|
4230
|
-
import
|
|
4524
|
+
import chalk24 from "chalk";
|
|
4231
4525
|
import { resolve as resolve9 } from "node:path";
|
|
4232
4526
|
async function fetchLatestVersion2() {
|
|
4233
4527
|
try {
|
|
@@ -4259,13 +4553,13 @@ function isNewer3(a, b) {
|
|
|
4259
4553
|
async function startCommand() {
|
|
4260
4554
|
const projectRoot = resolve9(process.cwd());
|
|
4261
4555
|
console.log("");
|
|
4262
|
-
console.log(
|
|
4263
|
-
console.log(
|
|
4556
|
+
console.log(chalk24.bold.cyan(` MD4AI v${CURRENT_VERSION}`));
|
|
4557
|
+
console.log(chalk24.dim(` ${projectRoot}`));
|
|
4264
4558
|
console.log("");
|
|
4265
|
-
console.log(
|
|
4559
|
+
console.log(chalk24.blue(" \u2460 Checking for updates..."));
|
|
4266
4560
|
const latest = await fetchLatestVersion2();
|
|
4267
4561
|
if (latest && isNewer3(latest, CURRENT_VERSION)) {
|
|
4268
|
-
console.log(
|
|
4562
|
+
console.log(chalk24.yellow(` Update available: v${CURRENT_VERSION} \u2192 v${latest}`));
|
|
4269
4563
|
if (process.stdin.isTTY) {
|
|
4270
4564
|
const { confirm: confirm4 } = await import("@inquirer/prompts");
|
|
4271
4565
|
const wantUpdate = await confirm4({
|
|
@@ -4274,61 +4568,179 @@ async function startCommand() {
|
|
|
4274
4568
|
});
|
|
4275
4569
|
if (wantUpdate) {
|
|
4276
4570
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
4277
|
-
console.log(
|
|
4571
|
+
console.log(chalk24.blue("\n Installing...\n"));
|
|
4278
4572
|
try {
|
|
4279
4573
|
execFileSync7("npm", ["install", "-g", "md4ai"], {
|
|
4280
4574
|
stdio: "inherit",
|
|
4281
4575
|
timeout: 6e4
|
|
4282
4576
|
});
|
|
4283
|
-
console.log(
|
|
4284
|
-
console.log(
|
|
4577
|
+
console.log(chalk24.green(" Updated successfully."));
|
|
4578
|
+
console.log(chalk24.dim(" Restarting with the new version...\n"));
|
|
4285
4579
|
const { spawn: spawn2 } = await import("node:child_process");
|
|
4286
4580
|
const child = spawn2("md4ai", ["start"], { stdio: "inherit", shell: false });
|
|
4287
4581
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
4288
4582
|
return;
|
|
4289
4583
|
} catch {
|
|
4290
|
-
console.log(
|
|
4584
|
+
console.log(chalk24.yellow(" Update failed \u2014 continuing with current version.\n"));
|
|
4291
4585
|
}
|
|
4292
4586
|
} else {
|
|
4293
4587
|
console.log("");
|
|
4294
4588
|
}
|
|
4295
4589
|
} else {
|
|
4296
|
-
console.log(
|
|
4590
|
+
console.log(chalk24.dim(" Run md4ai update to install it.\n"));
|
|
4297
4591
|
}
|
|
4298
4592
|
} else if (latest) {
|
|
4299
|
-
console.log(
|
|
4593
|
+
console.log(chalk24.green(" You're on the latest version.\n"));
|
|
4300
4594
|
} else {
|
|
4301
|
-
console.log(
|
|
4595
|
+
console.log(chalk24.dim(" Could not reach npm registry \u2014 skipping.\n"));
|
|
4302
4596
|
}
|
|
4303
|
-
console.log(
|
|
4597
|
+
console.log(chalk24.blue(" \u2461 Scanning project files..."));
|
|
4304
4598
|
console.log("");
|
|
4305
4599
|
await mapCommand(projectRoot, {});
|
|
4306
4600
|
if (!process.stdin.isTTY) {
|
|
4307
|
-
console.log(
|
|
4601
|
+
console.log(chalk24.dim("\n Non-interactive mode \u2014 skipping MCP monitor."));
|
|
4308
4602
|
return;
|
|
4309
4603
|
}
|
|
4310
4604
|
console.log("");
|
|
4311
|
-
console.log(
|
|
4605
|
+
console.log(chalk24.blue(" \u2462 Starting MCP monitor..."));
|
|
4312
4606
|
console.log("");
|
|
4313
4607
|
await mcpWatchCommand();
|
|
4314
4608
|
}
|
|
4315
4609
|
|
|
4316
4610
|
// dist/commands/config.js
|
|
4317
4611
|
init_config();
|
|
4318
|
-
import
|
|
4319
|
-
var ALLOWED_KEYS = ["vercel-token"];
|
|
4612
|
+
import chalk25 from "chalk";
|
|
4613
|
+
var ALLOWED_KEYS = ["vercel-token", "doppler-token"];
|
|
4320
4614
|
var KEY_MAP = {
|
|
4321
|
-
"vercel-token": "vercelToken"
|
|
4615
|
+
"vercel-token": "vercelToken",
|
|
4616
|
+
"doppler-token": "dopplerToken"
|
|
4322
4617
|
};
|
|
4323
4618
|
async function configSetCommand(key, value) {
|
|
4324
4619
|
if (!ALLOWED_KEYS.includes(key)) {
|
|
4325
|
-
console.error(
|
|
4620
|
+
console.error(chalk25.red(`Unknown config key: ${key}`));
|
|
4326
4621
|
console.log(` Allowed keys: ${ALLOWED_KEYS.join(", ")}`);
|
|
4327
4622
|
process.exit(1);
|
|
4328
4623
|
}
|
|
4329
4624
|
const credKey = KEY_MAP[key];
|
|
4330
4625
|
await mergeCredentials({ [credKey]: value });
|
|
4331
|
-
console.log(
|
|
4626
|
+
console.log(chalk25.green(`Saved ${key}.`));
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
// dist/commands/doppler.js
|
|
4630
|
+
init_config();
|
|
4631
|
+
init_api();
|
|
4632
|
+
init_auth();
|
|
4633
|
+
init_dist();
|
|
4634
|
+
import chalk26 from "chalk";
|
|
4635
|
+
import { input as input7 } from "@inquirer/prompts";
|
|
4636
|
+
async function dopplerConnectCommand() {
|
|
4637
|
+
const token = await input7({
|
|
4638
|
+
message: "Enter your Doppler service account token:",
|
|
4639
|
+
transformer: (val) => val.replace(/./g, "*"),
|
|
4640
|
+
validate: (val) => val.length > 0 || "Token is required"
|
|
4641
|
+
});
|
|
4642
|
+
console.log(chalk26.dim(" Validating token..."));
|
|
4643
|
+
const { valid, projectCount } = await validateDopplerToken(token);
|
|
4644
|
+
if (!valid) {
|
|
4645
|
+
console.error(chalk26.red("Token is invalid or has expired."));
|
|
4646
|
+
console.log(chalk26.yellow("Generate a service account token at https://dashboard.doppler.com"));
|
|
4647
|
+
process.exit(1);
|
|
4648
|
+
}
|
|
4649
|
+
await mergeCredentials({ dopplerToken: token });
|
|
4650
|
+
console.log(chalk26.green(`Token validated \u2014 ${projectCount} project${projectCount !== 1 ? "s" : ""} accessible.`));
|
|
4651
|
+
console.log(chalk26.green("Saved to ~/.md4ai/credentials.json"));
|
|
4652
|
+
const creds = await loadCredentials();
|
|
4653
|
+
if (!creds?.accessToken || !creds?.userId) {
|
|
4654
|
+
console.log("");
|
|
4655
|
+
console.log(chalk26.yellow("\u26A0 Not logged in to MD4AI \u2014 token saved locally only."));
|
|
4656
|
+
console.log(chalk26.yellow(" The web dashboard cannot show live Doppler status until the token is synced."));
|
|
4657
|
+
console.log(chalk26.yellow(' To fix: run "md4ai login", then "md4ai doppler connect" again.'));
|
|
4658
|
+
} else {
|
|
4659
|
+
let supabase = createSupabaseClient(getAnonKey(), creds.accessToken);
|
|
4660
|
+
let userId = creds.userId;
|
|
4661
|
+
if (Date.now() > creds.expiresAt) {
|
|
4662
|
+
const refreshed = await refreshSession();
|
|
4663
|
+
if (refreshed) {
|
|
4664
|
+
supabase = refreshed.supabase;
|
|
4665
|
+
userId = refreshed.userId;
|
|
4666
|
+
} else {
|
|
4667
|
+
console.log("");
|
|
4668
|
+
console.log(chalk26.yellow("\u26A0 Session expired \u2014 token saved locally only."));
|
|
4669
|
+
console.log(chalk26.yellow(' To sync to the web dashboard: run "md4ai login", then "md4ai doppler connect" again.'));
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
if (supabase) {
|
|
4673
|
+
try {
|
|
4674
|
+
const { error } = await supabase.from("user_secrets").upsert({ user_id: userId, doppler_token: token, updated_at: (/* @__PURE__ */ new Date()).toISOString() }, { onConflict: "user_id" });
|
|
4675
|
+
if (error) {
|
|
4676
|
+
console.log(chalk26.yellow(`\u26A0 Could not sync token to web dashboard: ${error.message}`));
|
|
4677
|
+
} else {
|
|
4678
|
+
const { data: verify } = await supabase.from("user_secrets").select("id").eq("user_id", userId).maybeSingle();
|
|
4679
|
+
if (verify) {
|
|
4680
|
+
console.log(chalk26.green("Token synced to web dashboard."));
|
|
4681
|
+
} else {
|
|
4682
|
+
console.log(chalk26.yellow("\u26A0 Token sync could not be verified \u2014 check the web dashboard."));
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
} catch (err) {
|
|
4686
|
+
console.log(chalk26.yellow(`\u26A0 Could not sync token to web dashboard: ${err instanceof Error ? err.message : "unknown error"}`));
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
console.log("");
|
|
4691
|
+
console.log(chalk26.cyan("Next step: link a Doppler project to your Claude project."));
|
|
4692
|
+
console.log(chalk26.dim(" The scanner looks for a doppler.yaml in the project root, e.g.:"));
|
|
4693
|
+
console.log(chalk26.dim(" project: my-doppler-project"));
|
|
4694
|
+
console.log(chalk26.dim(" Or use a manual override:"));
|
|
4695
|
+
console.log(chalk26.dim(" md4ai doppler set-project <slug>"));
|
|
4696
|
+
console.log(chalk26.dim(' Then run "md4ai scan" to fetch secret names from Doppler.'));
|
|
4697
|
+
}
|
|
4698
|
+
async function dopplerDisconnectCommand() {
|
|
4699
|
+
const creds = await loadCredentials();
|
|
4700
|
+
if (!creds?.dopplerToken) {
|
|
4701
|
+
console.log(chalk26.yellow("No Doppler token configured."));
|
|
4702
|
+
return;
|
|
4703
|
+
}
|
|
4704
|
+
const { dopplerToken: _, ...rest } = creds;
|
|
4705
|
+
const { writeFile: writeFile6, chmod: chmod2 } = await import("node:fs/promises");
|
|
4706
|
+
const { join: join17 } = await import("node:path");
|
|
4707
|
+
const { homedir: homedir10 } = await import("node:os");
|
|
4708
|
+
const credPath = join17(homedir10(), ".md4ai", "credentials.json");
|
|
4709
|
+
await writeFile6(credPath, JSON.stringify(rest, null, 2), "utf-8");
|
|
4710
|
+
await chmod2(credPath, 384);
|
|
4711
|
+
try {
|
|
4712
|
+
const currentCreds = await loadCredentials();
|
|
4713
|
+
if (currentCreds?.accessToken && currentCreds?.userId) {
|
|
4714
|
+
let supabase = createSupabaseClient(getAnonKey(), currentCreds.accessToken);
|
|
4715
|
+
let userId = currentCreds.userId;
|
|
4716
|
+
if (Date.now() > currentCreds.expiresAt) {
|
|
4717
|
+
const refreshed = await refreshSession();
|
|
4718
|
+
if (refreshed) {
|
|
4719
|
+
supabase = refreshed.supabase;
|
|
4720
|
+
userId = refreshed.userId;
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
await supabase.from("user_secrets").delete().eq("user_id", userId);
|
|
4724
|
+
console.log(chalk26.green("Doppler token removed from local and web dashboard."));
|
|
4725
|
+
} else {
|
|
4726
|
+
console.log(chalk26.green("Doppler token removed locally."));
|
|
4727
|
+
}
|
|
4728
|
+
} catch {
|
|
4729
|
+
console.log(chalk26.green("Doppler token removed locally."));
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
async function dopplerSetProjectCommand(slug) {
|
|
4733
|
+
const state = await loadState();
|
|
4734
|
+
const folderId = state.lastFolderId;
|
|
4735
|
+
if (!folderId) {
|
|
4736
|
+
console.error(chalk26.red('No project linked yet. Run "md4ai scan" first to link a project.'));
|
|
4737
|
+
process.exit(1);
|
|
4738
|
+
}
|
|
4739
|
+
const dopplerProjects = state.dopplerProjects ?? {};
|
|
4740
|
+
dopplerProjects[folderId] = slug;
|
|
4741
|
+
await saveState({ dopplerProjects });
|
|
4742
|
+
console.log(chalk26.green(`Doppler project "${slug}" linked to folder ${folderId.slice(0, 8)}...`));
|
|
4743
|
+
console.log(chalk26.dim("This override will be used instead of doppler.yaml."));
|
|
4332
4744
|
}
|
|
4333
4745
|
|
|
4334
4746
|
// dist/index.js
|
|
@@ -4357,6 +4769,10 @@ program.command("mcp-watch").description("Monitor MCP server status on this devi
|
|
|
4357
4769
|
program.command("init-manifest").description("Scaffold a starter env-manifest.md from detected .env files").action(initManifestCommand);
|
|
4358
4770
|
var config = program.command("config").description("Manage CLI configuration");
|
|
4359
4771
|
config.command("set <key> <value>").description("Set a configuration value (e.g. vercel-token)").action(configSetCommand);
|
|
4772
|
+
var doppler = program.command("doppler").description("Manage Doppler integration");
|
|
4773
|
+
doppler.command("connect").description("Configure Doppler token for secrets scanning").action(dopplerConnectCommand);
|
|
4774
|
+
doppler.command("disconnect").description("Remove stored Doppler token").action(dopplerDisconnectCommand);
|
|
4775
|
+
doppler.command("set-project <slug>").description("Override Doppler project slug for the current linked project").action(dopplerSetProjectCommand);
|
|
4360
4776
|
var admin = program.command("admin").description("Admin commands for managing the tools registry");
|
|
4361
4777
|
admin.command("update-tool").description("Add or update a tool in the master registry").requiredOption("--name <name>", "Canonical tool name (e.g. next, playwright)").option("--display <display>", 'Human-friendly display name (e.g. "Next.js")').option("--category <category>", "Tool category (framework|runtime|cli|mcp|package|database|other)").option("--stable <version>", "Latest stable version").option("--beta <version>", "Latest beta/RC version").option("--source <url>", "Source of truth URL for checking versions").option("--install <url>", "Download/install link").option("--notes <text>", "Compatibility notes or warnings").action(adminUpdateToolCommand);
|
|
4362
4778
|
admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
|