/**
* CLI command: build — build the deployable Docker image for this Tina4 app.
*
* A Tina4 app deploys as a container — `tina4nodejs init` and `tina4 deploy
* docker` both scaffold a Dockerfile — so `build` produces THAT artifact: the
* image. It shells out to the `docker` CLI (no new npm dependency) and fails
* loud with guidance when there is no Dockerfile or docker is not on PATH,
* instead of silently packaging the framework as a library.
*
* tina4nodejs build # docker build -t
:latest .
* tina4nodejs build --tag myapp:1.2 # explicit image tag
* tina4nodejs build --file docker/Dockerfile
*
* Mirrors the Python master's _build (tina4_python/cli/__init__.py).
*/
import { accessSync, constants, existsSync, statSync } from "node:fs";
import { basename, delimiter, join } from "node:path";
import { spawnSync } from "node:child_process";
/** Parse --key value / bare --flag (build only needs --tag and --file values). */
function parseFlags(args: string[]): Record {
const flags: Record = {};
let i = 0;
while (i < args.length) {
const arg = args[i];
if (arg.startsWith("--")) {
const key = arg.slice(2);
if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
flags[key] = args[i + 1];
i += 2;
} else {
flags[key] = true;
i += 1;
}
} else {
i += 1;
}
}
return flags;
}
/**
* Resolve `docker` on PATH, or null when it is genuinely absent.
*
* Scans PATH directly (zero-dep, cross-platform) rather than shelling out, so a
* deliberately emptied PATH really yields null — mirroring Python's
* shutil.which("docker"), which the fail-loud guard relies on.
*/
function whichDocker(): string | null {
const pathValue = process.env.PATH || process.env.Path || "";
if (!pathValue) return null;
const exts =
process.platform === "win32"
? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";")
: [""];
for (const dir of pathValue.split(delimiter)) {
if (!dir) continue;
for (const ext of exts) {
const candidate = join(dir, `docker${ext}`);
try {
accessSync(candidate, constants.X_OK);
if (statSync(candidate).isFile()) return candidate;
} catch {
// not here / not executable — keep looking
}
}
}
return null;
}
/** Build the deployable Docker image. Fails loud (exit 1+) on missing prerequisites. */
export function buildImage(args: string[]): void {
const flags = parseFlags(args);
let tag = typeof flags.tag === "string" ? flags.tag : "";
if (!tag) {
// Default tag: :latest, lower-cased (docker repo names must
// be lowercase). Fall back to a sane name for an unnamed cwd.
const dirName = basename(process.cwd()).toLowerCase();
tag = `${dirName || "tina4app"}:latest`;
}
const dockerfile = typeof flags.file === "string" && flags.file ? flags.file : "Dockerfile";
if (!existsSync(dockerfile) || !statSync(dockerfile).isFile()) {
console.log(` ✗ No ${dockerfile} found.`);
console.log(" A Tina4 app deploys as a container. Scaffold a Dockerfile first:");
console.log(" tina4 deploy docker (or: tina4nodejs init)");
process.exit(1);
}
const docker = whichDocker();
if (!docker) {
console.log(" ✗ docker was not found on PATH.");
console.log(" Install Docker to build the deployable image, or build manually:");
console.log(` docker build -t ${tag} -f ${dockerfile} .`);
process.exit(1);
}
console.log(` Building image ${tag} from ${dockerfile} ...`);
const result = spawnSync(docker, ["build", "-t", tag, "-f", dockerfile, "."], {
stdio: "inherit",
});
const code = result.status ?? 1;
if (code !== 0) {
console.log(` ✗ docker build failed (exit ${code})`);
process.exit(code);
}
console.log(` ✓ Built image ${tag}`);
console.log(` Run: docker run -p 7148:7148 ${tag}`);
}