buildai-cli 0.3.62__tar.gz → 0.3.63__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 (35) hide show
  1. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/PKG-INFO +1 -1
  2. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/auth_local.py +21 -6
  3. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/db_broker.py +140 -6
  4. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/pyproject.toml +1 -1
  5. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/.gitignore +0 -0
  6. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/AGENTS.md +0 -0
  7. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/CLAUDE.md +0 -0
  8. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/buildai_bootstrap.py +0 -0
  9. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/__init__.py +0 -0
  10. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/_has_core.py +0 -0
  11. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/__init__.py +0 -0
  12. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/api_proxy.py +0 -0
  13. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/auth.py +0 -0
  14. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/__init__.py +0 -0
  15. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/broker.py +0 -0
  16. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/common.py +0 -0
  17. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/migrate.py +0 -0
  18. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/query.py +0 -0
  19. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/schema.py +0 -0
  20. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/status.py +0 -0
  21. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/dev.py +0 -0
  22. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/doctor.py +0 -0
  23. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/gigcamera.py +0 -0
  24. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/processing.py +0 -0
  25. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/config.py +0 -0
  26. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/console.py +0 -0
  27. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/context.py +0 -0
  28. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/guard.py +0 -0
  29. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/internal_api.py +0 -0
  30. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/main.py +0 -0
  31. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/nl_query/__init__.py +0 -0
  32. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/nl_query/dataset_tools.py +0 -0
  33. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/ops_init.py +0 -0
  34. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/output.py +0 -0
  35. {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/pagination.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildai-cli
3
- Version: 0.3.62
3
+ Version: 0.3.63
4
4
  Summary: Build AI CLI (Typer)
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27.0
@@ -353,12 +353,27 @@ def _fixed_targets_for_environment(
353
353
  def _run_command(argv: list[str], *, timeout: int = 20) -> CommandResult:
354
354
  """Run one external command with bounded time and normalized output fields."""
355
355
 
356
- completed = subprocess.run(
357
- argv,
358
- capture_output=True,
359
- text=True,
360
- timeout=timeout,
361
- )
356
+ try:
357
+ completed = subprocess.run(
358
+ argv,
359
+ capture_output=True,
360
+ text=True,
361
+ timeout=timeout,
362
+ )
363
+ except FileNotFoundError as exc:
364
+ return CommandResult(
365
+ argv=tuple(argv),
366
+ returncode=127,
367
+ stdout="",
368
+ stderr=f"{argv[0]} is not installed or is not on PATH: {exc}",
369
+ )
370
+ except subprocess.TimeoutExpired as exc:
371
+ return CommandResult(
372
+ argv=tuple(argv),
373
+ returncode=124,
374
+ stdout=(exc.stdout or "").strip() if isinstance(exc.stdout, str) else "",
375
+ stderr=(exc.stderr or "").strip() if isinstance(exc.stderr, str) else "Command timed out.",
376
+ )
362
377
  return CommandResult(
363
378
  argv=tuple(argv),
364
379
  returncode=completed.returncode,
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import fcntl
5
+ import ctypes
6
6
  import hashlib
7
7
  import json
8
8
  import os
@@ -20,6 +20,11 @@ from typing import Any, Iterator
20
20
  import asyncpg
21
21
  from infra.settings import Settings
22
22
 
23
+ if os.name == "nt":
24
+ import msvcrt
25
+ else:
26
+ import fcntl
27
+
23
28
  REPO_ROOT = Path(__file__).resolve().parents[3]
24
29
  LEGACY_STATE_DIR = REPO_ROOT / ".buildai" / "db-brokers"
25
30
  STATE_DIR = Path(os.getenv("BUILDAI_DB_BROKER_STATE_DIR", Path.home() / ".buildai" / "db-brokers"))
@@ -192,6 +197,9 @@ def proxy_is_listening(*, host: str, port: int) -> bool:
192
197
  def process_is_running(pid: int) -> bool:
193
198
  """Return whether the broker PID still exists."""
194
199
 
200
+ if os.name == "nt":
201
+ return _windows_process_is_running(pid)
202
+
195
203
  try:
196
204
  os.kill(pid, 0)
197
205
  except ProcessLookupError:
@@ -201,18 +209,59 @@ def process_is_running(pid: int) -> bool:
201
209
  return True
202
210
 
203
211
 
212
+ def _windows_process_is_running(pid: int) -> bool:
213
+ """Check a Windows process handle without sending a signal to the process."""
214
+
215
+ if pid <= 0:
216
+ return False
217
+
218
+ synchronize = 0x00100000
219
+ wait_timeout = 0x00000102
220
+ kernel32 = ctypes.windll.kernel32
221
+ handle = kernel32.OpenProcess(synchronize, False, int(pid))
222
+ if not handle:
223
+ return False
224
+ try:
225
+ return kernel32.WaitForSingleObject(handle, 0) == wait_timeout
226
+ finally:
227
+ kernel32.CloseHandle(handle)
228
+
229
+
230
+ def _lock_file(handle: Any) -> None:
231
+ """Acquire an exclusive machine-local lock for the opened lock file."""
232
+
233
+ if os.name == "nt":
234
+ handle.seek(0)
235
+ handle.write(b"\0")
236
+ handle.flush()
237
+ handle.seek(0)
238
+ msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
239
+ return
240
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
241
+
242
+
243
+ def _unlock_file(handle: Any) -> None:
244
+ """Release a lock previously acquired by ``_lock_file``."""
245
+
246
+ if os.name == "nt":
247
+ handle.seek(0)
248
+ msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
249
+ return
250
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
251
+
252
+
204
253
  @contextmanager
205
254
  def _broker_lock(config: BrokerConfig) -> Iterator[None]:
206
255
  """Serialize local broker startup for one profile identity on this machine."""
207
256
 
208
257
  STATE_DIR.mkdir(parents=True, exist_ok=True)
209
258
  lock_path = STATE_DIR / f"{config.identity}.lock"
210
- with lock_path.open("w", encoding="utf-8") as handle:
211
- fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
259
+ with lock_path.open("a+b") as handle:
260
+ _lock_file(handle)
212
261
  try:
213
262
  yield
214
263
  finally:
215
- fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
264
+ _unlock_file(handle)
216
265
 
217
266
 
218
267
  def _state_paths(config: BrokerConfig) -> tuple[Path, ...]:
@@ -286,6 +335,9 @@ def _write_broker_state(config: BrokerConfig, state: BrokerState) -> None:
286
335
  def _process_command(pid: int) -> list[str]:
287
336
  """Return argv-like command text for a local PID when the OS exposes it."""
288
337
 
338
+ if os.name == "nt":
339
+ return _windows_process_command(pid)
340
+
289
341
  try:
290
342
  completed = subprocess.run(
291
343
  ["ps", "-p", str(pid), "-o", "command="],
@@ -307,9 +359,44 @@ def _process_command(pid: int) -> list[str]:
307
359
  return raw.split()
308
360
 
309
361
 
362
+ def _windows_process_command(pid: int) -> list[str]:
363
+ """Return command-line tokens for a Windows process when PowerShell exposes it."""
364
+
365
+ try:
366
+ completed = subprocess.run(
367
+ [
368
+ "powershell",
369
+ "-NoProfile",
370
+ "-Command",
371
+ (
372
+ "$p = Get-CimInstance Win32_Process -Filter "
373
+ f"'ProcessId = {int(pid)}'; if ($p) {{ $p.CommandLine }}"
374
+ ),
375
+ ],
376
+ capture_output=True,
377
+ text=True,
378
+ timeout=2,
379
+ check=False,
380
+ )
381
+ except Exception:
382
+ return []
383
+ if completed.returncode != 0:
384
+ return []
385
+ raw = completed.stdout.strip()
386
+ if not raw:
387
+ return []
388
+ try:
389
+ return shlex.split(raw, posix=False)
390
+ except ValueError:
391
+ return raw.split()
392
+
393
+
310
394
  def _listening_pids(*, host: str, port: int) -> list[int]:
311
395
  """Return local PIDs listening on a TCP port when OS tools expose them."""
312
396
 
397
+ if os.name == "nt":
398
+ return _windows_listening_pids(host=host, port=port)
399
+
313
400
  if shutil.which("lsof") is not None:
314
401
  try:
315
402
  completed = subprocess.run(
@@ -355,6 +442,41 @@ def _listening_pids(*, host: str, port: int) -> list[int]:
355
442
  return []
356
443
 
357
444
 
445
+ def _windows_listening_pids(*, host: str, port: int) -> list[int]:
446
+ """Return Windows PIDs listening on a local TCP port via netstat."""
447
+
448
+ try:
449
+ completed = subprocess.run(
450
+ ["netstat", "-ano", "-p", "tcp"],
451
+ capture_output=True,
452
+ text=True,
453
+ timeout=2,
454
+ check=False,
455
+ )
456
+ except Exception:
457
+ return []
458
+ if completed.returncode != 0:
459
+ return []
460
+
461
+ pids: list[int] = []
462
+ port_suffix = f":{port}"
463
+ for line in completed.stdout.splitlines():
464
+ parts = line.split()
465
+ if len(parts) < 5 or parts[0].upper() != "TCP":
466
+ continue
467
+ local_address = parts[1]
468
+ state = parts[3].upper()
469
+ if state != "LISTENING" or not local_address.endswith(port_suffix):
470
+ continue
471
+ if host not in ("0.0.0.0", "::", "localhost") and not local_address.startswith(host):
472
+ continue
473
+ try:
474
+ pids.append(int(parts[-1]))
475
+ except ValueError:
476
+ continue
477
+ return pids
478
+
479
+
358
480
  def _listener_processes(*, host: str, port: int) -> list[ListenerProcess]:
359
481
  """Return process metadata for listeners on the broker endpoint."""
360
482
 
@@ -382,7 +504,8 @@ def _is_matching_proxy_process(process: ListenerProcess, config: BrokerConfig) -
382
504
  command = process.command
383
505
  if not command:
384
506
  return False
385
- if Path(command[0]).name != "alloydb-auth-proxy":
507
+ executable_name = Path(command[0].strip('"')).name.lower()
508
+ if executable_name not in {"alloydb-auth-proxy", "alloydb-auth-proxy.exe"}:
386
509
  return False
387
510
  if config.instance_uri not in command:
388
511
  return False
@@ -435,6 +558,17 @@ def _broker_command(config: BrokerConfig, *, proxy_binary: str) -> list[str]:
435
558
  return command
436
559
 
437
560
 
561
+ def _proxy_popen_kwargs() -> dict[str, Any]:
562
+ """Return process-detach kwargs appropriate for the current platform."""
563
+
564
+ if os.name != "nt":
565
+ return {"start_new_session": True}
566
+ creationflags = 0
567
+ creationflags |= getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
568
+ creationflags |= getattr(subprocess, "DETACHED_PROCESS", 0)
569
+ return {"creationflags": creationflags}
570
+
571
+
438
572
  def _recent_broker_log(config: BrokerConfig, *, max_bytes: int = 12000) -> str:
439
573
  """Return the recent proxy log text that explains connection resets."""
440
574
 
@@ -565,8 +699,8 @@ def ensure_broker(config: BrokerConfig) -> BrokerState:
565
699
  command,
566
700
  stdout=log_file,
567
701
  stderr=log_file,
568
- start_new_session=True,
569
702
  env=os.environ.copy(),
703
+ **_proxy_popen_kwargs(),
570
704
  )
571
705
 
572
706
  deadline = time.monotonic() + 15.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "buildai-cli"
7
- version = "0.3.62"
7
+ version = "0.3.63"
8
8
  description = "Build AI CLI (Typer)"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes