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
This commit is contained in:
2026-09-07 11:27:20 +00:00
parent 24de80e058
commit 8b33afe03b
7 changed files with 249 additions and 61 deletions
+13 -3
View File
@@ -277,18 +277,28 @@ TEST_CASE("parse_args: -u doesn't swallow a following flag as compose_file_name"
CHECK(result.args.log_level_flag_given);
}
TEST_CASE("parse_args: -d/--down works the same way as -u/--up", "[unit]") {
auto default_name = run_parse({"--down", "/var/lib/images"});
TEST_CASE("parse_args: -d/--down takes a single optional compose-file-name, no images directory", "[unit]") {
auto default_name = run_parse({"--down"});
REQUIRE_FALSE(default_name.exit_code.has_value());
CHECK(default_name.args.mode == Mode::compose_down);
CHECK_FALSE(default_name.args.compose_images_directory.has_value());
CHECK(default_name.args.compose_file_name == "compose.yaml");
auto explicit_name = run_parse({"-d", "/var/lib/images", "my-stack.yaml"});
auto explicit_name = run_parse({"-d", "my-stack.yaml"});
REQUIRE_FALSE(explicit_name.exit_code.has_value());
CHECK(explicit_name.args.mode == Mode::compose_down);
CHECK_FALSE(explicit_name.args.compose_images_directory.has_value());
CHECK(explicit_name.args.compose_file_name == "my-stack.yaml");
}
TEST_CASE("parse_args: -d doesn't swallow a following flag as compose_file_name", "[unit]") {
auto result = run_parse({"-d", "--log-level", "debug"});
REQUIRE_FALSE(result.exit_code.has_value());
CHECK(result.args.mode == Mode::compose_down);
CHECK(result.args.compose_file_name == "compose.yaml");
CHECK(result.args.log_level_flag_given);
}
TEST_CASE("parse_args: -u and -d together on the same command line is a parse error", "[unit]") {
auto result = run_parse({"-u", "/var/lib/images", "-d", "/var/lib/images"});
REQUIRE(result.exit_code.has_value());