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:
76
packages/shade-cli/src/commands/init.ts
Normal file
76
packages/shade-cli/src/commands/init.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user