Agentic coding CLI on Bun, Ink, and the AI SDK. Core: streamText loop with SDK-level tool approval so a denied call provably never executes; endpoint fallback for OpenAI reasoning models; retry with backoff. Tools: read/write/edit/glob/grep/bash, path-jailed, gitignore-aware, ripgrep with a JS fallback, binary rejection, live bash streaming. Agents: five variants crossing thinking level with tool restriction; plan and review withhold mutating tools from the model. Extensibility: frontmatter skills with on-demand bodies, plugin host with blocking hooks, MCP stdio and HTTP, read-only subagents. State: durable per-project memory, session task lists, session persistence, compaction that repairs provider-item dependencies. Distribution: five-platform cross-compiled binaries with checksums, install scripts, CI on three operating systems. 404 tests, typecheck clean.
101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
import { mkdirSync, rmSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { VERSION } from '../src/version';
|
|
|
|
export type Target = {
|
|
/** Bun cross-compilation target. */
|
|
target: string;
|
|
/** Suffix in the artifact name, matching what the installers look for. */
|
|
name: string;
|
|
windows?: boolean;
|
|
};
|
|
|
|
export const TARGETS: Target[] = [
|
|
{ target: 'bun-linux-x64', name: 'linux-x64' },
|
|
{ target: 'bun-linux-arm64', name: 'linux-arm64' },
|
|
{ target: 'bun-darwin-x64', name: 'darwin-x64' },
|
|
{ target: 'bun-darwin-arm64', name: 'darwin-arm64' },
|
|
{ target: 'bun-windows-x64', name: 'windows-x64', windows: true },
|
|
];
|
|
|
|
const OUT = 'dist/release';
|
|
|
|
/**
|
|
* Builds one executable per platform.
|
|
*
|
|
* Bun cross-compiles from any host, so a single runner produces every artifact and
|
|
* no build matrix is needed. The version is compiled into the binary from
|
|
* src/version.ts; a release tag must agree with it or the build stops, because a
|
|
* binary reporting the wrong version is worse than a failed release.
|
|
*/
|
|
async function main(): Promise<void> {
|
|
const args = process.argv.slice(2);
|
|
const only = args.filter((a) => !a.startsWith('-'));
|
|
const wanted = only.length > 0 ? TARGETS.filter((t) => only.includes(t.name)) : TARGETS;
|
|
if (wanted.length === 0) {
|
|
console.error(`no matching target. Available: ${TARGETS.map((t) => t.name).join(', ')}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const pkg = (await Bun.file('package.json').json()) as { version?: string };
|
|
if (pkg.version !== VERSION) {
|
|
console.error(`version mismatch: package.json is ${pkg.version}, src/version.ts is ${VERSION}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const tag = (process.env['GITHUB_REF_NAME'] ?? '').replace(/^v/, '');
|
|
if (tag && tag !== VERSION) {
|
|
console.error(`tag v${tag} does not match src/version.ts (${VERSION}). Bump the version or retag.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
rmSync(OUT, { recursive: true, force: true });
|
|
mkdirSync(OUT, { recursive: true });
|
|
|
|
const built: { file: string; bytes: number }[] = [];
|
|
|
|
for (const t of wanted) {
|
|
const base = `shiro-${t.name}`;
|
|
const outfile = join(OUT, base);
|
|
|
|
const buildArgs = ['build', '--compile', '--minify', `--target=${t.target}`, 'src/cli.tsx', '--outfile', outfile];
|
|
if (t.windows) {
|
|
buildArgs.push(
|
|
'--windows-title=shiro-neko',
|
|
'--windows-description=Agentic coding CLI',
|
|
`--windows-version=${VERSION.split('-')[0]}.0`,
|
|
);
|
|
}
|
|
|
|
const proc = Bun.spawn(['bun', ...buildArgs], { stdout: 'inherit', stderr: 'inherit' });
|
|
const code = await proc.exited;
|
|
if (code !== 0) {
|
|
console.error(`\nbuild failed for ${t.name} (exit ${code})`);
|
|
process.exit(code);
|
|
}
|
|
|
|
const file = t.windows ? `${base}.exe` : base;
|
|
const artifact = Bun.file(join(OUT, file));
|
|
if (!(await artifact.exists())) {
|
|
console.error(`\n${file} was not produced`);
|
|
process.exit(1);
|
|
}
|
|
built.push({ file, bytes: artifact.size });
|
|
}
|
|
|
|
// Checksums let an installer verify a download without a second request.
|
|
const sums: string[] = [];
|
|
for (const { file } of built) {
|
|
const bytes = new Uint8Array(await Bun.file(join(OUT, file)).arrayBuffer());
|
|
sums.push(`${new Bun.CryptoHasher('sha256').update(bytes).digest('hex')} ${file}`);
|
|
}
|
|
await Bun.write(join(OUT, 'SHA256SUMS'), `${sums.join('\n')}\n`);
|
|
|
|
console.log(`\nshiro-neko ${VERSION}`);
|
|
for (const { file, bytes } of built) console.log(` ${file.padEnd(26)} ${Math.round(bytes / 1024 / 1024)} MB`);
|
|
console.log(` ${'SHA256SUMS'.padEnd(26)} ${built.length} entries`);
|
|
}
|
|
|
|
// Guarded so a test can import TARGETS without triggering a five-platform build.
|
|
if (import.meta.main) await main();
|