aircloud-cli 0.1.0__tar.gz → 0.2.0__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.
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: aircloud-cli
3
+ Version: 0.2.0
4
+ Summary: AirCloud CLI — endpoint management, SSH, and container exec
5
+ Author-email: AirCloud Team <support@aieev.com>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://aieev.com
8
+ Project-URL: Repository, https://github.com/aieev/aircloud-cli
9
+ Keywords: aircloud,ssh,cli,endpoint,exec
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development
22
+ Classifier: Topic :: System :: Shells
23
+ Classifier: Topic :: Utilities
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: websockets>=12.0
27
+ Requires-Dist: click>=8.0
28
+ Requires-Dist: httpx>=0.27.0
29
+ Requires-Dist: paramiko>=3.0
30
+ Requires-Dist: cryptography>=3.0
31
+
32
+ # aircloud-cli
33
+
34
+ CLI for the [AirCloud](https://aieev.com) platform — endpoint management,
35
+ SSH access, and container shell exec.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install aircloud-cli
41
+ ```
42
+
43
+ Requires Python 3.9+.
44
+
45
+ ## Quick start
46
+
47
+ ```bash
48
+ # one-time setup
49
+ aircloud config set api-key <YOUR_API_KEY>
50
+ aircloud config set api-base-url <API_BASE_URL>
51
+
52
+ # inspect endpoints
53
+ aircloud endpoints list
54
+ aircloud endpoints get <endpoint_id>
55
+
56
+ # open a shell in a container
57
+ aircloud ssh <endpoint_id> # public-key auth, sshd-based
58
+ aircloud exec <endpoint_id> # docker exec over WS, no sshd needed
59
+ ```
60
+
61
+ ## Commands
62
+
63
+ ### `aircloud config`
64
+
65
+ Manage CLI configuration in `~/.aircloud/config.json`.
66
+
67
+ ```bash
68
+ aircloud config set api-key <key>
69
+ aircloud config set api-base-url <url>
70
+ aircloud config list
71
+ aircloud config get <key>
72
+ ```
73
+
74
+ ### `aircloud endpoints`
75
+
76
+ ```bash
77
+ aircloud endpoints list # paged list
78
+ aircloud endpoints get <id> # detail
79
+ aircloud endpoints start <id>
80
+ aircloud endpoints stop <id>
81
+ aircloud endpoints scale <id> --replicas N
82
+ aircloud endpoints patch <id> --command "..." --port 8080 --env KEY=VALUE
83
+ aircloud endpoints replicas <id> # current replicas
84
+ aircloud endpoints logs <id> [--replica-id X] [--start-line N --end-line M]
85
+ ```
86
+
87
+ ### `aircloud ssh`
88
+
89
+ SSH into a container via WebSocket tunnel using public-key authentication.
90
+
91
+ ```bash
92
+ aircloud ssh <endpoint_id> # auto-discover ~/.ssh/id_*
93
+ aircloud ssh <endpoint_id> -i ~/.ssh/aircloud_key
94
+ aircloud ssh <endpoint_id> -r <replica_id> # pin to specific replica
95
+ aircloud ssh <endpoint_id> --tunnel-only # open tunnel only
96
+ ```
97
+
98
+ Auto-discovers identity files in this order: `~/.ssh/id_ed25519`,
99
+ `~/.ssh/id_ecdsa`, `~/.ssh/id_rsa`. Override with `-i / --identity-file`.
100
+
101
+ The container image must have `sshd` running and consume the
102
+ `AIRCLOUD_AUTHORIZED_KEYS` environment variable (AirCloud-provided template
103
+ images do this by default).
104
+
105
+ ### `aircloud ssh-proxy` / `aircloud ssh-config` (VS Code Remote-SSH, scp, git)
106
+
107
+ `aircloud ssh` opens its own tunnel and interactive shell. To use the
108
+ system `ssh`, `scp`, `git`, or **VS Code Remote-SSH** instead — without
109
+ pre-starting a tunnel or opening a local port — register `ssh-proxy` as an
110
+ OpenSSH `ProxyCommand`.
111
+
112
+ `ssh-proxy` bridges its own stdin/stdout to the WebSocket tunnel, so `ssh`
113
+ spawns one automatically per connection and tears it down on disconnect.
114
+
115
+ Install the config block into `~/.ssh/config`:
116
+
117
+ ```bash
118
+ aircloud ssh-config <endpoint_id> -i ~/.ssh/id_ed25519 --install
119
+ ```
120
+
121
+ `--install` writes the block between `# >>> aircloud managed: <id> >>>`
122
+ markers, so it is idempotent (re-running replaces in place, never
123
+ duplicates). It backs up `~/.ssh/config` first, resolves the absolute path
124
+ to `aircloud` (so GUI-launched VS Code finds it regardless of `PATH`), and
125
+ prepends the block above any catch-all `Host *` so its `User` / options win
126
+ under SSH's first-match-wins rule. The installed block:
127
+
128
+ ```
129
+ Host aircloud-<endpoint_id>
130
+ HostName <endpoint_id>
131
+ User root
132
+ IdentityFile ~/.ssh/id_ed25519
133
+ ProxyCommand /abs/path/to/aircloud ssh-proxy %h --stdio
134
+ ServerAliveInterval 30
135
+ ServerAliveCountMax 3
136
+ StrictHostKeyChecking accept-new
137
+ ```
138
+
139
+ Run without `--install` to print the block for review instead of writing
140
+ it, or with `--uninstall` to remove that endpoint's managed block.
141
+
142
+ Then connect with any SSH-based tool:
143
+
144
+ ```bash
145
+ ssh aircloud-<endpoint_id>
146
+ scp file.txt aircloud-<endpoint_id>:/workspace/
147
+ # VS Code: Remote-SSH → Connect to Host → aircloud-<endpoint_id>
148
+ ```
149
+
150
+ `ServerAliveInterval` keeps the session alive past L7 proxy idle timeouts.
151
+ The container image must run `sshd` (same requirement as `aircloud ssh`).
152
+
153
+ Running `aircloud ssh-proxy <endpoint_id> --stdio` directly in a terminal
154
+ only prints the server's SSH banner and then waits — it is a transport for
155
+ an SSH client, not a shell. Drive it through `ssh` / `ProxyCommand`.
156
+
157
+ #### Reconnection / transient relay slowness
158
+
159
+ When the relay is briefly slow (heavy workload on the node), the WebSocket
160
+ handshake can time out (`timed out during opening handshake`). `ssh-proxy`
161
+ retries the connect with exponential backoff so a transient blip is absorbed
162
+ instead of failing the connection. This applies to the **connect phase only**
163
+ — once a session is bridged, a mid-session drop ends it (SSH cannot resume),
164
+ and the client (VS Code, or you) reconnects, which spawns a fresh proxy that
165
+ retries again.
166
+
167
+ Tunables (env, read by `ssh-proxy`):
168
+
169
+ | Variable | Default | Meaning |
170
+ |---|---|---|
171
+ | `AIRCLOUD_SSH_OPEN_TIMEOUT` | `15` | Per-attempt WS handshake timeout (seconds) |
172
+ | `AIRCLOUD_SSH_CONNECT_RETRIES` | `3` | Retry count for transient failures (timeout / 5xx) |
173
+
174
+ Auth/routing failures (3xx/4xx) are not retried — they surface immediately.
175
+
176
+ For VS Code, raise its own connect timeout so a single attempt can span the
177
+ proxy's retries, in `settings.json`:
178
+
179
+ ```json
180
+ "remote.SSH.connectTimeout": 60
181
+ ```
182
+
183
+ ### `aircloud exec`
184
+
185
+ Open an interactive shell in the container via `docker exec` over a
186
+ WebSocket. Works on any image — no `sshd` required.
187
+
188
+ ```bash
189
+ aircloud exec <endpoint_id>
190
+ aircloud exec <endpoint_id> -r <replica_id> # pin replica
191
+ aircloud exec <endpoint_id> -c "/bin/sh" # custom command
192
+ ```
193
+
194
+ ### `aircloud whoami`
195
+
196
+ Print the identity associated with the current API key.
197
+
198
+ ```bash
199
+ aircloud whoami
200
+ ```
201
+
202
+ ## Development
203
+
204
+ Editable install — source edits are picked up immediately by the
205
+ `aircloud` command, no rebuild required:
206
+
207
+ ```bash
208
+ pip install -e .
209
+ aircloud --help
210
+ ```
211
+
212
+ ## Build & Release
213
+
214
+ Published to PyPI as `aircloud-cli`. Requires `build` and `twine`
215
+ (`pip install build twine`).
216
+
217
+ ```bash
218
+ # 1. bump the version in pyproject.toml (PyPI rejects re-uploading a version)
219
+
220
+ # 2. clean stale artifacts
221
+ rm -rf dist
222
+
223
+ # 3. build sdist + wheel into dist/
224
+ python -m build
225
+
226
+ # 4. upload to PyPI (needs a PyPI API token in ~/.pypirc or entered at the prompt)
227
+ twine upload dist/*
228
+
229
+ # 5. tag and push the release
230
+ git commit -am "release(x.y.z): <summary>"
231
+ git tag vx.y.z
232
+ git push origin main --tags
233
+ ```
234
+
235
+ Verify the published package in a clean environment:
236
+
237
+ ```bash
238
+ python -m venv /tmp/ac && /tmp/ac/bin/pip install "aircloud-cli==x.y.z"
239
+ /tmp/ac/bin/aircloud --help
240
+ ```
241
+
242
+ ## Status
243
+
244
+ This is an early functional release. Interfaces may change before `1.0.0`.
245
+
246
+ ## License
247
+
248
+ Proprietary. © AIEEV / AirCloud.
@@ -0,0 +1,217 @@
1
+ # aircloud-cli
2
+
3
+ CLI for the [AirCloud](https://aieev.com) platform — endpoint management,
4
+ SSH access, and container shell exec.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install aircloud-cli
10
+ ```
11
+
12
+ Requires Python 3.9+.
13
+
14
+ ## Quick start
15
+
16
+ ```bash
17
+ # one-time setup
18
+ aircloud config set api-key <YOUR_API_KEY>
19
+ aircloud config set api-base-url <API_BASE_URL>
20
+
21
+ # inspect endpoints
22
+ aircloud endpoints list
23
+ aircloud endpoints get <endpoint_id>
24
+
25
+ # open a shell in a container
26
+ aircloud ssh <endpoint_id> # public-key auth, sshd-based
27
+ aircloud exec <endpoint_id> # docker exec over WS, no sshd needed
28
+ ```
29
+
30
+ ## Commands
31
+
32
+ ### `aircloud config`
33
+
34
+ Manage CLI configuration in `~/.aircloud/config.json`.
35
+
36
+ ```bash
37
+ aircloud config set api-key <key>
38
+ aircloud config set api-base-url <url>
39
+ aircloud config list
40
+ aircloud config get <key>
41
+ ```
42
+
43
+ ### `aircloud endpoints`
44
+
45
+ ```bash
46
+ aircloud endpoints list # paged list
47
+ aircloud endpoints get <id> # detail
48
+ aircloud endpoints start <id>
49
+ aircloud endpoints stop <id>
50
+ aircloud endpoints scale <id> --replicas N
51
+ aircloud endpoints patch <id> --command "..." --port 8080 --env KEY=VALUE
52
+ aircloud endpoints replicas <id> # current replicas
53
+ aircloud endpoints logs <id> [--replica-id X] [--start-line N --end-line M]
54
+ ```
55
+
56
+ ### `aircloud ssh`
57
+
58
+ SSH into a container via WebSocket tunnel using public-key authentication.
59
+
60
+ ```bash
61
+ aircloud ssh <endpoint_id> # auto-discover ~/.ssh/id_*
62
+ aircloud ssh <endpoint_id> -i ~/.ssh/aircloud_key
63
+ aircloud ssh <endpoint_id> -r <replica_id> # pin to specific replica
64
+ aircloud ssh <endpoint_id> --tunnel-only # open tunnel only
65
+ ```
66
+
67
+ Auto-discovers identity files in this order: `~/.ssh/id_ed25519`,
68
+ `~/.ssh/id_ecdsa`, `~/.ssh/id_rsa`. Override with `-i / --identity-file`.
69
+
70
+ The container image must have `sshd` running and consume the
71
+ `AIRCLOUD_AUTHORIZED_KEYS` environment variable (AirCloud-provided template
72
+ images do this by default).
73
+
74
+ ### `aircloud ssh-proxy` / `aircloud ssh-config` (VS Code Remote-SSH, scp, git)
75
+
76
+ `aircloud ssh` opens its own tunnel and interactive shell. To use the
77
+ system `ssh`, `scp`, `git`, or **VS Code Remote-SSH** instead — without
78
+ pre-starting a tunnel or opening a local port — register `ssh-proxy` as an
79
+ OpenSSH `ProxyCommand`.
80
+
81
+ `ssh-proxy` bridges its own stdin/stdout to the WebSocket tunnel, so `ssh`
82
+ spawns one automatically per connection and tears it down on disconnect.
83
+
84
+ Install the config block into `~/.ssh/config`:
85
+
86
+ ```bash
87
+ aircloud ssh-config <endpoint_id> -i ~/.ssh/id_ed25519 --install
88
+ ```
89
+
90
+ `--install` writes the block between `# >>> aircloud managed: <id> >>>`
91
+ markers, so it is idempotent (re-running replaces in place, never
92
+ duplicates). It backs up `~/.ssh/config` first, resolves the absolute path
93
+ to `aircloud` (so GUI-launched VS Code finds it regardless of `PATH`), and
94
+ prepends the block above any catch-all `Host *` so its `User` / options win
95
+ under SSH's first-match-wins rule. The installed block:
96
+
97
+ ```
98
+ Host aircloud-<endpoint_id>
99
+ HostName <endpoint_id>
100
+ User root
101
+ IdentityFile ~/.ssh/id_ed25519
102
+ ProxyCommand /abs/path/to/aircloud ssh-proxy %h --stdio
103
+ ServerAliveInterval 30
104
+ ServerAliveCountMax 3
105
+ StrictHostKeyChecking accept-new
106
+ ```
107
+
108
+ Run without `--install` to print the block for review instead of writing
109
+ it, or with `--uninstall` to remove that endpoint's managed block.
110
+
111
+ Then connect with any SSH-based tool:
112
+
113
+ ```bash
114
+ ssh aircloud-<endpoint_id>
115
+ scp file.txt aircloud-<endpoint_id>:/workspace/
116
+ # VS Code: Remote-SSH → Connect to Host → aircloud-<endpoint_id>
117
+ ```
118
+
119
+ `ServerAliveInterval` keeps the session alive past L7 proxy idle timeouts.
120
+ The container image must run `sshd` (same requirement as `aircloud ssh`).
121
+
122
+ Running `aircloud ssh-proxy <endpoint_id> --stdio` directly in a terminal
123
+ only prints the server's SSH banner and then waits — it is a transport for
124
+ an SSH client, not a shell. Drive it through `ssh` / `ProxyCommand`.
125
+
126
+ #### Reconnection / transient relay slowness
127
+
128
+ When the relay is briefly slow (heavy workload on the node), the WebSocket
129
+ handshake can time out (`timed out during opening handshake`). `ssh-proxy`
130
+ retries the connect with exponential backoff so a transient blip is absorbed
131
+ instead of failing the connection. This applies to the **connect phase only**
132
+ — once a session is bridged, a mid-session drop ends it (SSH cannot resume),
133
+ and the client (VS Code, or you) reconnects, which spawns a fresh proxy that
134
+ retries again.
135
+
136
+ Tunables (env, read by `ssh-proxy`):
137
+
138
+ | Variable | Default | Meaning |
139
+ |---|---|---|
140
+ | `AIRCLOUD_SSH_OPEN_TIMEOUT` | `15` | Per-attempt WS handshake timeout (seconds) |
141
+ | `AIRCLOUD_SSH_CONNECT_RETRIES` | `3` | Retry count for transient failures (timeout / 5xx) |
142
+
143
+ Auth/routing failures (3xx/4xx) are not retried — they surface immediately.
144
+
145
+ For VS Code, raise its own connect timeout so a single attempt can span the
146
+ proxy's retries, in `settings.json`:
147
+
148
+ ```json
149
+ "remote.SSH.connectTimeout": 60
150
+ ```
151
+
152
+ ### `aircloud exec`
153
+
154
+ Open an interactive shell in the container via `docker exec` over a
155
+ WebSocket. Works on any image — no `sshd` required.
156
+
157
+ ```bash
158
+ aircloud exec <endpoint_id>
159
+ aircloud exec <endpoint_id> -r <replica_id> # pin replica
160
+ aircloud exec <endpoint_id> -c "/bin/sh" # custom command
161
+ ```
162
+
163
+ ### `aircloud whoami`
164
+
165
+ Print the identity associated with the current API key.
166
+
167
+ ```bash
168
+ aircloud whoami
169
+ ```
170
+
171
+ ## Development
172
+
173
+ Editable install — source edits are picked up immediately by the
174
+ `aircloud` command, no rebuild required:
175
+
176
+ ```bash
177
+ pip install -e .
178
+ aircloud --help
179
+ ```
180
+
181
+ ## Build & Release
182
+
183
+ Published to PyPI as `aircloud-cli`. Requires `build` and `twine`
184
+ (`pip install build twine`).
185
+
186
+ ```bash
187
+ # 1. bump the version in pyproject.toml (PyPI rejects re-uploading a version)
188
+
189
+ # 2. clean stale artifacts
190
+ rm -rf dist
191
+
192
+ # 3. build sdist + wheel into dist/
193
+ python -m build
194
+
195
+ # 4. upload to PyPI (needs a PyPI API token in ~/.pypirc or entered at the prompt)
196
+ twine upload dist/*
197
+
198
+ # 5. tag and push the release
199
+ git commit -am "release(x.y.z): <summary>"
200
+ git tag vx.y.z
201
+ git push origin main --tags
202
+ ```
203
+
204
+ Verify the published package in a clean environment:
205
+
206
+ ```bash
207
+ python -m venv /tmp/ac && /tmp/ac/bin/pip install "aircloud-cli==x.y.z"
208
+ /tmp/ac/bin/aircloud --help
209
+ ```
210
+
211
+ ## Status
212
+
213
+ This is an early functional release. Interfaces may change before `1.0.0`.
214
+
215
+ ## License
216
+
217
+ Proprietary. © AIEEV / AirCloud.
@@ -6,9 +6,10 @@ from urllib.parse import urlparse, urlunparse
6
6
  import click
7
7
 
8
8
  from aircloud_cli import config
9
+ from aircloud_cli import ssh_config as sshcfg
9
10
  from aircloud_cli.client import AirCloudAPIError, AirCloudClient, AirCloudError
10
11
  from aircloud_cli.container_exec import start_exec
11
- from aircloud_cli.ssh_tunnel import start_ssh, start_tunnel
12
+ from aircloud_cli.ssh_tunnel import run_stdio_proxy, start_ssh, start_tunnel
12
13
 
13
14
  DEFAULT_SSH_HOST = "your-aircloud-host.example.com"
14
15
 
@@ -437,6 +438,151 @@ def ssh(
437
438
  )
438
439
 
439
440
 
441
+ @cli.command("ssh-proxy")
442
+ @click.argument("endpoint_id")
443
+ @click.option(
444
+ "--stdio",
445
+ is_flag=True,
446
+ default=False,
447
+ help="Bridge stdin/stdout to the tunnel (for use as an ssh ProxyCommand).",
448
+ )
449
+ @click.option(
450
+ "--replica-id",
451
+ "-r",
452
+ default=None,
453
+ help="Pin the tunnel to a specific replica (default: load-balanced)",
454
+ )
455
+ @click.option(
456
+ "--host",
457
+ default=None,
458
+ help="Override host in the derived WS URL (default: inferred from endpoint)",
459
+ )
460
+ @click.option("--api-key", "-k", default=None, help="API key (default: from config)")
461
+ def ssh_proxy(
462
+ endpoint_id: str,
463
+ stdio: bool,
464
+ replica_id: str | None,
465
+ host: str | None,
466
+ api_key: str | None,
467
+ ):
468
+ """Bridge the SSH tunnel over stdin/stdout for use as an ssh ProxyCommand.
469
+
470
+ Designed for ~/.ssh/config so ssh / scp / VS Code Remote-SSH work
471
+ without pre-starting a tunnel or opening a local port:
472
+
473
+ \b
474
+ Host aircloud-myendpoint
475
+ HostName <ENDPOINT_ID>
476
+ User root
477
+ ProxyCommand aircloud ssh-proxy %h --stdio
478
+ ServerAliveInterval 30
479
+
480
+ Then `ssh aircloud-myendpoint` (or VS Code Remote-SSH) connects and
481
+ ssh spawns this proxy automatically. In --stdio mode stdout carries
482
+ raw SSH bytes, so all messages are emitted on stderr.
483
+ """
484
+ resolved_api_key = api_key or config.get_api_key()
485
+ if not resolved_api_key:
486
+ raise click.ClickException(
487
+ "API key not set. Run: aircloud config set api-key <key>"
488
+ )
489
+
490
+ client = _get_client(api_key=resolved_api_key)
491
+ endpoint = client.get_endpoint(endpoint_id)
492
+ serving_url = endpoint.get("serving_endpoint_url")
493
+ if not serving_url:
494
+ raise click.ClickException(
495
+ "Endpoint is missing serving_endpoint_url; cannot build SSH tunnel URL."
496
+ )
497
+
498
+ parsed = urlparse(serving_url)
499
+ scheme = "wss" if parsed.scheme == "https" else "ws"
500
+ netloc = host or parsed.netloc
501
+ path = parsed.path.rstrip("/") + "/ssh/tunnel"
502
+ ws_url = urlunparse((scheme, netloc, path, "", "", ""))
503
+
504
+ click.echo(
505
+ f"aircloud ssh-proxy: tunneling to {endpoint_id} via {netloc}", err=True
506
+ )
507
+ run_stdio_proxy(ws_url, resolved_api_key, replica_id)
508
+
509
+
510
+ @cli.command("ssh-config")
511
+ @click.argument("endpoint_id")
512
+ @click.option("--alias", default=None, help="Host alias (default: aircloud-<endpoint_id>)")
513
+ @click.option("--user", "-u", default="root", show_default=True, help="SSH user")
514
+ @click.option(
515
+ "--identity-file",
516
+ "-i",
517
+ default=None,
518
+ help="Path to SSH private key to pin in the config block",
519
+ )
520
+ @click.option(
521
+ "--install",
522
+ "do_install",
523
+ is_flag=True,
524
+ help="Write the managed block into ~/.ssh/config (idempotent, backs up first)",
525
+ )
526
+ @click.option(
527
+ "--uninstall",
528
+ "do_uninstall",
529
+ is_flag=True,
530
+ help="Remove the managed block for this endpoint from ~/.ssh/config",
531
+ )
532
+ def ssh_config(
533
+ endpoint_id: str,
534
+ alias: str | None,
535
+ user: str,
536
+ identity_file: str | None,
537
+ do_install: bool,
538
+ do_uninstall: bool,
539
+ ):
540
+ """Manage a ~/.ssh/config block that routes ssh/scp/VS Code through ssh-proxy.
541
+
542
+ With no flag the block is printed for review. --install writes it
543
+ idempotently between aircloud markers (backing up the file first and
544
+ using an absolute aircloud path so VS Code Remote-SSH can find it);
545
+ --uninstall removes that managed block.
546
+ """
547
+ if do_install and do_uninstall:
548
+ raise click.ClickException("Use only one of --install / --uninstall.")
549
+
550
+ host_alias = alias or f"aircloud-{endpoint_id}"
551
+
552
+ if do_uninstall:
553
+ path, removed, backup = sshcfg.uninstall(endpoint_id)
554
+ if removed:
555
+ click.echo(f"Removed aircloud block for {endpoint_id} from {path}")
556
+ if backup:
557
+ click.echo(f"Backup: {backup}")
558
+ else:
559
+ click.echo(f"No managed block for {endpoint_id} found in {path}")
560
+ return
561
+
562
+ aircloud_path = sshcfg.resolve_aircloud_path()
563
+
564
+ if do_install:
565
+ path, backup = sshcfg.install(
566
+ endpoint_id, host_alias, user, identity_file, aircloud_path
567
+ )
568
+ click.echo(f"Installed SSH config block into {path}")
569
+ if backup:
570
+ click.echo(f"Backup: {backup}")
571
+ click.echo(f"Connect: ssh {host_alias}")
572
+ click.echo(f"VS Code: Remote-SSH -> Connect to Host -> {host_alias}")
573
+ return
574
+
575
+ body = sshcfg.render_block(
576
+ endpoint_id, host_alias, user, identity_file, aircloud_path
577
+ )
578
+ click.echo(sshcfg.managed_block(endpoint_id, body).rstrip("\n"))
579
+ click.echo(
580
+ "\n# Append the block above to ~/.ssh/config, or rerun with --install.",
581
+ err=True,
582
+ )
583
+ click.echo(f"# Then: ssh {host_alias}", err=True)
584
+
585
+
440
586
  @cli.command("exec")
441
587
  @click.argument("endpoint_id")
442
588
  @click.option(