@overscore/cli 0.11.1 → 0.11.3

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 +167 -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
- }
72
- else {
73
- console.error("\n Error: No credentials found. Run 'npx @overscore/cli auth login' to set up.\n");
74
66
  }
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, projectSlug } = await loadEnv();
79
78
  const baseUrl = apiUrl.replace(/\/api$/, "");
80
79
  const url = `${baseUrl}${urlPath}`;
81
80
  const headers = {
@@ -93,6 +92,23 @@ async function apiRequest(method, urlPath, body, command) {
93
92
  body: body !== undefined ? JSON.stringify(body) : undefined,
94
93
  });
95
94
  const data = await res.json();
95
+ if (res.status === 401 && projectSlug) {
96
+ console.log("\n API key expired. Refreshing...\n");
97
+ const newKey = await refreshApiKey(projectSlug);
98
+ if (newKey) {
99
+ const retryRes = await fetch(url, {
100
+ method,
101
+ headers: { ...headers, Authorization: `Bearer ${newKey}` },
102
+ body: body !== undefined ? JSON.stringify(body) : undefined,
103
+ });
104
+ const retryData = await retryRes.json();
105
+ if (!retryRes.ok) {
106
+ console.error(`\n Error: ${retryData.error || `HTTP ${retryRes.status}`}\n`);
107
+ process.exit(1);
108
+ }
109
+ return retryData;
110
+ }
111
+ }
96
112
  if (!res.ok) {
97
113
  console.error(`\n Error: ${data.error || `HTTP ${res.status}`}\n`);
98
114
  process.exit(1);
@@ -117,6 +133,58 @@ function prompt(message) {
117
133
  });
118
134
  });
119
135
  }
136
+ async function refreshApiKey(projectSlug) {
137
+ const configDir = path.join(process.env.HOME || "", ".overscore");
138
+ const configPath = path.join(configDir, "config");
139
+ let cfg = fs.existsSync(configPath)
140
+ ? parseEnv(fs.readFileSync(configPath, "utf-8"))
141
+ : {};
142
+ let deviceToken = cfg.OVERSCORE_DEVICE_TOKEN;
143
+ if (!deviceToken) {
144
+ console.log("\n No session found. Launching browser login...\n");
145
+ await authLogin();
146
+ cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
147
+ deviceToken = cfg.OVERSCORE_DEVICE_TOKEN;
148
+ if (!deviceToken)
149
+ return null;
150
+ }
151
+ for (let attempt = 0; attempt < 2; attempt++) {
152
+ const keyRes = await fetch(`${HUB_URL}/api/projects/${projectSlug}/api-keys`, {
153
+ method: "POST",
154
+ headers: { Authorization: `Bearer ${deviceToken}`, "Content-Type": "application/json" },
155
+ body: JSON.stringify({ name: `cli-${Date.now()}` }),
156
+ });
157
+ if (keyRes.ok) {
158
+ const data = (await keyRes.json());
159
+ const newKey = data.raw_key;
160
+ if (!newKey)
161
+ return null;
162
+ // Update local .env if present
163
+ const envPath = path.resolve(process.cwd(), ".env");
164
+ if (fs.existsSync(envPath)) {
165
+ const envContent = fs.readFileSync(envPath, "utf-8");
166
+ fs.writeFileSync(envPath, envContent.replace(/^VITE_OVERSCORE_API_KEY=.*/m, `VITE_OVERSCORE_API_KEY=${newKey}`));
167
+ }
168
+ // Update global config
169
+ cfg.OVERSCORE_API_KEY = newKey;
170
+ fs.mkdirSync(configDir, { recursive: true });
171
+ fs.writeFileSync(configPath, Object.entries(cfg).map(([k, v]) => `${k}=${v}`).join("\n") + "\n", { mode: 0o600 });
172
+ console.log(" API key refreshed.\n");
173
+ return newKey;
174
+ }
175
+ if ((keyRes.status === 401 || keyRes.status === 403) && attempt === 0) {
176
+ console.log("\n Session expired. Launching browser login...\n");
177
+ await authLogin();
178
+ cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
179
+ deviceToken = cfg.OVERSCORE_DEVICE_TOKEN;
180
+ if (!deviceToken)
181
+ return null;
182
+ continue;
183
+ }
184
+ return null;
185
+ }
186
+ return null;
187
+ }
120
188
  const SOURCE_EXCLUDE = new Set([
121
189
  "node_modules",
122
190
  "dist",
@@ -195,7 +263,7 @@ function extractZip(zipBuffer, targetDir) {
195
263
  }
196
264
  // ── Commands ────────────────────────────────────────────────────────
197
265
  async function deploy() {
198
- const { apiUrl, apiKey, dashboardSlug } = loadEnv();
266
+ const { apiUrl, apiKey, dashboardSlug } = await loadEnv();
199
267
  // Parse --message flag
200
268
  const msgIndex = process.argv.indexOf("--message");
201
269
  const commitMessage = msgIndex !== -1 && process.argv[msgIndex + 1]
@@ -264,7 +332,7 @@ async function deploy() {
264
332
  URL: ${data.url}
265
333
  `);
266
334
  // Sync context files to Hub
267
- const { projectSlug } = loadEnv();
335
+ const { projectSlug } = await loadEnv();
268
336
  const baseUrl = apiUrl.replace(/\/api$/, "");
269
337
  // Upload knowledge base (project-level, shared across all artifacts)
270
338
  await uploadKnowledgeBase(process.cwd(), baseUrl, projectSlug, apiKey);
@@ -306,7 +374,7 @@ async function deploy() {
306
374
  await syncPlatformRules(process.cwd(), baseUrl, apiKey);
307
375
  }
308
376
  async function queryList() {
309
- const { dashboardSlug } = loadEnv();
377
+ const { dashboardSlug } = await loadEnv();
310
378
  const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`, undefined, "query-list"));
311
379
  console.log(`\n Dashboard: ${data.dashboard}\n`);
312
380
  // Registered queries
@@ -353,7 +421,7 @@ async function queryShow(name) {
353
421
  console.error("\n Usage: npx @overscore/cli query show <name>\n");
354
422
  process.exit(1);
355
423
  }
356
- const { dashboardSlug } = loadEnv();
424
+ const { dashboardSlug } = await loadEnv();
357
425
  const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`));
358
426
  const query = data.queries.find((q) => q.query_name === name);
359
427
  if (!query) {
@@ -421,7 +489,7 @@ async function queryAdd(name, sql) {
421
489
  console.error("\n Error: Query name must start with a lowercase letter and contain only lowercase letters, numbers, and underscores.\n");
422
490
  process.exit(1);
423
491
  }
424
- const { dashboardSlug } = loadEnv();
492
+ const { dashboardSlug } = await loadEnv();
425
493
  let body;
426
494
  if (datasetName) {
427
495
  // Look up the dataset UUID from project_datasets in the queries endpoint
@@ -454,7 +522,7 @@ async function queryUpdate(name, sql) {
454
522
  console.error(" npx @overscore/cli query update <name> --file query.sql\n");
455
523
  process.exit(1);
456
524
  }
457
- const { dashboardSlug } = loadEnv();
525
+ const { dashboardSlug } = await loadEnv();
458
526
  const data = (await apiRequest("PUT", `/api/dashboards/${dashboardSlug}/queries/${encodeURIComponent(name)}`, { sql_text: resolvedSql }));
459
527
  console.log(`\n Query "${data.query_name}" updated.`);
460
528
  console.log(" Cache has been invalidated — fresh data on next load.\n");
@@ -469,7 +537,7 @@ async function queryRemove(name) {
469
537
  console.log(" Cancelled.\n");
470
538
  return;
471
539
  }
472
- const { dashboardSlug } = loadEnv();
540
+ const { dashboardSlug } = await loadEnv();
473
541
  await apiRequest("DELETE", `/api/dashboards/${dashboardSlug}/queries/${encodeURIComponent(name)}`);
474
542
  console.log(`\n Query "${name}" deleted.\n`);
475
543
  }
@@ -527,7 +595,7 @@ async function queryTest(sql) {
527
595
  console.error(" npx @overscore/cli query run --file queries/foo.sql\n");
528
596
  process.exit(1);
529
597
  }
530
- const { projectSlug } = loadEnv();
598
+ const { projectSlug } = await loadEnv();
531
599
  if (!projectSlug) {
532
600
  const analysisMetaPath = path.resolve(process.cwd(), ".overscore", "analysis.json");
533
601
  if (fs.existsSync(analysisMetaPath)) {
@@ -797,7 +865,7 @@ VITE_OVERSCORE_API_URL=https://overscore.dev/api
797
865
  `);
798
866
  }
799
867
  async function versions() {
800
- const { dashboardSlug } = loadEnv();
868
+ const { dashboardSlug } = await loadEnv();
801
869
  const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/versions`));
802
870
  console.log(`\n Dashboard: ${dashboardSlug}\n`);
803
871
  if (data.versions.length === 0) {
@@ -821,7 +889,7 @@ async function versions() {
821
889
  console.log(`\n Pull a version: npx @overscore/cli pull ${dashboardSlug} --version N\n`);
822
890
  }
823
891
  async function listDashboards() {
824
- const { apiUrl, apiKey } = loadEnv();
892
+ const { apiUrl, apiKey } = await loadEnv();
825
893
  const baseUrl = apiUrl.replace(/\/api$/, "");
826
894
  const res = await fetch(`${baseUrl}/api/dashboards`, {
827
895
  headers: { Authorization: `Bearer ${apiKey}` },
@@ -927,44 +995,80 @@ async function loadAnalysisConfig() {
927
995
  }
928
996
  }
929
997
  }
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);
998
+ // (3) No valid API key use an existing device token or trigger browser auth
999
+ let existingCfg = fs.existsSync(configPath)
1000
+ ? parseEnv(fs.readFileSync(configPath, "utf-8"))
1001
+ : {};
1002
+ let deviceToken = existingCfg.OVERSCORE_DEVICE_TOKEN;
1003
+ if (!deviceToken) {
1004
+ console.log("\n No Overscore credentials found. Launching browser login...\n");
1005
+ await authLogin();
1006
+ existingCfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
1007
+ deviceToken = existingCfg.OVERSCORE_DEVICE_TOKEN;
1008
+ if (!deviceToken) {
1009
+ console.error("\n Error: Login did not complete. Please try again.\n");
1010
+ process.exit(1);
1011
+ }
936
1012
  }
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`, {
1013
+ // Resolve project slug
1014
+ let targetProjectSlug = existingCfg.OVERSCORE_PROJECT_SLUG || "";
1015
+ if (!targetProjectSlug) {
1016
+ targetProjectSlug = await prompt("Project slug:");
1017
+ if (!targetProjectSlug) {
1018
+ console.error("\n Error: Project slug is required.\n");
1019
+ process.exit(1);
1020
+ }
1021
+ }
1022
+ // Generate an API key via device token (retry once on expired token)
1023
+ let generatedKey;
1024
+ for (let attempt = 0; attempt < 2; attempt++) {
1025
+ const keyGenRes = await fetch(`${HUB_URL}/api/projects/${targetProjectSlug}/api-keys`, {
941
1026
  method: "POST",
942
- headers: { "Content-Type": "application/json" },
943
- body: JSON.stringify({ key: apiKey }),
1027
+ headers: {
1028
+ Authorization: `Bearer ${deviceToken}`,
1029
+ "Content-Type": "application/json",
1030
+ },
1031
+ body: JSON.stringify({ name: `cli-${Date.now()}` }),
944
1032
  });
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}`);
1033
+ if (keyGenRes.ok) {
1034
+ const data = (await keyGenRes.json());
1035
+ generatedKey = data.raw_key;
1036
+ break;
1037
+ }
1038
+ if ((keyGenRes.status === 401 || keyGenRes.status === 403) && attempt === 0) {
1039
+ console.log("\n Session expired. Launching browser login...\n");
1040
+ await authLogin();
1041
+ existingCfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
1042
+ deviceToken = existingCfg.OVERSCORE_DEVICE_TOKEN;
1043
+ if (!deviceToken) {
1044
+ console.error("\n Error: Login did not complete. Please try again.\n");
1045
+ process.exit(1);
950
1046
  }
1047
+ continue;
951
1048
  }
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);
1049
+ if (keyGenRes.status === 403) {
1050
+ console.error(`\n Error: Access denied for project "${targetProjectSlug}". Make sure you have access.\n`);
1051
+ }
1052
+ else {
1053
+ console.error(`\n Error: Failed to generate API key (HTTP ${keyGenRes.status}).\n`);
961
1054
  }
1055
+ process.exit(1);
1056
+ }
1057
+ if (!generatedKey) {
1058
+ console.error("\n Error: Failed to generate API key.\n");
1059
+ process.exit(1);
962
1060
  }
963
- // Save for next time
1061
+ // Persist the new key
1062
+ const newLines = [
1063
+ `OVERSCORE_API_URL=${HUB_URL}/api`,
1064
+ `OVERSCORE_DEVICE_TOKEN=${deviceToken}`,
1065
+ `OVERSCORE_API_KEY=${generatedKey}`,
1066
+ `OVERSCORE_PROJECT_SLUG=${targetProjectSlug}`,
1067
+ ];
964
1068
  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 });
1069
+ fs.writeFileSync(configPath, newLines.join("\n") + "\n", { mode: 0o600 });
966
1070
  console.log(`\n Saved to ~/.overscore/config\n`);
967
- return { apiUrl: `${HUB_URL}/api`, apiKey, projectSlug };
1071
+ return { apiUrl: `${HUB_URL}/api`, apiKey: generatedKey, projectSlug: targetProjectSlug };
968
1072
  }
969
1073
  async function analysisApiRequest(cfg, method, urlPath, body) {
970
1074
  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.3",
4
4
  "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"