mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
docker/agent.Dockerfile hardcoded the four npm-published CLIs it installs, one of the several lists that had to be kept in step with the registry by hand. It now takes them as `ARG CLI_NPM_PACKAGES`, supplied by scripts/build-agent-image.mjs from config/clis.stock.json, with the default set to today's list so a bare `docker build` still produces the same image. The arg is expanded unquoted because word splitting is what turns the list into several arguments, which is exactly why every token is validated against ^[@A-Za-z0-9][@A-Za-z0-9/._-]*$ on the producing side; a package name carrying a space or a metacharacter is refused rather than reaching the RUN line. Verified by building the layer: four packages in, four arguments out, and the default still applies with no arg. The list is filtered on each entry's `enabled` flag — the field whose absence was the maintainer's §3 finding, where a CLI shipping disabled still got baked into every image. No stock entry is disabled today, so that assertion would pass vacuously; a unit test feeds the pure helper a fabricated disabled entry so the fix is covered now rather than the first time someone ships one. ⚠️ It reads the STOCK catalogue, never the merged registry. A user's ~/.codeman/clis.json must not change what is inside an image tagged codeman/agent:base, or two machines holding that tag hold different images. Four CLIs keep hand-written layers because the registry cannot describe what makes them special: pi's --ignore-scripts, deepseek's pnpm companion and dsh-tui profile, and the three standalone installers. Rather than extend the schema for a Docker-only benefit, the coverage test requires each to carry a written reason AND still be present, so an exclusion cannot quietly become an omission. There are two producers of this command line and there have to be — the .mjs cannot import TypeScript, and src/docker-hosts.ts builds the same argv for the in-app auto-build — so a parity test pins them together, package list, arg pairs and rendered argv. Their order is pinned too: a different order is a different RUN string and so a needless cache miss between the two build paths. docker/server.Dockerfile is deliberately NOT edited (PRs #373 and #377 both modify it); its narrower list is asserted as a declared omission list instead, so the divergence is reviewable without touching the file. Also fixes the in-app hint at index.html, which the new coverage test caught still omitting omp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015EMxQreQUZX5ZyybxAGh12
81 lines
3.0 KiB
JavaScript
81 lines
3.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Build the Codeman agent base image locally (decision: "build locally on first
|
|
* use", see docs/docker-cases-plan.md). No registry account required.
|
|
*
|
|
* Usage:
|
|
* node scripts/build-agent-image.mjs [--engine docker|podman] [--image <ref>] [--no-cache]
|
|
*
|
|
* Defaults: engine=docker (falls back to podman if docker is absent),
|
|
* image=codeman/agent:base
|
|
*/
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
import { agentImageBuildArgPairs, readCatalog } from './lib/cli-catalog.mjs';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const REPO_ROOT = join(__dirname, '..');
|
|
const DOCKERFILE = join(REPO_ROOT, 'docker', 'agent.Dockerfile');
|
|
const DEFAULT_IMAGE = 'codeman/agent:base';
|
|
|
|
function parseArgs(argv) {
|
|
const args = { image: DEFAULT_IMAGE, engine: undefined, noCache: false };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === '--image') args.image = argv[++i];
|
|
else if (a === '--engine') args.engine = argv[++i];
|
|
else if (a === '--no-cache') args.noCache = true;
|
|
else if (a === '-h' || a === '--help') args.help = true;
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function engineAvailable(engine) {
|
|
const r = spawnSync(engine, ['--version'], { stdio: 'ignore' });
|
|
return r.status === 0;
|
|
}
|
|
|
|
function resolveEngine(preferred) {
|
|
if (preferred) {
|
|
if (!engineAvailable(preferred)) {
|
|
console.error(`[build-agent-image] engine "${preferred}" not found on PATH`);
|
|
process.exit(1);
|
|
}
|
|
return preferred;
|
|
}
|
|
if (engineAvailable('docker')) return 'docker';
|
|
if (engineAvailable('podman')) return 'podman';
|
|
console.error('[build-agent-image] neither docker nor podman found on PATH. Install one and retry.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help) {
|
|
console.log('Usage: node scripts/build-agent-image.mjs [--engine docker|podman] [--image <ref>] [--no-cache]');
|
|
process.exit(0);
|
|
}
|
|
|
|
const engine = resolveEngine(args.engine);
|
|
const buildArgs = ['build', '-f', DOCKERFILE, '-t', args.image];
|
|
if (args.noCache) buildArgs.push('--no-cache');
|
|
// The CLI list comes from the generated catalogue rather than the Dockerfile, so adding a
|
|
// stock CLI needs no edit in either. `src/docker-hosts.ts` assembles the same argv for the
|
|
// in-app auto-build; test/agent-image-build-args-parity.test.ts pins the two together, since
|
|
// two independent producers of one command line is exactly how they drift.
|
|
for (const [name, value] of agentImageBuildArgPairs(readCatalog())) {
|
|
buildArgs.push('--build-arg', `${name}=${value}`);
|
|
}
|
|
buildArgs.push(REPO_ROOT);
|
|
|
|
console.log(`[build-agent-image] ${engine} ${buildArgs.join(' ')}`);
|
|
const child = spawn(engine, buildArgs, { stdio: 'inherit' });
|
|
child.on('exit', (code) => {
|
|
if (code === 0) {
|
|
console.log(`\n[build-agent-image] built ${args.image}. Docker cases can now launch.`);
|
|
} else {
|
|
console.error(`\n[build-agent-image] build failed (exit ${code}).`);
|
|
}
|
|
process.exit(code ?? 1);
|
|
});
|