@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 +517 -30
- package/package.json +4 -3
- package/skills/scaffold-overscore-artifact/SKILL.md +78 -0
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>
|
|
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.
|
|
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.
|