@overscore/cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -295,7 +295,7 @@ async function queryList() {
295
295
  if (data.queries.length === 0) {
296
296
  console.log(" No queries registered yet.\n");
297
297
  console.log(" Add one with:");
298
- console.log(' npx overscore query add <name> "SELECT ..."\n');
298
+ console.log(' npx @overscore/cli query add <name> "SELECT ..."\n');
299
299
  return;
300
300
  }
301
301
  for (const q of data.queries) {
@@ -311,7 +311,7 @@ async function queryList() {
311
311
  }
312
312
  async function queryShow(name) {
313
313
  if (!name) {
314
- console.error("\n Usage: npx overscore query show <name>\n");
314
+ console.error("\n Usage: npx @overscore/cli query show <name>\n");
315
315
  process.exit(1);
316
316
  }
317
317
  const { dashboardSlug } = loadEnv();
@@ -356,9 +356,9 @@ function resolveSql(sqlOrNext) {
356
356
  async function queryAdd(name, sql) {
357
357
  const resolvedSql = resolveSql(sql);
358
358
  if (!name || !resolvedSql) {
359
- console.error('\n Usage: npx overscore query add <name> "<sql>"\n');
359
+ console.error('\n Usage: npx @overscore/cli query add <name> "<sql>"\n');
360
360
  console.error(" Or with a file:");
361
- console.error(" npx overscore query add <name> --file query.sql\n");
361
+ console.error(" npx @overscore/cli query add <name> --file query.sql\n");
362
362
  process.exit(1);
363
363
  }
364
364
  if (!/^[a-z][a-z0-9_]*$/.test(name) || name.length > 100) {
@@ -374,9 +374,9 @@ async function queryAdd(name, sql) {
374
374
  async function queryUpdate(name, sql) {
375
375
  const resolvedSql = resolveSql(sql);
376
376
  if (!name || !resolvedSql) {
377
- console.error('\n Usage: npx overscore query update <name> "<sql>"\n');
377
+ console.error('\n Usage: npx @overscore/cli query update <name> "<sql>"\n');
378
378
  console.error(" Or with a file:");
379
- console.error(" npx overscore query update <name> --file query.sql\n");
379
+ console.error(" npx @overscore/cli query update <name> --file query.sql\n");
380
380
  process.exit(1);
381
381
  }
382
382
  const { dashboardSlug } = loadEnv();
@@ -386,7 +386,7 @@ async function queryUpdate(name, sql) {
386
386
  }
387
387
  async function queryRemove(name) {
388
388
  if (!name) {
389
- console.error("\n Usage: npx overscore query remove <name>\n");
389
+ console.error("\n Usage: npx @overscore/cli query remove <name>\n");
390
390
  process.exit(1);
391
391
  }
392
392
  const yes = await confirm(`Delete query "${name}"? This cannot be undone.`);
@@ -400,9 +400,9 @@ async function queryRemove(name) {
400
400
  }
401
401
  async function queryTest(sql) {
402
402
  if (!sql) {
403
- console.error('\n Usage: npx overscore query test "<sql>"\n');
403
+ console.error('\n Usage: npx @overscore/cli query test "<sql>"\n');
404
404
  console.error(" Example:");
405
- console.error(' npx overscore query test "SELECT * FROM dataset.table LIMIT 10"\n');
405
+ console.error(' npx @overscore/cli query test "SELECT * FROM dataset.table LIMIT 10"\n');
406
406
  process.exit(1);
407
407
  }
408
408
  const { dashboardSlug } = loadEnv();
@@ -449,9 +449,9 @@ function formatCell(value) {
449
449
  }
450
450
  async function pull(slug) {
451
451
  if (!slug) {
452
- console.error("\n Usage: npx overscore pull <dashboard-slug> [--version N]\n");
452
+ console.error("\n Usage: npx @overscore/cli pull <dashboard-slug> [--version N]\n");
453
453
  console.error(" Example:");
454
- console.error(" npx overscore pull my-dashboard\n");
454
+ console.error(" npx @overscore/cli pull my-dashboard\n");
455
455
  process.exit(1);
456
456
  }
457
457
  // Parse --version flag
@@ -558,7 +558,7 @@ VITE_OVERSCORE_API_URL=https://overscore.dev/api
558
558
 
559
559
  When ready to deploy:
560
560
 
561
- npx overscore deploy --message "your changes"
561
+ npx @overscore/cli deploy --message "your changes"
562
562
  `);
563
563
  }
564
564
  async function versions() {
@@ -583,7 +583,7 @@ async function versions() {
583
583
  const by = v.deployed_by ? ` by ${v.deployed_by}` : "";
584
584
  console.log(` v${v.version_number} ${date} ${v.file_count} files ${size} ${source}${msg}${by}`);
585
585
  }
586
- console.log(`\n Pull a version: npx overscore pull ${dashboardSlug} --version N\n`);
586
+ console.log(`\n Pull a version: npx @overscore/cli pull ${dashboardSlug} --version N\n`);
587
587
  }
588
588
  async function listDashboards() {
589
589
  const { apiUrl, apiKey } = loadEnv();
@@ -629,6 +629,458 @@ function collectFiles(dir, prefix) {
629
629
  }
630
630
  return files;
631
631
  }
632
+ // ── Analysis commands ──────────────────────────────────────────────
633
+ const HUB_URL = "https://overscore.dev";
634
+ function slugify(input) {
635
+ return input
636
+ .toLowerCase()
637
+ .replace(/[^a-z0-9]+/g, "-")
638
+ .replace(/^-+|-+$/g, "")
639
+ .slice(0, 80);
640
+ }
641
+ /**
642
+ * Resolve API config for analysis commands.
643
+ *
644
+ * Priority:
645
+ * 1. Local .env in cwd (e.g. running from inside an existing dashboard project)
646
+ * 2. ~/.overscore/config (set by install-skills or a previous analysis new run)
647
+ * 3. Interactive prompt (asks for API key, project slug)
648
+ *
649
+ * Analysis commands work from anywhere — they don't require being inside a project folder.
650
+ */
651
+ async function loadAnalysisConfig() {
652
+ // (1) Local .env
653
+ const envPath = path.resolve(process.cwd(), ".env");
654
+ if (fs.existsSync(envPath)) {
655
+ const env = parseEnv(fs.readFileSync(envPath, "utf-8"));
656
+ const apiUrl = env.VITE_OVERSCORE_API_URL;
657
+ const apiKey = env.VITE_OVERSCORE_API_KEY;
658
+ const projectSlug = env.VITE_OVERSCORE_PROJECT_SLUG;
659
+ if (apiUrl && apiKey && projectSlug) {
660
+ return { apiUrl, apiKey, projectSlug };
661
+ }
662
+ }
663
+ // (2) ~/.overscore/config
664
+ const configDir = path.join(process.env.HOME || "", ".overscore");
665
+ const configPath = path.join(configDir, "config");
666
+ if (fs.existsSync(configPath)) {
667
+ const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
668
+ if (cfg.OVERSCORE_API_KEY && cfg.OVERSCORE_PROJECT_SLUG) {
669
+ return {
670
+ apiUrl: cfg.OVERSCORE_API_URL || `${HUB_URL}/api`,
671
+ apiKey: cfg.OVERSCORE_API_KEY,
672
+ projectSlug: cfg.OVERSCORE_PROJECT_SLUG,
673
+ };
674
+ }
675
+ }
676
+ // (3) Interactive
677
+ console.log("\n No Overscore config found. Let's set one up.\n");
678
+ const apiKey = await prompt("API key (from the Hub):");
679
+ if (!apiKey || !apiKey.startsWith("os_")) {
680
+ console.error("\n Error: API key should start with os_\n");
681
+ process.exit(1);
682
+ }
683
+ const projectSlug = await prompt("Project slug:");
684
+ if (!projectSlug) {
685
+ console.error("\n Error: Project slug is required\n");
686
+ process.exit(1);
687
+ }
688
+ // Save for next time
689
+ fs.mkdirSync(configDir, { recursive: true });
690
+ fs.writeFileSync(configPath, `OVERSCORE_API_URL=${HUB_URL}/api\nOVERSCORE_API_KEY=${apiKey}\nOVERSCORE_PROJECT_SLUG=${projectSlug}\n`, { mode: 0o600 });
691
+ console.log(`\n Saved to ~/.overscore/config\n`);
692
+ return { apiUrl: `${HUB_URL}/api`, apiKey, projectSlug };
693
+ }
694
+ async function analysisApiRequest(cfg, method, urlPath, body) {
695
+ const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
696
+ const url = `${baseUrl}${urlPath}`;
697
+ const headers = {
698
+ Authorization: `Bearer ${cfg.apiKey}`,
699
+ };
700
+ if (body !== undefined)
701
+ headers["Content-Type"] = "application/json";
702
+ const res = await fetch(url, {
703
+ method,
704
+ headers,
705
+ body: body !== undefined ? JSON.stringify(body) : undefined,
706
+ });
707
+ const data = await res.json();
708
+ if (!res.ok) {
709
+ console.error(`\n Error: ${data.error || `HTTP ${res.status}`}\n`);
710
+ process.exit(1);
711
+ }
712
+ return data;
713
+ }
714
+ const ANALYSIS_CLAUDE_MD = `# Overscore Analysis: {{TITLE}}
715
+
716
+ This is an **Overscore Analysis** — a one-off, frozen investigation. Analyses are NOT dashboards:
717
+
718
+ - **No React project, no \`package.json\`, no \`useQuery\` hook.**
719
+ - **No scheduled refreshes.** Numbers are hard-coded into \`report.html\` at publish time.
720
+ - The deliverable is a single self-contained HTML file that anyone can open in a browser.
721
+ - Analyses are meant to be shared (Slack, email) and reopened later for refresh-and-republish.
722
+
723
+ ## CRITICAL: Always maintain analysis-log.md
724
+
725
+ \`analysis-log.md\` is the **recoverable session state** for this analysis. As you work, you MUST append to it. Future sessions (including yourself, weeks from now, after the user runs \`analysis pull\`) read this file to know:
726
+
727
+ - What question the analysis is investigating
728
+ - Which BigQuery tables are involved
729
+ - Every SQL query you ran (paste the exact SQL)
730
+ - Conclusions reached and how you got there
731
+ - Open questions still being chased
732
+
733
+ Use this rough structure:
734
+
735
+ \`\`\`markdown
736
+ ## YYYY-MM-DD — Initial exploration
737
+
738
+ **Question:** <the user's question>
739
+
740
+ ### Tables involved
741
+ - \`dataset.table_name\` — what it contains, key columns
742
+
743
+ ### Queries run
744
+ 1. <one-line description of what this query investigated>
745
+ \`\`\`sql
746
+ SELECT ...
747
+ \`\`\`
748
+
749
+ ### Conclusions
750
+ - ...
751
+
752
+ ### Open questions
753
+ - ...
754
+ \`\`\`
755
+
756
+ Update it **as you go**, not at the end. Treat every meaningful exploration step as a new entry.
757
+
758
+ ## Where data context lives
759
+
760
+ \`.claude/rules/company-context.md\` was pulled from this analysis's parent project at scaffold time. It contains the project's table definitions, business logic, and data quirks shared across all dashboards and analyses in this project. **Read it first** before exploring blindly.
761
+
762
+ ## Generating report.html
763
+
764
+ When the user is happy with the work, generate \`report.html\` as a **fully self-contained** static HTML file:
765
+
766
+ - Inline all CSS (no external stylesheets, no Tailwind CDN at runtime).
767
+ - Hard-code the actual numbers and chart data into the HTML — **no \`fetch()\`, no API calls, no external scripts that depend on data being available at view time.**
768
+ - Charts: inline SVG, or embedded base64 PNG, or a self-contained chart library bundled inline. Recharts/D3 from CDN is OK only if the data is already baked into the page.
769
+ - Dark theme to match Overscore's brand: near-black background (\`#0a0a0f\`), white text, violet/blue/emerald accents, generous spacing.
770
+ - Title at top, executive summary, KPI cards, charts, supporting tables, conclusions.
771
+ - Footer with the date the analysis was generated.
772
+
773
+ The HTML is what gets shared in Slack and what stakeholders see. The numbers must be visible to anyone who opens the file, with no logins, no JS data fetching, no broken external dependencies.
774
+
775
+ ## Publishing
776
+
777
+ When the analysis is ready, **tell the user to run** (do not run it yourself without asking):
778
+
779
+ \`\`\`bash
780
+ npx @overscore/cli analysis publish
781
+ \`\`\`
782
+
783
+ This uploads \`report.html\` and the entire bundle (this CLAUDE.md, \`analysis-log.md\`, etc.) to Overscore. The analysis becomes available at \`https://<project>.overscore.dev/analysis/{{SLUG}}\`. Re-publishing overwrites the previous version.
784
+
785
+ ## When the user wants to turn this into a dashboard
786
+
787
+ Analyses are static. They have **no React project**, **no scheduled refreshes**, and **no \`useQuery\`**. If the user asks to "make this a real dashboard" or "turn this into something that updates," **do NOT add React code to this folder.** Instead:
788
+
789
+ 1. Tell them: "Analyses are frozen snapshots. To make this a tracked dashboard, we need to scaffold a new project — the analysis stays as the original investigation."
790
+ 2. Tell them to run, from the parent directory:
791
+ \`\`\`bash
792
+ cd .. && npx create-overscore
793
+ \`\`\`
794
+ Use the same project slug this analysis belongs to (found in \`.overscore/analysis.json\`).
795
+ 3. Once they're in the new dashboard folder, hand off a seed message that includes: this analysis's question, the tables explored (from \`analysis-log.md\`), the key queries that mattered, and the conclusions reached. The new dashboard's Claude session can use this as a starting point.
796
+
797
+ ## Do NOT
798
+
799
+ - Add \`package.json\`, \`node_modules\`, or any React/Vite scaffolding to this folder.
800
+ - Use \`useQuery()\` or any Overscore client hooks — they don't exist here.
801
+ - Call BigQuery from inside \`report.html\` at view time. Bake the numbers in.
802
+ - Publish without showing the user \`report.html\` in a browser first and getting their approval.
803
+ - Forget to update \`analysis-log.md\` as you work.
804
+ `;
805
+ async function analysisNew(title) {
806
+ if (!title) {
807
+ console.error('\n Usage: npx @overscore/cli analysis new "<title>"\n');
808
+ process.exit(1);
809
+ }
810
+ const cfg = await loadAnalysisConfig();
811
+ console.log(`\n Creating analysis: "${title}"\n`);
812
+ const data = (await analysisApiRequest(cfg, "POST", "/api/analyses", {
813
+ title,
814
+ }));
815
+ // Scaffold the local folder
816
+ const folderName = data.slug;
817
+ const targetDir = path.resolve(process.cwd(), folderName);
818
+ if (fs.existsSync(targetDir)) {
819
+ console.error(`\n Error: Directory "${folderName}" already exists.\n`);
820
+ process.exit(1);
821
+ }
822
+ fs.mkdirSync(targetDir, { recursive: true });
823
+ fs.mkdirSync(path.join(targetDir, ".claude", "rules"), { recursive: true });
824
+ fs.mkdirSync(path.join(targetDir, ".overscore"), { recursive: true });
825
+ // Write analysis.json metadata
826
+ fs.writeFileSync(path.join(targetDir, ".overscore", "analysis.json"), JSON.stringify({
827
+ id: data.id,
828
+ slug: data.slug,
829
+ title: data.title,
830
+ project_slug: data.project_slug,
831
+ created_at: new Date().toISOString(),
832
+ }, null, 2));
833
+ // Write CLAUDE.md from template
834
+ const claudeMd = ANALYSIS_CLAUDE_MD
835
+ .replace(/\{\{TITLE\}\}/g, data.title)
836
+ .replace(/\{\{SLUG\}\}/g, data.slug);
837
+ fs.writeFileSync(path.join(targetDir, ".claude", "CLAUDE.md"), claudeMd);
838
+ // Empty analysis log seeded with the question
839
+ const today = new Date().toISOString().slice(0, 10);
840
+ const analysisLog = `# ${data.title}
841
+
842
+ ## ${today} — Initial exploration
843
+
844
+ **Question:** ${data.title}
845
+
846
+ ### Tables involved
847
+ - _(populate as you explore — see \`.claude/rules/company-context.md\` for what's available)_
848
+
849
+ ### Queries run
850
+ - _(append every meaningful query here, with one-line description + SQL)_
851
+
852
+ ### Conclusions
853
+ - _(empty so far)_
854
+
855
+ ### Open questions
856
+ - _(empty so far)_
857
+ `;
858
+ fs.writeFileSync(path.join(targetDir, "analysis-log.md"), analysisLog);
859
+ // Empty report.html stub
860
+ fs.writeFileSync(path.join(targetDir, "report.html"), `<!doctype html>
861
+ <html lang="en">
862
+ <head>
863
+ <meta charset="utf-8" />
864
+ <title>${data.title}</title>
865
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
866
+ <style>
867
+ body { background: #0a0a0f; color: #fff; font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 4rem 2rem; max-width: 800px; margin: 0 auto; }
868
+ h1 { font-weight: 600; }
869
+ p { color: rgba(255,255,255,0.6); }
870
+ </style>
871
+ </head>
872
+ <body>
873
+ <h1>${data.title}</h1>
874
+ <p>This analysis is empty. Have Claude do the work and regenerate this file.</p>
875
+ </body>
876
+ </html>
877
+ `);
878
+ // Pull project's company-context.md (best-effort, non-fatal)
879
+ try {
880
+ const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
881
+ const ctxRes = await fetch(`${baseUrl}/api/projects/${data.project_slug}/context`, { headers: { Authorization: `Bearer ${cfg.apiKey}` } });
882
+ if (ctxRes.ok) {
883
+ const ctxData = (await ctxRes.json());
884
+ if (ctxData.context_md) {
885
+ fs.writeFileSync(path.join(targetDir, ".claude", "rules", "company-context.md"), ctxData.context_md);
886
+ }
887
+ }
888
+ }
889
+ catch {
890
+ // Non-fatal
891
+ }
892
+ console.log(` Analysis created: ${data.title}
893
+ Slug: ${data.slug}
894
+ Project: ${data.project_slug}
895
+ Folder: ./${folderName}/
896
+
897
+ Next steps:
898
+
899
+ cd ${folderName}
900
+ code .
901
+ claude
902
+
903
+ Then ask Claude to do the analysis. When the report.html looks good:
904
+
905
+ npx @overscore/cli analysis publish
906
+
907
+ The analysis will go live at:
908
+ https://${data.project_slug}.overscore.dev/analysis/${data.slug}
909
+ `);
910
+ }
911
+ function loadAnalysisMetadata() {
912
+ const metaPath = path.resolve(process.cwd(), ".overscore", "analysis.json");
913
+ if (!fs.existsSync(metaPath)) {
914
+ console.error("\n Error: Not in an analysis folder. Run 'npx @overscore/cli analysis new \"<title>\"' first.\n");
915
+ process.exit(1);
916
+ }
917
+ return JSON.parse(fs.readFileSync(metaPath, "utf-8"));
918
+ }
919
+ const ANALYSIS_PUBLISH_EXCLUDE = new Set([
920
+ "node_modules",
921
+ ".git",
922
+ ".DS_Store",
923
+ ".env",
924
+ ]);
925
+ function collectAnalysisFiles(dir, prefix) {
926
+ const files = [];
927
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
928
+ if (ANALYSIS_PUBLISH_EXCLUDE.has(entry.name))
929
+ continue;
930
+ if (entry.name.startsWith(".env"))
931
+ continue;
932
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
933
+ if (entry.isDirectory()) {
934
+ files.push(...collectAnalysisFiles(path.join(dir, entry.name), relativePath));
935
+ }
936
+ else {
937
+ files.push(relativePath);
938
+ }
939
+ }
940
+ return files;
941
+ }
942
+ async function analysisPublish() {
943
+ const meta = loadAnalysisMetadata();
944
+ const cfg = await loadAnalysisConfig();
945
+ const reportPath = path.resolve(process.cwd(), "report.html");
946
+ if (!fs.existsSync(reportPath)) {
947
+ console.error("\n Error: report.html not found in this folder. Generate it before publishing.\n");
948
+ process.exit(1);
949
+ }
950
+ console.log(`\n Publishing analysis: ${meta.title}\n`);
951
+ const files = collectAnalysisFiles(process.cwd(), "");
952
+ console.log(` Bundling ${files.length} files...`);
953
+ const formData = new FormData();
954
+ formData.append("analysis_slug", meta.slug);
955
+ for (const file of files) {
956
+ const filePath = path.join(process.cwd(), file);
957
+ const content = fs.readFileSync(filePath);
958
+ const blob = new Blob([content]);
959
+ formData.append(`file:${file}`, blob, file);
960
+ }
961
+ console.log(" Uploading...");
962
+ const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
963
+ const res = await fetch(`${baseUrl}/api/analyses/publish`, {
964
+ method: "POST",
965
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
966
+ body: formData,
967
+ });
968
+ const data = (await res.json());
969
+ if (!res.ok) {
970
+ console.error(`\n Publish failed: ${data.error}\n`);
971
+ process.exit(1);
972
+ }
973
+ console.log(`
974
+ Published successfully!
975
+
976
+ ${data.file_count} files uploaded
977
+ URL: ${data.url}
978
+ `);
979
+ }
980
+ async function analysisPull(slug) {
981
+ if (!slug) {
982
+ console.error("\n Usage: npx @overscore/cli analysis pull <slug>\n");
983
+ process.exit(1);
984
+ }
985
+ const cfg = await loadAnalysisConfig();
986
+ console.log(`\n Pulling analysis: ${slug}...`);
987
+ const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
988
+ const res = await fetch(`${baseUrl}/api/analyses/${slug}/source`, {
989
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
990
+ });
991
+ if (!res.ok) {
992
+ let errorMsg = `HTTP ${res.status}`;
993
+ try {
994
+ const body = await res.json();
995
+ errorMsg = body.error || errorMsg;
996
+ }
997
+ catch {
998
+ // not JSON
999
+ }
1000
+ console.error(`\n Error: ${errorMsg}\n`);
1001
+ process.exit(1);
1002
+ }
1003
+ const analysisSlug = res.headers.get("X-OS-Analysis-Slug") || slug;
1004
+ const analysisTitle = res.headers.get("X-OS-Analysis-Title") || slug;
1005
+ const projectSlug = res.headers.get("X-OS-Project-Slug") || cfg.projectSlug;
1006
+ const zipBuffer = Buffer.from(await res.arrayBuffer());
1007
+ const targetDir = path.resolve(process.cwd(), analysisSlug);
1008
+ if (fs.existsSync(targetDir)) {
1009
+ const yes = await confirm(`Directory "${analysisSlug}" already exists. Overwrite?`);
1010
+ if (!yes) {
1011
+ console.log(" Cancelled.\n");
1012
+ return;
1013
+ }
1014
+ fs.rmSync(targetDir, { recursive: true, force: true });
1015
+ }
1016
+ fs.mkdirSync(targetDir, { recursive: true });
1017
+ await extractZip(zipBuffer, targetDir);
1018
+ // Make sure analysis.json reflects the current project (in case the user
1019
+ // pulled into a different machine where their default project is different)
1020
+ const metaPath = path.join(targetDir, ".overscore", "analysis.json");
1021
+ if (fs.existsSync(metaPath)) {
1022
+ try {
1023
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
1024
+ meta.project_slug = projectSlug;
1025
+ meta.slug = analysisSlug;
1026
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
1027
+ }
1028
+ catch {
1029
+ // Non-fatal
1030
+ }
1031
+ }
1032
+ console.log(`
1033
+ Pulled "${analysisTitle}"
1034
+
1035
+ Next steps:
1036
+
1037
+ cd ${analysisSlug}
1038
+ code .
1039
+ claude
1040
+
1041
+ Claude will read .claude/CLAUDE.md and analysis-log.md to know what was done.
1042
+
1043
+ When you're ready to republish:
1044
+
1045
+ npx @overscore/cli analysis publish
1046
+ `);
1047
+ }
1048
+ // ── install-skills ──────────────────────────────────────────────────
1049
+ async function installSkills() {
1050
+ // Skills ship with the npm package at <pkg-root>/skills/
1051
+ // __dirname after build is dist/, so the skills dir is one level up.
1052
+ const distDir = path.dirname(new URL(import.meta.url).pathname);
1053
+ const pkgRoot = path.resolve(distDir, "..");
1054
+ const skillsSrc = path.join(pkgRoot, "skills");
1055
+ if (!fs.existsSync(skillsSrc)) {
1056
+ console.error(`\n Error: Bundled skills directory not found at ${skillsSrc}\n`);
1057
+ process.exit(1);
1058
+ }
1059
+ const homeSkills = path.join(process.env.HOME || "", ".claude", "skills");
1060
+ fs.mkdirSync(homeSkills, { recursive: true });
1061
+ const installed = [];
1062
+ for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
1063
+ if (!entry.isDirectory())
1064
+ continue;
1065
+ const srcDir = path.join(skillsSrc, entry.name);
1066
+ const destDir = path.join(homeSkills, entry.name);
1067
+ fs.rmSync(destDir, { recursive: true, force: true });
1068
+ fs.mkdirSync(destDir, { recursive: true });
1069
+ for (const f of fs.readdirSync(srcDir)) {
1070
+ fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
1071
+ }
1072
+ installed.push(entry.name);
1073
+ }
1074
+ console.log(`
1075
+ Installed ${installed.length} skill${installed.length === 1 ? "" : "s"} into ~/.claude/skills/:
1076
+ ${installed.map((s) => ` - ${s}`).join("\n")}
1077
+
1078
+ Claude Code will auto-detect and use these skills in any session,
1079
+ including when you switch between Overscore dashboards and analyses.
1080
+
1081
+ Re-run this command anytime to update.
1082
+ `);
1083
+ }
632
1084
  // ── Routing ─────────────────────────────────────────────────────────
633
1085
  const command = process.argv[2];
634
1086
  const subcommand = process.argv[3];
@@ -644,6 +1096,35 @@ else if (command === "versions") {
644
1096
  else if (command === "list") {
645
1097
  listDashboards();
646
1098
  }
1099
+ else if (command === "install-skills") {
1100
+ installSkills();
1101
+ }
1102
+ else if (command === "analysis") {
1103
+ if (subcommand === "new") {
1104
+ analysisNew(process.argv[4]);
1105
+ }
1106
+ else if (subcommand === "publish") {
1107
+ analysisPublish();
1108
+ }
1109
+ else if (subcommand === "pull") {
1110
+ analysisPull(process.argv[4]);
1111
+ }
1112
+ else {
1113
+ console.log(`
1114
+ overscore analysis — Manage Overscore Analyses (frozen investigations)
1115
+
1116
+ Commands:
1117
+ new "<title>" Create a new analysis in the current directory
1118
+ publish Upload report.html + bundle from the current analysis folder
1119
+ pull <slug> Re-download an existing analysis bundle to continue working
1120
+
1121
+ Examples:
1122
+ npx @overscore/cli analysis new "Why did Q1 churn spike"
1123
+ npx @overscore/cli analysis publish
1124
+ npx @overscore/cli analysis pull why-did-q1-churn-spike
1125
+ `);
1126
+ }
1127
+ }
647
1128
  else if (command === "query") {
648
1129
  if (subcommand === "list") {
649
1130
  queryList();
@@ -676,12 +1157,12 @@ else if (command === "query") {
676
1157
  test "<sql>" Test SQL without saving
677
1158
 
678
1159
  Examples:
679
- npx overscore query list
680
- npx overscore query show monthly_revenue
681
- npx overscore query add monthly_revenue --file queries/revenue.sql
682
- npx overscore query add monthly_revenue "SELECT ..."
683
- npx overscore query update monthly_revenue --file queries/revenue.sql
684
- npx overscore query test "SELECT * FROM dataset.table LIMIT 10"
1160
+ npx @overscore/cli query list
1161
+ npx @overscore/cli query show monthly_revenue
1162
+ npx @overscore/cli query add monthly_revenue --file queries/revenue.sql
1163
+ npx @overscore/cli query add monthly_revenue "SELECT ..."
1164
+ npx @overscore/cli query update monthly_revenue --file queries/revenue.sql
1165
+ npx @overscore/cli query test "SELECT * FROM dataset.table LIMIT 10"
685
1166
  `);
686
1167
  }
687
1168
  }
@@ -694,18 +1175,24 @@ else {
694
1175
  deploy Build and deploy the dashboard
695
1176
  pull <slug> Pull source code from the hub
696
1177
  versions List deploy history
697
- query <subcommand> Manage dashboard queries
1178
+ query <subcommand> Manage dashboard queries
1179
+ analysis <subcommand> Manage analyses (frozen investigations)
1180
+ install-skills Install the Overscore skill library into ~/.claude/skills/
698
1181
 
699
1182
  Usage:
700
- npx overscore list
701
- npx overscore deploy --message "description"
702
- npx overscore pull <slug>
703
- npx overscore pull <slug> --version 3
704
- npx overscore versions
705
- npx overscore query list
706
- npx overscore query add <name> "<sql>"
707
- npx overscore query update <name> "<sql>"
708
- npx overscore query remove <name>
709
- npx overscore query test "<sql>"
1183
+ npx @overscore/cli list
1184
+ npx @overscore/cli deploy --message "description"
1185
+ npx @overscore/cli pull <slug>
1186
+ npx @overscore/cli pull <slug> --version 3
1187
+ npx @overscore/cli versions
1188
+ npx @overscore/cli query list
1189
+ npx @overscore/cli query add <name> "<sql>"
1190
+ npx @overscore/cli query update <name> "<sql>"
1191
+ npx @overscore/cli query remove <name>
1192
+ npx @overscore/cli query test "<sql>"
1193
+ npx @overscore/cli analysis new "<title>"
1194
+ npx @overscore/cli analysis publish
1195
+ npx @overscore/cli analysis pull <slug>
1196
+ npx @overscore/cli install-skills
710
1197
  `);
711
1198
  }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@overscore/cli",
3
- "version": "0.6.0",
4
- "description": "CLI for deploying Overscore dashboards",
3
+ "version": "0.7.0",
4
+ "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"
7
7
  },
8
8
  "files": [
9
- "dist"
9
+ "dist",
10
+ "skills"
10
11
  ],
11
12
  "scripts": {
12
13
  "build": "tsc"
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: scaffold-overscore-artifact
3
+ description: Use when the user wants to create a new Overscore dashboard or analysis, or when they're inside one artifact type and want to switch to the other (e.g. "turn this analysis into a dashboard", "do a quick analysis on this dashboard's data", "I want to investigate why X happened", "I want to track X over time"). Follow this procedure to scaffold the new artifact in the correct location with the correct command and seed it with context from the current artifact.
4
+ ---
5
+
6
+ # Scaffolding Overscore Artifacts
7
+
8
+ Overscore has two artifact types. They are **parallel**, not a funnel. Knowing which one to use is the most important decision the user makes.
9
+
10
+ ## Dashboards vs. Analyses — the one-line difference
11
+
12
+ - **Dashboard** — a question you ask **forever**. Live React project, registered queries, scheduled cache refresh, deployed to a subdomain. Use when the user wants to *track* something over time.
13
+ - **Analysis** — a question you ask **once**. Static HTML report with numbers baked in, no React, no refreshes. Use when the user wants to *investigate* something specific and share the answer.
14
+
15
+ | If the user says... | Use |
16
+ |---|---|
17
+ | "I want to track X over time" | Dashboard |
18
+ | "I want to investigate why X happened" | Analysis |
19
+ | "I need this to refresh on a schedule" | Dashboard |
20
+ | "I want a one-page write-up to share in Slack" | Analysis |
21
+ | "I want a recurring view of our metrics" | Dashboard |
22
+ | "I want to do a one-off cohort analysis" | Analysis |
23
+
24
+ ## Analysis → Dashboard
25
+
26
+ If the user is inside an **analysis folder** (look for `.overscore/analysis.json`) and says something like "let's turn this into a real dashboard" or "make this update automatically":
27
+
28
+ **Do NOT** add React code, `package.json`, or any Vite scaffolding to the analysis folder. Analyses are static and that would corrupt the artifact.
29
+
30
+ Instead:
31
+
32
+ 1. Tell the user: "Analyses are frozen snapshots — to make this a tracked dashboard, we need to scaffold a new project. The analysis stays as the original investigation."
33
+ 2. Read `.overscore/analysis.json` to find the parent project slug.
34
+ 3. Tell them to run, from the parent directory:
35
+ ```bash
36
+ cd .. && npx create-overscore
37
+ ```
38
+ And use the same project slug (from `.overscore/analysis.json`) when prompted.
39
+ 4. Once the new dashboard folder exists, prepare a seed message for the new dashboard's first Claude session that includes:
40
+ - The analysis's question (from `analysis-log.md`)
41
+ - The tables involved (from `analysis-log.md` → "Tables involved" section)
42
+ - The key queries that mattered (from `analysis-log.md` → "Queries run")
43
+ - The conclusions reached
44
+ 5. Tell the user to paste this seed into Claude when they `cd` into the new dashboard.
45
+
46
+ The analysis remains intact as the original investigation. Don't delete it.
47
+
48
+ ## Dashboard → Analysis
49
+
50
+ If the user is inside a **dashboard folder** (look for `package.json` with `@overscore/client` dependency, or `.env` with `VITE_OVERSCORE_DASHBOARD_SLUG`) and says something like "I just want to do a quick one-off investigation" or "a write-up that won't be part of this dashboard":
51
+
52
+ **Do NOT** add throwaway components, ad-hoc routes, or unused queries to the dashboard. That pollutes a production artifact.
53
+
54
+ Instead:
55
+
56
+ 1. Tell the user: "That sounds like an Analysis — a one-off investigation that won't pollute this dashboard."
57
+ 2. Tell them to run, from the parent directory:
58
+ ```bash
59
+ cd .. && npx @overscore/cli analysis new "<title of the investigation>"
60
+ ```
61
+ 3. The new analysis folder will be a sibling of the dashboard, in the same project. The analysis's CLAUDE.md takes over from there.
62
+
63
+ ## Cold start (neither folder exists)
64
+
65
+ If the user is in a fresh terminal with no Overscore artifact present and says something like "I want to investigate X" or "I want to build a dashboard for Y":
66
+
67
+ 1. Match their intent against the table above.
68
+ 2. If it's an **analysis**: have them run `npx @overscore/cli analysis new "<title>"` (they'll be prompted for project + API key on first use).
69
+ 3. If it's a **dashboard**: have them run `npx create-overscore` and follow the prompts.
70
+ 4. Both commands scaffold a folder with a CLAUDE.md primer that takes over immediately.
71
+
72
+ ## Never confuse the two
73
+
74
+ An analysis folder has: `.overscore/analysis.json`, `analysis-log.md`, `report.html`, no `node_modules`.
75
+
76
+ A dashboard folder has: `package.json`, `node_modules`, `src/`, `vite.config.ts`, `.env` with `VITE_OVERSCORE_DASHBOARD_SLUG`.
77
+
78
+ If you see one shape and the user asks for behavior that belongs to the other shape, **always** route them to the correct command rather than wedging the wrong shape into the current folder.