feat(cli): M-Tool 1-3 — CLI, templates, Gitea publishing pipeline
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:
2026-04-11 00:38:00 +02:00
parent c95824f95f
commit 518dc68c4f
29 changed files with 1263 additions and 15 deletions

View File

@@ -0,0 +1,70 @@
import { tryLoadConfig } from '../config.js';
import { existsSync } from 'fs';
/**
* Diagnose common setup issues.
*/
export async function doctorCommand(): Promise<void> {
let ok = true;
console.log('\x1b[33mShade doctor\x1b[0m\n');
// 1. Config loadable?
const configResult = tryLoadConfig();
if (configResult.ok) {
console.log(' \x1b[32m✓\x1b[0m Config loaded from .shaderc.json or env vars');
const config = configResult.config;
console.log(` prekeyServer: ${config.prekeyServer}`);
console.log(` storage: ${config.storage}`);
// 2. Storage path accessible?
if (config.storage.startsWith('sqlite:')) {
const path = config.storage.slice('sqlite:'.length);
const dir = path.substring(0, path.lastIndexOf('/')) || '.';
if (existsSync(dir)) {
console.log(` \x1b[32m✓\x1b[0m Storage directory exists: ${dir}`);
} else {
console.log(` \x1b[31m✗\x1b[0m Storage directory missing: ${dir}`);
ok = false;
}
}
// 3. Prekey server reachable?
try {
const res = await fetch(`${config.prekeyServer}/health`, {
signal: AbortSignal.timeout(5000),
});
if (res.ok) {
console.log(` \x1b[32m✓\x1b[0m Prekey server is reachable`);
} else {
console.log(` \x1b[31m✗\x1b[0m Prekey server returned HTTP ${res.status}`);
ok = false;
}
} catch (err) {
console.log(` \x1b[31m✗\x1b[0m Cannot reach prekey server: ${(err as Error).message}`);
ok = false;
}
// 4. Observer token set?
if (config.observerToken) {
if (config.observerToken.length >= 16) {
console.log(` \x1b[32m✓\x1b[0m Observer token is set and long enough`);
} else {
console.log(` \x1b[31m✗\x1b[0m Observer token must be at least 16 characters`);
ok = false;
}
} else {
console.log(` \x1b[90m○\x1b[0m Observer token not set (dashboard disabled)`);
}
} else {
console.log(` \x1b[31m✗\x1b[0m ${configResult.error}`);
ok = false;
}
console.log();
if (ok) {
console.log('\x1b[32mAll checks passed.\x1b[0m');
} else {
console.log('\x1b[31mSome checks failed. Fix the issues above and re-run.\x1b[0m');
process.exitCode = 1;
}
}