clawdie-ai/setup/git.ts
Sam & Claude 3e4094d711 feat(git): mirror clawdie-iso into git jail; add build-iso skill v0.0.1
setup/git.ts:
- Add ensureRemoteBareRepo() — clones or updates a bare repo from a
  fixed remote URL, independent of GIT_DEFAULT_REPO_NAME/REMOTE_GIT_URL
- Mirror codeberg.org/Clawdie/Clawdie-ISO into git jail on every
  setup --step git run so the agent always has latest ISO build scripts

.agent/skills/build-iso/ (v0.0.1):
- SKILL.md: full procedure — git clone from jail, build.sh, nginx publish,
  troubleshooting table, version history
- scripts/build-iso.sh: clone from git jail → build.sh → publish to CMS
  jail nginx /downloads/ with dated archive + latest symlink; idempotent
  nginx location block injection + reload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---
Build: pass | Tests: FAIL — Tests  1 failed | 488 passed | 10 skipped (499)
2026-03-17 10:08:16 +00:00

260 lines
8.4 KiB
TypeScript

/**
* Step: git — Provision the default local git storage jail.
*
* Creates a persistent git jail, installs a minimal package baseline, creates
* /srv/git, and mirrors the current repository into a bare repo by default.
*/
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import {
CODE_HOSTING_MODE,
GIT_DEFAULT_REPO_NAME,
GIT_STORAGE_ROOT,
REMOTE_GIT_URL,
} from '../src/config.js';
import { getGitBastillePlan } from '../src/jail-config.js';
import { logger } from '../src/logger.js';
import { syncLocalHosts } from './hosts.js';
import { loadPackageList, mountPkgCacheInJail } from './packages.js';
import { ensureEnvFile, writeEnvLine } from './profile.js';
import { commandExists, getPlatform } from './platform.js';
import { emitStatus } from './status.js';
const BASTILLE_ROOT = '/usr/local/bastille';
const BASTILLE_RELEASES = path.join(BASTILLE_ROOT, 'releases');
const GIT_PACKAGES = loadPackageList('git-jail.txt');
function runCommand(cmd: string, opts: { inherit?: boolean } = {}): string {
const output = execSync(cmd, {
encoding: 'utf-8',
stdio: opts.inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
});
return typeof output === 'string' ? output.trim() : '';
}
function jailRoot(jailName: string): string {
return path.join(BASTILLE_ROOT, 'jails', jailName, 'root');
}
function jexec(jailName: string, cmd: string): string {
return runCommand(`jexec ${jailName} /bin/sh -lc ${JSON.stringify(cmd)}`);
}
function jailExists(jailName: string): boolean {
return fs.existsSync(path.join(BASTILLE_ROOT, 'jails', jailName));
}
function jailRunning(jailName: string): boolean {
try {
const output = runCommand('jls -N name');
return output.split('\n').map((line) => line.trim()).includes(jailName);
} catch {
return false;
}
}
function hasReleaseBootstrap(release: string): boolean {
return fs.existsSync(path.join(BASTILLE_RELEASES, release));
}
function bridgeHasGateway(bridge: string, gateway: string): boolean {
try {
return runCommand(`ifconfig ${bridge}`).includes(`inet ${gateway} `);
} catch {
return false;
}
}
function failSetup(error: string): never {
emitStatus('SETUP_GIT', {
STATUS: 'failed',
ERROR: error,
LOG: 'logs/setup.log',
});
process.exit(1);
}
function createGitJail(): void {
const plan = getGitBastillePlan();
logger.info({ jailName: plan.jailName, jailIp: plan.jailIp }, 'Creating git jail');
runCommand(`bastille ${plan.createArgs.join(' ')}`, { inherit: true });
runCommand(`bastille config ${plan.jailName} set host.hostname ${plan.hostname}`, {
inherit: true,
});
runCommand(`bastille restart ${plan.jailName}`, { inherit: true });
}
function ensureGitPackages(jailName: string): void {
runCommand(`bastille pkg ${jailName} install -y ${GIT_PACKAGES.join(' ')}`, {
inherit: true,
});
}
function ensureGitStorage(jailName: string): void {
jexec(jailName, `install -d -m 0755 ${GIT_STORAGE_ROOT}`);
}
function repoHostPath(jailName: string, repoName: string): string {
return path.join(jailRoot(jailName), GIT_STORAGE_ROOT.replace(/^\/+/, ''), repoName);
}
function repoExists(jailName: string, repoName: string): boolean {
return fs.existsSync(repoHostPath(jailName, repoName));
}
function currentProjectIsGitRepo(projectRoot: string): boolean {
return fs.existsSync(path.join(projectRoot, '.git'));
}
function ensureBareRepo(jailName: string, projectRoot: string, repoName: string): string {
const target = repoHostPath(jailName, repoName);
if (!repoExists(jailName, repoName)) {
jexec(jailName, `git init --bare ${path.posix.join(GIT_STORAGE_ROOT, repoName)}`);
}
if (currentProjectIsGitRepo(projectRoot)) {
runCommand(`git -C ${JSON.stringify(projectRoot)} push --mirror file://${target}`, {
inherit: true,
});
} else if (REMOTE_GIT_URL) {
jexec(
jailName,
[
`rm -rf ${path.posix.join(GIT_STORAGE_ROOT, repoName)}`,
`git clone --mirror ${JSON.stringify(REMOTE_GIT_URL)} ${path.posix.join(GIT_STORAGE_ROOT, repoName)}`,
].join(' && '),
);
}
if (REMOTE_GIT_URL) {
jexec(
jailName,
[
`git --git-dir ${path.posix.join(GIT_STORAGE_ROOT, repoName)} remote remove origin >/dev/null 2>&1 || true`,
`git --git-dir ${path.posix.join(GIT_STORAGE_ROOT, repoName)} remote add origin ${JSON.stringify(REMOTE_GIT_URL)}`,
].join(' && '),
);
}
return target;
}
function ensureRemoteBareRepo(jailName: string, remoteUrl: string, repoName: string): string {
const target = repoHostPath(jailName, repoName);
const repoPath = path.posix.join(GIT_STORAGE_ROOT, repoName);
if (!repoExists(jailName, repoName)) {
jexec(jailName, `git clone --mirror ${JSON.stringify(remoteUrl)} ${repoPath}`);
} else {
jexec(jailName, `git --git-dir ${repoPath} remote update`);
}
return target;
}
function verifyGitRepo(jailName: string, repoName: string): void {
jexec(
jailName,
`git --git-dir ${path.posix.join(GIT_STORAGE_ROOT, repoName)} rev-parse --is-bare-repository`,
);
}
function writeGitEnv(projectRoot: string, jailName: string, jailIp: string): void {
const envFile = ensureEnvFile(projectRoot);
writeEnvLine(envFile, 'FEATURE_GIT', 'YES');
if (CODE_HOSTING_MODE !== 'gitea') {
writeEnvLine(envFile, 'CODE_HOSTING_MODE', 'git');
writeEnvLine(envFile, 'FEATURE_GITEA', 'NO');
}
writeEnvLine(envFile, 'WARDEN_GIT_IP', jailIp);
writeEnvLine(envFile, 'GIT_JAIL_NAME', jailName);
writeEnvLine(envFile, 'GIT_JAIL_IP', jailIp);
writeEnvLine(envFile, 'GIT_STORAGE_ROOT', GIT_STORAGE_ROOT);
writeEnvLine(envFile, 'GIT_DEFAULT_REPO_NAME', GIT_DEFAULT_REPO_NAME);
if (REMOTE_GIT_URL) {
writeEnvLine(envFile, 'REMOTE_GIT_URL', REMOTE_GIT_URL);
}
}
export async function run(_args: string[]): Promise<void> {
const platform = getPlatform();
const projectRoot = process.cwd();
const plan = getGitBastillePlan();
if (platform !== 'freebsd') failSetup('unsupported_platform');
if (!commandExists('bastille')) failSetup('bastille_not_installed');
if (!commandExists('jexec')) failSetup('missing_jexec');
if (!commandExists('jls')) failSetup('missing_jls');
if (!commandExists('ifconfig')) failSetup('missing_ifconfig');
if (!hasReleaseBootstrap(plan.release)) failSetup('release_not_bootstrapped');
if (!bridgeHasGateway(plan.bridge, plan.gateway)) {
failSetup(`bridge_not_ready_${plan.bridge}_${plan.gateway}`);
}
logger.info(
{
jailName: plan.jailName,
jailIp: plan.jailIp,
repoName: GIT_DEFAULT_REPO_NAME,
storageRoot: GIT_STORAGE_ROOT,
},
'Starting git setup',
);
if (!jailExists(plan.jailName)) {
createGitJail();
} else if (!jailRunning(plan.jailName)) {
runCommand(`bastille start ${plan.jailName}`, { inherit: true });
}
runCommand(`bastille config ${plan.jailName} set host.hostname ${plan.hostname}`, {
inherit: true,
});
runCommand(`bastille restart ${plan.jailName}`, { inherit: true });
mountPkgCacheInJail(plan.jailName);
ensureGitPackages(plan.jailName);
ensureGitStorage(plan.jailName);
const repoPath = ensureBareRepo(plan.jailName, projectRoot, GIT_DEFAULT_REPO_NAME);
verifyGitRepo(plan.jailName, GIT_DEFAULT_REPO_NAME);
// Mirror clawdie-iso build scripts into the git jail so the agent can rebuild
// the USB installer from latest commits without leaving the host.
const ISO_REMOTE = 'https://codeberg.org/Clawdie/Clawdie-ISO.git';
const ISO_REPO_NAME = 'clawdie-iso';
ensureRemoteBareRepo(plan.jailName, ISO_REMOTE, ISO_REPO_NAME);
verifyGitRepo(plan.jailName, ISO_REPO_NAME);
writeGitEnv(projectRoot, plan.jailName, plan.jailIp);
syncLocalHosts();
emitStatus('SETUP_GIT', {
GIT_JAIL: plan.jailName,
GIT_IP: plan.jailIp,
CODE_HOSTING_MODE: CODE_HOSTING_MODE === 'gitea' ? 'gitea' : 'git',
REMOTE_GIT_URL: REMOTE_GIT_URL || 'none',
STORAGE_ROOT: GIT_STORAGE_ROOT,
DEFAULT_REPO: GIT_DEFAULT_REPO_NAME,
REPO_PATH: repoPath,
FEATURE_GIT: true,
STATUS: 'success',
LOG: 'logs/setup.log',
});
console.log('');
console.log('─'.repeat(52));
console.log(` git jail ready at ${plan.jailIp}`);
console.log(` Jail name : ${plan.jailName}`);
console.log(` Storage : ${GIT_STORAGE_ROOT}`);
console.log(` Repo : ${GIT_DEFAULT_REPO_NAME}`);
console.log('');
console.log(' Local mirror path on host:');
console.log(` ${repoPath}`);
console.log('');
console.log(` ISO repo : ${ISO_REPO_NAME}`);
console.log('─'.repeat(52));
console.log('');
}