@sightmap/sightmap 0.4.0 → 0.5.1

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/cli/index.js CHANGED
@@ -1498,10 +1498,10 @@ async function processFile(absPath, relPath, opts, fileResults, diagnostics) {
1498
1498
  }
1499
1499
 
1500
1500
  // src/cli/init/index.ts
1501
- import { resolve as resolve21 } from "path";
1502
- import { mkdir as mkdir4, readFile as readFile12, rm, writeFile as writeFile7 } from "fs/promises";
1501
+ import { resolve as resolve22 } from "path";
1502
+ import { mkdir as mkdir4, readFile as readFile14, rm, writeFile as writeFile7 } from "fs/promises";
1503
1503
  import { tmpdir } from "os";
1504
- import * as clack2 from "@clack/prompts";
1504
+ import * as clack3 from "@clack/prompts";
1505
1505
 
1506
1506
  // src/cli/init/detect/framework.ts
1507
1507
  import { readFile as readFile4, access } from "fs/promises";
@@ -1625,9 +1625,220 @@ async function detectSightmapDir(cwd) {
1625
1625
  return { present: true, viewCount: yamls.length };
1626
1626
  }
1627
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
+
1628
1757
  // src/cli/init/tui.ts
1629
1758
  import * as clack from "@clack/prompts";
1630
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
1631
1842
  var FRAMEWORK_LABELS = {
1632
1843
  "react-vite": "React (Vite)",
1633
1844
  "react-cra": "React (CRA)",
@@ -1642,11 +1853,23 @@ var HOST_LABELS = {
1642
1853
  cursor: "Cursor",
1643
1854
  opencode: "OpenCode"
1644
1855
  };
1856
+ function row(label, value) {
1857
+ return `${pc.cyan(label)} ${pc.white(value)}`;
1858
+ }
1645
1859
  function formatDetectionSummary(d) {
1646
- const fw = `Framework: ${FRAMEWORK_LABELS[d.framework]}`;
1647
- const hosts = d.hosts.length > 0 ? `Coding agents: ${d.hosts.map((h) => HOST_LABELS[h]).join(", ")}` : "Coding agents: none detected";
1648
- const vendor = d.sightmapAwareVendor ? "Browser MCP: Sightmap-aware vendor detected" : "Browser MCP: none detected";
1649
- const dir = d.sightmapDir.present ? `.sightmap/: present (${d.sightmapDir.viewCount ?? 0} views)` : ".sightmap/: not present";
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
+ );
1650
1873
  return [fw, hosts, vendor, dir];
1651
1874
  }
1652
1875
  function intro2() {
@@ -1656,7 +1879,49 @@ function outro2(message) {
1656
1879
  clack.outro(pc.green(message));
1657
1880
  }
1658
1881
  function showDetection(d) {
1659
- clack.note(formatDetectionSummary(d).join("\n"), pc.dim("Detected"));
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
+ }
1660
1925
  }
1661
1926
  async function selectInstallPath(suggestion = "plugin") {
1662
1927
  return clack.select({
@@ -1676,6 +1941,40 @@ async function selectInstallPath(suggestion = "plugin") {
1676
1941
  ]
1677
1942
  });
1678
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
+ }
1679
1978
  async function selectHosts(detected, defaults) {
1680
1979
  const options = ["claude-code", "codex", "cursor", "opencode"].map((h) => ({
1681
1980
  value: h,
@@ -1690,18 +1989,18 @@ async function selectHosts(detected, defaults) {
1690
1989
  }
1691
1990
 
1692
1991
  // src/cli/init/mcp-config/claude-code.ts
1693
- import { readFile as readFile6, writeFile as writeFile2, access as access5 } from "fs/promises";
1694
- import { resolve as resolve15 } from "path";
1992
+ import { readFile as readFile7, writeFile as writeFile2, access as access5 } from "fs/promises";
1993
+ import { resolve as resolve16 } from "path";
1695
1994
  async function readJsonIfExists2(path) {
1696
1995
  try {
1697
1996
  await access5(path);
1698
- return JSON.parse(await readFile6(path, "utf8"));
1997
+ return JSON.parse(await readFile7(path, "utf8"));
1699
1998
  } catch {
1700
1999
  return null;
1701
2000
  }
1702
2001
  }
1703
2002
  async function writeClaudeCodeMcp(opts) {
1704
- const path = resolve15(opts.cwd, ".mcp.json");
2003
+ const path = resolve16(opts.cwd, ".mcp.json");
1705
2004
  const existing = await readJsonIfExists2(path) ?? {};
1706
2005
  const servers = existing.mcpServers ?? {};
1707
2006
  const next = {
@@ -1715,14 +2014,14 @@ async function writeClaudeCodeMcp(opts) {
1715
2014
  }
1716
2015
 
1717
2016
  // src/cli/init/mcp-config/cursor.ts
1718
- import { readFile as readFile7, writeFile as writeFile3, access as access6, mkdir } from "fs/promises";
1719
- import { resolve as resolve16, dirname as dirname2 } from "path";
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";
1720
2019
  async function writeCursorMcp(opts) {
1721
- const path = resolve16(opts.cwd, ".cursor/mcp.json");
2020
+ const path = resolve17(opts.cwd, ".cursor/mcp.json");
1722
2021
  let existing = {};
1723
2022
  try {
1724
2023
  await access6(path);
1725
- existing = JSON.parse(await readFile7(path, "utf8"));
2024
+ existing = JSON.parse(await readFile8(path, "utf8"));
1726
2025
  } catch {
1727
2026
  await mkdir(dirname2(path), { recursive: true });
1728
2027
  }
@@ -1735,14 +2034,14 @@ async function writeCursorMcp(opts) {
1735
2034
  }
1736
2035
 
1737
2036
  // src/cli/init/mcp-config/opencode.ts
1738
- import { readFile as readFile8, writeFile as writeFile4, access as access7 } from "fs/promises";
1739
- import { resolve as resolve17 } from "path";
2037
+ import { readFile as readFile9, writeFile as writeFile4, access as access7 } from "fs/promises";
2038
+ import { resolve as resolve18 } from "path";
1740
2039
  async function writeOpenCodeMcp(opts) {
1741
- const path = resolve17(opts.cwd, "opencode.json");
2040
+ const path = resolve18(opts.cwd, "opencode.json");
1742
2041
  let existing = {};
1743
2042
  try {
1744
2043
  await access7(path);
1745
- existing = JSON.parse(await readFile8(path, "utf8"));
2044
+ existing = JSON.parse(await readFile9(path, "utf8"));
1746
2045
  } catch {
1747
2046
  }
1748
2047
  const mcp = existing.mcp ?? {};
@@ -1754,14 +2053,14 @@ async function writeOpenCodeMcp(opts) {
1754
2053
  }
1755
2054
 
1756
2055
  // src/cli/init/mcp-config/codex.ts
1757
- import { readFile as readFile9, writeFile as writeFile5, access as access8, mkdir as mkdir2 } from "fs/promises";
2056
+ import { readFile as readFile10, writeFile as writeFile5, access as access8, mkdir as mkdir2 } from "fs/promises";
1758
2057
  import { dirname as dirname3 } from "path";
1759
2058
  import { parse as parse2, stringify } from "smol-toml";
1760
2059
  async function writeCodexMcp(opts) {
1761
2060
  let existing = {};
1762
2061
  try {
1763
2062
  await access8(opts.configPath);
1764
- existing = parse2(await readFile9(opts.configPath, "utf8"));
2063
+ existing = parse2(await readFile10(opts.configPath, "utf8"));
1765
2064
  } catch {
1766
2065
  await mkdir2(dirname3(opts.configPath), { recursive: true });
1767
2066
  }
@@ -1778,13 +2077,13 @@ async function writeCodexMcp(opts) {
1778
2077
 
1779
2078
  // src/cli/init/tarball.ts
1780
2079
  import pacote from "pacote";
1781
- import { readFile as readFile10 } from "fs/promises";
1782
- import { resolve as resolve18 } from "path";
2080
+ import { readFile as readFile11 } from "fs/promises";
2081
+ import { resolve as resolve19 } from "path";
1783
2082
  async function fetchPluginTarball(opts) {
1784
2083
  const spec = `@sightmap/plugin@${opts.version}`;
1785
2084
  await pacote.extract(spec, opts.destDir);
1786
- const manifestPath = resolve18(opts.destDir, ".claude-plugin/plugin.json");
1787
- const manifest = JSON.parse(await readFile10(manifestPath, "utf8"));
2085
+ const manifestPath = resolve19(opts.destDir, ".claude-plugin/plugin.json");
2086
+ const manifest = JSON.parse(await readFile11(manifestPath, "utf8"));
1788
2087
  if (!manifest.compatibleMcpVersion) {
1789
2088
  throw new Error("Plugin tarball missing 'compatibleMcpVersion' in plugin.json");
1790
2089
  }
@@ -1796,56 +2095,148 @@ async function fetchPluginTarball(opts) {
1796
2095
  }
1797
2096
 
1798
2097
  // src/cli/init/skills-copy.ts
1799
- import { cp, mkdir as mkdir3, readFile as readFile11, writeFile as writeFile6, access as access9 } from "fs/promises";
1800
- import { resolve as resolve19 } from "path";
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
1801
2137
  async function copyPluginAssets(opts) {
1802
2138
  const targets = resolveTargets(opts.host, opts.projectDir);
1803
2139
  await mkdir3(targets.skills, { recursive: true });
1804
- await cp(resolve19(opts.tarballDir, "skills"), targets.skills, { recursive: true });
2140
+ await cp(resolve20(opts.tarballDir, "skills"), targets.skills, { recursive: true });
1805
2141
  if (targets.agents) {
1806
2142
  await mkdir3(targets.agents, { recursive: true });
1807
- await cp(resolve19(opts.tarballDir, "agents"), 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
+ });
1808
2162
  }
1809
- if (targets.hooksJson) {
1810
- const srcPath = resolve19(opts.tarballDir, "hooks/hooks.json");
1811
- const dstPath = targets.hooksJson;
1812
- let merged;
1813
- try {
1814
- await access9(dstPath);
1815
- const existing = JSON.parse(await readFile11(dstPath, "utf8"));
1816
- const incoming = JSON.parse(await readFile11(srcPath, "utf8"));
1817
- merged = { ...existing, ...incoming };
1818
- } catch {
1819
- merged = JSON.parse(await readFile11(srcPath, "utf8"));
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);
1820
2204
  }
1821
- await writeFile6(dstPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
1822
2205
  }
1823
2206
  }
1824
2207
  function resolveTargets(host, projectDir) {
1825
2208
  switch (host) {
1826
2209
  case "claude-code":
1827
2210
  return {
1828
- skills: resolve19(projectDir, ".claude/skills/sightmap"),
1829
- agents: resolve19(projectDir, ".claude/agents"),
1830
- hooksJson: resolve19(projectDir, ".claude/hooks.json")
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")
1831
2218
  };
1832
2219
  case "codex":
1833
2220
  return {
1834
- skills: resolve19(projectDir, ".codex/skills/sightmap"),
1835
- agents: resolve19(projectDir, ".codex/agents"),
1836
- hooksJson: resolve19(projectDir, ".codex/hooks.json")
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")
1837
2226
  };
1838
2227
  case "cursor":
1839
- return { skills: resolve19(projectDir, ".cursor/rules/sightmap") };
2228
+ return { skills: resolve20(projectDir, ".cursor/rules/sightmap") };
1840
2229
  case "opencode":
1841
- return { skills: resolve19(projectDir, ".opencode/plugins/sightmap") };
2230
+ return { skills: resolve20(projectDir, ".opencode/plugins/sightmap") };
1842
2231
  }
1843
2232
  }
1844
2233
 
1845
2234
  // src/cli/init/version-pin.ts
2235
+ var DEFAULT_LAUNCHER = { pm: "npm", command: "npx", runArgs: ["-y"] };
1846
2236
  function buildSightmapMcpServerDef(opts) {
2237
+ const launcher = opts.launcher ?? DEFAULT_LAUNCHER;
1847
2238
  const args = [
1848
- "-y",
2239
+ ...launcher.runArgs,
1849
2240
  `@sightmap/mcp@${opts.compatibleMcpVersion}`,
1850
2241
  "--sightmap-dir",
1851
2242
  opts.sightmapDir,
@@ -1853,35 +2244,152 @@ function buildSightmapMcpServerDef(opts) {
1853
2244
  opts.curateRoot
1854
2245
  ];
1855
2246
  if (opts.withBrowser) {
1856
- args.push("--", "npx", "-y", "@playwright/mcp@latest");
2247
+ args.push("--", launcher.command, ...launcher.runArgs, "@playwright/mcp@latest");
1857
2248
  } else {
1858
2249
  args.push("--curate-only");
1859
2250
  }
1860
2251
  return {
1861
- command: "npx",
2252
+ command: launcher.command,
1862
2253
  args,
1863
2254
  env: { SIGHTMAP_PLUGIN_VERSION: opts.pluginVersion }
1864
2255
  };
1865
2256
  }
1866
2257
 
1867
- // src/cli/init/plugin-path.ts
1868
- var SECTIONS = {
1869
- "claude-code": "Claude Code:\n /plugin marketplace add sightmap/plugin\n /plugin install sightmap@sightmap-marketplace",
1870
- codex: "Codex:\n codex plugin marketplace add sightmap/plugin\n codex plugin install sightmap@sightmap-marketplace",
1871
- cursor: "Cursor:\n /plugin add sightmap/plugin\n (in the Cursor command palette)",
1872
- opencode: 'OpenCode:\n Add to opencode.json plugin array:\n "plugin": ["sightmap@git+https://github.com/sightmap/plugin"]\n Then restart OpenCode.'
2258
+ // src/cli/init/smoke-mcp.ts
2259
+ import { spawn } from "child_process";
2260
+ import * as clack2 from "@clack/prompts";
2261
+ import pc2 from "picocolors";
2262
+ var DEFAULT_TIMEOUT_MS = 15e3;
2263
+ var INITIALIZE_PAYLOAD = {
2264
+ jsonrpc: "2.0",
2265
+ id: 1,
2266
+ method: "initialize",
2267
+ params: {
2268
+ protocolVersion: "2024-11-05",
2269
+ capabilities: {},
2270
+ clientInfo: { name: "sightmap-init-smoke", version: "1.0.0" }
2271
+ }
1873
2272
  };
1874
- function renderPluginInstructions(hosts) {
1875
- const head = "Run these to finish setup:\n";
1876
- const body = hosts.map((h) => SECTIONS[h]).join("\n\n");
1877
- const foot = "\n\nThen restart your agent.";
1878
- return head + "\n" + body + foot;
2273
+ async function runSmokeTest(opts) {
2274
+ const startedAt = Date.now();
2275
+ const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2276
+ return new Promise((resolveResult) => {
2277
+ let settled = false;
2278
+ const settle = (r) => {
2279
+ if (settled) return;
2280
+ settled = true;
2281
+ try {
2282
+ child.kill("SIGTERM");
2283
+ } catch {
2284
+ }
2285
+ resolveResult(r);
2286
+ };
2287
+ const child = spawn(opts.serverDef.command, opts.serverDef.args, {
2288
+ cwd: opts.cwd,
2289
+ env: { ...process.env, ...opts.serverDef.env },
2290
+ stdio: ["pipe", "pipe", "pipe"]
2291
+ });
2292
+ let stdoutBuf = "";
2293
+ let stderrBuf = "";
2294
+ child.stdout?.on("data", (chunk) => {
2295
+ stdoutBuf += chunk.toString("utf8");
2296
+ let nl;
2297
+ while ((nl = stdoutBuf.indexOf("\n")) !== -1) {
2298
+ const line = stdoutBuf.slice(0, nl).trim();
2299
+ stdoutBuf = stdoutBuf.slice(nl + 1);
2300
+ if (!line) continue;
2301
+ try {
2302
+ const msg = JSON.parse(line);
2303
+ if (msg.id === 1 && msg.result) {
2304
+ settle({ ok: true, latencyMs: Date.now() - startedAt });
2305
+ return;
2306
+ }
2307
+ if (msg.id === 1 && msg.error) {
2308
+ settle({
2309
+ ok: false,
2310
+ reason: `Server returned JSON-RPC error: ${JSON.stringify(msg.error)}`,
2311
+ latencyMs: Date.now() - startedAt
2312
+ });
2313
+ return;
2314
+ }
2315
+ } catch {
2316
+ }
2317
+ }
2318
+ });
2319
+ child.stderr?.on("data", (chunk) => {
2320
+ stderrBuf += chunk.toString("utf8");
2321
+ });
2322
+ child.on("error", (err) => {
2323
+ settle({
2324
+ ok: false,
2325
+ reason: `Failed to spawn: ${err.message}`,
2326
+ latencyMs: Date.now() - startedAt,
2327
+ ...stderrBuf ? { stderr: stderrBuf } : {}
2328
+ });
2329
+ });
2330
+ child.on("exit", (code, signal) => {
2331
+ if (settled) return;
2332
+ settle({
2333
+ ok: false,
2334
+ reason: stdoutBuf.length === 0 && stderrBuf.length === 0 ? `Spawn produced no output and exited (code=${code ?? "?"}, signal=${signal ?? "?"}). This is the silent-stdin bug \u2014 try a different package manager launcher.` : `Process exited before initialize response (code=${code ?? "?"}, signal=${signal ?? "?"})`,
2335
+ latencyMs: Date.now() - startedAt,
2336
+ ...stderrBuf ? { stderr: stderrBuf } : {}
2337
+ });
2338
+ });
2339
+ setTimeout(() => {
2340
+ settle({
2341
+ ok: false,
2342
+ reason: `Timed out after ${timeout}ms waiting for initialize response`,
2343
+ latencyMs: Date.now() - startedAt,
2344
+ ...stderrBuf ? { stderr: stderrBuf } : {}
2345
+ });
2346
+ }, timeout);
2347
+ try {
2348
+ child.stdin?.write(JSON.stringify(INITIALIZE_PAYLOAD) + "\n");
2349
+ child.stdin?.end();
2350
+ } catch (e) {
2351
+ settle({
2352
+ ok: false,
2353
+ reason: `Failed to write initialize payload: ${e.message}`,
2354
+ latencyMs: Date.now() - startedAt
2355
+ });
2356
+ }
2357
+ });
2358
+ }
2359
+ async function smokeTestMcp(opts) {
2360
+ const spinner3 = clack2.spinner();
2361
+ spinner3.start("Verifying MCP can launch");
2362
+ const result = await runSmokeTest(opts);
2363
+ if (result.ok) {
2364
+ spinner3.stop(`MCP ready (initialized in ${result.latencyMs}ms)`);
2365
+ return;
2366
+ }
2367
+ spinner3.stop("MCP smoke test failed");
2368
+ const cmd = `${opts.serverDef.command} ${opts.serverDef.args.join(" ")}`;
2369
+ const lines = [
2370
+ pc2.bold("Could not start the MCP server."),
2371
+ "",
2372
+ pc2.cyan("Reason:") + " " + result.reason,
2373
+ pc2.cyan("Command:") + " " + pc2.white(cmd)
2374
+ ];
2375
+ if (result.stderr) {
2376
+ lines.push("", pc2.cyan("stderr:"));
2377
+ for (const ln of result.stderr.trim().split("\n").slice(0, 6)) {
2378
+ lines.push(" " + pc2.dim(ln));
2379
+ }
2380
+ }
2381
+ lines.push(
2382
+ "",
2383
+ pc2.dim("The config was written. You can retry by restarting your agent."),
2384
+ pc2.dim("Re-run `npx @sightmap/sightmap init --no-smoke` to skip this check next time.")
2385
+ );
2386
+ clack2.log.warn(lines.join("\n"));
1879
2387
  }
1880
2388
 
1881
2389
  // src/cli/init/framework-setup/package-manager.ts
1882
- import { access as access10 } from "fs/promises";
1883
- import { resolve as resolve20 } from "path";
1884
- async function exists3(p) {
2390
+ import { access as access10, readFile as readFile13 } from "fs/promises";
2391
+ import { dirname as dirname4, resolve as resolve21 } from "path";
2392
+ async function exists4(p) {
1885
2393
  try {
1886
2394
  await access10(p);
1887
2395
  return true;
@@ -1889,22 +2397,75 @@ async function exists3(p) {
1889
2397
  return false;
1890
2398
  }
1891
2399
  }
2400
+ var SIGNALS = [
2401
+ // Bun first — bun.lock can coexist with package-lock.json during migration.
2402
+ { file: "bun.lock", pm: "bun" },
2403
+ { file: "bun.lockb", pm: "bun" },
2404
+ { file: "pnpm-lock.yaml", pm: "pnpm" },
2405
+ { file: "pnpm-workspace.yaml", pm: "pnpm" },
2406
+ { file: "yarn.lock", pm: "yarn" }
2407
+ ];
1892
2408
  async function detectPackageManager(cwd) {
1893
- if (await exists3(resolve20(cwd, "pnpm-lock.yaml"))) return "pnpm";
1894
- if (await exists3(resolve20(cwd, "yarn.lock"))) return "yarn";
1895
- return "npm";
2409
+ let dir = resolve21(cwd);
2410
+ while (true) {
2411
+ for (const { file, pm } of SIGNALS) {
2412
+ if (await exists4(resolve21(dir, file))) return pm;
2413
+ }
2414
+ const parent = dirname4(dir);
2415
+ if (parent === dir) return "npm";
2416
+ dir = parent;
2417
+ }
2418
+ }
2419
+ async function detectLauncher(cwd) {
2420
+ const pm = await detectPackageManager(cwd);
2421
+ if (pm === "bun") return { pm, command: "bunx", runArgs: [] };
2422
+ if (pm === "pnpm") return { pm, command: "pnpm", runArgs: ["dlx"] };
2423
+ if (pm === "yarn") {
2424
+ if (await isYarnBerry(cwd)) {
2425
+ return { pm, command: "yarn", runArgs: ["dlx"] };
2426
+ }
2427
+ return { pm: "npm", command: "npx", runArgs: ["-y"] };
2428
+ }
2429
+ return { pm: "npm", command: "npx", runArgs: ["-y"] };
2430
+ }
2431
+ async function isYarnBerry(cwd) {
2432
+ let dir = resolve21(cwd);
2433
+ while (true) {
2434
+ if (await exists4(resolve21(dir, ".yarnrc.yml"))) return true;
2435
+ const parent = dirname4(dir);
2436
+ if (parent === dir) break;
2437
+ dir = parent;
2438
+ }
2439
+ dir = resolve21(cwd);
2440
+ while (true) {
2441
+ const pkg = resolve21(dir, "package.json");
2442
+ if (await exists4(pkg)) {
2443
+ try {
2444
+ const parsed = JSON.parse(await readFile13(pkg, "utf8"));
2445
+ const pmField = parsed.packageManager ?? "";
2446
+ const match2 = /^yarn@(\d+)\./.exec(pmField);
2447
+ if (match2 && Number(match2[1]) >= 2) return true;
2448
+ } catch {
2449
+ }
2450
+ }
2451
+ const parent = dirname4(dir);
2452
+ if (parent === dir) break;
2453
+ dir = parent;
2454
+ }
2455
+ return false;
1896
2456
  }
1897
2457
 
1898
2458
  // src/cli/init/framework-setup/install-adapter.ts
1899
- import { spawn } from "child_process";
2459
+ import { spawn as spawn2 } from "child_process";
1900
2460
  var ADD_VERB = {
1901
2461
  pnpm: "add",
1902
2462
  yarn: "add",
1903
- npm: "install"
2463
+ npm: "install",
2464
+ bun: "add"
1904
2465
  };
1905
2466
  async function installAdapter(opts) {
1906
2467
  await new Promise((resolveP, reject) => {
1907
- const child = spawn(opts.packageManager, [ADD_VERB[opts.packageManager], opts.packageName], {
2468
+ const child = spawn2(opts.packageManager, [ADD_VERB[opts.packageManager], opts.packageName], {
1908
2469
  cwd: opts.cwd,
1909
2470
  stdio: "inherit"
1910
2471
  });
@@ -1914,23 +2475,24 @@ async function installAdapter(opts) {
1914
2475
  }
1915
2476
 
1916
2477
  // src/cli/init/framework-setup/codegen.ts
1917
- import { spawn as spawn2 } from "child_process";
2478
+ import { spawn as spawn3 } from "child_process";
1918
2479
  var REACT_FRAMEWORKS = ["react-vite", "react-cra", "next-app", "next-pages", "react-router-7"];
1919
2480
  var EXEC_VERB = {
1920
2481
  pnpm: ["exec"],
1921
2482
  yarn: ["exec"],
1922
- npm: ["exec", "--"]
2483
+ npm: ["exec", "--"],
2484
+ bun: ["x"]
1923
2485
  };
1924
2486
  function buildCodegenCommand(opts) {
1925
2487
  if (!REACT_FRAMEWORKS.includes(opts.framework)) return null;
1926
2488
  return {
1927
2489
  command: opts.packageManager,
1928
- args: [...EXEC_VERB[opts.packageManager], "sightmap-react", "gen"]
2490
+ args: [...EXEC_VERB[opts.packageManager], "sightmap-react", "gen", "--router=auto"]
1929
2491
  };
1930
2492
  }
1931
2493
  async function runCodegen(cwd, cmd) {
1932
2494
  await new Promise((resolveP, reject) => {
1933
- const child = spawn2(cmd.command, cmd.args, { cwd, stdio: "inherit" });
2495
+ const child = spawn3(cmd.command, cmd.args, { cwd, stdio: "inherit" });
1934
2496
  child.on("error", reject);
1935
2497
  child.on("exit", (code) => code === 0 ? resolveP() : reject(new Error(`codegen exited ${code}`)));
1936
2498
  });
@@ -1948,43 +2510,109 @@ var babelParser = {
1948
2510
  });
1949
2511
  }
1950
2512
  };
1951
- function wrapWithSightmapProvider(source) {
2513
+ function wrapWithSightmapProvider(source, opts = {}) {
1952
2514
  if (source.includes("SightmapProvider")) {
1953
2515
  return { code: source, changed: false, reason: "SightmapProvider already present" };
1954
2516
  }
1955
2517
  const ast = recast.parse(source, { parser: babelParser });
1956
2518
  const b = recast.types.builders;
1957
- let renderCall = null;
1958
- let renderArgIndex = -1;
2519
+ const wrap = (inner) => b.jsxElement(
2520
+ b.jsxOpeningElement(b.jsxIdentifier("SightmapProvider"), [], false),
2521
+ b.jsxClosingElement(b.jsxIdentifier("SightmapProvider")),
2522
+ [stripPrintMetadata(inner)]
2523
+ );
2524
+ let wrapped = false;
2525
+ let reason;
1959
2526
  recast.types.visit(ast, {
1960
2527
  visitCallExpression(path) {
2528
+ if (wrapped) return false;
1961
2529
  const callee = path.node.callee;
1962
2530
  if (callee.type === "MemberExpression" && callee.property.type === "Identifier" && callee.property.name === "render") {
1963
- renderCall = path.node;
1964
- renderArgIndex = 0;
2531
+ const arg = path.node.arguments[0];
2532
+ if (!arg) {
2533
+ reason = "render() called with no args";
2534
+ return false;
2535
+ }
2536
+ path.node.arguments[0] = wrap(arg);
2537
+ wrapped = true;
1965
2538
  return false;
1966
2539
  }
1967
2540
  this.traverse(path);
1968
2541
  }
1969
2542
  });
1970
- if (!renderCall) {
1971
- return { code: source, changed: false, reason: "no .render() call found" };
2543
+ if (!wrapped && opts.framework === "react-router-7") {
2544
+ wrapped = tryWrapDefaultExport(ast, wrap);
2545
+ if (!wrapped) reason = "no default export returning JSX found in app/root.tsx";
2546
+ }
2547
+ if (!wrapped) {
2548
+ return { code: source, changed: false, reason: reason ?? "no .render() call found" };
1972
2549
  }
1973
- const arg = renderCall.arguments[renderArgIndex];
1974
- if (!arg) return { code: source, changed: false, reason: "render() called with no args" };
1975
- const wrapped = b.jsxElement(
1976
- b.jsxOpeningElement(b.jsxIdentifier("SightmapProvider"), [], false),
1977
- b.jsxClosingElement(b.jsxIdentifier("SightmapProvider")),
1978
- [arg]
1979
- );
1980
- renderCall.arguments[renderArgIndex] = wrapped;
1981
2550
  const importDecl = b.importDeclaration(
1982
2551
  [b.importSpecifier(b.identifier("SightmapProvider"), b.identifier("SightmapProvider"))],
1983
2552
  b.stringLiteral("@sightmap/react")
1984
2553
  );
1985
- ast.program.body.unshift(importDecl);
1986
- const output = recast.print(ast, { quote: "single" }).code;
1987
- return { code: output, changed: true };
2554
+ insertImportAfterHeaderComments(ast, importDecl);
2555
+ return { code: recast.print(ast, { quote: "single" }).code, changed: true };
2556
+ }
2557
+ function insertImportAfterHeaderComments(ast, importDecl) {
2558
+ const body = ast.program.body;
2559
+ const firstStmt = body[0];
2560
+ if (firstStmt) {
2561
+ const newImp = importDecl;
2562
+ if (Array.isArray(firstStmt.leadingComments) && firstStmt.leadingComments.length > 0) {
2563
+ newImp.leadingComments = firstStmt.leadingComments;
2564
+ firstStmt.leadingComments = [];
2565
+ }
2566
+ if (Array.isArray(firstStmt.comments) && firstStmt.comments.length > 0) {
2567
+ newImp.comments = firstStmt.comments;
2568
+ firstStmt.comments = [];
2569
+ }
2570
+ }
2571
+ body.unshift(importDecl);
2572
+ }
2573
+ function stripPrintMetadata(node) {
2574
+ const n = node;
2575
+ if (n && typeof n === "object") {
2576
+ try {
2577
+ n.original = void 0;
2578
+ } catch {
2579
+ }
2580
+ try {
2581
+ n.loc = void 0;
2582
+ } catch {
2583
+ }
2584
+ if (n.extra) n.extra.parenthesized = false;
2585
+ }
2586
+ return node;
2587
+ }
2588
+ function tryWrapDefaultExport(ast, wrap) {
2589
+ let done = false;
2590
+ recast.types.visit(ast, {
2591
+ visitExportDefaultDeclaration(path) {
2592
+ if (done) return false;
2593
+ const decl = path.node.declaration;
2594
+ const body = decl.body;
2595
+ if ((decl.type === "FunctionDeclaration" || decl.type === "ArrowFunctionExpression" || decl.type === "FunctionExpression") && body && body.type === "BlockStatement" && Array.isArray(body.body)) {
2596
+ for (const stmt of body.body) {
2597
+ if (stmt.type === "ReturnStatement" && stmt.argument) {
2598
+ const arg = stmt.argument;
2599
+ if (arg.type === "JSXElement" || arg.type === "JSXFragment") {
2600
+ stmt.argument = wrap(stmt.argument);
2601
+ done = true;
2602
+ return false;
2603
+ }
2604
+ }
2605
+ }
2606
+ }
2607
+ if (decl.type === "ArrowFunctionExpression" && body && (body.type === "JSXElement" || body.type === "JSXFragment")) {
2608
+ decl.body = wrap(body);
2609
+ done = true;
2610
+ return false;
2611
+ }
2612
+ return false;
2613
+ }
2614
+ });
2615
+ return done;
1988
2616
  }
1989
2617
 
1990
2618
  // src/cli/init/existing-corpus.ts
@@ -2008,7 +2636,7 @@ function formatExistingCorpusReport(input) {
2008
2636
  var REACT_FRAMEWORKS2 = ["react-vite", "react-cra", "next-app", "next-pages", "react-router-7"];
2009
2637
  async function runInit(opts) {
2010
2638
  intro2();
2011
- const detect = {
2639
+ let detect = {
2012
2640
  framework: await detectFramework(opts.cwd),
2013
2641
  hosts: await detectHarnesses(opts.cwd),
2014
2642
  sightmapAwareVendor: false,
@@ -2016,6 +2644,20 @@ async function runInit(opts) {
2016
2644
  };
2017
2645
  detect.sightmapAwareVendor = await detectSightmapAwareVendor(opts.cwd, detect.hosts);
2018
2646
  showDetection(detect);
2647
+ if (detect.framework === "unknown" && !detect.sightmapDir.present) {
2648
+ const chosen = await maybePickWorkspace(opts);
2649
+ if (typeof chosen === "symbol") return 1;
2650
+ if (chosen) {
2651
+ opts = { ...opts, cwd: chosen.dir };
2652
+ clack3.log.info(`Targeting workspace: ${chosen.relPath}`);
2653
+ detect = {
2654
+ framework: await detectFramework(opts.cwd),
2655
+ hosts: detect.hosts,
2656
+ sightmapAwareVendor: detect.sightmapAwareVendor,
2657
+ sightmapDir: await detectSightmapDir(opts.cwd)
2658
+ };
2659
+ }
2660
+ }
2019
2661
  if (detect.sightmapDir.present) {
2020
2662
  await runExistingCorpusFlow(opts.cwd, detect.sightmapDir.viewCount ?? 0);
2021
2663
  } else {
@@ -2037,25 +2679,30 @@ async function runInit(opts) {
2037
2679
  }
2038
2680
  const withBrowser = resolveBrowserBundle(opts, detect);
2039
2681
  if (installPath === "plugin") {
2040
- clack2.note(renderPluginInstructions(hosts), "Next");
2682
+ showPluginInstructions(hosts);
2041
2683
  outro2("Run those, then restart your agent.");
2042
2684
  return 0;
2043
2685
  }
2044
- const tarballDir = resolve21(tmpdir(), `sightmap-plugin-${Date.now()}`);
2686
+ const tarballDir = resolve22(tmpdir(), `sightmap-plugin-${Date.now()}`);
2045
2687
  await mkdir4(tarballDir, { recursive: true });
2046
2688
  try {
2047
2689
  const tar = await fetchPluginTarball({ destDir: tarballDir, version: "latest" });
2690
+ const launcher = await detectLauncher(opts.cwd);
2048
2691
  const serverDef = buildSightmapMcpServerDef({
2049
2692
  compatibleMcpVersion: tar.compatibleMcpVersion,
2050
2693
  pluginVersion: tar.pluginVersion,
2051
2694
  withBrowser,
2052
2695
  sightmapDir: ".sightmap",
2053
- curateRoot: ".sightmap"
2696
+ curateRoot: ".sightmap",
2697
+ launcher
2054
2698
  });
2055
2699
  for (const h of hosts) {
2056
2700
  await writeMcpForHost(h, opts.cwd, serverDef);
2057
2701
  await copyPluginAssets({ tarballDir, projectDir: opts.cwd, host: h });
2058
2702
  }
2703
+ if (!opts.noSmoke) {
2704
+ await smokeTestMcp({ serverDef, cwd: opts.cwd });
2705
+ }
2059
2706
  } finally {
2060
2707
  await rm(tarballDir, { recursive: true, force: true });
2061
2708
  }
@@ -2064,9 +2711,9 @@ async function runInit(opts) {
2064
2711
  }
2065
2712
  async function runFreshFrameworkSetup(opts, detect) {
2066
2713
  if (!REACT_FRAMEWORKS2.includes(detect.framework)) {
2067
- await mkdir4(resolve21(opts.cwd, ".sightmap"), { recursive: true });
2714
+ await mkdir4(resolve22(opts.cwd, ".sightmap"), { recursive: true });
2068
2715
  await writeFile7(
2069
- resolve21(opts.cwd, ".sightmap/app.yaml"),
2716
+ resolve22(opts.cwd, ".sightmap/app.yaml"),
2070
2717
  `version: 1
2071
2718
  views: []
2072
2719
  `,
@@ -2075,35 +2722,66 @@ views: []
2075
2722
  return;
2076
2723
  }
2077
2724
  const pm = await detectPackageManager(opts.cwd);
2078
- const spinner2 = clack2.spinner();
2079
- spinner2.start(`Installing @sightmap/react via ${pm}`);
2725
+ const spinner3 = clack3.spinner();
2726
+ spinner3.start(`Installing @sightmap/react via ${pm}`);
2080
2727
  await installAdapter({ cwd: opts.cwd, packageManager: pm, packageName: "@sightmap/react" });
2081
- spinner2.stop("Installed @sightmap/react");
2728
+ spinner3.stop("Installed @sightmap/react");
2082
2729
  if (!opts.noProvider) {
2083
- const entry = await findAppEntry(opts.cwd, detect.framework);
2084
- if (entry) {
2085
- const source = await readFile12(entry, "utf8");
2086
- const result = wrapWithSightmapProvider(source);
2087
- if (result.changed) {
2088
- if (opts.yes || await confirmCodemod(entry)) {
2089
- await writeFile7(entry, result.code, "utf8");
2090
- }
2091
- } else if (result.reason) {
2092
- clack2.log.warn(`SightmapProvider codemod skipped: ${result.reason}. Add manually to ${entry}.`);
2093
- }
2094
- } else {
2095
- clack2.log.warn("Could not locate an app entry file. Add <SightmapProvider> manually.");
2096
- }
2730
+ await runProviderStep(opts, detect);
2097
2731
  }
2098
2732
  if (!opts.noCodegen) {
2099
2733
  const cmd = buildCodegenCommand({ framework: detect.framework, packageManager: pm });
2100
2734
  if (cmd) {
2101
- spinner2.start("Running sightmap-react gen");
2735
+ spinner3.start("Running sightmap-react gen");
2102
2736
  await runCodegen(opts.cwd, cmd);
2103
- spinner2.stop("Scaffolded .sightmap/");
2737
+ spinner3.stop("Scaffolded .sightmap/");
2104
2738
  }
2105
2739
  }
2106
2740
  }
2741
+ async function runProviderStep(opts, detect) {
2742
+ const entry = await findAppEntry(opts.cwd, detect.framework);
2743
+ if (!entry) {
2744
+ const snippet = renderProviderSnippet({
2745
+ framework: detect.framework,
2746
+ cwd: opts.cwd,
2747
+ entryAbsPath: "your app's root component file"
2748
+ });
2749
+ showProviderSnippet(snippet, "We couldn't locate the entry file \u2014 drop these lines into your app's root.");
2750
+ return;
2751
+ }
2752
+ const source = await readFile14(entry, "utf8");
2753
+ const result = wrapWithSightmapProvider(source, { framework: detect.framework });
2754
+ if (!result.changed) {
2755
+ const snippet = renderProviderSnippet({
2756
+ framework: detect.framework,
2757
+ cwd: opts.cwd,
2758
+ entryAbsPath: entry
2759
+ });
2760
+ const note = result.reason ? `Codemod skipped: ${result.reason}.` : void 0;
2761
+ showProviderSnippet(snippet, note);
2762
+ return;
2763
+ }
2764
+ if (opts.yes) {
2765
+ await writeFile7(entry, result.code, "utf8");
2766
+ return;
2767
+ }
2768
+ const entryRel = relativePath(opts.cwd, entry);
2769
+ const choice = await selectProviderAction(entryRel);
2770
+ if (choice === "auto") {
2771
+ await writeFile7(entry, result.code, "utf8");
2772
+ } else if (choice === "snippet") {
2773
+ const snippet = renderProviderSnippet({
2774
+ framework: detect.framework,
2775
+ cwd: opts.cwd,
2776
+ entryAbsPath: entry
2777
+ });
2778
+ showProviderSnippet(snippet);
2779
+ }
2780
+ }
2781
+ function relativePath(cwd, abs) {
2782
+ if (abs.startsWith(cwd + "/")) return abs.slice(cwd.length + 1);
2783
+ return abs;
2784
+ }
2107
2785
  async function findAppEntry(cwd, framework) {
2108
2786
  const candidates = {
2109
2787
  "react-vite": ["src/main.tsx", "src/main.jsx", "src/index.tsx", "src/index.jsx"],
@@ -2114,28 +2792,25 @@ async function findAppEntry(cwd, framework) {
2114
2792
  unknown: []
2115
2793
  };
2116
2794
  for (const rel of candidates[framework]) {
2117
- const abs = resolve21(cwd, rel);
2795
+ const abs = resolve22(cwd, rel);
2118
2796
  try {
2119
- await readFile12(abs, "utf8");
2797
+ await readFile14(abs, "utf8");
2120
2798
  return abs;
2121
2799
  } catch {
2122
2800
  }
2123
2801
  }
2124
2802
  return null;
2125
2803
  }
2126
- async function confirmCodemod(entry) {
2127
- const choice = await clack2.confirm({
2128
- message: `Wrap ${entry} with <SightmapProvider>?`,
2129
- initialValue: true
2130
- });
2131
- return typeof choice === "boolean" ? choice : false;
2132
- }
2133
2804
  async function runExistingCorpusFlow(cwd, viewCount) {
2134
- const spinner2 = clack2.spinner();
2135
- spinner2.start("Auditing existing corpus...");
2805
+ const spinner3 = clack3.spinner();
2806
+ spinner3.start("Auditing existing corpus...");
2136
2807
  await runLint({ path: ".sightmap", cwd, json: false, strict: false });
2137
- spinner2.stop("Audit complete");
2138
- clack2.note(formatExistingCorpusReport({ viewCount, diagnostics: [] }), `${viewCount} view${viewCount === 1 ? "" : "s"}`);
2808
+ spinner3.stop("Audit complete");
2809
+ const header = `${viewCount} view${viewCount === 1 ? "" : "s"}`;
2810
+ const body = formatExistingCorpusReport({ viewCount, diagnostics: [] });
2811
+ clack3.log.message(`${header}
2812
+
2813
+ ${body}`);
2139
2814
  }
2140
2815
  async function resolveHosts(opts, detect) {
2141
2816
  if (opts.hosts) {
@@ -2159,6 +2834,20 @@ function resolveBrowserBundle(opts, detect) {
2159
2834
  if (opts.curateOnly) return false;
2160
2835
  return !detect.sightmapAwareVendor;
2161
2836
  }
2837
+ async function maybePickWorkspace(opts) {
2838
+ const all = await enumerateWorkspaces({ root: opts.cwd });
2839
+ if (all.length === 0) return null;
2840
+ const withFramework = all.filter((w) => w.framework !== "unknown");
2841
+ if (withFramework.length === 0) return null;
2842
+ if (opts.yes) {
2843
+ if (withFramework.length === 1) return withFramework[0];
2844
+ clack3.log.warn(
2845
+ `Found ${withFramework.length} workspaces with a framework. Re-run from one of them, or without -y to pick interactively.`
2846
+ );
2847
+ return null;
2848
+ }
2849
+ return selectWorkspace(withFramework);
2850
+ }
2162
2851
  async function writeMcpForHost(host, cwd, serverDef) {
2163
2852
  const common = { cwd, serverName: "sightmap", serverDef };
2164
2853
  if (host === "claude-code") return writeClaudeCodeMcp(common);
@@ -2166,7 +2855,7 @@ async function writeMcpForHost(host, cwd, serverDef) {
2166
2855
  if (host === "opencode") return writeOpenCodeMcp(common);
2167
2856
  if (host === "codex") {
2168
2857
  return writeCodexMcp({
2169
- configPath: resolve21(process.env.HOME ?? cwd, ".codex/config.toml"),
2858
+ configPath: resolve22(process.env.HOME ?? cwd, ".codex/config.toml"),
2170
2859
  serverName: "sightmap",
2171
2860
  serverDef
2172
2861
  });
@@ -2266,7 +2955,7 @@ program.command("fmt [path]").description("Canonicalize .sightmap/*.yaml files (
2266
2955
  process.exit(code);
2267
2956
  }
2268
2957
  );
2269
- 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) => {
2958
+ 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").option("--no-smoke", "skip the MCP launch smoke test (CI / headless use)").action(async (opts) => {
2270
2959
  const code = await runInit({
2271
2960
  cwd: program.opts().cwd,
2272
2961
  yes: opts.yes === true,
@@ -2276,7 +2965,8 @@ program.command("init").description("Initialize Sightmap in this project (detect
2276
2965
  ...opts.withBrowser ? { withBrowser: true } : {},
2277
2966
  ...opts.curateOnly ? { curateOnly: true } : {},
2278
2967
  ...opts.codegen === false ? { noCodegen: true } : {},
2279
- ...opts.provider === false ? { noProvider: true } : {}
2968
+ ...opts.provider === false ? { noProvider: true } : {},
2969
+ ...opts.smoke === false ? { noSmoke: true } : {}
2280
2970
  });
2281
2971
  process.exit(code);
2282
2972
  });