@overscore/cli 0.10.1 → 0.11.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 +265 -110
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import http from "http";
|
|
5
|
+
import net from "net";
|
|
4
6
|
import { execSync } from "child_process";
|
|
5
7
|
import { createInterface } from "readline";
|
|
6
|
-
import { createHash } from "crypto";
|
|
8
|
+
import { createHash, randomBytes } from "crypto";
|
|
7
9
|
import yauzl from "yauzl";
|
|
8
10
|
// ── Shared helpers ──────────────────────────────────────────────────
|
|
9
11
|
function parseEnv(content) {
|
|
@@ -72,7 +74,7 @@ function loadEnv() {
|
|
|
72
74
|
}
|
|
73
75
|
process.exit(1);
|
|
74
76
|
}
|
|
75
|
-
async function apiRequest(method, urlPath, body) {
|
|
77
|
+
async function apiRequest(method, urlPath, body, command) {
|
|
76
78
|
const { apiUrl, apiKey } = loadEnv();
|
|
77
79
|
const baseUrl = apiUrl.replace(/\/api$/, "");
|
|
78
80
|
const url = `${baseUrl}${urlPath}`;
|
|
@@ -82,6 +84,9 @@ async function apiRequest(method, urlPath, body) {
|
|
|
82
84
|
if (body !== undefined) {
|
|
83
85
|
headers["Content-Type"] = "application/json";
|
|
84
86
|
}
|
|
87
|
+
if (command) {
|
|
88
|
+
headers["X-Overscore-Command"] = command;
|
|
89
|
+
}
|
|
85
90
|
const res = await fetch(url, {
|
|
86
91
|
method,
|
|
87
92
|
headers,
|
|
@@ -332,7 +337,7 @@ async function deploy() {
|
|
|
332
337
|
}
|
|
333
338
|
async function queryList() {
|
|
334
339
|
const { dashboardSlug } = loadEnv();
|
|
335
|
-
const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries
|
|
340
|
+
const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`, undefined, "query-list"));
|
|
336
341
|
console.log(`\n Dashboard: ${data.dashboard}\n`);
|
|
337
342
|
// Registered queries
|
|
338
343
|
if (data.queries.length === 0) {
|
|
@@ -450,7 +455,7 @@ async function queryAdd(name, sql) {
|
|
|
450
455
|
let body;
|
|
451
456
|
if (datasetName) {
|
|
452
457
|
// Look up the dataset UUID from project_datasets in the queries endpoint
|
|
453
|
-
const listData = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries
|
|
458
|
+
const listData = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`, undefined, "query-add"));
|
|
454
459
|
const dataset = (listData.project_datasets ?? []).find((ds) => ds.name === datasetName);
|
|
455
460
|
if (!dataset) {
|
|
456
461
|
console.error(`\n Error: Dataset "${datasetName}" not found in this project.\n`);
|
|
@@ -466,7 +471,7 @@ async function queryAdd(name, sql) {
|
|
|
466
471
|
else {
|
|
467
472
|
body = { query_name: name, sql_text: resolvedSql };
|
|
468
473
|
}
|
|
469
|
-
const data = (await apiRequest("POST", `/api/dashboards/${dashboardSlug}/queries`, body));
|
|
474
|
+
const data = (await apiRequest("POST", `/api/dashboards/${dashboardSlug}/queries`, body, "query-add"));
|
|
470
475
|
console.log(`\n Query "${data.query_name}" created.\n`);
|
|
471
476
|
console.log(` Use it in your dashboard:`);
|
|
472
477
|
console.log(` const { data } = useQuery("${data.query_name}");\n`);
|
|
@@ -638,11 +643,43 @@ async function pull(slug) {
|
|
|
638
643
|
const versionParam = versionIndex !== -1 && process.argv[versionIndex + 1]
|
|
639
644
|
? process.argv[versionIndex + 1]
|
|
640
645
|
: null;
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
+
// Resolve API key: device token → auto-create os_ key, else prompt
|
|
647
|
+
let apiKey = "";
|
|
648
|
+
const configPath = path.join(process.env.HOME || "", ".overscore", "config");
|
|
649
|
+
if (fs.existsSync(configPath)) {
|
|
650
|
+
const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
|
|
651
|
+
const deviceToken = cfg.OVERSCORE_DEVICE_TOKEN;
|
|
652
|
+
if (deviceToken) {
|
|
653
|
+
try {
|
|
654
|
+
const keyRes = await fetch(`${HUB_URL}/api/projects/${slug}/api-keys`, {
|
|
655
|
+
method: "POST",
|
|
656
|
+
headers: {
|
|
657
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
658
|
+
"Content-Type": "application/json",
|
|
659
|
+
},
|
|
660
|
+
body: JSON.stringify({ name: `${slug} pull key` }),
|
|
661
|
+
});
|
|
662
|
+
if (keyRes.status === 401 || keyRes.status === 403) {
|
|
663
|
+
console.error("\n Error: Device token is invalid or expired. Run 'npx @overscore/cli auth login' to re-authenticate.\n");
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
if (keyRes.ok) {
|
|
667
|
+
const keyData = (await keyRes.json());
|
|
668
|
+
apiKey = keyData.raw_key || "";
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
// Network error — fall through to prompt
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (!apiKey) {
|
|
677
|
+
console.log(" Tip: Run 'npx @overscore/cli auth login' to authenticate once and skip this step.");
|
|
678
|
+
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
|
+
}
|
|
646
683
|
}
|
|
647
684
|
const apiUrl = "https://overscore.dev/api";
|
|
648
685
|
const baseUrl = apiUrl.replace(/\/api$/, "");
|
|
@@ -664,10 +701,10 @@ async function pull(slug) {
|
|
|
664
701
|
console.error(`\n Error: ${errorMsg}\n`);
|
|
665
702
|
process.exit(1);
|
|
666
703
|
}
|
|
667
|
-
const projectSlug = res.headers.get("X-
|
|
668
|
-
const dashboardSlug = res.headers.get("X-
|
|
669
|
-
const version = res.headers.get("X-
|
|
670
|
-
const commitMsg = res.headers.get("X-
|
|
704
|
+
const projectSlug = res.headers.get("X-OS-Project-Slug") || "";
|
|
705
|
+
const dashboardSlug = res.headers.get("X-OS-Dashboard-Slug") || slug;
|
|
706
|
+
const version = res.headers.get("X-OS-Version") || "?";
|
|
707
|
+
const commitMsg = res.headers.get("X-OS-Commit-Message") || "";
|
|
671
708
|
// Download the zip
|
|
672
709
|
const zipBuffer = Buffer.from(await res.arrayBuffer());
|
|
673
710
|
// Extract to ./<slug>/
|
|
@@ -692,11 +729,10 @@ async function pull(slug) {
|
|
|
692
729
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
693
730
|
}
|
|
694
731
|
await extractZip(zipBuffer, targetDir);
|
|
695
|
-
// Generate .env (
|
|
732
|
+
// Generate .env (no API key — 'npx @overscore/cli dev' injects it at runtime)
|
|
696
733
|
const envContent = `# Overscore Configuration
|
|
697
734
|
VITE_OVERSCORE_PROJECT_SLUG=${projectSlug}
|
|
698
735
|
VITE_OVERSCORE_DASHBOARD_SLUG=${dashboardSlug}
|
|
699
|
-
VITE_OVERSCORE_API_KEY=${apiKey}
|
|
700
736
|
VITE_OVERSCORE_API_URL=https://overscore.dev/api
|
|
701
737
|
`;
|
|
702
738
|
fs.writeFileSync(path.join(targetDir, ".env"), envContent, { mode: 0o600 });
|
|
@@ -749,11 +785,7 @@ VITE_OVERSCORE_API_URL=https://overscore.dev/api
|
|
|
749
785
|
|
|
750
786
|
cd ${slug}
|
|
751
787
|
npm install
|
|
752
|
-
|
|
753
|
-
claude
|
|
754
|
-
|
|
755
|
-
Note: If this dashboard uses third-party APIs, add your
|
|
756
|
-
API keys to .env before running.
|
|
788
|
+
npx @overscore/cli dev # starts dev server with auth injected
|
|
757
789
|
|
|
758
790
|
When ready to deploy:
|
|
759
791
|
|
|
@@ -1755,67 +1787,122 @@ function analysisPreview() {
|
|
|
1755
1787
|
process.exit(1);
|
|
1756
1788
|
}
|
|
1757
1789
|
}
|
|
1790
|
+
// ── dev ─────────────────────────────────────────────────────────────
|
|
1791
|
+
async function dev() {
|
|
1792
|
+
// Must be run from inside a dashboard project folder
|
|
1793
|
+
const envPath = path.resolve(process.cwd(), ".env");
|
|
1794
|
+
if (!fs.existsSync(envPath)) {
|
|
1795
|
+
console.error("\n Error: No .env file found. Run this from inside a dashboard project folder.\n");
|
|
1796
|
+
process.exit(1);
|
|
1797
|
+
}
|
|
1798
|
+
const localEnv = parseEnv(fs.readFileSync(envPath, "utf-8"));
|
|
1799
|
+
const projectSlug = localEnv.VITE_OVERSCORE_PROJECT_SLUG;
|
|
1800
|
+
if (!projectSlug) {
|
|
1801
|
+
console.error("\n Error: VITE_OVERSCORE_PROJECT_SLUG not set in .env.\n");
|
|
1802
|
+
process.exit(1);
|
|
1803
|
+
}
|
|
1804
|
+
// Get device token from global config
|
|
1805
|
+
const configPath = path.join(process.env.HOME || "", ".overscore", "config");
|
|
1806
|
+
if (!fs.existsSync(configPath)) {
|
|
1807
|
+
console.error("\n Error: Not authenticated. Run 'npx @overscore/cli auth login' first.\n");
|
|
1808
|
+
process.exit(1);
|
|
1809
|
+
}
|
|
1810
|
+
const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
|
|
1811
|
+
const deviceToken = cfg.OVERSCORE_DEVICE_TOKEN;
|
|
1812
|
+
if (!deviceToken) {
|
|
1813
|
+
console.error("\n Error: No device token found. Run 'npx @overscore/cli auth login' first.\n");
|
|
1814
|
+
process.exit(1);
|
|
1815
|
+
}
|
|
1816
|
+
// Create a temporary dev session key
|
|
1817
|
+
console.log("\n Creating dev session key...");
|
|
1818
|
+
let keyId = null;
|
|
1819
|
+
let apiKey = null;
|
|
1820
|
+
try {
|
|
1821
|
+
const keyRes = await fetch(`${HUB_URL}/api/projects/${projectSlug}/api-keys`, {
|
|
1822
|
+
method: "POST",
|
|
1823
|
+
headers: {
|
|
1824
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
1825
|
+
"Content-Type": "application/json",
|
|
1826
|
+
},
|
|
1827
|
+
body: JSON.stringify({ name: "dev session (auto-cleanup)" }),
|
|
1828
|
+
});
|
|
1829
|
+
if (keyRes.status === 401 || keyRes.status === 403) {
|
|
1830
|
+
console.error("\n Error: Device token is invalid or expired. Run 'npx @overscore/cli auth login' to re-authenticate.\n");
|
|
1831
|
+
process.exit(1);
|
|
1832
|
+
}
|
|
1833
|
+
if (!keyRes.ok) {
|
|
1834
|
+
const errData = await keyRes.json().catch(() => ({}));
|
|
1835
|
+
console.error(`\n Error: ${errData.error || `HTTP ${keyRes.status}`}\n`);
|
|
1836
|
+
process.exit(1);
|
|
1837
|
+
}
|
|
1838
|
+
const keyData = (await keyRes.json());
|
|
1839
|
+
keyId = keyData.id ?? null;
|
|
1840
|
+
apiKey = keyData.raw_key ?? null;
|
|
1841
|
+
}
|
|
1842
|
+
catch {
|
|
1843
|
+
console.error("\n Error: Could not reach overscore.dev to create dev session key.\n");
|
|
1844
|
+
process.exit(1);
|
|
1845
|
+
}
|
|
1846
|
+
if (!apiKey) {
|
|
1847
|
+
console.error("\n Error: Failed to get API key for dev session.\n");
|
|
1848
|
+
process.exit(1);
|
|
1849
|
+
}
|
|
1850
|
+
// Best-effort cleanup: revoke the session key on exit
|
|
1851
|
+
let cleaned = false;
|
|
1852
|
+
async function cleanup() {
|
|
1853
|
+
if (cleaned || !keyId)
|
|
1854
|
+
return;
|
|
1855
|
+
cleaned = true;
|
|
1856
|
+
try {
|
|
1857
|
+
await fetch(`${HUB_URL}/api/projects/${projectSlug}/api-keys`, {
|
|
1858
|
+
method: "DELETE",
|
|
1859
|
+
headers: {
|
|
1860
|
+
Authorization: `Bearer ${deviceToken}`,
|
|
1861
|
+
"Content-Type": "application/json",
|
|
1862
|
+
},
|
|
1863
|
+
body: JSON.stringify({ id: keyId }),
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
catch {
|
|
1867
|
+
// Best effort — key will remain but that's acceptable
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
// Spawn npm run dev with the key injected into the environment
|
|
1871
|
+
const { spawn } = await import("child_process");
|
|
1872
|
+
const child = spawn("npm", ["run", "dev"], {
|
|
1873
|
+
stdio: "inherit",
|
|
1874
|
+
env: { ...process.env, VITE_OVERSCORE_API_KEY: apiKey },
|
|
1875
|
+
});
|
|
1876
|
+
child.on("exit", async (code) => {
|
|
1877
|
+
await cleanup();
|
|
1878
|
+
process.exit(code ?? 0);
|
|
1879
|
+
});
|
|
1880
|
+
process.on("SIGINT", async () => {
|
|
1881
|
+
await cleanup();
|
|
1882
|
+
process.exit(0);
|
|
1883
|
+
});
|
|
1884
|
+
process.on("SIGTERM", async () => {
|
|
1885
|
+
await cleanup();
|
|
1886
|
+
process.exit(0);
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1758
1889
|
// ── auth ────────────────────────────────────────────────────────────
|
|
1759
1890
|
async function authStatus() {
|
|
1760
|
-
const
|
|
1761
|
-
const configPath = path.join(configDir, "config");
|
|
1762
|
-
// Check global config
|
|
1891
|
+
const configPath = path.join(process.env.HOME || "", ".overscore", "config");
|
|
1763
1892
|
if (fs.existsSync(configPath)) {
|
|
1764
1893
|
const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
|
|
1765
|
-
const
|
|
1894
|
+
const deviceToken = cfg.OVERSCORE_DEVICE_TOKEN;
|
|
1766
1895
|
const project = cfg.OVERSCORE_PROJECT_SLUG;
|
|
1767
|
-
if (
|
|
1768
|
-
|
|
1769
|
-
let valid = false;
|
|
1770
|
-
try {
|
|
1771
|
-
const hubUrl = cfg.OVERSCORE_API_URL?.replace(/\/api$/, "") || HUB_URL;
|
|
1772
|
-
const res = await fetch(`${hubUrl}/api/projects/verify-key`, {
|
|
1773
|
-
method: "POST",
|
|
1774
|
-
headers: { "Content-Type": "application/json" },
|
|
1775
|
-
body: JSON.stringify({ key }),
|
|
1776
|
-
});
|
|
1777
|
-
if (res.ok) {
|
|
1778
|
-
const data = (await res.json());
|
|
1779
|
-
valid = true;
|
|
1780
|
-
console.log(`
|
|
1896
|
+
if (deviceToken) {
|
|
1897
|
+
console.log(`
|
|
1781
1898
|
Authenticated
|
|
1782
1899
|
|
|
1783
|
-
|
|
1784
|
-
Project:
|
|
1785
|
-
Config:
|
|
1786
|
-
Status: Valid
|
|
1787
|
-
`);
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
catch {
|
|
1791
|
-
// network error
|
|
1792
|
-
}
|
|
1793
|
-
if (!valid) {
|
|
1794
|
-
console.log(`
|
|
1795
|
-
Authentication problem
|
|
1796
|
-
|
|
1797
|
-
Key: ${key.slice(0, 10)}...
|
|
1798
|
-
Project: ${project}
|
|
1799
|
-
Config: ~/.overscore/config
|
|
1800
|
-
Status: Invalid or expired
|
|
1801
|
-
|
|
1802
|
-
Run 'npx @overscore/cli auth login' to update your key.
|
|
1803
|
-
`);
|
|
1804
|
-
}
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
// Check local .env
|
|
1809
|
-
const envPath = path.resolve(process.cwd(), ".env");
|
|
1810
|
-
if (fs.existsSync(envPath)) {
|
|
1811
|
-
const env = parseEnv(fs.readFileSync(envPath, "utf-8"));
|
|
1812
|
-
if (env.VITE_OVERSCORE_API_KEY) {
|
|
1813
|
-
console.log(`
|
|
1814
|
-
Using local .env (this project only)
|
|
1900
|
+
Device token: ${deviceToken.slice(0, 12)}...
|
|
1901
|
+
Project: ${project || "—"}
|
|
1902
|
+
Config: ~/.overscore/config
|
|
1815
1903
|
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
Dashboard: ${env.VITE_OVERSCORE_DASHBOARD_SLUG || "—"}
|
|
1904
|
+
Run 'npx @overscore/cli dev' to start the dev server.
|
|
1905
|
+
Run 'npx @overscore/cli auth logout' to sign out.
|
|
1819
1906
|
`);
|
|
1820
1907
|
return;
|
|
1821
1908
|
}
|
|
@@ -1823,64 +1910,126 @@ async function authStatus() {
|
|
|
1823
1910
|
console.log(`
|
|
1824
1911
|
Not authenticated
|
|
1825
1912
|
|
|
1826
|
-
|
|
1913
|
+
Run 'npx @overscore/cli auth login' to authenticate.
|
|
1827
1914
|
`);
|
|
1828
1915
|
}
|
|
1829
1916
|
async function authLogin() {
|
|
1830
1917
|
const configDir = path.join(process.env.HOME || "", ".overscore");
|
|
1831
1918
|
const configPath = path.join(configDir, "config");
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1919
|
+
const state = randomBytes(16).toString("hex");
|
|
1920
|
+
// Find a free port in range 9876–9886
|
|
1921
|
+
let chosenPort = null;
|
|
1922
|
+
for (let p = 9876; p <= 9886; p++) {
|
|
1923
|
+
const free = await new Promise((resolve) => {
|
|
1924
|
+
const srv = net.createServer();
|
|
1925
|
+
srv.once("error", () => resolve(false));
|
|
1926
|
+
srv.once("listening", () => { srv.close(() => resolve(true)); });
|
|
1927
|
+
srv.listen(p, "127.0.0.1");
|
|
1928
|
+
});
|
|
1929
|
+
if (free) {
|
|
1930
|
+
chosenPort = p;
|
|
1931
|
+
break;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
if (!chosenPort) {
|
|
1935
|
+
console.error("\n Error: No free port found in range 9876–9886.\n");
|
|
1836
1936
|
process.exit(1);
|
|
1837
1937
|
}
|
|
1838
|
-
//
|
|
1839
|
-
let
|
|
1938
|
+
// Start local callback server
|
|
1939
|
+
let resolveCallback;
|
|
1940
|
+
const callbackPromise = new Promise((resolve) => {
|
|
1941
|
+
resolveCallback = resolve;
|
|
1942
|
+
});
|
|
1943
|
+
const server = http.createServer((req, res) => {
|
|
1944
|
+
const reqUrl = new URL(req.url ?? "/", `http://localhost:${chosenPort}`);
|
|
1945
|
+
if (reqUrl.pathname !== "/callback") {
|
|
1946
|
+
res.writeHead(404);
|
|
1947
|
+
res.end();
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
const token = reqUrl.searchParams.get("token") ?? "";
|
|
1951
|
+
const returnedState = reqUrl.searchParams.get("state") ?? "";
|
|
1952
|
+
const project = reqUrl.searchParams.get("project") ?? undefined;
|
|
1953
|
+
if (returnedState !== state || !token.startsWith("ocli_")) {
|
|
1954
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
1955
|
+
res.end("Invalid callback");
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1959
|
+
res.end(`<!DOCTYPE html><html><head><title>Overscore CLI</title></head>` +
|
|
1960
|
+
`<body style="font-family:sans-serif;text-align:center;padding:3rem">` +
|
|
1961
|
+
`<h2>Authenticated! You can close this tab.</h2></body></html>`);
|
|
1962
|
+
resolveCallback({ token, project });
|
|
1963
|
+
});
|
|
1964
|
+
server.listen(chosenPort, "127.0.0.1");
|
|
1965
|
+
const loginUrl = `${HUB_URL}/login?next=${encodeURIComponent(`/api/cli/auth/finalize?port=${chosenPort}&state=${state}`)}&mode=signin`;
|
|
1966
|
+
console.log(`\n Opening browser to authenticate...`);
|
|
1967
|
+
console.log(`\n If your browser doesn't open automatically, visit:\n ${loginUrl}\n`);
|
|
1840
1968
|
try {
|
|
1841
|
-
const
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
const data = (await res.json());
|
|
1848
|
-
projectSlug = data.project_slug || null;
|
|
1849
|
-
if (projectSlug) {
|
|
1850
|
-
console.log(`\n Detected project: ${projectSlug}`);
|
|
1851
|
-
}
|
|
1969
|
+
const platform = process.platform;
|
|
1970
|
+
if (platform === "darwin") {
|
|
1971
|
+
execSync(`open '${loginUrl}'`);
|
|
1972
|
+
}
|
|
1973
|
+
else if (platform === "win32") {
|
|
1974
|
+
execSync(`start "" "${loginUrl}"`);
|
|
1852
1975
|
}
|
|
1853
1976
|
else {
|
|
1854
|
-
|
|
1855
|
-
process.exit(1);
|
|
1977
|
+
execSync(`xdg-open '${loginUrl}'`);
|
|
1856
1978
|
}
|
|
1857
1979
|
}
|
|
1858
1980
|
catch {
|
|
1859
|
-
|
|
1860
|
-
process.exit(1);
|
|
1981
|
+
// Browser open failed — user can follow the printed URL
|
|
1861
1982
|
}
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1983
|
+
// Wait up to 120 seconds for the callback
|
|
1984
|
+
const timeout = setTimeout(() => {
|
|
1985
|
+
console.error("\n Error: Timed out waiting for browser login (120s).\n");
|
|
1986
|
+
server.close();
|
|
1987
|
+
process.exit(1);
|
|
1988
|
+
}, 120000);
|
|
1989
|
+
const { token: deviceToken, project: projectSlug } = await callbackPromise;
|
|
1990
|
+
clearTimeout(timeout);
|
|
1991
|
+
server.close();
|
|
1992
|
+
// Preserve any existing os_ API key so dashboard .env fallback still works
|
|
1993
|
+
let existingApiKey = "";
|
|
1994
|
+
if (fs.existsSync(configPath)) {
|
|
1995
|
+
const existing = parseEnv(fs.readFileSync(configPath, "utf-8"));
|
|
1996
|
+
existingApiKey = existing.OVERSCORE_API_KEY || "";
|
|
1868
1997
|
}
|
|
1869
1998
|
fs.mkdirSync(configDir, { recursive: true });
|
|
1870
|
-
|
|
1999
|
+
const lines = [
|
|
2000
|
+
`OVERSCORE_API_URL=${HUB_URL}/api`,
|
|
2001
|
+
`OVERSCORE_DEVICE_TOKEN=${deviceToken}`,
|
|
2002
|
+
];
|
|
2003
|
+
if (projectSlug)
|
|
2004
|
+
lines.push(`OVERSCORE_PROJECT_SLUG=${projectSlug}`);
|
|
2005
|
+
if (existingApiKey)
|
|
2006
|
+
lines.push(`OVERSCORE_API_KEY=${existingApiKey}`);
|
|
2007
|
+
fs.writeFileSync(configPath, lines.join("\n") + "\n", { mode: 0o600 });
|
|
1871
2008
|
console.log(`
|
|
1872
2009
|
Authenticated!
|
|
1873
2010
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
You're ready to use the CLI.
|
|
2011
|
+
Device token saved to ~/.overscore/config.
|
|
2012
|
+
You won't need to copy API keys manually — create-overscore and pull
|
|
2013
|
+
will authenticate automatically from now on.
|
|
1879
2014
|
`);
|
|
1880
2015
|
}
|
|
1881
2016
|
async function authLogout() {
|
|
1882
2017
|
const configPath = path.join(process.env.HOME || "", ".overscore", "config");
|
|
1883
2018
|
if (fs.existsSync(configPath)) {
|
|
2019
|
+
// Attempt server-side revocation of the device token (non-fatal)
|
|
2020
|
+
try {
|
|
2021
|
+
const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
|
|
2022
|
+
const deviceToken = cfg.OVERSCORE_DEVICE_TOKEN;
|
|
2023
|
+
if (deviceToken) {
|
|
2024
|
+
await fetch(`${HUB_URL}/api/cli/auth/revoke`, {
|
|
2025
|
+
method: "POST",
|
|
2026
|
+
headers: { Authorization: `Bearer ${deviceToken}` },
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
catch {
|
|
2031
|
+
// Ignore — Hub may be unreachable
|
|
2032
|
+
}
|
|
1884
2033
|
fs.unlinkSync(configPath);
|
|
1885
2034
|
console.log("\n Logged out. Global config removed.\n");
|
|
1886
2035
|
}
|
|
@@ -1927,7 +2076,10 @@ ${installed.map((s) => ` - ${s}`).join("\n")}
|
|
|
1927
2076
|
// ── Routing ─────────────────────────────────────────────────────────
|
|
1928
2077
|
const command = process.argv[2];
|
|
1929
2078
|
const subcommand = process.argv[3];
|
|
1930
|
-
if (command === "
|
|
2079
|
+
if (command === "dev") {
|
|
2080
|
+
dev();
|
|
2081
|
+
}
|
|
2082
|
+
else if (command === "deploy") {
|
|
1931
2083
|
deploy();
|
|
1932
2084
|
}
|
|
1933
2085
|
else if (command === "pull") {
|
|
@@ -2036,8 +2188,9 @@ else {
|
|
|
2036
2188
|
|
|
2037
2189
|
Commands:
|
|
2038
2190
|
auth Check authentication status
|
|
2039
|
-
auth login
|
|
2191
|
+
auth login Authenticate via browser (one time per machine)
|
|
2040
2192
|
auth logout Remove saved credentials
|
|
2193
|
+
dev Start the dev server (injects auth automatically)
|
|
2041
2194
|
list List all dashboards in the project
|
|
2042
2195
|
deploy Build and deploy the dashboard
|
|
2043
2196
|
pull <slug> Pull source code from the hub
|
|
@@ -2047,6 +2200,8 @@ else {
|
|
|
2047
2200
|
install-skills Install the Overscore skill library into ~/.claude/skills/
|
|
2048
2201
|
|
|
2049
2202
|
Usage:
|
|
2203
|
+
npx @overscore/cli auth login
|
|
2204
|
+
npx @overscore/cli dev
|
|
2050
2205
|
npx @overscore/cli list
|
|
2051
2206
|
npx @overscore/cli deploy --message "description"
|
|
2052
2207
|
npx @overscore/cli pull <slug>
|