thoughtleaders-cli 0.9.8__tar.gz → 0.9.10__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.
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/.claude-plugin/plugin.json +1 -1
- thoughtleaders_cli-0.9.10/.github/workflows/ci.yml +66 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/PKG-INFO +20 -1
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/README.md +19 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/__init__.py +1 -1
- thoughtleaders_cli-0.9.10/src/tl_cli/__main__.py +13 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/setup.py +164 -18
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/self_update.py +5 -1
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_setup.py +135 -0
- thoughtleaders_cli-0.9.8/.github/workflows/ci.yml +0 -38
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/.github/dependabot.yml +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/.github/workflows/cli-integration.yml +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/.gitignore +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/API.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/LICENSE +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/agents/keyword-context-classifier.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/agents/keyword-entity-resolver.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/agents/keyword-relevance-validator.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/agents/youtube-comment-classifier.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl/references/postgres-schema.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/.gitignore +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/references/scoring.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/report.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/score.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-create-workflow/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-create-workflow/references/creating-in-app.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-create-workflow/references/methodology.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-create-workflow/references/pitfalls.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-create-workflow/references/workflow-model.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-keyword-research/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-keyword-research/references/elasticsearch-content-search.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-keyword-research/references/help.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-keyword-research/scripts/build_report.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-keyword-research/scripts/expand_entities.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-keyword-research/scripts/fetch_context.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-keyword-research/scripts/probe.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-keyword-research/scripts/search_channels.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-keyword-research/scripts/search_videos.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-keyword-research/scripts/select_keywords.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-save-report/references/widgets.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-top-partnerships/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-views-guarantee/SKILL.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/skills/tl-views-guarantee/scripts/vg.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/_typer_utils.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/memory.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/profiles.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/skills.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/commands/workflows.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/query_history.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/src/tl_cli/skill_registry.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_brands_winner_channels.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_build_report.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_channels_lookalike.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_db_repeat_warning.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_describe.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_error_hints.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_expand_entities.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_memory.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_probe.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_profiles.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_quota_refusal_message.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_refusal_rendering.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_scripts_executable.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_search_channels.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_search_videos.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_select_keywords.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_setup_marker_respect.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_skill_registry.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_skills_command.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_whoami_newsletter.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests/test_workflows.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests_cli/AGENTS.md +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests_cli/conftest.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests_cli/test_balance.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests_cli/test_db_es.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests_cli/test_db_fb.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests_cli/test_db_pg.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests_cli/test_schema.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/tests_cli/test_whoami.py +0 -0
- {thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/uv.lock +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Fast, hermetic checks on every push to main and every PR. These run the
|
|
4
|
+
# mocked unit suite under tests/ — no live API, no credits, fully
|
|
5
|
+
# deterministic. The live CLI tests live in cli-integration.yml.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches: [main]
|
|
10
|
+
pull_request:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
unit-tests:
|
|
21
|
+
name: Unit tests (mocked) · py${{ matrix.python-version }}
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
strategy:
|
|
24
|
+
fail-fast: false
|
|
25
|
+
matrix:
|
|
26
|
+
# Matches `requires-python = ">=3.12"`.
|
|
27
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v7
|
|
30
|
+
- uses: astral-sh/setup-uv@v7
|
|
31
|
+
with:
|
|
32
|
+
enable-cache: true
|
|
33
|
+
- name: Sync locked dependencies
|
|
34
|
+
run: uv sync --frozen --python ${{ matrix.python-version }}
|
|
35
|
+
- name: Run unit tests
|
|
36
|
+
# tests/ mock the HTTP client (CliRunner + fake get_client), so there
|
|
37
|
+
# is no network, no API key, and no credit spend.
|
|
38
|
+
run: uv run --python ${{ matrix.python-version }} --with pytest pytest tests/ -v
|
|
39
|
+
|
|
40
|
+
windows-launcher:
|
|
41
|
+
name: Windows launcher (tl setup windows)
|
|
42
|
+
# `tl setup windows` writes a tl.cmd that runs the CLI through the signed
|
|
43
|
+
# python.exe, so the unsigned pipx stub can't be blocked by Smart App
|
|
44
|
+
# Control. This job proves the wrapper is written and actually launches the
|
|
45
|
+
# CLI. NOTE: GitHub runners don't have SAC in enforcement, so this verifies
|
|
46
|
+
# the mechanism, not the block itself — real SAC validation needs a SAC/WDAC
|
|
47
|
+
# machine. Hermetic: a uv project install is a dev install, so the CLI's
|
|
48
|
+
# auto-updater no-ops (no network).
|
|
49
|
+
runs-on: windows-latest
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v7
|
|
52
|
+
- uses: astral-sh/setup-uv@v7
|
|
53
|
+
with:
|
|
54
|
+
enable-cache: true
|
|
55
|
+
- name: Sync locked dependencies
|
|
56
|
+
run: uv sync --frozen
|
|
57
|
+
- name: Install the launcher wrapper
|
|
58
|
+
run: uv run tl setup windows
|
|
59
|
+
- name: Wrapper exists and launches the CLI through python
|
|
60
|
+
shell: pwsh
|
|
61
|
+
run: |
|
|
62
|
+
$cmd = "$env:LOCALAPPDATA\Programs\tl-cli\tl.cmd"
|
|
63
|
+
if (-not (Test-Path $cmd)) { throw "wrapper not written at $cmd" }
|
|
64
|
+
Get-Content $cmd
|
|
65
|
+
& $cmd --version
|
|
66
|
+
if ($LASTEXITCODE -ne 0) { throw "wrapper failed to launch the CLI (exit $LASTEXITCODE)" }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.10
|
|
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
|
|
@@ -92,6 +92,25 @@ uv tool install thoughtleaders-cli
|
|
|
92
92
|
pip install thoughtleaders-cli
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
#### Windows with Smart App Control / WDAC
|
|
96
|
+
|
|
97
|
+
The pipx/uv install above creates an **unsigned** launcher stub that Windows
|
|
98
|
+
Smart App Control (and enforced WDAC policies) block — `tl` fails to start with
|
|
99
|
+
a Code Integrity error. Fix it with:
|
|
100
|
+
|
|
101
|
+
```powershell
|
|
102
|
+
tl setup windows
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
That drops a small `tl.cmd` on your `PATH` (ahead of the blocked stub) which
|
|
106
|
+
launches the CLI through the already-signed `python.exe`, so it runs cleanly
|
|
107
|
+
under SAC. If `tl` is *already* blocked and won't start, bootstrap it through
|
|
108
|
+
the signed interpreter directly, then reopen your terminal:
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
& "$env:USERPROFILE\pipx\venvs\thoughtleaders-cli\Scripts\python.exe" -m tl_cli setup windows
|
|
112
|
+
```
|
|
113
|
+
|
|
95
114
|
Then set up:
|
|
96
115
|
```bash
|
|
97
116
|
tl auth login # authenticate with ThoughtLeaders (OAuth2 browser flow, device code, or API key)
|
|
@@ -64,6 +64,25 @@ uv tool install thoughtleaders-cli
|
|
|
64
64
|
pip install thoughtleaders-cli
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
#### Windows with Smart App Control / WDAC
|
|
68
|
+
|
|
69
|
+
The pipx/uv install above creates an **unsigned** launcher stub that Windows
|
|
70
|
+
Smart App Control (and enforced WDAC policies) block — `tl` fails to start with
|
|
71
|
+
a Code Integrity error. Fix it with:
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
tl setup windows
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
That drops a small `tl.cmd` on your `PATH` (ahead of the blocked stub) which
|
|
78
|
+
launches the CLI through the already-signed `python.exe`, so it runs cleanly
|
|
79
|
+
under SAC. If `tl` is *already* blocked and won't start, bootstrap it through
|
|
80
|
+
the signed interpreter directly, then reopen your terminal:
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
& "$env:USERPROFILE\pipx\venvs\thoughtleaders-cli\Scripts\python.exe" -m tl_cli setup windows
|
|
84
|
+
```
|
|
85
|
+
|
|
67
86
|
Then set up:
|
|
68
87
|
```bash
|
|
69
88
|
tl auth login # authenticate with ThoughtLeaders (OAuth2 browser flow, device code, or API key)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Module entry point for the CLI.
|
|
2
|
+
|
|
3
|
+
Enables `python -m tl_cli` and serves as the single entry script for the
|
|
4
|
+
PyInstaller-frozen standalone build (see packaging/tl.spec). Invoking the CLI
|
|
5
|
+
through the signed `python.exe` / signed frozen exe is what keeps it runnable
|
|
6
|
+
under Windows Smart App Control, which blocks the unsigned launcher stub that
|
|
7
|
+
pipx/uv generate per install.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from tl_cli.main import cli
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
cli()
|
|
@@ -160,14 +160,69 @@ def _run_claude(args: list[str], claude_bin: str) -> tuple[bool, str]:
|
|
|
160
160
|
return False, str(e)
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
def _installed_plugin_version() -> str | None:
|
|
164
|
+
"""Version of the plugin Claude Code actually has on disk, or None.
|
|
165
|
+
|
|
166
|
+
Reads Claude Code's own install record rather than trusting the stamp
|
|
167
|
+
written at setup time: `plugin install` is a no-op once the plugin is
|
|
168
|
+
present, so a stamp saying "whatever the CLI version was when setup last
|
|
169
|
+
ran" can name a version that was never installed. Returns None when the
|
|
170
|
+
record is missing or unreadable — callers treat that as "unknown", never
|
|
171
|
+
as "outdated", so a change to Claude Code's bookkeeping cannot turn into
|
|
172
|
+
a false warning for every user at once.
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
record = json.loads((CLAUDE_PLUGINS_DIR / "installed_plugins.json").read_text(encoding="utf-8"))
|
|
176
|
+
entries = record["plugins"][PLUGIN_KEY]
|
|
177
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
178
|
+
return None
|
|
179
|
+
if not isinstance(entries, list):
|
|
180
|
+
return None
|
|
181
|
+
# One entry per install scope; setup installs at user scope, so prefer it.
|
|
182
|
+
scoped = sorted((e for e in entries if isinstance(e, dict)), key=lambda e: e.get("scope") != "user")
|
|
183
|
+
for entry in scoped:
|
|
184
|
+
version = entry.get("version")
|
|
185
|
+
if isinstance(version, str) and version and version != "unknown":
|
|
186
|
+
return version
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
163
190
|
def _get_installed_plugin_version() -> str | None:
|
|
164
|
-
"""
|
|
191
|
+
"""Installed plugin version: Claude Code's record, else the setup stamp.
|
|
192
|
+
|
|
193
|
+
The stamp is the fallback for installs that never reached the plugin
|
|
194
|
+
registry (the standalone-skills path), where the copied skills really are
|
|
195
|
+
the ones this CLI shipped with.
|
|
196
|
+
"""
|
|
197
|
+
version = _installed_plugin_version()
|
|
198
|
+
if version:
|
|
199
|
+
return version
|
|
165
200
|
version_file = CLAUDE_PLUGINS_DIR / "tl-cli" / ".version"
|
|
166
201
|
if version_file.exists():
|
|
167
202
|
return version_file.read_text().strip()
|
|
168
203
|
return None
|
|
169
204
|
|
|
170
205
|
|
|
206
|
+
def _write_version_stamp(version: str) -> None:
|
|
207
|
+
"""Record which plugin version is installed, for `check_plugin_version()`."""
|
|
208
|
+
version_dir = CLAUDE_PLUGINS_DIR / "tl-cli"
|
|
209
|
+
version_dir.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
(version_dir / ".version").write_text(version)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _update_plugin(claude_bin: str) -> tuple[bool, bool, str]:
|
|
214
|
+
"""Pull the installed plugin up to the marketplace's current version.
|
|
215
|
+
|
|
216
|
+
`plugin install` exits successfully without doing anything when the
|
|
217
|
+
plugin is already installed, so this is what actually advances an
|
|
218
|
+
existing install. Returns (ok, changed, output); `changed` is False when
|
|
219
|
+
the plugin was already at the latest version.
|
|
220
|
+
"""
|
|
221
|
+
ok, output = _run_claude(["plugin", "update", PLUGIN_KEY], claude_bin)
|
|
222
|
+
changed = ok and "already at the latest" not in output.lower()
|
|
223
|
+
return ok, changed, output
|
|
224
|
+
|
|
225
|
+
|
|
171
226
|
def check_plugin_version() -> list[str]:
|
|
172
227
|
"""Check if installed plugin versions match CLI version.
|
|
173
228
|
|
|
@@ -176,11 +231,9 @@ def check_plugin_version() -> list[str]:
|
|
|
176
231
|
warnings = []
|
|
177
232
|
|
|
178
233
|
# Claude Code
|
|
179
|
-
|
|
180
|
-
if
|
|
181
|
-
installed
|
|
182
|
-
if installed != __version__:
|
|
183
|
-
warnings.append(f"Claude Code plugin is outdated (v{installed} vs CLI v{__version__}). Run 'tl setup claude' to update.")
|
|
234
|
+
installed = _get_installed_plugin_version()
|
|
235
|
+
if installed and installed != __version__:
|
|
236
|
+
warnings.append(f"Claude Code plugin is outdated (v{installed} vs CLI v{__version__}). Run 'tl setup claude' to update.")
|
|
184
237
|
|
|
185
238
|
# OpenCode
|
|
186
239
|
opencode_version_file = OPENCODE_SKILLS_DIR / ".tl-version"
|
|
@@ -368,9 +421,9 @@ def setup_claude(
|
|
|
368
421
|
) -> None:
|
|
369
422
|
"""Install the TL CLI plugin for Claude Code.
|
|
370
423
|
|
|
371
|
-
Registers the ThoughtLeaders marketplace, installs
|
|
372
|
-
and adds a /tl shim command so the plugin's tl skill can
|
|
373
|
-
without the plugin namespace. Standalone skill copies in ~/.claude/skills
|
|
424
|
+
Registers the ThoughtLeaders marketplace, installs and updates the
|
|
425
|
+
tl-cli plugin, and adds a /tl shim command so the plugin's tl skill can
|
|
426
|
+
be invoked without the plugin namespace. Standalone skill copies in ~/.claude/skills
|
|
374
427
|
are only installed as a fallback when the plugin can't be installed;
|
|
375
428
|
unmodified copies left by earlier versions are removed.
|
|
376
429
|
|
|
@@ -445,7 +498,23 @@ def setup_claude(
|
|
|
445
498
|
_print_manual_instructions()
|
|
446
499
|
raise SystemExit(1)
|
|
447
500
|
|
|
448
|
-
# Step 3:
|
|
501
|
+
# Step 3: Update the plugin. An install that found the plugin already
|
|
502
|
+
# present did nothing, so without this an existing install stays pinned
|
|
503
|
+
# at whatever version it was first installed at.
|
|
504
|
+
console.print("[bold]Updating plugin...[/bold]")
|
|
505
|
+
ok, changed, output = _update_plugin(claude_bin)
|
|
506
|
+
installed_version = _installed_plugin_version()
|
|
507
|
+
version_label = f"v{installed_version}" if installed_version else "version unknown"
|
|
508
|
+
if not ok:
|
|
509
|
+
console.print(f" [yellow]![/yellow] Plugin update failed: {output}")
|
|
510
|
+
console.print(f" Continuing with the installed plugin ({version_label}).")
|
|
511
|
+
elif changed:
|
|
512
|
+
console.print(f" [green]✓[/green] Plugin updated to {version_label}")
|
|
513
|
+
console.print(" [yellow]Restart Claude Code to apply the new version.[/yellow]")
|
|
514
|
+
else:
|
|
515
|
+
console.print(f" [green]✓[/green] Plugin already up to date ({version_label})")
|
|
516
|
+
|
|
517
|
+
# Step 4: /tl shim command + cleanup of standalone copies from older versions
|
|
449
518
|
console.print("[bold]Installing /tl shortcut...[/bold]")
|
|
450
519
|
shim = _install_command_shim()
|
|
451
520
|
console.print(f" [green]✓[/green] /tl command installed: {shim}")
|
|
@@ -456,10 +525,7 @@ def setup_claude(
|
|
|
456
525
|
console.print(f" [yellow]![/yellow] Kept {kept} modified standalone skill(s) in {CLAUDE_SKILLS_DIR}")
|
|
457
526
|
console.print(" These differ from the plugin's versions and shadow nothing — remove manually if unwanted.")
|
|
458
527
|
|
|
459
|
-
|
|
460
|
-
version_dir = CLAUDE_PLUGINS_DIR / "tl-cli"
|
|
461
|
-
version_dir.mkdir(parents=True, exist_ok=True)
|
|
462
|
-
(version_dir / ".version").write_text(__version__)
|
|
528
|
+
_write_version_stamp(installed_version or __version__)
|
|
463
529
|
|
|
464
530
|
console.print()
|
|
465
531
|
console.print("[green]Setup complete![/green]")
|
|
@@ -523,6 +589,14 @@ def _setup_noninteractive(fmt: str = "json") -> None:
|
|
|
523
589
|
|
|
524
590
|
ok, output = _run_claude(["plugin", "install", PLUGIN_KEY], claude_bin)
|
|
525
591
|
result["plugin_installed"] = ok or "already" in output.lower()
|
|
592
|
+
|
|
593
|
+
# `install` is a no-op on an already-installed plugin — this is what
|
|
594
|
+
# advances it to the marketplace's current version.
|
|
595
|
+
if result["plugin_installed"]:
|
|
596
|
+
ok, changed, output = _update_plugin(claude_bin)
|
|
597
|
+
result["plugin_updated"] = changed
|
|
598
|
+
if not ok:
|
|
599
|
+
result["plugin_update_error"] = output
|
|
526
600
|
else:
|
|
527
601
|
result["marketplace_registered"] = False
|
|
528
602
|
result["plugin_installed"] = False
|
|
@@ -541,10 +615,11 @@ def _setup_noninteractive(fmt: str = "json") -> None:
|
|
|
541
615
|
result["command_shim_installed"] = False
|
|
542
616
|
result["standalone_skills_installed"] = _install_standalone_skills(plugin_root)
|
|
543
617
|
|
|
544
|
-
#
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
618
|
+
# Stamp what is actually installed. The standalone fallback copied this
|
|
619
|
+
# CLI's own bundled skills, so there __version__ is the honest answer.
|
|
620
|
+
installed_version = _installed_plugin_version() if result["plugin_installed"] else None
|
|
621
|
+
result["plugin_version"] = installed_version
|
|
622
|
+
_write_version_stamp(installed_version or __version__)
|
|
548
623
|
|
|
549
624
|
result["status"] = "ok"
|
|
550
625
|
_emit_setup_result(result, fmt)
|
|
@@ -756,3 +831,74 @@ def setup_codex(
|
|
|
756
831
|
json_output=json_output,
|
|
757
832
|
toon_output=toon_output,
|
|
758
833
|
)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _prepend_user_path(new_dir: str) -> bool:
|
|
837
|
+
"""Prepend ``new_dir`` to the persistent per-user PATH.
|
|
838
|
+
|
|
839
|
+
Writes HKCU\\Environment and broadcasts the change so new processes pick it
|
|
840
|
+
up without a sign-out. Returns True if PATH was changed, False if new_dir
|
|
841
|
+
was already present. Windows-only.
|
|
842
|
+
"""
|
|
843
|
+
import ctypes
|
|
844
|
+
import winreg
|
|
845
|
+
|
|
846
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment", 0, winreg.KEY_READ | winreg.KEY_WRITE) as key:
|
|
847
|
+
try:
|
|
848
|
+
current, _ = winreg.QueryValueEx(key, "Path")
|
|
849
|
+
except FileNotFoundError:
|
|
850
|
+
current = ""
|
|
851
|
+
parts = [p for p in current.split(os.pathsep) if p]
|
|
852
|
+
target = os.path.normcase(os.path.normpath(new_dir))
|
|
853
|
+
if any(os.path.normcase(os.path.normpath(p)) == target for p in parts):
|
|
854
|
+
return False
|
|
855
|
+
updated = os.pathsep.join([new_dir, *parts]) if parts else new_dir
|
|
856
|
+
winreg.SetValueEx(key, "Path", 0, winreg.REG_EXPAND_SZ, updated)
|
|
857
|
+
|
|
858
|
+
# Tell running shells the environment changed (best-effort).
|
|
859
|
+
ctypes.windll.user32.SendMessageTimeoutW(0xFFFF, 0x1A, 0, "Environment", 0x2, 5000, None)
|
|
860
|
+
return True
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
@app.command("windows")
|
|
864
|
+
def setup_windows(
|
|
865
|
+
force: bool = typer.Option(False, "--force", help="Overwrite an existing tl.cmd wrapper"),
|
|
866
|
+
) -> None:
|
|
867
|
+
"""Make `tl` runnable under Windows Smart App Control (Windows only).
|
|
868
|
+
|
|
869
|
+
pipx/uv expose `tl` through an *unsigned* launcher stub that Smart App
|
|
870
|
+
Control (and enforced WDAC policies) block. This writes a small `tl.cmd`
|
|
871
|
+
that runs the CLI through the already-signed `python.exe` instead, and
|
|
872
|
+
puts it ahead of the blocked stub on your PATH.
|
|
873
|
+
|
|
874
|
+
Run once. On a machine where `tl` is already blocked, bootstrap it with
|
|
875
|
+
the venv's signed python directly:
|
|
876
|
+
|
|
877
|
+
<pipx-venv>\\Scripts\\python.exe -m tl_cli setup windows
|
|
878
|
+
|
|
879
|
+
Examples:
|
|
880
|
+
tl setup windows
|
|
881
|
+
"""
|
|
882
|
+
if sys.platform != "win32":
|
|
883
|
+
console.print("[yellow]`tl setup windows` only applies on Windows — nothing to do here.[/yellow]")
|
|
884
|
+
return
|
|
885
|
+
|
|
886
|
+
python_exe = Path(sys.executable)
|
|
887
|
+
wrapper_dir = Path(os.environ.get("LOCALAPPDATA", Path.home())) / "Programs" / "tl-cli"
|
|
888
|
+
wrapper = wrapper_dir / "tl.cmd"
|
|
889
|
+
|
|
890
|
+
if wrapper.exists() and not force:
|
|
891
|
+
console.print(f"[yellow]Wrapper already exists:[/yellow] {wrapper}\nRe-run with --force to regenerate it.")
|
|
892
|
+
return
|
|
893
|
+
|
|
894
|
+
wrapper_dir.mkdir(parents=True, exist_ok=True)
|
|
895
|
+
# Absolute path to the signed interpreter bypasses PATH so the launch can't
|
|
896
|
+
# resolve back to the blocked stub. %* forwards all arguments unchanged.
|
|
897
|
+
wrapper.write_text(f'@echo off\r\n"{python_exe}" -m tl_cli %*\r\n', encoding="ascii", newline="")
|
|
898
|
+
|
|
899
|
+
console.print(f"[green]✓[/green] Wrote signed-launcher wrapper: {wrapper}")
|
|
900
|
+
if _prepend_user_path(str(wrapper_dir)):
|
|
901
|
+
console.print(f"[green]✓[/green] Added to your user PATH (ahead of the blocked stub): {wrapper_dir}")
|
|
902
|
+
else:
|
|
903
|
+
console.print(f"[dim]Already on your user PATH: {wrapper_dir}[/dim]")
|
|
904
|
+
console.print("[dim]Open a new terminal, then run:[/dim] tl whoami")
|
|
@@ -426,7 +426,11 @@ def _resync_integrations() -> None:
|
|
|
426
426
|
subprocess.run(
|
|
427
427
|
[tl_bin, "setup", tool, "--json"],
|
|
428
428
|
capture_output=True,
|
|
429
|
-
|
|
429
|
+
# The Claude Code path shells out to `claude` several times
|
|
430
|
+
# (marketplace add + update, plugin install + update), each
|
|
431
|
+
# with its own timeout — a 120s budget for the whole resync
|
|
432
|
+
# can expire mid-sequence on a slow network.
|
|
433
|
+
timeout=300,
|
|
430
434
|
check=False,
|
|
431
435
|
)
|
|
432
436
|
except (OSError, subprocess.TimeoutExpired):
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Tests for `tl setup` helpers."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
@@ -8,8 +9,10 @@ from tl_cli.commands.setup import (
|
|
|
8
9
|
_bundled_skill_blurbs,
|
|
9
10
|
_find_claude_binary,
|
|
10
11
|
_install_command_shim,
|
|
12
|
+
_installed_plugin_version,
|
|
11
13
|
_remove_matching_standalone_skills,
|
|
12
14
|
_trees_identical,
|
|
15
|
+
_update_plugin,
|
|
13
16
|
)
|
|
14
17
|
|
|
15
18
|
|
|
@@ -191,3 +194,135 @@ class TestFindClaudeBinary:
|
|
|
191
194
|
if sys.platform == "win32":
|
|
192
195
|
monkeypatch.setenv("APPDATA", str(tmp_path / "AppData" / "Roaming"))
|
|
193
196
|
assert _find_claude_binary() is None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TestInstalledPluginVersion:
|
|
200
|
+
def _record(self, tmp_path, monkeypatch, payload):
|
|
201
|
+
plugins = tmp_path / "plugins"
|
|
202
|
+
plugins.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
monkeypatch.setattr(setup, "CLAUDE_PLUGINS_DIR", plugins)
|
|
204
|
+
(plugins / "installed_plugins.json").write_text(json.dumps(payload), encoding="utf-8")
|
|
205
|
+
return plugins
|
|
206
|
+
|
|
207
|
+
def test_reads_version_from_install_record(self, tmp_path, monkeypatch):
|
|
208
|
+
self._record(tmp_path, monkeypatch, {"version": 2, "plugins": {setup.PLUGIN_KEY: [{"scope": "user", "version": "0.7.2"}]}})
|
|
209
|
+
assert _installed_plugin_version() == "0.7.2"
|
|
210
|
+
|
|
211
|
+
def test_prefers_user_scope(self, tmp_path, monkeypatch):
|
|
212
|
+
self._record(
|
|
213
|
+
tmp_path,
|
|
214
|
+
monkeypatch,
|
|
215
|
+
{"plugins": {setup.PLUGIN_KEY: [{"scope": "project", "version": "0.1.0"}, {"scope": "user", "version": "0.9.9"}]}},
|
|
216
|
+
)
|
|
217
|
+
assert _installed_plugin_version() == "0.9.9"
|
|
218
|
+
|
|
219
|
+
def test_ignores_unknown_version(self, tmp_path, monkeypatch):
|
|
220
|
+
self._record(tmp_path, monkeypatch, {"plugins": {setup.PLUGIN_KEY: [{"scope": "user", "version": "unknown"}]}})
|
|
221
|
+
assert _installed_plugin_version() is None
|
|
222
|
+
|
|
223
|
+
def test_missing_file(self, tmp_path, monkeypatch):
|
|
224
|
+
monkeypatch.setattr(setup, "CLAUDE_PLUGINS_DIR", tmp_path / "plugins")
|
|
225
|
+
assert _installed_plugin_version() is None
|
|
226
|
+
|
|
227
|
+
def test_unreadable_record_is_unknown_not_outdated(self, tmp_path, monkeypatch):
|
|
228
|
+
plugins = tmp_path / "plugins"
|
|
229
|
+
plugins.mkdir(parents=True)
|
|
230
|
+
monkeypatch.setattr(setup, "CLAUDE_PLUGINS_DIR", plugins)
|
|
231
|
+
(plugins / "installed_plugins.json").write_text("{not json", encoding="utf-8")
|
|
232
|
+
assert _installed_plugin_version() is None
|
|
233
|
+
assert not [w for w in setup.check_plugin_version() if "Claude Code plugin" in w]
|
|
234
|
+
|
|
235
|
+
def test_unexpected_shape_is_unknown(self, tmp_path, monkeypatch):
|
|
236
|
+
self._record(tmp_path, monkeypatch, {"plugins": {setup.PLUGIN_KEY: {"scope": "user", "version": "0.7.2"}}})
|
|
237
|
+
assert _installed_plugin_version() is None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TestCheckPluginVersion:
|
|
241
|
+
def test_warns_on_record_behind_cli(self, tmp_path, monkeypatch):
|
|
242
|
+
plugins = tmp_path / "plugins"
|
|
243
|
+
plugins.mkdir(parents=True)
|
|
244
|
+
monkeypatch.setattr(setup, "CLAUDE_PLUGINS_DIR", plugins)
|
|
245
|
+
monkeypatch.setattr(setup, "OPENCODE_SKILLS_DIR", tmp_path / "opencode")
|
|
246
|
+
monkeypatch.setattr(setup, "AGENTS_SKILLS_DIR", tmp_path / "agents")
|
|
247
|
+
(plugins / "installed_plugins.json").write_text(
|
|
248
|
+
json.dumps({"plugins": {setup.PLUGIN_KEY: [{"scope": "user", "version": "0.7.2"}]}}), encoding="utf-8"
|
|
249
|
+
)
|
|
250
|
+
# A stamp claiming the current version must not mask the real one.
|
|
251
|
+
(plugins / "tl-cli").mkdir()
|
|
252
|
+
(plugins / "tl-cli" / ".version").write_text(setup.__version__)
|
|
253
|
+
|
|
254
|
+
warnings = setup.check_plugin_version()
|
|
255
|
+
assert any("0.7.2" in w for w in warnings)
|
|
256
|
+
|
|
257
|
+
def test_falls_back_to_stamp_without_record(self, tmp_path, monkeypatch):
|
|
258
|
+
plugins = tmp_path / "plugins"
|
|
259
|
+
(plugins / "tl-cli").mkdir(parents=True)
|
|
260
|
+
monkeypatch.setattr(setup, "CLAUDE_PLUGINS_DIR", plugins)
|
|
261
|
+
monkeypatch.setattr(setup, "OPENCODE_SKILLS_DIR", tmp_path / "opencode")
|
|
262
|
+
monkeypatch.setattr(setup, "AGENTS_SKILLS_DIR", tmp_path / "agents")
|
|
263
|
+
(plugins / "tl-cli" / ".version").write_text("0.1.0")
|
|
264
|
+
|
|
265
|
+
assert any("0.1.0" in w for w in setup.check_plugin_version())
|
|
266
|
+
|
|
267
|
+
def test_silent_when_nothing_installed(self, tmp_path, monkeypatch):
|
|
268
|
+
monkeypatch.setattr(setup, "CLAUDE_PLUGINS_DIR", tmp_path / "plugins")
|
|
269
|
+
monkeypatch.setattr(setup, "OPENCODE_SKILLS_DIR", tmp_path / "opencode")
|
|
270
|
+
monkeypatch.setattr(setup, "AGENTS_SKILLS_DIR", tmp_path / "agents")
|
|
271
|
+
assert setup.check_plugin_version() == []
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestUpdatePlugin:
|
|
275
|
+
def test_already_latest_is_not_a_change(self, monkeypatch):
|
|
276
|
+
monkeypatch.setattr(setup, "_run_claude", lambda args, b: (True, 'tl-cli is already at the latest version (0.9.9).'))
|
|
277
|
+
assert _update_plugin("claude") == (True, False, 'tl-cli is already at the latest version (0.9.9).')
|
|
278
|
+
|
|
279
|
+
def test_version_advance_is_a_change(self, monkeypatch):
|
|
280
|
+
monkeypatch.setattr(setup, "_run_claude", lambda args, b: (True, 'Plugin "tl-cli" updated from 0.7.2 to 0.9.9.'))
|
|
281
|
+
ok, changed, _ = _update_plugin("claude")
|
|
282
|
+
assert (ok, changed) == (True, True)
|
|
283
|
+
|
|
284
|
+
def test_failure_is_not_a_change(self, monkeypatch):
|
|
285
|
+
monkeypatch.setattr(setup, "_run_claude", lambda args, b: (False, "boom"))
|
|
286
|
+
assert _update_plugin("claude") == (False, False, "boom")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class TestSetupCallSequence:
|
|
290
|
+
def _stub_env(self, tmp_path, monkeypatch, record=None):
|
|
291
|
+
plugin_root = tmp_path / "plugin"
|
|
292
|
+
(plugin_root / "skills").mkdir(parents=True)
|
|
293
|
+
plugins = tmp_path / "plugins"
|
|
294
|
+
plugins.mkdir()
|
|
295
|
+
if record is not None:
|
|
296
|
+
(plugins / "installed_plugins.json").write_text(json.dumps(record), encoding="utf-8")
|
|
297
|
+
monkeypatch.setattr(setup, "_find_plugin_root", lambda: plugin_root)
|
|
298
|
+
monkeypatch.setattr(setup, "_find_claude_binary", lambda: "/usr/bin/claude")
|
|
299
|
+
monkeypatch.setattr(setup, "CLAUDE_PLUGINS_DIR", plugins)
|
|
300
|
+
monkeypatch.setattr(setup, "CLAUDE_SKILLS_DIR", tmp_path / "skills")
|
|
301
|
+
monkeypatch.setattr(setup, "CLAUDE_COMMANDS_DIR", tmp_path / "commands")
|
|
302
|
+
calls = []
|
|
303
|
+
monkeypatch.setattr(setup, "_run_claude", lambda args, b: (calls.append(args), (True, "ok"))[1])
|
|
304
|
+
return calls, plugins
|
|
305
|
+
|
|
306
|
+
def test_interactive_updates_after_install(self, tmp_path, monkeypatch):
|
|
307
|
+
calls, _ = self._stub_env(tmp_path, monkeypatch)
|
|
308
|
+
setup.setup_claude(json_output=False, toon_output=False)
|
|
309
|
+
assert calls.index(["plugin", "update", setup.PLUGIN_KEY]) > calls.index(["plugin", "install", setup.PLUGIN_KEY])
|
|
310
|
+
|
|
311
|
+
def test_noninteractive_updates_after_install(self, tmp_path, monkeypatch, capsys):
|
|
312
|
+
calls, _ = self._stub_env(tmp_path, monkeypatch)
|
|
313
|
+
setup._setup_noninteractive()
|
|
314
|
+
assert calls.index(["plugin", "update", setup.PLUGIN_KEY]) > calls.index(["plugin", "install", setup.PLUGIN_KEY])
|
|
315
|
+
assert json.loads(capsys.readouterr().out)["plugin_updated"] is True
|
|
316
|
+
|
|
317
|
+
def test_stamp_records_installed_version_not_cli_version(self, tmp_path, monkeypatch, capsys):
|
|
318
|
+
record = {"plugins": {setup.PLUGIN_KEY: [{"scope": "user", "version": "0.7.2"}]}}
|
|
319
|
+
_, plugins = self._stub_env(tmp_path, monkeypatch, record=record)
|
|
320
|
+
setup._setup_noninteractive()
|
|
321
|
+
assert (plugins / "tl-cli" / ".version").read_text() == "0.7.2"
|
|
322
|
+
assert json.loads(capsys.readouterr().out)["plugin_version"] == "0.7.2"
|
|
323
|
+
|
|
324
|
+
def test_standalone_fallback_stamps_cli_version(self, tmp_path, monkeypatch, capsys):
|
|
325
|
+
_, plugins = self._stub_env(tmp_path, monkeypatch)
|
|
326
|
+
monkeypatch.setattr(setup, "_find_claude_binary", lambda: None)
|
|
327
|
+
setup._setup_noninteractive()
|
|
328
|
+
assert (plugins / "tl-cli" / ".version").read_text() == setup.__version__
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
# Fast, hermetic checks on every push to main and every PR. These run the
|
|
4
|
-
# mocked unit suite under tests/ — no live API, no credits, fully
|
|
5
|
-
# deterministic. The live CLI tests live in cli-integration.yml.
|
|
6
|
-
|
|
7
|
-
on:
|
|
8
|
-
push:
|
|
9
|
-
branches: [main]
|
|
10
|
-
pull_request:
|
|
11
|
-
|
|
12
|
-
permissions:
|
|
13
|
-
contents: read
|
|
14
|
-
|
|
15
|
-
concurrency:
|
|
16
|
-
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
17
|
-
cancel-in-progress: true
|
|
18
|
-
|
|
19
|
-
jobs:
|
|
20
|
-
unit-tests:
|
|
21
|
-
name: Unit tests (mocked) · py${{ matrix.python-version }}
|
|
22
|
-
runs-on: ubuntu-latest
|
|
23
|
-
strategy:
|
|
24
|
-
fail-fast: false
|
|
25
|
-
matrix:
|
|
26
|
-
# Matches `requires-python = ">=3.12"`.
|
|
27
|
-
python-version: ["3.12", "3.13", "3.14"]
|
|
28
|
-
steps:
|
|
29
|
-
- uses: actions/checkout@v7
|
|
30
|
-
- uses: astral-sh/setup-uv@v7
|
|
31
|
-
with:
|
|
32
|
-
enable-cache: true
|
|
33
|
-
- name: Sync locked dependencies
|
|
34
|
-
run: uv sync --frozen --python ${{ matrix.python-version }}
|
|
35
|
-
- name: Run unit tests
|
|
36
|
-
# tests/ mock the HTTP client (CliRunner + fake get_client), so there
|
|
37
|
-
# is no network, no API key, and no credit spend.
|
|
38
|
-
run: uv run --python ${{ matrix.python-version }} --with pytest pytest tests/ -v
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/.github/workflows/cli-integration.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.9.8 → thoughtleaders_cli-0.9.10}/agents/keyword-relevance-validator.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|