Files
nextcraft/apps/cli/src/lib/container.ts
T
CIAgent 1a88ab3199
Release / release (push) Successful in 3m45s
CI / python (push) Successful in 1m52s
CI / node (push) Successful in 1m0s
merge: milestone/v0.8-ship-anywhere -> main (v0.8 complete — Ship Anywhere)
The v0.8 milestone: installable on any Debian-based server via docker
(Gitea registry image, honest sandbox degrade), deb (amd64+arm64,
advisory postinst), and arch-aware binary — with release consistency
enforced by pre-commit hooks + Gitea Actions (3-runner fleet on CT 113,
machine-made releases with the full 8-asset matrix + registry image), the
CLI carrying the full suite (guide, channel-aware doctor), and the admin
runbook (deploy/README.md, evidence-backed recipes).

---ci---
phase: 6
milestone: v0.8
status: complete
requirements:
  covered: [REQ-8-001, REQ-8-002, REQ-8-003, REQ-8-004, REQ-8-005]
  partial: []
---/ci---
2026-09-29 16:04:40 +00:00

40 lines
1.6 KiB
TypeScript

// In-container semantics (v0.8 P2, D-066): the docker image embeds the
// CLI; doctor/verify/guide work in-container, the repo/daemon commands
// (bootstrap/deploy/dev/stop/log) refuse honestly — the image is
// immutable, upgrades are `docker pull` + recreate (D-071).
//
// Detection: NC_CONTAINER=1 (the image sets it) OR /.dockerenv exists —
// belt and suspenders. The refusal text names the remediation; it is the
// single source shared by every refused command (they cannot drift).
const REFUSAL = [
"✗ this command manages a nextcraft clone, but this is the docker image —",
" the image is immutable (no repo clone, no daemons inside).",
" hint: upgrades = docker pull + recreate the container (see deploy guide);",
" in-container: nextcraft doctor / nextcraft verify / nextcraft guide",
].join("\n");
export interface ContainerMode {
inContainer: boolean;
}
export function detectContainer(
env: Record<string, string | undefined>,
): ContainerMode {
// NC_CONTAINER=1 is the image's definitive marker. /.dockerenv ALONE is
// NOT a trigger (rc2 finding: CI job containers, dev boxes-in-docker all
// carry /.dockerenv — the host-path CLI must keep working there).
const marker = env.NC_CONTAINER === "1";
return { inContainer: marker };
}
export function refuseIfContainer(
ctx: { env: Record<string, string | undefined>; exists: (p: string) => boolean; stderr: { write(s: string): void } },
command: string,
): boolean {
const { inContainer } = detectContainer(ctx.env);
if (!inContainer) return false;
ctx.stderr.write(`✗ \`${command}\` is unavailable in the docker image\n${REFUSAL}\n`);
return true;
}