cctally 1.59.0 → 1.61.0
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.
- package/CHANGELOG.md +31 -0
- package/bin/_cctally_cache.py +323 -48
- package/bin/_cctally_core.py +8 -0
- package/bin/_cctally_dashboard.py +913 -143
- package/bin/_cctally_doctor.py +2 -0
- package/bin/_cctally_forecast.py +24 -2
- package/bin/_cctally_parser.py +28 -2
- package/bin/_cctally_tui.py +676 -16
- package/bin/_cctally_update.py +49 -9
- package/bin/_cctally_weekrefs.py +92 -2
- package/bin/_lib_doctor.py +10 -0
- package/bin/_lib_snapshot_cache.py +988 -0
- package/bin/_lib_view_models.py +117 -12
- package/bin/cctally +57 -0
- package/dashboard/static/assets/index-0jzYm75p.css +1 -0
- package/dashboard/static/assets/index-D_Ylyqsf.js +80 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-BatfACUX.css +0 -1
- package/dashboard/static/assets/index-VUPDfWul.js +0 -80
package/bin/_lib_view_models.py
CHANGED
|
@@ -264,6 +264,16 @@ class TuiSessionRow:
|
|
|
264
264
|
# without re-reading ``session_files`` and so the share path can
|
|
265
265
|
# privacy-scrub via ``_lib_share._scrub``.
|
|
266
266
|
project_path: str | None = None
|
|
267
|
+
# Dashboard/TUI-only: the human-readable session title (AI-generated
|
|
268
|
+
# title when present, else the first non-marker user prompt). Populated
|
|
269
|
+
# by the dashboard/TUI snapshot wrapper (``_cctally_tui._tui_build_sessions``)
|
|
270
|
+
# via ``_lib_conversation_query._session_titles_map``; left ``None`` on the
|
|
271
|
+
# CLI ``session`` / share paths (they call ``build_sessions_view`` directly).
|
|
272
|
+
# This is transcript-derived content — it rides the per-request transcript
|
|
273
|
+
# privacy gate at envelope serialization (never emitted when the gate is
|
|
274
|
+
# closed). Defaults ``None`` so fixture rows built positionally stay
|
|
275
|
+
# compatible.
|
|
276
|
+
title: str | None = None
|
|
267
277
|
|
|
268
278
|
|
|
269
279
|
# === Internal helpers ======================================================
|
|
@@ -325,7 +335,8 @@ class DailyView:
|
|
|
325
335
|
display_tz_label: str = ""
|
|
326
336
|
|
|
327
337
|
|
|
328
|
-
def build_daily_view(entries, *, now_utc, display_tz=None, mode="auto"
|
|
338
|
+
def build_daily_view(entries, *, now_utc, display_tz=None, mode="auto",
|
|
339
|
+
aggregated_override=None):
|
|
329
340
|
"""Build a ``DailyView`` from raw ``UsageEntry`` list (spec §5.1).
|
|
330
341
|
|
|
331
342
|
Gap-free: only days with entries appear in ``view.rows`` /
|
|
@@ -341,9 +352,22 @@ def build_daily_view(entries, *, now_utc, display_tz=None, mode="auto"):
|
|
|
341
352
|
Leaves ``DailyPanelRow.label`` and ``intensity_bucket`` at dataclass
|
|
342
353
|
defaults — the dashboard envelope adapter populates them. CLI /
|
|
343
354
|
share consumers ignore them and read ``view.aggregated`` instead.
|
|
355
|
+
|
|
356
|
+
``aggregated_override`` (#268): when provided (a list/tuple of
|
|
357
|
+
``BucketUsage`` in ascending bucket-key order), skip the
|
|
358
|
+
``_aggregate_daily(entries, ...)`` re-costing and build the view over
|
|
359
|
+
those pre-aggregated buckets instead. This is the reuse seam the
|
|
360
|
+
dashboard's cached daily builder uses to serve immutable past days
|
|
361
|
+
from memory while keeping every downstream derivation (rows,
|
|
362
|
+
``cache_hit_pct``, totals, ``period_start``) single-sourced here. When
|
|
363
|
+
``None`` (CLI / share / cold path) behavior is unchanged and
|
|
364
|
+
byte-identical.
|
|
344
365
|
"""
|
|
345
366
|
_agg = _load_lib("_lib_aggregators")
|
|
346
|
-
|
|
367
|
+
if aggregated_override is not None:
|
|
368
|
+
buckets = list(aggregated_override)
|
|
369
|
+
else:
|
|
370
|
+
buckets = _agg._aggregate_daily(entries, mode=mode, tz=display_tz)
|
|
347
371
|
if not buckets:
|
|
348
372
|
return DailyView(
|
|
349
373
|
rows=(),
|
|
@@ -434,7 +458,8 @@ class MonthlyView:
|
|
|
434
458
|
display_tz_label: str = ""
|
|
435
459
|
|
|
436
460
|
|
|
437
|
-
def build_monthly_view(entries, *, now_utc, n=12, display_tz=None, mode="auto"
|
|
461
|
+
def build_monthly_view(entries, *, now_utc, n=12, display_tz=None, mode="auto",
|
|
462
|
+
aggregated_override=None):
|
|
438
463
|
"""Build a ``MonthlyView`` for the trailing ``n`` calendar months
|
|
439
464
|
(spec §5.2).
|
|
440
465
|
|
|
@@ -445,9 +470,21 @@ def build_monthly_view(entries, *, now_utc, n=12, display_tz=None, mode="auto"):
|
|
|
445
470
|
Totals (``total_cost_usd`` / ``total_tokens``) sum over the
|
|
446
471
|
truncated row set so the React panel sees the same number as the
|
|
447
472
|
CLI table footer would.
|
|
473
|
+
|
|
474
|
+
``aggregated_override`` (#268): when provided (a list/tuple of
|
|
475
|
+
``BucketUsage`` in ascending bucket-key order), skip the
|
|
476
|
+
``_aggregate_monthly(entries, ...)`` re-costing and build the view over
|
|
477
|
+
those pre-aggregated buckets. The reverse + cap-to-``n`` +
|
|
478
|
+
``delta_cost_pct`` + ``is_current`` presentation still runs over the
|
|
479
|
+
assembled list, so the current month's delta sees the prior (cached)
|
|
480
|
+
month (Codex F3). ``None`` preserves the CLI/share/cold behavior
|
|
481
|
+
byte-identically.
|
|
448
482
|
"""
|
|
449
483
|
_agg = _load_lib("_lib_aggregators")
|
|
450
|
-
|
|
484
|
+
if aggregated_override is not None:
|
|
485
|
+
buckets = list(aggregated_override)
|
|
486
|
+
else:
|
|
487
|
+
buckets = _agg._aggregate_monthly(entries, mode=mode, tz=display_tz)
|
|
451
488
|
if not buckets:
|
|
452
489
|
return MonthlyView(
|
|
453
490
|
rows=(), aggregated=(),
|
|
@@ -535,7 +572,7 @@ class WeeklyView:
|
|
|
535
572
|
|
|
536
573
|
|
|
537
574
|
def build_weekly_view(conn, entries, *, weeks, now_utc, display_tz=None,
|
|
538
|
-
as_of_utc=None, mode="auto"):
|
|
575
|
+
as_of_utc=None, mode="auto", aggregated_override=None):
|
|
539
576
|
"""Build a ``WeeklyView`` from subscription-week boundaries
|
|
540
577
|
(spec §5.3).
|
|
541
578
|
|
|
@@ -552,10 +589,22 @@ def build_weekly_view(conn, entries, *, weeks, now_utc, display_tz=None,
|
|
|
552
589
|
|
|
553
590
|
Output is newest-first across ``rows`` / ``aggregated`` /
|
|
554
591
|
``overlay`` — the CLI re-reverses for asc rendering.
|
|
592
|
+
|
|
593
|
+
``aggregated_override`` (#268): when provided (a list/tuple of
|
|
594
|
+
``BucketUsage`` in ascending bucket-key order — one per SubWeek WITH
|
|
595
|
+
data), skip the ``_aggregate_weekly(entries, weeks)`` re-costing and
|
|
596
|
+
run the overlay / delta / is_current presentation over those
|
|
597
|
+
pre-aggregated buckets. The overlay (``weekly_usage_snapshots``) is
|
|
598
|
+
always re-read fresh here, so a snapshot change is reflected even for a
|
|
599
|
+
cached bucket. ``None`` preserves the CLI/share/cold behavior
|
|
600
|
+
byte-identically.
|
|
555
601
|
"""
|
|
556
602
|
_agg = _load_lib("_lib_aggregators")
|
|
557
603
|
_cct_core = _load_lib("_cctally_core")
|
|
558
|
-
|
|
604
|
+
if aggregated_override is not None:
|
|
605
|
+
buckets_asc = list(aggregated_override)
|
|
606
|
+
else:
|
|
607
|
+
buckets_asc = _agg._aggregate_weekly(entries, weeks, mode=mode)
|
|
559
608
|
if not buckets_asc:
|
|
560
609
|
return WeeklyView(
|
|
561
610
|
rows=(), aggregated=(), overlay=(),
|
|
@@ -709,7 +758,8 @@ class TrendView:
|
|
|
709
758
|
display_tz_label: str = ""
|
|
710
759
|
|
|
711
760
|
|
|
712
|
-
def build_trend_view(conn, *, now_utc, n=8, display_tz=None
|
|
761
|
+
def build_trend_view(conn, *, now_utc, n=8, display_tz=None, skip_sync=False,
|
|
762
|
+
use_weekref_cost_cache=False):
|
|
713
763
|
"""Build a ``TrendView`` of the last ``n`` subscription weeks
|
|
714
764
|
(spec §5.4).
|
|
715
765
|
|
|
@@ -721,6 +771,17 @@ def build_trend_view(conn, *, now_utc, n=8, display_tz=None):
|
|
|
721
771
|
cmd_report path (``bin/cctally:~7969-7979``) and ``_tui_build_trend``
|
|
722
772
|
(``bin/_cctally_tui.py:~1515-1524``).
|
|
723
773
|
|
|
774
|
+
``skip_sync`` (default ``False``) threads through to
|
|
775
|
+
``_compute_cost_for_weekref`` → ``_sum_cost_for_range`` so the
|
|
776
|
+
reset-event live-cost path can read the cache WITHOUT running a
|
|
777
|
+
JSONL ingest. The #268 dashboard/TUI sync-thread rebuild sets it
|
|
778
|
+
``True`` (it ingests exactly once at the top of the rebuild); every
|
|
779
|
+
other caller — ``cmd_report``, ``_tui_build_trend`` panel, etc. —
|
|
780
|
+
keeps the default so their sync-on-read behavior is byte-identical.
|
|
781
|
+
At scale (~19 reset-event weeks over a 10K-file instance) the default
|
|
782
|
+
ran a full 10K-file glob PER reset week PER call site — the CPU peg
|
|
783
|
+
the #268 M1 sync-once refactor was meant to remove.
|
|
784
|
+
|
|
724
785
|
Rows are emitted oldest-first (chronological); each row populates
|
|
725
786
|
all 17 ``TuiTrendRow`` fields (7 historical + 10 extended).
|
|
726
787
|
``delta_dpp`` is computed against the prior chronological row's
|
|
@@ -800,7 +861,33 @@ def build_trend_view(conn, *, now_utc, n=8, display_tz=None):
|
|
|
800
861
|
)
|
|
801
862
|
usage_captured_at = usage["captured_at_utc"] if usage else None
|
|
802
863
|
if c._week_ref_has_reset_event(conn, week_ref):
|
|
803
|
-
|
|
864
|
+
if (
|
|
865
|
+
use_weekref_cost_cache
|
|
866
|
+
and week_ref.week_start_at
|
|
867
|
+
and week_ref.week_end_at
|
|
868
|
+
):
|
|
869
|
+
# #269 §4: a closed subscription week's cost is immutable, so
|
|
870
|
+
# route the reset-event live-cost through the shared per-weekref
|
|
871
|
+
# cache (the OPEN week is decided per call and never cached).
|
|
872
|
+
# `compute` is the from-scratch closure — invoked synchronously
|
|
873
|
+
# inside this iteration, so `week_ref` capture is safe — and
|
|
874
|
+
# `skip_sync=True` unconditionally (the dashboard already synced
|
|
875
|
+
# once at the top of the rebuild; the flag is only True there).
|
|
876
|
+
_sc = _load_lib("_lib_snapshot_cache")
|
|
877
|
+
cost_usd = _sc.cached_weekref_cost(
|
|
878
|
+
week_start_at=parse_iso(
|
|
879
|
+
week_ref.week_start_at, "week_start_at"
|
|
880
|
+
),
|
|
881
|
+
week_end_at=parse_iso(week_ref.week_end_at, "week_end_at"),
|
|
882
|
+
now_utc=now_utc,
|
|
883
|
+
compute=lambda: c._compute_cost_for_weekref(
|
|
884
|
+
week_ref, skip_sync=True
|
|
885
|
+
),
|
|
886
|
+
)
|
|
887
|
+
else:
|
|
888
|
+
cost_usd = c._compute_cost_for_weekref(
|
|
889
|
+
week_ref, skip_sync=skip_sync
|
|
890
|
+
)
|
|
804
891
|
cost_captured_at = (
|
|
805
892
|
usage_captured_at if cost_usd is not None else None
|
|
806
893
|
)
|
|
@@ -1029,10 +1116,21 @@ class SessionsView:
|
|
|
1029
1116
|
display_tz_label: str = ""
|
|
1030
1117
|
|
|
1031
1118
|
|
|
1032
|
-
def build_sessions_view(entries, *, now_utc, limit=None, display_tz=None,
|
|
1119
|
+
def build_sessions_view(entries, *, now_utc, limit=None, display_tz=None,
|
|
1120
|
+
mode="auto", aggregated_override=None):
|
|
1033
1121
|
"""Build a ``SessionsView`` from joined Claude session entries
|
|
1034
1122
|
(spec §5.5).
|
|
1035
1123
|
|
|
1124
|
+
``aggregated_override`` (#268 Group B): when provided, SKIP the
|
|
1125
|
+
``_aggregate_claude_sessions(entries)`` re-aggregation and use this
|
|
1126
|
+
pre-aggregated ``list[ClaudeSessionUsage]`` verbatim (the caller has
|
|
1127
|
+
already assembled + sorted-by-last_activity-desc the full session set
|
|
1128
|
+
from the module-level ``SessionCache``). ``entries`` is then unused for
|
|
1129
|
+
aggregation. The ``limit`` truncation + the per-row ``TuiSessionRow``
|
|
1130
|
+
derivation below run identically over the override, so a cached rebuild
|
|
1131
|
+
is byte-identical to the from-scratch pass. ``None`` (CLI ``session`` /
|
|
1132
|
+
share / cold paths) is unchanged — the aggregator runs on ``entries``.
|
|
1133
|
+
|
|
1036
1134
|
``entries`` is the ``list[_JoinedClaudeEntry]`` from
|
|
1037
1135
|
``get_claude_session_entries(range_start, range_end)``. The caller
|
|
1038
1136
|
controls the date window + ``skip_sync`` semantics; the builder
|
|
@@ -1059,8 +1157,11 @@ def build_sessions_view(entries, *, now_utc, limit=None, display_tz=None, mode="
|
|
|
1059
1157
|
is informational only.
|
|
1060
1158
|
"""
|
|
1061
1159
|
import os as _os # late: keep top-level imports lean.
|
|
1062
|
-
|
|
1063
|
-
|
|
1160
|
+
if aggregated_override is not None:
|
|
1161
|
+
aggregated = list(aggregated_override)
|
|
1162
|
+
else:
|
|
1163
|
+
_agg = _load_lib("_lib_aggregators")
|
|
1164
|
+
aggregated = _agg._aggregate_claude_sessions(entries, mode=mode)
|
|
1064
1165
|
# Apply limit truncation up front so `rows` and `aggregated` stay
|
|
1065
1166
|
# in lockstep (spec §4.3 invariant: `total_sessions == len(rows)
|
|
1066
1167
|
# == len(aggregated)`). limit=None → keep everything.
|
|
@@ -1497,6 +1598,7 @@ def build_forecast_view(
|
|
|
1497
1598
|
targets=(100, 90),
|
|
1498
1599
|
skip_sync: bool = False,
|
|
1499
1600
|
display_tz=None,
|
|
1601
|
+
use_weekref_cost_cache: bool = False,
|
|
1500
1602
|
):
|
|
1501
1603
|
"""Build a ``ForecastView`` (issue #57).
|
|
1502
1604
|
|
|
@@ -1515,7 +1617,10 @@ def build_forecast_view(
|
|
|
1515
1617
|
refresh skip).
|
|
1516
1618
|
"""
|
|
1517
1619
|
c = _cctally()
|
|
1518
|
-
inputs = c._load_forecast_inputs(
|
|
1620
|
+
inputs = c._load_forecast_inputs(
|
|
1621
|
+
conn, now_utc, skip_sync=skip_sync,
|
|
1622
|
+
use_weekref_cost_cache=use_weekref_cost_cache,
|
|
1623
|
+
)
|
|
1519
1624
|
if inputs is None:
|
|
1520
1625
|
return ForecastView(
|
|
1521
1626
|
output=None,
|
package/bin/cctally
CHANGED
|
@@ -487,6 +487,26 @@ build_codex_weekly_view = _lib_view_models.build_codex_weekly_view
|
|
|
487
487
|
CodexSessionView = _lib_view_models.CodexSessionView
|
|
488
488
|
build_codex_session_view = _lib_view_models.build_codex_session_view
|
|
489
489
|
|
|
490
|
+
# Snapshot-rebuild cache infrastructure (#268) — the composite data-version
|
|
491
|
+
# signature, the new-entry watermark, the generation counter, the Group A/B
|
|
492
|
+
# cache holders, and the Group A cached-bucket recompute helper. Registered
|
|
493
|
+
# here (before _cctally_dashboard / _cctally_tui load) so those siblings can
|
|
494
|
+
# `from _lib_snapshot_cache import ...` at module top under the _load_sibling
|
|
495
|
+
# loader, which does not place bin/ on sys.path. _load_sibling dedups via
|
|
496
|
+
# sys.modules, so this is the same instance pytest imports directly.
|
|
497
|
+
_lib_snapshot_cache = _load_sibling("_lib_snapshot_cache")
|
|
498
|
+
SnapshotSignature = _lib_snapshot_cache.SnapshotSignature
|
|
499
|
+
compute_signature = _lib_snapshot_cache.compute_signature
|
|
500
|
+
new_min_timestamp = _lib_snapshot_cache.new_min_timestamp
|
|
501
|
+
bump_generation = _lib_snapshot_cache.bump_generation
|
|
502
|
+
current_generation = _lib_snapshot_cache.current_generation
|
|
503
|
+
BucketCache = _lib_snapshot_cache.BucketCache
|
|
504
|
+
SessionCache = _lib_snapshot_cache.SessionCache
|
|
505
|
+
cached_buckets = _lib_snapshot_cache.cached_buckets
|
|
506
|
+
build_cached_group_a = _lib_snapshot_cache.build_cached_group_a
|
|
507
|
+
group_a_cache = _lib_snapshot_cache.group_a_cache
|
|
508
|
+
reset_group_a_state = _lib_snapshot_cache.reset_group_a_state
|
|
509
|
+
|
|
490
510
|
_lib_render = _load_sibling("_lib_render")
|
|
491
511
|
_CODEX_MONTHS = _lib_render._CODEX_MONTHS
|
|
492
512
|
_render_blocks_table = _lib_render._render_blocks_table
|
|
@@ -665,6 +685,26 @@ _cctally_doctor = _load_sibling("_cctally_doctor")
|
|
|
665
685
|
doctor_gather_state = _cctally_doctor.doctor_gather_state
|
|
666
686
|
cmd_doctor = _cctally_doctor.cmd_doctor
|
|
667
687
|
|
|
688
|
+
# Maintainer-local preview channel (hidden `__preview` subcommand). The pure
|
|
689
|
+
# kernel `_lib_preview` loads FIRST so `_cctally_preview`'s late-bound `_lp()`
|
|
690
|
+
# accessor resolves it via sys.modules; `cmd_preview` is the parser's
|
|
691
|
+
# `set_defaults(func=c.cmd_preview)` target. Both modules are excluded from the
|
|
692
|
+
# public mirror (maintainer-only, like the release tooling) — but the public
|
|
693
|
+
# `bin/cctally` still carries THESE load lines (the mirror emits full public
|
|
694
|
+
# file content on the next non-skip commit that touches this file). Tolerate
|
|
695
|
+
# the modules being absent so a public npm/brew build never bricks at import:
|
|
696
|
+
# on failure `cmd_preview` is None and the parser skips registering the hidden
|
|
697
|
+
# `__preview` subcommand entirely (see `_cctally_parser.py`'s
|
|
698
|
+
# `getattr(c, "cmd_preview", None)` guard). In the private tree the modules ARE
|
|
699
|
+
# present, so this is a no-op — `cmd_preview` is defined and `__preview`
|
|
700
|
+
# registers and works exactly as before.
|
|
701
|
+
try:
|
|
702
|
+
_lib_preview = _load_sibling("_lib_preview")
|
|
703
|
+
_cctally_preview = _load_sibling("_cctally_preview")
|
|
704
|
+
cmd_preview = _cctally_preview.cmd_preview
|
|
705
|
+
except Exception:
|
|
706
|
+
cmd_preview = None
|
|
707
|
+
|
|
668
708
|
# config.json reader/writer/lock + validators + `cctally config` entry point.
|
|
669
709
|
# Eager-loaded with per-symbol re-export so bare-name callers in cctally
|
|
670
710
|
# (dashboard `/api/settings`, `cmd_record_usage` reading `load_config()`,
|
|
@@ -794,6 +834,9 @@ _discover_codex_session_files = _cctally_cache._discover_codex_session_files
|
|
|
794
834
|
IngestStats = _cctally_cache.IngestStats
|
|
795
835
|
_progress_stderr = _cctally_cache._progress_stderr
|
|
796
836
|
_ensure_session_files_row = _cctally_cache._ensure_session_files_row
|
|
837
|
+
_acquire_cache_flock = _cctally_cache._acquire_cache_flock
|
|
838
|
+
PruneResult = _cctally_cache.PruneResult
|
|
839
|
+
_prune_orphaned_cache_entries = _cctally_cache._prune_orphaned_cache_entries
|
|
797
840
|
sync_cache = _cctally_cache.sync_cache
|
|
798
841
|
backfill_conversation_messages = _cctally_cache.backfill_conversation_messages
|
|
799
842
|
backfill_ai_titles = _cctally_cache.backfill_ai_titles
|
|
@@ -811,6 +854,7 @@ get_codex_entries = _cctally_cache.get_codex_entries
|
|
|
811
854
|
_sum_codex_cost_for_range = _cctally_cache._sum_codex_cost_for_range
|
|
812
855
|
get_entries = _cctally_cache.get_entries
|
|
813
856
|
open_cache_db = _cctally_cache.open_cache_db
|
|
857
|
+
_reset_orphan_warning_throttle = _cctally_cache._reset_orphan_warning_throttle
|
|
814
858
|
cmd_cache_sync = _cctally_cache.cmd_cache_sync
|
|
815
859
|
|
|
816
860
|
|
|
@@ -1041,6 +1085,8 @@ snapshot_to_envelope = _cctally_dashboard.snapshot_to_envelope
|
|
|
1041
1085
|
_session_detail_to_envelope = _cctally_dashboard._session_detail_to_envelope
|
|
1042
1086
|
_DASHBOARD_SYNC_LOCK_TIMEOUT_SECONDS = _cctally_dashboard._DASHBOARD_SYNC_LOCK_TIMEOUT_SECONDS
|
|
1043
1087
|
DashboardHTTPHandler = _cctally_dashboard.DashboardHTTPHandler
|
|
1088
|
+
_dashboard_self_heal_orphans = _cctally_dashboard._dashboard_self_heal_orphans
|
|
1089
|
+
_register_faulthandler_sigusr1 = _cctally_dashboard._register_faulthandler_sigusr1
|
|
1044
1090
|
cmd_dashboard = _cctally_dashboard.cmd_dashboard
|
|
1045
1091
|
|
|
1046
1092
|
|
|
@@ -2707,6 +2753,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
2707
2753
|
base += f" (dev — {_cctally_core.APP_DIR})"
|
|
2708
2754
|
elif _cctally_core._is_dev_checkout():
|
|
2709
2755
|
base += f" (dev checkout, custom data dir — {_cctally_core.APP_DIR})"
|
|
2756
|
+
# Preview channel (CCTALLY_CHANNEL=preview): append the channel marker
|
|
2757
|
+
# so diagnostics reveal a preview-channel binary. Gated → prod output
|
|
2758
|
+
# is byte-identical when the marker is unset.
|
|
2759
|
+
if _cctally_core.is_preview_channel():
|
|
2760
|
+
base += " (preview)"
|
|
2710
2761
|
print(base)
|
|
2711
2762
|
return 0
|
|
2712
2763
|
if not getattr(args, "func", None):
|
|
@@ -2796,6 +2847,12 @@ def _post_command_update_hooks(command: str | None, args) -> None:
|
|
|
2796
2847
|
return
|
|
2797
2848
|
if command == "repair-symlinks":
|
|
2798
2849
|
return
|
|
2850
|
+
if command == "__preview":
|
|
2851
|
+
# Maintainer-local dev tooling: must not load_config()/spawn a
|
|
2852
|
+
# background update-check or write anything outside the preview dir
|
|
2853
|
+
# (it runs before the wrapper sets CCTALLY_DATA_DIR, so APP_DIR is
|
|
2854
|
+
# still cctally-dev at this point). Same rationale class as doctor.
|
|
2855
|
+
return
|
|
2799
2856
|
# Self-heal: reconcile current_version with the running binary's
|
|
2800
2857
|
# CHANGELOG. Cheap (one CHANGELOG read, write only on the first
|
|
2801
2858
|
# command after a manual upgrade). Runs before load_config so a
|