@awesomate/hosting-mcp
Version:
Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases
116 lines (104 loc) • 5.42 kB
JavaScript
/**
* Zero-knowledge GitHub scaffolder. Idempotent — safe to re-run.
*
* node github-setup.mjs <repo-name> [--public] [--path <dir>]
*
* Steps: verify gh auth → ensure .gitignore excludes .env → SECRET GUARD
* (refuse to commit a .env or an obvious key/token) → git init + first commit →
* `gh repo create --source=. --push` (private by default; personal accounts OK).
*
* Deploy secrets are NOT handled here: run deploy-key.mjs <appId> afterwards,
* which mints a per-app key and sets SSH_HOST/SSH_USER/SSH_PRIVATE_KEY itself.
* Prints only non-sensitive output.
*/
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
const args = process.argv.slice(2);
const flags = new Set(args.filter((a) => a.startsWith('--')));
const positional = args.filter((a) => !a.startsWith('--'));
const repoName = positional[0];
const pathIdx = args.indexOf('--path');
const cwd = pathIdx >= 0 && args[pathIdx + 1] ? args[pathIdx + 1] : process.cwd();
const isPublic = flags.has('--public');
if (!repoName || !/^[a-z0-9][a-z0-9._-]{0,99}$/i.test(repoName)) {
console.error('usage: node github-setup.mjs <repo-name> [--public] [--path <dir>]');
console.error('repo-name: letters/digits/._- , starts alphanumeric');
process.exit(2);
}
function run(cmd, cmdArgs, opts = {}) {
return spawnSync(cmd, cmdArgs, { cwd, encoding: 'utf8', ...opts });
}
function die(msg) {
console.error(`\n✗ ${msg}`);
process.exit(1);
}
// --- 0. gh present + authenticated ----------------------------------------
if (run('gh', ['--version']).status !== 0) {
die("GitHub CLI 'gh' isn't installed. Install it (macOS: brew install gh) then re-run.");
}
if (run('gh', ['auth', 'status']).status !== 0) {
die("You're not connected to GitHub yet. Run: gh auth login (GitHub.com → HTTPS → Login with a web browser), then re-run this.");
}
// --- 1. .gitignore excludes .env ------------------------------------------
const giPath = join(cwd, '.gitignore');
// .awesomate/ is the secret-drop inbox — the credentials skill tells users it is
// ignored, and the SECRET_RE guard only catches known key SHAPES, so a database
// URL or bare token in there would otherwise be committable.
const needed = ['.env', '.env.local', '.awesomate/', '*.log', '.DS_Store', 'node_modules/'];
let gi = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
const have = new Set(gi.split(/\r?\n/).map((l) => l.trim()));
const toAdd = needed.filter((n) => !have.has(n));
if (toAdd.length) {
gi = (gi.endsWith('\n') || gi === '' ? gi : gi + '\n') + toAdd.join('\n') + '\n';
writeFileSync(giPath, gi);
console.log(`• .gitignore: added ${toAdd.join(', ')}`);
}
// --- init early so the secret guard can inspect the staged set ------------
if (!existsSync(join(cwd, '.git'))) {
if (run('git', ['init', '-b', 'main']).status !== 0) die('git init failed.');
console.log('• initialised a git repo (branch: main)');
}
if (run('git', ['add', '-A']).status !== 0) die('git add failed.');
// --- 2. SECRET GUARD -------------------------------------------------------
const staged = run('git', ['diff', '--cached', '--name-only']).stdout.split(/\r?\n/).filter(Boolean);
const envLeak = staged.find((f) => /(^|\/)\.env(\.|$)/.test(f) && !/\.env\.example$/.test(f));
if (envLeak) {
die(`Refusing to commit '${envLeak}' — that's a secrets file. It's now gitignored; run \`git rm --cached ${envLeak}\` and re-run. Store secrets via the awesomate-credentials skill, never in the repo.`);
}
const SECRET_RE = /(sk-[A-Za-z0-9]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[bp]-[A-Za-z0-9-]{10,}|-----BEGIN [A-Z ]*PRIVATE KEY-----)/;
for (const f of staged) {
const abs = join(cwd, f);
try {
if (statSync(abs).size > 512 * 1024) continue; // skip large/binary
if (SECRET_RE.test(readFileSync(abs, 'utf8'))) {
die(`Refusing to commit '${f}' — it contains something shaped like a secret (API key / token / private key). Move it out (use the awesomate-credentials skill) and re-run.`);
}
} catch {
/* unreadable/binary — skip */
}
}
// --- 3. first commit (only if there's anything to commit) -----------------
if (run('git', ['diff', '--cached', '--quiet']).status !== 0) {
if (run('git', ['commit', '-m', 'Initial commit']).status !== 0) die('git commit failed.');
console.log('• made the first commit');
} else {
console.log('• nothing new to commit');
}
// --- 4. create the GitHub repo + push -------------------------------------
const already = run('git', ['remote', 'get-url', 'origin']);
if (already.status === 0) {
console.log(`• remote already set: ${already.stdout.trim()}`);
run('git', ['push', '-u', 'origin', 'HEAD']);
} else {
const vis = isPublic ? '--public' : '--private';
const create = run('gh', ['repo', 'create', repoName, vis, '--source=.', '--remote=origin', '--push']);
if (create.status !== 0) {
die(`gh repo create failed: ${(create.stderr || create.stdout || '').trim()}`);
}
console.log(`• created ${isPublic ? 'public' : 'private'} repo and pushed`);
}
// --- done ------------------------------------------------------------------
const url = run('gh', ['repo', 'view', '--json', 'url', '-q', '.url']).stdout.trim();
console.log(`\n✓ Done. Your project is on GitHub${url ? `: ${url}` : ''}.`);