import { loadConfig, saveConfig, loadCache, saveCache, ONE_DAY, TWO_HOURS, DEFAULT_ENDPOINT, type Cache, } from "./config.js"; import { api, trySyncRemote } from "./api.js"; export function shouldAutoSync(cache: Cache | null): boolean { 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; } export async function cmdSync(): Promise { 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})`); } export async function cmdEnv(): Promise { 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}'`); } } export async function cmdGet(key: string, remote: boolean): Promise { 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); } export async function cmdSet(args: string[]): Promise { 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: string[] = []; 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); } } export async function cmdUnset(args: string[]): Promise { 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); } } export async function cmdList(): Promise { 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: string[] = []; 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(`\nTotal: ${keys.length} keys (agent: ${cache.agent_id})`); console.log(`Last sync: ${cache.synced_at}`); } export function cmdToken(token: string): void { const cfg = loadConfig(); cfg.token = token; saveConfig(cfg); console.log("✓ Token saved"); } export async function cmdFlags(args: string[]): Promise { 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: Record = {}; 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); } } export async function cmdAdminAddUser(args: string[]): Promise { 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}`); } export async function cmdAdminRemoveUser(args: string[]): Promise { 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}`); } export async function cmdAdminListAgents(): Promise { 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}`); } } export async function cmdAdminRefreshToken(args: string[]): Promise { 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}`); } export async function cmdAdminInspect(args: string[]): Promise { 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(`\nTotal: ${keys.length} keys (agent: ${data.agent_id})`); } export function showHelp(): void { 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`); }