@overscore/cli 0.13.7 → 0.13.9

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
@@ -41,10 +41,17 @@ function parseEnv(content) {
41
41
  function safeApiUrl(apiUrl) {
42
42
  try {
43
43
  const h = new URL(apiUrl).hostname;
44
+ // Also allow the configured hub's host — prod by default, or a staging host
45
+ // only if the operator set OVERSCORE_HUB_URL (a trusted, user-set env var).
46
+ // If OVERSCORE_HUB_URL is unset, hubHost === overscore.dev and behavior is
47
+ // identical to before, so real users are unaffected.
48
+ const hubHost = new URL(HUB_URL).hostname;
44
49
  if (h === "overscore.dev" ||
45
50
  h.endsWith(".overscore.dev") ||
46
51
  h === "localhost" ||
47
- h === "127.0.0.1") {
52
+ h === "127.0.0.1" ||
53
+ h === hubHost ||
54
+ h.endsWith("." + hubHost)) {
48
55
  return apiUrl;
49
56
  }
50
57
  }
@@ -176,7 +183,17 @@ async function apiRequest(method, urlPath, body, command) {
176
183
  }
177
184
  }
178
185
  if (!res.ok) {
179
- console.error(`\n Error: ${data.error || `HTTP ${res.status}`}\n`);
186
+ const errMsg = data.error || `HTTP ${res.status}`;
187
+ console.error(`\n Error: ${errMsg}\n`);
188
+ // The warehouse "no data connection" error is a common onboarding dead-end:
189
+ // a dataset-backed project has no BigQuery connection, but `query run`/`test`
190
+ // only hit BigQuery. Point the user at the path that actually works.
191
+ if (/no data connection/i.test(errMsg)) {
192
+ console.error(" This usually means one of two things:");
193
+ console.error(" • The project uses an uploaded dataset (not BigQuery). `query run`/`test` only hit");
194
+ console.error(" BigQuery — inspect dataset columns and sample rows with: npm run queries");
195
+ console.error(" • The project should use BigQuery but no warehouse is connected yet — connect it in the Hub.\n");
196
+ }
180
197
  process.exit(1);
181
198
  }
182
199
  return data;
@@ -268,6 +285,10 @@ const SOURCE_EXCLUDE = new Set([
268
285
  // this baseline exclusion protects the dashboard deploy path too, regardless of
269
286
  // whether a project has a .overscoreignore.
270
287
  "data",
288
+ // Vendored DuckDB runtime from a local-preview workaround (public/_runtime).
289
+ // Production serves /_runtime from the edge; shipping ~74MB of .wasm/.js would
290
+ // bloat the deploy. The built-files guard in deploy() also blocks it in dist/.
291
+ "_runtime",
271
292
  ]);
272
293
  const SECRET_FILE_EXTENSIONS = new Set([
273
294
  ".pem",
@@ -437,6 +458,30 @@ async function deploy() {
437
458
  }
438
459
  const builtFiles = collectFiles(distDir, "");
439
460
  console.log(` Found ${builtFiles.length} built files.`);
461
+ // Guard: a vendored DuckDB runtime (public/_runtime) gets copied into dist/ by
462
+ // Vite and would ship ~74MB of .wasm/.js that production already serves from the
463
+ // edge. This is a local-preview workaround that must never deploy. Fail closed
464
+ // unless explicitly overridden with --allow-runtime.
465
+ const runtimeFiles = builtFiles.filter((f) => f === "_runtime" || f.startsWith("_runtime/"));
466
+ if (runtimeFiles.length > 0 && !process.argv.includes("--allow-runtime")) {
467
+ let bytes = 0;
468
+ for (const f of runtimeFiles) {
469
+ try {
470
+ bytes += fs.statSync(path.join(distDir, f)).size;
471
+ }
472
+ catch {
473
+ // ignore unreadable entries when sizing
474
+ }
475
+ }
476
+ const mb = (bytes / (1024 * 1024)).toFixed(1);
477
+ console.error(`\n Deploy aborted: dist/_runtime contains ${runtimeFiles.length} file(s) (${mb}MB).`);
478
+ console.error(" This is a vendored DuckDB runtime, usually from a public/_runtime local-preview");
479
+ console.error(" workaround. Production serves /_runtime from the edge, so shipping it just bloats");
480
+ console.error(" the deploy.");
481
+ console.error("\n Fix: rm -rf public/_runtime (then deploy again)");
482
+ console.error(" Or pass --allow-runtime to deploy it anyway.\n");
483
+ process.exit(1);
484
+ }
440
485
  // Collect source files (skipped entirely with --no-source). Source is saved
441
486
  // for versioning/restore; symlinks and secrets are always excluded.
442
487
  const noSource = process.argv.includes("--no-source");
@@ -873,8 +918,8 @@ async function pull(slug) {
873
918
  process.exit(1);
874
919
  }
875
920
  }
876
- const apiUrl = "https://overscore.dev/api";
877
- const baseUrl = apiUrl.replace(/\/api$/, "");
921
+ const apiUrl = `${HUB_URL}/api`;
922
+ const baseUrl = HUB_URL;
878
923
  const versionQuery = versionParam ? `?version=${versionParam}` : "";
879
924
  const url = `${baseUrl}/api/dashboards/${slug}/source${versionQuery}`;
880
925
  console.log(`\n Pulling source for: ${slug}${versionParam ? ` (v${versionParam})` : ""}...`);
@@ -925,7 +970,7 @@ async function pull(slug) {
925
970
  const envContent = `# Overscore Configuration
926
971
  VITE_OVERSCORE_PROJECT_SLUG=${projectSlug}
927
972
  VITE_OVERSCORE_DASHBOARD_SLUG=${dashboardSlug}
928
- VITE_OVERSCORE_API_URL=https://overscore.dev/api
973
+ VITE_OVERSCORE_API_URL=${HUB_URL}/api
929
974
  `;
930
975
  fs.writeFileSync(path.join(targetDir, ".env"), envContent, { mode: 0o600 });
931
976
  const rulesDir = path.join(targetDir, ".claude", "rules");
@@ -1087,7 +1132,23 @@ function collectFiles(dir, prefix) {
1087
1132
  return files;
1088
1133
  }
1089
1134
  // ── Analysis commands ──────────────────────────────────────────────
1090
- const HUB_URL = "https://overscore.dev";
1135
+ // Hub the CLI talks to. Defaults to production. A TRUSTED override — the
1136
+ // OVERSCORE_HUB_URL env var, set deliberately by the operator (e.g. to point at
1137
+ // a staging environment during a security audit) — can repoint it. This is
1138
+ // distinct from the project-.env override (VITE_OVERSCORE_API_URL), which stays
1139
+ // restricted by safeApiUrl() so a poisoned project can't redirect the API key +
1140
+ // bundle to an attacker host (audit L6).
1141
+ const HUB_URL = (process.env.OVERSCORE_HUB_URL || "https://overscore.dev").replace(/\/+$/, "");
1142
+ // Bare host of the hub (overscore.dev, or overscore-test.com on staging) for
1143
+ // building the {slug}.<host> URLs we print back to the user.
1144
+ const HUB_HOST = (() => {
1145
+ try {
1146
+ return new URL(HUB_URL).hostname;
1147
+ }
1148
+ catch {
1149
+ return "overscore.dev";
1150
+ }
1151
+ })();
1091
1152
  function slugify(input) {
1092
1153
  return input
1093
1154
  .toLowerCase()
@@ -1754,7 +1815,7 @@ async function analysisNew(title) {
1754
1815
  npx @overscore/cli analysis publish
1755
1816
 
1756
1817
  The analysis will go live at:
1757
- https://${data.project_slug}.overscore.dev/analysis/${data.slug}
1818
+ https://${data.project_slug}.${HUB_HOST}/analysis/${data.slug}
1758
1819
  `);
1759
1820
  }
1760
1821
  function loadAnalysisMetadata() {
@@ -2347,7 +2408,7 @@ async function dev() {
2347
2408
  apiKey = keyData.raw_key ?? null;
2348
2409
  }
2349
2410
  catch {
2350
- console.error("\n Error: Could not reach overscore.dev to create dev session key.\n");
2411
+ console.error(`\n Error: Could not reach ${HUB_HOST} to create dev session key.\n`);
2351
2412
  process.exit(1);
2352
2413
  }
2353
2414
  if (!apiKey) {
@@ -2606,7 +2667,7 @@ async function templatePull(slug) {
2606
2667
  ? apiSlug.slice(apiSlug.indexOf("_") + 1)
2607
2668
  : apiSlug;
2608
2669
  const targetDir = path.resolve(process.cwd(), process.argv[5] || localSlug);
2609
- const url = `https://overscore.dev/api/templates/${apiSlug}/source`;
2670
+ const url = `${HUB_URL}/api/templates/${apiSlug}/source`;
2610
2671
  console.log(`\n Pulling template: ${slug}...`);
2611
2672
  let res;
2612
2673
  try {
@@ -2625,7 +2686,7 @@ async function templatePull(slug) {
2625
2686
  });
2626
2687
  }
2627
2688
  catch {
2628
- console.error(`\n Error: Could not reach overscore.dev\n`);
2689
+ console.error(`\n Error: Could not reach ${HUB_HOST}\n`);
2629
2690
  process.exit(1);
2630
2691
  }
2631
2692
  if (!res.ok) {
@@ -34,7 +34,8 @@
34
34
  import fs from "fs";
35
35
  import path from "path";
36
36
  import { execSync } from "child_process";
37
- const HUB_URL = "https://overscore.dev";
37
+ // Trusted operator override (e.g. staging audit); defaults to production.
38
+ const HUB_URL = (process.env.OVERSCORE_HUB_URL || "https://overscore.dev").replace(/\/+$/, "");
38
39
  // Mirrors the templates_slug_format CHECK in the marketplace migration.
39
40
  // `_` is the handle namespace delimiter in the stored slug ({handle}_{local}).
40
41
  const TEMPLATE_SLUG_RE = /^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$/;
@@ -263,7 +264,7 @@ export async function templatePush(slugArg) {
263
264
  });
264
265
  }
265
266
  catch {
266
- console.error(`\n Error: Could not reach overscore.dev\n`);
267
+ console.error(`\n Error: Could not reach ${new URL(HUB_URL).hostname}\n`);
267
268
  process.exit(1);
268
269
  }
269
270
  const data = (await res.json().catch(() => ({})));
@@ -300,7 +301,8 @@ export async function templatePush(slugArg) {
300
301
  const sourceCount = sourceFiles.length;
301
302
  const listingUrl = data.detail_url
302
303
  ? `${HUB_URL}${data.detail_url}`
303
- : `${HUB_URL}/explore/${slug.replace("_", "/")}`;
304
+ : // Mirror the hub's templateExploreUrl: /u/{handle}/{slug} (first `_` only).
305
+ `${HUB_URL}/u/${slug.replace("_", "/")}`;
304
306
  console.log(`
305
307
  Pushed successfully!
306
308
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overscore/cli",
3
- "version": "0.13.7",
3
+ "version": "0.13.9",
4
4
  "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"
@@ -1,40 +0,0 @@
1
- ---
2
- name: bind-to-my-data
3
- description: Binds a pulled or forked Overscore template to the user's own warehouse by regenerating SQL against their real schema and registering it under the template's query names, turning a sample-data demo into a live dashboard. Use after pulling or forking a template, when a dashboard shows mock or sample data and the user wants their own numbers, or when the user says "connect this template to my data", "bind to my warehouse", "make this use my real data", "wire up the queries", "regenerate the queries for my schema".
4
- ---
5
-
6
- # Bind a template to your data
7
-
8
- A pulled or forked template ships design + sample data + query **names** — never the author's SQL. To go live, you regenerate SQL against the user's OWN warehouse and register it under the same names. **All SQL generation runs here, on the user's own Claude tokens;** Overscore only stores and executes the result.
9
-
10
- ## Step 0 — detect state
11
-
12
- | You see… | State | Do |
13
- | --- | --- | --- |
14
- | `.overscore/template.json`, no `.env` | **unbound demo** (`template pull`) | a pulled template has no project binding and runs on baked sample data. Land it in a project first: `npx create-overscore --template <handle>/<slug>`, or fork it in the hub and then `npx @overscore/cli pull <dashboard-slug>` to get a bound copy. Then continue. |
15
- | `.env` with `VITE_OVERSCORE_DASHBOARD_SLUG` | **bound dashboard** (forked + pulled) | ready to bind — continue. |
16
-
17
- ## Process
18
-
19
- 1. **Ensure a data source exists.** The project needs either a BigQuery connection (hub → the project's **Settings** page → **Connect BigQuery**, paste a read-only service-account JSON) or a dataset. New accounts auto-seed a sample dataset; the user can also upload one. Without one of these there is nothing real to bind to.
20
- 2. **Read the contract.** Find every query the template expects. If the template still calls `useQuery("<name>")`, grep those names. If it was already converted for publishing, the `useQuery` calls are gone — the contract now lives in `src/mockData.ts`: each `MOCK_*` export's shape, plus any `// query:` annotation naming it. If names aren't annotated, infer them from the export names and how each component consumes the array. Run `npx @overscore/cli query list` to see what's already registered (a fresh fork has none).
21
- 3. **Inspect the real schema.** Look at the user's actual tables and columns. Design SQL that produces each query's required shape from THEIR data — explore with `npx @overscore/cli query run "<sql>"` (ad-hoc; results land in `./data` and are never deployed).
22
- 4. **Register SQL under the SAME names.** For each query name:
23
- ```bash
24
- npx @overscore/cli query add <name> "<sql>" # BigQuery-backed
25
- npx @overscore/cli query add <name> --dataset <name> # or dataset-backed
26
- ```
27
- The name MUST match the template's `useQuery` call exactly, or that chart stays empty.
28
- 5. **Go live in the components.** If the template renders from `src/mockData.ts` (converted for publishing), rewire each `import { MOCK_X }` back to `const { data } = useQuery("x")`. Keep the charts and layout identical — only the data source changes.
29
- 6. **Verify and deploy.**
30
- ```bash
31
- npx @overscore/cli dev # resolves the names against your registered SQL
32
- npx @overscore/cli deploy --message "Bound <template> to my data"
33
- ```
34
-
35
- ## What this skill does NOT do
36
-
37
- - **It does not generate SQL on Overscore's servers.** Every query is reasoned out here, against the user's schema, on the user's own Claude tokens.
38
- - **It does not invent query names.** Register under the EXACT names the template references — that mapping is the contract.
39
- - **It does not write SQL inside React.** Queries are referenced by name via `useQuery`; the SQL lives in the registry via `query add` only.
40
- - **It does not need the author's queries.** Query SQL isn't registered with a template — queries live by name only — so you rebuild it from the design + names against your own data.
@@ -1,43 +0,0 @@
1
- ---
2
- name: regenerate-clean
3
- description: Converts an Overscore dashboard or pulled template into a publishable template by replacing every live query with typed synthetic mock data in src/mockData.ts, so the frozen bundle renders standalone and no real warehouse data ever ships. Use when preparing a dashboard to publish to the marketplace, when a pulled template still shows "Failed to load" or runs live queries, or when the user says "publish this as a template", "convert this to mock data", "regenerate clean", "make this template safe to share", "prep this for the marketplace".
4
- ---
5
-
6
- # Regenerate clean — convert a dashboard into a safe template
7
-
8
- Run this in a pulled template directory (or a from-scratch dashboard) to swap every live `useQuery(...)` for fresh **synthetic** mock data, so the published template renders on its own. A template carries design + sample data + query **names** — never the creator's real SQL, schema, or rows. You generate the mock data here, locally, on the user's own Claude tokens. **Overscore's servers never generate anything.**
9
-
10
- ## Step 0 — detect context
11
-
12
- | You see… | You're in… | Do |
13
- | --- | --- | --- |
14
- | `.overscore/template.json` | a **pulled template** (`template pull`) | convert it in place, then `template push` |
15
- | `.env` with `VITE_OVERSCORE_DASHBOARD_SLUG` + live `useQuery` calls | a **live dashboard** | DON'T convert in place. Tell the user to open the dashboard's **Publish as template** modal first — for a live-data dashboard it creates a **private draft** copy. Then `npx @overscore/cli template pull <handle>/<slug>` that draft and run this skill on the copy. **Never touch the live dashboard.** |
16
- | `src/mockData.ts` present, no `useQuery` left | already converted | skip to Verify |
17
-
18
- The original live dashboard is never modified. If it has live queries, the rule is **convert a private copy, attest, then promote** — never push a live-data dashboard straight to public.
19
-
20
- ## Process
21
-
22
- 1. **Inventory the data contract.** Grep the source for every `useQuery("<name>")` call. For each, record (a) the query **name** and (b) the row **shape** the component consumes — every column key and its type. These names + shapes ARE the template's contract; preserve them exactly.
23
- 2. **Generate synthetic data — locally, on the user's tokens.** Create `src/mockData.ts`: one exported `interface` + one exported `MOCK_<NAME>` array per query, with realistic-but-fabricated rows matching each shape (same column names, types, rough cardinality). Annotate each export with its query name, e.g. `// query: mrr_monthly` — once `useQuery` is gone (step 3) this comment is the only record of the name a consumer needs to rebind. **Scrub every real company name, customer name, and identifying number** — invent plausible substitutes.
24
- 3. **Rewire the components.** Replace each `const { data } = useQuery("x")` with `import { MOCK_X } from "./mockData"`. Change ONLY the data source — keep every chart, metric, filter, and layout byte-for-byte identical.
25
- 4. **Strip real data.** Delete any `data/` directory (raw query dumps) and any `fixtures/`, `dumps/`, or `exports/` holding real rows. Confirm there is no `.env*`, `*.log`, or credential file in the tree.
26
- 5. **Self-review for inline leaks.** The publish floor rejects `data/`, `.env*`, `*.log`, and secret-named files — but it is **content-blind**. Read `src/mockData.ts` and the components and confirm no real value (a real revenue figure, a real account name) was left inline.
27
-
28
- ## Verify, then publish
29
-
30
- ```bash
31
- npx @overscore/cli dev # renders on mock data — no API key or warehouse needed
32
- npm run build # confirm the rewire compiles
33
- npx @overscore/cli template push # upload the converted bundle (slug auto-read from .overscore/template.json)
34
- ```
35
-
36
- After push, open the listing's **Update** modal, **read** the mock-data attestation, and only then check it and set the visibility (Public / Unlisted / Private). Attestation is a truthful claim that the data is synthetic — never check it on an unconverted bundle.
37
-
38
- ## What this skill does NOT do
39
-
40
- - **It does not call any Overscore server-side AI.** All synthetic data is generated here, by you, on the user's own Claude tokens. Never tell the user "Overscore will convert this for you."
41
- - **It does not modify the original live dashboard.** Conversion always happens on a pulled template copy.
42
- - **It does not attest or publish for the user.** The user reads and checks the attestation themselves after reviewing the result.
43
- - **It does not lean on the server strip as the strategy.** The floor is a backstop; keeping real data out of the bundle in the first place is your job.