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,76 @@
import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const here = dirname(fileURLToPath(import.meta.url));
const TEMPLATES_DIR = join(here, '..', '..', 'templates');
export interface InitOptions {
name?: string;
template?: string;
prekeyServer?: string;
cwd?: string;
}
export async function initCommand(opts: InitOptions = {}): Promise<void> {
const name = opts.name ?? 'my-shade-app';
const template = opts.template ?? 'bun-server';
const cwd = opts.cwd ?? process.cwd();
const target = join(cwd, name);
if (existsSync(target)) {
throw new Error(`Target directory "${target}" already exists`);
}
const templateDir = join(TEMPLATES_DIR, template);
if (!existsSync(templateDir)) {
const available = listTemplates();
throw new Error(
`Template "${template}" not found. Available: ${available.join(', ')}`,
);
}
// Recursive copy with placeholder substitution
mkdirSync(target, { recursive: true });
copyRecursive(templateDir, target, {
__PROJECT_NAME__: name,
__PREKEY_SERVER__: opts.prekeyServer ?? 'http://localhost:3900',
});
console.log(`✓ Created ${name} from template "${template}"`);
console.log('');
console.log(` cd ${name}`);
console.log(' bun install');
console.log(' bun run start');
}
export function listTemplates(): string[] {
if (!existsSync(TEMPLATES_DIR)) return [];
return readdirSync(TEMPLATES_DIR).filter((name) => {
return statSync(join(TEMPLATES_DIR, name)).isDirectory();
});
}
function copyRecursive(
source: string,
dest: string,
replacements: Record<string, string>,
): void {
mkdirSync(dest, { recursive: true });
for (const entry of readdirSync(source)) {
const srcPath = join(source, entry);
const destPath = join(dest, entry);
const st = statSync(srcPath);
if (st.isDirectory()) {
copyRecursive(srcPath, destPath, replacements);
} else {
const content = readFileSync(srcPath, 'utf-8');
const substituted = Object.entries(replacements).reduce(
(acc, [key, value]) => acc.replaceAll(key, value),
content,
);
writeFileSync(destPath, substituted);
}
}
}