Skip to content
Open
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
13 changes: 13 additions & 0 deletions etc/_devbase
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ _devbase() {
'delete:Delete a variable'
'edit:Open .env in editor'
'project:Setup project-specific variables'
'export:Export .env files as an encrypted bundle (age)'
)

plugin_subcommands=(
Expand Down Expand Up @@ -150,6 +151,18 @@ _devbase() {
get|delete)
_arguments '1:key:'
;;
export)
_arguments \
'1:dest:_files' \
'*--include-project[Limit to specified project (repeatable)]:name:' \
'*--exclude-project[Exclude project (repeatable)]:name:' \
'--no-global[Exclude $DEVBASE_ROOT/.env]' \
'--no-metadata[Exclude $DEVBASE_ROOT/.env.sources.yml]' \
'*--recipient[age / OpenSSH public key (repeatable)]:key:' \
'--passphrase-env[Read passphrase from env var]:var:' \
'--passphrase-stdin[Read passphrase from stdin]' \
'--force-unencrypted[Write as plaintext tar.gz]'
;;
*)
_describe -t env-commands 'env command' env_subcommands
;;
Expand Down
7 changes: 6 additions & 1 deletion etc/devbase-completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ _devbase_completions() {

local commands="init status shell-rc container ct env plugin pl snapshot ss up down login build ps help"
local container_subcommands="up down ps login logs scale build"
local env_subcommands="init sync list set get delete edit project"
local env_subcommands="init sync list set get delete edit project export"
local plugin_subcommands="list install uninstall update info sync repo"
local repo_subcommands="add remove list refresh"
local snapshot_subcommands="create list restore copy delete rotate"
Expand Down Expand Up @@ -81,6 +81,11 @@ _devbase_completions() {
COMPREPLY=($(compgen -W "--project -p" -- "$cur"))
fi
;;
export)
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "--include-project --exclude-project --no-global --no-metadata --recipient --passphrase-env --passphrase-stdin --force-unencrypted" -- "$cur"))
fi
;;
esac
fi
# plugin subcommand arguments
Expand Down
61 changes: 55 additions & 6 deletions lib/devbase/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,20 @@
# Subcommand map for prefix resolution: {(aliases...): [subcmds]}
SUBCMD_MAP = {
('container', 'ct'): ['up', 'down', 'ps', 'login', 'logs', 'scale', 'build'],
('env',): ['init', 'sync', 'list', 'set', 'get', 'delete', 'edit', 'project'],
('env',): ['init', 'sync', 'list', 'set', 'get', 'delete', 'edit', 'project', 'export'],
('plugin', 'pl'): ['list', 'install', 'uninstall', 'update', 'info', 'sync', 'repo'],
('snapshot', 'ss'): ['create', 'list', 'restore', 'copy', 'delete', 'rotate'],
}

# 後方互換: prefix が複数候補にマッチする場合に、特定の入力を特定のサブコマンドに
# 優先的に解決させる。例えば `devbase env e` は従来 `edit` のみに解決されていたが、
# `export` 追加後は ambiguous になるため、既存ショートカットを維持するために維持先を明示する。
SUBCMD_PREFIX_PREFERENCES = {
('env',): {
'e': 'edit',
},
}


def _require_devbase_root() -> Path:
"""Get DEVBASE_ROOT from environment, exiting if not set."""
Expand Down Expand Up @@ -109,6 +118,37 @@ def _add_env_parser(subparsers):
env_sub.add_parser('edit', help='Open .env in editor')
env_sub.add_parser('project', help='Setup project-specific variables')

env_export = env_sub.add_parser(
'export',
help='Export .env files as an encrypted bundle (age)',
)
env_export.add_argument('dest', nargs='?', default=None,
help="Output path (default: ./devbase-env-<TS>.dbenv, '-' for stdout)")
env_export.add_argument('--include-project', action='append', default=None,
metavar='NAME', dest='include_projects',
help='Limit to specified project (repeatable)')
env_export.add_argument('--exclude-project', action='append', default=[],
metavar='NAME', dest='exclude_projects',
help='Exclude project (repeatable)')
env_export.add_argument('--no-global', action='store_true',
help='Exclude $DEVBASE_ROOT/.env')
env_export.add_argument('--no-metadata', action='store_true',
help='Exclude $DEVBASE_ROOT/.env.sources.yml')
env_export.add_argument('--recipient', action='append', default=[],
metavar='KEY', dest='recipients',
help=("age / OpenSSH public key (repeatable). "
"Formats: 'age1...', 'ssh-ed25519 AAAA...', 'ssh-rsa AAAA...', "
"'@PATH' for file reference. "
"Default: ~/.ssh/id_ed25519.pub, then ~/.ssh/id_rsa.pub "
"(first existing one)"))
env_export.add_argument('--passphrase-env', metavar='VAR', default=None,
help='Read passphrase from environment variable VAR')
env_export.add_argument('--passphrase-stdin', action='store_true',
help='Read passphrase from the first line of stdin')
env_export.add_argument('--force-unencrypted', action='store_true',
help='Write as plaintext tar.gz (rejected by default; '
'warns when sensitive keys are detected)')


def _add_plugin_parser(subparsers):
"""Plugin group parser"""
Expand Down Expand Up @@ -250,14 +290,22 @@ def _create_parser():
return parser


def _resolve_prefix(input_cmd, candidates):
def _resolve_prefix(input_cmd, candidates, preferences=None):
"""Resolve an abbreviated command to its full name via unique prefix matching.

Returns the full command name if exactly one candidate matches,
otherwise returns the input as-is (ambiguous or no match).
Returns the full command name if exactly one candidate matches.
If ambiguous, falls back to `preferences[input_cmd]` (if provided) to keep
backward compatibility with previously-unique abbreviations.
Otherwise returns the input as-is.
"""
matches = [c for c in candidates if c.startswith(input_cmd)]
return matches[0] if len(matches) == 1 else input_cmd
if len(matches) == 1:
return matches[0]
if preferences and input_cmd in preferences:
preferred = preferences[input_cmd]
if preferred in matches:
return preferred
return input_cmd


def _expand_argv():
Expand All @@ -273,7 +321,8 @@ def _expand_argv():
cmd = sys.argv[1]
for aliases, subcmds in SUBCMD_MAP.items():
if cmd in aliases:
sys.argv[2] = _resolve_prefix(sys.argv[2], subcmds)
preferences = SUBCMD_PREFIX_PREFERENCES.get(aliases)
sys.argv[2] = _resolve_prefix(sys.argv[2], subcmds, preferences)
break

# plugin repo sub-subcommand
Expand Down
19 changes: 19 additions & 0 deletions lib/devbase/commands/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def cmd_env(devbase_root: Path, args) -> int:
'delete': lambda: cmd_env_delete(devbase_root, getattr(args, 'key', '')),
'edit': lambda: cmd_env_edit(devbase_root),
'project': lambda: cmd_env_project(devbase_root),
'export': lambda: cmd_env_export(devbase_root, args),
}

handler = handlers.get(subcmd)
Expand Down Expand Up @@ -382,6 +383,24 @@ def cmd_env_project(devbase_root: Path) -> int:
return 0


def cmd_env_export(devbase_root: Path, args) -> int:
"""devbase env export"""
from devbase.env.io_export import ExportOptions, export

opts = ExportOptions(
dest=getattr(args, 'dest', None),
include_global=not getattr(args, 'no_global', False),
include_metadata=not getattr(args, 'no_metadata', False),
include_projects=getattr(args, 'include_projects', None),
exclude_projects=list(getattr(args, 'exclude_projects', []) or []),
recipients=list(getattr(args, 'recipients', []) or []),
passphrase_env=getattr(args, 'passphrase_env', None),
passphrase_stdin=getattr(args, 'passphrase_stdin', False),
force_unencrypted=getattr(args, 'force_unencrypted', False),
)
return export(devbase_root, opts)


def _update_source_metadata(devbase_root: Path, env_file: EnvFile) -> None:
"""ソースメタデータを更新する"""
sources = SourcesManager(devbase_root)
Expand Down
Loading
Loading