diff --git a/CHANGELOG.md b/CHANGELOG.md index cc399f8..2cc53b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- `--auth-with` provision mode: passing a non-existent credentials file + path now offers to create it via interactive TUI login instead of + erroring. The container runs with the file bind-mounted; after exit + deva validates and reports the captured credentials (subscription type, + expiry), or removes the placeholder on abort. Combine with `-Q --rm` + for a clean one-shot provisioning flow. + ## [0.14.0] - 2026-07-10 ### Added diff --git a/agents/claude.sh b/agents/claude.sh index cf0f1ce..db15799 100644 --- a/agents/claude.sh +++ b/agents/claude.sh @@ -167,9 +167,17 @@ setup_claude_auth() { if [ -z "${CUSTOM_CREDENTIALS_FILE:-}" ]; then auth_error "CUSTOM_CREDENTIALS_FILE not set for credentials-file auth" fi - AUTH_DETAILS="credentials-file ($CUSTOM_CREDENTIALS_FILE)" + if [ "$AUTH_PROVISION_MODE" = true ]; then + mkdir -p "$(dirname "$CUSTOM_CREDENTIALS_FILE")" + printf '{}' > "$CUSTOM_CREDENTIALS_FILE" + chmod 600 "$CUSTOM_CREDENTIALS_FILE" + AUTH_DETAILS="credentials-file (provisioning: $CUSTOM_CREDENTIALS_FILE)" + echo "Provisioning credentials: log in via the TUI to populate $CUSTOM_CREDENTIALS_FILE" >&2 + else + AUTH_DETAILS="credentials-file ($CUSTOM_CREDENTIALS_FILE)" + echo "Using custom credentials: $CUSTOM_CREDENTIALS_FILE -> /home/deva/.claude/.credentials.json" >&2 + fi DOCKER_ARGS+=("-v" "$CUSTOM_CREDENTIALS_FILE:/home/deva/.claude/.credentials.json") - echo "Using custom credentials: $CUSTOM_CREDENTIALS_FILE -> /home/deva/.claude/.credentials.json" >&2 ;; *) auth_error "auth method '$method' not implemented" diff --git a/agents/shared_auth.sh b/agents/shared_auth.sh index 90b5db4..88d2a47 100644 --- a/agents/shared_auth.sh +++ b/agents/shared_auth.sh @@ -226,18 +226,95 @@ is_file_path() { [[ "$arg" == /* ]] || [[ "$arg" == ~* ]] || [[ "$arg" == ./* ]] || [[ "$arg" == ../* ]] || [[ "$arg" == *.json ]] } -expand_and_validate_file() { +AUTH_PROVISION_MODE=false + +expand_auth_file_tilde() { local path="$1" if [[ "$path" == ~* ]]; then path="${path/#\~/$HOME}" fi - if [ ! -f "$path" ]; then - auth_error "Credentials file not found: $path" - fi + printf '%s' "$path" +} + +resolve_absolute_path() { + local path="$1" if [[ "$path" == /* ]]; then - echo "$path" + printf '%s' "$path" + else + printf '%s/%s' "$(cd "$(dirname "$path")" && pwd)" "$(basename "$path")" + fi +} + +expand_and_validate_file() { + local path + path="$(expand_auth_file_tilde "$1")" + + if [ -f "$path" ]; then + resolve_absolute_path "$path" + return + fi + + # File missing — attempt provision mode + local dir + dir="$(dirname "$path")" + if [ ! -d "$dir" ]; then + auth_error "Credentials file not found: $path" \ + "Parent directory does not exist: $dir" + fi + + if [ ! -t 0 ]; then + auth_error "Credentials file not found: $path" \ + "Run interactively to provision via TUI login" + fi + + printf 'Credentials file not found: %s\n' "$path" >&2 + printf 'Create it via TUI login? [y/N] ' >&2 + local reply + read -r reply + case "$reply" in + y | Y | yes | YES) ;; + *) auth_error "Aborted: credentials file not found" ;; + esac + + AUTH_PROVISION_MODE=true + resolve_absolute_path "$path" +} + +finish_auth_provision() { + [ "$AUTH_PROVISION_MODE" = true ] || return 0 + [ "${DRY_RUN:-false}" = true ] && return 0 + local f="${CUSTOM_CREDENTIALS_FILE:-}" + [ -n "$f" ] || return 0 + + local ok=false + if [ -s "$f" ]; then + if command -v jq >/dev/null 2>&1; then + jq -e '.claudeAiOauth.accessToken' "$f" >/dev/null 2>&1 && ok=true + else + grep -q '"accessToken"' "$f" 2>/dev/null && ok=true + fi + fi + + if [ "$ok" = true ]; then + chmod 600 "$f" 2>/dev/null || true + echo "" >&2 + echo "Credentials captured: $f" >&2 + if command -v jq >/dev/null 2>&1; then + local sub expires_ms now_s days + sub=$(jq -r '.claudeAiOauth.subscriptionType // "unknown"' "$f" 2>/dev/null) + expires_ms=$(jq -r '.claudeAiOauth.expiresAt // 0' "$f" 2>/dev/null) + now_s=$(date +%s) + if [ "$expires_ms" -gt 0 ] 2>/dev/null; then + days=$(( (expires_ms / 1000 - now_s) / 86400 )) + echo " subscription: $sub, expires in ~${days}d" >&2 + else + echo " subscription: $sub" >&2 + fi + fi + echo "Reuse: deva claude --auth-with $f" >&2 else - echo "$(cd "$(dirname "$path")" && pwd)/$(basename "$path")" + rm -f "$f" + echo "warning: no credentials captured; removed placeholder $f" >&2 fi } diff --git a/deva.sh b/deva.sh index 7bf04e4..4c6ff14 100755 --- a/deva.sh +++ b/deva.sh @@ -3684,12 +3684,22 @@ if [ "$EPHEMERAL_MODE" = false ]; then update_session_file fi - exec docker exec "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" + if [ "$AUTH_PROVISION_MODE" = true ]; then + docker exec "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" || true + finish_auth_provision + else + exec docker exec "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" + fi else echo "Launching ${ACTIVE_AGENT} (ephemeral mode) via $(docker_image_ref)" write_session_file - if ! docker "${DOCKER_ARGS[@]}" "${AGENT_COMMAND[@]}"; then - echo "error: failed to launch ephemeral container" >&2 - exit 1 + if [ "$AUTH_PROVISION_MODE" = true ]; then + docker "${DOCKER_ARGS[@]}" "${AGENT_COMMAND[@]}" || true + finish_auth_provision + else + if ! docker "${DOCKER_ARGS[@]}" "${AGENT_COMMAND[@]}"; then + echo "error: failed to launch ephemeral container" >&2 + exit 1 + fi fi fi diff --git a/docs/authentication.md b/docs/authentication.md index 594a9fd..c0f40cc 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -152,6 +152,24 @@ Example: deva.sh claude --auth-with ~/work/claude-prod.credentials.json ``` +#### Provisioning new credentials + +If the file does not exist, deva offers to create it via TUI login: + +```bash +deva claude -Q --rm --auth-with ~/creds/xxx-claude-max.credentials.json -- --resume +# -> "Credentials file not found: .../xxx-claude-max.credentials.json" +# -> "Create it via TUI login? [y/N]" +# Log in via /login in the TUI, then exit when done. +# -> "Credentials captured: .../xxx-claude-max.credentials.json" +# -> " subscription: max, expires in ~365d" +# -> "Reuse: deva claude --auth-with .../xxx-claude-max.credentials.json" +``` + +Use `-Q` (bare mode) to avoid polluting your default config home with the +new account's identity. The placeholder file is removed automatically if +no credentials are captured. + ## Codex ### Default: `--auth-with chatgpt`