Skip to content

Commit cca87c3

Browse files
pofallonclaude
andauthored
feat: set-up subcommand — MVP scaffold + lifecycle hooks (PR-6a) (#36)
Implements the core value of `set-up` per `docs/subcommand-specs/set-up/SPEC.md`: convert an already-running container into a DevContainer by applying configuration + image metadata and executing lifecycle hooks, emitting a single-line JSON result on stdout. ## CLI surface `deacon set-up --container-id <id> [--config <path>] [--skip-post-create] [--skip-non-blocking-commands] [--remote-env NAME=VALUE]... [--include-configuration] [--include-merged-configuration] [--container-data-folder <path>]` ## What this PR includes - `--container-id` resolution + inspect validation. Missing container fails with the upstream-aligned summary `"Dev container not found."` - Optional `--config` load via `ConfigLoader::load_with_extends` (extends chain honored per CLAUDE.md). Missing path fails with `"Dev container config (<path>) not found."` - Image-metadata extraction from the container's `devcontainer.metadata` label. Tolerates BOTH the JSON-array form (PR-2 / #27) and the single-object form for older images. - Config merge: file config wins over image metadata on scalar fields (spec §4 `mergeConfiguration(config.config, imageMetadata)`). - Variable substitution for both `configuration` and `mergedConfiguration`. - Lifecycle hook execution via `ContainerLifecycle` (onCreate → updateContent → postCreate → postStart → postAttach), gated by `--skip-post-create` (skips ALL phases per spec §2) and `--skip-non-blocking-commands` (stops after the configured `waitFor`). - JSON output per spec §10: `{outcome: "success", configuration?, mergedConfiguration?}`. `containerId` is intentionally excluded (spec §16 design decision). ## Deferred to PR-6b - `/etc/environment` + `/etc/profile` root-side patches with system markers under `/var/devcontainer/` - Dotfiles installer with target-path marker (would reuse `crates/deacon/src/commands/up/dotfiles.rs`) - A second substitution pass against the live container environment (`${containerEnv:VAR}`) — current pass uses the configured `container_env`, not a live `docker exec` env probe These are spec §5 phases 3a and 3c; both marked "best-effort" with graceful fallback on failure. Splitting them out keeps PR-6a reviewable. ## Tests 14 new unit tests in `set_up::tests`: - `--remote-env` parsing (accepts `NAME=VALUE`, rejects malformed input) - `--config` loading (default when absent, error when missing path) - Image-metadata label parsing (missing → None, array form, single-object form, invalid JSON → error) - Config merging (file wins over metadata; file-only when no metadata) - Argument defaults and JSON-result shape (outcome field, optional fields) Verification: - `cargo fmt --all -- --check` - `cargo clippy --all-targets -- -D warnings` - `cargo test -p deacon --lib` → 227 pass - `make test-nextest-fast` → 1927 pass (no regression) Refs: issue #34 (Tier 1 progress tracker), plan `/home/vscode/.claude/plans/let-s-come-up-with-recursive-sutherland.md` (PR-6 sequencing rationale). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a53328a commit cca87c3

3 files changed

Lines changed: 719 additions & 1 deletion

File tree

crates/deacon/src/cli.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ pub enum Commands {
517517
/// Skip postCreate lifecycle phase
518518
#[arg(long)]
519519
skip_post_create: bool,
520-
/// Skip postAttach lifecycle phase
520+
/// Skip postAttach lifecycle phase
521521
#[arg(long)]
522522
skip_post_attach: bool,
523523
/// Skip non-blocking commands (postStart & postAttach phases)
@@ -537,6 +537,41 @@ pub enum Commands {
537537
id_label: Vec<String>,
538538
},
539539

540+
/// Convert an already-running container into a DevContainer by applying
541+
/// configuration + image metadata, executing lifecycle hooks, and emitting
542+
/// a JSON snapshot of the resulting configuration.
543+
///
544+
/// See `docs/subcommand-specs/set-up/SPEC.md` for the authoritative behavior.
545+
#[cfg(feature = "full")]
546+
SetUp {
547+
/// Target container ID (required). The container must already exist.
548+
#[arg(long)]
549+
container_id: String,
550+
/// Optional path to a devcontainer.json to layer on top of the
551+
/// container's embedded image metadata.
552+
#[arg(long)]
553+
config: Option<PathBuf>,
554+
/// Skip all lifecycle hooks (onCreate, updateContent, postCreate,
555+
/// postStart, postAttach) and dotfiles installation.
556+
#[arg(long)]
557+
skip_post_create: bool,
558+
/// Stop after the configured `waitFor` hook (default `updateContent`).
559+
#[arg(long)]
560+
skip_non_blocking_commands: bool,
561+
/// Extra remote env to inject when running hooks (repeatable).
562+
#[arg(long = "remote-env", action = clap::ArgAction::Append)]
563+
remote_env: Vec<String>,
564+
/// Include the (substituted) configuration in the JSON result.
565+
#[arg(long)]
566+
include_configuration: bool,
567+
/// Include the (substituted) merged configuration in the JSON result.
568+
#[arg(long)]
569+
include_merged_configuration: bool,
570+
/// Inside-container user data root (default `~/.devcontainer`).
571+
#[arg(long)]
572+
container_data_folder: Option<PathBuf>,
573+
},
574+
540575
/// Stop and optionally remove development container or compose project
541576
Down {
542577
/// Remove containers after stopping them
@@ -1402,6 +1437,37 @@ impl Cli {
14021437

14031438
execute_run_user_commands(args).await
14041439
}
1440+
#[cfg(feature = "full")]
1441+
Some(Commands::SetUp {
1442+
container_id,
1443+
config,
1444+
skip_post_create,
1445+
skip_non_blocking_commands,
1446+
remote_env,
1447+
include_configuration,
1448+
include_merged_configuration,
1449+
container_data_folder,
1450+
}) => {
1451+
use crate::commands::set_up::{execute_set_up, SetUpArgs};
1452+
1453+
let args = SetUpArgs {
1454+
container_id,
1455+
// Per spec §2: --config is local to set-up and overrides
1456+
// the global --config when both are present.
1457+
config_path: config.or(self.config.clone()),
1458+
skip_post_create,
1459+
skip_non_blocking_commands,
1460+
remote_env,
1461+
include_configuration,
1462+
include_merged_configuration,
1463+
container_data_folder: container_data_folder
1464+
.or_else(|| self.container_data_folder.clone()),
1465+
docker_path: self.docker_path.clone(),
1466+
progress_tracker: progress_tracker.clone(),
1467+
};
1468+
1469+
execute_set_up(args).await
1470+
}
14051471
Some(Commands::Down {
14061472
remove,
14071473
all,

crates/deacon/src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub mod outdated;
1313
pub mod read_configuration;
1414
#[cfg(feature = "full")]
1515
pub mod run_user_commands;
16+
#[cfg(feature = "full")]
17+
pub mod set_up;
1618
pub mod shared;
1719
#[cfg(feature = "full")]
1820
pub mod templates;

0 commit comments

Comments
 (0)