coderouter-cli 2.13.0__py3-none-any.whl → 2.14.0__py3-none-any.whl

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.
coderouter/vscode_init.py CHANGED
@@ -395,7 +395,19 @@ def _apply_envrc(
395
395
  force: bool,
396
396
  dry_run: bool,
397
397
  ) -> FileOutcome:
398
- new_text = _render_envrc(base_url=base_url, profile=profile)
398
+ """Write (or refresh) the managed block in ``.envrc``.
399
+
400
+ Three cases, and only the third can refuse:
401
+
402
+ * **No file** — create it with the fenced block.
403
+ * **Fenced file** — replace the block, keep everything outside it
404
+ byte-for-byte. No ``--force`` needed; this is not destructive.
405
+ * **Unfenced file we did not write** — refuse, unless ``--force``,
406
+ which now *appends* the block rather than replacing the file. The
407
+ operator's lines survive either way.
408
+ """
409
+ block = _render_envrc_block(base_url=base_url, profile=profile)
410
+
399
411
  old_text = ""
400
412
  if path.exists():
401
413
  try:
@@ -407,32 +419,76 @@ def _apply_envrc(
407
419
  reason=f"cannot read existing file: {exc}",
408
420
  )
409
421
 
422
+ if not old_text:
423
+ new_text = block + "\n"
424
+ else:
425
+ split = _split_managed_block(old_text)
426
+ if split is None and _looks_like_our_legacy_output(
427
+ old_text, base_url=base_url, profile=profile
428
+ ):
429
+ # Our own pre-v2.14.0 output: adopt it into the fence silently.
430
+ split = ("", "")
431
+ if split is None:
432
+ outside = "" # --force adopts the file wholesale; see below
433
+ if not force:
434
+ would = old_text.rstrip("\n") + "\n\n" + block + "\n"
435
+ return FileOutcome(
436
+ path=path,
437
+ action="conflict",
438
+ diff=_text_diff(old_text, would, path),
439
+ reason=(
440
+ ".envrc exists and carries no "
441
+ f"`{_ENVRC_BEGIN}` fence, so CodeRouter did not write it. "
442
+ "Re-run with --force to APPEND the managed block "
443
+ "(your existing lines are kept — unlike before "
444
+ "v2.14.0, --force no longer replaces the whole file); "
445
+ f"the previous contents are still saved to "
446
+ f"{_backup_path(path).name}."
447
+ ),
448
+ )
449
+ # --force on an unfenced file means "adopt it, mine wins". The
450
+ # block is APPENDED, not substituted for the file: direnv applies
451
+ # exports in order, so a duplicate the operator already had is
452
+ # shadowed rather than deleted, and their other lines survive. The
453
+ # conflict scan is skipped here on purpose — refusing would leave
454
+ # --force with nothing it could actually do.
455
+ new_text = old_text.rstrip("\n") + "\n\n" + block + "\n"
456
+ else:
457
+ before, after = split
458
+ outside = before + after
459
+ new_text = (
460
+ before.rstrip("\n") + ("\n\n" if before.strip() else "")
461
+ ) + block + ("\n\n" + after.lstrip("\n") if after.strip() else "\n")
462
+
463
+ conflicts = _unmanaged_conflicts(outside)
464
+ if conflicts:
465
+ # direnv applies exports in order, so a duplicate outside the
466
+ # fence would silently win or lose depending on placement.
467
+ # Refusing is the only answer that cannot surprise anyone.
468
+ return FileOutcome(
469
+ path=path,
470
+ action="conflict",
471
+ diff=_text_diff(old_text, new_text, path),
472
+ reason=(
473
+ "these variables are already exported outside the managed "
474
+ f"block: {', '.join(conflicts)}. CodeRouter will not write "
475
+ "a second export for a value you own — direnv would apply "
476
+ "whichever comes last. Remove your line, or move it inside "
477
+ f"`{_ENVRC_BEGIN}` … `{_ENVRC_END}`."
478
+ ),
479
+ )
480
+
410
481
  if new_text == old_text:
411
482
  return FileOutcome(path=path, action="unchanged")
412
483
 
413
484
  diff = _text_diff(old_text, new_text, path)
414
485
 
415
- if old_text and not force:
416
- return FileOutcome(
417
- path=path,
418
- action="conflict",
419
- diff=diff,
420
- reason=(
421
- ".envrc already exists and its contents differ. "
422
- "Re-run with --force to replace the WHOLE file with the "
423
- "generated contents (this is not a merge — custom lines "
424
- "such as `source_env_if_exists .envrc.local` are dropped); "
425
- f"the previous contents are saved to {_backup_path(path).name}. "
426
- "(Tip: keep secrets in a separate .envrc.local and "
427
- "``source_env_if_exists`` from .envrc.)"
428
- ),
429
- )
430
-
431
486
  reason = ""
432
487
  if not dry_run:
433
- # H-11: --force replaces the whole file, so stash the old bytes
434
- # (mode included) before they are gone. Inside the dry_run guard
435
- # on purpose — --dry-run must not touch the filesystem at all.
488
+ # H-11: keep the old bytes (mode included) even though the write is
489
+ # now surgical. A bug in the fence logic would take exactly this
490
+ # path. Inside the dry_run guard on purpose — --dry-run must not
491
+ # touch the filesystem at all.
436
492
  if old_text:
437
493
  backup = _make_backup(path)
438
494
  reason = (
@@ -463,6 +519,86 @@ def _apply_envrc(
463
519
  )
464
520
 
465
521
 
522
+ # ---------------------------------------------------------------------------
523
+ # v2.14.0 — marker-delimited managed block in .envrc
524
+ # ---------------------------------------------------------------------------
525
+ #
526
+ # Until now, ``--force`` on ``.envrc`` was a whole-file replace: it wrote the
527
+ # generated contents over whatever was there, dropping any line the operator
528
+ # had added. That is how H-11 happened, and "we back it up first" only helps
529
+ # somebody who knows to look for the backup.
530
+ #
531
+ # The fix is the one codex-router uses on ``~/.codex/config.toml``: fence the
532
+ # part we own between markers, rewrite only that, and leave everything else
533
+ # byte-for-byte. Once the fence exists, a re-run is no longer destructive, so
534
+ # it needs no ``--force`` at all — ``--force`` shrinks back to its honest
535
+ # meaning of "yes, adopt a file I did not write".
536
+
537
+ _ENVRC_BEGIN = "# BEGIN coderouter-managed"
538
+ _ENVRC_END = "# END coderouter-managed"
539
+
540
+ # The variables the managed block owns. A value for one of these sitting
541
+ # OUTSIDE the block is a conflict we refuse rather than silently shadow —
542
+ # direnv applies exports in order, so whichever came last would win and the
543
+ # operator would have no idea which one is live.
544
+ _MANAGED_ENVRC_VARS = ("ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "CODEROUTER_MODE")
545
+
546
+
547
+ def _split_managed_block(text: str) -> tuple[str, str] | None:
548
+ """Split ``text`` around the managed block.
549
+
550
+ Returns ``(before, after)`` with the block (and its markers) removed,
551
+ or ``None`` when the file carries no fence. A file with a begin marker
552
+ and no end marker also returns ``None`` — a half-written fence is not
553
+ something to guess at, and the caller turns that into a refusal.
554
+ """
555
+ start = text.find(_ENVRC_BEGIN)
556
+ if start == -1:
557
+ return None
558
+ end = text.find(_ENVRC_END, start)
559
+ if end == -1:
560
+ return None
561
+ return text[:start], text[end + len(_ENVRC_END) :].lstrip("\n")
562
+
563
+
564
+ def _render_envrc_block(*, base_url: str, profile: str | None) -> str:
565
+ """The fenced block CodeRouter owns, markers included."""
566
+ lines = [
567
+ _ENVRC_BEGIN,
568
+ "# Managed by `coderouter vscode-init`. Edits inside this block are",
569
+ "# overwritten on the next run; put your own lines outside it.",
570
+ f'export ANTHROPIC_BASE_URL="{base_url}"',
571
+ f'export ANTHROPIC_AUTH_TOKEN="{DEFAULT_TOKEN}"',
572
+ ]
573
+ if profile:
574
+ lines.append(f'export CODEROUTER_MODE="{profile}"')
575
+ lines.append(_ENVRC_END)
576
+ return "\n".join(lines)
577
+
578
+
579
+ def _unmanaged_conflicts(outside_text: str) -> list[str]:
580
+ """Managed variables the operator exports outside our block."""
581
+ found: list[str] = []
582
+ for line in outside_text.splitlines():
583
+ stripped = line.strip()
584
+ if stripped.startswith("#"):
585
+ continue
586
+ for var in _MANAGED_ENVRC_VARS:
587
+ if stripped.startswith((f"export {var}=", f"{var}=")) and var not in found:
588
+ found.append(var)
589
+ return found
590
+
591
+
592
+ def _looks_like_our_legacy_output(text: str, *, base_url: str, profile: str | None) -> bool:
593
+ """True when the file is byte-identical to a pre-v2.14.0 generation.
594
+
595
+ An operator upgrading has an unfenced ``.envrc`` that we wrote — asking
596
+ them to pass ``--force`` to adopt our own previous output would be
597
+ rude, and appending a second block would double the exports.
598
+ """
599
+ return text == _render_envrc(base_url=base_url, profile=profile)
600
+
601
+
466
602
  def _render_envrc(*, base_url: str, profile: str | None) -> str:
467
603
  """Build the .envrc contents. Kept split for test targeting."""
468
604
  lines = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.13.0
3
+ Version: 2.14.0
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -1,10 +1,11 @@
1
1
  coderouter/__init__.py,sha256=ghdjPrLtnRzY8fyQ4CJZI1UJKADyNTLtA3G7se8H7Ns,696
2
2
  coderouter/__main__.py,sha256=-LCgxJnvgUV240HjQKv7ly-mn2NuKHpC4nCpvTHjeSU,130
3
- coderouter/cli.py,sha256=TyI29d8-Zc8SClcKn92Oj8DGyQRxMpBSWTxLiFTH3Qo,37043
3
+ coderouter/cli.py,sha256=Nbx1ur_HnssFtuM48yPELwhY40veVNnTAfK7OVF2uVc,42400
4
4
  coderouter/cli_stats.py,sha256=CCjzc1G4hTRHZ2gG1XhxhDpUkJnnl3NXbcbp1T18jpg,29894
5
5
  coderouter/cost.py,sha256=32h6uzb4nxh2eA5d2Hn3kD9yJbtis6CFDAbeIy5KRkM,7431
6
+ coderouter/credentials.py,sha256=R_PdrcA941_jJX_U8hxrjPDTCVVzLd4dsw2c00KwAwE,12991
6
7
  coderouter/doctor.py,sha256=wmPunlvqm2s7x3VGLjexqHPLD2oejwE3Mu04V8y-xn4,89869
7
- coderouter/doctor_apply.py,sha256=UqRvzRqENHWqsL3OCsuSotNdxCMw5rP2LnCWMBLmZow,28033
8
+ coderouter/doctor_apply.py,sha256=tZT3bEd1G6t67YRhf1TGY4NlSvZuJJbnEqN-3AKy8Hc,29972
8
9
  coderouter/env_security.py,sha256=FEBZnXfJ0xE39kmMMn39zk0W_DRRnmcB_REmP9f4xWo,14796
9
10
  coderouter/errors.py,sha256=Xmq67lheyw8iv3Ox39jh2c4tvNI5RcUR4QkoxVDN6l4,1130
10
11
  coderouter/gguf_introspect.py,sha256=Scu0JjUfLHj7T7TL0HQIm1AZcGYrXsjkaMMSN9HkTro,16121
@@ -13,21 +14,23 @@ coderouter/language_tax.py,sha256=LTbE3tIfoJuV2O3T0NixRKhzq_dEOTUuPEerJv2q9uk,93
13
14
  coderouter/launcher_devices.py,sha256=DAV34Q1crGptmj1zOAk21YQzlxQNAt5Ud7qcldTD95w,29406
14
15
  coderouter/launcher_speculative.py,sha256=h0RTGsD1Y56sJVywDJwybrT75_Ta3GF1Jz_B1d42aPg,12931
15
16
  coderouter/launcher_swap.py,sha256=tMW5ZElDdf9T1VQGsUkvfLiSCdj_oXN_UtkV7DE8ySY,24175
16
- coderouter/logging.py,sha256=5rfi4_R4aoeeUYDc642dgs61wA0aBX7wvubyB-k3Uyo,58074
17
+ coderouter/logging.py,sha256=jhEyCNqvES6x8bohn-F6CofxRNY8vyKfmli0Tj8RYWU,58472
17
18
  coderouter/output_filters.py,sha256=0ry_rPiS_kC-FnHgaNVP6v7e6Al2djxzu9vBzZ8kEkE,25314
19
+ coderouter/rollback.py,sha256=Bqxx1MALTDLYc0BtGMEMqRyiZIWFoAZJH_RBQw-M5ow,7675
20
+ coderouter/secret_redaction.py,sha256=Rg71gqKN6jnybZ6UvCvXIvnThI9uRBvmOqC2grSUSUk,23159
18
21
  coderouter/token_estimation.py,sha256=7klQYA4FIg1vtjQGWAhwiBhiLwyz8KTetrLVXFJLGIc,16957
19
22
  coderouter/token_estimation_accurate.py,sha256=GTfzrBVnvAGjeVzmzAeUdOYZvWZKLAxcxPpFiJGlzjk,4609
20
- coderouter/vscode_init.py,sha256=fW0chCDTngvWQuJgQ7aqYmE6hnZO3hCk8mAiEkZ9FCo,26741
23
+ coderouter/vscode_init.py,sha256=fM-f9RbX91F8ioVoiltoU5Hfp2Z6E7vxppSTmrvKTaQ,32969
21
24
  coderouter/adapters/__init__.py,sha256=7dIDSZ-FE_0iSqLSDc_lK1idRdLTKcM2hP9tCJipgPI,463
22
- coderouter/adapters/anthropic_native.py,sha256=-FGTQFlFL5Z2IZms4NUGPcNkIN-fVvQrCY8e8nxW17s,24777
25
+ coderouter/adapters/anthropic_native.py,sha256=jGeJqL13mYyPfmwTsYFoi89-Uq-yxdZxvhfnbGdjH4U,24931
23
26
  coderouter/adapters/base.py,sha256=ykKMaiIVVGr23oFFXilK2iP_YhI8-CtsEiJyFXaC9bE,10396
24
- coderouter/adapters/openai_compat.py,sha256=wcrd_UpV7N6ISpUyKvdFPGU-YjCrX5oMqGpjAFEeWfg,20356
27
+ coderouter/adapters/openai_compat.py,sha256=aGdXi7XpsGVFP-vzy-b2z6CEvuGYmc8smLvSKjOeqM0,20580
25
28
  coderouter/adapters/registry.py,sha256=XNc1wmFv_oZlqKS0rA693TRDdbq1sZ-OIcofXOVOtq0,3680
26
29
  coderouter/config/__init__.py,sha256=FODEn74fN-qZnt4INPSHswqhOlEgpL6-_onxsitSx8g,274
27
30
  coderouter/config/capability_registry.py,sha256=RZgzHF54dXy0CCDocRCz2ee21qXGbbgTSKTkRI9iLL4,16936
28
31
  coderouter/config/env_file.py,sha256=CoMK27fuAXm-NtoLzXb8yN2E-wDFjHQuFwiIlmgTBQw,10356
29
- coderouter/config/loader.py,sha256=vk1LFm-OCiyq2GB5G53g7kTnrJgmQjkZPqgr9X13wpc,11294
30
- coderouter/config/schemas.py,sha256=J05km7-YXgNxy6guKUDcNsuMyJ21SnDIurjx8Rz_hSg,136672
32
+ coderouter/config/loader.py,sha256=7FoJXWaO_vN9FAjUStJDwI4aAYcUQQgMJtq5BNy8Pnc,12419
33
+ coderouter/config/schemas.py,sha256=y4zdG0S1k-kGYYM8XrMpYB6BR1HBxc2XMX6bTftRWpQ,142367
31
34
  coderouter/data/__init__.py,sha256=uNyfD9jaCvTWsBAWtaw1Fr25OSxzv3psGMfBjT1z0Cc,328
32
35
  coderouter/data/model-capabilities.yaml,sha256=S9jt6SC6-3s2-icZ_n-a14iEMnc2yB1C2R6q-N_tZWQ,19309
33
36
  coderouter/guards/__init__.py,sha256=5qliYBqygvVPneej7nx0uSjxDKsz7t8VzvrDgVBJlvU,1170
@@ -44,10 +47,11 @@ coderouter/guards/tool_loop.py,sha256=EzeMcmU7BLeTW2jsRVevU81l5rhWcn1oUr7EpzgXjV
44
47
  coderouter/ingress/__init__.py,sha256=WQsCH2CGJCAhy0mS6GSEdeYZRkkQu2OHDsP4CJWTLug,155
45
48
  coderouter/ingress/anthropic_routes.py,sha256=1ThXzx8VgxRV7UCi3NF5ey7kbr0ADPKTXyQuYS5gK1w,27063
46
49
  coderouter/ingress/app.py,sha256=ZOx7dSvMxaWS4CzXOOEwgzFuycAVgAeWKCN6lFNF-F0,22433
47
- coderouter/ingress/dashboard_routes.py,sha256=1SqNNrVTfQiZcx5n2MvjAzWirittX0e61tCt-MKp4Ag,25617
50
+ coderouter/ingress/dashboard_routes.py,sha256=nPW5_pJe27-hzqCs4sqVVn0BrgXB_7Wcd4qZ6cirvWo,26736
48
51
  coderouter/ingress/launcher_routes.py,sha256=uP_Otp9JE0fydUt1YP-wYcnS8Kj5RPLNIFf88YpfkPM,141535
49
- coderouter/ingress/metrics_routes.py,sha256=M22dwOGn24P05Ge4W3c7d7mYytSGWjIR-pPSPOAiHJY,3965
52
+ coderouter/ingress/metrics_routes.py,sha256=4s_f9CY8nVIdQBfXxyUMlN7r5_cXHOaAu7PvNbUuoxg,4609
50
53
  coderouter/ingress/openai_routes.py,sha256=TrugsQNJfNPgKKLzfT1aXzrs4emWYGE4XebEBuaZoWk,12505
54
+ coderouter/ingress/ui_auth.py,sha256=uCWB5IaMQRNFYdY39FQ5DDVhsoRQJoqimZNSAu_QK_Q,3742
51
55
  coderouter/metrics/__init__.py,sha256=7Es351DPS7yLM0yVF_F0eesmiD83n7Zzhie44chht38,1465
52
56
  coderouter/metrics/collector.py,sha256=wdGuX5yOBWEHCz1KVIfaUBZP5NXW3U34yyYlLGwgoCE,61212
53
57
  coderouter/metrics/prometheus.py,sha256=THYkvrcpQK3ZNW-BV0h3O-PoT2ppNZ0dapQuu1nyHwE,24410
@@ -62,9 +66,9 @@ coderouter/routing/budget.py,sha256=PblmVKJGs_BwNa9uDHAA8hmZ4XIVKv38mHAeU0V3OMs,
62
66
  coderouter/routing/capability.py,sha256=rRQhzTgQYFTFDNYcSLKvq3xjdw6B1w7T-3jS7PTMI14,27864
63
67
  coderouter/routing/fallback.py,sha256=fqwjz5hTNsQDEf45nYcVTwGJwz2mFxQ5ZmZwdvFSKsI,156394
64
68
  coderouter/state/__init__.py,sha256=XoGcPmmBQSiZWML2S0juSveQ78xfhtdeCliNnVyzu7E,1088
65
- coderouter/state/audit_log.py,sha256=n7vuDsTfd5iuNUJhlRQPakJ2tMSsT9EfgPCzs1GEaac,10941
69
+ coderouter/state/audit_log.py,sha256=-6okO4NZ63a0C4Q2BHwhBHqB-0NlwOgSN_d4ProDFhM,11227
66
70
  coderouter/state/replay.py,sha256=Z_YHKroTKZdrL8qObFxcoLOAQWWXZvXFdLfxzvBhEJg,11230
67
- coderouter/state/request_log.py,sha256=mNxQJ9IDGpUtT5rZbCo_-CTyhxmvig7C2Gfvdn9rcss,8612
71
+ coderouter/state/request_log.py,sha256=JlEvkWHwGtoLesnqr0XJy9CyXQDX4ZRHFMTPJ_0FegQ,8898
68
72
  coderouter/state/store.py,sha256=h-rsMJq8GILsOfCP94nI40cuHaj4Vqycsm9UNN77REI,7445
69
73
  coderouter/state/suggest_rules.py,sha256=FvdhEvao5NvdKp9zs8AkcoFKHY4yqqXY2HekvSjpDFA,16670
70
74
  coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8IYOtG8,1788
@@ -72,8 +76,8 @@ coderouter/translation/anthropic.py,sha256=ZQQGS9itN_3o7IKD68lKr5gR_Kem32Jsd7OcG
72
76
  coderouter/translation/convert.py,sha256=VrstDAMsUpcQ1VgMTn_-ETp1OjGX8v0ZGhCkcd4icyU,53037
73
77
  coderouter/translation/tool_repair.py,sha256=1p8rmdyyscGX2g0DDOYitMnLv-nKfR1SLuLaRB4z7GM,64244
74
78
  coderouter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- coderouter_cli-2.13.0.dist-info/METADATA,sha256=J6h8klUNsTIhWoMjrax70AR9nCFXYK_rFP2tbpChn8I,16897
76
- coderouter_cli-2.13.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
77
- coderouter_cli-2.13.0.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
78
- coderouter_cli-2.13.0.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
79
- coderouter_cli-2.13.0.dist-info/RECORD,,
79
+ coderouter_cli-2.14.0.dist-info/METADATA,sha256=GFrjK39UG8-agX8MIVToBeSanFbzCJ0zRVCfktderhs,16897
80
+ coderouter_cli-2.14.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
81
+ coderouter_cli-2.14.0.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
82
+ coderouter_cli-2.14.0.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
83
+ coderouter_cli-2.14.0.dist-info/RECORD,,