secondbrain-py 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brain/__init__.py +0 -0
- brain/__main__.py +18 -0
- brain/_capture_command.py +445 -0
- brain/_compose.py +52 -0
- brain/activity.py +206 -0
- brain/ask.py +631 -0
- brain/audio.py +591 -0
- brain/backfill/__init__.py +12 -0
- brain/backfill/search_extras.py +141 -0
- brain/backfill/source_rows.py +101 -0
- brain/bin/__init__.py +1 -0
- brain/bin/_launcher.py +107 -0
- brain/bin/down.py +8 -0
- brain/bin/launchd.py +268 -0
- brain/bin/monitor.py +570 -0
- brain/bin/rebuild.py +8 -0
- brain/bin/status.py +8 -0
- brain/bin/up.py +8 -0
- brain/brief.py +272 -0
- brain/capture.py +49 -0
- brain/chat.py +293 -0
- brain/cli.py +9760 -0
- brain/cli_claude.py +81 -0
- brain/cli_connect.py +285 -0
- brain/cli_demo.py +266 -0
- brain/config.py +1949 -0
- brain/connect.py +925 -0
- brain/db.py +540 -0
- brain/demo/__init__.py +452 -0
- brain/demo/corpus/manifest.json +403 -0
- brain/demo/embedder.py +74 -0
- brain/durations.py +84 -0
- brain/edit_session.py +156 -0
- brain/editor.py +67 -0
- brain/elicit/__init__.py +16 -0
- brain/elicit/detectors.py +250 -0
- brain/elicit/drafter.py +70 -0
- brain/elicit/queue.py +220 -0
- brain/elicit/schema.py +48 -0
- brain/elicit/session.py +445 -0
- brain/embedding_targets.py +54 -0
- brain/embeddings.py +424 -0
- brain/enrichment.py +808 -0
- brain/errors.py +357 -0
- brain/eval/__init__.py +129 -0
- brain/eval/answer_eval.py +281 -0
- brain/eval/baseline.py +265 -0
- brain/eval/concept_extraction.py +378 -0
- brain/eval/corpus.py +152 -0
- brain/eval/errors.py +19 -0
- brain/eval/graph_baseline.py +226 -0
- brain/eval/graph_retrieval.py +202 -0
- brain/eval/graph_runner.py +319 -0
- brain/eval/metrics.py +101 -0
- brain/eval/runner.py +223 -0
- brain/format.py +783 -0
- brain/gaps.py +390 -0
- brain/graph_rag/__init__.py +94 -0
- brain/graph_rag/_retrieval_common.py +113 -0
- brain/graph_rag/aggregates.py +303 -0
- brain/graph_rag/aliases/__init__.py +583 -0
- brain/graph_rag/backends/__init__.py +10 -0
- brain/graph_rag/backends/_age_helpers.py +473 -0
- brain/graph_rag/backends/age.py +782 -0
- brain/graph_rag/backends/base.py +272 -0
- brain/graph_rag/build.py +344 -0
- brain/graph_rag/communities.py +644 -0
- brain/graph_rag/communities_summary.py +437 -0
- brain/graph_rag/concepts.py +202 -0
- brain/graph_rag/cooccur.py +193 -0
- brain/graph_rag/cross_type.py +312 -0
- brain/graph_rag/extract.py +885 -0
- brain/graph_rag/fuse.py +371 -0
- brain/graph_rag/global_.py +412 -0
- brain/graph_rag/grouping.py +372 -0
- brain/graph_rag/person_resolver.py +167 -0
- brain/graph_rag/reconcile.py +792 -0
- brain/graph_rag/relational.py +353 -0
- brain/graph_rag/retrieve.py +526 -0
- brain/graph_rag/router.py +288 -0
- brain/graph_rag/schema.py +320 -0
- brain/graph_rag/sync.py +237 -0
- brain/graph_rag/tenancy.py +43 -0
- brain/graph_rag/themes.py +501 -0
- brain/graph_rag/weighting.py +202 -0
- brain/ingest/__init__.py +1926 -0
- brain/ingest/chunker.py +249 -0
- brain/ingest/docx.py +40 -0
- brain/ingest/gmail.py +621 -0
- brain/ingest/markdown.py +37 -0
- brain/ingest/pdf.py +61 -0
- brain/ingest/stdin.py +22 -0
- brain/ingest/sub_tokens.py +91 -0
- brain/ingest/text.py +16 -0
- brain/interactions.py +205 -0
- brain/maintenance.py +355 -0
- brain/mcp_server.py +3405 -0
- brain/migrations/001_init.sql +43 -0
- brain/migrations/002_qwen3_embedding.sql +17 -0
- brain/migrations/003_vault_model.sql +41 -0
- brain/migrations/004_relax_content_hash_uniqueness.sql +18 -0
- brain/migrations/005_derived_links.sql +67 -0
- brain/migrations/006_dedup_file_by_source_path.sql +25 -0
- brain/migrations/007_email_thread_and_draft.sql +15 -0
- brain/migrations/008_gmail_thread_unique.sql +11 -0
- brain/migrations/009_chunks_weighted_tsv.sql +28 -0
- brain/migrations/010_interactions.sql +30 -0
- brain/migrations/011_documents_summary.sql +23 -0
- brain/migrations/012_graphrag.sql +171 -0
- brain/migrations/013_graphrag_communities.sql +125 -0
- brain/migrations/014_graphrag_community_summary_hash.sql +33 -0
- brain/migrations/015_interactions_graph_targets.sql +89 -0
- brain/migrations/016_index_hygiene.sql +61 -0
- brain/migrations/017_elicit.sql +30 -0
- brain/migrations/018_review_gap_signal_kinds.sql +40 -0
- brain/migrations/019_search_queries.sql +35 -0
- brain/migrations/020_link_suggestions.sql +40 -0
- brain/migrations/021_timeline_doc_date.sql +34 -0
- brain/migrations/022_link_suggestions_undirected.sql +84 -0
- brain/migrations/023_search_queries_fts_count.sql +28 -0
- brain/quartz_overrides/__init__.py +8 -0
- brain/quartz_overrides/quartz/bootstrap-cli.mjs +65 -0
- brain/quartz_overrides/quartz/build.ts +568 -0
- brain/quartz_overrides/quartz/cli/args.js +152 -0
- brain/quartz_overrides/quartz/cli/build_partial_handler.js +544 -0
- brain/quartz_overrides/quartz/cli/handlers.js +636 -0
- brain/quartz_overrides/quartz/components/CommandPalette.tsx +172 -0
- brain/quartz_overrides/quartz/components/Explorer.tsx +198 -0
- brain/quartz_overrides/quartz/components/Footer.tsx +27 -0
- brain/quartz_overrides/quartz/components/Graph.tsx +468 -0
- brain/quartz_overrides/quartz/components/PageTitle.tsx +72 -0
- brain/quartz_overrides/quartz/components/RelatedDocs.tsx +38 -0
- brain/quartz_overrides/quartz/components/Search.tsx +161 -0
- brain/quartz_overrides/quartz/components/SummaryLede.tsx +72 -0
- brain/quartz_overrides/quartz/components/index.ts +92 -0
- brain/quartz_overrides/quartz/components/pages/TagContent.tsx +272 -0
- brain/quartz_overrides/quartz/components/scripts/commandPalette.inline.ts +665 -0
- brain/quartz_overrides/quartz/components/scripts/explorer.inline.ts +768 -0
- brain/quartz_overrides/quartz/components/scripts/graph.inline.ts +2302 -0
- brain/quartz_overrides/quartz/components/scripts/relatedDocs.inline.ts +163 -0
- brain/quartz_overrides/quartz/components/scripts/search.inline.ts +1011 -0
- brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts +546 -0
- brain/quartz_overrides/quartz/plugins/transformers/codeCopy.ts +94 -0
- brain/quartz_overrides/quartz/plugins/transformers/derivedFenceMark.ts +302 -0
- brain/quartz_overrides/quartz/plugins/transformers/emailThread.ts +148 -0
- brain/quartz_overrides/quartz/plugins/transformers/emptyDoorFilter.ts +213 -0
- brain/quartz_overrides/quartz/plugins/transformers/index.ts +114 -0
- brain/quartz_overrides/quartz/plugins/transformers/linkKindMark.ts +205 -0
- brain/quartz_overrides/quartz/plugins/transformers/linkSourceTag.ts +104 -0
- brain/quartz_overrides/quartz/plugins/transformers/relativeDate.ts +100 -0
- brain/quartz_overrides/quartz/plugins/transformers/reloadSignal.ts +131 -0
- brain/quartz_overrides/quartz/processors/parse.ts +371 -0
- brain/quartz_overrides/quartz/processors/parser_cache.ts +78 -0
- brain/quartz_overrides/quartz/static/brain-logo-dark.png +0 -0
- brain/quartz_overrides/quartz/static/brain-logo-light.png +0 -0
- brain/quartz_overrides/quartz/static/codeCopy.js +196 -0
- brain/quartz_overrides/quartz/static/emailThread.js +334 -0
- brain/quartz_overrides/quartz/static/favicon.ico +0 -0
- brain/quartz_overrides/quartz/static/icon.png +0 -0
- brain/quartz_overrides/quartz/static/linkSourceTag.js +104 -0
- brain/quartz_overrides/quartz/static/relativeDate.js +142 -0
- brain/quartz_overrides/quartz/static/reload.js +168 -0
- brain/quartz_overrides/quartz/styles/brain/_article.scss +252 -0
- brain/quartz_overrides/quartz/styles/brain/_atmosphere.scss +113 -0
- brain/quartz_overrides/quartz/styles/brain/_callouts.scss +180 -0
- brain/quartz_overrides/quartz/styles/brain/_cmdk.scss +7 -0
- brain/quartz_overrides/quartz/styles/brain/_code.scss +208 -0
- brain/quartz_overrides/quartz/styles/brain/_command_palette.scss +369 -0
- brain/quartz_overrides/quartz/styles/brain/_email_thread.scss +228 -0
- brain/quartz_overrides/quartz/styles/brain/_explorer.scss +142 -0
- brain/quartz_overrides/quartz/styles/brain/_home.scss +182 -0
- brain/quartz_overrides/quartz/styles/brain/_links.scss +322 -0
- brain/quartz_overrides/quartz/styles/brain/_marginalia.scss +117 -0
- brain/quartz_overrides/quartz/styles/brain/_motion.scss +175 -0
- brain/quartz_overrides/quartz/styles/brain/_people_hub.scss +100 -0
- brain/quartz_overrides/quartz/styles/brain/_related_docs.scss +137 -0
- brain/quartz_overrides/quartz/styles/brain/_search.scss +252 -0
- brain/quartz_overrides/quartz/styles/brain/_sidebar.scss +468 -0
- brain/quartz_overrides/quartz/styles/brain/_summary_lede.scss +56 -0
- brain/quartz_overrides/quartz/styles/brain/_surface.scss +43 -0
- brain/quartz_overrides/quartz/styles/brain/_tag_content.scss +118 -0
- brain/quartz_overrides/quartz/styles/brain/_tokens.scss +197 -0
- brain/quartz_overrides/quartz/styles/brain/_typography.scss +92 -0
- brain/quartz_overrides/quartz/styles/custom.scss +89 -0
- brain/quartz_overrides/quartz/styles/graph.scss +505 -0
- brain/quartz_overrides/quartz/util/ctx.ts +92 -0
- brain/quartz_overrides/quartz/util/fastpath_manifest.ts +608 -0
- brain/quartz_overrides/quartz/util/path.ts +358 -0
- brain/quartz_overrides/quartz/util/sourceIcons.ts +55 -0
- brain/quartz_overrides/quartz.config.ts +270 -0
- brain/quartz_overrides/quartz.layout.ts +314 -0
- brain/queries.py +1188 -0
- brain/rank_fusion.py +8 -0
- brain/resurface.py +210 -0
- brain/review/__init__.py +26 -0
- brain/review/emit.py +27 -0
- brain/review/queries.py +436 -0
- brain/review/render.py +196 -0
- brain/review/scans.py +355 -0
- brain/review/weekly.py +413 -0
- brain/search.py +704 -0
- brain/set_similarity.py +15 -0
- brain/setup.py +1205 -0
- brain/tags.py +56 -0
- brain/templates/Caddyfile.j2 +9 -0
- brain/templates/__init__.py +1 -0
- brain/templates/bin/__init__.py +1 -0
- brain/templates/bin/_brain-brief-fg.sh +25 -0
- brain/templates/bin/_brain-build-fg.sh +53 -0
- brain/templates/bin/_brain-watcher-fg.sh +65 -0
- brain/templates/bin/brain-down.sh +89 -0
- brain/templates/bin/brain-status.sh +83 -0
- brain/templates/bin/brain-up.sh +221 -0
- brain/templates/docker/age/Dockerfile +79 -0
- brain/templates/docker-compose.stock.yml.j2 +26 -0
- brain/templates/docker-compose.yml.j2 +34 -0
- brain/templates/env.example +190 -0
- brain/templates/launchd/__init__.py +1 -0
- brain/templates/launchd/com.brain.brief.plist.j2 +45 -0
- brain/templates/launchd/com.brain.build.plist.j2 +46 -0
- brain/templates/launchd/com.brain.watcher.plist.j2 +46 -0
- brain/templates/skill/SKILL.md +63 -0
- brain/templates/skill/__init__.py +1 -0
- brain/timeline.py +834 -0
- brain/todo.py +124 -0
- brain/uninstall.py +185 -0
- brain/vault/__init__.py +115 -0
- brain/vault/_atomic.py +25 -0
- brain/vault/daily_index.py +228 -0
- brain/vault/derived_links/__init__.py +50 -0
- brain/vault/derived_links/directory.py +683 -0
- brain/vault/derived_links/fence.py +408 -0
- brain/vault/derived_links/gws.py +64 -0
- brain/vault/derived_links/participants.py +143 -0
- brain/vault/derived_links/pass_runner.py +362 -0
- brain/vault/derived_links/rules.py +137 -0
- brain/vault/export.py +683 -0
- brain/vault/frontmatter.py +165 -0
- brain/vault/graph.py +620 -0
- brain/vault/graph_format.py +388 -0
- brain/vault/link_rewrite.py +235 -0
- brain/vault/links.py +260 -0
- brain/vault/note_builder.py +211 -0
- brain/vault/paths.py +55 -0
- brain/vault/quartz_overlay.py +236 -0
- brain/vault/rename.py +591 -0
- brain/vault/resolver.py +304 -0
- brain/vault/slug.py +127 -0
- brain/vault/sync.py +1513 -0
- brain/vault/sync_summaries.py +264 -0
- brain/vault/templates.py +145 -0
- brain/vault/watch.py +1052 -0
- brain/wiki/__init__.py +6 -0
- brain/wiki/_github_slugger.py +76 -0
- brain/wiki/_person_name.py +314 -0
- brain/wiki/build_homepage.py +541 -0
- brain/wiki/build_partial.py +273 -0
- brain/wiki/build_people.py +934 -0
- brain/wiki/build_related.py +758 -0
- brain/wiki/build_swap.py +585 -0
- brain/wiki/build_watcher.py +975 -0
- brain/wiki/edit_classifier.py +215 -0
- brain/wiki/errors.py +10 -0
- brain/wiki/fastpath_manifest.py +475 -0
- brain/wiki/fastpath_state.py +174 -0
- brain/wiki/install.py +296 -0
- brain/wiki/slug.py +111 -0
- secondbrain_py-0.2.1.dist-info/METADATA +195 -0
- secondbrain_py-0.2.1.dist-info/RECORD +273 -0
- secondbrain_py-0.2.1.dist-info/WHEEL +5 -0
- secondbrain_py-0.2.1.dist-info/entry_points.txt +11 -0
- secondbrain_py-0.2.1.dist-info/licenses/LICENSE +21 -0
- secondbrain_py-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Quartz overlay step — copies ``brain/quartz_overrides/`` into a Quartz workspace.
|
|
2
|
+
|
|
3
|
+
The brain package ships a small set of overrides under ``brain/quartz_overrides/``
|
|
4
|
+
that customize how Quartz renders the vault (custom Graph component,
|
|
5
|
+
extended contentIndex emitter, derived-fence transformer, layout, scss).
|
|
6
|
+
``brain vault render --overlay`` copies these files over the user's
|
|
7
|
+
Quartz workspace right before invoking ``npx quartz build``.
|
|
8
|
+
|
|
9
|
+
Special case — `_upstreamContentIndex.tsx` rename:
|
|
10
|
+
Our overlay's ``contentIndex.ts`` is a thin wrapper that imports
|
|
11
|
+
Quartz's stock emitter via ``./_upstreamContentIndex``. To make
|
|
12
|
+
that import resolve, this module renames the stock
|
|
13
|
+
``contentIndex.tsx`` shipped by Quartz out of the way before
|
|
14
|
+
copying. The rename is idempotent — on a workspace that has
|
|
15
|
+
already been overlaid, only ``_upstreamContentIndex.tsx`` is
|
|
16
|
+
present and no rename is needed.
|
|
17
|
+
|
|
18
|
+
The module exposes a pure :func:`plan_overlay` (no filesystem
|
|
19
|
+
mutations) plus :func:`apply_overlay` (performs rename + copy). The
|
|
20
|
+
two-step shape lets the CLI implement ``--print-overlay`` (plan
|
|
21
|
+
without applying) and lets unit tests exercise planning and applying
|
|
22
|
+
separately.
|
|
23
|
+
"""
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from importlib.resources import files as resource_files
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Literal
|
|
30
|
+
|
|
31
|
+
from ..errors import BrainError
|
|
32
|
+
|
|
33
|
+
# brain: special case — see brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts
|
|
34
|
+
# header for the why. The wrapper imports from `./_upstreamContentIndex`,
|
|
35
|
+
# so we rename Quartz's stock emitter out of the way before the copy.
|
|
36
|
+
_UPSTREAM_RENAME_FROM = Path("quartz/plugins/emitters/contentIndex.tsx")
|
|
37
|
+
_UPSTREAM_RENAME_TO = Path("quartz/plugins/emitters/_upstreamContentIndex.tsx")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
RenameState = Literal["needed", "already_applied", "missing_both"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OverlayError(BrainError):
|
|
44
|
+
"""Raised when the overlay step cannot proceed safely.
|
|
45
|
+
|
|
46
|
+
Two cases trigger this: a missing ``brain/quartz_overrides/`` source dir
|
|
47
|
+
(brain package is broken), or an inconsistent Quartz workspace
|
|
48
|
+
where BOTH the upstream ``contentIndex.tsx`` AND the renamed
|
|
49
|
+
``_upstreamContentIndex.tsx`` are present at the same time. The
|
|
50
|
+
second case we refuse to auto-resolve — picking one would silently
|
|
51
|
+
discard the other, which may be a user customization.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class OverlayPlan:
|
|
57
|
+
"""A computed-but-not-applied overlay snapshot.
|
|
58
|
+
|
|
59
|
+
``pairs`` is the ordered list of (source, destination) absolute
|
|
60
|
+
paths the copy step would write, in deterministic (sorted-by-src)
|
|
61
|
+
order. ``rename`` is the upstream-rename pair if it should fire
|
|
62
|
+
on this workspace, else ``None``. ``rename_state`` distinguishes
|
|
63
|
+
the two ``rename is None`` sub-cases (already applied vs. neither
|
|
64
|
+
file present) so ``--print-overlay`` can surface the difference.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
quartz_dir: Path
|
|
68
|
+
pairs: tuple[tuple[Path, Path], ...]
|
|
69
|
+
rename: tuple[Path, Path] | None
|
|
70
|
+
rename_state: RenameState
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _overlay_source_root() -> Path:
|
|
74
|
+
"""Resolve the ``quartz_overrides/`` tree from inside the installed brain package.
|
|
75
|
+
|
|
76
|
+
Uses ``importlib.resources.files("brain.quartz_overrides")`` so the path is
|
|
77
|
+
valid in both editable installs (``pip install -e``) and wheel-installed pipx
|
|
78
|
+
environments — importlib.resources handles both cases.
|
|
79
|
+
|
|
80
|
+
Raises :class:`OverlayError` if the package resource is not backed by a regular
|
|
81
|
+
filesystem directory (e.g. if the package were installed inside a zip archive,
|
|
82
|
+
which is not expected for brain).
|
|
83
|
+
"""
|
|
84
|
+
root = resource_files("brain.quartz_overrides")
|
|
85
|
+
if not isinstance(root, os.PathLike):
|
|
86
|
+
raise OverlayError(
|
|
87
|
+
"brain.quartz_overrides must be installed as a directory, not inside a "
|
|
88
|
+
"zip archive. Re-install brain with 'pip install brain' (not as a zipapp)."
|
|
89
|
+
)
|
|
90
|
+
return Path(root)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def plan_overlay(quartz_dir: Path) -> OverlayPlan:
|
|
94
|
+
"""Enumerate every overlay file + figure out the upstream rename.
|
|
95
|
+
|
|
96
|
+
Pure planning step — no filesystem mutations. Raises
|
|
97
|
+
:class:`OverlayError` if the brain package's ``quartz_overrides/``
|
|
98
|
+
directory is missing or unreadable, or if the Quartz workspace is in
|
|
99
|
+
an inconsistent state we won't auto-resolve.
|
|
100
|
+
"""
|
|
101
|
+
overrides_root = _overlay_source_root()
|
|
102
|
+
quartz_dir_resolved = quartz_dir.resolve()
|
|
103
|
+
if not overrides_root.is_dir():
|
|
104
|
+
raise OverlayError(
|
|
105
|
+
f"overlay source directory not found: {overrides_root}\n"
|
|
106
|
+
f" Expected the brain package's quartz_overrides/ tree to be installed "
|
|
107
|
+
f"at that path. Try reinstalling brain."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
pairs: list[tuple[Path, Path]] = []
|
|
111
|
+
for src in sorted(overrides_root.rglob("*")):
|
|
112
|
+
if not src.is_file():
|
|
113
|
+
continue
|
|
114
|
+
# Skip macOS metadata + dotfiles defensively; never legitimate
|
|
115
|
+
# overlay content.
|
|
116
|
+
if src.name.startswith("."):
|
|
117
|
+
continue
|
|
118
|
+
# Skip Python package metadata files and bytecode caches — they are
|
|
119
|
+
# not overlay content. __pycache__/ is generated by the Python
|
|
120
|
+
# interpreter when the package is imported and must not be copied.
|
|
121
|
+
if src.suffix in {".py", ".pyc"}:
|
|
122
|
+
continue
|
|
123
|
+
if "__pycache__" in src.parts:
|
|
124
|
+
continue
|
|
125
|
+
# Defense in depth: confirm src resolves inside overrides_root
|
|
126
|
+
# before we honor it (in case a future symlink ever points out).
|
|
127
|
+
try:
|
|
128
|
+
relative = src.resolve().relative_to(overrides_root)
|
|
129
|
+
except ValueError as e:
|
|
130
|
+
raise OverlayError(
|
|
131
|
+
f"overlay source escaped {overrides_root}: {src}"
|
|
132
|
+
) from e
|
|
133
|
+
dest = quartz_dir_resolved / relative
|
|
134
|
+
pairs.append((src, dest))
|
|
135
|
+
|
|
136
|
+
rename, rename_state = _plan_upstream_rename(quartz_dir_resolved)
|
|
137
|
+
return OverlayPlan(
|
|
138
|
+
quartz_dir=quartz_dir_resolved,
|
|
139
|
+
pairs=tuple(pairs),
|
|
140
|
+
rename=rename,
|
|
141
|
+
rename_state=rename_state,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _plan_upstream_rename(
|
|
146
|
+
quartz_dir: Path,
|
|
147
|
+
) -> tuple[tuple[Path, Path] | None, RenameState]:
|
|
148
|
+
"""Inspect the workspace and decide if the upstream rename should fire.
|
|
149
|
+
|
|
150
|
+
Three states:
|
|
151
|
+
* ``needed`` — only the original ``contentIndex.tsx`` is present;
|
|
152
|
+
rename it out of the way.
|
|
153
|
+
* ``already_applied`` — only ``_upstreamContentIndex.tsx`` is
|
|
154
|
+
present; rename has already happened, no-op.
|
|
155
|
+
* ``missing_both`` — neither is present; workspace is in an
|
|
156
|
+
unexpected state but we don't pre-empt it. The wrapper's
|
|
157
|
+
defensive load-time guard will surface a clear error when the
|
|
158
|
+
build runs.
|
|
159
|
+
|
|
160
|
+
Raises :class:`OverlayError` if BOTH files exist at once — that's
|
|
161
|
+
an inconsistent state we refuse to auto-resolve.
|
|
162
|
+
"""
|
|
163
|
+
src = quartz_dir / _UPSTREAM_RENAME_FROM
|
|
164
|
+
dest = quartz_dir / _UPSTREAM_RENAME_TO
|
|
165
|
+
src_exists = src.is_file()
|
|
166
|
+
dest_exists = dest.is_file()
|
|
167
|
+
if src_exists and dest_exists:
|
|
168
|
+
raise OverlayError(
|
|
169
|
+
f"both upstream and renamed contentIndex files exist:\n"
|
|
170
|
+
f" {src}\n"
|
|
171
|
+
f" {dest}\n"
|
|
172
|
+
f" Delete whichever is stale and re-run. Keep "
|
|
173
|
+
f"`_upstreamContentIndex.tsx` if you want the brain wrapper; "
|
|
174
|
+
f"keep `contentIndex.tsx` if you want stock Quartz."
|
|
175
|
+
)
|
|
176
|
+
if src_exists:
|
|
177
|
+
return ((src, dest), "needed")
|
|
178
|
+
if dest_exists:
|
|
179
|
+
return (None, "already_applied")
|
|
180
|
+
return (None, "missing_both")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def apply_overlay(plan: OverlayPlan) -> list[tuple[Path, Path]]:
|
|
184
|
+
"""Apply an overlay plan: rename upstream (if needed), then copy files.
|
|
185
|
+
|
|
186
|
+
Returns the list of (src, dest) pairs actually copied. The rename
|
|
187
|
+
runs first so the wrapper's ``./_upstreamContentIndex`` import
|
|
188
|
+
resolves once the build runs. Existing destinations are
|
|
189
|
+
overwritten via ``shutil.copy2`` — that's the whole point of the
|
|
190
|
+
overlay.
|
|
191
|
+
|
|
192
|
+
Re-runnable after a partial failure: the rename step is idempotent
|
|
193
|
+
via the three-state detection in :func:`_plan_upstream_rename`
|
|
194
|
+
(already-applied state is a no-op), and copies always overwrite —
|
|
195
|
+
so a fresh ``plan_overlay`` + ``apply_overlay`` pass converges
|
|
196
|
+
without manual cleanup.
|
|
197
|
+
|
|
198
|
+
Raises :class:`OverlayError` if any filesystem operation fails;
|
|
199
|
+
callers (CLI, MCP) need only catch ``OverlayError`` to convert
|
|
200
|
+
every failure mode into a friendly user-facing error.
|
|
201
|
+
"""
|
|
202
|
+
if plan.rename is not None:
|
|
203
|
+
src, dest = plan.rename
|
|
204
|
+
try:
|
|
205
|
+
src.rename(dest)
|
|
206
|
+
except OSError as e:
|
|
207
|
+
raise OverlayError(
|
|
208
|
+
f"overlay rename failed: {src} → {dest}: {e}"
|
|
209
|
+
) from e
|
|
210
|
+
for src, dest in plan.pairs:
|
|
211
|
+
try:
|
|
212
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
shutil.copy2(src, dest)
|
|
214
|
+
# Shebanged files must be executable. shutil.copy2 preserves
|
|
215
|
+
# mode bits, so an overlay source checked in without +x
|
|
216
|
+
# produces a non-executable workspace copy (and an npx-cache
|
|
217
|
+
# install with the same mode), which makes `npx <bin>` fail
|
|
218
|
+
# with "Permission denied" at the shell layer. Restoring +x
|
|
219
|
+
# here guarantees the destination is runnable regardless of
|
|
220
|
+
# the source-file's tracked mode.
|
|
221
|
+
if _has_shebang(dest):
|
|
222
|
+
dest.chmod(dest.stat().st_mode | 0o111)
|
|
223
|
+
except OSError as e:
|
|
224
|
+
raise OverlayError(
|
|
225
|
+
f"overlay copy failed: {src} → {dest}: {e}"
|
|
226
|
+
) from e
|
|
227
|
+
return list(plan.pairs)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _has_shebang(path: Path) -> bool:
|
|
231
|
+
"""Return True when ``path`` starts with ``#!`` (Unix shebang)."""
|
|
232
|
+
try:
|
|
233
|
+
with path.open("rb") as f:
|
|
234
|
+
return f.read(2) == b"#!"
|
|
235
|
+
except OSError:
|
|
236
|
+
return False
|