3636# Subcommand map for prefix resolution: {(aliases...): [subcmds]}
3737SUBCMD_MAP = {
3838 ('container' , 'ct' ): ['up' , 'down' , 'ps' , 'login' , 'logs' , 'scale' , 'build' ],
39- ('env' ,): ['init' , 'sync' , 'list' , 'set' , 'get' , 'delete' , 'edit' , 'project' ],
39+ ('env' ,): ['init' , 'sync' , 'list' , 'set' , 'get' , 'delete' , 'edit' , 'project' , 'export' ],
4040 ('plugin' , 'pl' ): ['list' , 'install' , 'uninstall' , 'update' , 'info' , 'sync' , 'repo' ],
4141 ('snapshot' , 'ss' ): ['create' , 'list' , 'restore' , 'copy' , 'delete' , 'rotate' ],
4242}
4343
44+ # 後方互換: prefix が複数候補にマッチする場合に、特定の入力を特定のサブコマンドに
45+ # 優先的に解決させる。例えば `devbase env e` は従来 `edit` のみに解決されていたが、
46+ # `export` 追加後は ambiguous になるため、既存ショートカットを維持するために維持先を明示する。
47+ SUBCMD_PREFIX_PREFERENCES = {
48+ ('env' ,): {
49+ 'e' : 'edit' ,
50+ },
51+ }
52+
4453
4554def _require_devbase_root () -> Path :
4655 """Get DEVBASE_ROOT from environment, exiting if not set."""
@@ -109,6 +118,37 @@ def _add_env_parser(subparsers):
109118 env_sub .add_parser ('edit' , help = 'Open .env in editor' )
110119 env_sub .add_parser ('project' , help = 'Setup project-specific variables' )
111120
121+ env_export = env_sub .add_parser (
122+ 'export' ,
123+ help = 'Export .env files as an encrypted bundle (age)' ,
124+ )
125+ env_export .add_argument ('dest' , nargs = '?' , default = None ,
126+ help = "Output path (default: ./devbase-env-<TS>.dbenv, '-' for stdout)" )
127+ env_export .add_argument ('--include-project' , action = 'append' , default = None ,
128+ metavar = 'NAME' , dest = 'include_projects' ,
129+ help = 'Limit to specified project (repeatable)' )
130+ env_export .add_argument ('--exclude-project' , action = 'append' , default = [],
131+ metavar = 'NAME' , dest = 'exclude_projects' ,
132+ help = 'Exclude project (repeatable)' )
133+ env_export .add_argument ('--no-global' , action = 'store_true' ,
134+ help = 'Exclude $DEVBASE_ROOT/.env' )
135+ env_export .add_argument ('--no-metadata' , action = 'store_true' ,
136+ help = 'Exclude $DEVBASE_ROOT/.env.sources.yml' )
137+ env_export .add_argument ('--recipient' , action = 'append' , default = [],
138+ metavar = 'KEY' , dest = 'recipients' ,
139+ help = ("age / OpenSSH public key (repeatable). "
140+ "Formats: 'age1...', 'ssh-ed25519 AAAA...', 'ssh-rsa AAAA...', "
141+ "'@PATH' for file reference. "
142+ "Default: ~/.ssh/id_ed25519.pub, then ~/.ssh/id_rsa.pub "
143+ "(first existing one)" ))
144+ env_export .add_argument ('--passphrase-env' , metavar = 'VAR' , default = None ,
145+ help = 'Read passphrase from environment variable VAR' )
146+ env_export .add_argument ('--passphrase-stdin' , action = 'store_true' ,
147+ help = 'Read passphrase from the first line of stdin' )
148+ env_export .add_argument ('--force-unencrypted' , action = 'store_true' ,
149+ help = 'Write as plaintext tar.gz (rejected by default; '
150+ 'warns when sensitive keys are detected)' )
151+
112152
113153def _add_plugin_parser (subparsers ):
114154 """Plugin group parser"""
@@ -250,14 +290,22 @@ def _create_parser():
250290 return parser
251291
252292
253- def _resolve_prefix (input_cmd , candidates ):
293+ def _resolve_prefix (input_cmd , candidates , preferences = None ):
254294 """Resolve an abbreviated command to its full name via unique prefix matching.
255295
256- Returns the full command name if exactly one candidate matches,
257- otherwise returns the input as-is (ambiguous or no match).
296+ Returns the full command name if exactly one candidate matches.
297+ If ambiguous, falls back to `preferences[input_cmd]` (if provided) to keep
298+ backward compatibility with previously-unique abbreviations.
299+ Otherwise returns the input as-is.
258300 """
259301 matches = [c for c in candidates if c .startswith (input_cmd )]
260- return matches [0 ] if len (matches ) == 1 else input_cmd
302+ if len (matches ) == 1 :
303+ return matches [0 ]
304+ if preferences and input_cmd in preferences :
305+ preferred = preferences [input_cmd ]
306+ if preferred in matches :
307+ return preferred
308+ return input_cmd
261309
262310
263311def _expand_argv ():
@@ -273,7 +321,8 @@ def _expand_argv():
273321 cmd = sys .argv [1 ]
274322 for aliases , subcmds in SUBCMD_MAP .items ():
275323 if cmd in aliases :
276- sys .argv [2 ] = _resolve_prefix (sys .argv [2 ], subcmds )
324+ preferences = SUBCMD_PREFIX_PREFERENCES .get (aliases )
325+ sys .argv [2 ] = _resolve_prefix (sys .argv [2 ], subcmds , preferences )
277326 break
278327
279328 # plugin repo sub-subcommand
0 commit comments