mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(docker): derive the agent image's npm CLI list from the catalogue
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
This commit is contained in:
+48
-3
@@ -25,6 +25,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
||||
import { enabledCliIds, getCli } from './config/cli-registry/registry.js';
|
||||
import { STOCK_CLIS } from './config/cli-registry/stock.js';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
import { createHash } from 'node:crypto';
|
||||
@@ -488,8 +489,52 @@ export function buildDockerCreateArgs(ctx: DockerCreateContext): string[] {
|
||||
* scripts/build-agent-image.mjs): `build -f <dockerfile> -t <image> [--no-cache]
|
||||
* <contextDir>`. Kept pure + unit-testable; the caller prepends the engine binary.
|
||||
*/
|
||||
export function agentImageBuildArgs(dockerfile: string, image: string, contextDir: string, noCache = false): string[] {
|
||||
return ['build', '-f', dockerfile, '-t', image, ...(noCache ? ['--no-cache'] : []), contextDir];
|
||||
export function agentImageBuildArgs(
|
||||
dockerfile: string,
|
||||
image: string,
|
||||
contextDir: string,
|
||||
noCache = false,
|
||||
buildArgs: Array<[string, string]> = []
|
||||
): string[] {
|
||||
return [
|
||||
'build',
|
||||
'-f',
|
||||
dockerfile,
|
||||
'-t',
|
||||
image,
|
||||
...(noCache ? ['--no-cache'] : []),
|
||||
...buildArgs.flatMap(([name, value]) => ['--build-arg', `${name}=${value}`]),
|
||||
contextDir,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* npm packages the agent image installs in its shared layer, from the STOCK catalogue.
|
||||
*
|
||||
* ⚠️ Stock, deliberately, NOT the merged registry. A user's `~/.codeman/clis.json` must not
|
||||
* change what lands inside an image tagged `codeman/agent:base`, or two machines holding that
|
||||
* same tag hold different images and every cache-hit decision downstream is a lie.
|
||||
*
|
||||
* ⚠️ This mirrors `agentImageNpmPackages()` in `scripts/lib/cli-catalog.mjs`, which the CLI
|
||||
* build path uses because a `.mjs` cannot import TypeScript. Two producers of one command
|
||||
* line drift; `test/agent-image-build-args-parity.test.ts` is what stops them.
|
||||
*/
|
||||
export function agentImageNpmPackages(): string[] {
|
||||
return STOCK_CLIS.filter((entry) => entry.enabled && !AGENT_IMAGE_SPECIAL_CASE_IDS.has(entry.id as string))
|
||||
.map((entry) => entry.discovery.install.npmPackage)
|
||||
.filter((pkg): pkg is string => Boolean(pkg));
|
||||
}
|
||||
|
||||
/**
|
||||
* CLIs the agent image installs in their OWN Dockerfile layer rather than the shared npm one.
|
||||
* The registry cannot express what makes each special, so the layers stay hand-written and
|
||||
* `test/docker-agent-image-coverage.test.ts` requires each to carry a reason and still exist.
|
||||
*/
|
||||
const AGENT_IMAGE_SPECIAL_CASE_IDS = new Set(['pi', 'deepseek']);
|
||||
|
||||
/** The `--build-arg` pairs the agent image takes. */
|
||||
export function agentImageBuildArgPairs(): Array<[string, string]> {
|
||||
return [['CLI_NPM_PACKAGES', agentImageNpmPackages().join(' ')]];
|
||||
}
|
||||
|
||||
// ========== Credential mount resolution (IO) ==========
|
||||
@@ -1020,7 +1065,7 @@ function buildAgentImage(
|
||||
const argv = dockerEngineArgv(docker);
|
||||
const args = [
|
||||
...argv.slice(1),
|
||||
...agentImageBuildArgs(resolved.dockerfile, image, resolved.contextDir, opts.noCache),
|
||||
...agentImageBuildArgs(resolved.dockerfile, image, resolved.contextDir, opts.noCache, agentImageBuildArgPairs()),
|
||||
];
|
||||
return new Promise<EnsureImageResult>((resolve) => {
|
||||
// async spawn (NEVER spawnSync) so a multi-minute build never wedges the event loop.
|
||||
|
||||
@@ -2905,7 +2905,7 @@
|
||||
<div class="form-row docker-create-only">
|
||||
<label>Image</label>
|
||||
<input type="text" id="dockerImage" placeholder="codeman/agent:base" autocomplete="off" autocapitalize="off" spellcheck="false">
|
||||
<span class="form-hint">Build it once with <code>node scripts/build-agent-image.mjs</code>. Contains node + claude/codex/gemini/opencode/agy/pi/grok/dsh + tmux.</span>
|
||||
<span class="form-hint">Build it once with <code>node scripts/build-agent-image.mjs</code>. Contains node + claude/opencode/codex/gemini/agy/pi/grok/dsh/omp + tmux.</span>
|
||||
</div>
|
||||
<div class="form-row docker-create-only">
|
||||
<label>Network</label>
|
||||
|
||||
Reference in New Issue
Block a user