Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Apple Dev Container CLI (adevcontainer)

[![CI](https://github.com/wcgomes/apple-devcontainers/actions/workflows/ci.yml/badge.svg)](https://github.com/wcgomes/apple-devcontainers/actions/workflows/ci.yml)
[![tests](https://img.shields.io/badge/tests-1163%2B-brightgreen)](https://github.com/wcgomes/apple-devcontainers)
[![tests](https://img.shields.io/badge/tests-1176%2B-brightgreen)](https://github.com/wcgomes/apple-devcontainers)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Native Swift CLI that reads `devcontainer.json` and runs development environments on Apple [`container`](https://github.com/apple/container).
Expand Down Expand Up @@ -103,7 +103,7 @@ Apple upgrades wipe the plugin directory. PATH `adevcontainer` survives. Restage

### Interactive picker

The `start`, `exec`, `stop`, `delete`, `purge`, `rebuild`, and `inspect` commands open an interactive picker (picklist) in an interactive TTY when `--name` is not provided and multiple managed containers exist. In a non-interactive terminal, `--name <name>` is required in this case.
The `start`, `refresh-credentials`, `exec`, `stop`, `delete`, `purge`, `rebuild`, and `inspect` commands open an interactive picker (picklist) in an interactive TTY when `--name` is not provided and multiple managed containers exist. In a non-interactive terminal, `--name <name>` is required in this case.

| Command | Purpose |
| --- | --- |
Expand All @@ -114,7 +114,8 @@ The `start`, `exec`, `stop`, `delete`, `purge`, `rebuild`, and `inspect` command
| `adevcontainer clone <git-url> [--vscode] [--resume <config-dir>]` | Clone a repository into a named volume and start its dev container |
| `adevcontainer exec [-it] [--name <name>] [--] [cmd…]` | Open a shell or run a command in a running managed container |
| `adevcontainer list [--json]` | List managed dev containers |
| `adevcontainer start [--name <name>] [--vscode] [--json]` | Start a container by name or with the interactive picker |
| `adevcontainer start [--name <name>] [--vscode] [--json]` | Start a container by name or with the interactive picker. A real start refreshes git credentials; already running does not |
| `adevcontainer refresh-credentials [--name <name>]` | Refresh git credentials in a running managed container from the host |
| `adevcontainer stop [--name <name>]` | Stop a container by name or with the interactive picker |
| `adevcontainer inspect [--name <name>]` | Inspect a container by name or with the interactive picker |
| `adevcontainer delete [--name <name>]` | Remove a container, keeping its managed volumes |
Expand Down
51 changes: 51 additions & 0 deletions Sources/ADevContainerLib/Commands/RefreshCredentialsCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation

public enum RefreshCredentialsCommand {
/// Refresh git credentials in an already-running managed container from host
/// `git credential fill`. Does not start, stop, fetch, pull, push, or write the
/// workspace. Credential material enters the guest via stdin only.
public static func run(
name: String? = nil,
runtime: AppleContainerRuntime,
picker: InteractivePicker = .default,
credentials: any GitCredentialProviding = HostGitCredential()
) throws {
let info = try ManagedContainers.resolveSelection(
name: name,
runtime: runtime,
picker: picker
)
guard info.isRunning else {
throw CLIError(
code: CLIErrorCode.containerNotRunning,
message: "Container \(info.name) is not running (state: \(info.state))",
hint: "Run '\(CommandSurface.commandPrefix) start --name \(info.name)' to start it and refresh credentials"
)
}

StatusPrinter.status("Refreshing git credentials", item: info.name)
let refreshed: Bool
do {
refreshed = try GuestGitCredentialSeed(
credentials: credentials,
runner: LifecycleRunner.hostProcessRunnerOverride ?? FoundationProcessRunner()
).seedFromLabels(
containerId: info.id,
labels: info.labels,
connectionUser: GuestGitCredentialSeed.connectionUser(from: info.labels),
runtime: runtime
)
} catch let error as CLIError {
throw CLIError(
code: error.code,
message: "Failed to refresh git credentials in the container (\(error.message))",
hint: error.hint
)
}
if refreshed {
StatusPrinter.status("Git credentials refreshed", item: info.name)
} else {
StatusPrinter.status("No host git credentials to refresh", item: info.name)
}
}
}
12 changes: 11 additions & 1 deletion Sources/ADevContainerLib/Commands/StartCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public enum StartCommand {
picker: InteractivePicker = .default,
isTTY: Bool = AppleContainerConfig.stdinIsTTY(),
openEditorPrompt: RecoveryOpenEditorPrompt = .default,
localEnv: [String: String] = ProcessInfo.processInfo.environment
localEnv: [String: String] = ProcessInfo.processInfo.environment,
credentials: any GitCredentialProviding = HostGitCredential()
) throws {
let info = try ManagedContainers.resolveSelection(
name: options.name,
Expand Down Expand Up @@ -82,6 +83,15 @@ public enum StartCommand {
)
return
}
// Stopped → running only. Already-running returned above. Soft-fail: a bad
// refresh must not fail start, delete the container, or enter recovery.
GuestGitCredentialSeed.refreshSoft(
containerId: info.id,
labels: info.labels,
connectionUser: GuestGitCredentialSeed.connectionUser(from: info.labels),
runtime: runtime,
credentials: credentials
)
// Remelt after start for postStart/postAttach. Never run initialize here:
// volume config becoming readable must not violate initialize-before-start.
if hooksConfig == nil {
Expand Down
9 changes: 9 additions & 0 deletions Sources/ADevContainerLib/Commands/UpCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,15 @@ public enum UpCommand {
resetExistingName: existing.name
)
}
// Stopped → running only. Reuse of an already-running container returned
// above. Soft-fail: do not fail up, delete the container, or recover.
GuestGitCredentialSeed.refreshSoft(
containerId: existing.id,
labels: existing.labels,
connectionUser: GuestGitCredentialSeed.connectionUser(from: existing.labels),
runtime: runtime,
credentials: credentials
)
var reuseConfig = configForReuse(resolved.config, labels: existing.labels)
PostAttachConfigLoader.mergeFeaturePostAttach(
into: &reuseConfig,
Expand Down
Loading
Loading