@overscore/cli 0.13.14 → 0.13.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -913,7 +913,7 @@ function formatCell(value) {
913
913
  }
914
914
  async function pull(slug) {
915
915
  if (!slug) {
916
- console.error("\n Usage: npx @overscore/cli pull <dashboard-slug> [--version N]\n");
916
+ console.error("\n Usage: npx @overscore/cli pull <dashboard-slug> [--version N] [--force]\n");
917
917
  console.error(" Example:");
918
918
  console.error(" npx @overscore/cli pull my-dashboard\n");
919
919
  process.exit(1);
@@ -923,35 +923,47 @@ async function pull(slug) {
923
923
  const versionParam = versionIndex !== -1 && process.argv[versionIndex + 1]
924
924
  ? process.argv[versionIndex + 1]
925
925
  : null;
926
- // Resolve API key: device token auto-create os_ key, else prompt
926
+ // --force skips the "directory exists, overwrite?" confirm needed for non-interactive
927
+ // re-pulls (scripted rebuilds / `workspace sync`). Source is versioned in the Hub, and the
928
+ // clean step below already preserves node_modules + .env, so overwriting is safe.
929
+ const force = process.argv.includes("--force");
930
+ // Resolve API key: device token → auto-create os_ key, else the saved project key, else prompt.
927
931
  let apiKey = "";
928
932
  const configPath = path.join(process.env.HOME || "", ".overscore", "config");
929
- if (fs.existsSync(configPath)) {
930
- const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
931
- const deviceToken = cfg.OVERSCORE_DEVICE_TOKEN;
932
- if (deviceToken) {
933
- try {
934
- const keyRes = await fetch(`${HUB_URL}/api/projects/${slug}/api-keys`, {
935
- method: "POST",
936
- headers: {
937
- Authorization: `Bearer ${deviceToken}`,
938
- "Content-Type": "application/json",
939
- },
940
- body: JSON.stringify({ name: `${slug} pull key` }),
941
- });
942
- if (keyRes.status === 401 || keyRes.status === 403) {
943
- console.error("\n Error: Device token is invalid or expired. Run 'npx @overscore/cli auth login' to re-authenticate.\n");
944
- process.exit(1);
945
- }
946
- if (keyRes.ok) {
947
- const keyData = (await keyRes.json());
948
- apiKey = keyData.raw_key || "";
949
- }
933
+ const cfg = fs.existsSync(configPath)
934
+ ? parseEnv(fs.readFileSync(configPath, "utf-8"))
935
+ : {};
936
+ const deviceToken = cfg.OVERSCORE_DEVICE_TOKEN;
937
+ if (deviceToken) {
938
+ try {
939
+ const keyRes = await fetch(`${HUB_URL}/api/projects/${slug}/api-keys`, {
940
+ method: "POST",
941
+ headers: {
942
+ Authorization: `Bearer ${deviceToken}`,
943
+ "Content-Type": "application/json",
944
+ },
945
+ body: JSON.stringify({ name: `${slug} pull key` }),
946
+ });
947
+ if (keyRes.status === 401 || keyRes.status === 403) {
948
+ console.error("\n Error: Device token is invalid or expired. Run 'npx @overscore/cli auth login' to re-authenticate.\n");
949
+ process.exit(1);
950
950
  }
951
- catch {
952
- // Network error fall through to prompt
951
+ if (keyRes.ok) {
952
+ const keyData = (await keyRes.json());
953
+ apiKey = keyData.raw_key || "";
953
954
  }
954
955
  }
956
+ catch {
957
+ // Network error — fall through to the saved key / prompt
958
+ }
959
+ }
960
+ // The mint above targets /api/projects/<slug>, but `slug` is a DASHBOARD slug — for any
961
+ // project whose name differs from the dashboard (i.e. any multi-dashboard project) that
962
+ // 404s and leaves apiKey empty. Fall back to the project API key saved by `auth login`:
963
+ // it authorizes every dashboard in the logged-in project, which is the common pull case
964
+ // (and what makes non-interactive `pull` / scripted rebuilds work).
965
+ if (!apiKey && cfg.OVERSCORE_API_KEY) {
966
+ apiKey = cfg.OVERSCORE_API_KEY;
955
967
  }
956
968
  if (!apiKey) {
957
969
  console.log(" Tip: Run 'npx @overscore/cli auth login' to authenticate once and skip this step.");
@@ -989,7 +1001,7 @@ async function pull(slug) {
989
1001
  const zipBuffer = Buffer.from(await res.arrayBuffer());
990
1002
  // Extract to ./<slug>/
991
1003
  const targetDir = path.resolve(process.cwd(), slug);
992
- if (fs.existsSync(targetDir)) {
1004
+ if (fs.existsSync(targetDir) && !force) {
993
1005
  const yes = await confirm(`Directory "${slug}" already exists. Overwrite source files?`);
994
1006
  if (!yes) {
995
1007
  console.log(" Cancelled.\n");
@@ -1608,6 +1620,37 @@ function decidePlatformWrite(fullPath, serverContent, serverHash, lastSyncedHash
1608
1620
  }
1609
1621
  return { action: "skip", reason: "user-modified" };
1610
1622
  }
1623
+ // The Hub platform-rules channel currently serves dashboard content (React
1624
+ // rules, deploy-dashboard/start-dashboard skills, and dashboard flavors of
1625
+ // skills the analysis template ships its own versions of). Syncing it into
1626
+ // an analysis misleads Claude and marks the analysis's own skills as
1627
+ // "user-modified" — so analyses only receive the artifact-neutral files.
1628
+ const ANALYSIS_PLATFORM_ALLOW = [
1629
+ "platform-capabilities.md",
1630
+ "skills/update-platform/",
1631
+ ];
1632
+ function isAnalysisDir(targetDir) {
1633
+ if (fs.existsSync(path.join(targetDir, ".overscore", "analysis.json"))) {
1634
+ return true;
1635
+ }
1636
+ // Pulled bundles don't include .overscore/ — fall back to the CLAUDE.md
1637
+ // heading, the same detection syncClaudeMdImports uses.
1638
+ const claudeMdPath = path.join(targetDir, ".claude", "CLAUDE.md");
1639
+ if (!fs.existsSync(claudeMdPath))
1640
+ return false;
1641
+ return /^#\s+Overscore\s+Analysis/im.test(fs.readFileSync(claudeMdPath, "utf-8"));
1642
+ }
1643
+ function filterPlatformFilesForArtifact(targetDir, files) {
1644
+ if (!isAnalysisDir(targetDir))
1645
+ return files;
1646
+ const kept = {};
1647
+ for (const [relPath, content] of Object.entries(files)) {
1648
+ if (ANALYSIS_PLATFORM_ALLOW.some((p) => relPath === p || relPath.startsWith(p))) {
1649
+ kept[relPath] = content;
1650
+ }
1651
+ }
1652
+ return kept;
1653
+ }
1611
1654
  /**
1612
1655
  * Fetch the latest platform-owned Claude Code rules from the Hub and write
1613
1656
  * them into the project's .claude/ directory. Called from `deploy` (before
@@ -1615,6 +1658,7 @@ function decidePlatformWrite(fullPath, serverContent, serverHash, lastSyncedHash
1615
1658
  * paths). User-owned files (knowledge/, this-dashboard.md) are NOT touched.
1616
1659
  *
1617
1660
  * Preserves user edits via .claude/.platform-meta.json hash tracking.
1661
+ * Analyses only receive the artifact-neutral subset (see ANALYSIS_PLATFORM_ALLOW).
1618
1662
  */
1619
1663
  async function syncPlatformRules(targetDir, baseUrl, apiKey) {
1620
1664
  try {
@@ -1626,10 +1670,11 @@ async function syncPlatformRules(targetDir, baseUrl, apiKey) {
1626
1670
  const data = (await res.json());
1627
1671
  if (!data.files)
1628
1672
  return;
1673
+ const files = filterPlatformFilesForArtifact(targetDir, data.files);
1629
1674
  const meta = readPlatformMeta(targetDir);
1630
1675
  const written = [];
1631
1676
  const preserved = [];
1632
- for (const [relPath, content] of Object.entries(data.files)) {
1677
+ for (const [relPath, content] of Object.entries(files)) {
1633
1678
  const fullPath = path.join(targetDir, ".claude", relPath);
1634
1679
  const serverHash = data.hashes?.[relPath] ?? md5(content);
1635
1680
  const lastSyncedHash = meta.last_synced[relPath] ?? null;
@@ -1695,10 +1740,11 @@ async function platformUpdate(checkOnly = false, force = false) {
1695
1740
  console.error("\n Error: No files in platform-rules response.\n");
1696
1741
  process.exit(1);
1697
1742
  }
1743
+ const files = filterPlatformFilesForArtifact(process.cwd(), data.files);
1698
1744
  const meta = readPlatformMeta(process.cwd());
1699
1745
  const updated = [];
1700
1746
  const preserved = [];
1701
- for (const [relPath, content] of Object.entries(data.files)) {
1747
+ for (const [relPath, content] of Object.entries(files)) {
1702
1748
  const fullPath = path.join(process.cwd(), ".claude", relPath);
1703
1749
  const serverHash = data.hashes?.[relPath] ?? md5(content);
1704
1750
  const lastSyncedHash = meta.last_synced[relPath] ?? null;
@@ -1937,6 +1983,9 @@ const ANALYSIS_PUBLISH_BASELINE_EXCLUDE = new Set([
1937
1983
  ".git",
1938
1984
  ".DS_Store",
1939
1985
  ".overscore",
1986
+ // Raw query-result cache (real warehouse rows) — never publishable, even
1987
+ // if .overscoreignore is edited. Mirrors the dashboard deploy SOURCE_EXCLUDE.
1988
+ "data",
1940
1989
  ]);
1941
1990
  function isBaselineExcluded(name) {
1942
1991
  if (ANALYSIS_PUBLISH_BASELINE_EXCLUDE.has(name))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overscore/cli",
3
- "version": "0.13.14",
3
+ "version": "0.13.16",
4
4
  "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"
@@ -11,7 +11,7 @@ paths:
11
11
 
12
12
  - **Inline all CSS.** No `<link rel="stylesheet">`, no Tailwind CDN, no Google Fonts (or use a `data:` URI / system stack). Use a single `<style>` block in the `<head>`.
13
13
  - **Bake the data into the HTML.** No `fetch()`, no `XMLHttpRequest`, no API calls. The numbers must be visible to anyone who opens the file with no logins, no tokens, no broken external dependencies.
14
- - **Charts are inline.** Inline SVG is the safest option. Embedded base64 PNGs are OK. A self-contained chart library (Recharts/D3 from CDN) is acceptable **only if the data is already baked into the page** and the script tag is inline-loaded — but inline SVG is preferred because it survives indefinitely.
14
+ - **Charts are inline.** Inline SVG is the safest option. Embedded base64 PNGs are OK. **No CDN or external `<script src>` — the published report is served under a CSP that blocks all external scripts**, so a CDN-loaded chart library works in local preview but silently breaks after publish. If you must use a JS chart library, paste its source into an inline `<script>` block — but inline SVG is preferred because it survives indefinitely.
15
15
  - **No `useQuery`, no `@overscore/client`, no React.** This is a static HTML file, not a React app.
16
16
 
17
17
  ## Aesthetic direction