@overscore/cli 0.11.1 → 0.11.2

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.
Files changed (2) hide show
  1. package/dist/index.js +98 -63
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ function parseEnv(content) {
23
23
  }
24
24
  return env;
25
25
  }
26
- function loadEnv() {
26
+ async function loadEnv() {
27
27
  // (1) Local .env (inside a dashboard project)
28
28
  const envPath = path.resolve(process.cwd(), ".env");
29
29
  if (fs.existsSync(envPath)) {
@@ -49,33 +49,32 @@ function loadEnv() {
49
49
  };
50
50
  }
51
51
  }
52
- // No creds. Tailor the message based on what kind of folder we're in so the
53
- // user gets pointed at the right setup command.
54
- const inAnalysisFolder = fs.existsSync(path.resolve(process.cwd(), ".overscore", "analysis.json"));
55
- if (inAnalysisFolder) {
56
- let analysisSlug = null;
57
- try {
58
- const meta = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), ".overscore", "analysis.json"), "utf-8"));
59
- analysisSlug = typeof meta.slug === "string" ? meta.slug : null;
60
- }
61
- catch {
62
- // Fall through to the generic message.
52
+ // No creds trigger browser auth inline, then re-check
53
+ console.log("\n No Overscore credentials found. Launching browser login...\n");
54
+ await authLogin();
55
+ const configPath2 = path.join(process.env.HOME || "", ".overscore", "config");
56
+ if (fs.existsSync(configPath2)) {
57
+ const cfg2 = parseEnv(fs.readFileSync(configPath2, "utf-8"));
58
+ if (cfg2.OVERSCORE_API_KEY) {
59
+ return {
60
+ apiUrl: cfg2.OVERSCORE_API_URL || `${HUB_URL}/api`,
61
+ apiKey: cfg2.OVERSCORE_API_KEY,
62
+ dashboardSlug: path.basename(process.cwd()),
63
+ projectSlug: cfg2.OVERSCORE_PROJECT_SLUG || "",
64
+ };
63
65
  }
64
- console.error("\n Error: No credentials found in this analysis folder.");
65
- console.error(" This usually means the analysis hasn't been linked to your account yet.");
66
- console.error(" Run:");
67
- console.error(` npx @overscore/cli analysis pull ${analysisSlug || "<slug>"}`);
68
- console.error(" to refresh credentials and link this folder, or:");
69
- console.error(" npx @overscore/cli auth login");
70
- console.error(" if you've never set up the CLI on this machine.\n");
71
66
  }
72
- else {
73
- console.error("\n Error: No credentials found. Run 'npx @overscore/cli auth login' to set up.\n");
74
- }
75
- process.exit(1);
67
+ // Logged in but no project API key yet — the user needs to run setup first
68
+ console.log(`
69
+ Logged in! To get your first project set up:
70
+
71
+ Create a dashboard: npx create-overscore --dashboard <name> --project <slug>
72
+ Pull an analysis: npx @overscore/cli analysis pull <slug>
73
+ `);
74
+ process.exit(0);
76
75
  }
77
76
  async function apiRequest(method, urlPath, body, command) {
78
- const { apiUrl, apiKey } = loadEnv();
77
+ const { apiUrl, apiKey } = await loadEnv();
79
78
  const baseUrl = apiUrl.replace(/\/api$/, "");
80
79
  const url = `${baseUrl}${urlPath}`;
81
80
  const headers = {
@@ -195,7 +194,7 @@ function extractZip(zipBuffer, targetDir) {
195
194
  }
196
195
  // ── Commands ────────────────────────────────────────────────────────
197
196
  async function deploy() {
198
- const { apiUrl, apiKey, dashboardSlug } = loadEnv();
197
+ const { apiUrl, apiKey, dashboardSlug } = await loadEnv();
199
198
  // Parse --message flag
200
199
  const msgIndex = process.argv.indexOf("--message");
201
200
  const commitMessage = msgIndex !== -1 && process.argv[msgIndex + 1]
@@ -264,7 +263,7 @@ async function deploy() {
264
263
  URL: ${data.url}
265
264
  `);
266
265
  // Sync context files to Hub
267
- const { projectSlug } = loadEnv();
266
+ const { projectSlug } = await loadEnv();
268
267
  const baseUrl = apiUrl.replace(/\/api$/, "");
269
268
  // Upload knowledge base (project-level, shared across all artifacts)
270
269
  await uploadKnowledgeBase(process.cwd(), baseUrl, projectSlug, apiKey);
@@ -306,7 +305,7 @@ async function deploy() {
306
305
  await syncPlatformRules(process.cwd(), baseUrl, apiKey);
307
306
  }
308
307
  async function queryList() {
309
- const { dashboardSlug } = loadEnv();
308
+ const { dashboardSlug } = await loadEnv();
310
309
  const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`, undefined, "query-list"));
311
310
  console.log(`\n Dashboard: ${data.dashboard}\n`);
312
311
  // Registered queries
@@ -353,7 +352,7 @@ async function queryShow(name) {
353
352
  console.error("\n Usage: npx @overscore/cli query show <name>\n");
354
353
  process.exit(1);
355
354
  }
356
- const { dashboardSlug } = loadEnv();
355
+ const { dashboardSlug } = await loadEnv();
357
356
  const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`));
358
357
  const query = data.queries.find((q) => q.query_name === name);
359
358
  if (!query) {
@@ -421,7 +420,7 @@ async function queryAdd(name, sql) {
421
420
  console.error("\n Error: Query name must start with a lowercase letter and contain only lowercase letters, numbers, and underscores.\n");
422
421
  process.exit(1);
423
422
  }
424
- const { dashboardSlug } = loadEnv();
423
+ const { dashboardSlug } = await loadEnv();
425
424
  let body;
426
425
  if (datasetName) {
427
426
  // Look up the dataset UUID from project_datasets in the queries endpoint
@@ -454,7 +453,7 @@ async function queryUpdate(name, sql) {
454
453
  console.error(" npx @overscore/cli query update <name> --file query.sql\n");
455
454
  process.exit(1);
456
455
  }
457
- const { dashboardSlug } = loadEnv();
456
+ const { dashboardSlug } = await loadEnv();
458
457
  const data = (await apiRequest("PUT", `/api/dashboards/${dashboardSlug}/queries/${encodeURIComponent(name)}`, { sql_text: resolvedSql }));
459
458
  console.log(`\n Query "${data.query_name}" updated.`);
460
459
  console.log(" Cache has been invalidated — fresh data on next load.\n");
@@ -469,7 +468,7 @@ async function queryRemove(name) {
469
468
  console.log(" Cancelled.\n");
470
469
  return;
471
470
  }
472
- const { dashboardSlug } = loadEnv();
471
+ const { dashboardSlug } = await loadEnv();
473
472
  await apiRequest("DELETE", `/api/dashboards/${dashboardSlug}/queries/${encodeURIComponent(name)}`);
474
473
  console.log(`\n Query "${name}" deleted.\n`);
475
474
  }
@@ -527,7 +526,7 @@ async function queryTest(sql) {
527
526
  console.error(" npx @overscore/cli query run --file queries/foo.sql\n");
528
527
  process.exit(1);
529
528
  }
530
- const { projectSlug } = loadEnv();
529
+ const { projectSlug } = await loadEnv();
531
530
  if (!projectSlug) {
532
531
  const analysisMetaPath = path.resolve(process.cwd(), ".overscore", "analysis.json");
533
532
  if (fs.existsSync(analysisMetaPath)) {
@@ -797,7 +796,7 @@ VITE_OVERSCORE_API_URL=https://overscore.dev/api
797
796
  `);
798
797
  }
799
798
  async function versions() {
800
- const { dashboardSlug } = loadEnv();
799
+ const { dashboardSlug } = await loadEnv();
801
800
  const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/versions`));
802
801
  console.log(`\n Dashboard: ${dashboardSlug}\n`);
803
802
  if (data.versions.length === 0) {
@@ -821,7 +820,7 @@ async function versions() {
821
820
  console.log(`\n Pull a version: npx @overscore/cli pull ${dashboardSlug} --version N\n`);
822
821
  }
823
822
  async function listDashboards() {
824
- const { apiUrl, apiKey } = loadEnv();
823
+ const { apiUrl, apiKey } = await loadEnv();
825
824
  const baseUrl = apiUrl.replace(/\/api$/, "");
826
825
  const res = await fetch(`${baseUrl}/api/dashboards`, {
827
826
  headers: { Authorization: `Bearer ${apiKey}` },
@@ -927,44 +926,80 @@ async function loadAnalysisConfig() {
927
926
  }
928
927
  }
929
928
  }
930
- // (3) Interactiveonly ask for the API key, resolve project automatically
931
- console.log("\n No Overscore config found. Let's set one up.\n");
932
- const apiKey = await prompt("API key (from the Hub):");
933
- if (!apiKey || !apiKey.startsWith("os_")) {
934
- console.error("\n Error: API key should start with os_\n");
935
- process.exit(1);
929
+ // (3) No valid API key use an existing device token or trigger browser auth
930
+ let existingCfg = fs.existsSync(configPath)
931
+ ? parseEnv(fs.readFileSync(configPath, "utf-8"))
932
+ : {};
933
+ let deviceToken = existingCfg.OVERSCORE_DEVICE_TOKEN;
934
+ if (!deviceToken) {
935
+ console.log("\n No Overscore credentials found. Launching browser login...\n");
936
+ await authLogin();
937
+ existingCfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
938
+ deviceToken = existingCfg.OVERSCORE_DEVICE_TOKEN;
939
+ if (!deviceToken) {
940
+ console.error("\n Error: Login did not complete. Please try again.\n");
941
+ process.exit(1);
942
+ }
936
943
  }
937
- // Look up the project slug from the key
938
- let projectSlug = null;
939
- try {
940
- const res = await fetch(`${HUB_URL}/api/projects/verify-key`, {
944
+ // Resolve project slug
945
+ let targetProjectSlug = existingCfg.OVERSCORE_PROJECT_SLUG || "";
946
+ if (!targetProjectSlug) {
947
+ targetProjectSlug = await prompt("Project slug:");
948
+ if (!targetProjectSlug) {
949
+ console.error("\n Error: Project slug is required.\n");
950
+ process.exit(1);
951
+ }
952
+ }
953
+ // Generate an API key via device token (retry once on expired token)
954
+ let generatedKey;
955
+ for (let attempt = 0; attempt < 2; attempt++) {
956
+ const keyGenRes = await fetch(`${HUB_URL}/api/projects/${targetProjectSlug}/api-keys`, {
941
957
  method: "POST",
942
- headers: { "Content-Type": "application/json" },
943
- body: JSON.stringify({ key: apiKey }),
958
+ headers: {
959
+ Authorization: `Bearer ${deviceToken}`,
960
+ "Content-Type": "application/json",
961
+ },
962
+ body: JSON.stringify({ name: `cli-${Date.now()}` }),
944
963
  });
945
- if (res.ok) {
946
- const data = (await res.json());
947
- projectSlug = data.project_slug || null;
948
- if (projectSlug) {
949
- console.log(`\n Detected project: ${projectSlug}`);
964
+ if (keyGenRes.ok) {
965
+ const data = (await keyGenRes.json());
966
+ generatedKey = data.raw_key;
967
+ break;
968
+ }
969
+ if ((keyGenRes.status === 401 || keyGenRes.status === 403) && attempt === 0) {
970
+ console.log("\n Session expired. Launching browser login...\n");
971
+ await authLogin();
972
+ existingCfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
973
+ deviceToken = existingCfg.OVERSCORE_DEVICE_TOKEN;
974
+ if (!deviceToken) {
975
+ console.error("\n Error: Login did not complete. Please try again.\n");
976
+ process.exit(1);
950
977
  }
978
+ continue;
951
979
  }
952
- }
953
- catch {
954
- // Fall back to manual entry
955
- }
956
- if (!projectSlug) {
957
- projectSlug = await prompt("Project slug:");
958
- if (!projectSlug) {
959
- console.error("\n Error: Project slug is required\n");
960
- process.exit(1);
980
+ if (keyGenRes.status === 403) {
981
+ console.error(`\n Error: Access denied for project "${targetProjectSlug}". Make sure you have access.\n`);
961
982
  }
983
+ else {
984
+ console.error(`\n Error: Failed to generate API key (HTTP ${keyGenRes.status}).\n`);
985
+ }
986
+ process.exit(1);
962
987
  }
963
- // Save for next time
988
+ if (!generatedKey) {
989
+ console.error("\n Error: Failed to generate API key.\n");
990
+ process.exit(1);
991
+ }
992
+ // Persist the new key
993
+ const newLines = [
994
+ `OVERSCORE_API_URL=${HUB_URL}/api`,
995
+ `OVERSCORE_DEVICE_TOKEN=${deviceToken}`,
996
+ `OVERSCORE_API_KEY=${generatedKey}`,
997
+ `OVERSCORE_PROJECT_SLUG=${targetProjectSlug}`,
998
+ ];
964
999
  fs.mkdirSync(configDir, { recursive: true });
965
- fs.writeFileSync(configPath, `OVERSCORE_API_URL=${HUB_URL}/api\nOVERSCORE_API_KEY=${apiKey}\nOVERSCORE_PROJECT_SLUG=${projectSlug}\n`, { mode: 0o600 });
1000
+ fs.writeFileSync(configPath, newLines.join("\n") + "\n", { mode: 0o600 });
966
1001
  console.log(`\n Saved to ~/.overscore/config\n`);
967
- return { apiUrl: `${HUB_URL}/api`, apiKey, projectSlug };
1002
+ return { apiUrl: `${HUB_URL}/api`, apiKey: generatedKey, projectSlug: targetProjectSlug };
968
1003
  }
969
1004
  async function analysisApiRequest(cfg, method, urlPath, body) {
970
1005
  const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overscore/cli",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"