thoughtleaders-cli 0.6.28__tar.gz → 0.6.31__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 (95) hide show
  1. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/PKG-INFO +1 -1
  3. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/pyproject.toml +1 -1
  4. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl/SKILL.md +1 -2
  5. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl/references/business-glossary.md +1 -1
  6. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/__init__.py +1 -1
  7. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/self_update.py +176 -11
  8. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/.claude-plugin/marketplace.json +0 -0
  9. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/.github/workflows/python-publish.yml +0 -0
  10. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/.gitignore +0 -0
  11. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/AGENTS.md +0 -0
  12. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/CLAUDE.md +0 -0
  13. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/LICENSE +0 -0
  14. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/README.md +0 -0
  15. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/agents/tl-analyst.md +0 -0
  16. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/commands/tl-balance.md +0 -0
  17. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/commands/tl-reports.md +0 -0
  18. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/commands/tl-sponsorships.md +0 -0
  19. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/commands/tl.md +0 -0
  20. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/docs/architecture.md +0 -0
  21. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/hooks/hooks.json +0 -0
  22. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/hooks/scripts/post-usage.sh +0 -0
  23. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/hooks/scripts/pre-check.sh +0 -0
  24. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl/references/elasticsearch-schema.md +0 -0
  25. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl/references/firebolt-schema.md +0 -0
  26. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl/references/postgres-schema.md +0 -0
  27. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-import/SKILL.md +0 -0
  28. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/SKILL.md +0 -0
  29. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
  30. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/examples/golden_queries.md +0 -0
  31. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/references/columns_brands.md +0 -0
  32. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/references/columns_channels.md +0 -0
  33. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/references/columns_content.md +0 -0
  34. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
  35. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
  36. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
  37. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/references/report_glossary.md +0 -0
  38. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/references/sortable_columns.json +0 -0
  39. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
  40. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
  41. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/references/widgets.md +0 -0
  42. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/tools/column_builder.md +0 -0
  43. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/tools/database_query.md +0 -0
  44. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/tools/keyword_research.md +0 -0
  45. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/tools/name_resolver.md +0 -0
  46. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/tools/sample_judge.md +0 -0
  47. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/tools/similar_channels.md +0 -0
  48. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
  49. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/skills/tl-report-builder/tools/widget_builder.md +0 -0
  50. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/_completions.py +0 -0
  51. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/auth/__init__.py +0 -0
  52. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/auth/commands.py +0 -0
  53. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/auth/finalize.py +0 -0
  54. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/auth/login.py +0 -0
  55. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/auth/pkce.py +0 -0
  56. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/auth/token_store.py +0 -0
  57. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/client/__init__.py +0 -0
  58. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/client/errors.py +0 -0
  59. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/client/http.py +0 -0
  60. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/__init__.py +0 -0
  61. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/_comments_common.py +0 -0
  62. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/ask.py +0 -0
  63. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/balance.py +0 -0
  64. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/brands.py +0 -0
  65. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/bulk_import.py +0 -0
  66. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/changelog.py +0 -0
  67. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/channels.py +0 -0
  68. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/credits.py +0 -0
  69. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/db.py +0 -0
  70. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/deals.py +0 -0
  71. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/describe.py +0 -0
  72. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/doctor.py +0 -0
  73. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/matches.py +0 -0
  74. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/proposals.py +0 -0
  75. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/recommender.py +0 -0
  76. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/reports.py +0 -0
  77. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/schema.py +0 -0
  78. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/setup.py +0 -0
  79. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/snapshots.py +0 -0
  80. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/sponsorships.py +0 -0
  81. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/uploads.py +0 -0
  82. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/commands/whoami.py +0 -0
  83. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/config.py +0 -0
  84. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/filters.py +0 -0
  85. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/hints.py +0 -0
  86. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/main.py +0 -0
  87. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/output/__init__.py +0 -0
  88. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/src/tl_cli/output/formatter.py +0 -0
  89. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/tests/__init__.py +0 -0
  90. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/tests/test_auth.py +0 -0
  91. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/tests/test_filters.py +0 -0
  92. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/tests/test_output.py +0 -0
  93. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/tests/test_reports.py +0 -0
  94. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/tests/test_sponsorships.py +0 -0
  95. {thoughtleaders_cli-0.6.28 → thoughtleaders_cli-0.6.31}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.6.28",
3
+ "version": "0.6.31",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.6.28
3
+ Version: 0.6.31
4
4
  Summary: ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence
5
5
  Project-URL: Homepage, https://thoughtleaders.io
6
6
  Project-URL: Repository, https://github.com/ThoughtLeaders-io/thoughtleaders-cli
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.6.28"
7
+ version = "0.6.31"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -318,7 +318,7 @@ Always load the [references/business-glossary.md](references/business-glossary.m
318
318
 
319
319
  ### Key business concepts
320
320
 
321
- See [references/business-glossary.md](references/business-glossary.md) for revenue/pipeline definitions, performance grades, ownership fields, MSN/TPP, and team rosters.
321
+ See [references/business-glossary.md](references/business-glossary.md) for revenue/pipeline definitions, performance grades, ownership fields, channel quality and availability, MSN/TPP, and team rosters.
322
322
 
323
323
  ### Limitations of the `tl`-only data path
324
324
 
@@ -522,4 +522,3 @@ tl recommender inspect-brand Nike # Per-tag breakdo
522
522
  tl recommender similar-to-profile 842 --limit 30 # Channels closest to a brand profile's ideal profile
523
523
  ```
524
524
  Use `tl recommender top` for category/topic discovery (it's ranked) and `tl channels similar` / `tl brands similar` for 1:1 lookalike searches.
525
-
@@ -18,6 +18,7 @@ Maps business terms to database concepts.
18
18
  | **Weighted pipeline** | `SUM(weighted_price)` for open opps | Pre-calculated on save |
19
19
  | **Ad is live** | `publish_date IS NOT NULL` | Until publish_date is set, ad is not on YouTube |
20
20
  | **Cancellation risk** | Sold but `publish_date IS NULL` | Sold deals without publish_date can still be canceled |
21
+ | **Immediately bookable** | `is_tl_channel = true` | TPP channels are immediately bookable |
21
22
 
22
23
  ## Performance Grade (`adlink.performance_grade`)
23
24
 
@@ -240,4 +241,3 @@ WHERE al.owner_publisher_id IN (218, 5710, 873, 9011, 11361)
240
241
  SELECT ... FROM thoughtleaders_profile p
241
242
  WHERE p.owner_publisher_id IN (71, 9274, 18159, 5799, 5804, 10743, 11592)
242
243
  ```
243
-
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.6.28"
3
+ __version__ = "0.6.31"
@@ -23,10 +23,12 @@ from pathlib import Path
23
23
 
24
24
  from tl_cli import __version__
25
25
 
26
- CACHE_PATH = Path.home() / ".cache" / "tl-cli" / "version-check.json"
26
+ CACHE_DIR = Path.home() / ".cache" / "tl-cli"
27
+ CACHE_PATH = CACHE_DIR / "version-check.json"
27
28
  CACHE_TTL_SECONDS = 3600 # 1 hour
28
29
  LATEST_URL = "https://api.github.com/repos/ThoughtLeaders-io/thoughtleaders-cli/releases/latest"
29
30
  REQUEST_TIMEOUT = 2 # tight — the user is already waiting to see their shell prompt back
31
+ WIN_UPGRADE_RESCHEDULE_WINDOW = 600 # 10 minutes: don't re-schedule a background upgrade we already queued
30
32
 
31
33
 
32
34
  def _detect_install_method() -> str | None:
@@ -92,17 +94,24 @@ REPO_URL = "https://github.com/ThoughtLeaders-io/thoughtleaders-cli.git"
92
94
 
93
95
 
94
96
  def _run_upgrade(method: str, latest: str) -> None:
95
- """Block briefly to run the upgrade. Progress goes to stderr so piped
96
- stdout stays clean.
97
+ """Run the upgrade. Progress goes to stderr so piped stdout stays clean.
97
98
 
98
99
  Uses `install --force` with the new tag URL. pipx/uv pin the original
99
100
  install spec including the git tag, so a plain `upgrade` re-installs
100
101
  the same version — `--force` is the only way to advance the pinned tag.
101
102
 
102
- On a successful upgrade, re-syncs Claude Code and OpenCode skills if
103
- their respective binaries are on PATH, so the new version's skills
104
- land in ~/.claude/ and ~/.config/opencode/ without the user having
105
- to remember to run `tl setup ...`.
103
+ On Windows the running tl.exe holds an exclusive lock on its own file,
104
+ so pipx/uv can never replace it in-process every attempt fails with
105
+ WinError 32 and leaves ``~``-prefixed orphan dirs in site-packages
106
+ that wedge the next launch with ``ModuleNotFoundError: No module
107
+ named 'tl_cli'``. The Windows path spawns a detached helper instead
108
+ that waits for our PID to exit and then runs the upgrade.
109
+
110
+ On a successful upgrade (POSIX inline path, or the detached helper),
111
+ Claude Code and OpenCode skills are re-synced if their binaries are
112
+ on PATH, so the new version's skills land in ~/.claude/ and
113
+ ~/.config/opencode/ without the user having to remember to run
114
+ `tl setup ...`.
106
115
  """
107
116
  tagged_url = f"git+{REPO_URL}@v{latest}"
108
117
  cmd = {
@@ -111,14 +120,25 @@ def _run_upgrade(method: str, latest: str) -> None:
111
120
  }.get(method)
112
121
  if not cmd:
113
122
  return
123
+
124
+ if sys.platform == "win32":
125
+ if _spawn_detached_windows_upgrade(cmd, latest):
126
+ print(
127
+ f"[tl-cli] upgrade {__version__} → {latest} scheduled "
128
+ f"(runs after this command exits; log: "
129
+ f"{CACHE_DIR / f'upgrade-{latest}.log'})",
130
+ file=sys.stderr,
131
+ )
132
+ _mark_upgrade_scheduled(latest)
133
+ return
134
+
114
135
  print(
115
136
  f"[tl-cli] upgrading {__version__} → {latest} via {method}…",
116
137
  file=sys.stderr,
117
138
  )
118
- # Capture output so a noisy traceback from a broken upgrader (seen on
119
- # Windows pipx shims that lose track of their own module) doesn't get
120
- # dumped into the user's shell — we surface it deliberately on failure
121
- # alongside an actionable next-step message.
139
+ # Capture output so a noisy traceback from a broken upgrader doesn't
140
+ # get dumped into the user's shell we surface it deliberately on
141
+ # failure alongside an actionable next-step message.
122
142
  try:
123
143
  result = subprocess.run(cmd, check=False, timeout=60, capture_output=True, text=True)
124
144
  except (OSError, subprocess.TimeoutExpired) as exc:
@@ -134,6 +154,146 @@ def _run_upgrade(method: str, latest: str) -> None:
134
154
  _report_upgrade_failure(method, cmd, result)
135
155
 
136
156
 
157
+ def _spawn_detached_windows_upgrade(cmd: list[str], latest: str) -> bool:
158
+ """Schedule the upgrade to run after this process exits.
159
+
160
+ Writes a small .cmd helper that polls until our PID disappears and
161
+ then runs the upgrader. Spawned with CREATE_NO_WINDOW |
162
+ CREATE_BREAKAWAY_FROM_JOB so it survives this process and any
163
+ job-object-owned shell that launched us. Output is appended to a
164
+ log file under ~/.cache/tl-cli/ so the user can diagnose failures
165
+ after their shell prompt returns.
166
+
167
+ Returns True on successful schedule. Idempotent against repeated
168
+ invocations: see `_already_scheduled`.
169
+ """
170
+ import os
171
+
172
+ try:
173
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
174
+ except OSError as exc:
175
+ print(
176
+ f"[tl-cli] could not create cache dir for upgrade helper: {exc}",
177
+ file=sys.stderr,
178
+ )
179
+ return False
180
+
181
+ log_path = CACHE_DIR / f"upgrade-{latest}.log"
182
+ script_path = CACHE_DIR / f"upgrade-{latest}.cmd"
183
+ # Per-helper temp file for tasklist output (a pipe would die; see below).
184
+ tmp_path = CACHE_DIR / f"upgrade-{latest}.tasklist.tmp"
185
+
186
+ parent_pid = os.getpid()
187
+ quoted_cmd = " ".join(f'"{a}"' for a in cmd)
188
+
189
+ # CRLF line endings + cmd.exe-safe quoting.
190
+ #
191
+ # We deliberately avoid the pipe-based idiom `tasklist | findstr`: a
192
+ # detached cmd.exe (CREATE_NO_WINDOW | CREATE_BREAKAWAY_FROM_JOB) has
193
+ # no console for the pipe sub-shells to attach to, and they exit
194
+ # silently the moment the helper hits a `|`. Routing through a temp
195
+ # file keeps every command as a plain redirected child.
196
+ script = (
197
+ "@echo off\r\n"
198
+ f'echo [tl-cli upgrader] waiting for parent PID {parent_pid} to exit > "{log_path}"\r\n'
199
+ ":wait\r\n"
200
+ f'tasklist /FI "PID eq {parent_pid}" /NH 2>NUL > "{tmp_path}"\r\n'
201
+ f'findstr /C:"{parent_pid}" "{tmp_path}" >NUL\r\n'
202
+ "if not errorlevel 1 (\r\n"
203
+ " ping -n 2 127.0.0.1 >NUL\r\n"
204
+ " goto wait\r\n"
205
+ ")\r\n"
206
+ f'del "{tmp_path}" 2>NUL\r\n'
207
+ f'echo [tl-cli upgrader] running: {quoted_cmd} >> "{log_path}"\r\n'
208
+ f'{quoted_cmd} >> "{log_path}" 2>&1\r\n'
209
+ "set RC=%ERRORLEVEL%\r\n"
210
+ f'echo [tl-cli upgrader] exit code %RC% >> "{log_path}"\r\n'
211
+ "if not %RC%==0 goto end\r\n"
212
+ "where claude >NUL 2>&1\r\n"
213
+ "if not errorlevel 1 (\r\n"
214
+ f' echo [tl-cli upgrader] re-syncing claude skills >> "{log_path}"\r\n'
215
+ f' tl setup claude --json >> "{log_path}" 2>&1\r\n'
216
+ ")\r\n"
217
+ "where opencode >NUL 2>&1\r\n"
218
+ "if not errorlevel 1 (\r\n"
219
+ f' echo [tl-cli upgrader] re-syncing opencode skills >> "{log_path}"\r\n'
220
+ f' tl setup opencode --json >> "{log_path}" 2>&1\r\n'
221
+ ")\r\n"
222
+ ":end\r\n"
223
+ )
224
+
225
+ try:
226
+ script_path.write_text(script)
227
+ except OSError as exc:
228
+ print(f"[tl-cli] could not write upgrade helper: {exc}", file=sys.stderr)
229
+ return False
230
+
231
+ # creationflags constants — repeated here rather than referenced from
232
+ # subprocess.* so this works on Python builds where the symbols are
233
+ # guarded behind sys.platform checks.
234
+ #
235
+ # CREATE_NO_WINDOW (not DETACHED_PROCESS): a fully-detached cmd.exe
236
+ # has no console for spawned child commands to inherit, which breaks
237
+ # piped sub-shells and a few utilities that try to query the console.
238
+ # CREATE_NO_WINDOW gives us "no visible window" while keeping the
239
+ # console subsystem available for children.
240
+ CREATE_NEW_PROCESS_GROUP = 0x00000200
241
+ CREATE_BREAKAWAY_FROM_JOB = 0x01000000
242
+ CREATE_NO_WINDOW = 0x08000000
243
+
244
+ try:
245
+ subprocess.Popen(
246
+ ["cmd.exe", "/c", str(script_path)],
247
+ creationflags=(
248
+ CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB
249
+ ),
250
+ stdin=subprocess.DEVNULL,
251
+ stdout=subprocess.DEVNULL,
252
+ stderr=subprocess.DEVNULL,
253
+ close_fds=True,
254
+ cwd=str(CACHE_DIR),
255
+ )
256
+ except OSError as exc:
257
+ print(
258
+ f"[tl-cli] could not schedule background upgrade: {exc}\n"
259
+ f"[tl-cli] upgrade manually with:\n {quoted_cmd}",
260
+ file=sys.stderr,
261
+ )
262
+ return False
263
+ return True
264
+
265
+
266
+ def _mark_upgrade_scheduled(latest: str) -> None:
267
+ """Record in the version-check cache that we've queued a background
268
+ upgrade for ``latest`` so subsequent invocations don't re-schedule
269
+ while the first helper is still pending."""
270
+ try:
271
+ cache = json.loads(CACHE_PATH.read_text())
272
+ except (OSError, json.JSONDecodeError):
273
+ cache = {}
274
+ cache["scheduled_at"] = time.time()
275
+ cache["scheduled_for"] = latest
276
+ try:
277
+ CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
278
+ CACHE_PATH.write_text(json.dumps(cache))
279
+ except OSError:
280
+ pass
281
+
282
+
283
+ def _already_scheduled(latest: str) -> bool:
284
+ """True if we recently queued the same upgrade — caller should skip."""
285
+ try:
286
+ cache = json.loads(CACHE_PATH.read_text())
287
+ except (OSError, json.JSONDecodeError):
288
+ return False
289
+ if cache.get("scheduled_for") != latest:
290
+ return False
291
+ scheduled_at = cache.get("scheduled_at")
292
+ if not isinstance(scheduled_at, (int, float)):
293
+ return False
294
+ return time.time() - scheduled_at < WIN_UPGRADE_RESCHEDULE_WINDOW
295
+
296
+
137
297
  def _report_upgrade_failure(method: str, cmd: list[str], result: subprocess.CompletedProcess) -> None:
138
298
  """Print a user-friendly failure message after a non-zero upgrader exit.
139
299
 
@@ -215,6 +375,11 @@ def check_and_upgrade() -> None:
215
375
  except ValueError:
216
376
  return
217
377
 
378
+ # On Windows the upgrade is detached: don't re-queue it on every
379
+ # subsequent tl invocation while the first helper is still pending.
380
+ if sys.platform == "win32" and _already_scheduled(latest):
381
+ return
382
+
218
383
  _run_upgrade(method, latest)
219
384
  except Exception:
220
385
  # Never let a version-check bug break the user's workflow.