feat(cli): M-Tool 1-3 — CLI, templates, Gitea publishing pipeline
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
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>
This commit is contained in:
132
packages/shade-cli/src/cli.ts
Normal file
132
packages/shade-cli/src/cli.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user