@overscore/cli 0.10.2 → 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.
Files changed (2) hide show
  1. package/dist/index.js +258 -106
  2. 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) {
@@ -641,11 +643,43 @@ async function pull(slug) {
641
643
  const versionParam = versionIndex !== -1 && process.argv[versionIndex + 1]
642
644
  ? process.argv[versionIndex + 1]
643
645
  : null;
644
- // Interactive prompts for credentials
645
- const apiKey = await prompt("API key (from the Hub):");
646
- if (!apiKey || !apiKey.startsWith("os_")) {
647
- console.error("\n Error: API key should start with os_\n");
648
- process.exit(1);
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
+ }
649
683
  }
650
684
  const apiUrl = "https://overscore.dev/api";
651
685
  const baseUrl = apiUrl.replace(/\/api$/, "");
@@ -667,10 +701,10 @@ async function pull(slug) {
667
701
  console.error(`\n Error: ${errorMsg}\n`);
668
702
  process.exit(1);
669
703
  }
670
- const projectSlug = res.headers.get("X-VD-Project-Slug") || "";
671
- const dashboardSlug = res.headers.get("X-VD-Dashboard-Slug") || slug;
672
- const version = res.headers.get("X-VD-Version") || "?";
673
- const commitMsg = res.headers.get("X-VD-Commit-Message") || "";
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") || "";
674
708
  // Download the zip
675
709
  const zipBuffer = Buffer.from(await res.arrayBuffer());
676
710
  // Extract to ./<slug>/
@@ -695,11 +729,10 @@ async function pull(slug) {
695
729
  fs.mkdirSync(targetDir, { recursive: true });
696
730
  }
697
731
  await extractZip(zipBuffer, targetDir);
698
- // Generate .env (only overscore configuser adds their own API keys)
732
+ // Generate .env (no API key'npx @overscore/cli dev' injects it at runtime)
699
733
  const envContent = `# Overscore Configuration
700
734
  VITE_OVERSCORE_PROJECT_SLUG=${projectSlug}
701
735
  VITE_OVERSCORE_DASHBOARD_SLUG=${dashboardSlug}
702
- VITE_OVERSCORE_API_KEY=${apiKey}
703
736
  VITE_OVERSCORE_API_URL=https://overscore.dev/api
704
737
  `;
705
738
  fs.writeFileSync(path.join(targetDir, ".env"), envContent, { mode: 0o600 });
@@ -752,11 +785,7 @@ VITE_OVERSCORE_API_URL=https://overscore.dev/api
752
785
 
753
786
  cd ${slug}
754
787
  npm install
755
- code .
756
- claude
757
-
758
- Note: If this dashboard uses third-party APIs, add your
759
- API keys to .env before running.
788
+ npx @overscore/cli dev # starts dev server with auth injected
760
789
 
761
790
  When ready to deploy:
762
791
 
@@ -1758,67 +1787,122 @@ function analysisPreview() {
1758
1787
  process.exit(1);
1759
1788
  }
1760
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
+ }
1761
1889
  // ── auth ────────────────────────────────────────────────────────────
1762
1890
  async function authStatus() {
1763
- const configDir = path.join(process.env.HOME || "", ".overscore");
1764
- const configPath = path.join(configDir, "config");
1765
- // Check global config
1891
+ const configPath = path.join(process.env.HOME || "", ".overscore", "config");
1766
1892
  if (fs.existsSync(configPath)) {
1767
1893
  const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
1768
- const key = cfg.OVERSCORE_API_KEY;
1894
+ const deviceToken = cfg.OVERSCORE_DEVICE_TOKEN;
1769
1895
  const project = cfg.OVERSCORE_PROJECT_SLUG;
1770
- if (key && project) {
1771
- // Verify the key is still valid
1772
- let valid = false;
1773
- try {
1774
- const hubUrl = cfg.OVERSCORE_API_URL?.replace(/\/api$/, "") || HUB_URL;
1775
- const res = await fetch(`${hubUrl}/api/projects/verify-key`, {
1776
- method: "POST",
1777
- headers: { "Content-Type": "application/json" },
1778
- body: JSON.stringify({ key }),
1779
- });
1780
- if (res.ok) {
1781
- const data = (await res.json());
1782
- valid = true;
1783
- console.log(`
1896
+ if (deviceToken) {
1897
+ console.log(`
1784
1898
  Authenticated
1785
1899
 
1786
- Key: ${key.slice(0, 10)}...
1787
- Project: ${data.project_slug || project}
1788
- Config: ~/.overscore/config
1789
- Status: Valid
1790
- `);
1791
- }
1792
- }
1793
- catch {
1794
- // network error
1795
- }
1796
- if (!valid) {
1797
- console.log(`
1798
- Authentication problem
1799
-
1800
- Key: ${key.slice(0, 10)}...
1801
- Project: ${project}
1802
- Config: ~/.overscore/config
1803
- Status: Invalid or expired
1804
-
1805
- Run 'npx @overscore/cli auth login' to update your key.
1806
- `);
1807
- }
1808
- return;
1809
- }
1810
- }
1811
- // Check local .env
1812
- const envPath = path.resolve(process.cwd(), ".env");
1813
- if (fs.existsSync(envPath)) {
1814
- const env = parseEnv(fs.readFileSync(envPath, "utf-8"));
1815
- if (env.VITE_OVERSCORE_API_KEY) {
1816
- console.log(`
1817
- Using local .env (this project only)
1900
+ Device token: ${deviceToken.slice(0, 12)}...
1901
+ Project: ${project || "—"}
1902
+ Config: ~/.overscore/config
1818
1903
 
1819
- Key: ${env.VITE_OVERSCORE_API_KEY.slice(0, 10)}...
1820
- Project: ${env.VITE_OVERSCORE_PROJECT_SLUG || "—"}
1821
- 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.
1822
1906
  `);
1823
1907
  return;
1824
1908
  }
@@ -1826,64 +1910,126 @@ async function authStatus() {
1826
1910
  console.log(`
1827
1911
  Not authenticated
1828
1912
 
1829
- No API key found. Run 'npx @overscore/cli auth login' to set one up.
1913
+ Run 'npx @overscore/cli auth login' to authenticate.
1830
1914
  `);
1831
1915
  }
1832
1916
  async function authLogin() {
1833
1917
  const configDir = path.join(process.env.HOME || "", ".overscore");
1834
1918
  const configPath = path.join(configDir, "config");
1835
- console.log("\n Set up Overscore CLI authentication.\n");
1836
- const apiKey = await prompt(" API key (from the Hub):");
1837
- if (!apiKey || !apiKey.startsWith("os_")) {
1838
- console.error("\n Error: API key should start with os_\n");
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");
1839
1936
  process.exit(1);
1840
1937
  }
1841
- // Auto-resolve project from key
1842
- let projectSlug = null;
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`);
1843
1968
  try {
1844
- const res = await fetch(`${HUB_URL}/api/projects/verify-key`, {
1845
- method: "POST",
1846
- headers: { "Content-Type": "application/json" },
1847
- body: JSON.stringify({ key: apiKey }),
1848
- });
1849
- if (res.ok) {
1850
- const data = (await res.json());
1851
- projectSlug = data.project_slug || null;
1852
- if (projectSlug) {
1853
- console.log(`\n Detected project: ${projectSlug}`);
1854
- }
1969
+ const platform = process.platform;
1970
+ if (platform === "darwin") {
1971
+ execSync(`open '${loginUrl}'`);
1972
+ }
1973
+ else if (platform === "win32") {
1974
+ execSync(`start "" "${loginUrl}"`);
1855
1975
  }
1856
1976
  else {
1857
- console.error("\n Error: API key is invalid or expired.\n");
1858
- process.exit(1);
1977
+ execSync(`xdg-open '${loginUrl}'`);
1859
1978
  }
1860
1979
  }
1861
1980
  catch {
1862
- console.error("\n Error: Could not reach overscore.dev to verify key.\n");
1863
- process.exit(1);
1981
+ // Browser open failed user can follow the printed URL
1864
1982
  }
1865
- if (!projectSlug) {
1866
- projectSlug = await prompt(" Project slug:");
1867
- if (!projectSlug) {
1868
- console.error("\n Error: Project slug is required\n");
1869
- process.exit(1);
1870
- }
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 || "";
1871
1997
  }
1872
1998
  fs.mkdirSync(configDir, { recursive: true });
1873
- fs.writeFileSync(configPath, `OVERSCORE_API_URL=${HUB_URL}/api\nOVERSCORE_API_KEY=${apiKey}\nOVERSCORE_PROJECT_SLUG=${projectSlug}\n`, { mode: 0o600 });
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 });
1874
2008
  console.log(`
1875
2009
  Authenticated!
1876
2010
 
1877
- Key: ${apiKey.slice(0, 10)}...
1878
- Project: ${projectSlug}
1879
- Config: ~/.overscore/config
1880
-
1881
- 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.
1882
2014
  `);
1883
2015
  }
1884
2016
  async function authLogout() {
1885
2017
  const configPath = path.join(process.env.HOME || "", ".overscore", "config");
1886
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
+ }
1887
2033
  fs.unlinkSync(configPath);
1888
2034
  console.log("\n Logged out. Global config removed.\n");
1889
2035
  }
@@ -1930,7 +2076,10 @@ ${installed.map((s) => ` - ${s}`).join("\n")}
1930
2076
  // ── Routing ─────────────────────────────────────────────────────────
1931
2077
  const command = process.argv[2];
1932
2078
  const subcommand = process.argv[3];
1933
- if (command === "deploy") {
2079
+ if (command === "dev") {
2080
+ dev();
2081
+ }
2082
+ else if (command === "deploy") {
1934
2083
  deploy();
1935
2084
  }
1936
2085
  else if (command === "pull") {
@@ -2039,8 +2188,9 @@ else {
2039
2188
 
2040
2189
  Commands:
2041
2190
  auth Check authentication status
2042
- auth login Set up or update your API key
2191
+ auth login Authenticate via browser (one time per machine)
2043
2192
  auth logout Remove saved credentials
2193
+ dev Start the dev server (injects auth automatically)
2044
2194
  list List all dashboards in the project
2045
2195
  deploy Build and deploy the dashboard
2046
2196
  pull <slug> Pull source code from the hub
@@ -2050,6 +2200,8 @@ else {
2050
2200
  install-skills Install the Overscore skill library into ~/.claude/skills/
2051
2201
 
2052
2202
  Usage:
2203
+ npx @overscore/cli auth login
2204
+ npx @overscore/cli dev
2053
2205
  npx @overscore/cli list
2054
2206
  npx @overscore/cli deploy --message "description"
2055
2207
  npx @overscore/cli pull <slug>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overscore/cli",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"