hop3-cli 0.5.0.dev1__tar.gz → 0.5.0.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/PKG-INFO +22 -10
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/README.md +21 -9
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/pyproject.toml +1 -1
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/arguments.py +24 -7
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/destructive.py +1 -1
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/flags.py +58 -25
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/__init__.py +30 -27
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/context_cmd.py +134 -55
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/help_text.py +4 -4
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/init_cmd.py +12 -2
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/login_cmd.py +41 -70
- hop3_cli-0.5.0.dev3/src/hop3_cli/commands/local/project_context_cmd.py +776 -0
- hop3_cli-0.5.0.dev3/src/hop3_cli/commands/local/server_cmd.py +599 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/ssh_ops.py +20 -1
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/config.py +73 -53
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/core/aliases.py +0 -13
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/core/app_scope.py +7 -1
- hop3_cli-0.5.0.dev3/src/hop3_cli/core/cli_state.py +117 -0
- hop3_cli-0.5.0.dev3/src/hop3_cli/core/context_names.py +72 -0
- hop3_cli-0.5.0.dev3/src/hop3_cli/core/deploy_preview.py +427 -0
- hop3_cli-0.5.0.dev3/src/hop3_cli/core/hop3_toml.py +86 -0
- hop3_cli-0.5.0.dev3/src/hop3_cli/core/local_overlay.py +335 -0
- hop3_cli-0.5.0.dev3/src/hop3_cli/core/project_guard.py +171 -0
- hop3_cli-0.5.0.dev3/src/hop3_cli/core/resolution.py +930 -0
- hop3_cli-0.5.0.dev3/src/hop3_cli/core/server_registry.py +309 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/exit_codes.py +24 -36
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/main.py +281 -38
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/rpc/client.py +11 -17
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/rpc/responses.py +7 -2
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/rpc/streaming.py +68 -31
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/tokens.py +6 -3
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/types.py +0 -1
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/ui/prompts.py +0 -11
- hop3_cli-0.5.0.dev1/src/hop3_cli/core/resolution.py +0 -186
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/__init__.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/__init__.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/help.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/aliases_cmd.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/auth_cmd.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/completion_cmd.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/settings_cmd.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/use_cmd.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/commands/local/version_cmd.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/core/__init__.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/core/alias_registry.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/core/suggest.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/exceptions.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/rpc/__init__.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/rpc/tunnel.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/ui/__init__.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/ui/console.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/ui/messages.py +0 -0
- {hop3_cli-0.5.0.dev1 → hop3_cli-0.5.0.dev3}/src/hop3_cli/ui/rich_printer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hop3-cli
|
|
3
|
-
Version: 0.5.0.
|
|
3
|
+
Version: 0.5.0.dev3
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Stefane Fermigier
|
|
6
6
|
Author-email: Stefane Fermigier <sf@abilian.com>
|
|
@@ -64,36 +64,48 @@ hop3 restart
|
|
|
64
64
|
|
|
65
65
|
## App Resolution
|
|
66
66
|
|
|
67
|
-
Commands that act on a single app (`logs`, `restart`, `config set`, `run`, …) no longer take the app name as a positional argument. Instead the CLI resolves it from a layered chain, walking sources until one supplies a value (ADR 036 D7):
|
|
67
|
+
Commands that act on a single app (`logs`, `restart`, `config set`, `run`, …) no longer take the app name as a positional argument. Instead the CLI resolves it from a layered chain, walking sources until one supplies a value (ADR 042 §Resolution chains; supersedes ADR 036 §D7):
|
|
68
68
|
|
|
69
69
|
1. `--app <name>` / `-a <name>` — explicit flag, wins over everything.
|
|
70
70
|
2. `$HOP3_APP` — environment variable for the current shell.
|
|
71
71
|
3. `.hop3-app` — one-line file in CWD or any ancestor up to `$HOME`. Drop it in a project repo and `hop3` from inside picks up the right app.
|
|
72
72
|
4. `hop3.toml [cli].app` — same search path as `.hop3-app`, lower priority.
|
|
73
|
-
5.
|
|
73
|
+
5. `hop3.toml [contexts.<resolved-context>].app` — when a project context resolves (see below), its `app` field becomes the source. Lets one project map per-environment to different apps.
|
|
74
|
+
6. `hop3.toml [metadata].id` — the project's canonical name. The "I'm physically standing in this project" source; outranks any server-level default so being inside a project always wins over a sticky binding from elsewhere.
|
|
75
|
+
7. Git remote `hop3-<env>` parsed for an app name — supports `git push hop3-prod main` style deploys.
|
|
76
|
+
8. Active server's `default_app` from `~/.config/hop3-cli/servers.toml` — set via `hop3 server use --default-app <app>`. Lowest-priority fallback for single-app users.
|
|
74
77
|
|
|
75
|
-
Use `hop3 --why <command>` to print the full trace and see which source won.
|
|
78
|
+
Use `hop3 --why <command>` to print the full trace and see which source won. `--why` is diagnostic-only: the command itself is not executed (so `hop3 deploy --why` is safe to run).
|
|
76
79
|
|
|
77
80
|
```bash
|
|
78
81
|
# Explicit (always works):
|
|
79
82
|
hop3 logs --app myapp
|
|
80
83
|
hop3 config set --app myapp KEY=value
|
|
81
84
|
|
|
82
|
-
# Sticky app for the context:
|
|
83
|
-
hop3 use myapp
|
|
84
|
-
hop3 logs
|
|
85
|
-
|
|
86
85
|
# Per-shell:
|
|
87
86
|
export HOP3_APP=myapp
|
|
88
87
|
|
|
89
|
-
# Per-directory:
|
|
88
|
+
# Per-directory (legacy pin file):
|
|
90
89
|
echo myapp > .hop3-app
|
|
91
90
|
|
|
91
|
+
# Per-project, declared in hop3.toml:
|
|
92
|
+
# [metadata]
|
|
93
|
+
# id = "myapp"
|
|
94
|
+
# [contexts.staging]
|
|
95
|
+
# app = "myapp-staging"
|
|
96
|
+
|
|
92
97
|
# Debug:
|
|
93
98
|
hop3 --why logs
|
|
94
99
|
```
|
|
95
100
|
|
|
96
|
-
|
|
101
|
+
The ADR 042 model splits the old "context" noun into two concepts:
|
|
102
|
+
|
|
103
|
+
- **Server records** — credentialed bindings to a Hop3 server. Live in `~/.config/hop3-cli/servers.toml`. Managed with `hop3 server` (`add` / `list` / `show` / `use` / `login` / `remove`).
|
|
104
|
+
- **Project contexts** — environment-shaped overlays on a single project. Declared as `[contexts.<name>]` blocks inside `hop3.toml`. Managed with `hop3 context` from inside a project tree (`init` / `use` / `list` / `show` / `add` / `remove`). The currently-selected one is recorded in `.hop3-local.toml` (gitignored automatically).
|
|
105
|
+
|
|
106
|
+
A `hop3 deploy` from a project tree resolves: server (which Hop3 to talk to) + context (which environment of this project) + app (which app on that server) — and shows a preview-and-confirm prompt with the plan before invoking the deploy RPC. `--dry-run` exits after printing the plan; `--force` bypasses the project-mismatch safety guard.
|
|
107
|
+
|
|
108
|
+
See [CLI Reference: App Resolution](../../docs/src/reference/cli.md#app-resolution) and [ADR 042](../../notes/adrs/042-cli-context-model.md) for the full specification.
|
|
97
109
|
|
|
98
110
|
## Configuration
|
|
99
111
|
|
|
@@ -44,36 +44,48 @@ hop3 restart
|
|
|
44
44
|
|
|
45
45
|
## App Resolution
|
|
46
46
|
|
|
47
|
-
Commands that act on a single app (`logs`, `restart`, `config set`, `run`, …) no longer take the app name as a positional argument. Instead the CLI resolves it from a layered chain, walking sources until one supplies a value (ADR 036 D7):
|
|
47
|
+
Commands that act on a single app (`logs`, `restart`, `config set`, `run`, …) no longer take the app name as a positional argument. Instead the CLI resolves it from a layered chain, walking sources until one supplies a value (ADR 042 §Resolution chains; supersedes ADR 036 §D7):
|
|
48
48
|
|
|
49
49
|
1. `--app <name>` / `-a <name>` — explicit flag, wins over everything.
|
|
50
50
|
2. `$HOP3_APP` — environment variable for the current shell.
|
|
51
51
|
3. `.hop3-app` — one-line file in CWD or any ancestor up to `$HOME`. Drop it in a project repo and `hop3` from inside picks up the right app.
|
|
52
52
|
4. `hop3.toml [cli].app` — same search path as `.hop3-app`, lower priority.
|
|
53
|
-
5.
|
|
53
|
+
5. `hop3.toml [contexts.<resolved-context>].app` — when a project context resolves (see below), its `app` field becomes the source. Lets one project map per-environment to different apps.
|
|
54
|
+
6. `hop3.toml [metadata].id` — the project's canonical name. The "I'm physically standing in this project" source; outranks any server-level default so being inside a project always wins over a sticky binding from elsewhere.
|
|
55
|
+
7. Git remote `hop3-<env>` parsed for an app name — supports `git push hop3-prod main` style deploys.
|
|
56
|
+
8. Active server's `default_app` from `~/.config/hop3-cli/servers.toml` — set via `hop3 server use --default-app <app>`. Lowest-priority fallback for single-app users.
|
|
54
57
|
|
|
55
|
-
Use `hop3 --why <command>` to print the full trace and see which source won.
|
|
58
|
+
Use `hop3 --why <command>` to print the full trace and see which source won. `--why` is diagnostic-only: the command itself is not executed (so `hop3 deploy --why` is safe to run).
|
|
56
59
|
|
|
57
60
|
```bash
|
|
58
61
|
# Explicit (always works):
|
|
59
62
|
hop3 logs --app myapp
|
|
60
63
|
hop3 config set --app myapp KEY=value
|
|
61
64
|
|
|
62
|
-
# Sticky app for the context:
|
|
63
|
-
hop3 use myapp
|
|
64
|
-
hop3 logs
|
|
65
|
-
|
|
66
65
|
# Per-shell:
|
|
67
66
|
export HOP3_APP=myapp
|
|
68
67
|
|
|
69
|
-
# Per-directory:
|
|
68
|
+
# Per-directory (legacy pin file):
|
|
70
69
|
echo myapp > .hop3-app
|
|
71
70
|
|
|
71
|
+
# Per-project, declared in hop3.toml:
|
|
72
|
+
# [metadata]
|
|
73
|
+
# id = "myapp"
|
|
74
|
+
# [contexts.staging]
|
|
75
|
+
# app = "myapp-staging"
|
|
76
|
+
|
|
72
77
|
# Debug:
|
|
73
78
|
hop3 --why logs
|
|
74
79
|
```
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
The ADR 042 model splits the old "context" noun into two concepts:
|
|
82
|
+
|
|
83
|
+
- **Server records** — credentialed bindings to a Hop3 server. Live in `~/.config/hop3-cli/servers.toml`. Managed with `hop3 server` (`add` / `list` / `show` / `use` / `login` / `remove`).
|
|
84
|
+
- **Project contexts** — environment-shaped overlays on a single project. Declared as `[contexts.<name>]` blocks inside `hop3.toml`. Managed with `hop3 context` from inside a project tree (`init` / `use` / `list` / `show` / `add` / `remove`). The currently-selected one is recorded in `.hop3-local.toml` (gitignored automatically).
|
|
85
|
+
|
|
86
|
+
A `hop3 deploy` from a project tree resolves: server (which Hop3 to talk to) + context (which environment of this project) + app (which app on that server) — and shows a preview-and-confirm prompt with the plan before invoking the deploy RPC. `--dry-run` exits after printing the plan; `--force` bypasses the project-mismatch safety guard.
|
|
87
|
+
|
|
88
|
+
See [CLI Reference: App Resolution](../../docs/src/reference/cli.md#app-resolution) and [ADR 042](../../notes/adrs/042-cli-context-model.md) for the full specification.
|
|
77
89
|
|
|
78
90
|
## Configuration
|
|
79
91
|
|
|
@@ -133,26 +133,43 @@ def _resolve_password_inputs(args: list[str]) -> None:
|
|
|
133
133
|
the next positional after the command tokens. Server-side commands keep
|
|
134
134
|
their existing positional contract — the CLI is the security boundary.
|
|
135
135
|
|
|
136
|
-
Applies
|
|
136
|
+
Applies to the three commands that take a password:
|
|
137
137
|
hop3 user add <username> <email> <password>
|
|
138
138
|
hop3 user set-password <username> <password>
|
|
139
|
+
hop3 auth login <username> <password>
|
|
139
140
|
"""
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if args[1] not in {"add", "set-password"}:
|
|
141
|
+
insert_at = _password_insert_index(args)
|
|
142
|
+
if insert_at is None:
|
|
143
143
|
return
|
|
144
144
|
|
|
145
145
|
password = _extract_password_flag(args)
|
|
146
146
|
if password is None:
|
|
147
147
|
return
|
|
148
148
|
|
|
149
|
-
# Insertion index: after `user add <user> <email>` for add; after
|
|
150
|
-
# `user set-password <user>` for set-password.
|
|
151
|
-
insert_at = 4 if args[1] == "add" else 3
|
|
152
149
|
insert_at = min(insert_at, len(args))
|
|
153
150
|
args.insert(insert_at, password)
|
|
154
151
|
|
|
155
152
|
|
|
153
|
+
def _password_insert_index(args: list[str]) -> int | None:
|
|
154
|
+
"""Return the positional index where the resolved password should land.
|
|
155
|
+
|
|
156
|
+
Returns None if the current ``args`` does not target a command that
|
|
157
|
+
accepts a password.
|
|
158
|
+
"""
|
|
159
|
+
if len(args) < 2:
|
|
160
|
+
return None
|
|
161
|
+
if args[0] == "user" and args[1] == "add":
|
|
162
|
+
# user add <username> <email> <password>
|
|
163
|
+
return 4
|
|
164
|
+
if args[0] == "user" and args[1] == "set-password":
|
|
165
|
+
# user set-password <username> <password>
|
|
166
|
+
return 3
|
|
167
|
+
if args[0] == "auth" and args[1] == "login":
|
|
168
|
+
# auth login <username> <password>
|
|
169
|
+
return 3
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
156
173
|
def _extract_password_flag(args: list[str]) -> str | None:
|
|
157
174
|
"""Pop --password-file / --stdin from args and return the resolved password.
|
|
158
175
|
|
|
@@ -79,7 +79,7 @@ def _confirm_protected_context(config: Config | None) -> tuple[bool, str | None]
|
|
|
79
79
|
return True, context_name
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
def confirm_destructive_action(
|
|
82
|
+
def confirm_destructive_action( # noqa: PLR0911 — sequential decision tree, each return is a distinct escape hatch (json mode, no-match, missing-args, --confirm, --no-input, protected context, …) with its own side effects; flattening into a result var would obscure the safety story.
|
|
83
83
|
cli_args: list[str],
|
|
84
84
|
printer: RichPrinter,
|
|
85
85
|
config: Config | None = None,
|
|
@@ -32,7 +32,15 @@ class CliFlags:
|
|
|
32
32
|
"""CLI flags that control output and behavior."""
|
|
33
33
|
|
|
34
34
|
json_output: bool = False # --json, -j: Machine-readable JSON output
|
|
35
|
-
skip_confirm: bool = False # -y, --yes
|
|
35
|
+
skip_confirm: bool = False # -y, --yes: Skip confirmation prompts
|
|
36
|
+
|
|
37
|
+
# ADR 042 §D14: --force is the dedicated bypass for the project-
|
|
38
|
+
# mismatch guard. It also implies skip_confirm (a --force user has
|
|
39
|
+
# already opted into "yes really do it"), but the converse is NOT
|
|
40
|
+
# true: -y/--yes alone must NOT silence the safety guard, or scripts
|
|
41
|
+
# running `hop3 deploy -y` from the wrong directory deploy to the
|
|
42
|
+
# wrong app — exactly the scenario the guard exists to prevent.
|
|
43
|
+
force: bool = False
|
|
36
44
|
|
|
37
45
|
# Verbosity is now stored as a level (0=quiet, 1=normal, 2=verbose, 3=debug)
|
|
38
46
|
# This allows -vv, -vvv, -qq, etc.
|
|
@@ -41,12 +49,18 @@ class CliFlags:
|
|
|
41
49
|
# Context override for multi-server support
|
|
42
50
|
context: str | None = None # --context <name>: Use a specific context
|
|
43
51
|
|
|
52
|
+
# ADR 042: `--server` / `-s` selects a server from the global registry
|
|
53
|
+
# explicitly. Highest-priority source in the server resolution chain;
|
|
54
|
+
# bypasses any context-derived server.
|
|
55
|
+
server: str | None = None
|
|
56
|
+
|
|
44
57
|
# ADR 036 D5: `--app` / `-a` is always a flag, never positional.
|
|
45
58
|
# ADR 036 D7: if not set, the CLI will resolve one via the app-resolution
|
|
46
59
|
# chain (env, .hop3-app file, hop3.toml, context default).
|
|
47
60
|
app: str | None = None
|
|
48
61
|
|
|
49
|
-
# ADR 036: `--why` prints the resolution trace
|
|
62
|
+
# ADR 036: `--why` prints the resolution trace and exits without running
|
|
63
|
+
# the command (diagnostic-only — avoids `hop3 deploy --why` deploying).
|
|
50
64
|
why: bool = False
|
|
51
65
|
|
|
52
66
|
# ADR 036: `--no-alias` bypasses alias resolution.
|
|
@@ -62,6 +76,11 @@ class CliFlags:
|
|
|
62
76
|
# automation/CI; complements `--yes` (which says "yes, take action").
|
|
63
77
|
no_input: bool = False
|
|
64
78
|
|
|
79
|
+
# ADR 042 §Deploy preview: `--dry-run` prints the deploy plan and
|
|
80
|
+
# exits without invoking the RPC. Analogous to `--why` but for the
|
|
81
|
+
# action plan rather than the resolution trace.
|
|
82
|
+
dry_run: bool = False
|
|
83
|
+
|
|
65
84
|
@property
|
|
66
85
|
def quiet(self) -> bool:
|
|
67
86
|
"""True if verbosity is 0 (quiet mode)."""
|
|
@@ -78,7 +97,7 @@ class CliFlags:
|
|
|
78
97
|
return self.verbosity >= 3
|
|
79
98
|
|
|
80
99
|
|
|
81
|
-
def _parse_verbosity_flag(arg: str, current: int) -> int | None:
|
|
100
|
+
def _parse_verbosity_flag(arg: str, current: int) -> int | None: # noqa: PLR0911 — mix of exact matches (--debug/--verbose/--quiet) and pattern matches (-v*/-d*/-q* with length-dependent verbosity); a unified table would have to encode the per-prefix length-to-level math and would read worse than the straight-line cascade.
|
|
82
101
|
"""Parse a verbosity-related flag; return the new verbosity or None if not one."""
|
|
83
102
|
if arg == "--debug":
|
|
84
103
|
return 3
|
|
@@ -135,12 +154,15 @@ def parse_flags(args: list[str]) -> tuple[CliFlags, list[str]]:
|
|
|
135
154
|
state: dict[str, Any] = {
|
|
136
155
|
"json_output": False,
|
|
137
156
|
"skip_confirm": False,
|
|
157
|
+
"force": False,
|
|
138
158
|
"context": None,
|
|
159
|
+
"server": None,
|
|
139
160
|
"app": None,
|
|
140
161
|
"why": False,
|
|
141
162
|
"no_alias": False,
|
|
142
163
|
"confirm_value": None,
|
|
143
164
|
"no_input": False,
|
|
165
|
+
"dry_run": False,
|
|
144
166
|
"verbosity": _get_env_verbosity() or 1,
|
|
145
167
|
}
|
|
146
168
|
|
|
@@ -157,8 +179,30 @@ def parse_flags(args: list[str]) -> tuple[CliFlags, list[str]]:
|
|
|
157
179
|
return CliFlags(**state), remaining_args
|
|
158
180
|
|
|
159
181
|
|
|
160
|
-
|
|
161
|
-
|
|
182
|
+
# Boolean flags: token → state field to set True. One row per logical flag,
|
|
183
|
+
# with aliases grouped in the tuple key.
|
|
184
|
+
_BOOL_FLAGS: dict[tuple[str, ...], str] = {
|
|
185
|
+
("--json", "-j"): "json_output",
|
|
186
|
+
("-y", "--yes"): "skip_confirm",
|
|
187
|
+
("--why",): "why",
|
|
188
|
+
("--no-alias",): "no_alias",
|
|
189
|
+
("--no-input",): "no_input",
|
|
190
|
+
("--dry-run",): "dry_run",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# --force needs its own row because it sets two flags at once: the
|
|
195
|
+
# §D14 bypass AND skip_confirm (a --force user has implicitly opted
|
|
196
|
+
# into the prompt skip). Handled in _apply_flag.
|
|
197
|
+
_FORCE_FLAG = "--force"
|
|
198
|
+
|
|
199
|
+
# Two-token "--flag value" pairs.
|
|
200
|
+
_PAIR_FLAGS: dict[tuple[str, ...], str] = {
|
|
201
|
+
("--context", "-c"): "context",
|
|
202
|
+
("--server", "-s"): "server",
|
|
203
|
+
("--app", "-a"): "app",
|
|
204
|
+
("--confirm",): "confirm_value",
|
|
205
|
+
}
|
|
162
206
|
|
|
163
207
|
|
|
164
208
|
def _apply_flag(args: list[str], i: int, state: dict[str, Any]) -> int:
|
|
@@ -169,32 +213,21 @@ def _apply_flag(args: list[str], i: int, state: dict[str, Any]) -> int:
|
|
|
169
213
|
"""
|
|
170
214
|
arg = args[i]
|
|
171
215
|
|
|
172
|
-
if arg
|
|
173
|
-
state["
|
|
174
|
-
return 1
|
|
175
|
-
if arg in _YES_FLAGS:
|
|
216
|
+
if arg == _FORCE_FLAG:
|
|
217
|
+
state["force"] = True
|
|
176
218
|
state["skip_confirm"] = True
|
|
177
219
|
return 1
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if arg == "--no-input":
|
|
185
|
-
state["no_input"] = True
|
|
186
|
-
return 1
|
|
220
|
+
|
|
221
|
+
for keys, field_name in _BOOL_FLAGS.items():
|
|
222
|
+
if arg in keys:
|
|
223
|
+
state[field_name] = True
|
|
224
|
+
return 1
|
|
225
|
+
|
|
187
226
|
if arg.startswith("--confirm="):
|
|
188
227
|
state["confirm_value"] = arg.split("=", 1)[1]
|
|
189
228
|
return 1
|
|
190
229
|
|
|
191
|
-
|
|
192
|
-
pair_keys = {
|
|
193
|
-
("--context", "-c"): "context",
|
|
194
|
-
("--app", "-a"): "app",
|
|
195
|
-
("--confirm",): "confirm_value",
|
|
196
|
-
}
|
|
197
|
-
for keys, field_name in pair_keys.items():
|
|
230
|
+
for keys, field_name in _PAIR_FLAGS.items():
|
|
198
231
|
if arg in keys and i + 1 < len(args):
|
|
199
232
|
state[field_name] = args[i + 1]
|
|
200
233
|
return 2
|
|
@@ -22,6 +22,7 @@ from .completion_cmd import handle_completion
|
|
|
22
22
|
from .context_cmd import handle_context
|
|
23
23
|
from .init_cmd import handle_init
|
|
24
24
|
from .login_cmd import handle_login, handle_login_token
|
|
25
|
+
from .server_cmd import handle_server
|
|
25
26
|
from .settings_cmd import handle_settings, settings_get, settings_set, settings_show
|
|
26
27
|
from .ssh_ops import BootstrapError, extract_token, infer_server_url
|
|
27
28
|
from .use_cmd import handle_use
|
|
@@ -44,6 +45,7 @@ __all__ = [
|
|
|
44
45
|
"handle_local_command",
|
|
45
46
|
"handle_login",
|
|
46
47
|
"handle_login_token",
|
|
48
|
+
"handle_server",
|
|
47
49
|
"handle_settings",
|
|
48
50
|
"handle_use",
|
|
49
51
|
"handle_version",
|
|
@@ -59,9 +61,10 @@ __all__ = [
|
|
|
59
61
|
LOCAL_COMMANDS_INFO = {
|
|
60
62
|
"aliases": "List all effective aliases (built-in, plugin, user).",
|
|
61
63
|
"completion": "Generate shell completion scripts.",
|
|
62
|
-
"context": "Manage
|
|
64
|
+
"context": "Manage project deploy contexts (or legacy server contexts).",
|
|
63
65
|
"init": "Initialize connection to a Hop3 server via SSH.",
|
|
64
66
|
"login": "Authenticate to a server.",
|
|
67
|
+
"server": "Manage server bindings (ADR 042).",
|
|
65
68
|
"settings": "Manage local CLI settings (server URL, token, SSL).",
|
|
66
69
|
"use": "Set / show / clear the current context's default app.",
|
|
67
70
|
"version": "Show CLI version.",
|
|
@@ -97,6 +100,23 @@ def is_local_command(args: list[str]) -> bool:
|
|
|
97
100
|
return not (command == "auth" and len(args) > 1 and not args[1].startswith("-"))
|
|
98
101
|
|
|
99
102
|
|
|
103
|
+
# Dispatch table for local commands. Each entry maps a CLI token to its
|
|
104
|
+
# handler. Handlers that always claim the command return None; ``auth`` is
|
|
105
|
+
# the one exception — it returns a bool to delegate back to the server when
|
|
106
|
+
# the subcommand belongs in the server-side `auth` namespace.
|
|
107
|
+
_LOCAL_HANDLERS = {
|
|
108
|
+
"completion": handle_completion,
|
|
109
|
+
"context": handle_context,
|
|
110
|
+
"init": handle_init,
|
|
111
|
+
"login": handle_login,
|
|
112
|
+
"server": handle_server,
|
|
113
|
+
"settings": handle_settings,
|
|
114
|
+
"aliases": handle_aliases,
|
|
115
|
+
"use": handle_use,
|
|
116
|
+
}
|
|
117
|
+
_VERSION_TOKENS = {"version", "--version", "-V"}
|
|
118
|
+
|
|
119
|
+
|
|
100
120
|
def handle_local_command(args: list[str], config: Config, printer: RichPrinter) -> bool:
|
|
101
121
|
"""Handle a local command.
|
|
102
122
|
|
|
@@ -106,35 +126,18 @@ def handle_local_command(args: list[str], config: Config, printer: RichPrinter)
|
|
|
106
126
|
if not args:
|
|
107
127
|
return False
|
|
108
128
|
|
|
109
|
-
command = args[0]
|
|
110
|
-
cmd_args = args[1:]
|
|
129
|
+
command, cmd_args = args[0], args[1:]
|
|
111
130
|
|
|
112
|
-
if command
|
|
113
|
-
handle_completion(cmd_args, config, printer)
|
|
114
|
-
return True
|
|
115
|
-
if command == "context":
|
|
116
|
-
handle_context(cmd_args, config, printer)
|
|
117
|
-
return True
|
|
118
|
-
if command == "init":
|
|
119
|
-
handle_init(cmd_args, config, printer)
|
|
120
|
-
return True
|
|
121
|
-
if command == "login":
|
|
122
|
-
handle_login(cmd_args, config, printer)
|
|
123
|
-
return True
|
|
124
|
-
if command == "settings":
|
|
125
|
-
handle_settings(cmd_args, config, printer)
|
|
126
|
-
return True
|
|
127
|
-
if command == "aliases":
|
|
128
|
-
handle_aliases(cmd_args, config, printer)
|
|
129
|
-
return True
|
|
130
|
-
if command == "use":
|
|
131
|
-
handle_use(cmd_args, config, printer)
|
|
132
|
-
return True
|
|
133
|
-
if command in {"version", "--version", "-V"}:
|
|
131
|
+
if command in _VERSION_TOKENS:
|
|
134
132
|
handle_version(cmd_args, config, printer)
|
|
135
133
|
return True
|
|
134
|
+
|
|
136
135
|
if command == "auth":
|
|
137
|
-
# handle_auth returns False if command should go to server
|
|
136
|
+
# handle_auth returns False if command should go to server.
|
|
138
137
|
return handle_auth(cmd_args, config, printer)
|
|
139
138
|
|
|
140
|
-
|
|
139
|
+
handler = _LOCAL_HANDLERS.get(command)
|
|
140
|
+
if handler is None:
|
|
141
|
+
return False
|
|
142
|
+
handler(cmd_args, config, printer)
|
|
143
|
+
return True
|