avr-cli 0.4.0__tar.gz → 0.5.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.
Files changed (107) hide show
  1. {avr_cli-0.4.0 → avr_cli-0.5.0}/PKG-INFO +82 -5
  2. {avr_cli-0.4.0 → avr_cli-0.5.0}/README.md +80 -4
  3. {avr_cli-0.4.0 → avr_cli-0.5.0}/avr_cli.egg-info/PKG-INFO +82 -5
  4. {avr_cli-0.4.0 → avr_cli-0.5.0}/avr_cli.egg-info/SOURCES.txt +10 -0
  5. {avr_cli-0.4.0 → avr_cli-0.5.0}/avr_cli.egg-info/requires.txt +1 -0
  6. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/_build_info.py +1 -1
  7. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/api_client.py +27 -0
  8. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/auth.py +5 -0
  9. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/click_ext.py +21 -0
  10. avr_cli-0.5.0/avrea_cli/commands/artifact.py +272 -0
  11. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/auth_cmd.py +101 -7
  12. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/config_cmd.py +20 -2
  13. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/job.py +3 -1
  14. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/repo.py +309 -40
  15. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/run.py +5 -0
  16. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/sbom.py +266 -56
  17. avr_cli-0.5.0/avrea_cli/commands/token.py +553 -0
  18. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/config.py +11 -5
  19. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/display.py +25 -3
  20. avr_cli-0.5.0/avrea_cli/git_mirror.py +207 -0
  21. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/helpers.py +125 -15
  22. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/json_output.py +16 -9
  23. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/log_display.py +3 -0
  24. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/main.py +3 -0
  25. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/repo_context.py +140 -56
  26. avr_cli-0.5.0/avrea_cli/spawn_script.py +89 -0
  27. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/transport.py +9 -2
  28. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/vm.py +226 -32
  29. {avr_cli-0.4.0 → avr_cli-0.5.0}/pyproject.toml +5 -1
  30. avr_cli-0.5.0/tests/test_artifact.py +164 -0
  31. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_env_conventions.py +57 -0
  32. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_helpers.py +59 -0
  33. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_job_list.py +11 -0
  34. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_job_metrics_cmd.py +4 -3
  35. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_log_display.py +11 -0
  36. avr_cli-0.5.0/tests/test_repo_clone.py +775 -0
  37. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_repo_context.py +48 -15
  38. avr_cli-0.5.0/tests/test_repo_mirror.py +612 -0
  39. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_run_view.py +11 -0
  40. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_sbom.py +556 -9
  41. avr_cli-0.5.0/tests/test_scoped_token.py +519 -0
  42. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_settings.py +12 -12
  43. avr_cli-0.5.0/tests/test_token.py +774 -0
  44. avr_cli-0.5.0/tests/test_token_output.py +534 -0
  45. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_vm.py +6 -3
  46. avr_cli-0.5.0/tests/test_vm_spawn_script.py +190 -0
  47. avr_cli-0.4.0/tests/test_repo_mirror.py +0 -310
  48. {avr_cli-0.4.0 → avr_cli-0.5.0}/LICENSE +0 -0
  49. {avr_cli-0.4.0 → avr_cli-0.5.0}/avr_cli.egg-info/dependency_links.txt +0 -0
  50. {avr_cli-0.4.0 → avr_cli-0.5.0}/avr_cli.egg-info/entry_points.txt +0 -0
  51. {avr_cli-0.4.0 → avr_cli-0.5.0}/avr_cli.egg-info/top_level.txt +0 -0
  52. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/__init__.py +0 -0
  53. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/assets/logo_mark.svg +0 -0
  54. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/audit.py +0 -0
  55. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/billing.py +0 -0
  56. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/__init__.py +0 -0
  57. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/cache.py +0 -0
  58. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/health.py +0 -0
  59. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/internal.py +0 -0
  60. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/log.py +0 -0
  61. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/org.py +0 -0
  62. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/pr.py +0 -0
  63. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/settings.py +0 -0
  64. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/status.py +0 -0
  65. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/commands/workflow.py +0 -0
  66. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/docs_gen.py +0 -0
  67. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/firewall.py +0 -0
  68. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/metrics_display.py +0 -0
  69. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/output.py +0 -0
  70. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/paths.py +0 -0
  71. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/run_diagnostics.py +0 -0
  72. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/run_diagnostics_display.py +0 -0
  73. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/run_refs.py +0 -0
  74. {avr_cli-0.4.0 → avr_cli-0.5.0}/avrea_cli/version.py +0 -0
  75. {avr_cli-0.4.0 → avr_cli-0.5.0}/setup.cfg +0 -0
  76. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_api_client.py +0 -0
  77. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_auth.py +0 -0
  78. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_auth_login.py +0 -0
  79. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_auth_sso.py +0 -0
  80. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_cache.py +0 -0
  81. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_config.py +0 -0
  82. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_config_cmd.py +0 -0
  83. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_display.py +0 -0
  84. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_docs_gen.py +0 -0
  85. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_firewall.py +0 -0
  86. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_global_flags.py +0 -0
  87. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_job_view_logs.py +0 -0
  88. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_json_output.py +0 -0
  89. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_lazy_registry.py +0 -0
  90. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_metrics_display.py +0 -0
  91. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_org_email_domain.py +0 -0
  92. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_org_saml.py +0 -0
  93. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_output.py +0 -0
  94. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_piped_output.py +0 -0
  95. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_pr_list.py +0 -0
  96. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_public_mirrors.py +0 -0
  97. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_run_attempts.py +0 -0
  98. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_run_command_refs.py +0 -0
  99. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_run_diagnose.py +0 -0
  100. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_run_list_flags.py +0 -0
  101. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_run_logs.py +0 -0
  102. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_run_refs.py +0 -0
  103. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_transport.py +0 -0
  104. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_web_flag.py +0 -0
  105. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_workflow_actions.py +0 -0
  106. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_workflow_run.py +0 -0
  107. {avr_cli-0.4.0 → avr_cli-0.5.0}/tests/test_workflow_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avr-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Avrea command-line client
5
5
  Author-email: Avrea <support@avrea.com>
6
6
  License-Expression: Apache-2.0
@@ -15,6 +15,7 @@ Requires-Python: >=3.14
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: anyio>=4.14.2
18
+ Requires-Dist: certifi>=2026.10.19
18
19
  Requires-Dist: click>=8.3.1
19
20
  Requires-Dist: httpx>=0.28.1
20
21
  Requires-Dist: platformdirs>=4.0
@@ -117,6 +118,61 @@ Auto-detection means most commands "just work" without `--repo`. Outside a git t
117
118
 
118
119
  ## Common workflows
119
120
 
121
+ **Clone from a nearby Git mirror**
122
+
123
+ ```sh
124
+ avr repo clone foo/faa
125
+ avr repo clone foo/faa my-checkout --depth 1 --branch main
126
+ avr repo clone foo/faa --placement https://git.fi.a.avrea.io
127
+ ```
128
+
129
+ Avrea discovers the repository's placements and picks the first mirror that
130
+ responds successfully from your machine. `--placement` accepts an advertised
131
+ mirror URL or hostname to select one explicitly. Give the `owner/repository` name
132
+ or an Avrea repository ID (`rep-...`).
133
+ The repository must already have Git mirroring enabled and a usable placement.
134
+
135
+ `origin` stays on the selected read-only mirror. The clone installs a
136
+ repository-local credential helper so ordinary `git fetch` and `git pull`
137
+ obtain fresh Git tokens through your Avrea login. Push changes to the source
138
+ repository using a separate remote, for example:
139
+
140
+ ```sh
141
+ git remote add upstream git@github.com:foo/faa.git
142
+ git push upstream HEAD
143
+ ```
144
+
145
+ Tokens normally last one hour; scoped API keys are limited to ten minutes or
146
+ their remaining lifetime. Git tokens are never saved in the remote URL or
147
+ repository configuration. Keep `avr` installed and logged in for later Git
148
+ operations; the helper continues to use the API host used for the clone.
149
+
150
+ **Create a mirror at a location**
151
+
152
+ ```sh
153
+ avr repo mirror locations
154
+ avr repo mirror create foo/faa --location gsc-fi
155
+ avr repo mirror create group/subgroup/project --source gitlab --location gsc-fi
156
+ avr repo mirror status --repo group/subgroup/project --source gitlab
157
+ ```
158
+
159
+ `--source` defaults to `github`; `gitlab` supports nested group names. The
160
+ repository must already be connected to your Avrea organization. Use `--org`
161
+ to select an organization. Mirror management requires the organization's
162
+ Git-mirror feature and an admin role.
163
+
164
+ `--location` accepts a cluster ID, datacenter ID, or unique name from `locations`.
165
+ The command enables mirroring and adds the placement, preserving existing
166
+ placements. Sync runs asynchronously; use `status` to check it. Repeating
167
+ `create` is safe, including when enabling succeeded but placement failed.
168
+ `locations` and `create` also support `--json` and `--jq`.
169
+
170
+ GitLab repositories cannot be mirrored until GitLab mirroring is enabled
171
+ for your organization. `--source gitlab` is available for deployments that
172
+ support it, including repositories in nested groups. If a GitLab repository
173
+ returns “not found,” check its connection to Avrea, your access, and whether
174
+ GitLab mirroring is enabled.
175
+
120
176
  **Watch a run live**
121
177
 
122
178
  ```sh
@@ -239,11 +295,32 @@ case $? in
239
295
  esac
240
296
  ```
241
297
 
242
- Exit code `4` is reserved for "auth required". `1` is general failure; `2` is a usage error.
298
+ Exit code `4` is reserved for "auth required". `1` is general failure; `2` is a usage error. `130` means Ctrl-C
299
+ ended the command, including a watch, a log follow or a tunnel that is stopped that way.
300
+
301
+ **Scoped tokens for scripts and agents**
302
+
303
+ ```sh
304
+ avr token create --name ci-read --org <org-id> --repo acme/api --ttl 8h # prints the credential once
305
+ avr token create --name agent --org <org-id> --vm <vm-id> --allow-vm-create --vm-create-limit 3
306
+ avr token list
307
+ avr token view <token-id>
308
+ avr token revoke <token-id>
309
+ ```
310
+
311
+ A scoped token is a short-lived credential (60 seconds to 7 days, 8 hours by default) bound to one organization. Repository grants are read-only and VM grants are admin: it reads only the repositories it names, has admin access to only the VMs it names, and cannot create further tokens. `create` prints the `export AVR_TOKEN=...` and `export AVR_ORG=...` lines for whatever will use the token. Scoped tokens require the `org-...` ID in `AVR_ORG` or `--org`; they ignore the login's stored organization and cannot resolve a slug. `avr auth status` reports this configuration without checking whether the token is still live. A 403 or 404 comes with a reminder that the token reaches only what it names.
312
+
313
+ `avr repo clone acme/api` uses the `owner/repository` name directly.
314
+
315
+ With a scoped token, a repository name (`--repo acme/api`, `AVR_REPO` or the checkout's remote) is matched against the repositories the token reaches; a `rep-...` ID works as well. For runs, use the run ID rather than an Avrea console URL, whose organization slug cannot be resolved with a scoped token. Token IDs for `view` and `revoke` are `key-` followed by 32 lowercase hexadecimal characters.
316
+
317
+ Creation returns the credential only once. If `--jq` fails or removes it, the command falls back to the requested JSON without filtering and exits nonzero. Save the credential from that JSON. Output failures also report a revoke command on stderr without exposing the credential there. If the creation request is interrupted, its response is lost, or the answer is a server error (5xx), a token may already exist: check `avr token list` before retrying. Only a 503 that carries the API's own reason means that no token was created.
318
+
319
+ If `avr token create` returns a 404, check the API's reason, the organization and the API host. If scoped-token access is unavailable, contact support@avrea.com to request it. VM grants and `--allow-vm-create` also require customer VMs to be enabled for the organization. `list`, `view` and `revoke` remain available when token creation is disabled.
243
320
 
244
321
  **Pipe-aware output**
245
322
 
246
- When stdout isn't a TTY, list commands switch to tab-separated rows (no color, no truncation, ISO timestamps), so `avr run list | awk` works without flags. `avr run watch | jq -c .` automatically switches to NDJSON event mode.
323
+ When stdout isn't a TTY, list commands switch to tab-separated rows (no color, no truncation, ISO timestamps), so `avr run list | awk` works without flags. Control characters, Unicode line separators and bidirectional controls inside cells are escaped (`\t`, `\x1b`, `\u2028`), keeping each item on one row with stable columns. The escaping is not reversible: a literal backslash is left as it is, so a cell that reads `a\tb` may hold either a tab or a backslash and a `t`. Use `--json` when the exact value matters; JSON retains the original values. `avr run watch | jq -c .` automatically switches to NDJSON event mode.
247
324
 
248
325
  ## Configuration
249
326
 
@@ -252,8 +329,8 @@ When stdout isn't a TTY, list commands switch to tab-separated rows (no color, n
252
329
  | Variable | Purpose |
253
330
  | ---------------- | ------------------------------------------------------------------------------------------------ |
254
331
  | `AVR_HOST` | Avrea API URL. Defaults to the active host in `hosts.json`, then `https://api.avrea.com`. |
255
- | `AVR_TOKEN` | API key. Overrides whatever's stored for the active host. |
256
- | `AVR_ORG` | Default organization ID. Overrides the stored default. |
332
+ | `AVR_TOKEN` | API key or scoped token. Overrides whatever's stored for the active host. |
333
+ | `AVR_ORG` | Default organization ID. Overrides the stored default. A scoped token needs it, or `--org`, except for `repo clone`. |
257
334
  | `AVR_REPO` | Default repository (`org/name` or `rep-xxx`). Overrides git auto-detect. |
258
335
  | `AVR_BROWSER` | Browser to launch for `--web` and OAuth login. Falls back to `BROWSER`, then system default. |
259
336
  | `AVR_PAGER` | Pager for long output. Overrides `PAGER`. Set to empty string to disable paging. |
@@ -85,6 +85,61 @@ Auto-detection means most commands "just work" without `--repo`. Outside a git t
85
85
 
86
86
  ## Common workflows
87
87
 
88
+ **Clone from a nearby Git mirror**
89
+
90
+ ```sh
91
+ avr repo clone foo/faa
92
+ avr repo clone foo/faa my-checkout --depth 1 --branch main
93
+ avr repo clone foo/faa --placement https://git.fi.a.avrea.io
94
+ ```
95
+
96
+ Avrea discovers the repository's placements and picks the first mirror that
97
+ responds successfully from your machine. `--placement` accepts an advertised
98
+ mirror URL or hostname to select one explicitly. Give the `owner/repository` name
99
+ or an Avrea repository ID (`rep-...`).
100
+ The repository must already have Git mirroring enabled and a usable placement.
101
+
102
+ `origin` stays on the selected read-only mirror. The clone installs a
103
+ repository-local credential helper so ordinary `git fetch` and `git pull`
104
+ obtain fresh Git tokens through your Avrea login. Push changes to the source
105
+ repository using a separate remote, for example:
106
+
107
+ ```sh
108
+ git remote add upstream git@github.com:foo/faa.git
109
+ git push upstream HEAD
110
+ ```
111
+
112
+ Tokens normally last one hour; scoped API keys are limited to ten minutes or
113
+ their remaining lifetime. Git tokens are never saved in the remote URL or
114
+ repository configuration. Keep `avr` installed and logged in for later Git
115
+ operations; the helper continues to use the API host used for the clone.
116
+
117
+ **Create a mirror at a location**
118
+
119
+ ```sh
120
+ avr repo mirror locations
121
+ avr repo mirror create foo/faa --location gsc-fi
122
+ avr repo mirror create group/subgroup/project --source gitlab --location gsc-fi
123
+ avr repo mirror status --repo group/subgroup/project --source gitlab
124
+ ```
125
+
126
+ `--source` defaults to `github`; `gitlab` supports nested group names. The
127
+ repository must already be connected to your Avrea organization. Use `--org`
128
+ to select an organization. Mirror management requires the organization's
129
+ Git-mirror feature and an admin role.
130
+
131
+ `--location` accepts a cluster ID, datacenter ID, or unique name from `locations`.
132
+ The command enables mirroring and adds the placement, preserving existing
133
+ placements. Sync runs asynchronously; use `status` to check it. Repeating
134
+ `create` is safe, including when enabling succeeded but placement failed.
135
+ `locations` and `create` also support `--json` and `--jq`.
136
+
137
+ GitLab repositories cannot be mirrored until GitLab mirroring is enabled
138
+ for your organization. `--source gitlab` is available for deployments that
139
+ support it, including repositories in nested groups. If a GitLab repository
140
+ returns “not found,” check its connection to Avrea, your access, and whether
141
+ GitLab mirroring is enabled.
142
+
88
143
  **Watch a run live**
89
144
 
90
145
  ```sh
@@ -207,11 +262,32 @@ case $? in
207
262
  esac
208
263
  ```
209
264
 
210
- Exit code `4` is reserved for "auth required". `1` is general failure; `2` is a usage error.
265
+ Exit code `4` is reserved for "auth required". `1` is general failure; `2` is a usage error. `130` means Ctrl-C
266
+ ended the command, including a watch, a log follow or a tunnel that is stopped that way.
267
+
268
+ **Scoped tokens for scripts and agents**
269
+
270
+ ```sh
271
+ avr token create --name ci-read --org <org-id> --repo acme/api --ttl 8h # prints the credential once
272
+ avr token create --name agent --org <org-id> --vm <vm-id> --allow-vm-create --vm-create-limit 3
273
+ avr token list
274
+ avr token view <token-id>
275
+ avr token revoke <token-id>
276
+ ```
277
+
278
+ A scoped token is a short-lived credential (60 seconds to 7 days, 8 hours by default) bound to one organization. Repository grants are read-only and VM grants are admin: it reads only the repositories it names, has admin access to only the VMs it names, and cannot create further tokens. `create` prints the `export AVR_TOKEN=...` and `export AVR_ORG=...` lines for whatever will use the token. Scoped tokens require the `org-...` ID in `AVR_ORG` or `--org`; they ignore the login's stored organization and cannot resolve a slug. `avr auth status` reports this configuration without checking whether the token is still live. A 403 or 404 comes with a reminder that the token reaches only what it names.
279
+
280
+ `avr repo clone acme/api` uses the `owner/repository` name directly.
281
+
282
+ With a scoped token, a repository name (`--repo acme/api`, `AVR_REPO` or the checkout's remote) is matched against the repositories the token reaches; a `rep-...` ID works as well. For runs, use the run ID rather than an Avrea console URL, whose organization slug cannot be resolved with a scoped token. Token IDs for `view` and `revoke` are `key-` followed by 32 lowercase hexadecimal characters.
283
+
284
+ Creation returns the credential only once. If `--jq` fails or removes it, the command falls back to the requested JSON without filtering and exits nonzero. Save the credential from that JSON. Output failures also report a revoke command on stderr without exposing the credential there. If the creation request is interrupted, its response is lost, or the answer is a server error (5xx), a token may already exist: check `avr token list` before retrying. Only a 503 that carries the API's own reason means that no token was created.
285
+
286
+ If `avr token create` returns a 404, check the API's reason, the organization and the API host. If scoped-token access is unavailable, contact support@avrea.com to request it. VM grants and `--allow-vm-create` also require customer VMs to be enabled for the organization. `list`, `view` and `revoke` remain available when token creation is disabled.
211
287
 
212
288
  **Pipe-aware output**
213
289
 
214
- When stdout isn't a TTY, list commands switch to tab-separated rows (no color, no truncation, ISO timestamps), so `avr run list | awk` works without flags. `avr run watch | jq -c .` automatically switches to NDJSON event mode.
290
+ When stdout isn't a TTY, list commands switch to tab-separated rows (no color, no truncation, ISO timestamps), so `avr run list | awk` works without flags. Control characters, Unicode line separators and bidirectional controls inside cells are escaped (`\t`, `\x1b`, `\u2028`), keeping each item on one row with stable columns. The escaping is not reversible: a literal backslash is left as it is, so a cell that reads `a\tb` may hold either a tab or a backslash and a `t`. Use `--json` when the exact value matters; JSON retains the original values. `avr run watch | jq -c .` automatically switches to NDJSON event mode.
215
291
 
216
292
  ## Configuration
217
293
 
@@ -220,8 +296,8 @@ When stdout isn't a TTY, list commands switch to tab-separated rows (no color, n
220
296
  | Variable | Purpose |
221
297
  | ---------------- | ------------------------------------------------------------------------------------------------ |
222
298
  | `AVR_HOST` | Avrea API URL. Defaults to the active host in `hosts.json`, then `https://api.avrea.com`. |
223
- | `AVR_TOKEN` | API key. Overrides whatever's stored for the active host. |
224
- | `AVR_ORG` | Default organization ID. Overrides the stored default. |
299
+ | `AVR_TOKEN` | API key or scoped token. Overrides whatever's stored for the active host. |
300
+ | `AVR_ORG` | Default organization ID. Overrides the stored default. A scoped token needs it, or `--org`, except for `repo clone`. |
225
301
  | `AVR_REPO` | Default repository (`org/name` or `rep-xxx`). Overrides git auto-detect. |
226
302
  | `AVR_BROWSER` | Browser to launch for `--web` and OAuth login. Falls back to `BROWSER`, then system default. |
227
303
  | `AVR_PAGER` | Pager for long output. Overrides `PAGER`. Set to empty string to disable paging. |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avr-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Avrea command-line client
5
5
  Author-email: Avrea <support@avrea.com>
6
6
  License-Expression: Apache-2.0
@@ -15,6 +15,7 @@ Requires-Python: >=3.14
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: anyio>=4.14.2
18
+ Requires-Dist: certifi>=2026.10.19
18
19
  Requires-Dist: click>=8.3.1
19
20
  Requires-Dist: httpx>=0.28.1
20
21
  Requires-Dist: platformdirs>=4.0
@@ -117,6 +118,61 @@ Auto-detection means most commands "just work" without `--repo`. Outside a git t
117
118
 
118
119
  ## Common workflows
119
120
 
121
+ **Clone from a nearby Git mirror**
122
+
123
+ ```sh
124
+ avr repo clone foo/faa
125
+ avr repo clone foo/faa my-checkout --depth 1 --branch main
126
+ avr repo clone foo/faa --placement https://git.fi.a.avrea.io
127
+ ```
128
+
129
+ Avrea discovers the repository's placements and picks the first mirror that
130
+ responds successfully from your machine. `--placement` accepts an advertised
131
+ mirror URL or hostname to select one explicitly. Give the `owner/repository` name
132
+ or an Avrea repository ID (`rep-...`).
133
+ The repository must already have Git mirroring enabled and a usable placement.
134
+
135
+ `origin` stays on the selected read-only mirror. The clone installs a
136
+ repository-local credential helper so ordinary `git fetch` and `git pull`
137
+ obtain fresh Git tokens through your Avrea login. Push changes to the source
138
+ repository using a separate remote, for example:
139
+
140
+ ```sh
141
+ git remote add upstream git@github.com:foo/faa.git
142
+ git push upstream HEAD
143
+ ```
144
+
145
+ Tokens normally last one hour; scoped API keys are limited to ten minutes or
146
+ their remaining lifetime. Git tokens are never saved in the remote URL or
147
+ repository configuration. Keep `avr` installed and logged in for later Git
148
+ operations; the helper continues to use the API host used for the clone.
149
+
150
+ **Create a mirror at a location**
151
+
152
+ ```sh
153
+ avr repo mirror locations
154
+ avr repo mirror create foo/faa --location gsc-fi
155
+ avr repo mirror create group/subgroup/project --source gitlab --location gsc-fi
156
+ avr repo mirror status --repo group/subgroup/project --source gitlab
157
+ ```
158
+
159
+ `--source` defaults to `github`; `gitlab` supports nested group names. The
160
+ repository must already be connected to your Avrea organization. Use `--org`
161
+ to select an organization. Mirror management requires the organization's
162
+ Git-mirror feature and an admin role.
163
+
164
+ `--location` accepts a cluster ID, datacenter ID, or unique name from `locations`.
165
+ The command enables mirroring and adds the placement, preserving existing
166
+ placements. Sync runs asynchronously; use `status` to check it. Repeating
167
+ `create` is safe, including when enabling succeeded but placement failed.
168
+ `locations` and `create` also support `--json` and `--jq`.
169
+
170
+ GitLab repositories cannot be mirrored until GitLab mirroring is enabled
171
+ for your organization. `--source gitlab` is available for deployments that
172
+ support it, including repositories in nested groups. If a GitLab repository
173
+ returns “not found,” check its connection to Avrea, your access, and whether
174
+ GitLab mirroring is enabled.
175
+
120
176
  **Watch a run live**
121
177
 
122
178
  ```sh
@@ -239,11 +295,32 @@ case $? in
239
295
  esac
240
296
  ```
241
297
 
242
- Exit code `4` is reserved for "auth required". `1` is general failure; `2` is a usage error.
298
+ Exit code `4` is reserved for "auth required". `1` is general failure; `2` is a usage error. `130` means Ctrl-C
299
+ ended the command, including a watch, a log follow or a tunnel that is stopped that way.
300
+
301
+ **Scoped tokens for scripts and agents**
302
+
303
+ ```sh
304
+ avr token create --name ci-read --org <org-id> --repo acme/api --ttl 8h # prints the credential once
305
+ avr token create --name agent --org <org-id> --vm <vm-id> --allow-vm-create --vm-create-limit 3
306
+ avr token list
307
+ avr token view <token-id>
308
+ avr token revoke <token-id>
309
+ ```
310
+
311
+ A scoped token is a short-lived credential (60 seconds to 7 days, 8 hours by default) bound to one organization. Repository grants are read-only and VM grants are admin: it reads only the repositories it names, has admin access to only the VMs it names, and cannot create further tokens. `create` prints the `export AVR_TOKEN=...` and `export AVR_ORG=...` lines for whatever will use the token. Scoped tokens require the `org-...` ID in `AVR_ORG` or `--org`; they ignore the login's stored organization and cannot resolve a slug. `avr auth status` reports this configuration without checking whether the token is still live. A 403 or 404 comes with a reminder that the token reaches only what it names.
312
+
313
+ `avr repo clone acme/api` uses the `owner/repository` name directly.
314
+
315
+ With a scoped token, a repository name (`--repo acme/api`, `AVR_REPO` or the checkout's remote) is matched against the repositories the token reaches; a `rep-...` ID works as well. For runs, use the run ID rather than an Avrea console URL, whose organization slug cannot be resolved with a scoped token. Token IDs for `view` and `revoke` are `key-` followed by 32 lowercase hexadecimal characters.
316
+
317
+ Creation returns the credential only once. If `--jq` fails or removes it, the command falls back to the requested JSON without filtering and exits nonzero. Save the credential from that JSON. Output failures also report a revoke command on stderr without exposing the credential there. If the creation request is interrupted, its response is lost, or the answer is a server error (5xx), a token may already exist: check `avr token list` before retrying. Only a 503 that carries the API's own reason means that no token was created.
318
+
319
+ If `avr token create` returns a 404, check the API's reason, the organization and the API host. If scoped-token access is unavailable, contact support@avrea.com to request it. VM grants and `--allow-vm-create` also require customer VMs to be enabled for the organization. `list`, `view` and `revoke` remain available when token creation is disabled.
243
320
 
244
321
  **Pipe-aware output**
245
322
 
246
- When stdout isn't a TTY, list commands switch to tab-separated rows (no color, no truncation, ISO timestamps), so `avr run list | awk` works without flags. `avr run watch | jq -c .` automatically switches to NDJSON event mode.
323
+ When stdout isn't a TTY, list commands switch to tab-separated rows (no color, no truncation, ISO timestamps), so `avr run list | awk` works without flags. Control characters, Unicode line separators and bidirectional controls inside cells are escaped (`\t`, `\x1b`, `\u2028`), keeping each item on one row with stable columns. The escaping is not reversible: a literal backslash is left as it is, so a cell that reads `a\tb` may hold either a tab or a backslash and a `t`. Use `--json` when the exact value matters; JSON retains the original values. `avr run watch | jq -c .` automatically switches to NDJSON event mode.
247
324
 
248
325
  ## Configuration
249
326
 
@@ -252,8 +329,8 @@ When stdout isn't a TTY, list commands switch to tab-separated rows (no color, n
252
329
  | Variable | Purpose |
253
330
  | ---------------- | ------------------------------------------------------------------------------------------------ |
254
331
  | `AVR_HOST` | Avrea API URL. Defaults to the active host in `hosts.json`, then `https://api.avrea.com`. |
255
- | `AVR_TOKEN` | API key. Overrides whatever's stored for the active host. |
256
- | `AVR_ORG` | Default organization ID. Overrides the stored default. |
332
+ | `AVR_TOKEN` | API key or scoped token. Overrides whatever's stored for the active host. |
333
+ | `AVR_ORG` | Default organization ID. Overrides the stored default. A scoped token needs it, or `--org`, except for `repo clone`. |
257
334
  | `AVR_REPO` | Default repository (`org/name` or `rep-xxx`). Overrides git auto-detect. |
258
335
  | `AVR_BROWSER` | Browser to launch for `--web` and OAuth login. Falls back to `BROWSER`, then system default. |
259
336
  | `AVR_PAGER` | Pager for long output. Overrides `PAGER`. Set to empty string to disable paging. |
@@ -18,6 +18,7 @@ avrea_cli/config.py
18
18
  avrea_cli/display.py
19
19
  avrea_cli/docs_gen.py
20
20
  avrea_cli/firewall.py
21
+ avrea_cli/git_mirror.py
21
22
  avrea_cli/helpers.py
22
23
  avrea_cli/json_output.py
23
24
  avrea_cli/log_display.py
@@ -29,11 +30,13 @@ avrea_cli/repo_context.py
29
30
  avrea_cli/run_diagnostics.py
30
31
  avrea_cli/run_diagnostics_display.py
31
32
  avrea_cli/run_refs.py
33
+ avrea_cli/spawn_script.py
32
34
  avrea_cli/transport.py
33
35
  avrea_cli/version.py
34
36
  avrea_cli/vm.py
35
37
  avrea_cli/assets/logo_mark.svg
36
38
  avrea_cli/commands/__init__.py
39
+ avrea_cli/commands/artifact.py
37
40
  avrea_cli/commands/auth_cmd.py
38
41
  avrea_cli/commands/cache.py
39
42
  avrea_cli/commands/config_cmd.py
@@ -48,8 +51,10 @@ avrea_cli/commands/run.py
48
51
  avrea_cli/commands/sbom.py
49
52
  avrea_cli/commands/settings.py
50
53
  avrea_cli/commands/status.py
54
+ avrea_cli/commands/token.py
51
55
  avrea_cli/commands/workflow.py
52
56
  tests/test_api_client.py
57
+ tests/test_artifact.py
53
58
  tests/test_auth.py
54
59
  tests/test_auth_login.py
55
60
  tests/test_auth_sso.py
@@ -75,6 +80,7 @@ tests/test_output.py
75
80
  tests/test_piped_output.py
76
81
  tests/test_pr_list.py
77
82
  tests/test_public_mirrors.py
83
+ tests/test_repo_clone.py
78
84
  tests/test_repo_context.py
79
85
  tests/test_repo_mirror.py
80
86
  tests/test_run_attempts.py
@@ -85,9 +91,13 @@ tests/test_run_logs.py
85
91
  tests/test_run_refs.py
86
92
  tests/test_run_view.py
87
93
  tests/test_sbom.py
94
+ tests/test_scoped_token.py
88
95
  tests/test_settings.py
96
+ tests/test_token.py
97
+ tests/test_token_output.py
89
98
  tests/test_transport.py
90
99
  tests/test_vm.py
100
+ tests/test_vm_spawn_script.py
91
101
  tests/test_web_flag.py
92
102
  tests/test_workflow_actions.py
93
103
  tests/test_workflow_run.py
@@ -1,4 +1,5 @@
1
1
  anyio>=4.14.2
2
+ certifi>=2026.10.19
2
3
  click>=8.3.1
3
4
  httpx>=0.28.1
4
5
  platformdirs>=4.0
@@ -1,2 +1,2 @@
1
1
  # Auto-generated by .github/workflows/release.yml; do not edit.
2
- GIT_COMMIT = "5d74471e85fe9ce7a40209ef3daebaad435c38d8"
2
+ GIT_COMMIT = "620a47d50c11e7e1905980829c01fe2d974fe837"
@@ -2,6 +2,7 @@
2
2
 
3
3
  from avrea_cli.config import CliConfig
4
4
  from avrea_cli.transport import CompressingTransport
5
+ from collections.abc import Iterator
5
6
  from typing import Any
6
7
  import click
7
8
  import httpx
@@ -76,6 +77,32 @@ class ApiClient:
76
77
  response.raise_for_status()
77
78
  return response.content
78
79
 
80
+ def public_download(self, path: str) -> Iterator[bytes]:
81
+ """Stream an authenticated response without following credential redirects."""
82
+ url = f"{self.config.public_api_url}{path}"
83
+ with self._http.stream(
84
+ "GET",
85
+ url,
86
+ headers={**self.config.get_api_headers(), "Accept-Encoding": "identity"},
87
+ timeout=self.timeout,
88
+ follow_redirects=False,
89
+ ) as response:
90
+ self._log("GET", str(response.request.url), response.status_code)
91
+ if not response.is_success:
92
+ # Error formatting needs a buffered body, but a failed download
93
+ # must not buffer an arbitrarily large response.
94
+ body = bytearray()
95
+ for chunk in response.iter_bytes(chunk_size=4096):
96
+ body.extend(chunk)
97
+ if len(body) > 64 * 1024:
98
+ body.clear()
99
+ break
100
+ headers = {"Retry-After": response.headers["Retry-After"]} if "Retry-After" in response.headers else {}
101
+ httpx.Response(
102
+ response.status_code, headers=headers, content=bytes(body), request=response.request
103
+ ).raise_for_status()
104
+ yield from response.iter_bytes(chunk_size=1024 * 1024)
105
+
79
106
  def public_post(
80
107
  self,
81
108
  path: str,
@@ -42,6 +42,11 @@ _LOGO_SVG = (Path(__file__).parent / "assets" / "logo_mark.svg").read_text(encod
42
42
  HOSTS_FILE = PATHS.hosts_file
43
43
 
44
44
 
45
+ def is_scoped_token(token: str | None) -> bool:
46
+ """Identify organization-bound credentials without making a request."""
47
+ return token is not None and token.startswith("avs_")
48
+
49
+
45
50
  def _read() -> dict[str, Any]:
46
51
  """Load the hosts file. Returns an empty wrapper if missing or malformed
47
52
  — callers treat absence as 'no credentials yet'."""
@@ -6,6 +6,9 @@ import importlib
6
6
 
7
7
  SECTION_ORDER = ["Core Commands", "Setup & Config", "Additional Commands"]
8
8
 
9
+ # 128 + SIGINT, the shell convention for a command ended by Ctrl-C.
10
+ EXIT_INTERRUPTED = 130
11
+
9
12
  LEARN_MORE = 'Use "avr <command> <subcommand> --help" for more information about a command.\nRead the docs at https://docs.avrea.com/cli'
10
13
 
11
14
 
@@ -188,6 +191,24 @@ class AliasGroup(GhHelpMixin, LazyGroup):
188
191
  finally:
189
192
  self._current_section = prev
190
193
 
194
+ def invoke(self, ctx: click.Context):
195
+ """Exit 130 when Ctrl-C ends the command.
196
+
197
+ Click reports an interrupt as exit 1, the same as a general failure.
198
+ A prompt turns the interrupt into ``click.Abort`` before it gets here,
199
+ so that case is recognised by the exception it replaced. An ``Abort``
200
+ raised for any other reason, end of input included, keeps exit 1.
201
+ """
202
+ try:
203
+ return super().invoke(ctx)
204
+ except KeyboardInterrupt:
205
+ click.echo(err=True)
206
+ except click.Abort as exc:
207
+ if not isinstance(exc.__context__, KeyboardInterrupt):
208
+ raise
209
+ click.echo("Aborted!", err=True)
210
+ raise click.exceptions.Exit(EXIT_INTERRUPTED)
211
+
191
212
  def add_command(self, cmd, name=None):
192
213
  super().add_command(cmd, name)
193
214
  if self._current_section is not None: