#!/usr/bin/env node // src/cli.ts import { readFileSync, writeFileSync, mkdirSync } from "fs"; import { join } from "path"; import { homedir } from "os"; var CONFIG_DIR = join(homedir(), ".config", "cfg"); var CONFIG_FILE = join(CONFIG_DIR, "config.json"); var CACHE_FILE = join(CONFIG_DIR, "cache.json"); var DEFAULT_ENDPOINT = "https://config.shazhou.work"; var ONE_DAY = 24 * 60 * 60 * 1000; var TWO_HOURS = 2 * 60 * 60 * 1000; function loadConfig() { try { return JSON.parse(readFileSync(CONFIG_FILE, "utf-8")); } catch { return {}; } } function saveConfig(cfg) { mkdirSync(CONFIG_DIR, { recursive: true }); writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + ` `); } function loadCache() { try { return JSON.parse(readFileSync(CACHE_FILE, "utf-8")); } catch { return null; } } function saveCache(cache) { mkdirSync(CONFIG_DIR, { recursive: true }); writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2) + ` `); } function getToken() { const cfg = loadConfig(); const token = process.env.CFG_TOKEN || cfg.token; if (!token) { console.error("No token configured. Run: cfg token "); process.exit(1); } return token; } function getEndpoint() { const cfg = loadConfig(); return cfg.endpoint || process.env.CFG_ENDPOINT || DEFAULT_ENDPOINT; } async function api(method, path, body) { const url = `${getEndpoint()}${path}`; const headers = { Authorization: `Bearer ${getToken()}`, "Content-Type": "application/json" }; let res; try { res = await fetch(url, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined }); } catch (err) { console.error(`Error: ${err?.cause?.message || err?.message || "network error"}`); process.exit(1); } if (res.status === 204) return { data: null, status: 204 }; const text = await res.text(); let data; try { data = JSON.parse(text); } catch { data = text; } if (!res.ok) { const msg = data?.error || text; console.error(`Error: ${msg}`); process.exit(1); } return { data, status: res.status }; } async function trySyncRemote() { const url = `${getEndpoint()}/config`; const headers = { Authorization: `Bearer ${getToken()}`, "Content-Type": "application/json" }; try { const res = await fetch(url, { method: "GET", headers }); if (!res.ok) return false; const data = await res.json(); if (!data?.secrets) return false; const now = new Date().toISOString(); const cache = { agent_id: data.agent_id, secrets: data.secrets, synced_at: now, attempted_at: now }; saveCache(cache); return true; } catch { const cache = loadCache(); if (cache) { cache.attempted_at = new Date().toISOString(); saveCache(cache); } return false; } } function shouldAutoSync(cache) { if (!cache) return true; const now = Date.now(); const syncedAt = new Date(cache.synced_at).getTime(); const attemptedAt = new Date(cache.attempted_at || cache.synced_at).getTime(); if (syncedAt >= attemptedAt && now - syncedAt < ONE_DAY) { return false; } if (now - attemptedAt < TWO_HOURS) { return false; } return true; } async function cmdSync() { const { data } = await api("GET", "/config"); if (!data?.secrets) { console.error("No secrets found"); process.exit(1); } const now = new Date().toISOString(); const cache = { agent_id: data.agent_id, secrets: data.secrets, synced_at: now, attempted_at: now }; saveCache(cache); const count = Object.keys(cache.secrets).length; console.log(`✓ Synced ${count} keys (agent: ${cache.agent_id})`); } async function cmdEnv() { let cache = loadCache(); if (shouldAutoSync(cache)) { const ok = await trySyncRemote(); if (ok) { cache = loadCache(); } else if (!cache) { console.error("No local cache and sync failed. Run: cfg sync"); process.exit(1); } } for (const [key, entry] of Object.entries(cache.secrets).sort(([a], [b]) => a.localeCompare(b))) { if (entry.env === false) continue; const escaped = entry.value.replace(/'/g, "'\\''"); console.log(`export ${key}='${escaped}'`); } } async function cmdGet(key, remote) { if (remote) { const { data } = await api("GET", `/config/${encodeURIComponent(key)}`); console.log(data.value); return; } let cache = loadCache(); if (shouldAutoSync(cache)) { const ok = await trySyncRemote(); if (ok) cache = loadCache(); else if (!cache) { console.error("No local cache and sync failed. Run: cfg sync"); process.exit(1); } } if (!cache) { console.error("No local cache. Run: cfg sync"); process.exit(1); } const entry = cache.secrets[key]; if (!entry) { console.error(`Error: ${key} not found in cache. Try: cfg sync`); process.exit(1); } console.log(entry.value); } async function cmdSet(args) { const shared = args.includes("--shared"); const noEnv = args.includes("--no-env"); const isSecret = args.includes("--secret"); const filtered = args.filter((a) => !["--shared", "--no-env", "--secret"].includes(a)); if (filtered.length < 2) { console.error("Usage: cfg set [--shared] [--no-env] [--secret] "); process.exit(1); } const [key, ...rest] = filtered; const value = rest.join(" "); const scope = shared ? "shared" : "personal"; const body = { value, env: !noEnv, secret: isSecret }; await api("PUT", `/config/${encodeURIComponent(key)}?scope=${scope}`, body); const flags = []; if (noEnv) flags.push("no-env"); if (isSecret) flags.push("secret"); const flagStr = flags.length ? ` [${flags.join(", ")}]` : ""; console.log(`✓ ${key} (${scope})${flagStr}`); const cache = loadCache(); if (cache) { cache.secrets[key] = { value, scope, env: !noEnv, secret: isSecret, updated_at: new Date().toISOString() }; saveCache(cache); } } async function cmdUnset(args) { const shared = args.includes("--shared"); const filtered = args.filter((a) => a !== "--shared"); if (filtered.length < 1) { console.error("Usage: cfg unset [--shared] "); process.exit(1); } const key = filtered[0]; const scope = shared ? "shared" : "personal"; await api("DELETE", `/config/${encodeURIComponent(key)}?scope=${scope}`); console.log(`✓ Deleted ${key} (${scope})`); const cache = loadCache(); if (cache && cache.secrets[key]) { delete cache.secrets[key]; saveCache(cache); } } async function cmdList() { let cache = loadCache(); if (shouldAutoSync(cache)) { const ok = await trySyncRemote(); if (ok) cache = loadCache(); else if (!cache) { console.error("No local cache and sync failed. Run: cfg sync"); process.exit(1); } } if (!cache) { console.error("No local cache. Run: cfg sync"); process.exit(1); } if (!cache.secrets || Object.keys(cache.secrets).length === 0) { console.log("No keys found"); return; } const keys = Object.keys(cache.secrets).sort(); const maxLen = Math.max(...keys.map((k) => k.length)); for (const key of keys) { const entry = cache.secrets[key]; const scope = entry.scope === "personal" ? "\x1B[32mpersonal\x1B[0m" : "\x1B[34mshared\x1B[0m "; const flags = []; if (entry.env === false) flags.push("\x1B[33mno-env\x1B[0m"); if (entry.secret) flags.push("\x1B[31msecret\x1B[0m"); const flagStr = flags.length ? " " + flags.join(" ") : ""; console.log(` ${key.padEnd(maxLen + 2)}${scope}${flagStr}`); } console.log(` Total: ${keys.length} keys (agent: ${cache.agent_id})`); console.log(`Last sync: ${cache.synced_at}`); } function cmdToken(token) { const cfg = loadConfig(); cfg.token = token; saveConfig(cfg); console.log("✓ Token saved"); } async function cmdAdminAddUser(args) { const role = args.includes("--admin") ? "admin" : "agent"; const filtered = args.filter((a) => a !== "--admin"); if (!filtered[0]) { console.error("Usage: cfg admin add [--admin]"); process.exit(1); } const { data } = await api("POST", "/admin/token", { agent_id: filtered[0], role }); console.log(`✓ Created ${data.role} token for ${data.agent_id}`); console.log(` Token: ${data.token}`); } async function cmdAdminRemoveUser(args) { if (!args[0]) { console.error("Usage: cfg admin remove "); process.exit(1); } const { data } = await api("DELETE", `/admin/token/${encodeURIComponent(args[0])}`); console.log(`✓ Revoked ${data.tokens_revoked} token(s) for ${data.agent_id}`); } async function cmdAdminListAgents() { const { data } = await api("GET", "/admin/agents"); if (!data.agents?.length) { console.log("No agents found"); return; } console.log("Agents:"); for (const agent of data.agents) { console.log(` ${agent}`); } } async function cmdAdminRefreshToken(args) { if (!args[0]) { console.error("Usage: cfg admin refresh "); process.exit(1); } const agentId = args[0]; await api("DELETE", `/admin/token/${encodeURIComponent(agentId)}`); const { data } = await api("POST", "/admin/token", { agent_id: agentId }); console.log(`✓ Refreshed token for ${data.agent_id}`); console.log(` Token: ${data.token}`); } async function cmdAdminInspect(args) { if (!args[0]) { console.error("Usage: cfg admin inspect "); process.exit(1); } const { data } = await api("GET", `/admin/agent/${encodeURIComponent(args[0])}`); if (!data.secrets || Object.keys(data.secrets).length === 0) { console.log(`No keys for ${data.agent_id}`); return; } const keys = Object.keys(data.secrets).sort(); const maxLen = Math.max(...keys.map((k) => k.length)); for (const key of keys) { const entry = data.secrets[key]; const scope = entry.scope === "personal" ? "\x1B[32mpersonal\x1B[0m" : "\x1B[34mshared\x1B[0m "; console.log(` ${key.padEnd(maxLen + 2)}${scope}`); } console.log(` Total: ${keys.length} keys (agent: ${data.agent_id})`); } async function cmdFlags(args) { const shared = args.includes("--shared"); const filtered = args.filter((a) => !["--shared"].includes(a)); if (filtered.length < 1) { console.error("Usage: cfg flags [--shared] [--env|--no-env] [--secret|--no-secret]"); process.exit(1); } const key = filtered[0]; const flagArgs = filtered.slice(1); const body = {}; if (flagArgs.includes("--env")) body.env = true; if (flagArgs.includes("--no-env")) body.env = false; if (flagArgs.includes("--secret")) body.secret = true; if (flagArgs.includes("--no-secret")) body.secret = false; if (Object.keys(body).length === 0) { // Just show current flags const { data } = await api("GET", `/config/${encodeURIComponent(key)}`); console.log(`${key}: env=${data.env ?? true}, secret=${data.secret ?? false} (${data.scope})`); return; } const scope = shared ? "shared" : "personal"; const { data } = await api("PATCH", `/config/${encodeURIComponent(key)}?scope=${scope}`, body); console.log(`✓ ${key} flags updated: env=${data.env}, secret=${data.secret}`); // Update cache const cache = loadCache(); if (cache && cache.secrets[key]) { if (body.env !== undefined) cache.secrets[key].env = body.env; if (body.secret !== undefined) cache.secrets[key].secret = body.secret; saveCache(cache); } } function showHelp() { console.log(`cfg — config.shazhou.work CLI Usage: cfg sync Fetch all config to local cache cfg env Output export statements (from cache, auto-syncs if stale) cfg get Read a key (from cache) cfg get --remote Read a key (from server) cfg set Write to personal scope cfg set --shared Write to shared scope (admin only) cfg set --no-env Mark as non-env (won't export via cfg env) cfg set --secret Mark as secret (sensitive, UI masked) cfg flags Show flags for a key cfg flags --no-env Set no-env flag cfg flags --secret Set secret flag cfg unset Delete from personal scope cfg unset --shared Delete from shared scope (admin only) cfg list List all keys with scope (from cache) cfg token Save auth token Admin: cfg admin agents List all agents cfg admin add [--admin] Create agent token (default role: agent) cfg admin remove Revoke all tokens for an agent cfg admin refresh Revoke + recreate token cfg admin inspect View an agent's resolved config Auto-sync: cfg env auto-syncs when cache is stale (>1 day since last success, or last sync failed and >2 hours since last attempt). Failed syncs fall back to stale cache silently. Environment: CFG_TOKEN Auth token (overrides saved token) CFG_ENDPOINT API endpoint (default: ${DEFAULT_ENDPOINT}) Shell setup: eval $(cfg env) Add to .profile / .bashrc / .zshrc`); } var [cmd, ...args] = process.argv.slice(2); switch (cmd) { case "sync": await cmdSync(); break; case "env": await cmdEnv(); break; case "get": { const remote = args.includes("--remote"); const filtered = args.filter((a) => a !== "--remote"); if (!filtered[0]) { console.error("Usage: cfg get [--remote] "); process.exit(1); } await cmdGet(filtered[0], remote); break; } case "set": await cmdSet(args); break; case "unset": await cmdUnset(args); break; case "flags": await cmdFlags(args); break; case "list": await cmdList(); break; case "token": if (!args[0]) { console.error("Usage: cfg token "); process.exit(1); } cmdToken(args[0]); break; case "admin": { const [sub, ...subArgs] = args; switch (sub) { case "agents": await cmdAdminListAgents(); break; case "add": await cmdAdminAddUser(subArgs); break; case "remove": await cmdAdminRemoveUser(subArgs); break; case "refresh": await cmdAdminRefreshToken(subArgs); break; case "inspect": await cmdAdminInspect(subArgs); break; default: console.error(`Unknown admin command: ${sub}`); showHelp(); process.exit(1); } break; } case "help": case "--help": case "-h": case undefined: showHelp(); break; default: console.error(`Unknown command: ${cmd}`); showHelp(); process.exit(1); }