7 Commits

Author SHA1 Message Date
ceamac 8b33afe03b Implement -d/--down, taking only an optional compose file name
Splits -u/-d's previously-shared CLI parsing: -u/--up keeps its two-token
(required images directory + optional compose file) shape, while -d/--down
becomes its own no_argument option with a single manually-peeked optional
trailing token -- no images directory needed at all, since stopping a
stack doesn't mount or resolve any image.

stop_compose_services() (compose_orchestrator.{h,cpp}) reads the state
file a previous -u/--up wrote for the same compose path, stops every
recorded session via kill_session() (the same graceful mechanism --kill
already uses), then removes the state file. If the compose file still
exists and parses, each service's own stop_grace_period_seconds (parsed
since compose_file.cpp's first commit but unused until now) is honored as
that service's own grace period instead of kill_session()'s 10s default,
falling back to it otherwise -- the state file alone already has
everything strictly required.

Known scope limitation: doesn't tear down the compose file's own managed
networks/volumes, matching real `docker compose down`'s own default.

Verified manually end to end: -u followed by a bare -d correctly finds and
stops both services via the recorded state file, removes it, and leaves no
processes behind; -d against a compose file with nothing recorded exits
cleanly (not an error).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-09-07 11:27:20 +00:00
ceamac 24de80e058 Add --list-containers to list compose-started containers
list_compose_containers() (compose_orchestrator.{h,cpp}) scans every
compose state file under xdg_state_dir()/"compose"/ and reports one row
per recorded service: compose file, service, container name, pid, and a
real liveness status cross-referenced against list_sessions() -- not just
"this line exists in the state file". record_compose_services() now also
writes the compose file's own real path as a header line, since the state
file's own name only encodes a lossy, sanitized version of it.

commands.cpp's own pad_column() factors out the per-column tab-alignment
scheme every other list command in this file already duplicates inline,
since this one needs it across four columns.

Verified manually: --list-containers against a real running 2-service
compose stack shows both running with correct fields; killing one flips
just that row to exited on a re-run, confirming real liveness checking.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-09-07 11:17:58 +00:00
ceamac f354463b40 Add -u/--up and -d/--down, wired to compose-parser stub commands
Both take a required OCI images directory plus an optional compose file
name (defaulting to "compose.yaml", resolved relative to the cwd) via the
same manual two-token consumption -v/--volume already uses, just with the
second token optional. The stub commands aren't no-ops: they resolve the
images directory, load and validate the compose file through the existing
load_compose_file()/validate_compose_external_state(), and print a summary
-- confirming the CLI wiring and parser work end to end -- before logging
that actual orchestration isn't implemented yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-09-07 10:45:48 +00:00
ceamac cbec986e78 Split config.yaml into global+persistent files; add -c/--config-file
config.yaml now holds only the global section (log-level, unshare-*,
with-veth, with-ipv6); a new persistent.yaml holds volumes/networks.
load_config_file()/write_config_file() are replaced by
load_global_config()/load_persistent_config()/write_global_config()/
write_persistent_config(), each touching only their own file.

-c/--config-file <path> lets one invocation use an alternate file for the
global section only -- persistent.yaml is always the one fixed path,
regardless of -c, so an experiment can never affect real volumes/networks
(a -c file's own volumes/networks, if any, are simply never read either).
A -c path that doesn't exist is a hard error, unlike the default path's
existing missing-file leniency.

migrate_legacy_config_if_needed() moves volumes/networks out of an
old-format config.yaml into persistent.yaml on first run after upgrading,
always against the fixed default paths regardless of -c. A name collision
aborts the migration for that run (touching neither file) rather than
risking data loss.

Required reordering main() to parse CLI args before loading config (so
-c's value is known first) -- ParsedArgs::log_level_flag_given tracks
whether --log-level was already given so the config file's own log-level
doesn't clobber it despite the reversed call order.
2026-09-05 09:32:36 +00:00
ceamac d2d631117d Drop -m/-u/-c short options for --mount/--umount/--cleanup
Now that -r/--run and -x/--exec cover normal use, --mount/--umount/--cleanup
are debug-only escape hatches not worth a short letter. Reassigned their
long_options codes to long-option-only constants (options::mount/umount/
cleanup) and dropped m:/u:/c: from getopt_long's own short-options string.

Updated the fixture smoke test (tests/run_test.py) and docs, which invoked
-m/-u/-c directly.
2026-09-05 08:43:58 +00:00
ceamac cdf9dcd210 Add global.with-veth/with-ipv6 config defaults for -n/--network creation
Replaces --no-ipv6/--no-veth (plain flags) with --with-ipv6/--with-veth,
each taking an explicit true/false value (e.g. --with-veth=false), parsed
via the same parse_bool_flag() the config file itself already uses (now
exported from config_file.h so cli_args.cpp can reuse it).

create_network_command() now resolves ipv6/veth as CLI flag -> config's own
global.with-ipv6/global.with-veth -> true, so a host that always wants the
tap+relay fallback (or no IPv6) can set it once in the config instead of
passing the flag on every network creation. -w/--write-config fills in both
new keys like the existing six unshare-* bools.
2026-09-05 08:36:12 +00:00
ceamac 42f9d36edf Add [unit] tests: port_forward, env_spec, network_subnet, cli_args
One file per source area, exercising the pure/isolated parsing and CIDR-
arithmetic functions already exposed via headers with no side effects --
parse_port_forward_spec() (protocol suffix parsing/validation, network
resolution left to add_port_forward()), resolve_env_specs() (literal and
--env-file parsing, ordering, error cases -- a small local RAII ScratchFile
helper writes the --env-file fixtures under /tmp), network_subnet.h's CIDR
validation/overlap/allocation/address-arithmetic functions, and parse_args()
itself against synthetic argv's.

Two real bugs found running parse_args() repeatedly in one process (never
possible before -- a real invocation only ever calls it once), not
assumed:
1. getopt_long's scanning position (`optind`) is process-global and never
   reset, so a second parse_args() call would silently resume scanning
   wherever the first one left off. Fixing this alone (optind = 1) wasn't
   enough on its own, either --
2. -h/-V return out of the getopt_long loop early (their own `return 0`
   case), before a call ever completes its scan and lets getopt_long null
   out its own private `nextchar` pointer -- the *next* parse_args() call
   then resumed scanning through that stale pointer into the *previous*
   call's already-destroyed argv strings, misparsing its own fresh argv.
   glibc documents `optind = 0` (not 1) as the "fully reinitialize private
   state before rescanning a new argv" signal; switching to it fixed this
   for good, confirmed by 3 repeated runs each in both random and
   deterministic (--order lex) Catch2 ordering with zero flakiness either
   way.

Neither bug could ever have surfaced in real usage (parse_args() is only
ever called once per process from main()) -- purely a testability gap the
new unit tests exposed, now fixed at the source rather than worked around
in the test file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-09-04 11:23:24 +00:00