@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.
- package/dist/index.js +167 -63
- 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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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)
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
//
|
|
938
|
-
let
|
|
939
|
-
|
|
940
|
-
|
|
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: {
|
|
943
|
-
|
|
1027
|
+
headers: {
|
|
1028
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
1029
|
+
"Content-Type": "application/json",
|
|
1030
|
+
},
|
|
1031
|
+
body: JSON.stringify({ name: `cli-${Date.now()}` }),
|
|
944
1032
|
});
|
|
945
|
-
if (
|
|
946
|
-
const data = (await
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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
|
-
//
|
|
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,
|
|
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$/, "");
|