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.
Files changed (77) hide show
  1. {devin_cli-1.5.0 → devin_cli-1.5.1}/CHANGELOG.md +11 -0
  2. {devin_cli-1.5.0 → devin_cli-1.5.1}/PKG-INFO +1 -1
  3. {devin_cli-1.5.0 → devin_cli-1.5.1}/pyproject.toml +1 -1
  4. devin_cli-1.5.1/src/devin_cli/__init__.py +1 -0
  5. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/cli.py +64 -40
  6. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/config.py +17 -7
  7. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_cli.py +2 -1
  8. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_cli_global_flags.py +91 -0
  9. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_configure.py +60 -0
  10. devin_cli-1.5.0/src/devin_cli/__init__.py +0 -1
  11. {devin_cli-1.5.0 → devin_cli-1.5.1}/.github/workflows/pypi-publish.yml +0 -0
  12. {devin_cli-1.5.0 → devin_cli-1.5.1}/.gitignore +0 -0
  13. {devin_cli-1.5.0 → devin_cli-1.5.1}/LICENSE +0 -0
  14. {devin_cli-1.5.0 → devin_cli-1.5.1}/README.md +0 -0
  15. {devin_cli-1.5.0 → devin_cli-1.5.1}/assets/logo.png +0 -0
  16. {devin_cli-1.5.0 → devin_cli-1.5.1}/generate_facades.py +0 -0
  17. {devin_cli-1.5.0 → devin_cli-1.5.1}/setup.py +0 -0
  18. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/__init__.py +0 -0
  19. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/attachments.py +0 -0
  20. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/audit_logs.py +0 -0
  21. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/blueprints.py +0 -0
  22. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/client.py +0 -0
  23. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/code_scans.py +0 -0
  24. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/consumption.py +0 -0
  25. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/guardrail_violations.py +0 -0
  26. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/ip_access_list.py +0 -0
  27. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/knowledge.py +0 -0
  28. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/members.py +0 -0
  29. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/organizations.py +0 -0
  30. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/playbooks.py +0 -0
  31. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/pr_reviews.py +0 -0
  32. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/queue.py +0 -0
  33. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/repositories.py +0 -0
  34. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/schedules.py +0 -0
  35. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/secrets.py +0 -0
  36. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/service_users.py +0 -0
  37. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/sessions.py +0 -0
  38. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/tags.py +0 -0
  39. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/__init__.py +0 -0
  40. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/attachments.py +0 -0
  41. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/knowledge.py +0 -0
  42. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/playbooks.py +0 -0
  43. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/secrets.py +0 -0
  44. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/sessions.py +0 -0
  45. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/__init__.py +0 -0
  46. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/attachments.py +0 -0
  47. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/audit_logs.py +0 -0
  48. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/blueprints.py +0 -0
  49. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/code_scans.py +0 -0
  50. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/consumption.py +0 -0
  51. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/guardrail_violations.py +0 -0
  52. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/ip_access_list.py +0 -0
  53. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/knowledge.py +0 -0
  54. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/members.py +0 -0
  55. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/organizations.py +0 -0
  56. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/playbooks.py +0 -0
  57. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/pr_reviews.py +0 -0
  58. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/queue.py +0 -0
  59. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/repositories.py +0 -0
  60. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/schedules.py +0 -0
  61. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/secrets.py +0 -0
  62. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/service_users.py +0 -0
  63. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/sessions.py +0 -0
  64. {devin_cli-1.5.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/tags.py +0 -0
  65. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/__init__.py +0 -0
  66. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/conftest.py +0 -0
  67. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_attachments.py +0 -0
  68. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_enterprise_endpoints.py +0 -0
  69. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_knowledge.py +0 -0
  70. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_playbooks.py +0 -0
  71. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_pr_reviews.py +0 -0
  72. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_repositories.py +0 -0
  73. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_schedules.py +0 -0
  74. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_secrets.py +0 -0
  75. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_api/test_sessions.py +0 -0
  76. {devin_cli-1.5.0 → devin_cli-1.5.1}/tests/test_cli_extended.py +0 -0
  77. {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.0
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "devin-cli"
3
- version = "1.5.0"
3
+ version = "1.5.1"
4
4
  description = "Unofficial CLI for Devin AI - The first AI Software Engineer"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
- if flag in sys.argv:
50
- try:
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
- except ValueError:
55
- pass
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 config_file:
156
- config.set_config_file(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 token:
160
- config.runtime.api_token = token
161
- if org:
162
- config.runtime.org_id = org
163
- if base_url:
164
- config.runtime.base_url = base_url
165
- if api_version:
166
- config.runtime.api_version = api_version
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
- is_interactive = not yes and sys.stdin.isatty()
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 = token or config.api_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
- final_token = token
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
- final_base_url = base_url or Prompt.ask("Devin API Base URL", default=config.base_url or "https://api.devin.ai/v3")
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
- if org_id is not None:
264
- final_org = org_id
265
- else:
266
- org_prompt = Prompt.ask("Organization ID (optional)", default=config.org_id or "")
267
- final_org = org_prompt if org_prompt else None
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
- if api_version is not None:
270
- final_api_version = api_version.lower()
271
- else:
272
- final_api_version = Prompt.ask(f"API Version for profile '{config.active_profile}' (v3/v1)", default=config.api_version or "v3").lower()
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: config.temporary_org_id = 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 = RuntimeCredentials()
52
- _runtime_cv.set(rt)
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
- _runtime_cv.set(RuntimeCredentials())
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 = self.runtime.api_token or os.environ.get("DEVIN_API_TOKEN") or self._get_profile_data().get("api_token")
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 "1.5.0" in result.stdout
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