luckiest-co 1.0.46 → 1.0.48
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/ab-testing/CHANGELOG.md +5 -0
- package/skills/ab-testing/SKILL.md +12 -9
- package/skills/ab-testing/evals/evals.json +18 -7
- package/skills/ab-testing/references/sample-size-guide.md +2 -2
- package/skills/ads/CHANGELOG.md +5 -0
- package/skills/ads/SKILL.md +4 -4
- package/skills/ai-seo/CHANGELOG.md +7 -0
- package/skills/ai-seo/SKILL.md +27 -20
- package/skills/analytics/CHANGELOG.md +6 -0
- package/skills/analytics/SKILL.md +4 -3
- package/skills/analytics/evals/evals.json +27 -0
- package/skills/analytics/references/ga4-implementation.md +7 -3
- package/skills/analytics/references/gtm-implementation.md +8 -2
- package/skills/aso/CHANGELOG.md +5 -0
- package/skills/aso/SKILL.md +4 -4
- package/skills/aso/evals/evals.json +15 -2
- package/skills/aso/references/benchmarks.md +4 -2
- package/skills/aso/references/google-play-specs.md +5 -1
- package/skills/brainstorm/SKILL.md +59 -16
- package/skills/brainstorm/builder-check-prompt.md +23 -0
- package/skills/churn-prevention/CHANGELOG.md +5 -0
- package/skills/churn-prevention/SKILL.md +3 -3
- package/skills/churn-prevention/evals/evals.json +14 -0
- package/skills/churn-prevention/references/dunning-playbook.md +6 -1
- package/skills/coder-brainstorming/CHANGELOG.md +10 -0
- package/skills/coder-brainstorming/SKILL.md +62 -20
- package/skills/coder-brainstorming/builder-check-prompt.md +23 -0
- package/skills/coder-brainstorming/scripts/server.cjs +1 -1
- package/skills/coder-debugging/CHANGELOG.md +4 -0
- package/skills/coder-debugging/SKILL.md +5 -5
- package/skills/coder-requesting-code-review/CHANGELOG.md +4 -0
- package/skills/coder-requesting-code-review/SKILL.md +4 -4
- package/skills/coder-subagent-driven-development/CHANGELOG.md +4 -0
- package/skills/coder-subagent-driven-development/SKILL.md +3 -3
- package/skills/coder-subagent-driven-development/scripts/sdd-workspace +3 -1
- package/skills/cold-email/CHANGELOG.md +7 -0
- package/skills/cold-email/SKILL.md +27 -7
- package/skills/cold-email/evals/evals.json +30 -0
- package/skills/cold-email/references/benchmarks.md +15 -11
- package/skills/cold-email/references/deliverability.md +110 -0
- package/skills/cold-email/references/linkedin-outreach.md +91 -0
- package/skills/cold-email/references/multichannel-cadence.md +79 -0
- package/skills/cold-email/references/outbound-plays.md +88 -0
- package/skills/cold-email/references/personalization.md +11 -0
- package/skills/cold-email/references/reply-handling.md +65 -0
- package/skills/competitor-profiling/CHANGELOG.md +5 -0
- package/skills/competitor-profiling/SKILL.md +4 -4
- package/skills/content-strategy/CHANGELOG.md +5 -0
- package/skills/content-strategy/SKILL.md +5 -3
- package/skills/copy-editing/CHANGELOG.md +5 -0
- package/skills/copy-editing/SKILL.md +5 -5
- package/skills/copy-editing/references/content-refresh.md +33 -0
- package/skills/cro/CHANGELOG.md +5 -0
- package/skills/cro/SKILL.md +14 -3
- package/skills/cro/references/form.md +1 -1
- package/skills/directory-submissions/CHANGELOG.md +5 -0
- package/skills/directory-submissions/SKILL.md +13 -4
- package/skills/directory-submissions/evals/evals.json +15 -1
- package/skills/directory-submissions/references/directory-list.md +2 -4
- package/skills/emails/CHANGELOG.md +5 -0
- package/skills/emails/SKILL.md +14 -4
- package/skills/launch/CHANGELOG.md +7 -0
- package/skills/launch/SKILL.md +8 -4
- package/skills/launch/evals/evals.json +29 -0
- package/skills/launch/references/shipped-changes.md +84 -0
- package/skills/launch/references/site-launch-qa.md +55 -0
- package/skills/lead-magnets/CHANGELOG.md +5 -0
- package/skills/lead-magnets/SKILL.md +6 -4
- package/skills/lead-magnets/evals/evals.json +18 -0
- package/skills/lead-magnets/references/content-to-skill-corpus-design.md +89 -0
- package/skills/lead-magnets/references/content-to-skill-release.md +71 -0
- package/skills/lead-magnets/references/content-to-skill.md +108 -0
- package/skills/marketing-plan/CHANGELOG.md +5 -0
- package/skills/marketing-plan/SKILL.md +3 -3
- package/skills/marketing-plan/evals/evals.json +13 -0
- package/skills/marketing-plan/references/budget-planning.md +26 -17
- package/skills/pricing/CHANGELOG.md +5 -0
- package/skills/pricing/SKILL.md +4 -4
- package/skills/pricing/evals/evals.json +12 -0
- package/skills/pricing/references/research-methods.md +8 -4
- package/skills/programmatic-seo/CHANGELOG.md +5 -0
- package/skills/programmatic-seo/SKILL.md +8 -4
- package/skills/programmatic-seo/evals/evals.json +13 -0
- package/skills/programmatic-seo/references/implementation-platforms.md +37 -0
- package/skills/prospecting/CHANGELOG.md +7 -0
- package/skills/prospecting/SKILL.md +27 -13
- package/skills/prospecting/evals/evals.json +41 -0
- package/skills/prospecting/references/account-research.md +59 -0
- package/skills/prospecting/references/b2b-prospecting.md +1 -1
- package/skills/prospecting/references/compliance.md +44 -6
- package/skills/prospecting/references/data-sources.md +13 -15
- package/skills/prospecting/references/enrichment-playbook.md +61 -0
- package/skills/prospecting/references/saas-prospecting.md +1 -1
- package/skills/prospecting/references/signal-plays.md +99 -0
- package/skills/prospecting/references/sourcing-and-enrichment.md +89 -0
- package/skills/referrals/CHANGELOG.md +5 -0
- package/skills/referrals/SKILL.md +10 -4
- package/skills/referrals/evals/evals.json +99 -0
- package/skills/referrals/references/affiliate-operations.md +64 -0
- package/skills/referrals/references/affiliate-programs.md +2 -2
- package/skills/referrals/references/program-examples.md +14 -6
- package/skills/referrals/references/review-platform-rules.md +36 -0
- package/skills/referrals/references/review-program-operations.md +62 -0
- package/skills/referrals/references/review-programs.md +96 -0
- package/skills/revops/CHANGELOG.md +5 -0
- package/skills/revops/SKILL.md +10 -6
- package/skills/revops/evals/evals.json +13 -0
- package/skills/revops/references/automation-playbooks.md +1 -1
- package/skills/revops/references/inbound-routing.md +43 -0
- package/skills/revops/references/lifecycle-definitions.md +31 -0
- package/skills/sales-enablement/CHANGELOG.md +5 -0
- package/skills/sales-enablement/SKILL.md +6 -4
- package/skills/sales-enablement/evals/evals.json +14 -0
- package/skills/sales-enablement/references/cold-call-scripts.md +68 -0
- package/skills/schema/CHANGELOG.md +5 -0
- package/skills/schema/SKILL.md +20 -8
- package/skills/schema/evals/evals.json +40 -0
- package/skills/schema/references/schema-examples.md +9 -15
- package/skills/seo-audit/CHANGELOG.md +5 -0
- package/skills/seo-audit/SKILL.md +10 -16
- package/skills/seo-audit/evals/evals.json +68 -0
- package/skills/seo-audit/references/international-seo.md +9 -11
- package/skills/seo-audit/references/local-seo-pages-and-measurement.md +39 -0
- package/skills/seo-audit/references/local-seo-profile-and-citations.md +41 -0
- package/skills/seo-audit/references/local-seo.md +86 -0
- package/skills/seo-audit/references/rankings-push.md +62 -0
- package/skills/seo-audit/references/title-tags.md +89 -0
- package/skills/signup/CHANGELOG.md +5 -0
- package/skills/signup/SKILL.md +7 -3
- package/skills/site-architecture/CHANGELOG.md +5 -0
- package/skills/site-architecture/SKILL.md +5 -4
- package/skills/site-architecture/evals/evals.json +25 -0
- package/skills/site-architecture/references/platforms-and-migration.md +84 -0
- package/skills/social/CHANGELOG.md +5 -0
- package/skills/social/SKILL.md +4 -4
- package/skills/trends/.skillignore +3 -0
- package/skills/trends/CHANGELOG.md +20 -0
- package/skills/trends/SKILL.md +12 -8
- package/skills/trends/references/agent-mode.md +36 -0
- package/skills/trends/references/category-peers.md +38 -0
- package/skills/trends/references/comparison.md +227 -0
- package/skills/trends/references/competitors.md +29 -0
- package/skills/trends/references/discovery.md +90 -0
- package/skills/trends/references/engine-contract.md +111 -1889
- package/skills/trends/references/followups.md +112 -0
- package/skills/trends/references/grok-bot-x.md +55 -0
- package/skills/trends/references/hiring-signals.md +19 -0
- package/skills/trends/references/library-queue.md +33 -0
- package/skills/trends/references/recommendations.md +86 -0
- package/skills/trends/references/research-runbook.md +781 -0
- package/skills/trends/references/runtime.md +165 -0
- package/skills/trends/references/save-html-brief.md +39 -20
- package/skills/trends/references/setup-wizard.md +253 -0
- package/skills/trends/references/synthesis.md +289 -0
- package/skills/trends/scripts/box_chrome_login.py +209 -0
- package/skills/trends/scripts/briefing.py +9 -17
- package/skills/trends/scripts/last30days.py +3247 -280
- package/skills/trends/scripts/lib/agentcookie.py +196 -0
- package/skills/trends/scripts/lib/amazon.py +877 -0
- package/skills/trends/scripts/lib/arxiv.py +42 -8
- package/skills/trends/scripts/lib/backends.py +769 -0
- package/skills/trends/scripts/lib/bird_x.py +340 -57
- package/skills/trends/scripts/lib/bluesky.py +84 -10
- package/skills/trends/scripts/lib/bounded_get.py +156 -0
- package/skills/trends/scripts/lib/brightdata.py +291 -0
- package/skills/trends/scripts/lib/categories.py +7 -1
- package/skills/trends/scripts/lib/chrome_cdp.py +455 -0
- package/skills/trends/scripts/lib/chrome_cookies.py +132 -39
- package/skills/trends/scripts/lib/cluster.py +31 -54
- package/skills/trends/scripts/lib/competitors.py +10 -1
- package/skills/trends/scripts/lib/cookie_extract.py +221 -44
- package/skills/trends/scripts/lib/cookie_paths.py +27 -0
- package/skills/trends/scripts/lib/corpus.py +439 -0
- package/skills/trends/scripts/lib/devto.py +313 -0
- package/skills/trends/scripts/lib/discovery_handoff.py +988 -0
- package/skills/trends/scripts/lib/doctor.py +2166 -0
- package/skills/trends/scripts/lib/dripstack.py +203 -0
- package/skills/trends/scripts/lib/entity_extract.py +37 -0
- package/skills/trends/scripts/lib/env.py +1092 -138
- package/skills/trends/scripts/lib/fanout.py +11 -1
- package/skills/trends/scripts/lib/feed.py +61 -0
- package/skills/trends/scripts/lib/fetch_memo.py +135 -0
- package/skills/trends/scripts/lib/freshness.py +566 -0
- package/skills/trends/scripts/lib/fusion.py +314 -28
- package/skills/trends/scripts/lib/github.py +438 -24
- package/skills/trends/scripts/lib/grok_x.py +1237 -0
- package/skills/trends/scripts/lib/grounding.py +129 -11
- package/skills/trends/scripts/lib/hackernews.py +61 -4
- package/skills/trends/scripts/lib/health.py +373 -0
- package/skills/trends/scripts/lib/hosted.py +333 -0
- package/skills/trends/scripts/lib/html_publish.py +34 -0
- package/skills/trends/scripts/lib/html_render.py +169 -3
- package/skills/trends/scripts/lib/http.py +1215 -70
- package/skills/trends/scripts/lib/instagram.py +132 -9
- package/skills/trends/scripts/lib/jobs.py +4 -2
- package/skills/trends/scripts/lib/library.py +308 -0
- package/skills/trends/scripts/lib/library_index.py +477 -0
- package/skills/trends/scripts/lib/linkedin.py +271 -24
- package/skills/trends/scripts/lib/log.py +5 -2
- package/skills/trends/scripts/lib/meta_ads.py +982 -0
- package/skills/trends/scripts/lib/normalize.py +382 -28
- package/skills/trends/scripts/lib/parallel_mcp.py +234 -0
- package/skills/trends/scripts/lib/permission_preflight.py +42 -3
- package/skills/trends/scripts/lib/perplexity.py +1080 -468
- package/skills/trends/scripts/lib/pipeline.py +5079 -211
- package/skills/trends/scripts/lib/planner.py +396 -53
- package/skills/trends/scripts/lib/polymarket.py +285 -23
- package/skills/trends/scripts/lib/prescriptions.py +432 -0
- package/skills/trends/scripts/lib/private_files.py +21 -0
- package/skills/trends/scripts/lib/providers.py +231 -10
- package/skills/trends/scripts/lib/quality_nudge.py +122 -37
- package/skills/trends/scripts/lib/reddit.py +177 -12
- package/skills/trends/scripts/lib/reddit_arctic.py +146 -5
- package/skills/trends/scripts/lib/reddit_keyless.py +121 -60
- package/skills/trends/scripts/lib/reddit_listing.py +215 -14
- package/skills/trends/scripts/lib/reddit_public.py +12 -13
- package/skills/trends/scripts/lib/reddit_search.py +304 -0
- package/skills/trends/scripts/lib/reddit_shreddit.py +51 -7
- package/skills/trends/scripts/lib/registers.py +134 -0
- package/skills/trends/scripts/lib/relevance.py +17 -0
- package/skills/trends/scripts/lib/render.py +2161 -271
- package/skills/trends/scripts/lib/rerank.py +623 -55
- package/skills/trends/scripts/lib/resolve.py +50 -1
- package/skills/trends/scripts/lib/safari_cookies.py +42 -15
- package/skills/trends/scripts/lib/schema.py +782 -1
- package/skills/trends/scripts/lib/setup_wizard.py +779 -120
- package/skills/trends/scripts/lib/signals.py +206 -7
- package/skills/trends/scripts/lib/stocktwits.py +374 -0
- package/skills/trends/scripts/lib/subproc.py +307 -53
- package/skills/trends/scripts/lib/techmeme.py +89 -64
- package/skills/trends/scripts/lib/telegram.py +317 -0
- package/skills/trends/scripts/lib/tiktok.py +13 -9
- package/skills/trends/scripts/lib/topic_shape.py +340 -0
- package/skills/trends/scripts/lib/transcribe.py +12 -3
- package/skills/trends/scripts/lib/trustpilot.py +256 -5
- package/skills/trends/scripts/lib/truthsocial.py +9 -1
- package/skills/trends/scripts/lib/ui.py +6 -0
- package/skills/trends/scripts/lib/usage.py +132 -0
- package/skills/trends/scripts/lib/web_search_keyless.py +63 -4
- package/skills/trends/scripts/lib/windows_job.py +138 -0
- package/skills/trends/scripts/lib/x_api.py +824 -0
- package/skills/trends/scripts/lib/x_envelope.py +733 -0
- package/skills/trends/scripts/lib/x_judge.py +292 -0
- package/skills/trends/scripts/lib/xai_x.py +26 -3
- package/skills/trends/scripts/lib/xquik.py +89 -15
- package/skills/trends/scripts/lib/xurl_x.py +284 -91
- package/skills/trends/scripts/lib/youtube_yt.py +570 -119
- package/skills/trends/scripts/setup-keychain.sh +12 -4
- package/skills/trends/scripts/setup-pass.sh +3 -0
- package/skills/trends/scripts/store.py +444 -50
- package/skills/trends/scripts/watchlist.py +38 -11
- package/skills/trends/scripts/x_lane_parity.py +107 -0
- package/skills/video-studio/CHANGELOG.md +8 -0
- package/skills/video-studio/SKILL.md +6 -5
- package/skills/video-studio/references/styles/product-flight.md +45 -0
- package/skills/video-studio/references/styles.md +3 -1
- package/skills/video-studio/scripts/qa.mjs +64 -2
- package/skills/video-studio/scripts/qa.test.mjs +29 -1
- package/skills/video-studio-motion/ATTRIBUTION.md +1 -1
- package/skills/video-studio-motion/CHANGELOG.md +4 -0
- package/skills/video-studio-motion/SKILL.md +8 -5
- package/skills/coder-brainstorming/spec-document-reviewer-prompt.md +0 -49
- package/skills/trends/scripts/lib/reddit_rss.py +0 -224
|
@@ -13,9 +13,15 @@ doomed *sources/tools*.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import os
|
|
16
17
|
import shutil
|
|
17
18
|
import subprocess
|
|
19
|
+
import time
|
|
18
20
|
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Dict, Iterable, List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
from . import subproc
|
|
19
25
|
|
|
20
26
|
# Health states, best to worst.
|
|
21
27
|
OK = "ok"
|
|
@@ -25,6 +31,27 @@ BROKEN = "broken" # present but won't execute (stale shim, bad perms)
|
|
|
25
31
|
TIMEOUT = "timeout" # exceeded the probe deadline
|
|
26
32
|
ERROR = "error" # ran and failed for another reason
|
|
27
33
|
|
|
34
|
+
# Per-run outcomes. Doctor does not emit these: it predicts source readiness
|
|
35
|
+
# before retrieval, while Report.source_status records what happened in one run.
|
|
36
|
+
NO_RESULTS = "no-results"
|
|
37
|
+
PARTIAL = "partial"
|
|
38
|
+
RATE_LIMITED = "rate-limited"
|
|
39
|
+
AUTH_FAILED = "auth-failed"
|
|
40
|
+
PAYMENT_REQUIRED = "payment-required" # HTTP 402 / credits exhausted: top up, not re-login
|
|
41
|
+
UNREACHABLE = "unreachable"
|
|
42
|
+
SCHEMA_DRIFT = "schema-drift"
|
|
43
|
+
SKIPPED_UNCONFIGURED = "skipped-unconfigured"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def credits_exhausted_label(source: str) -> str:
|
|
47
|
+
"""Human label for a ``PAYMENT_REQUIRED`` outcome on ``source``.
|
|
48
|
+
|
|
49
|
+
The X source names the API whose credits ran out; every other source
|
|
50
|
+
gets the generic phrasing so the same label serves render summaries and
|
|
51
|
+
the doctor post-mortem without each surface hand-writing its own.
|
|
52
|
+
"""
|
|
53
|
+
return "X API credits exhausted" if source == "x" else "credits exhausted"
|
|
54
|
+
|
|
28
55
|
|
|
29
56
|
@dataclass
|
|
30
57
|
class SourceHealth:
|
|
@@ -90,3 +117,349 @@ def probe_command(
|
|
|
90
117
|
detail = (proc.stderr or proc.stdout or "").strip().splitlines()
|
|
91
118
|
first = detail[0] if detail else f"exit {proc.returncode}"
|
|
92
119
|
return SourceHealth(name=name, state=ERROR, reason=f"{name}: {first}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Dependency probes (doctor command, issue #692).
|
|
124
|
+
#
|
|
125
|
+
# ``probe_dependency`` generalizes ``probe_command`` for the skill's external
|
|
126
|
+
# binaries (yt-dlp, Printing Press CLIs, node for the vendored bird client,
|
|
127
|
+
# ffmpeg). It answers three questions the bare shutil.which gate cannot:
|
|
128
|
+
# - Is the binary genuinely runnable (a stale shim that resolves on PATH but
|
|
129
|
+
# cannot exec is BROKEN, not available)?
|
|
130
|
+
# - If not, WHICH fix applies (install vs reinstall vs a PATH edit), keyed to
|
|
131
|
+
# the package manager that owns the binary on this machine?
|
|
132
|
+
# - Is an on-disk binary merely off the agent-subprocess PATH (the Digg
|
|
133
|
+
# ~/.local/bin case) — MISSING with a PATH-fix, never "installed"?
|
|
134
|
+
#
|
|
135
|
+
# Semantics follow the engine gate: availability means PATH-resolvable in THIS
|
|
136
|
+
# process, not present-on-disk. Probes are one short-timeout version exec each
|
|
137
|
+
# and memoized per process, so doctor and setup can consult them freely.
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
# Per-probe budget in seconds: a healthy --version exec is near-instant, so a
|
|
141
|
+
# slow probe is itself a diagnostic (network-mounted shim, hung interpreter).
|
|
142
|
+
PROBE_TIMEOUT = 5.0
|
|
143
|
+
|
|
144
|
+
_PP_CLI_SUFFIX = "-pp-cli"
|
|
145
|
+
# Matches setup_wizard.PRINTING_PRESS_NPM (pinned catalog installer).
|
|
146
|
+
_PRINTING_PRESS_NPM = "@mvanhorn/printing-press-library@0.1.16"
|
|
147
|
+
|
|
148
|
+
# Dependencies the doctor probes by default.
|
|
149
|
+
KNOWN_DEPENDENCIES: Tuple[str, ...] = ("yt-dlp", "digg-pp-cli", "node", "ffmpeg", "grok")
|
|
150
|
+
|
|
151
|
+
# Cheap side-effect-free version invocation per dependency (default --version).
|
|
152
|
+
_VERSION_ARGS: Dict[str, List[str]] = {
|
|
153
|
+
"ffmpeg": ["-version"],
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Package managers each dependency may be owned by, in preference order, and
|
|
157
|
+
# the (install, reinstall) prescription for each. "reinstall" wording matters:
|
|
158
|
+
# a BROKEN binary is present, so telling the user to "install" it reads as a
|
|
159
|
+
# no-op ("it's already installed") — the stale-shim trap this module exists
|
|
160
|
+
# to name.
|
|
161
|
+
_MANAGER_PRESCRIPTIONS: Dict[str, Dict[str, Tuple[str, str]]] = {
|
|
162
|
+
"yt-dlp": {
|
|
163
|
+
"brew": ("brew install yt-dlp", "brew reinstall yt-dlp"),
|
|
164
|
+
"pipx": ("pipx install yt-dlp", "pipx reinstall yt-dlp"),
|
|
165
|
+
},
|
|
166
|
+
"node": {
|
|
167
|
+
"brew": ("brew install node", "brew reinstall node"),
|
|
168
|
+
"nvm": ("nvm install --lts", "reinstall node via nvm: nvm install --lts && nvm use --lts"),
|
|
169
|
+
},
|
|
170
|
+
"ffmpeg": {
|
|
171
|
+
"brew": ("brew install ffmpeg", "brew reinstall ffmpeg"),
|
|
172
|
+
"apt": ("sudo apt-get install -y ffmpeg", "sudo apt-get install -y --reinstall ffmpeg"),
|
|
173
|
+
},
|
|
174
|
+
# The official installer is the primary path; npm is a real alternative
|
|
175
|
+
# (the package is published as @xai-official/grok) and fits the existing
|
|
176
|
+
# manager-preference machinery.
|
|
177
|
+
"grok": {
|
|
178
|
+
"npm": (
|
|
179
|
+
"npm install -g @xai-official/grok",
|
|
180
|
+
"reinstall the Grok CLI: npm install -g @xai-official/grok@latest",
|
|
181
|
+
),
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Last-resort prescriptions when no known package manager is detected.
|
|
186
|
+
_FALLBACK_PRESCRIPTIONS: Dict[str, Tuple[str, str]] = {
|
|
187
|
+
"yt-dlp": (
|
|
188
|
+
"install yt-dlp (https://github.com/yt-dlp/yt-dlp#installation) and ensure it is on PATH",
|
|
189
|
+
"reinstall yt-dlp (https://github.com/yt-dlp/yt-dlp#installation); the current binary won't run",
|
|
190
|
+
),
|
|
191
|
+
"node": (
|
|
192
|
+
"install Node.js 22+ (https://nodejs.org) and ensure `node` is on PATH",
|
|
193
|
+
"reinstall Node.js 22+ (https://nodejs.org); the current binary won't run",
|
|
194
|
+
),
|
|
195
|
+
"ffmpeg": (
|
|
196
|
+
"install ffmpeg (https://ffmpeg.org/download.html) and ensure it is on PATH",
|
|
197
|
+
"reinstall ffmpeg (https://ffmpeg.org/download.html); the current binary won't run",
|
|
198
|
+
),
|
|
199
|
+
"grok": (
|
|
200
|
+
"install the Grok CLI: curl -fsSL https://x.ai/cli/install.sh | bash, then run `grok login`",
|
|
201
|
+
"reinstall the Grok CLI: curl -fsSL https://x.ai/cli/install.sh | bash; the current binary won't run",
|
|
202
|
+
),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class DependencyProbe:
|
|
208
|
+
"""Uniform probe result for one external dependency.
|
|
209
|
+
|
|
210
|
+
``status`` is one of the module-level constants (OK/MISSING/BROKEN/TIMEOUT).
|
|
211
|
+
``detail`` says what was observed (version string, exec error, off-PATH
|
|
212
|
+
location). ``prescription`` is the copy-pasteable fix, empty when OK.
|
|
213
|
+
``owner_pkg_manager`` names the manager the prescription targets
|
|
214
|
+
("brew", "pipx", "apt", "nvm", "npx"), or "" for PATH fixes / fallbacks.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
name: str
|
|
218
|
+
status: str
|
|
219
|
+
detail: str = ""
|
|
220
|
+
prescription: str = ""
|
|
221
|
+
owner_pkg_manager: str = ""
|
|
222
|
+
# True for the on-disk-but-off-PATH case: MISSING (the engine gate would
|
|
223
|
+
# not pass) but the fix is a PATH edit, not an install.
|
|
224
|
+
off_path: bool = False
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def ok(self) -> bool:
|
|
228
|
+
return self.status == OK
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# Safe under the GIL (dict get/set are atomic) and each dependency name is
|
|
232
|
+
# probed from a single builder today; worst case is one redundant probe.
|
|
233
|
+
_dependency_probe_cache: Dict[str, DependencyProbe] = {}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def clear_dependency_probe_cache() -> None:
|
|
237
|
+
"""Reset memoized probes (tests, or a doctor re-run after a fix)."""
|
|
238
|
+
_dependency_probe_cache.clear()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _nvm_present() -> bool:
|
|
242
|
+
return bool(os.environ.get("NVM_DIR")) or (Path.home() / ".nvm").is_dir()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _manager_available(manager: str) -> bool:
|
|
246
|
+
if manager == "nvm":
|
|
247
|
+
return _nvm_present()
|
|
248
|
+
if manager == "apt":
|
|
249
|
+
return shutil.which("apt-get") is not None
|
|
250
|
+
return shutil.which(manager) is not None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _is_pp_cli(name: str) -> bool:
|
|
254
|
+
return name.endswith(_PP_CLI_SUFFIX) and len(name) > len(_PP_CLI_SUFFIX)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _pp_install_cmd(name: str) -> str:
|
|
258
|
+
slug = name[: -len(_PP_CLI_SUFFIX)]
|
|
259
|
+
return f"npx -y {_PRINTING_PRESS_NPM} install {slug} --cli-only"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def pp_install_cmd(slug: str) -> str:
|
|
263
|
+
"""Public catalog-install command for the Printing Press CLI ``<slug>-pp-cli``."""
|
|
264
|
+
return _pp_install_cmd(f"{slug}{_PP_CLI_SUFFIX}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def static_prescription(name: str, manager: str) -> Tuple[str, str]:
|
|
268
|
+
"""Public ``(install, reinstall)`` strings for one dependency/manager pair.
|
|
269
|
+
|
|
270
|
+
Reads the static table without probing manager availability; raises
|
|
271
|
+
KeyError for unknown pairs so consumers fail loudly at import time.
|
|
272
|
+
"""
|
|
273
|
+
return _MANAGER_PRESCRIPTIONS[name][manager]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _prescription(name: str, kind: str) -> Tuple[str, str]:
|
|
277
|
+
"""Return ``(prescription, owner_pkg_manager)`` for install/reinstall.
|
|
278
|
+
|
|
279
|
+
``kind`` is "install" (MISSING) or "reinstall" (BROKEN). Printing Press
|
|
280
|
+
CLIs always re-run the catalog installer; other deps pick the first
|
|
281
|
+
detected manager from their preference table, falling back to a generic
|
|
282
|
+
but still actionable instruction.
|
|
283
|
+
"""
|
|
284
|
+
idx = 0 if kind == "install" else 1
|
|
285
|
+
if _is_pp_cli(name):
|
|
286
|
+
cmd = _pp_install_cmd(name)
|
|
287
|
+
if kind == "reinstall":
|
|
288
|
+
return f"re-run the Printing Press install: {cmd}", "npx"
|
|
289
|
+
return cmd, "npx"
|
|
290
|
+
for manager, prescriptions in _MANAGER_PRESCRIPTIONS.get(name, {}).items():
|
|
291
|
+
if _manager_available(manager):
|
|
292
|
+
return prescriptions[idx], manager
|
|
293
|
+
fallback = _FALLBACK_PRESCRIPTIONS.get(name)
|
|
294
|
+
if fallback:
|
|
295
|
+
return fallback[idx], ""
|
|
296
|
+
verb = "install" if kind == "install" else "reinstall"
|
|
297
|
+
return f"{verb} {name} and ensure it is on PATH", ""
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def windows_printing_press_bin_dir() -> Optional[Path]:
|
|
301
|
+
"""Windows managed install dir for Printing Press CLIs, when applicable.
|
|
302
|
+
|
|
303
|
+
Returns ``%LOCALAPPDATA%/Programs/PrintingPress/bin`` on Windows when
|
|
304
|
+
LOCALAPPDATA is set; ``None`` otherwise.
|
|
305
|
+
"""
|
|
306
|
+
if os.name != "nt":
|
|
307
|
+
return None
|
|
308
|
+
local_app = os.environ.get("LOCALAPPDATA") or os.environ.get("LocalAppData")
|
|
309
|
+
if not local_app:
|
|
310
|
+
return None
|
|
311
|
+
return Path(local_app) / "Programs" / "PrintingPress" / "bin"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def installer_bin_dirs() -> List[Path]:
|
|
315
|
+
"""Installer-managed bin dirs shared with setup_wizard's Digg candidates.
|
|
316
|
+
|
|
317
|
+
Single source of truth for where installers drop binaries: the Printing
|
|
318
|
+
Press library default (~/.local/bin), Go bins, and — on Windows — the
|
|
319
|
+
managed %LOCALAPPDATA%/Programs/PrintingPress/bin dir.
|
|
320
|
+
``setup_wizard._digg_bin_candidate_paths`` derives its Digg-specific
|
|
321
|
+
paths from this list; keep the two in lockstep by editing only here.
|
|
322
|
+
"""
|
|
323
|
+
home = Path.home()
|
|
324
|
+
dirs = [home / ".local" / "bin"]
|
|
325
|
+
gopath = os.environ.get("GOPATH")
|
|
326
|
+
if gopath:
|
|
327
|
+
dirs.append(Path(gopath) / "bin")
|
|
328
|
+
dirs.append(home / "go" / "bin")
|
|
329
|
+
win_dir = windows_printing_press_bin_dir()
|
|
330
|
+
if win_dir is not None:
|
|
331
|
+
dirs.append(win_dir)
|
|
332
|
+
return dirs
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _off_path_candidate_dirs() -> List[Path]:
|
|
336
|
+
"""Directories where installers drop binaries that PATH may not cover.
|
|
337
|
+
|
|
338
|
+
The shared installer dirs (``installer_bin_dirs``, which also backs
|
|
339
|
+
setup_wizard's Digg candidates) plus the Homebrew prefixes (an agent
|
|
340
|
+
subprocess PATH sometimes omits even those).
|
|
341
|
+
"""
|
|
342
|
+
dirs = installer_bin_dirs()
|
|
343
|
+
dirs.extend([Path("/opt/homebrew/bin"), Path("/usr/local/bin")])
|
|
344
|
+
return dirs
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _off_path_binary(name: str) -> Optional[Path]:
|
|
348
|
+
"""Return an executable for ``name`` in a known dir that PATH misses."""
|
|
349
|
+
names = [name, f"{name}.exe"] if os.name == "nt" else [name]
|
|
350
|
+
for directory in _off_path_candidate_dirs():
|
|
351
|
+
for candidate_name in names:
|
|
352
|
+
candidate = directory / candidate_name
|
|
353
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
354
|
+
return candidate
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _path_hint(directory: Path) -> str:
|
|
359
|
+
"""Render a bin dir with $HOME substituted for copy-pasteable PATH edits."""
|
|
360
|
+
raw = str(directory)
|
|
361
|
+
if os.name == "nt":
|
|
362
|
+
return raw
|
|
363
|
+
home = str(Path.home())
|
|
364
|
+
if raw == home:
|
|
365
|
+
return "$HOME"
|
|
366
|
+
if raw.startswith(home + os.sep):
|
|
367
|
+
return "$HOME/" + raw[len(home) + 1:].replace(os.sep, "/")
|
|
368
|
+
return raw
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def probe_dependency(name: str, timeout: float = PROBE_TIMEOUT) -> DependencyProbe:
|
|
372
|
+
"""Probe one external dependency: OK | MISSING | BROKEN | TIMEOUT.
|
|
373
|
+
|
|
374
|
+
- MISSING: not resolvable on this process's PATH. If the binary exists in
|
|
375
|
+
a known install dir, the prescription is a PATH edit, not an install —
|
|
376
|
+
installing again would not fix anything.
|
|
377
|
+
- BROKEN: shutil.which resolves it but a cheap version exec fails
|
|
378
|
+
(OSError/exec-format, or any non-zero exit). Prescription says
|
|
379
|
+
*reinstall* — the #692 stale-shim class must never read as available.
|
|
380
|
+
- TIMEOUT: the version exec exceeded the per-probe budget.
|
|
381
|
+
- OK: version exec exited 0; ``detail`` carries the version line.
|
|
382
|
+
|
|
383
|
+
Memoized per process; ``clear_dependency_probe_cache()`` resets.
|
|
384
|
+
"""
|
|
385
|
+
cached = _dependency_probe_cache.get(name)
|
|
386
|
+
if cached is not None:
|
|
387
|
+
return cached
|
|
388
|
+
probe = _probe_dependency_uncached(name, timeout)
|
|
389
|
+
_dependency_probe_cache[name] = probe
|
|
390
|
+
return probe
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _probe_dependency_uncached(name: str, timeout: float) -> DependencyProbe:
|
|
394
|
+
deadline = time.monotonic() + timeout
|
|
395
|
+
resolved = shutil.which(name)
|
|
396
|
+
if resolved is None:
|
|
397
|
+
off_path = _off_path_binary(name)
|
|
398
|
+
if off_path is not None:
|
|
399
|
+
hint = _path_hint(off_path.parent)
|
|
400
|
+
return DependencyProbe(
|
|
401
|
+
name=name,
|
|
402
|
+
status=MISSING,
|
|
403
|
+
detail=f"{name} is installed at {off_path} but that directory is not on this process's PATH",
|
|
404
|
+
prescription=f'add {hint} to PATH (e.g. export PATH="{hint}:$PATH") so {name} resolves',
|
|
405
|
+
owner_pkg_manager="",
|
|
406
|
+
off_path=True,
|
|
407
|
+
)
|
|
408
|
+
prescription, manager = _prescription(name, "install")
|
|
409
|
+
return DependencyProbe(
|
|
410
|
+
name=name,
|
|
411
|
+
status=MISSING,
|
|
412
|
+
detail=f"{name} not found on PATH",
|
|
413
|
+
prescription=prescription,
|
|
414
|
+
owner_pkg_manager=manager,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
command = [name] + _VERSION_ARGS.get(name, ["--version"])
|
|
418
|
+
try:
|
|
419
|
+
cleanup_grace = 0.1
|
|
420
|
+
proc = subproc.run_with_timeout(
|
|
421
|
+
command,
|
|
422
|
+
timeout=max(0, deadline - time.monotonic() - 2 * cleanup_grace),
|
|
423
|
+
deadline_monotonic=deadline - 2 * cleanup_grace,
|
|
424
|
+
cleanup_grace=cleanup_grace,
|
|
425
|
+
capture_limit_bytes=64 * 1024,
|
|
426
|
+
)
|
|
427
|
+
except (FileNotFoundError, OSError) as exc:
|
|
428
|
+
prescription, manager = _prescription(name, "reinstall")
|
|
429
|
+
return DependencyProbe(
|
|
430
|
+
name=name,
|
|
431
|
+
status=BROKEN,
|
|
432
|
+
detail=f"{name} resolves to {resolved} but won't execute: {exc}",
|
|
433
|
+
prescription=prescription,
|
|
434
|
+
owner_pkg_manager=manager,
|
|
435
|
+
)
|
|
436
|
+
except subproc.SubprocTimeout:
|
|
437
|
+
prescription, manager = _prescription(name, "reinstall")
|
|
438
|
+
return DependencyProbe(
|
|
439
|
+
name=name,
|
|
440
|
+
status=TIMEOUT,
|
|
441
|
+
detail=f"{name} version probe timed out after {timeout:g}s",
|
|
442
|
+
prescription=f"re-run doctor; if the timeout persists: {prescription}",
|
|
443
|
+
owner_pkg_manager=manager,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
if proc.returncode == 0:
|
|
447
|
+
lines = (proc.stdout or proc.stderr or "").strip().splitlines()
|
|
448
|
+
version = lines[0].strip() if lines else ""
|
|
449
|
+
return DependencyProbe(name=name, status=OK, detail=version)
|
|
450
|
+
|
|
451
|
+
lines = (proc.stderr or proc.stdout or "").strip().splitlines()
|
|
452
|
+
why = lines[0].strip() if lines else f"exit {proc.returncode}"
|
|
453
|
+
prescription, manager = _prescription(name, "reinstall")
|
|
454
|
+
return DependencyProbe(
|
|
455
|
+
name=name,
|
|
456
|
+
status=BROKEN,
|
|
457
|
+
detail=f"{name} resolves to {resolved} but the version probe failed: {why}",
|
|
458
|
+
prescription=prescription,
|
|
459
|
+
owner_pkg_manager=manager,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def probe_dependencies(names: Optional[Iterable[str]] = None) -> Dict[str, DependencyProbe]:
|
|
464
|
+
"""Probe every known dependency (or ``names``), memoized per process."""
|
|
465
|
+
return {name: probe_dependency(name) for name in (names or KNOWN_DEPENDENCIES)}
|