@sightmap/sightmap 0.3.1 → 0.5.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/README.md +31 -0
- package/dist/cli/index.js +1198 -0
- package/dist/cli/index.js.map +1 -1
- package/package.json +9 -1
package/dist/cli/index.js
CHANGED
|
@@ -1497,6 +1497,1190 @@ async function processFile(absPath, relPath, opts, fileResults, diagnostics) {
|
|
|
1497
1497
|
}
|
|
1498
1498
|
}
|
|
1499
1499
|
|
|
1500
|
+
// src/cli/init/index.ts
|
|
1501
|
+
import { resolve as resolve22 } from "path";
|
|
1502
|
+
import { mkdir as mkdir4, readFile as readFile13, rm, writeFile as writeFile7 } from "fs/promises";
|
|
1503
|
+
import { tmpdir } from "os";
|
|
1504
|
+
import * as clack2 from "@clack/prompts";
|
|
1505
|
+
|
|
1506
|
+
// src/cli/init/detect/framework.ts
|
|
1507
|
+
import { readFile as readFile4, access } from "fs/promises";
|
|
1508
|
+
import { resolve as resolve11 } from "path";
|
|
1509
|
+
async function exists(path) {
|
|
1510
|
+
try {
|
|
1511
|
+
await access(path);
|
|
1512
|
+
return true;
|
|
1513
|
+
} catch {
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
async function detectFramework(cwd) {
|
|
1518
|
+
const pkgPath = resolve11(cwd, "package.json");
|
|
1519
|
+
if (!await exists(pkgPath)) return "unknown";
|
|
1520
|
+
const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
1521
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1522
|
+
if (deps.next) {
|
|
1523
|
+
if (await exists(resolve11(cwd, "app"))) return "next-app";
|
|
1524
|
+
if (await exists(resolve11(cwd, "pages"))) return "next-pages";
|
|
1525
|
+
return "next-app";
|
|
1526
|
+
}
|
|
1527
|
+
const rrVersion = deps["react-router"];
|
|
1528
|
+
const rr7 = rrVersion ? /^[\^~>=<]*7\./.test(rrVersion) : false;
|
|
1529
|
+
if (rr7 || await exists(resolve11(cwd, "react-router.config.ts"))) {
|
|
1530
|
+
return "react-router-7";
|
|
1531
|
+
}
|
|
1532
|
+
if (deps.react) {
|
|
1533
|
+
if (deps.vite || await exists(resolve11(cwd, "vite.config.ts")) || await exists(resolve11(cwd, "vite.config.js"))) {
|
|
1534
|
+
return "react-vite";
|
|
1535
|
+
}
|
|
1536
|
+
if (deps["react-scripts"]) return "react-cra";
|
|
1537
|
+
}
|
|
1538
|
+
return "unknown";
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/cli/init/detect/harness.ts
|
|
1542
|
+
import { access as access2 } from "fs/promises";
|
|
1543
|
+
import { resolve as resolve12 } from "path";
|
|
1544
|
+
async function exists2(path) {
|
|
1545
|
+
try {
|
|
1546
|
+
await access2(path);
|
|
1547
|
+
return true;
|
|
1548
|
+
} catch {
|
|
1549
|
+
return false;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
var HARNESS_ORDER = ["claude-code", "codex", "cursor", "opencode"];
|
|
1553
|
+
async function detectHarnesses(cwd) {
|
|
1554
|
+
const out = [];
|
|
1555
|
+
for (const h of HARNESS_ORDER) {
|
|
1556
|
+
let found = false;
|
|
1557
|
+
if (h === "claude-code") found = await exists2(resolve12(cwd, ".claude"));
|
|
1558
|
+
else if (h === "cursor") found = await exists2(resolve12(cwd, ".cursor"));
|
|
1559
|
+
else if (h === "codex") found = await exists2(resolve12(cwd, ".codex"));
|
|
1560
|
+
else if (h === "opencode") found = await exists2(resolve12(cwd, "opencode.json"));
|
|
1561
|
+
if (found) out.push(h);
|
|
1562
|
+
}
|
|
1563
|
+
return out;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/cli/init/detect/vendor.ts
|
|
1567
|
+
import { readFile as readFile5, access as access3 } from "fs/promises";
|
|
1568
|
+
import { resolve as resolve13 } from "path";
|
|
1569
|
+
|
|
1570
|
+
// src/cli/init/vendor-allowlist.ts
|
|
1571
|
+
var KNOWN_SIGHTMAP_VENDORS = ["subtext"];
|
|
1572
|
+
function isKnownVendor(entry) {
|
|
1573
|
+
const haystacks = [entry.name, entry.command, ...entry.args];
|
|
1574
|
+
return KNOWN_SIGHTMAP_VENDORS.some(
|
|
1575
|
+
(vendor) => haystacks.some((h) => h.toLowerCase().includes(vendor.toLowerCase()))
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/cli/init/detect/vendor.ts
|
|
1580
|
+
async function readJsonIfExists(path) {
|
|
1581
|
+
try {
|
|
1582
|
+
await access3(path);
|
|
1583
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
1584
|
+
} catch {
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function extractServers(parsed) {
|
|
1589
|
+
if (!parsed || typeof parsed !== "object") return [];
|
|
1590
|
+
const root = parsed;
|
|
1591
|
+
const servers = root.mcpServers ?? {};
|
|
1592
|
+
return Object.entries(servers).map(([name, def]) => ({
|
|
1593
|
+
name,
|
|
1594
|
+
command: def.command ?? "",
|
|
1595
|
+
args: def.args ?? []
|
|
1596
|
+
}));
|
|
1597
|
+
}
|
|
1598
|
+
async function detectSightmapAwareVendor(cwd, hosts) {
|
|
1599
|
+
const candidates = [];
|
|
1600
|
+
if (hosts.includes("claude-code")) candidates.push(resolve13(cwd, ".mcp.json"));
|
|
1601
|
+
if (hosts.includes("cursor")) candidates.push(resolve13(cwd, ".cursor/mcp.json"));
|
|
1602
|
+
if (hosts.includes("opencode")) candidates.push(resolve13(cwd, "opencode.json"));
|
|
1603
|
+
for (const path of candidates) {
|
|
1604
|
+
const parsed = await readJsonIfExists(path);
|
|
1605
|
+
if (!parsed) continue;
|
|
1606
|
+
for (const entry of extractServers(parsed)) {
|
|
1607
|
+
if (isKnownVendor(entry)) return true;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// src/cli/init/detect/sightmap-dir.ts
|
|
1614
|
+
import { access as access4, readdir as readdir4 } from "fs/promises";
|
|
1615
|
+
import { resolve as resolve14 } from "path";
|
|
1616
|
+
async function detectSightmapDir(cwd) {
|
|
1617
|
+
const dir = resolve14(cwd, ".sightmap");
|
|
1618
|
+
try {
|
|
1619
|
+
await access4(dir);
|
|
1620
|
+
} catch {
|
|
1621
|
+
return { present: false };
|
|
1622
|
+
}
|
|
1623
|
+
const entries = await readdir4(dir);
|
|
1624
|
+
const yamls = entries.filter((e) => e.endsWith(".yaml") || e.endsWith(".yml"));
|
|
1625
|
+
return { present: true, viewCount: yamls.length };
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// src/cli/init/detect/workspaces.ts
|
|
1629
|
+
import { readFile as readFile6, readdir as readdir5, stat as stat3 } from "fs/promises";
|
|
1630
|
+
import { resolve as resolve15, relative as relative6 } from "path";
|
|
1631
|
+
import { parse as parseYaml } from "yaml";
|
|
1632
|
+
async function exists3(p) {
|
|
1633
|
+
try {
|
|
1634
|
+
await stat3(p);
|
|
1635
|
+
return true;
|
|
1636
|
+
} catch {
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
async function readWorkspacePatterns(root) {
|
|
1641
|
+
const patterns = [];
|
|
1642
|
+
const pnpmWs = resolve15(root, "pnpm-workspace.yaml");
|
|
1643
|
+
if (await exists3(pnpmWs)) {
|
|
1644
|
+
try {
|
|
1645
|
+
const parsed = parseYaml(await readFile6(pnpmWs, "utf8"));
|
|
1646
|
+
const list = parsed && Array.isArray(parsed.packages) ? parsed.packages : [];
|
|
1647
|
+
for (const p of list) {
|
|
1648
|
+
if (typeof p === "string") patterns.push(p);
|
|
1649
|
+
}
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
const pkgJson = resolve15(root, "package.json");
|
|
1654
|
+
if (await exists3(pkgJson)) {
|
|
1655
|
+
try {
|
|
1656
|
+
const pkg = JSON.parse(await readFile6(pkgJson, "utf8"));
|
|
1657
|
+
const ws = pkg.workspaces;
|
|
1658
|
+
if (Array.isArray(ws)) patterns.push(...ws);
|
|
1659
|
+
else if (ws && Array.isArray(ws.packages)) patterns.push(...ws.packages);
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return patterns;
|
|
1664
|
+
}
|
|
1665
|
+
async function expandPattern(root, pattern) {
|
|
1666
|
+
const trimmed = pattern.replace(/^\.\//, "").replace(/\/$/, "");
|
|
1667
|
+
if (trimmed.startsWith("!")) return [];
|
|
1668
|
+
if (trimmed.includes("**")) return expandDoubleStar(root, trimmed);
|
|
1669
|
+
const segments = trimmed.split("/");
|
|
1670
|
+
return expandSegments(root, segments, 0, root);
|
|
1671
|
+
}
|
|
1672
|
+
async function expandSegments(root, segs, idx, current) {
|
|
1673
|
+
if (idx >= segs.length) {
|
|
1674
|
+
if (await isPackageDir(current)) return [current];
|
|
1675
|
+
return [];
|
|
1676
|
+
}
|
|
1677
|
+
const seg = segs[idx];
|
|
1678
|
+
if (seg === void 0) return [];
|
|
1679
|
+
if (seg === "*") {
|
|
1680
|
+
let entries = [];
|
|
1681
|
+
try {
|
|
1682
|
+
entries = await readdir5(current);
|
|
1683
|
+
} catch {
|
|
1684
|
+
return [];
|
|
1685
|
+
}
|
|
1686
|
+
const out = [];
|
|
1687
|
+
for (const name of entries) {
|
|
1688
|
+
if (name.startsWith(".")) continue;
|
|
1689
|
+
const child = resolve15(current, name);
|
|
1690
|
+
let s;
|
|
1691
|
+
try {
|
|
1692
|
+
s = await stat3(child);
|
|
1693
|
+
} catch {
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
if (!s.isDirectory()) continue;
|
|
1697
|
+
out.push(...await expandSegments(root, segs, idx + 1, child));
|
|
1698
|
+
}
|
|
1699
|
+
return out;
|
|
1700
|
+
}
|
|
1701
|
+
return expandSegments(root, segs, idx + 1, resolve15(current, seg));
|
|
1702
|
+
}
|
|
1703
|
+
async function expandDoubleStar(root, pattern) {
|
|
1704
|
+
const prefix = (pattern.split("**")[0] ?? "").replace(/\/$/, "");
|
|
1705
|
+
const base = prefix ? resolve15(root, prefix) : root;
|
|
1706
|
+
const out = [];
|
|
1707
|
+
await walk2(base, 0, 5, out);
|
|
1708
|
+
return out;
|
|
1709
|
+
}
|
|
1710
|
+
async function walk2(dir, depth, maxDepth, out) {
|
|
1711
|
+
if (depth > maxDepth) return;
|
|
1712
|
+
if (await isPackageDir(dir)) out.push(dir);
|
|
1713
|
+
let entries = [];
|
|
1714
|
+
try {
|
|
1715
|
+
entries = await readdir5(dir);
|
|
1716
|
+
} catch {
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
for (const name of entries) {
|
|
1720
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
1721
|
+
const child = resolve15(dir, name);
|
|
1722
|
+
let s;
|
|
1723
|
+
try {
|
|
1724
|
+
s = await stat3(child);
|
|
1725
|
+
} catch {
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
if (s.isDirectory()) await walk2(child, depth + 1, maxDepth, out);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
async function isPackageDir(dir) {
|
|
1732
|
+
return exists3(resolve15(dir, "package.json"));
|
|
1733
|
+
}
|
|
1734
|
+
async function enumerateWorkspaces(opts) {
|
|
1735
|
+
const { root } = opts;
|
|
1736
|
+
const patterns = await readWorkspacePatterns(root);
|
|
1737
|
+
if (patterns.length === 0) return [];
|
|
1738
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
1739
|
+
for (const pat of patterns) {
|
|
1740
|
+
for (const d of await expandPattern(root, pat)) dirs.add(d);
|
|
1741
|
+
}
|
|
1742
|
+
const result = [];
|
|
1743
|
+
for (const dir of dirs) {
|
|
1744
|
+
if (dir === root) continue;
|
|
1745
|
+
const framework = await detectFramework(dir);
|
|
1746
|
+
result.push({ dir, relPath: relative6(root, dir) || ".", framework });
|
|
1747
|
+
}
|
|
1748
|
+
result.sort((a, b) => {
|
|
1749
|
+
const ak = a.framework === "unknown" ? 1 : 0;
|
|
1750
|
+
const bk = b.framework === "unknown" ? 1 : 0;
|
|
1751
|
+
if (ak !== bk) return ak - bk;
|
|
1752
|
+
return a.relPath.localeCompare(b.relPath);
|
|
1753
|
+
});
|
|
1754
|
+
return result;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// src/cli/init/tui.ts
|
|
1758
|
+
import * as clack from "@clack/prompts";
|
|
1759
|
+
import pc from "picocolors";
|
|
1760
|
+
|
|
1761
|
+
// src/cli/init/framework-setup/provider-snippet.ts
|
|
1762
|
+
import { relative as relative7 } from "path";
|
|
1763
|
+
var IMPORT_LINE = `import { SightmapProvider } from '@sightmap/react';`;
|
|
1764
|
+
function renderProviderSnippet(opts) {
|
|
1765
|
+
const relPath = relative7(opts.cwd, opts.entryAbsPath) || opts.entryAbsPath;
|
|
1766
|
+
switch (opts.framework) {
|
|
1767
|
+
case "react-vite":
|
|
1768
|
+
case "react-cra":
|
|
1769
|
+
return {
|
|
1770
|
+
relPath,
|
|
1771
|
+
importLine: IMPORT_LINE,
|
|
1772
|
+
instruction: "Wrap your render root with <SightmapProvider>:",
|
|
1773
|
+
example: `ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
1774
|
+
<SightmapProvider>
|
|
1775
|
+
<App />
|
|
1776
|
+
</SightmapProvider>,
|
|
1777
|
+
);`
|
|
1778
|
+
};
|
|
1779
|
+
case "next-app":
|
|
1780
|
+
return {
|
|
1781
|
+
relPath,
|
|
1782
|
+
importLine: IMPORT_LINE,
|
|
1783
|
+
instruction: "Wrap the layout's {children} with <SightmapProvider>:",
|
|
1784
|
+
example: `export default function RootLayout({ children }) {
|
|
1785
|
+
return (
|
|
1786
|
+
<html>
|
|
1787
|
+
<body>
|
|
1788
|
+
<SightmapProvider>{children}</SightmapProvider>
|
|
1789
|
+
</body>
|
|
1790
|
+
</html>
|
|
1791
|
+
);
|
|
1792
|
+
}`
|
|
1793
|
+
};
|
|
1794
|
+
case "next-pages":
|
|
1795
|
+
return {
|
|
1796
|
+
relPath,
|
|
1797
|
+
importLine: IMPORT_LINE,
|
|
1798
|
+
instruction: "Wrap <Component {...pageProps} /> with <SightmapProvider>:",
|
|
1799
|
+
example: `export default function App({ Component, pageProps }) {
|
|
1800
|
+
return (
|
|
1801
|
+
<SightmapProvider>
|
|
1802
|
+
<Component {...pageProps} />
|
|
1803
|
+
</SightmapProvider>
|
|
1804
|
+
);
|
|
1805
|
+
}`
|
|
1806
|
+
};
|
|
1807
|
+
case "react-router-7":
|
|
1808
|
+
return {
|
|
1809
|
+
relPath,
|
|
1810
|
+
importLine: IMPORT_LINE,
|
|
1811
|
+
instruction: "Wrap the default export's returned JSX with <SightmapProvider>:",
|
|
1812
|
+
example: `export default function Root() {
|
|
1813
|
+
return (
|
|
1814
|
+
<SightmapProvider>
|
|
1815
|
+
<Outlet />
|
|
1816
|
+
</SightmapProvider>
|
|
1817
|
+
);
|
|
1818
|
+
}`
|
|
1819
|
+
};
|
|
1820
|
+
case "unknown":
|
|
1821
|
+
return {
|
|
1822
|
+
relPath,
|
|
1823
|
+
importLine: IMPORT_LINE,
|
|
1824
|
+
instruction: "Wrap your application's root component with <SightmapProvider>.",
|
|
1825
|
+
example: `<SightmapProvider>{/* your app */}</SightmapProvider>`
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
function formatSnippet(snippet) {
|
|
1830
|
+
return [
|
|
1831
|
+
`In ${snippet.relPath}:`,
|
|
1832
|
+
"",
|
|
1833
|
+
` ${snippet.importLine}`,
|
|
1834
|
+
"",
|
|
1835
|
+
snippet.instruction,
|
|
1836
|
+
"",
|
|
1837
|
+
...snippet.example.split("\n").map((l) => ` ${l}`)
|
|
1838
|
+
].join("\n");
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/cli/init/tui.ts
|
|
1842
|
+
var FRAMEWORK_LABELS = {
|
|
1843
|
+
"react-vite": "React (Vite)",
|
|
1844
|
+
"react-cra": "React (CRA)",
|
|
1845
|
+
"next-app": "Next.js (App Router)",
|
|
1846
|
+
"next-pages": "Next.js (Pages Router)",
|
|
1847
|
+
"react-router-7": "React Router 7",
|
|
1848
|
+
unknown: "unknown"
|
|
1849
|
+
};
|
|
1850
|
+
var HOST_LABELS = {
|
|
1851
|
+
"claude-code": "Claude Code",
|
|
1852
|
+
codex: "Codex",
|
|
1853
|
+
cursor: "Cursor",
|
|
1854
|
+
opencode: "OpenCode"
|
|
1855
|
+
};
|
|
1856
|
+
function row(label, value) {
|
|
1857
|
+
return `${pc.cyan(label)} ${pc.white(value)}`;
|
|
1858
|
+
}
|
|
1859
|
+
function formatDetectionSummary(d) {
|
|
1860
|
+
const fw = row("Framework:", FRAMEWORK_LABELS[d.framework]);
|
|
1861
|
+
const hosts = row(
|
|
1862
|
+
"Coding agents:",
|
|
1863
|
+
d.hosts.length > 0 ? d.hosts.map((h) => HOST_LABELS[h]).join(", ") : "none detected"
|
|
1864
|
+
);
|
|
1865
|
+
const vendor = row(
|
|
1866
|
+
"Browser MCP:",
|
|
1867
|
+
d.sightmapAwareVendor ? "Sightmap-aware vendor detected" : "none detected"
|
|
1868
|
+
);
|
|
1869
|
+
const dir = row(
|
|
1870
|
+
".sightmap/:",
|
|
1871
|
+
d.sightmapDir.present ? `present (${d.sightmapDir.viewCount ?? 0} views)` : "not present"
|
|
1872
|
+
);
|
|
1873
|
+
return [fw, hosts, vendor, dir];
|
|
1874
|
+
}
|
|
1875
|
+
function intro2() {
|
|
1876
|
+
clack.intro(pc.cyan("\u25C6 Sightmap") + " curate your view inventory");
|
|
1877
|
+
}
|
|
1878
|
+
function outro2(message) {
|
|
1879
|
+
clack.outro(pc.green(message));
|
|
1880
|
+
}
|
|
1881
|
+
function showDetection(d) {
|
|
1882
|
+
clack.log.message(
|
|
1883
|
+
[pc.bold("Detected"), "", ...formatDetectionSummary(d)].join("\n"),
|
|
1884
|
+
{ symbol: pc.cyan("\u25C7") }
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
function showPluginInstructions(hosts) {
|
|
1888
|
+
const lines = [pc.bold("Next: run these to finish setup"), ""];
|
|
1889
|
+
for (let i = 0; i < hosts.length; i++) {
|
|
1890
|
+
const h = hosts[i];
|
|
1891
|
+
lines.push(...pluginSection(h));
|
|
1892
|
+
if (i < hosts.length - 1) lines.push("");
|
|
1893
|
+
}
|
|
1894
|
+
lines.push("", pc.dim("Then restart your agent."));
|
|
1895
|
+
clack.log.message(lines.join("\n"), { symbol: pc.cyan("\u25C7") });
|
|
1896
|
+
}
|
|
1897
|
+
function pluginSection(host) {
|
|
1898
|
+
switch (host) {
|
|
1899
|
+
case "claude-code":
|
|
1900
|
+
return [
|
|
1901
|
+
pc.cyan("Claude Code"),
|
|
1902
|
+
pc.white(" /plugin marketplace add sightmap/plugin"),
|
|
1903
|
+
pc.white(" /plugin install sightmap@sightmap-marketplace")
|
|
1904
|
+
];
|
|
1905
|
+
case "codex":
|
|
1906
|
+
return [
|
|
1907
|
+
pc.cyan("Codex"),
|
|
1908
|
+
pc.white(" codex plugin marketplace add sightmap/plugin"),
|
|
1909
|
+
pc.white(" codex plugin install sightmap@sightmap-marketplace")
|
|
1910
|
+
];
|
|
1911
|
+
case "cursor":
|
|
1912
|
+
return [
|
|
1913
|
+
pc.cyan("Cursor"),
|
|
1914
|
+
pc.white(" /plugin add sightmap/plugin"),
|
|
1915
|
+
pc.dim(" (in the Cursor command palette)")
|
|
1916
|
+
];
|
|
1917
|
+
case "opencode":
|
|
1918
|
+
return [
|
|
1919
|
+
pc.cyan("OpenCode"),
|
|
1920
|
+
pc.white(" Add to opencode.json plugin array:"),
|
|
1921
|
+
pc.white(' "plugin": ["sightmap@git+https://github.com/sightmap/plugin"]'),
|
|
1922
|
+
pc.dim(" Then restart OpenCode.")
|
|
1923
|
+
];
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
async function selectInstallPath(suggestion = "plugin") {
|
|
1927
|
+
return clack.select({
|
|
1928
|
+
message: "How would you like to install Sightmap?",
|
|
1929
|
+
initialValue: suggestion,
|
|
1930
|
+
options: [
|
|
1931
|
+
{
|
|
1932
|
+
value: "plugin",
|
|
1933
|
+
label: "Plugin (recommended)",
|
|
1934
|
+
hint: "Two copy-paste commands per host. Skills, hooks, MCP \u2014 all together."
|
|
1935
|
+
},
|
|
1936
|
+
{
|
|
1937
|
+
value: "manual",
|
|
1938
|
+
label: "Manual",
|
|
1939
|
+
hint: "Write MCP config + copy skills/hooks into this repo."
|
|
1940
|
+
}
|
|
1941
|
+
]
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
async function selectWorkspace(workspaces) {
|
|
1945
|
+
const options = workspaces.map((w) => ({
|
|
1946
|
+
value: w,
|
|
1947
|
+
label: w.relPath,
|
|
1948
|
+
hint: w.framework === "unknown" ? pc.dim("no framework") : FRAMEWORK_LABELS[w.framework]
|
|
1949
|
+
}));
|
|
1950
|
+
return clack.select({
|
|
1951
|
+
message: "Multiple workspaces found. Which one should we set up?",
|
|
1952
|
+
options,
|
|
1953
|
+
initialValue: workspaces[0]
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
async function selectProviderAction(entryRelPath) {
|
|
1957
|
+
const options = [
|
|
1958
|
+
{ value: "auto", label: "Wrap automatically", hint: "Edit the file in place via codemod." },
|
|
1959
|
+
{ value: "snippet", label: "Show me the snippet to paste", hint: "Print the exact lines; I'll add them myself." },
|
|
1960
|
+
{ value: "skip", label: "Skip", hint: "Already done, or I'll handle it later." }
|
|
1961
|
+
];
|
|
1962
|
+
return clack.select({
|
|
1963
|
+
message: `Add <SightmapProvider> to ${entryRelPath}?`,
|
|
1964
|
+
initialValue: "auto",
|
|
1965
|
+
options
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
function showProviderSnippet(snippet, note) {
|
|
1969
|
+
const header = pc.bold("Add <SightmapProvider> manually");
|
|
1970
|
+
const body = formatSnippet(snippet);
|
|
1971
|
+
const trailer = note ? `
|
|
1972
|
+
|
|
1973
|
+
${pc.dim(note)}` : "";
|
|
1974
|
+
clack.log.message(`${header}
|
|
1975
|
+
|
|
1976
|
+
${body}${trailer}`, { symbol: pc.cyan("\u25C7") });
|
|
1977
|
+
}
|
|
1978
|
+
async function selectHosts(detected, defaults) {
|
|
1979
|
+
const options = ["claude-code", "codex", "cursor", "opencode"].map((h) => ({
|
|
1980
|
+
value: h,
|
|
1981
|
+
label: HOST_LABELS[h] + (detected.includes(h) ? pc.dim(" (detected)") : "")
|
|
1982
|
+
}));
|
|
1983
|
+
return clack.multiselect({
|
|
1984
|
+
message: "Which hosts?",
|
|
1985
|
+
options,
|
|
1986
|
+
initialValues: defaults,
|
|
1987
|
+
required: true
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// src/cli/init/mcp-config/claude-code.ts
|
|
1992
|
+
import { readFile as readFile7, writeFile as writeFile2, access as access5 } from "fs/promises";
|
|
1993
|
+
import { resolve as resolve16 } from "path";
|
|
1994
|
+
async function readJsonIfExists2(path) {
|
|
1995
|
+
try {
|
|
1996
|
+
await access5(path);
|
|
1997
|
+
return JSON.parse(await readFile7(path, "utf8"));
|
|
1998
|
+
} catch {
|
|
1999
|
+
return null;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
async function writeClaudeCodeMcp(opts) {
|
|
2003
|
+
const path = resolve16(opts.cwd, ".mcp.json");
|
|
2004
|
+
const existing = await readJsonIfExists2(path) ?? {};
|
|
2005
|
+
const servers = existing.mcpServers ?? {};
|
|
2006
|
+
const next = {
|
|
2007
|
+
...existing,
|
|
2008
|
+
mcpServers: {
|
|
2009
|
+
...servers,
|
|
2010
|
+
[opts.serverName]: opts.serverDef
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
await writeFile2(path, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// src/cli/init/mcp-config/cursor.ts
|
|
2017
|
+
import { readFile as readFile8, writeFile as writeFile3, access as access6, mkdir } from "fs/promises";
|
|
2018
|
+
import { resolve as resolve17, dirname as dirname2 } from "path";
|
|
2019
|
+
async function writeCursorMcp(opts) {
|
|
2020
|
+
const path = resolve17(opts.cwd, ".cursor/mcp.json");
|
|
2021
|
+
let existing = {};
|
|
2022
|
+
try {
|
|
2023
|
+
await access6(path);
|
|
2024
|
+
existing = JSON.parse(await readFile8(path, "utf8"));
|
|
2025
|
+
} catch {
|
|
2026
|
+
await mkdir(dirname2(path), { recursive: true });
|
|
2027
|
+
}
|
|
2028
|
+
const servers = existing.mcpServers ?? {};
|
|
2029
|
+
const next = {
|
|
2030
|
+
...existing,
|
|
2031
|
+
mcpServers: { ...servers, [opts.serverName]: opts.serverDef }
|
|
2032
|
+
};
|
|
2033
|
+
await writeFile3(path, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// src/cli/init/mcp-config/opencode.ts
|
|
2037
|
+
import { readFile as readFile9, writeFile as writeFile4, access as access7 } from "fs/promises";
|
|
2038
|
+
import { resolve as resolve18 } from "path";
|
|
2039
|
+
async function writeOpenCodeMcp(opts) {
|
|
2040
|
+
const path = resolve18(opts.cwd, "opencode.json");
|
|
2041
|
+
let existing = {};
|
|
2042
|
+
try {
|
|
2043
|
+
await access7(path);
|
|
2044
|
+
existing = JSON.parse(await readFile9(path, "utf8"));
|
|
2045
|
+
} catch {
|
|
2046
|
+
}
|
|
2047
|
+
const mcp = existing.mcp ?? {};
|
|
2048
|
+
const next = {
|
|
2049
|
+
...existing,
|
|
2050
|
+
mcp: { ...mcp, [opts.serverName]: opts.serverDef }
|
|
2051
|
+
};
|
|
2052
|
+
await writeFile4(path, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// src/cli/init/mcp-config/codex.ts
|
|
2056
|
+
import { readFile as readFile10, writeFile as writeFile5, access as access8, mkdir as mkdir2 } from "fs/promises";
|
|
2057
|
+
import { dirname as dirname3 } from "path";
|
|
2058
|
+
import { parse as parse2, stringify } from "smol-toml";
|
|
2059
|
+
async function writeCodexMcp(opts) {
|
|
2060
|
+
let existing = {};
|
|
2061
|
+
try {
|
|
2062
|
+
await access8(opts.configPath);
|
|
2063
|
+
existing = parse2(await readFile10(opts.configPath, "utf8"));
|
|
2064
|
+
} catch {
|
|
2065
|
+
await mkdir2(dirname3(opts.configPath), { recursive: true });
|
|
2066
|
+
}
|
|
2067
|
+
const servers = existing.mcp_servers ?? {};
|
|
2068
|
+
const next = {
|
|
2069
|
+
...existing,
|
|
2070
|
+
mcp_servers: {
|
|
2071
|
+
...servers,
|
|
2072
|
+
[opts.serverName]: opts.serverDef
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
await writeFile5(opts.configPath, stringify(next), "utf8");
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// src/cli/init/tarball.ts
|
|
2079
|
+
import pacote from "pacote";
|
|
2080
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2081
|
+
import { resolve as resolve19 } from "path";
|
|
2082
|
+
async function fetchPluginTarball(opts) {
|
|
2083
|
+
const spec = `@sightmap/plugin@${opts.version}`;
|
|
2084
|
+
await pacote.extract(spec, opts.destDir);
|
|
2085
|
+
const manifestPath = resolve19(opts.destDir, ".claude-plugin/plugin.json");
|
|
2086
|
+
const manifest = JSON.parse(await readFile11(manifestPath, "utf8"));
|
|
2087
|
+
if (!manifest.compatibleMcpVersion) {
|
|
2088
|
+
throw new Error("Plugin tarball missing 'compatibleMcpVersion' in plugin.json");
|
|
2089
|
+
}
|
|
2090
|
+
return {
|
|
2091
|
+
destDir: opts.destDir,
|
|
2092
|
+
compatibleMcpVersion: manifest.compatibleMcpVersion,
|
|
2093
|
+
pluginVersion: manifest.version ?? "0.0.0"
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// src/cli/init/skills-copy.ts
|
|
2098
|
+
import { chmod, cp, mkdir as mkdir3, readFile as readFile12, readdir as readdir6, stat as stat4, writeFile as writeFile6, access as access9 } from "fs/promises";
|
|
2099
|
+
import { resolve as resolve20 } from "path";
|
|
2100
|
+
|
|
2101
|
+
// src/cli/init/settings-merge.ts
|
|
2102
|
+
var PLUGIN_ROOT_TOKEN = /\$\{CLAUDE_PLUGIN_ROOT\}/g;
|
|
2103
|
+
function rewriteHookCommands(hooks, replacement) {
|
|
2104
|
+
const out = {};
|
|
2105
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
2106
|
+
out[event] = groups.map((g) => ({
|
|
2107
|
+
...g.matcher !== void 0 ? { matcher: g.matcher } : {},
|
|
2108
|
+
hooks: g.hooks.map((h) => ({
|
|
2109
|
+
...h,
|
|
2110
|
+
command: h.command.replace(PLUGIN_ROOT_TOKEN, replacement)
|
|
2111
|
+
}))
|
|
2112
|
+
}));
|
|
2113
|
+
}
|
|
2114
|
+
return out;
|
|
2115
|
+
}
|
|
2116
|
+
function mergeHooksIntoSettings(existing, incoming) {
|
|
2117
|
+
const merged = { ...existing };
|
|
2118
|
+
const mergedHooks = { ...existing.hooks ?? {} };
|
|
2119
|
+
for (const [event, incomingGroups] of Object.entries(incoming)) {
|
|
2120
|
+
const existingGroups = mergedHooks[event] ?? [];
|
|
2121
|
+
const combined = [...existingGroups];
|
|
2122
|
+
for (const g of incomingGroups) {
|
|
2123
|
+
if (!hasGroup(combined, g)) combined.push(g);
|
|
2124
|
+
}
|
|
2125
|
+
mergedHooks[event] = combined;
|
|
2126
|
+
}
|
|
2127
|
+
merged.hooks = mergedHooks;
|
|
2128
|
+
return merged;
|
|
2129
|
+
}
|
|
2130
|
+
function hasGroup(groups, candidate) {
|
|
2131
|
+
return groups.some(
|
|
2132
|
+
(g) => (g.matcher ?? "") === (candidate.matcher ?? "") && g.hooks.length === candidate.hooks.length && g.hooks.every((h, i) => h.command === candidate.hooks[i]?.command)
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// src/cli/init/skills-copy.ts
|
|
2137
|
+
async function copyPluginAssets(opts) {
|
|
2138
|
+
const targets = resolveTargets(opts.host, opts.projectDir);
|
|
2139
|
+
await mkdir3(targets.skills, { recursive: true });
|
|
2140
|
+
await cp(resolve20(opts.tarballDir, "skills"), targets.skills, { recursive: true });
|
|
2141
|
+
if (targets.agents) {
|
|
2142
|
+
await mkdir3(targets.agents, { recursive: true });
|
|
2143
|
+
await cp(resolve20(opts.tarballDir, "agents"), targets.agents, { recursive: true });
|
|
2144
|
+
}
|
|
2145
|
+
if (targets.bin) {
|
|
2146
|
+
await mkdir3(targets.bin, { recursive: true });
|
|
2147
|
+
await cp(resolve20(opts.tarballDir, "bin"), targets.bin, { recursive: true });
|
|
2148
|
+
await chmodShellScripts(targets.bin);
|
|
2149
|
+
}
|
|
2150
|
+
if (targets.settingsJson && targets.bin && targets.binEnvPath) {
|
|
2151
|
+
await installHooksIntoSettings({
|
|
2152
|
+
tarballDir: opts.tarballDir,
|
|
2153
|
+
settingsPath: targets.settingsJson,
|
|
2154
|
+
binEnvPath: targets.binEnvPath
|
|
2155
|
+
});
|
|
2156
|
+
} else if (targets.hooksJson) {
|
|
2157
|
+
await installLegacyHooksJson({
|
|
2158
|
+
tarballDir: opts.tarballDir,
|
|
2159
|
+
dstPath: targets.hooksJson,
|
|
2160
|
+
binEnvPath: targets.binEnvPath ?? `\${CLAUDE_PROJECT_DIR}/.claude/sightmap`
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
async function installHooksIntoSettings(opts) {
|
|
2165
|
+
const incomingRaw = JSON.parse(
|
|
2166
|
+
await readFile12(resolve20(opts.tarballDir, "hooks/hooks.json"), "utf8")
|
|
2167
|
+
);
|
|
2168
|
+
const incoming = incomingRaw.hooks ?? {};
|
|
2169
|
+
const rewritten = rewriteHookCommands(incoming, opts.binEnvPath);
|
|
2170
|
+
let existing = {};
|
|
2171
|
+
try {
|
|
2172
|
+
await access9(opts.settingsPath);
|
|
2173
|
+
existing = JSON.parse(await readFile12(opts.settingsPath, "utf8"));
|
|
2174
|
+
} catch {
|
|
2175
|
+
}
|
|
2176
|
+
const merged = mergeHooksIntoSettings(existing, rewritten);
|
|
2177
|
+
await mkdir3(resolve20(opts.settingsPath, ".."), { recursive: true });
|
|
2178
|
+
await writeFile6(opts.settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2179
|
+
}
|
|
2180
|
+
async function installLegacyHooksJson(opts) {
|
|
2181
|
+
const srcRaw = JSON.parse(
|
|
2182
|
+
await readFile12(resolve20(opts.tarballDir, "hooks/hooks.json"), "utf8")
|
|
2183
|
+
);
|
|
2184
|
+
const rewritten = rewriteHookCommands(srcRaw.hooks ?? {}, opts.binEnvPath);
|
|
2185
|
+
let merged = { hooks: rewritten };
|
|
2186
|
+
try {
|
|
2187
|
+
await access9(opts.dstPath);
|
|
2188
|
+
const existing = JSON.parse(await readFile12(opts.dstPath, "utf8"));
|
|
2189
|
+
merged = { hooks: { ...existing.hooks ?? {}, ...rewritten } };
|
|
2190
|
+
} catch {
|
|
2191
|
+
}
|
|
2192
|
+
await mkdir3(resolve20(opts.dstPath, ".."), { recursive: true });
|
|
2193
|
+
await writeFile6(opts.dstPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2194
|
+
}
|
|
2195
|
+
async function chmodShellScripts(dir) {
|
|
2196
|
+
const entries = await readdir6(dir);
|
|
2197
|
+
for (const name of entries) {
|
|
2198
|
+
const p = resolve20(dir, name);
|
|
2199
|
+
const s = await stat4(p);
|
|
2200
|
+
if (s.isDirectory()) {
|
|
2201
|
+
await chmodShellScripts(p);
|
|
2202
|
+
} else if (name.endsWith(".sh")) {
|
|
2203
|
+
await chmod(p, 493);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
function resolveTargets(host, projectDir) {
|
|
2208
|
+
switch (host) {
|
|
2209
|
+
case "claude-code":
|
|
2210
|
+
return {
|
|
2211
|
+
skills: resolve20(projectDir, ".claude/skills/sightmap"),
|
|
2212
|
+
agents: resolve20(projectDir, ".claude/agents"),
|
|
2213
|
+
bin: resolve20(projectDir, ".claude/sightmap/bin"),
|
|
2214
|
+
// Stand-in for CLAUDE_PLUGIN_ROOT — tarball commands are written as
|
|
2215
|
+
// ${CLAUDE_PLUGIN_ROOT}/bin/X.sh, so we replace the plugin root, not the bin dir.
|
|
2216
|
+
binEnvPath: "${CLAUDE_PROJECT_DIR}/.claude/sightmap",
|
|
2217
|
+
settingsJson: resolve20(projectDir, ".claude/settings.json")
|
|
2218
|
+
};
|
|
2219
|
+
case "codex":
|
|
2220
|
+
return {
|
|
2221
|
+
skills: resolve20(projectDir, ".codex/skills/sightmap"),
|
|
2222
|
+
agents: resolve20(projectDir, ".codex/agents"),
|
|
2223
|
+
bin: resolve20(projectDir, ".codex/sightmap/bin"),
|
|
2224
|
+
binEnvPath: "${CLAUDE_PROJECT_DIR}/.codex/sightmap",
|
|
2225
|
+
hooksJson: resolve20(projectDir, ".codex/hooks.json")
|
|
2226
|
+
};
|
|
2227
|
+
case "cursor":
|
|
2228
|
+
return { skills: resolve20(projectDir, ".cursor/rules/sightmap") };
|
|
2229
|
+
case "opencode":
|
|
2230
|
+
return { skills: resolve20(projectDir, ".opencode/plugins/sightmap") };
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
// src/cli/init/version-pin.ts
|
|
2235
|
+
function buildSightmapMcpServerDef(opts) {
|
|
2236
|
+
const args = [
|
|
2237
|
+
"-y",
|
|
2238
|
+
`@sightmap/mcp@${opts.compatibleMcpVersion}`,
|
|
2239
|
+
"--sightmap-dir",
|
|
2240
|
+
opts.sightmapDir,
|
|
2241
|
+
"--curate-root",
|
|
2242
|
+
opts.curateRoot
|
|
2243
|
+
];
|
|
2244
|
+
if (opts.withBrowser) {
|
|
2245
|
+
args.push("--", "npx", "-y", "@playwright/mcp@latest");
|
|
2246
|
+
} else {
|
|
2247
|
+
args.push("--curate-only");
|
|
2248
|
+
}
|
|
2249
|
+
return {
|
|
2250
|
+
command: "npx",
|
|
2251
|
+
args,
|
|
2252
|
+
env: { SIGHTMAP_PLUGIN_VERSION: opts.pluginVersion }
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// src/cli/init/framework-setup/package-manager.ts
|
|
2257
|
+
import { access as access10 } from "fs/promises";
|
|
2258
|
+
import { dirname as dirname4, resolve as resolve21 } from "path";
|
|
2259
|
+
async function exists4(p) {
|
|
2260
|
+
try {
|
|
2261
|
+
await access10(p);
|
|
2262
|
+
return true;
|
|
2263
|
+
} catch {
|
|
2264
|
+
return false;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
var SIGNALS = [
|
|
2268
|
+
{ file: "pnpm-lock.yaml", pm: "pnpm" },
|
|
2269
|
+
{ file: "pnpm-workspace.yaml", pm: "pnpm" },
|
|
2270
|
+
{ file: "yarn.lock", pm: "yarn" }
|
|
2271
|
+
];
|
|
2272
|
+
async function detectPackageManager(cwd) {
|
|
2273
|
+
let dir = resolve21(cwd);
|
|
2274
|
+
while (true) {
|
|
2275
|
+
for (const { file, pm } of SIGNALS) {
|
|
2276
|
+
if (await exists4(resolve21(dir, file))) return pm;
|
|
2277
|
+
}
|
|
2278
|
+
const parent = dirname4(dir);
|
|
2279
|
+
if (parent === dir) return "npm";
|
|
2280
|
+
dir = parent;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// src/cli/init/framework-setup/install-adapter.ts
|
|
2285
|
+
import { spawn } from "child_process";
|
|
2286
|
+
var ADD_VERB = {
|
|
2287
|
+
pnpm: "add",
|
|
2288
|
+
yarn: "add",
|
|
2289
|
+
npm: "install"
|
|
2290
|
+
};
|
|
2291
|
+
async function installAdapter(opts) {
|
|
2292
|
+
await new Promise((resolveP, reject) => {
|
|
2293
|
+
const child = spawn(opts.packageManager, [ADD_VERB[opts.packageManager], opts.packageName], {
|
|
2294
|
+
cwd: opts.cwd,
|
|
2295
|
+
stdio: "inherit"
|
|
2296
|
+
});
|
|
2297
|
+
child.on("error", reject);
|
|
2298
|
+
child.on("exit", (code) => code === 0 ? resolveP() : reject(new Error(`${opts.packageManager} exited with code ${code}`)));
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
// src/cli/init/framework-setup/codegen.ts
|
|
2303
|
+
import { spawn as spawn2 } from "child_process";
|
|
2304
|
+
var REACT_FRAMEWORKS = ["react-vite", "react-cra", "next-app", "next-pages", "react-router-7"];
|
|
2305
|
+
var EXEC_VERB = {
|
|
2306
|
+
pnpm: ["exec"],
|
|
2307
|
+
yarn: ["exec"],
|
|
2308
|
+
npm: ["exec", "--"]
|
|
2309
|
+
};
|
|
2310
|
+
function buildCodegenCommand(opts) {
|
|
2311
|
+
if (!REACT_FRAMEWORKS.includes(opts.framework)) return null;
|
|
2312
|
+
return {
|
|
2313
|
+
command: opts.packageManager,
|
|
2314
|
+
args: [...EXEC_VERB[opts.packageManager], "sightmap-react", "gen", "--router=auto"]
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
async function runCodegen(cwd, cmd) {
|
|
2318
|
+
await new Promise((resolveP, reject) => {
|
|
2319
|
+
const child = spawn2(cmd.command, cmd.args, { cwd, stdio: "inherit" });
|
|
2320
|
+
child.on("error", reject);
|
|
2321
|
+
child.on("exit", (code) => code === 0 ? resolveP() : reject(new Error(`codegen exited ${code}`)));
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// src/cli/init/framework-setup/provider-codemod.ts
|
|
2326
|
+
import * as recast from "recast";
|
|
2327
|
+
import * as parser from "@babel/parser";
|
|
2328
|
+
var babelParser = {
|
|
2329
|
+
parse(source) {
|
|
2330
|
+
return parser.parse(source, {
|
|
2331
|
+
sourceType: "module",
|
|
2332
|
+
tokens: true,
|
|
2333
|
+
plugins: ["jsx", "typescript"]
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
function wrapWithSightmapProvider(source, opts = {}) {
|
|
2338
|
+
if (source.includes("SightmapProvider")) {
|
|
2339
|
+
return { code: source, changed: false, reason: "SightmapProvider already present" };
|
|
2340
|
+
}
|
|
2341
|
+
const ast = recast.parse(source, { parser: babelParser });
|
|
2342
|
+
const b = recast.types.builders;
|
|
2343
|
+
const wrap = (inner) => b.jsxElement(
|
|
2344
|
+
b.jsxOpeningElement(b.jsxIdentifier("SightmapProvider"), [], false),
|
|
2345
|
+
b.jsxClosingElement(b.jsxIdentifier("SightmapProvider")),
|
|
2346
|
+
[stripPrintMetadata(inner)]
|
|
2347
|
+
);
|
|
2348
|
+
let wrapped = false;
|
|
2349
|
+
let reason;
|
|
2350
|
+
recast.types.visit(ast, {
|
|
2351
|
+
visitCallExpression(path) {
|
|
2352
|
+
if (wrapped) return false;
|
|
2353
|
+
const callee = path.node.callee;
|
|
2354
|
+
if (callee.type === "MemberExpression" && callee.property.type === "Identifier" && callee.property.name === "render") {
|
|
2355
|
+
const arg = path.node.arguments[0];
|
|
2356
|
+
if (!arg) {
|
|
2357
|
+
reason = "render() called with no args";
|
|
2358
|
+
return false;
|
|
2359
|
+
}
|
|
2360
|
+
path.node.arguments[0] = wrap(arg);
|
|
2361
|
+
wrapped = true;
|
|
2362
|
+
return false;
|
|
2363
|
+
}
|
|
2364
|
+
this.traverse(path);
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
if (!wrapped && opts.framework === "react-router-7") {
|
|
2368
|
+
wrapped = tryWrapDefaultExport(ast, wrap);
|
|
2369
|
+
if (!wrapped) reason = "no default export returning JSX found in app/root.tsx";
|
|
2370
|
+
}
|
|
2371
|
+
if (!wrapped) {
|
|
2372
|
+
return { code: source, changed: false, reason: reason ?? "no .render() call found" };
|
|
2373
|
+
}
|
|
2374
|
+
const importDecl = b.importDeclaration(
|
|
2375
|
+
[b.importSpecifier(b.identifier("SightmapProvider"), b.identifier("SightmapProvider"))],
|
|
2376
|
+
b.stringLiteral("@sightmap/react")
|
|
2377
|
+
);
|
|
2378
|
+
insertImportAfterHeaderComments(ast, importDecl);
|
|
2379
|
+
return { code: recast.print(ast, { quote: "single" }).code, changed: true };
|
|
2380
|
+
}
|
|
2381
|
+
function insertImportAfterHeaderComments(ast, importDecl) {
|
|
2382
|
+
const body = ast.program.body;
|
|
2383
|
+
const firstStmt = body[0];
|
|
2384
|
+
if (firstStmt) {
|
|
2385
|
+
const newImp = importDecl;
|
|
2386
|
+
if (Array.isArray(firstStmt.leadingComments) && firstStmt.leadingComments.length > 0) {
|
|
2387
|
+
newImp.leadingComments = firstStmt.leadingComments;
|
|
2388
|
+
firstStmt.leadingComments = [];
|
|
2389
|
+
}
|
|
2390
|
+
if (Array.isArray(firstStmt.comments) && firstStmt.comments.length > 0) {
|
|
2391
|
+
newImp.comments = firstStmt.comments;
|
|
2392
|
+
firstStmt.comments = [];
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
body.unshift(importDecl);
|
|
2396
|
+
}
|
|
2397
|
+
function stripPrintMetadata(node) {
|
|
2398
|
+
const n = node;
|
|
2399
|
+
if (n && typeof n === "object") {
|
|
2400
|
+
try {
|
|
2401
|
+
n.original = void 0;
|
|
2402
|
+
} catch {
|
|
2403
|
+
}
|
|
2404
|
+
try {
|
|
2405
|
+
n.loc = void 0;
|
|
2406
|
+
} catch {
|
|
2407
|
+
}
|
|
2408
|
+
if (n.extra) n.extra.parenthesized = false;
|
|
2409
|
+
}
|
|
2410
|
+
return node;
|
|
2411
|
+
}
|
|
2412
|
+
function tryWrapDefaultExport(ast, wrap) {
|
|
2413
|
+
let done = false;
|
|
2414
|
+
recast.types.visit(ast, {
|
|
2415
|
+
visitExportDefaultDeclaration(path) {
|
|
2416
|
+
if (done) return false;
|
|
2417
|
+
const decl = path.node.declaration;
|
|
2418
|
+
const body = decl.body;
|
|
2419
|
+
if ((decl.type === "FunctionDeclaration" || decl.type === "ArrowFunctionExpression" || decl.type === "FunctionExpression") && body && body.type === "BlockStatement" && Array.isArray(body.body)) {
|
|
2420
|
+
for (const stmt of body.body) {
|
|
2421
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
2422
|
+
const arg = stmt.argument;
|
|
2423
|
+
if (arg.type === "JSXElement" || arg.type === "JSXFragment") {
|
|
2424
|
+
stmt.argument = wrap(stmt.argument);
|
|
2425
|
+
done = true;
|
|
2426
|
+
return false;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
if (decl.type === "ArrowFunctionExpression" && body && (body.type === "JSXElement" || body.type === "JSXFragment")) {
|
|
2432
|
+
decl.body = wrap(body);
|
|
2433
|
+
done = true;
|
|
2434
|
+
return false;
|
|
2435
|
+
}
|
|
2436
|
+
return false;
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
return done;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// src/cli/init/existing-corpus.ts
|
|
2443
|
+
function formatExistingCorpusReport(input) {
|
|
2444
|
+
const errors = input.diagnostics.filter((d) => d.severity === "error");
|
|
2445
|
+
const warnings = input.diagnostics.filter((d) => d.severity === "warning");
|
|
2446
|
+
const lines = [];
|
|
2447
|
+
lines.push(`sightmap check passed (${input.viewCount} views, ${errors.length} schema errors)`);
|
|
2448
|
+
if (warnings.length > 0) {
|
|
2449
|
+
const byCode = /* @__PURE__ */ new Map();
|
|
2450
|
+
for (const w of warnings) byCode.set(w.code, (byCode.get(w.code) ?? 0) + 1);
|
|
2451
|
+
const emptyMemory = byCode.get("EMPTY_MEMORY") ?? 0;
|
|
2452
|
+
if (emptyMemory > 0) {
|
|
2453
|
+
lines.push(`${emptyMemory} views have empty memory blocks \u2014 run /sightmap audit for details`);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return lines.join("\n");
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// src/cli/init/index.ts
|
|
2460
|
+
var REACT_FRAMEWORKS2 = ["react-vite", "react-cra", "next-app", "next-pages", "react-router-7"];
|
|
2461
|
+
async function runInit(opts) {
|
|
2462
|
+
intro2();
|
|
2463
|
+
let detect = {
|
|
2464
|
+
framework: await detectFramework(opts.cwd),
|
|
2465
|
+
hosts: await detectHarnesses(opts.cwd),
|
|
2466
|
+
sightmapAwareVendor: false,
|
|
2467
|
+
sightmapDir: await detectSightmapDir(opts.cwd)
|
|
2468
|
+
};
|
|
2469
|
+
detect.sightmapAwareVendor = await detectSightmapAwareVendor(opts.cwd, detect.hosts);
|
|
2470
|
+
showDetection(detect);
|
|
2471
|
+
if (detect.framework === "unknown" && !detect.sightmapDir.present) {
|
|
2472
|
+
const chosen = await maybePickWorkspace(opts);
|
|
2473
|
+
if (typeof chosen === "symbol") return 1;
|
|
2474
|
+
if (chosen) {
|
|
2475
|
+
opts = { ...opts, cwd: chosen.dir };
|
|
2476
|
+
clack2.log.info(`Targeting workspace: ${chosen.relPath}`);
|
|
2477
|
+
detect = {
|
|
2478
|
+
framework: await detectFramework(opts.cwd),
|
|
2479
|
+
hosts: detect.hosts,
|
|
2480
|
+
sightmapAwareVendor: detect.sightmapAwareVendor,
|
|
2481
|
+
sightmapDir: await detectSightmapDir(opts.cwd)
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
if (detect.sightmapDir.present) {
|
|
2486
|
+
await runExistingCorpusFlow(opts.cwd, detect.sightmapDir.viewCount ?? 0);
|
|
2487
|
+
} else {
|
|
2488
|
+
await runFreshFrameworkSetup(opts, detect);
|
|
2489
|
+
}
|
|
2490
|
+
let installPath = opts.installPath;
|
|
2491
|
+
if (!installPath) {
|
|
2492
|
+
if (opts.yes) installPath = "plugin";
|
|
2493
|
+
else {
|
|
2494
|
+
const choice = await selectInstallPath("plugin");
|
|
2495
|
+
if (typeof choice === "symbol") return 1;
|
|
2496
|
+
installPath = choice;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
const hosts = await resolveHosts(opts, detect);
|
|
2500
|
+
if (hosts.length === 0) {
|
|
2501
|
+
outro2("No hosts selected \u2014 nothing to do.");
|
|
2502
|
+
return 0;
|
|
2503
|
+
}
|
|
2504
|
+
const withBrowser = resolveBrowserBundle(opts, detect);
|
|
2505
|
+
if (installPath === "plugin") {
|
|
2506
|
+
showPluginInstructions(hosts);
|
|
2507
|
+
outro2("Run those, then restart your agent.");
|
|
2508
|
+
return 0;
|
|
2509
|
+
}
|
|
2510
|
+
const tarballDir = resolve22(tmpdir(), `sightmap-plugin-${Date.now()}`);
|
|
2511
|
+
await mkdir4(tarballDir, { recursive: true });
|
|
2512
|
+
try {
|
|
2513
|
+
const tar = await fetchPluginTarball({ destDir: tarballDir, version: "latest" });
|
|
2514
|
+
const serverDef = buildSightmapMcpServerDef({
|
|
2515
|
+
compatibleMcpVersion: tar.compatibleMcpVersion,
|
|
2516
|
+
pluginVersion: tar.pluginVersion,
|
|
2517
|
+
withBrowser,
|
|
2518
|
+
sightmapDir: ".sightmap",
|
|
2519
|
+
curateRoot: ".sightmap"
|
|
2520
|
+
});
|
|
2521
|
+
for (const h of hosts) {
|
|
2522
|
+
await writeMcpForHost(h, opts.cwd, serverDef);
|
|
2523
|
+
await copyPluginAssets({ tarballDir, projectDir: opts.cwd, host: h });
|
|
2524
|
+
}
|
|
2525
|
+
} finally {
|
|
2526
|
+
await rm(tarballDir, { recursive: true, force: true });
|
|
2527
|
+
}
|
|
2528
|
+
outro2("Done. Restart your agent to pick up the new MCP.");
|
|
2529
|
+
return 0;
|
|
2530
|
+
}
|
|
2531
|
+
async function runFreshFrameworkSetup(opts, detect) {
|
|
2532
|
+
if (!REACT_FRAMEWORKS2.includes(detect.framework)) {
|
|
2533
|
+
await mkdir4(resolve22(opts.cwd, ".sightmap"), { recursive: true });
|
|
2534
|
+
await writeFile7(
|
|
2535
|
+
resolve22(opts.cwd, ".sightmap/app.yaml"),
|
|
2536
|
+
`version: 1
|
|
2537
|
+
views: []
|
|
2538
|
+
`,
|
|
2539
|
+
"utf8"
|
|
2540
|
+
);
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const pm = await detectPackageManager(opts.cwd);
|
|
2544
|
+
const spinner2 = clack2.spinner();
|
|
2545
|
+
spinner2.start(`Installing @sightmap/react via ${pm}`);
|
|
2546
|
+
await installAdapter({ cwd: opts.cwd, packageManager: pm, packageName: "@sightmap/react" });
|
|
2547
|
+
spinner2.stop("Installed @sightmap/react");
|
|
2548
|
+
if (!opts.noProvider) {
|
|
2549
|
+
await runProviderStep(opts, detect);
|
|
2550
|
+
}
|
|
2551
|
+
if (!opts.noCodegen) {
|
|
2552
|
+
const cmd = buildCodegenCommand({ framework: detect.framework, packageManager: pm });
|
|
2553
|
+
if (cmd) {
|
|
2554
|
+
spinner2.start("Running sightmap-react gen");
|
|
2555
|
+
await runCodegen(opts.cwd, cmd);
|
|
2556
|
+
spinner2.stop("Scaffolded .sightmap/");
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
async function runProviderStep(opts, detect) {
|
|
2561
|
+
const entry = await findAppEntry(opts.cwd, detect.framework);
|
|
2562
|
+
if (!entry) {
|
|
2563
|
+
const snippet = renderProviderSnippet({
|
|
2564
|
+
framework: detect.framework,
|
|
2565
|
+
cwd: opts.cwd,
|
|
2566
|
+
entryAbsPath: "your app's root component file"
|
|
2567
|
+
});
|
|
2568
|
+
showProviderSnippet(snippet, "We couldn't locate the entry file \u2014 drop these lines into your app's root.");
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
const source = await readFile13(entry, "utf8");
|
|
2572
|
+
const result = wrapWithSightmapProvider(source, { framework: detect.framework });
|
|
2573
|
+
if (!result.changed) {
|
|
2574
|
+
const snippet = renderProviderSnippet({
|
|
2575
|
+
framework: detect.framework,
|
|
2576
|
+
cwd: opts.cwd,
|
|
2577
|
+
entryAbsPath: entry
|
|
2578
|
+
});
|
|
2579
|
+
const note = result.reason ? `Codemod skipped: ${result.reason}.` : void 0;
|
|
2580
|
+
showProviderSnippet(snippet, note);
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
if (opts.yes) {
|
|
2584
|
+
await writeFile7(entry, result.code, "utf8");
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const entryRel = relativePath(opts.cwd, entry);
|
|
2588
|
+
const choice = await selectProviderAction(entryRel);
|
|
2589
|
+
if (choice === "auto") {
|
|
2590
|
+
await writeFile7(entry, result.code, "utf8");
|
|
2591
|
+
} else if (choice === "snippet") {
|
|
2592
|
+
const snippet = renderProviderSnippet({
|
|
2593
|
+
framework: detect.framework,
|
|
2594
|
+
cwd: opts.cwd,
|
|
2595
|
+
entryAbsPath: entry
|
|
2596
|
+
});
|
|
2597
|
+
showProviderSnippet(snippet);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
function relativePath(cwd, abs) {
|
|
2601
|
+
if (abs.startsWith(cwd + "/")) return abs.slice(cwd.length + 1);
|
|
2602
|
+
return abs;
|
|
2603
|
+
}
|
|
2604
|
+
async function findAppEntry(cwd, framework) {
|
|
2605
|
+
const candidates = {
|
|
2606
|
+
"react-vite": ["src/main.tsx", "src/main.jsx", "src/index.tsx", "src/index.jsx"],
|
|
2607
|
+
"react-cra": ["src/index.tsx", "src/index.jsx"],
|
|
2608
|
+
"next-app": ["app/layout.tsx", "app/layout.jsx"],
|
|
2609
|
+
"next-pages": ["pages/_app.tsx", "pages/_app.jsx"],
|
|
2610
|
+
"react-router-7": ["app/root.tsx", "app/root.jsx", "src/root.tsx"],
|
|
2611
|
+
unknown: []
|
|
2612
|
+
};
|
|
2613
|
+
for (const rel of candidates[framework]) {
|
|
2614
|
+
const abs = resolve22(cwd, rel);
|
|
2615
|
+
try {
|
|
2616
|
+
await readFile13(abs, "utf8");
|
|
2617
|
+
return abs;
|
|
2618
|
+
} catch {
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
return null;
|
|
2622
|
+
}
|
|
2623
|
+
async function runExistingCorpusFlow(cwd, viewCount) {
|
|
2624
|
+
const spinner2 = clack2.spinner();
|
|
2625
|
+
spinner2.start("Auditing existing corpus...");
|
|
2626
|
+
await runLint({ path: ".sightmap", cwd, json: false, strict: false });
|
|
2627
|
+
spinner2.stop("Audit complete");
|
|
2628
|
+
const header = `${viewCount} view${viewCount === 1 ? "" : "s"}`;
|
|
2629
|
+
const body = formatExistingCorpusReport({ viewCount, diagnostics: [] });
|
|
2630
|
+
clack2.log.message(`${header}
|
|
2631
|
+
|
|
2632
|
+
${body}`);
|
|
2633
|
+
}
|
|
2634
|
+
async function resolveHosts(opts, detect) {
|
|
2635
|
+
if (opts.hosts) {
|
|
2636
|
+
const valid = /* @__PURE__ */ new Set(["claude-code", "codex", "cursor", "opencode"]);
|
|
2637
|
+
const invalid = opts.hosts.filter((h) => !valid.has(h));
|
|
2638
|
+
if (invalid.length > 0) {
|
|
2639
|
+
throw new Error(
|
|
2640
|
+
`Unknown host(s): ${invalid.join(", ")}. Valid: claude-code, codex, cursor, opencode`
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2643
|
+
return opts.hosts;
|
|
2644
|
+
}
|
|
2645
|
+
if (opts.yes) return detect.hosts.length > 0 ? detect.hosts : ["claude-code"];
|
|
2646
|
+
const defaults = detect.hosts.length > 0 ? detect.hosts : ["claude-code"];
|
|
2647
|
+
const choice = await selectHosts(detect.hosts, defaults);
|
|
2648
|
+
if (typeof choice === "symbol") return [];
|
|
2649
|
+
return choice;
|
|
2650
|
+
}
|
|
2651
|
+
function resolveBrowserBundle(opts, detect) {
|
|
2652
|
+
if (opts.withBrowser) return true;
|
|
2653
|
+
if (opts.curateOnly) return false;
|
|
2654
|
+
return !detect.sightmapAwareVendor;
|
|
2655
|
+
}
|
|
2656
|
+
async function maybePickWorkspace(opts) {
|
|
2657
|
+
const all = await enumerateWorkspaces({ root: opts.cwd });
|
|
2658
|
+
if (all.length === 0) return null;
|
|
2659
|
+
const withFramework = all.filter((w) => w.framework !== "unknown");
|
|
2660
|
+
if (withFramework.length === 0) return null;
|
|
2661
|
+
if (opts.yes) {
|
|
2662
|
+
if (withFramework.length === 1) return withFramework[0];
|
|
2663
|
+
clack2.log.warn(
|
|
2664
|
+
`Found ${withFramework.length} workspaces with a framework. Re-run from one of them, or without -y to pick interactively.`
|
|
2665
|
+
);
|
|
2666
|
+
return null;
|
|
2667
|
+
}
|
|
2668
|
+
return selectWorkspace(withFramework);
|
|
2669
|
+
}
|
|
2670
|
+
async function writeMcpForHost(host, cwd, serverDef) {
|
|
2671
|
+
const common = { cwd, serverName: "sightmap", serverDef };
|
|
2672
|
+
if (host === "claude-code") return writeClaudeCodeMcp(common);
|
|
2673
|
+
if (host === "cursor") return writeCursorMcp(common);
|
|
2674
|
+
if (host === "opencode") return writeOpenCodeMcp(common);
|
|
2675
|
+
if (host === "codex") {
|
|
2676
|
+
return writeCodexMcp({
|
|
2677
|
+
configPath: resolve22(process.env.HOME ?? cwd, ".codex/config.toml"),
|
|
2678
|
+
serverName: "sightmap",
|
|
2679
|
+
serverDef
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
|
|
1500
2684
|
// src/cli/index.ts
|
|
1501
2685
|
var program = new Command();
|
|
1502
2686
|
program.name("sightmap").description("CLI for the Sightmap spec \u2014 validate, lint, match, explain.").version("0.1.0");
|
|
@@ -1590,6 +2774,20 @@ program.command("fmt [path]").description("Canonicalize .sightmap/*.yaml files (
|
|
|
1590
2774
|
process.exit(code);
|
|
1591
2775
|
}
|
|
1592
2776
|
);
|
|
2777
|
+
program.command("init").description("Initialize Sightmap in this project (detects framework + coding agent, writes config).").option("--yes", "accept all defaults (non-interactive)").option("--plugin", "force plugin install path").option("--manual", "force manual install path").option("--host <names>", "comma-separated hosts: claude-code,codex,cursor,opencode").option("--with-browser", "force bundled Playwright + browser tools").option("--curate-only", "force curation-only (skip browser tools)").option("--no-codegen", "skip sightmap-react gen").option("--no-provider", "skip <SightmapProvider> codemod").action(async (opts) => {
|
|
2778
|
+
const code = await runInit({
|
|
2779
|
+
cwd: program.opts().cwd,
|
|
2780
|
+
yes: opts.yes === true,
|
|
2781
|
+
...opts.plugin ? { installPath: "plugin" } : {},
|
|
2782
|
+
...opts.manual ? { installPath: "manual" } : {},
|
|
2783
|
+
...opts.host !== void 0 ? { hosts: opts.host.split(",").map((s) => s.trim()) } : {},
|
|
2784
|
+
...opts.withBrowser ? { withBrowser: true } : {},
|
|
2785
|
+
...opts.curateOnly ? { curateOnly: true } : {},
|
|
2786
|
+
...opts.codegen === false ? { noCodegen: true } : {},
|
|
2787
|
+
...opts.provider === false ? { noProvider: true } : {}
|
|
2788
|
+
});
|
|
2789
|
+
process.exit(code);
|
|
2790
|
+
});
|
|
1593
2791
|
program.parseAsync(process.argv).catch((err) => {
|
|
1594
2792
|
process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}
|
|
1595
2793
|
`);
|