- Restructured as bun monorepo with packages/cfg and packages/worker - CLI rewritten in TypeScript with modular architecture - Published as @shazhou/cfg@1.0.0 (replaces @shazhou/config) - Deprecated @shazhou/config on npm - Removed legacy Python scripts and old cli-npm package
345 lines
11 KiB
TypeScript
345 lines
11 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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] <KEY> <VALUE>"
|
|
);
|
|
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<void> {
|
|
const shared = args.includes("--shared");
|
|
const filtered = args.filter((a) => a !== "--shared");
|
|
if (filtered.length < 1) {
|
|
console.error("Usage: cfg unset [--shared] <KEY>");
|
|
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<void> {
|
|
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<void> {
|
|
const shared = args.includes("--shared");
|
|
const filtered = args.filter((a) => !["--shared"].includes(a));
|
|
if (filtered.length < 1) {
|
|
console.error(
|
|
"Usage: cfg flags [--shared] <KEY> [--env|--no-env] [--secret|--no-secret]"
|
|
);
|
|
process.exit(1);
|
|
}
|
|
const key = filtered[0];
|
|
const flagArgs = filtered.slice(1);
|
|
const body: Record<string, boolean> = {};
|
|
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<void> {
|
|
const role = args.includes("--admin") ? "admin" : "agent";
|
|
const filtered = args.filter((a) => a !== "--admin");
|
|
if (!filtered[0]) {
|
|
console.error("Usage: cfg admin add <AGENT_ID> [--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<void> {
|
|
if (!args[0]) {
|
|
console.error("Usage: cfg admin remove <AGENT_ID>");
|
|
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<void> {
|
|
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<void> {
|
|
if (!args[0]) {
|
|
console.error("Usage: cfg admin refresh <AGENT_ID>");
|
|
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<void> {
|
|
if (!args[0]) {
|
|
console.error("Usage: cfg admin inspect <AGENT_ID>");
|
|
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 <KEY> Read a key (from cache)
|
|
cfg get --remote <KEY> Read a key (from server)
|
|
cfg set <KEY> <VALUE> Write to personal scope
|
|
cfg set --shared <KEY> <VALUE> Write to shared scope (admin only)
|
|
cfg set --no-env <KEY> <VALUE> Mark as non-env (won't export via cfg env)
|
|
cfg set --secret <KEY> <VALUE> Mark as secret (sensitive, UI masked)
|
|
cfg flags <KEY> Show flags for a key
|
|
cfg flags <KEY> --no-env Set no-env flag
|
|
cfg flags <KEY> --secret Set secret flag
|
|
cfg unset <KEY> Delete from personal scope
|
|
cfg unset --shared <KEY> Delete from shared scope (admin only)
|
|
cfg list List all keys with scope (from cache)
|
|
cfg token <TOKEN> Save auth token
|
|
|
|
Admin:
|
|
cfg admin agents List all agents
|
|
cfg admin add <ID> [--admin] Create agent token (default role: agent)
|
|
cfg admin remove <ID> Revoke all tokens for an agent
|
|
cfg admin refresh <ID> Revoke + recreate token
|
|
cfg admin inspect <ID> 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`);
|
|
}
|