refactor: bun monorepo, @shazhou/cfg CLI (TS), cleanup old cli/scripts

- 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
This commit is contained in:
团子 2026-04-21 02:59:24 +00:00
parent 211533346b
commit 117e334a07
13 changed files with 773 additions and 295 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
.wrangler/ .wrangler/
.npmrc

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "config-service",
"private": true,
"workspaces": ["packages/*"]
}

1
packages/cfg/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist/

17
packages/cfg/package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "@shazhou/cfg",
"version": "1.0.0",
"type": "module",
"bin": {
"cfg": "./dist/cli.js"
},
"files": ["dist"],
"scripts": {
"build": "bun build src/cli.ts --outfile dist/cli.js --target node --minify && { echo '#!/usr/bin/env node'; cat dist/cli.js; } > dist/cli.tmp && mv dist/cli.tmp dist/cli.js",
"prepublishOnly": "bun run build"
},
"publishConfig": {
"access": "public"
},
"license": "MIT"
}

70
packages/cfg/src/api.ts Normal file
View File

@ -0,0 +1,70 @@
import { getToken, getEndpoint, loadCache, saveCache } from "./config.js";
export async function api(
method: string,
path: string,
body?: unknown
): Promise<{ data: any; status: number }> {
const url = `${getEndpoint()}${path}`;
const headers: Record<string, string> = {
Authorization: `Bearer ${getToken()}`,
"Content-Type": "application/json",
};
let res: Response;
try {
res = await fetch(url, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
} catch (err: any) {
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: any;
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 };
}
export async function trySyncRemote(): Promise<boolean> {
const url = `${getEndpoint()}/config`;
const headers: Record<string, string> = {
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() as any;
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;
}
}

91
packages/cfg/src/cli.ts Normal file
View File

@ -0,0 +1,91 @@
import {
cmdSync,
cmdEnv,
cmdGet,
cmdSet,
cmdUnset,
cmdFlags,
cmdList,
cmdToken,
cmdAdminAddUser,
cmdAdminRemoveUser,
cmdAdminListAgents,
cmdAdminRefreshToken,
cmdAdminInspect,
showHelp,
} from "./commands.js";
const [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] <KEY>");
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 <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);
}

374
cli/cfg.js → packages/cfg/src/commands.ts Executable file → Normal file
View File

@ -1,119 +1,17 @@
#!/usr/bin/env node import {
loadConfig,
saveConfig,
loadCache,
saveCache,
ONE_DAY,
TWO_HOURS,
DEFAULT_ENDPOINT,
type Cache,
} from "./config.js";
import { api, trySyncRemote } from "./api.js";
// src/cli.ts export function shouldAutoSync(cache: Cache | null): boolean {
import { readFileSync, writeFileSync, mkdirSync } from "fs"; if (!cache) return true;
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 <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 now = Date.now();
const syncedAt = new Date(cache.synced_at).getTime(); const syncedAt = new Date(cache.synced_at).getTime();
const attemptedAt = new Date(cache.attempted_at || cache.synced_at).getTime(); const attemptedAt = new Date(cache.attempted_at || cache.synced_at).getTime();
@ -125,7 +23,8 @@ function shouldAutoSync(cache) {
} }
return true; return true;
} }
async function cmdSync() {
export async function cmdSync(): Promise<void> {
const { data } = await api("GET", "/config"); const { data } = await api("GET", "/config");
if (!data?.secrets) { if (!data?.secrets) {
console.error("No secrets found"); console.error("No secrets found");
@ -136,13 +35,14 @@ async function cmdSync() {
agent_id: data.agent_id, agent_id: data.agent_id,
secrets: data.secrets, secrets: data.secrets,
synced_at: now, synced_at: now,
attempted_at: now attempted_at: now,
}; };
saveCache(cache); saveCache(cache);
const count = Object.keys(cache.secrets).length; const count = Object.keys(cache.secrets).length;
console.log(`✓ Synced ${count} keys (agent: ${cache.agent_id})`); console.log(`✓ Synced ${count} keys (agent: ${cache.agent_id})`);
} }
async function cmdEnv() {
export async function cmdEnv(): Promise<void> {
let cache = loadCache(); let cache = loadCache();
if (shouldAutoSync(cache)) { if (shouldAutoSync(cache)) {
const ok = await trySyncRemote(); const ok = await trySyncRemote();
@ -153,13 +53,16 @@ async function cmdEnv() {
process.exit(1); process.exit(1);
} }
} }
for (const [key, entry] of Object.entries(cache.secrets).sort(([a], [b]) => a.localeCompare(b))) { for (const [key, entry] of Object.entries(cache!.secrets).sort(([a], [b]) =>
a.localeCompare(b)
)) {
if (entry.env === false) continue; if (entry.env === false) continue;
const escaped = entry.value.replace(/'/g, "'\\''"); const escaped = entry.value.replace(/'/g, "'\\''");
console.log(`export ${key}='${escaped}'`); console.log(`export ${key}='${escaped}'`);
} }
} }
async function cmdGet(key, remote) {
export async function cmdGet(key: string, remote: boolean): Promise<void> {
if (remote) { if (remote) {
const { data } = await api("GET", `/config/${encodeURIComponent(key)}`); const { data } = await api("GET", `/config/${encodeURIComponent(key)}`);
console.log(data.value); console.log(data.value);
@ -168,8 +71,7 @@ async function cmdGet(key, remote) {
let cache = loadCache(); let cache = loadCache();
if (shouldAutoSync(cache)) { if (shouldAutoSync(cache)) {
const ok = await trySyncRemote(); const ok = await trySyncRemote();
if (ok) if (ok) cache = loadCache();
cache = loadCache();
else if (!cache) { else if (!cache) {
console.error("No local cache and sync failed. Run: cfg sync"); console.error("No local cache and sync failed. Run: cfg sync");
process.exit(1); process.exit(1);
@ -186,13 +88,18 @@ async function cmdGet(key, remote) {
} }
console.log(entry.value); console.log(entry.value);
} }
async function cmdSet(args) {
export async function cmdSet(args: string[]): Promise<void> {
const shared = args.includes("--shared"); const shared = args.includes("--shared");
const noEnv = args.includes("--no-env"); const noEnv = args.includes("--no-env");
const isSecret = args.includes("--secret"); const isSecret = args.includes("--secret");
const filtered = args.filter((a) => !["--shared", "--no-env", "--secret"].includes(a)); const filtered = args.filter(
(a) => !["--shared", "--no-env", "--secret"].includes(a)
);
if (filtered.length < 2) { if (filtered.length < 2) {
console.error("Usage: cfg set [--shared] [--no-env] [--secret] <KEY> <VALUE>"); console.error(
"Usage: cfg set [--shared] [--no-env] [--secret] <KEY> <VALUE>"
);
process.exit(1); process.exit(1);
} }
const [key, ...rest] = filtered; const [key, ...rest] = filtered;
@ -200,18 +107,25 @@ async function cmdSet(args) {
const scope = shared ? "shared" : "personal"; const scope = shared ? "shared" : "personal";
const body = { value, env: !noEnv, secret: isSecret }; const body = { value, env: !noEnv, secret: isSecret };
await api("PUT", `/config/${encodeURIComponent(key)}?scope=${scope}`, body); await api("PUT", `/config/${encodeURIComponent(key)}?scope=${scope}`, body);
const flags = []; const flags: string[] = [];
if (noEnv) flags.push("no-env"); if (noEnv) flags.push("no-env");
if (isSecret) flags.push("secret"); if (isSecret) flags.push("secret");
const flagStr = flags.length ? ` [${flags.join(", ")}]` : ""; const flagStr = flags.length ? ` [${flags.join(", ")}]` : "";
console.log(`${key} (${scope})${flagStr}`); console.log(`${key} (${scope})${flagStr}`);
const cache = loadCache(); const cache = loadCache();
if (cache) { if (cache) {
cache.secrets[key] = { value, scope, env: !noEnv, secret: isSecret, updated_at: new Date().toISOString() }; cache.secrets[key] = {
value,
scope,
env: !noEnv,
secret: isSecret,
updated_at: new Date().toISOString(),
};
saveCache(cache); saveCache(cache);
} }
} }
async function cmdUnset(args) {
export async function cmdUnset(args: string[]): Promise<void> {
const shared = args.includes("--shared"); const shared = args.includes("--shared");
const filtered = args.filter((a) => a !== "--shared"); const filtered = args.filter((a) => a !== "--shared");
if (filtered.length < 1) { if (filtered.length < 1) {
@ -228,12 +142,12 @@ async function cmdUnset(args) {
saveCache(cache); saveCache(cache);
} }
} }
async function cmdList() {
export async function cmdList(): Promise<void> {
let cache = loadCache(); let cache = loadCache();
if (shouldAutoSync(cache)) { if (shouldAutoSync(cache)) {
const ok = await trySyncRemote(); const ok = await trySyncRemote();
if (ok) if (ok) cache = loadCache();
cache = loadCache();
else if (!cache) { else if (!cache) {
console.error("No local cache and sync failed. Run: cfg sync"); console.error("No local cache and sync failed. Run: cfg sync");
process.exit(1); process.exit(1);
@ -251,43 +165,95 @@ async function cmdList() {
const maxLen = Math.max(...keys.map((k) => k.length)); const maxLen = Math.max(...keys.map((k) => k.length));
for (const key of keys) { for (const key of keys) {
const entry = cache.secrets[key]; const entry = cache.secrets[key];
const scope = entry.scope === "personal" ? "\x1B[32mpersonal\x1B[0m" : "\x1B[34mshared\x1B[0m "; const scope =
const flags = []; 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.env === false) flags.push("\x1B[33mno-env\x1B[0m");
if (entry.secret) flags.push("\x1B[31msecret\x1B[0m"); if (entry.secret) flags.push("\x1B[31msecret\x1B[0m");
const flagStr = flags.length ? " " + flags.join(" ") : ""; const flagStr = flags.length ? " " + flags.join(" ") : "";
console.log(` ${key.padEnd(maxLen + 2)}${scope}${flagStr}`); console.log(` ${key.padEnd(maxLen + 2)}${scope}${flagStr}`);
} }
console.log(` console.log(`\nTotal: ${keys.length} keys (agent: ${cache.agent_id})`);
Total: ${keys.length} keys (agent: ${cache.agent_id})`);
console.log(`Last sync: ${cache.synced_at}`); console.log(`Last sync: ${cache.synced_at}`);
} }
function cmdToken(token) {
export function cmdToken(token: string): void {
const cfg = loadConfig(); const cfg = loadConfig();
cfg.token = token; cfg.token = token;
saveConfig(cfg); saveConfig(cfg);
console.log("✓ Token saved"); console.log("✓ Token saved");
} }
async function cmdAdminAddUser(args) {
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 role = args.includes("--admin") ? "admin" : "agent";
const filtered = args.filter((a) => a !== "--admin"); const filtered = args.filter((a) => a !== "--admin");
if (!filtered[0]) { if (!filtered[0]) {
console.error("Usage: cfg admin add <AGENT_ID> [--admin]"); console.error("Usage: cfg admin add <AGENT_ID> [--admin]");
process.exit(1); process.exit(1);
} }
const { data } = await api("POST", "/admin/token", { agent_id: filtered[0], role }); const { data } = await api("POST", "/admin/token", {
agent_id: filtered[0],
role,
});
console.log(`✓ Created ${data.role} token for ${data.agent_id}`); console.log(`✓ Created ${data.role} token for ${data.agent_id}`);
console.log(` Token: ${data.token}`); console.log(` Token: ${data.token}`);
} }
async function cmdAdminRemoveUser(args) {
export async function cmdAdminRemoveUser(args: string[]): Promise<void> {
if (!args[0]) { if (!args[0]) {
console.error("Usage: cfg admin remove <AGENT_ID>"); console.error("Usage: cfg admin remove <AGENT_ID>");
process.exit(1); process.exit(1);
} }
const { data } = await api("DELETE", `/admin/token/${encodeURIComponent(args[0])}`); const { data } = await api(
"DELETE",
`/admin/token/${encodeURIComponent(args[0])}`
);
console.log(`✓ Revoked ${data.tokens_revoked} token(s) for ${data.agent_id}`); console.log(`✓ Revoked ${data.tokens_revoked} token(s) for ${data.agent_id}`);
} }
async function cmdAdminListAgents() {
export async function cmdAdminListAgents(): Promise<void> {
const { data } = await api("GET", "/admin/agents"); const { data } = await api("GET", "/admin/agents");
if (!data.agents?.length) { if (!data.agents?.length) {
console.log("No agents found"); console.log("No agents found");
@ -298,7 +264,8 @@ async function cmdAdminListAgents() {
console.log(` ${agent}`); console.log(` ${agent}`);
} }
} }
async function cmdAdminRefreshToken(args) {
export async function cmdAdminRefreshToken(args: string[]): Promise<void> {
if (!args[0]) { if (!args[0]) {
console.error("Usage: cfg admin refresh <AGENT_ID>"); console.error("Usage: cfg admin refresh <AGENT_ID>");
process.exit(1); process.exit(1);
@ -309,12 +276,16 @@ async function cmdAdminRefreshToken(args) {
console.log(`✓ Refreshed token for ${data.agent_id}`); console.log(`✓ Refreshed token for ${data.agent_id}`);
console.log(` Token: ${data.token}`); console.log(` Token: ${data.token}`);
} }
async function cmdAdminInspect(args) {
export async function cmdAdminInspect(args: string[]): Promise<void> {
if (!args[0]) { if (!args[0]) {
console.error("Usage: cfg admin inspect <AGENT_ID>"); console.error("Usage: cfg admin inspect <AGENT_ID>");
process.exit(1); process.exit(1);
} }
const { data } = await api("GET", `/admin/agent/${encodeURIComponent(args[0])}`); const { data } = await api(
"GET",
`/admin/agent/${encodeURIComponent(args[0])}`
);
if (!data.secrets || Object.keys(data.secrets).length === 0) { if (!data.secrets || Object.keys(data.secrets).length === 0) {
console.log(`No keys for ${data.agent_id}`); console.log(`No keys for ${data.agent_id}`);
return; return;
@ -323,44 +294,16 @@ async function cmdAdminInspect(args) {
const maxLen = Math.max(...keys.map((k) => k.length)); const maxLen = Math.max(...keys.map((k) => k.length));
for (const key of keys) { for (const key of keys) {
const entry = data.secrets[key]; const entry = data.secrets[key];
const scope = entry.scope === "personal" ? "\x1B[32mpersonal\x1B[0m" : "\x1B[34mshared\x1B[0m "; const scope =
entry.scope === "personal"
? "\x1B[32mpersonal\x1B[0m"
: "\x1B[34mshared\x1B[0m ";
console.log(` ${key.padEnd(maxLen + 2)}${scope}`); console.log(` ${key.padEnd(maxLen + 2)}${scope}`);
} }
console.log(` console.log(`\nTotal: ${keys.length} keys (agent: ${data.agent_id})`);
Total: ${keys.length} keys (agent: ${data.agent_id})`);
} }
async function cmdFlags(args) {
const shared = args.includes("--shared"); export function showHelp(): void {
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 = {};
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 console.log(`cfg — config.shazhou.work CLI
Usage: Usage:
@ -399,76 +342,3 @@ Environment:
Shell setup: Shell setup:
eval $(cfg env) Add to .profile / .bashrc / .zshrc`); 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] <KEY>");
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 <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);
}

View File

@ -0,0 +1,73 @@
import { readFileSync, writeFileSync, mkdirSync } from "fs";
import { join } from "path";
import { homedir } from "os";
export const CONFIG_DIR = join(homedir(), ".config", "cfg");
export const CONFIG_FILE = join(CONFIG_DIR, "config.json");
export const CACHE_FILE = join(CONFIG_DIR, "cache.json");
export const DEFAULT_ENDPOINT = "https://config.shazhou.work";
export const ONE_DAY = 24 * 60 * 60 * 1000;
export const TWO_HOURS = 2 * 60 * 60 * 1000;
export interface Config {
token?: string;
endpoint?: string;
[key: string]: unknown;
}
export interface SecretEntry {
value: string;
scope?: string;
env?: boolean;
secret?: boolean;
updated_at?: string;
[key: string]: unknown;
}
export interface Cache {
agent_id: string;
secrets: Record<string, SecretEntry>;
synced_at: string;
attempted_at: string;
}
export function loadConfig(): Config {
try {
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
} catch {
return {};
}
}
export function saveConfig(cfg: Config): void {
mkdirSync(CONFIG_DIR, { recursive: true });
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
}
export function loadCache(): Cache | null {
try {
return JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
} catch {
return null;
}
}
export function saveCache(cache: Cache): void {
mkdirSync(CONFIG_DIR, { recursive: true });
writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2) + "\n");
}
export function getToken(): string {
const cfg = loadConfig();
const token = process.env.CFG_TOKEN || cfg.token;
if (!token) {
console.error("No token configured. Run: cfg token <TOKEN>");
process.exit(1);
}
return token;
}
export function getEndpoint(): string {
const cfg = loadConfig();
return cfg.endpoint || process.env.CFG_ENDPOINT || DEFAULT_ENDPOINT;
}

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}

381
packages/worker/src/ui.ts Normal file
View File

@ -0,0 +1,381 @@
export function renderUI(): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Config Service</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; color: #e1e4e8; min-height: 100vh; }
.login { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.login-box { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 32px; width: 360px; }
.login-box h1 { font-size: 20px; margin-bottom: 20px; color: #f0f6fc; }
.login-box input { width: 100%; padding: 10px 12px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; font-size: 14px; margin-bottom: 12px; }
.login-box input:focus { outline: none; border-color: #58a6ff; }
.login-box button { width: 100%; padding: 10px; background: #238636; border: none; border-radius: 6px; color: #fff; font-size: 14px; cursor: pointer; font-weight: 600; }
.login-box button:hover { background: #2ea043; }
.app { display: none; }
.header { background: #161b22; border-bottom: 1px solid #30363d; padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; }
.header h1 { font-size: 18px; font-weight: 600; }
.header .info { display: flex; align-items: center; gap: 12px; font-size: 13px; color: #8b949e; }
.badge { padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; }
.badge.admin { background: #da3633; color: #fff; }
.badge.agent { background: #1f6feb; color: #fff; }
.badge.shared { background: #388bfd26; color: #58a6ff; border: 1px solid #388bfd66; }
.badge.personal { background: #23863626; color: #3fb950; border: 1px solid #2ea04366; }
.toolbar { padding: 16px 24px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.toolbar input { padding: 8px 12px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; font-size: 13px; width: 240px; }
.toolbar select { padding: 8px 12px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; font-size: 13px; }
.toolbar button { padding: 8px 16px; border: 1px solid #30363d; border-radius: 6px; background: #21262d; color: #e1e4e8; font-size: 13px; cursor: pointer; }
.toolbar button:hover { background: #30363d; }
.toolbar button.primary { background: #238636; border-color: #238636; }
.toolbar button.primary:hover { background: #2ea043; }
.toolbar .spacer { flex: 1; }
.table-wrap { padding: 0 24px 24px; }
table { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }
th { text-align: left; padding: 10px 16px; background: #1c2128; font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid #30363d; }
td { padding: 10px 16px; border-bottom: 1px solid #21262d; font-size: 13px; vertical-align: middle; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: #1c2128; }
.val { font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; font-size: 12px; max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; color: #8b949e; }
.val.revealed { color: #e1e4e8; white-space: pre-wrap; word-break: break-all; }
.val:hover { color: #58a6ff; }
.time { color: #8b949e; font-size: 12px; }
.actions button { padding: 4px 10px; font-size: 12px; border-radius: 4px; border: 1px solid #30363d; background: #21262d; color: #e1e4e8; cursor: pointer; margin-right: 4px; }
.actions button:hover { background: #30363d; }
.actions button.danger { color: #f85149; }
.actions button.danger:hover { background: #da363322; }
.modal-overlay { display: none; position: fixed; inset: 0; background: #00000088; z-index: 100; align-items: center; justify-content: center; }
.modal-overlay.active { display: flex; }
.modal { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 24px; width: 440px; }
.modal h2 { font-size: 16px; margin-bottom: 16px; }
.modal label { display: block; font-size: 13px; color: #8b949e; margin-bottom: 4px; margin-top: 12px; }
.modal input, .modal textarea { width: 100%; padding: 8px 12px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; font-size: 13px; font-family: inherit; }
.modal textarea { min-height: 80px; resize: vertical; font-family: 'SF Mono', Monaco, monospace; }
.modal .btns { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
.modal .btns button { padding: 8px 16px; border-radius: 6px; border: none; font-size: 13px; cursor: pointer; font-weight: 600; }
.modal .cancel { background: #21262d; color: #e1e4e8; border: 1px solid #30363d; }
.modal .save { background: #238636; color: #fff; }
.agent-tabs { padding: 0 24px; display: flex; gap: 0; margin-bottom: 16px; flex-wrap: wrap; }
.agent-tab { padding: 8px 16px; font-size: 13px; border: 1px solid #30363d; background: #0d1117; color: #8b949e; cursor: pointer; border-bottom: none; }
.agent-tab:first-child { border-radius: 6px 0 0 0; }
.agent-tab:last-child { border-radius: 0 6px 0 0; }
.agent-tab.active { background: #161b22; color: #f0f6fc; border-bottom: 2px solid #58a6ff; }
.agent-tab:hover { color: #e1e4e8; }
.empty { text-align: center; padding: 40px; color: #8b949e; }
.toast { position: fixed; bottom: 24px; right: 24px; padding: 12px 20px; background: #238636; color: #fff; border-radius: 8px; font-size: 13px; z-index: 200; display: none; }
.toast.error { background: #da3633; }
</style>
</head>
<body>
<div class="login" id="loginView">
<div class="login-box">
<h1>🔑 Config Service</h1>
<input type="password" id="tokenInput" placeholder="Enter your token..." autofocus>
<button onclick="doLogin()">Sign In</button>
</div>
</div>
<div class="app" id="appView">
<div class="header">
<h1>🔑 Config Service</h1>
<div class="info">
<span id="agentName"></span>
<span id="roleBadge"></span>
<button onclick="doLogout()" style="padding:4px 12px;font-size:12px;border:1px solid #30363d;border-radius:4px;background:#21262d;color:#e1e4e8;cursor:pointer;">Logout</button>
</div>
</div>
<div class="agent-tabs" id="agentTabs" style="display:none;"></div>
<div class="toolbar">
<input type="text" id="searchInput" placeholder="Filter keys..." oninput="renderTable()">
<select id="scopeFilter" onchange="renderTable()">
<option value="all">All scopes</option>
<option value="shared">Shared only</option>
<option value="personal">Personal only</option>
</select>
<div class="spacer"></div>
<button class="primary" onclick="openAddModal()">+ Add Key</button>
<button onclick="loadData()"> Refresh</button>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Key</th><th>Value</th><th>Scope</th><th>Updated</th><th>Actions</th></tr></thead>
<tbody id="tbody"></tbody>
</table>
</div>
</div>
<div class="modal-overlay" id="addModal">
<div class="modal">
<h2 id="modalTitle">Add Key</h2>
<label>Key</label>
<input type="text" id="modalKey" placeholder="e.g. API_KEY">
<label>Value</label>
<textarea id="modalValue" placeholder="secret value"></textarea>
<label>Scope</label>
<select id="modalScope" style="width:100%;padding:8px 12px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e1e4e8;font-size:13px;">
<option value="personal">Personal</option>
<option value="shared">Shared (admin only)</option>
</select>
<div class="btns">
<button class="cancel" onclick="closeModal()">Cancel</button>
<button class="save" onclick="saveKey()">Save</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
let TOKEN = localStorage.getItem('cfg_token') || '';
let authInfo = null;
let allData = {};
let currentAgent = null; // for admin viewing other agents
let allAgents = []; // admin only: list of all agents
const BASE = '';
async function api(method, path, body) {
const opts = { method, headers: { 'Authorization': 'Bearer ' + TOKEN, 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
const r = await fetch(BASE + path, opts);
if (!r.ok) {
const e = await r.json().catch(() => ({ error: 'request failed' }));
throw new Error(e.error || 'request failed');
}
if (r.status === 204) return {};
return r.json();
}
async function doLogin() {
TOKEN = document.getElementById('tokenInput').value.trim();
if (!TOKEN) return;
try {
const data = await api('GET', '/config');
localStorage.setItem('cfg_token', TOKEN);
authInfo = { agent_id: data.agent_id, role: data.secrets ? 'admin' : 'agent' };
// Detect role from response
if (data.agent_id) {
authInfo.agent_id = data.agent_id;
}
document.getElementById('loginView').style.display = 'none';
document.getElementById('appView').style.display = 'block';
await initApp(data);
} catch (e) {
toast('Invalid token', true);
}
}
function doLogout() {
localStorage.removeItem('cfg_token');
TOKEN = '';
authInfo = null;
document.getElementById('loginView').style.display = '';
document.getElementById('appView').style.display = 'none';
}
async function initApp(data) {
// Get auth info by checking the whoami-like response
// The /config (list all) response includes agent_id
allData = data.secrets || {};
authInfo = { agent_id: data.agent_id, role: 'agent' };
// Check if admin by trying to list all agents (admin can see shared scope)
try {
// Try admin-level endpoint: list all personal keys across agents
const sharedList = await api('GET', '/config?scope=shared');
if (sharedList.keys) {
authInfo.role = 'admin';
}
} catch (e) {}
// Actually we can detect admin from the auth endpoint. For now, let's check
// if user can write shared scope as a test... or just show what we have.
// The sync response already gives us everything we need.
document.getElementById('agentName').textContent = data.agent_id;
const badge = document.getElementById('roleBadge');
badge.textContent = authInfo.role;
badge.className = 'badge ' + authInfo.role;
if (authInfo.role === 'admin') {
await loadAdminAgentList();
}
renderTable();
}
async function loadAdminAgentList() {
// Admin: discover agents by listing all personal: keys
try {
const resp = await api('GET', '/admin/agents');
if (resp.agents) {
allAgents = resp.agents;
renderAgentTabs();
}
} catch(e) {
// endpoint might not exist yet, just skip
}
}
function renderAgentTabs() {
const tabs = document.getElementById('agentTabs');
if (allAgents.length === 0) { tabs.style.display = 'none'; return; }
tabs.style.display = 'flex';
tabs.innerHTML = '<div class="agent-tab active" onclick="switchAgent(null)">My View</div>' +
allAgents.map(a => '<div class="agent-tab" onclick="switchAgent(\\'' + a + '\\')">' + a + '</div>').join('');
}
async function switchAgent(agentId) {
currentAgent = agentId;
document.querySelectorAll('.agent-tab').forEach((t, i) => {
t.classList.toggle('active', agentId === null ? i === 0 : t.textContent === agentId);
});
await loadData();
}
async function loadData() {
try {
let data;
if (currentAgent && authInfo.role === 'admin') {
data = await api('GET', '/admin/agent/' + currentAgent);
} else {
data = await api('GET', '/config');
}
allData = data.secrets || {};
renderTable();
} catch (e) {
toast(e.message, true);
}
}
function renderTable() {
const search = document.getElementById('searchInput').value.toLowerCase();
const scopeFilter = document.getElementById('scopeFilter').value;
const tbody = document.getElementById('tbody');
const entries = Object.entries(allData)
.filter(([k, v]) => {
if (search && !k.toLowerCase().includes(search)) return false;
if (scopeFilter !== 'all' && v.scope !== scopeFilter) return false;
return true;
})
.sort((a, b) => a[0].localeCompare(b[0]));
if (entries.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty">No keys found</td></tr>';
return;
}
tbody.innerHTML = entries.map(([key, v]) => {
const masked = '•'.repeat(Math.min(v.value.length, 32));
const time = v.updated_at ? new Date(v.updated_at).toLocaleString() : '-';
return '<tr>' +
'<td><strong>' + esc(key) + '</strong></td>' +
'<td><span class="val" onclick="toggleReveal(this)" data-val="' + esc(v.value) + '">' + masked + '</span></td>' +
'<td><span class="badge ' + v.scope + '">' + v.scope + '</span></td>' +
'<td class="time">' + time + '</td>' +
'<td class="actions">' +
'<button onclick="copyVal(\\'' + esc(key) + '\\')">Copy</button>' +
'<button onclick="editKey(\\'' + esc(key) + '\\')">Edit</button>' +
'<button class="danger" onclick="deleteKey(\\'' + esc(key) + '\\', \\'' + v.scope + '\\')">Del</button>' +
'</td>' +
'</tr>';
}).join('');
}
function toggleReveal(el) {
if (el.classList.contains('revealed')) {
el.textContent = '•'.repeat(Math.min(el.dataset.val.length, 32));
el.classList.remove('revealed');
} else {
el.textContent = el.dataset.val;
el.classList.add('revealed');
}
}
function copyVal(key) {
const v = allData[key];
if (v) navigator.clipboard.writeText(v.value).then(() => toast('Copied!'));
}
function esc(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
let editingKey = null;
function openAddModal() {
editingKey = null;
document.getElementById('modalTitle').textContent = 'Add Key';
document.getElementById('modalKey').value = '';
document.getElementById('modalKey').disabled = false;
document.getElementById('modalValue').value = '';
document.getElementById('modalScope').value = 'personal';
document.getElementById('addModal').classList.add('active');
}
function editKey(key) {
editingKey = key;
const v = allData[key];
document.getElementById('modalTitle').textContent = 'Edit: ' + key;
document.getElementById('modalKey').value = key;
document.getElementById('modalKey').disabled = true;
document.getElementById('modalValue').value = v.value;
document.getElementById('modalScope').value = v.scope;
document.getElementById('addModal').classList.add('active');
}
function closeModal() {
document.getElementById('addModal').classList.remove('active');
}
async function saveKey() {
const key = document.getElementById('modalKey').value.trim();
const value = document.getElementById('modalValue').value;
const scope = document.getElementById('modalScope').value;
if (!key) { toast('Key is required', true); return; }
try {
await api('PUT', '/config/' + encodeURIComponent(key) + '?scope=' + scope, { value });
toast('Saved!');
closeModal();
await loadData();
} catch(e) { toast(e.message, true); }
}
async function deleteKey(key, scope) {
if (!confirm('Delete ' + key + ' from ' + scope + '?')) return;
try {
await api('DELETE', '/config/' + encodeURIComponent(key) + '?scope=' + scope);
toast('Deleted');
await loadData();
} catch(e) { toast(e.message, true); }
}
function toast(msg, isError) {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast' + (isError ? ' error' : '');
t.style.display = 'block';
setTimeout(() => t.style.display = 'none', 2500);
}
// Auto-login if token saved
if (TOKEN) {
doLogin();
}
</script>
</body>
</html>`;
}

View File

@ -1,43 +0,0 @@
#!/usr/bin/env python3
"""Register an agent token in the config service KV.
Usage: python3 register_agent.py <agent_id> <role> [token]
If token is omitted, a random one is generated.
Outputs the KV entry to add via wrangler CLI.
"""
import hashlib
import json
import secrets
import sys
def main():
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <agent_id> <role> [token]")
print(" role: agent or admin")
sys.exit(1)
agent_id = sys.argv[1]
role = sys.argv[2]
token = sys.argv[3] if len(sys.argv) > 3 else secrets.token_urlsafe(32)
if role not in ("agent", "admin"):
print("Role must be 'agent' or 'admin'", file=sys.stderr)
sys.exit(1)
token_hash = hashlib.sha256(token.encode()).hexdigest()
entry = json.dumps({"agent_id": agent_id, "role": role})
print(f"Agent ID: {agent_id}")
print(f"Role: {role}")
print(f"Token: {token}")
print(f"Token hash: {token_hash}")
print()
print("Add to KV:")
print(f' wrangler kv key put --binding CONFIG_KV "auth:{token_hash}" \'{entry}\'')
if __name__ == "__main__":
main()