Files
Shade/packages/shade-cli/src/cli.ts
Sterister 518dc68c4f
Some checks failed
Test / test (push) Has been cancelled
feat(cli): M-Tool 1-3 — CLI, templates, Gitea publishing pipeline
Phase B complete: Shade now has a full developer tooling story.

@shade/cli
- shade init with project scaffolding from templates
- shade fingerprint (own or peer)
- shade publish (re-upload bundle)
- shade rotate (--identity for full rotation, otherwise signed prekey)
- shade peer add/list/verify/remove
- shade dashboard (opens observer in browser)
- shade doctor (diagnose config, storage, prekey server reachability)
- Config from .shaderc.json or SHADE_* env vars

Templates (in packages/shade-cli/templates/)
- bun-server — Bun + Hono backend with /send + /receive endpoints
- chat-demo — Two-process Alice/Bob chat over HTTP

Publishing pipeline (Gitea npm registry)
- .gitea/workflows/test.yml — CI on push/PR with PostgreSQL service
- .gitea/workflows/publish.yml — publish on git tag v*
- scripts/publish-all.ts — local publish helper with DRY_RUN support
- scripts/bump-version.ts — lockstep version bump across all packages
- Root package.json scripts: version, publish:dry, publish:all

Also: /health endpoint now lives in createPrekeyRoutes so doctor can
probe it without needing the full standalone setup.

Dry-run verified: all 11 packages pack cleanly.
246 tests passing, 0 failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:38:00 +02:00

133 lines
3.9 KiB
TypeScript

#!/usr/bin/env bun
import { initCommand, listTemplates } from './commands/init.js';
import { fingerprintCommand } from './commands/fingerprint.js';
import { publishCommand } from './commands/publish.js';
import { rotateCommand } from './commands/rotate.js';
import {
peerAddCommand,
peerListCommand,
peerVerifyCommand,
peerRemoveCommand,
} from './commands/peer.js';
import { dashboardCommand } from './commands/dashboard.js';
import { doctorCommand } from './commands/doctor.js';
const VERSION = '0.1.0';
const HELP = `
Shade CLI v${VERSION}
Usage: shade <command> [args]
Commands:
init [name] Scaffold a new Shade project
--template <name> Template to use (default: bun-server)
--prekey-server <url> Override prekey server URL
fingerprint [address] Print your own or a peer's fingerprint
publish Re-upload your bundle to the prekey server
rotate Rotate the signed prekey
--identity Rotate the full identity (destructive)
peer add <address> Establish a session with a peer
peer list List active sessions
peer verify <address> <fingerprint>
Check a peer's fingerprint matches
peer remove <address> Delete a session
dashboard Open the observer dashboard in the browser
doctor Diagnose setup issues
help Show this message
Config:
Reads .shaderc.json from cwd, or env vars:
SHADE_PREKEY_SERVER, SHADE_DB_PATH, SHADE_OBSERVER_TOKEN,
SHADE_OBSERVER_URL, SHADE_ADDRESS
`;
async function main(): Promise<void> {
const args = process.argv.slice(2);
const cmd = args[0];
try {
switch (cmd) {
case 'init': {
const options = parseInitArgs(args.slice(1));
await initCommand(options);
break;
}
case 'fingerprint':
await fingerprintCommand(args[1]);
break;
case 'publish':
await publishCommand();
break;
case 'rotate':
await rotateCommand({ identity: args.includes('--identity') });
break;
case 'peer': {
const sub = args[1];
if (sub === 'add') await peerAddCommand(requireArg(args[2], 'address'));
else if (sub === 'list') await peerListCommand();
else if (sub === 'verify')
await peerVerifyCommand(
requireArg(args[2], 'address'),
args.slice(3).join(' '),
);
else if (sub === 'remove') await peerRemoveCommand(requireArg(args[2], 'address'));
else {
console.error(`Unknown peer subcommand: ${sub}`);
process.exit(1);
}
break;
}
case 'dashboard':
await dashboardCommand();
break;
case 'doctor':
await doctorCommand();
break;
case 'help':
case '--help':
case '-h':
case undefined:
console.log(HELP);
console.log('\nAvailable templates:');
for (const name of listTemplates()) console.log(` ${name}`);
break;
case '--version':
case '-v':
console.log(VERSION);
break;
default:
console.error(`Unknown command: ${cmd}`);
console.log(HELP);
process.exit(1);
}
} catch (err) {
console.error(`\x1b[31mError:\x1b[0m ${(err as Error).message}`);
process.exit(1);
}
}
function parseInitArgs(args: string[]): {
name?: string;
template?: string;
prekeyServer?: string;
} {
const options: ReturnType<typeof parseInitArgs> = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--template') options.template = args[++i];
else if (args[i] === '--prekey-server') options.prekeyServer = args[++i];
else if (!args[i]!.startsWith('--')) options.name = args[i];
}
return options;
}
function requireArg(arg: string | undefined, name: string): string {
if (!arg) {
console.error(`Missing required argument: ${name}`);
process.exit(1);
}
return arg;
}
main();