devin-cli 1.5.0__tar.gz → 1.5.1__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.
- {devin_cli-1.5.0 → devin_cli-1.5.1}/CHANGELOG.md +11 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/PKG-INFO +1 -1
- {devin_cli-1.5.0 → devin_cli-1.5.1}/pyproject.toml +1 -1
- devin_cli-1.5.1/src/devin_cli/__init__.py +1 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/cli.py +64 -40
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/config.py +17 -7
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_cli.py +2 -1
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_cli_global_flags.py +91 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_configure.py +60 -0
- devin_cli-1.5.0/src/devin_cli/__init__.py +0 -1
- {devin_cli-1.5.0 → devin_cli-1.5.1}/.github/workflows/pypi-publish.yml +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/.gitignore +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/LICENSE +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/README.md +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/assets/logo.png +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/generate_facades.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/setup.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/__init__.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/attachments.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/audit_logs.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/blueprints.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/client.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/code_scans.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/consumption.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/guardrail_violations.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/ip_access_list.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/knowledge.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/members.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/organizations.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/playbooks.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/pr_reviews.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/queue.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/repositories.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/schedules.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/secrets.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/service_users.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/sessions.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/tags.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/__init__.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/attachments.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/knowledge.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/playbooks.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/secrets.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/sessions.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/__init__.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/attachments.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/audit_logs.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/blueprints.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/code_scans.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/consumption.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/guardrail_violations.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/ip_access_list.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/knowledge.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/members.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/organizations.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/playbooks.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/pr_reviews.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/queue.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/repositories.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/schedules.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/secrets.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/service_users.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/sessions.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/tags.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/__init__.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/conftest.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_attachments.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_enterprise_endpoints.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_knowledge.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_playbooks.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_pr_reviews.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_repositories.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_schedules.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_secrets.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_sessions.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_cli_extended.py +0 -0
- {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_config.py +0 -0
|
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.1] - 2026-09-04
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Thread Context Propagation for Concurrent Commands (`repos status`, `sessions cost`)**:
|
|
11
|
+
- Resolved an issue where commands utilizing `concurrent.futures.ThreadPoolExecutor` lost runtime credentials (`--token`, `--org`) in worker threads, causing `repos status` to emit fake 404 `"Not registered in Devin (404)"` errors when relying solely on global flags without environment variables.
|
|
12
|
+
- Ensured `contextvars.copy_context().run` is executed per worker task and backed by instance-level fallback in `Config` to guarantee thread-safe credential access across any thread pool.
|
|
13
|
+
- **Root Callback Runtime Credential Re-application**:
|
|
14
|
+
- Re-applied parsed flags and supported equals syntax (`--token=...`, `--org=...`) and flag aliases (`-t`, `-o`, `-b`, `-c`) so commands execute with correct credentials regardless of CLI argument order.
|
|
15
|
+
- **Non-Interactive `configure` Wizard Bypass**:
|
|
16
|
+
- Completely eliminated interactive prompts in `devin configure` when `--token` is provided or in CI environments (`CI`, `GITHUB_ACTIONS`, `CONTINUOUS_INTEGRATION`, `TF_BUILD`), falling back cleanly to defaults (`base_url: https://api.devin.ai/v3`, `api_version: v3`) without requiring `--yes`.
|
|
17
|
+
- Added graceful handling of `EOFError` / `KeyboardInterrupt` to prevent unhandled tracebacks.
|
|
18
|
+
|
|
8
19
|
## [1.5.0] - 2026-09-04
|
|
9
20
|
### Added
|
|
10
21
|
- **On-the-Fly Configuration & Global CLI Flags**: Root command now supports `--token`, `--org`, `--base-url`, `--api-version`, and `--config-file` flags, allowing complete credential configuration on the fly without writing to disk or modifying saved configs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devin-cli
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: Unofficial CLI for Devin AI - The first AI Software Engineer
|
|
5
5
|
Project-URL: Homepage, https://github.com/revanthpobala/devin-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/revanthpobala/devin-cli.git
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.5.1"
|
|
@@ -46,16 +46,15 @@ except Exception:
|
|
|
46
46
|
# Early CLI flag detection for accurate menus, config loading, and mode setup
|
|
47
47
|
def _extract_cli_arg(flags: list) -> Optional[str]:
|
|
48
48
|
for flag in flags:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
idx = sys.argv.index(flag)
|
|
49
|
+
for idx, arg in enumerate(sys.argv):
|
|
50
|
+
if arg == flag:
|
|
52
51
|
if idx + 1 < len(sys.argv) and not sys.argv[idx + 1].startswith("-"):
|
|
53
52
|
return sys.argv[idx + 1]
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
elif arg.startswith(f"{flag}="):
|
|
54
|
+
return arg.split("=", 1)[1]
|
|
56
55
|
return None
|
|
57
56
|
|
|
58
|
-
early_config_file = _extract_cli_arg(["--config-file"])
|
|
57
|
+
early_config_file = _extract_cli_arg(["--config-file", "-c"])
|
|
59
58
|
if early_config_file:
|
|
60
59
|
config.set_config_file(early_config_file)
|
|
61
60
|
elif os.environ.get("DEVIN_CONFIG_FILE"):
|
|
@@ -65,15 +64,16 @@ early_profile = _extract_cli_arg(["--profile", "-p"])
|
|
|
65
64
|
if early_profile:
|
|
66
65
|
config.active_profile = early_profile
|
|
67
66
|
|
|
68
|
-
early_token = _extract_cli_arg(["--token"])
|
|
67
|
+
early_token = _extract_cli_arg(["--token", "-t"])
|
|
69
68
|
if early_token:
|
|
70
69
|
config.runtime.api_token = early_token
|
|
71
70
|
|
|
72
|
-
early_org = _extract_cli_arg(["--org"])
|
|
71
|
+
early_org = _extract_cli_arg(["--org", "-o"])
|
|
73
72
|
if early_org:
|
|
74
73
|
config.runtime.org_id = early_org
|
|
74
|
+
config.temporary_org_id = early_org
|
|
75
75
|
|
|
76
|
-
early_base_url = _extract_cli_arg(["--base-url"])
|
|
76
|
+
early_base_url = _extract_cli_arg(["--base-url", "-b"])
|
|
77
77
|
if early_base_url:
|
|
78
78
|
config.runtime.base_url = early_base_url
|
|
79
79
|
|
|
@@ -145,6 +145,12 @@ def main(
|
|
|
145
145
|
"""
|
|
146
146
|
config.reset_runtime()
|
|
147
147
|
|
|
148
|
+
eff_token = token or _extract_cli_arg(["--token", "-t"])
|
|
149
|
+
eff_org = org or _extract_cli_arg(["--org", "-o"])
|
|
150
|
+
eff_base_url = base_url or _extract_cli_arg(["--base-url", "-b"])
|
|
151
|
+
eff_api_version = api_version or _extract_cli_arg(["--api-version"])
|
|
152
|
+
eff_config_file = config_file or _extract_cli_arg(["--config-file", "-c"])
|
|
153
|
+
|
|
148
154
|
if json_format:
|
|
149
155
|
os.environ["DEVIN_OUTPUT_FORMAT"] = "json"
|
|
150
156
|
|
|
@@ -152,18 +158,19 @@ def main(
|
|
|
152
158
|
console.print(f"devin CLI version: {__version__}")
|
|
153
159
|
raise typer.Exit()
|
|
154
160
|
|
|
155
|
-
if
|
|
156
|
-
config.set_config_file(
|
|
161
|
+
if eff_config_file:
|
|
162
|
+
config.set_config_file(eff_config_file)
|
|
157
163
|
if profile:
|
|
158
164
|
config.active_profile = profile
|
|
159
|
-
if
|
|
160
|
-
config.runtime.api_token =
|
|
161
|
-
if
|
|
162
|
-
config.runtime.org_id =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
if eff_token:
|
|
166
|
+
config.runtime.api_token = eff_token
|
|
167
|
+
if eff_org:
|
|
168
|
+
config.runtime.org_id = eff_org
|
|
169
|
+
config.temporary_org_id = eff_org
|
|
170
|
+
if eff_base_url:
|
|
171
|
+
config.runtime.base_url = eff_base_url
|
|
172
|
+
if eff_api_version:
|
|
173
|
+
config.runtime.api_version = eff_api_version
|
|
167
174
|
|
|
168
175
|
if ctx.invoked_subcommand is None:
|
|
169
176
|
console.print(ASCII_LOGO)
|
|
@@ -240,36 +247,49 @@ def configure(
|
|
|
240
247
|
if profile:
|
|
241
248
|
config.active_profile = profile
|
|
242
249
|
|
|
243
|
-
|
|
250
|
+
is_ci = bool(
|
|
251
|
+
os.environ.get("CI")
|
|
252
|
+
or os.environ.get("GITHUB_ACTIONS")
|
|
253
|
+
or os.environ.get("CONTINUOUS_INTEGRATION")
|
|
254
|
+
or os.environ.get("TF_BUILD")
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Check if token was provided via argument, early CLI arg, or runtime
|
|
258
|
+
token_val = token or _extract_cli_arg(["--token", "-t"]) or config.runtime.api_token
|
|
259
|
+
|
|
260
|
+
# Prompt interactively ONLY when no token was provided, --yes is not passed, not in CI, and stdin is a TTY
|
|
261
|
+
is_interactive = not yes and not is_ci and sys.stdin.isatty() and token_val is None
|
|
244
262
|
|
|
245
263
|
if not is_interactive:
|
|
246
|
-
final_token =
|
|
264
|
+
final_token = token_val or config.api_token
|
|
247
265
|
if not final_token:
|
|
248
266
|
console.print("[bold red]Error:[/bold red] API token is required. Pass --token or set DEVIN_API_TOKEN.")
|
|
249
267
|
raise typer.Exit(1)
|
|
250
|
-
final_base_url = base_url or config.base_url or "https://api.devin.ai/v3"
|
|
251
|
-
final_api_version = (api_version or config.api_version or "v3").lower()
|
|
252
|
-
final_org = org_id if org_id is not None else config.org_id
|
|
268
|
+
final_base_url = base_url or _extract_cli_arg(["--base-url", "-b"]) or config.base_url or "https://api.devin.ai/v3"
|
|
269
|
+
final_api_version = (api_version or _extract_cli_arg(["--api-version"]) or config.api_version or "v3").lower()
|
|
270
|
+
final_org = org_id if org_id is not None else (_extract_cli_arg(["--org"]) or config.org_id)
|
|
253
271
|
else:
|
|
254
|
-
|
|
255
|
-
if not final_token:
|
|
272
|
+
try:
|
|
256
273
|
final_token = Prompt.ask("Devin API Token (starts with apk_ or cog_)")
|
|
257
274
|
if not final_token:
|
|
258
275
|
console.print("[bold red]Error:[/bold red] Token cannot be empty.")
|
|
259
276
|
raise typer.Exit(1)
|
|
260
277
|
|
|
261
|
-
|
|
278
|
+
final_base_url = base_url or Prompt.ask("Devin API Base URL", default=config.base_url or "https://api.devin.ai/v3")
|
|
262
279
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
280
|
+
if org_id is not None:
|
|
281
|
+
final_org = org_id
|
|
282
|
+
else:
|
|
283
|
+
org_prompt = Prompt.ask("Organization ID (optional)", default=config.org_id or "")
|
|
284
|
+
final_org = org_prompt if org_prompt else None
|
|
268
285
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
286
|
+
if api_version is not None:
|
|
287
|
+
final_api_version = api_version.lower()
|
|
288
|
+
else:
|
|
289
|
+
final_api_version = Prompt.ask(f"API Version for profile '{config.active_profile}' (v3/v1)", default=config.api_version or "v3").lower()
|
|
290
|
+
except (EOFError, KeyboardInterrupt):
|
|
291
|
+
console.print("[bold red]Aborted.[/bold red]")
|
|
292
|
+
raise typer.Exit(1)
|
|
273
293
|
|
|
274
294
|
if not (final_token.startswith("apk_") or final_token.startswith("cog_")):
|
|
275
295
|
console.print("[bold yellow]Warning:[/bold yellow] Token format might be outdated. v3 tokens usually start with 'apk_' or 'cog_'.")
|
|
@@ -612,9 +632,10 @@ def session_cost_cmd(
|
|
|
612
632
|
except Exception:
|
|
613
633
|
return None
|
|
614
634
|
|
|
635
|
+
import contextvars
|
|
615
636
|
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
|
|
616
|
-
f_session = executor.submit(fetch_session)
|
|
617
|
-
f_cons = executor.submit(fetch_consumption)
|
|
637
|
+
f_session = executor.submit(contextvars.copy_context().run, fetch_session)
|
|
638
|
+
f_cons = executor.submit(contextvars.copy_context().run, fetch_consumption)
|
|
618
639
|
|
|
619
640
|
resp = f_session.result()
|
|
620
641
|
cons_resp = f_cons.result()
|
|
@@ -1241,12 +1262,15 @@ def status_repo_cmd(
|
|
|
1241
1262
|
json_output: bool = typer.Option(False, "--json"),
|
|
1242
1263
|
):
|
|
1243
1264
|
"""Get indexing status for one or more repositories."""
|
|
1244
|
-
if org:
|
|
1265
|
+
if org:
|
|
1266
|
+
config.temporary_org_id = org
|
|
1267
|
+
config.runtime.org_id = org
|
|
1245
1268
|
|
|
1246
1269
|
results = []
|
|
1247
1270
|
exit_code = 0
|
|
1248
1271
|
|
|
1249
1272
|
from concurrent.futures import ThreadPoolExecutor
|
|
1273
|
+
import contextvars
|
|
1250
1274
|
|
|
1251
1275
|
def fetch_status(path):
|
|
1252
1276
|
try:
|
|
@@ -1269,7 +1293,7 @@ def status_repo_cmd(
|
|
|
1269
1293
|
|
|
1270
1294
|
max_workers = min(10, len(paths))
|
|
1271
1295
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
1272
|
-
futures = {executor.submit(fetch_status, path): path for path in paths}
|
|
1296
|
+
futures = {executor.submit(contextvars.copy_context().run, fetch_status, path): path for path in paths}
|
|
1273
1297
|
for future in futures:
|
|
1274
1298
|
path = futures[future]
|
|
1275
1299
|
try:
|
|
@@ -39,6 +39,7 @@ class Config:
|
|
|
39
39
|
)
|
|
40
40
|
self._custom_config_file: Optional[Path] = self._initial_config_file
|
|
41
41
|
self._custom_config_dir: Optional[Path] = self._initial_config_dir
|
|
42
|
+
self._runtime: RuntimeCredentials = RuntimeCredentials()
|
|
42
43
|
self.reset_runtime()
|
|
43
44
|
self._data = {}
|
|
44
45
|
self._loaded_file: Optional[Path] = None
|
|
@@ -47,13 +48,13 @@ class Config:
|
|
|
47
48
|
@property
|
|
48
49
|
def runtime(self) -> RuntimeCredentials:
|
|
49
50
|
rt = _runtime_cv.get()
|
|
50
|
-
if rt is None:
|
|
51
|
-
rt
|
|
52
|
-
|
|
53
|
-
return rt
|
|
51
|
+
if rt is not None:
|
|
52
|
+
return rt
|
|
53
|
+
return self._runtime
|
|
54
54
|
|
|
55
55
|
def reset_runtime(self):
|
|
56
|
-
|
|
56
|
+
self._runtime = RuntimeCredentials()
|
|
57
|
+
_runtime_cv.set(self._runtime)
|
|
57
58
|
self._temporary_org_id = None
|
|
58
59
|
self._runtime_profile = None
|
|
59
60
|
self._custom_config_file = getattr(self, "_initial_config_file", None)
|
|
@@ -159,7 +160,12 @@ class Config:
|
|
|
159
160
|
@property
|
|
160
161
|
def api_token(self) -> Optional[str]:
|
|
161
162
|
# Precedence: CLI flag (runtime) -> Env var -> Config file profile
|
|
162
|
-
val =
|
|
163
|
+
val = (
|
|
164
|
+
self.runtime.api_token
|
|
165
|
+
or self._runtime.api_token
|
|
166
|
+
or os.environ.get("DEVIN_API_TOKEN")
|
|
167
|
+
or self._get_profile_data().get("api_token")
|
|
168
|
+
)
|
|
163
169
|
return val.strip() if isinstance(val, str) else val
|
|
164
170
|
|
|
165
171
|
@api_token.setter
|
|
@@ -172,6 +178,7 @@ class Config:
|
|
|
172
178
|
val = (
|
|
173
179
|
self.runtime.org_id
|
|
174
180
|
or getattr(self, "_temporary_org_id", None)
|
|
181
|
+
or self._runtime.org_id
|
|
175
182
|
or os.environ.get("DEVIN_ORG_ID")
|
|
176
183
|
or self._get_profile_data().get("org_id")
|
|
177
184
|
)
|
|
@@ -183,18 +190,20 @@ class Config:
|
|
|
183
190
|
|
|
184
191
|
@property
|
|
185
192
|
def temporary_org_id(self) -> Optional[str]:
|
|
186
|
-
return self.runtime.org_id or getattr(self, "_temporary_org_id", None)
|
|
193
|
+
return self.runtime.org_id or getattr(self, "_temporary_org_id", None) or self._runtime.org_id
|
|
187
194
|
|
|
188
195
|
@temporary_org_id.setter
|
|
189
196
|
def temporary_org_id(self, value: Optional[str]):
|
|
190
197
|
self._temporary_org_id = value
|
|
191
198
|
self.runtime.org_id = value
|
|
199
|
+
self._runtime.org_id = value
|
|
192
200
|
|
|
193
201
|
@property
|
|
194
202
|
def base_url(self) -> str:
|
|
195
203
|
# Precedence: CLI flag (runtime) -> Env var -> Config file profile -> Default
|
|
196
204
|
val = (
|
|
197
205
|
self.runtime.base_url
|
|
206
|
+
or self._runtime.base_url
|
|
198
207
|
or os.environ.get("DEVIN_BASE_URL")
|
|
199
208
|
or self._get_profile_data().get("base_url", "https://api.devin.ai/v3")
|
|
200
209
|
)
|
|
@@ -209,6 +218,7 @@ class Config:
|
|
|
209
218
|
# Precedence: CLI flag (runtime) -> Env var -> Config file profile -> Default
|
|
210
219
|
val = (
|
|
211
220
|
self.runtime.api_version
|
|
221
|
+
or self._runtime.api_version
|
|
212
222
|
or os.environ.get("DEVIN_API_VERSION")
|
|
213
223
|
or self._get_profile_data().get("api_version", "v3")
|
|
214
224
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typer.testing import CliRunner
|
|
2
2
|
from unittest.mock import patch
|
|
3
|
+
from devin_cli import __version__
|
|
3
4
|
from devin_cli.cli import app
|
|
4
5
|
|
|
5
6
|
runner = CliRunner()
|
|
@@ -29,6 +30,6 @@ def test_cli_handles_api_error():
|
|
|
29
30
|
def test_app_version():
|
|
30
31
|
result = runner.invoke(app, ["--version"])
|
|
31
32
|
assert result.exit_code == 0
|
|
32
|
-
assert
|
|
33
|
+
assert __version__ in result.stdout
|
|
33
34
|
|
|
34
35
|
|
|
@@ -110,3 +110,94 @@ def test_config_file_flag_override(mock_list, tmp_path):
|
|
|
110
110
|
assert result.exit_code == 0
|
|
111
111
|
assert config.api_token == "apk_from_custom_file"
|
|
112
112
|
assert config.org_id == "org_from_custom_file"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
import respx
|
|
116
|
+
from httpx import Response
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@respx.mock
|
|
120
|
+
def test_repos_status_global_flags_thread_context(tmp_path, monkeypatch):
|
|
121
|
+
monkeypatch.delenv("DEVIN_CONFIG_FILE", raising=False)
|
|
122
|
+
monkeypatch.delenv("DEVIN_API_TOKEN", raising=False)
|
|
123
|
+
monkeypatch.delenv("DEVIN_ORG_ID", raising=False)
|
|
124
|
+
|
|
125
|
+
route = respx.get(
|
|
126
|
+
"https://api.devin.ai/v3beta1/organizations/org-12cf-test/repositories/cigna-group/csp-inf-utils/indexing"
|
|
127
|
+
).mock(
|
|
128
|
+
return_value=Response(200, json={"repo_path": "cigna-group/csp-inf-utils", "indexing_enabled": True})
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
result = runner.invoke(app, [
|
|
132
|
+
"--token", "cog_xxx_test",
|
|
133
|
+
"--org", "org-12cf-test",
|
|
134
|
+
"--json",
|
|
135
|
+
"repos", "status", "cigna-group/csp-inf-utils"
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
assert result.exit_code == 0
|
|
139
|
+
assert route.called
|
|
140
|
+
assert route.calls.last.request.headers["authorization"] == "Bearer cog_xxx_test"
|
|
141
|
+
parsed = json.loads(result.stdout)
|
|
142
|
+
assert parsed["repo_path"] == "cigna-group/csp-inf-utils"
|
|
143
|
+
assert parsed["indexing_enabled"] is True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@respx.mock
|
|
147
|
+
def test_repos_status_global_flags_equals_syntax(tmp_path, monkeypatch):
|
|
148
|
+
monkeypatch.delenv("DEVIN_CONFIG_FILE", raising=False)
|
|
149
|
+
monkeypatch.delenv("DEVIN_API_TOKEN", raising=False)
|
|
150
|
+
monkeypatch.delenv("DEVIN_ORG_ID", raising=False)
|
|
151
|
+
|
|
152
|
+
route = respx.get(
|
|
153
|
+
"https://api.devin.ai/v3beta1/organizations/org-equals/repositories/test-owner/test-repo/indexing"
|
|
154
|
+
).mock(
|
|
155
|
+
return_value=Response(200, json={"repo_path": "test-owner/test-repo", "indexing_enabled": True})
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
result = runner.invoke(app, [
|
|
159
|
+
"--token=cog_equals_token",
|
|
160
|
+
"--org=org-equals",
|
|
161
|
+
"--json",
|
|
162
|
+
"repos", "status", "test-owner/test-repo"
|
|
163
|
+
])
|
|
164
|
+
|
|
165
|
+
assert result.exit_code == 0
|
|
166
|
+
assert route.called
|
|
167
|
+
assert route.calls.last.request.headers["authorization"] == "Bearer cog_equals_token"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@respx.mock
|
|
171
|
+
def test_repos_status_multiple_repos_threadpool_propagation(tmp_path, monkeypatch):
|
|
172
|
+
monkeypatch.delenv("DEVIN_CONFIG_FILE", raising=False)
|
|
173
|
+
monkeypatch.delenv("DEVIN_API_TOKEN", raising=False)
|
|
174
|
+
monkeypatch.delenv("DEVIN_ORG_ID", raising=False)
|
|
175
|
+
|
|
176
|
+
r1 = respx.get(
|
|
177
|
+
"https://api.devin.ai/v3beta1/organizations/org-multi/repositories/owner/repo1/indexing"
|
|
178
|
+
).mock(
|
|
179
|
+
return_value=Response(200, json={"repo_path": "owner/repo1", "indexing_enabled": True})
|
|
180
|
+
)
|
|
181
|
+
r2 = respx.get(
|
|
182
|
+
"https://api.devin.ai/v3beta1/organizations/org-multi/repositories/owner/repo2/indexing"
|
|
183
|
+
).mock(
|
|
184
|
+
return_value=Response(200, json={"repo_path": "owner/repo2", "indexing_enabled": False})
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
result = runner.invoke(app, [
|
|
188
|
+
"--token", "cog_multi_token",
|
|
189
|
+
"--org", "org-multi",
|
|
190
|
+
"--json",
|
|
191
|
+
"repos", "status", "owner/repo1", "owner/repo2"
|
|
192
|
+
])
|
|
193
|
+
|
|
194
|
+
assert result.exit_code == 0
|
|
195
|
+
assert r1.called
|
|
196
|
+
assert r2.called
|
|
197
|
+
parsed = json.loads(result.stdout)
|
|
198
|
+
items = parsed["results"]
|
|
199
|
+
assert len(items) == 2
|
|
200
|
+
paths = [p["repo_path"] for p in items]
|
|
201
|
+
assert "owner/repo1" in paths
|
|
202
|
+
assert "owner/repo2" in paths
|
|
203
|
+
|
|
@@ -100,3 +100,63 @@ def test_configure_custom_profile(tmp_path, monkeypatch):
|
|
|
100
100
|
profile = data["profiles"]["ci-runner"]
|
|
101
101
|
assert profile["api_token"] == "cog_service_token"
|
|
102
102
|
assert profile["org_id"] == "org-enterprise"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_configure_non_interactive_when_token_provided_without_yes(tmp_path, monkeypatch):
|
|
106
|
+
custom_cfg = tmp_path / "devin_test.json"
|
|
107
|
+
monkeypatch.setenv("DEVIN_CONFIG_FILE", str(custom_cfg))
|
|
108
|
+
monkeypatch.delenv("DEVIN_API_TOKEN", raising=False)
|
|
109
|
+
monkeypatch.delenv("DEVIN_ORG_ID", raising=False)
|
|
110
|
+
|
|
111
|
+
# Calling configure with --token and --org should NOT prompt even without --yes
|
|
112
|
+
result = runner.invoke(app, [
|
|
113
|
+
"configure",
|
|
114
|
+
"--token", "apk_auto_token_999",
|
|
115
|
+
"--org", "org-auto-ci-999",
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
assert result.exit_code == 0
|
|
119
|
+
assert "Configuration saved to" in result.stdout
|
|
120
|
+
data = json.loads(custom_cfg.read_text())
|
|
121
|
+
profile = data["profiles"]["default"]
|
|
122
|
+
assert profile["api_token"] == "apk_auto_token_999"
|
|
123
|
+
assert profile["org_id"] == "org-auto-ci-999"
|
|
124
|
+
assert profile["api_version"] == "v3"
|
|
125
|
+
assert profile["base_url"] == "https://api.devin.ai/v3"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_configure_global_flags_before_subcommand_without_yes(tmp_path, monkeypatch):
|
|
129
|
+
custom_cfg = tmp_path / "devin_test.json"
|
|
130
|
+
monkeypatch.setenv("DEVIN_CONFIG_FILE", str(custom_cfg))
|
|
131
|
+
monkeypatch.delenv("DEVIN_API_TOKEN", raising=False)
|
|
132
|
+
monkeypatch.delenv("DEVIN_ORG_ID", raising=False)
|
|
133
|
+
|
|
134
|
+
# Calling devin --token ... --org ... configure should NOT prompt
|
|
135
|
+
result = runner.invoke(app, [
|
|
136
|
+
"--token", "apk_global_token_777",
|
|
137
|
+
"--org", "org-global-ci-777",
|
|
138
|
+
"configure",
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
assert result.exit_code == 0
|
|
142
|
+
assert "Configuration saved to" in result.stdout
|
|
143
|
+
data = json.loads(custom_cfg.read_text())
|
|
144
|
+
profile = data["profiles"]["default"]
|
|
145
|
+
assert profile["api_token"] == "apk_global_token_777"
|
|
146
|
+
assert profile["org_id"] == "org-global-ci-777"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_configure_ci_environment_uses_env_token(tmp_path, monkeypatch):
|
|
150
|
+
custom_cfg = tmp_path / "devin_test.json"
|
|
151
|
+
monkeypatch.setenv("DEVIN_CONFIG_FILE", str(custom_cfg))
|
|
152
|
+
monkeypatch.setenv("CI", "true")
|
|
153
|
+
monkeypatch.setenv("DEVIN_API_TOKEN", "apk_ci_env_token")
|
|
154
|
+
monkeypatch.setenv("DEVIN_ORG_ID", "org-ci-env")
|
|
155
|
+
|
|
156
|
+
result = runner.invoke(app, ["configure"])
|
|
157
|
+
assert result.exit_code == 0
|
|
158
|
+
data = json.loads(custom_cfg.read_text())
|
|
159
|
+
profile = data["profiles"]["default"]
|
|
160
|
+
assert profile["api_token"] == "apk_ci_env_token"
|
|
161
|
+
assert profile["org_id"] == "org-ci-env"
|
|
162
|
+
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.5.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|