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.
Files changed (273) hide show
  1. brain/__init__.py +0 -0
  2. brain/__main__.py +18 -0
  3. brain/_capture_command.py +445 -0
  4. brain/_compose.py +52 -0
  5. brain/activity.py +206 -0
  6. brain/ask.py +631 -0
  7. brain/audio.py +591 -0
  8. brain/backfill/__init__.py +12 -0
  9. brain/backfill/search_extras.py +141 -0
  10. brain/backfill/source_rows.py +101 -0
  11. brain/bin/__init__.py +1 -0
  12. brain/bin/_launcher.py +107 -0
  13. brain/bin/down.py +8 -0
  14. brain/bin/launchd.py +268 -0
  15. brain/bin/monitor.py +570 -0
  16. brain/bin/rebuild.py +8 -0
  17. brain/bin/status.py +8 -0
  18. brain/bin/up.py +8 -0
  19. brain/brief.py +272 -0
  20. brain/capture.py +49 -0
  21. brain/chat.py +293 -0
  22. brain/cli.py +9760 -0
  23. brain/cli_claude.py +81 -0
  24. brain/cli_connect.py +285 -0
  25. brain/cli_demo.py +266 -0
  26. brain/config.py +1949 -0
  27. brain/connect.py +925 -0
  28. brain/db.py +540 -0
  29. brain/demo/__init__.py +452 -0
  30. brain/demo/corpus/manifest.json +403 -0
  31. brain/demo/embedder.py +74 -0
  32. brain/durations.py +84 -0
  33. brain/edit_session.py +156 -0
  34. brain/editor.py +67 -0
  35. brain/elicit/__init__.py +16 -0
  36. brain/elicit/detectors.py +250 -0
  37. brain/elicit/drafter.py +70 -0
  38. brain/elicit/queue.py +220 -0
  39. brain/elicit/schema.py +48 -0
  40. brain/elicit/session.py +445 -0
  41. brain/embedding_targets.py +54 -0
  42. brain/embeddings.py +424 -0
  43. brain/enrichment.py +808 -0
  44. brain/errors.py +357 -0
  45. brain/eval/__init__.py +129 -0
  46. brain/eval/answer_eval.py +281 -0
  47. brain/eval/baseline.py +265 -0
  48. brain/eval/concept_extraction.py +378 -0
  49. brain/eval/corpus.py +152 -0
  50. brain/eval/errors.py +19 -0
  51. brain/eval/graph_baseline.py +226 -0
  52. brain/eval/graph_retrieval.py +202 -0
  53. brain/eval/graph_runner.py +319 -0
  54. brain/eval/metrics.py +101 -0
  55. brain/eval/runner.py +223 -0
  56. brain/format.py +783 -0
  57. brain/gaps.py +390 -0
  58. brain/graph_rag/__init__.py +94 -0
  59. brain/graph_rag/_retrieval_common.py +113 -0
  60. brain/graph_rag/aggregates.py +303 -0
  61. brain/graph_rag/aliases/__init__.py +583 -0
  62. brain/graph_rag/backends/__init__.py +10 -0
  63. brain/graph_rag/backends/_age_helpers.py +473 -0
  64. brain/graph_rag/backends/age.py +782 -0
  65. brain/graph_rag/backends/base.py +272 -0
  66. brain/graph_rag/build.py +344 -0
  67. brain/graph_rag/communities.py +644 -0
  68. brain/graph_rag/communities_summary.py +437 -0
  69. brain/graph_rag/concepts.py +202 -0
  70. brain/graph_rag/cooccur.py +193 -0
  71. brain/graph_rag/cross_type.py +312 -0
  72. brain/graph_rag/extract.py +885 -0
  73. brain/graph_rag/fuse.py +371 -0
  74. brain/graph_rag/global_.py +412 -0
  75. brain/graph_rag/grouping.py +372 -0
  76. brain/graph_rag/person_resolver.py +167 -0
  77. brain/graph_rag/reconcile.py +792 -0
  78. brain/graph_rag/relational.py +353 -0
  79. brain/graph_rag/retrieve.py +526 -0
  80. brain/graph_rag/router.py +288 -0
  81. brain/graph_rag/schema.py +320 -0
  82. brain/graph_rag/sync.py +237 -0
  83. brain/graph_rag/tenancy.py +43 -0
  84. brain/graph_rag/themes.py +501 -0
  85. brain/graph_rag/weighting.py +202 -0
  86. brain/ingest/__init__.py +1926 -0
  87. brain/ingest/chunker.py +249 -0
  88. brain/ingest/docx.py +40 -0
  89. brain/ingest/gmail.py +621 -0
  90. brain/ingest/markdown.py +37 -0
  91. brain/ingest/pdf.py +61 -0
  92. brain/ingest/stdin.py +22 -0
  93. brain/ingest/sub_tokens.py +91 -0
  94. brain/ingest/text.py +16 -0
  95. brain/interactions.py +205 -0
  96. brain/maintenance.py +355 -0
  97. brain/mcp_server.py +3405 -0
  98. brain/migrations/001_init.sql +43 -0
  99. brain/migrations/002_qwen3_embedding.sql +17 -0
  100. brain/migrations/003_vault_model.sql +41 -0
  101. brain/migrations/004_relax_content_hash_uniqueness.sql +18 -0
  102. brain/migrations/005_derived_links.sql +67 -0
  103. brain/migrations/006_dedup_file_by_source_path.sql +25 -0
  104. brain/migrations/007_email_thread_and_draft.sql +15 -0
  105. brain/migrations/008_gmail_thread_unique.sql +11 -0
  106. brain/migrations/009_chunks_weighted_tsv.sql +28 -0
  107. brain/migrations/010_interactions.sql +30 -0
  108. brain/migrations/011_documents_summary.sql +23 -0
  109. brain/migrations/012_graphrag.sql +171 -0
  110. brain/migrations/013_graphrag_communities.sql +125 -0
  111. brain/migrations/014_graphrag_community_summary_hash.sql +33 -0
  112. brain/migrations/015_interactions_graph_targets.sql +89 -0
  113. brain/migrations/016_index_hygiene.sql +61 -0
  114. brain/migrations/017_elicit.sql +30 -0
  115. brain/migrations/018_review_gap_signal_kinds.sql +40 -0
  116. brain/migrations/019_search_queries.sql +35 -0
  117. brain/migrations/020_link_suggestions.sql +40 -0
  118. brain/migrations/021_timeline_doc_date.sql +34 -0
  119. brain/migrations/022_link_suggestions_undirected.sql +84 -0
  120. brain/migrations/023_search_queries_fts_count.sql +28 -0
  121. brain/quartz_overrides/__init__.py +8 -0
  122. brain/quartz_overrides/quartz/bootstrap-cli.mjs +65 -0
  123. brain/quartz_overrides/quartz/build.ts +568 -0
  124. brain/quartz_overrides/quartz/cli/args.js +152 -0
  125. brain/quartz_overrides/quartz/cli/build_partial_handler.js +544 -0
  126. brain/quartz_overrides/quartz/cli/handlers.js +636 -0
  127. brain/quartz_overrides/quartz/components/CommandPalette.tsx +172 -0
  128. brain/quartz_overrides/quartz/components/Explorer.tsx +198 -0
  129. brain/quartz_overrides/quartz/components/Footer.tsx +27 -0
  130. brain/quartz_overrides/quartz/components/Graph.tsx +468 -0
  131. brain/quartz_overrides/quartz/components/PageTitle.tsx +72 -0
  132. brain/quartz_overrides/quartz/components/RelatedDocs.tsx +38 -0
  133. brain/quartz_overrides/quartz/components/Search.tsx +161 -0
  134. brain/quartz_overrides/quartz/components/SummaryLede.tsx +72 -0
  135. brain/quartz_overrides/quartz/components/index.ts +92 -0
  136. brain/quartz_overrides/quartz/components/pages/TagContent.tsx +272 -0
  137. brain/quartz_overrides/quartz/components/scripts/commandPalette.inline.ts +665 -0
  138. brain/quartz_overrides/quartz/components/scripts/explorer.inline.ts +768 -0
  139. brain/quartz_overrides/quartz/components/scripts/graph.inline.ts +2302 -0
  140. brain/quartz_overrides/quartz/components/scripts/relatedDocs.inline.ts +163 -0
  141. brain/quartz_overrides/quartz/components/scripts/search.inline.ts +1011 -0
  142. brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts +546 -0
  143. brain/quartz_overrides/quartz/plugins/transformers/codeCopy.ts +94 -0
  144. brain/quartz_overrides/quartz/plugins/transformers/derivedFenceMark.ts +302 -0
  145. brain/quartz_overrides/quartz/plugins/transformers/emailThread.ts +148 -0
  146. brain/quartz_overrides/quartz/plugins/transformers/emptyDoorFilter.ts +213 -0
  147. brain/quartz_overrides/quartz/plugins/transformers/index.ts +114 -0
  148. brain/quartz_overrides/quartz/plugins/transformers/linkKindMark.ts +205 -0
  149. brain/quartz_overrides/quartz/plugins/transformers/linkSourceTag.ts +104 -0
  150. brain/quartz_overrides/quartz/plugins/transformers/relativeDate.ts +100 -0
  151. brain/quartz_overrides/quartz/plugins/transformers/reloadSignal.ts +131 -0
  152. brain/quartz_overrides/quartz/processors/parse.ts +371 -0
  153. brain/quartz_overrides/quartz/processors/parser_cache.ts +78 -0
  154. brain/quartz_overrides/quartz/static/brain-logo-dark.png +0 -0
  155. brain/quartz_overrides/quartz/static/brain-logo-light.png +0 -0
  156. brain/quartz_overrides/quartz/static/codeCopy.js +196 -0
  157. brain/quartz_overrides/quartz/static/emailThread.js +334 -0
  158. brain/quartz_overrides/quartz/static/favicon.ico +0 -0
  159. brain/quartz_overrides/quartz/static/icon.png +0 -0
  160. brain/quartz_overrides/quartz/static/linkSourceTag.js +104 -0
  161. brain/quartz_overrides/quartz/static/relativeDate.js +142 -0
  162. brain/quartz_overrides/quartz/static/reload.js +168 -0
  163. brain/quartz_overrides/quartz/styles/brain/_article.scss +252 -0
  164. brain/quartz_overrides/quartz/styles/brain/_atmosphere.scss +113 -0
  165. brain/quartz_overrides/quartz/styles/brain/_callouts.scss +180 -0
  166. brain/quartz_overrides/quartz/styles/brain/_cmdk.scss +7 -0
  167. brain/quartz_overrides/quartz/styles/brain/_code.scss +208 -0
  168. brain/quartz_overrides/quartz/styles/brain/_command_palette.scss +369 -0
  169. brain/quartz_overrides/quartz/styles/brain/_email_thread.scss +228 -0
  170. brain/quartz_overrides/quartz/styles/brain/_explorer.scss +142 -0
  171. brain/quartz_overrides/quartz/styles/brain/_home.scss +182 -0
  172. brain/quartz_overrides/quartz/styles/brain/_links.scss +322 -0
  173. brain/quartz_overrides/quartz/styles/brain/_marginalia.scss +117 -0
  174. brain/quartz_overrides/quartz/styles/brain/_motion.scss +175 -0
  175. brain/quartz_overrides/quartz/styles/brain/_people_hub.scss +100 -0
  176. brain/quartz_overrides/quartz/styles/brain/_related_docs.scss +137 -0
  177. brain/quartz_overrides/quartz/styles/brain/_search.scss +252 -0
  178. brain/quartz_overrides/quartz/styles/brain/_sidebar.scss +468 -0
  179. brain/quartz_overrides/quartz/styles/brain/_summary_lede.scss +56 -0
  180. brain/quartz_overrides/quartz/styles/brain/_surface.scss +43 -0
  181. brain/quartz_overrides/quartz/styles/brain/_tag_content.scss +118 -0
  182. brain/quartz_overrides/quartz/styles/brain/_tokens.scss +197 -0
  183. brain/quartz_overrides/quartz/styles/brain/_typography.scss +92 -0
  184. brain/quartz_overrides/quartz/styles/custom.scss +89 -0
  185. brain/quartz_overrides/quartz/styles/graph.scss +505 -0
  186. brain/quartz_overrides/quartz/util/ctx.ts +92 -0
  187. brain/quartz_overrides/quartz/util/fastpath_manifest.ts +608 -0
  188. brain/quartz_overrides/quartz/util/path.ts +358 -0
  189. brain/quartz_overrides/quartz/util/sourceIcons.ts +55 -0
  190. brain/quartz_overrides/quartz.config.ts +270 -0
  191. brain/quartz_overrides/quartz.layout.ts +314 -0
  192. brain/queries.py +1188 -0
  193. brain/rank_fusion.py +8 -0
  194. brain/resurface.py +210 -0
  195. brain/review/__init__.py +26 -0
  196. brain/review/emit.py +27 -0
  197. brain/review/queries.py +436 -0
  198. brain/review/render.py +196 -0
  199. brain/review/scans.py +355 -0
  200. brain/review/weekly.py +413 -0
  201. brain/search.py +704 -0
  202. brain/set_similarity.py +15 -0
  203. brain/setup.py +1205 -0
  204. brain/tags.py +56 -0
  205. brain/templates/Caddyfile.j2 +9 -0
  206. brain/templates/__init__.py +1 -0
  207. brain/templates/bin/__init__.py +1 -0
  208. brain/templates/bin/_brain-brief-fg.sh +25 -0
  209. brain/templates/bin/_brain-build-fg.sh +53 -0
  210. brain/templates/bin/_brain-watcher-fg.sh +65 -0
  211. brain/templates/bin/brain-down.sh +89 -0
  212. brain/templates/bin/brain-status.sh +83 -0
  213. brain/templates/bin/brain-up.sh +221 -0
  214. brain/templates/docker/age/Dockerfile +79 -0
  215. brain/templates/docker-compose.stock.yml.j2 +26 -0
  216. brain/templates/docker-compose.yml.j2 +34 -0
  217. brain/templates/env.example +190 -0
  218. brain/templates/launchd/__init__.py +1 -0
  219. brain/templates/launchd/com.brain.brief.plist.j2 +45 -0
  220. brain/templates/launchd/com.brain.build.plist.j2 +46 -0
  221. brain/templates/launchd/com.brain.watcher.plist.j2 +46 -0
  222. brain/templates/skill/SKILL.md +63 -0
  223. brain/templates/skill/__init__.py +1 -0
  224. brain/timeline.py +834 -0
  225. brain/todo.py +124 -0
  226. brain/uninstall.py +185 -0
  227. brain/vault/__init__.py +115 -0
  228. brain/vault/_atomic.py +25 -0
  229. brain/vault/daily_index.py +228 -0
  230. brain/vault/derived_links/__init__.py +50 -0
  231. brain/vault/derived_links/directory.py +683 -0
  232. brain/vault/derived_links/fence.py +408 -0
  233. brain/vault/derived_links/gws.py +64 -0
  234. brain/vault/derived_links/participants.py +143 -0
  235. brain/vault/derived_links/pass_runner.py +362 -0
  236. brain/vault/derived_links/rules.py +137 -0
  237. brain/vault/export.py +683 -0
  238. brain/vault/frontmatter.py +165 -0
  239. brain/vault/graph.py +620 -0
  240. brain/vault/graph_format.py +388 -0
  241. brain/vault/link_rewrite.py +235 -0
  242. brain/vault/links.py +260 -0
  243. brain/vault/note_builder.py +211 -0
  244. brain/vault/paths.py +55 -0
  245. brain/vault/quartz_overlay.py +236 -0
  246. brain/vault/rename.py +591 -0
  247. brain/vault/resolver.py +304 -0
  248. brain/vault/slug.py +127 -0
  249. brain/vault/sync.py +1513 -0
  250. brain/vault/sync_summaries.py +264 -0
  251. brain/vault/templates.py +145 -0
  252. brain/vault/watch.py +1052 -0
  253. brain/wiki/__init__.py +6 -0
  254. brain/wiki/_github_slugger.py +76 -0
  255. brain/wiki/_person_name.py +314 -0
  256. brain/wiki/build_homepage.py +541 -0
  257. brain/wiki/build_partial.py +273 -0
  258. brain/wiki/build_people.py +934 -0
  259. brain/wiki/build_related.py +758 -0
  260. brain/wiki/build_swap.py +585 -0
  261. brain/wiki/build_watcher.py +975 -0
  262. brain/wiki/edit_classifier.py +215 -0
  263. brain/wiki/errors.py +10 -0
  264. brain/wiki/fastpath_manifest.py +475 -0
  265. brain/wiki/fastpath_state.py +174 -0
  266. brain/wiki/install.py +296 -0
  267. brain/wiki/slug.py +111 -0
  268. secondbrain_py-0.2.1.dist-info/METADATA +195 -0
  269. secondbrain_py-0.2.1.dist-info/RECORD +273 -0
  270. secondbrain_py-0.2.1.dist-info/WHEEL +5 -0
  271. secondbrain_py-0.2.1.dist-info/entry_points.txt +11 -0
  272. secondbrain_py-0.2.1.dist-info/licenses/LICENSE +21 -0
  273. secondbrain_py-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,636 @@
1
+ // Brain wiki overlay — quartz/cli/handlers.js
2
+ //
3
+ // This file shadows the upstream ``~/brain-vault/.quartz/quartz/cli/handlers.js``.
4
+ // All upstream functionality is preserved verbatim. The only addition is the
5
+ // re-export of ``partialBuildContent`` (from build_partial_handler.js) for the
6
+ // T4 ``build-partial`` subcommand.
7
+ //
8
+ // T4 addition (Plan B v3 — per-file emit fast path):
9
+ // ``export { handlePartialBuild as partialBuildContent }`` — delegated to
10
+ // quartz/cli/build_partial_handler.js which contains the full machinery.
11
+
12
+ import { promises } from "fs"
13
+ import path from "path"
14
+ import esbuild from "esbuild"
15
+ import { styleText } from "util"
16
+ import { sassPlugin } from "esbuild-sass-plugin"
17
+ import fs from "fs"
18
+ import { intro, outro, select, text } from "@clack/prompts"
19
+ import { rm } from "fs/promises"
20
+ import chokidar from "chokidar"
21
+ import prettyBytes from "pretty-bytes"
22
+ import { execSync, spawnSync } from "child_process"
23
+ import http from "http"
24
+ import serveHandler from "serve-handler"
25
+ import { WebSocketServer } from "ws"
26
+ import { randomUUID } from "crypto"
27
+ import { Mutex } from "async-mutex"
28
+ import { CreateArgv } from "./args.js"
29
+ import { globby } from "globby"
30
+ import {
31
+ exitIfCancel,
32
+ escapePath,
33
+ gitPull,
34
+ popContentFolder,
35
+ stashContentFolder,
36
+ } from "./helpers.js"
37
+ import {
38
+ UPSTREAM_NAME,
39
+ QUARTZ_SOURCE_BRANCH,
40
+ ORIGIN_NAME,
41
+ version,
42
+ fp,
43
+ cacheFile,
44
+ cwd,
45
+ } from "./constants.js"
46
+
47
+ /**
48
+ * Resolve content directory path
49
+ * @param contentPath path to resolve
50
+ */
51
+ function resolveContentPath(contentPath) {
52
+ if (path.isAbsolute(contentPath)) return path.relative(cwd, contentPath)
53
+ return path.join(cwd, contentPath)
54
+ }
55
+
56
+ /**
57
+ * Handles `npx quartz create`
58
+ * @param {*} argv arguments for `create`
59
+ */
60
+ export async function handleCreate(argv) {
61
+ console.log()
62
+ intro(styleText(["bgGreen", "black"], ` Quartz v${version} `))
63
+ const contentFolder = resolveContentPath(argv.directory)
64
+ let setupStrategy = argv.strategy?.toLowerCase()
65
+ let linkResolutionStrategy = argv.links?.toLowerCase()
66
+ const sourceDirectory = argv.source
67
+
68
+ // If all cmd arguments were provided, check if they're valid
69
+ if (setupStrategy && linkResolutionStrategy) {
70
+ // If setup isn't, "new", source argument is required
71
+ if (setupStrategy !== "new") {
72
+ // Error handling
73
+ if (!sourceDirectory) {
74
+ outro(
75
+ styleText(
76
+ "red",
77
+ `Setup strategies (arg '${styleText(
78
+ "yellow",
79
+ `-${CreateArgv.strategy.alias[0]}`,
80
+ )}') other than '${styleText(
81
+ "yellow",
82
+ "new",
83
+ )}' require content folder argument ('${styleText(
84
+ "yellow",
85
+ `-${CreateArgv.source.alias[0]}`,
86
+ )}') to be set`,
87
+ ),
88
+ )
89
+ process.exit(1)
90
+ } else {
91
+ if (!fs.existsSync(sourceDirectory)) {
92
+ outro(
93
+ styleText(
94
+ "red",
95
+ `Input directory to copy/symlink 'content' from not found ('${styleText(
96
+ "yellow",
97
+ sourceDirectory,
98
+ )}', invalid argument "${styleText("yellow", `-${CreateArgv.source.alias[0]}`)})`,
99
+ ),
100
+ )
101
+ process.exit(1)
102
+ } else if (!fs.lstatSync(sourceDirectory).isDirectory()) {
103
+ outro(
104
+ styleText(
105
+ "red",
106
+ `Source directory to copy/symlink 'content' from is not a directory (found file at '${styleText(
107
+ "yellow",
108
+ sourceDirectory,
109
+ )}', invalid argument ${styleText("yellow", `-${CreateArgv.source.alias[0]}`)}")`,
110
+ ),
111
+ )
112
+ process.exit(1)
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ // Use cli process if cmd args werent provided
119
+ if (!setupStrategy) {
120
+ setupStrategy = exitIfCancel(
121
+ await select({
122
+ message: `Choose how to initialize the content in \`${contentFolder}\``,
123
+ options: [
124
+ { value: "new", label: "Empty Quartz" },
125
+ { value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" },
126
+ {
127
+ value: "symlink",
128
+ label: "Symlink an existing folder",
129
+ hint: "don't select this unless you know what you are doing!",
130
+ },
131
+ ],
132
+ }),
133
+ )
134
+ }
135
+
136
+ async function rmContentFolder() {
137
+ const contentStat = await fs.promises.lstat(contentFolder)
138
+ if (contentStat.isSymbolicLink()) {
139
+ await fs.promises.unlink(contentFolder)
140
+ } else {
141
+ await rm(contentFolder, { recursive: true, force: true })
142
+ }
143
+ }
144
+
145
+ const gitkeepPath = path.join(contentFolder, ".gitkeep")
146
+ if (fs.existsSync(gitkeepPath)) {
147
+ await fs.promises.unlink(gitkeepPath)
148
+ }
149
+ if (setupStrategy === "copy" || setupStrategy === "symlink") {
150
+ let originalFolder = sourceDirectory
151
+
152
+ // If input directory was not passed, use cli
153
+ if (!sourceDirectory) {
154
+ originalFolder = escapePath(
155
+ exitIfCancel(
156
+ await text({
157
+ message: "Enter the full path to existing content folder",
158
+ placeholder:
159
+ "On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path",
160
+ validate(fp) {
161
+ const fullPath = escapePath(fp)
162
+ if (!fs.existsSync(fullPath)) {
163
+ return "The given path doesn't exist"
164
+ } else if (!fs.lstatSync(fullPath).isDirectory()) {
165
+ return "The given path is not a folder"
166
+ }
167
+ },
168
+ }),
169
+ ),
170
+ )
171
+ }
172
+
173
+ await rmContentFolder()
174
+ if (setupStrategy === "copy") {
175
+ await fs.promises.cp(originalFolder, contentFolder, {
176
+ recursive: true,
177
+ preserveTimestamps: true,
178
+ })
179
+ } else if (setupStrategy === "symlink") {
180
+ await fs.promises.symlink(originalFolder, contentFolder, "dir")
181
+ }
182
+ } else if (setupStrategy === "new") {
183
+ await fs.promises.writeFile(
184
+ path.join(contentFolder, "index.md"),
185
+ `---
186
+ title: Welcome to Quartz
187
+ ---
188
+
189
+ This is a blank Quartz installation.
190
+ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
191
+ `,
192
+ )
193
+ }
194
+
195
+ // Use cli process if cmd args werent provided
196
+ if (!linkResolutionStrategy) {
197
+ // get a preferred link resolution strategy
198
+ linkResolutionStrategy = exitIfCancel(
199
+ await select({
200
+ message: `Choose how Quartz should resolve links in your content. This should match Obsidian's link format. You can change this later in \`quartz.config.ts\`.`,
201
+ options: [
202
+ {
203
+ value: "shortest",
204
+ label: "Treat links as shortest path",
205
+ hint: "(default)",
206
+ },
207
+ {
208
+ value: "absolute",
209
+ label: "Treat links as absolute path",
210
+ },
211
+ {
212
+ value: "relative",
213
+ label: "Treat links as relative paths",
214
+ },
215
+ ],
216
+ }),
217
+ )
218
+ }
219
+
220
+ // now, do config changes
221
+ const configFilePath = path.join(cwd, "quartz.config.ts")
222
+ let configContent = await fs.promises.readFile(configFilePath, { encoding: "utf-8" })
223
+ configContent = configContent.replace(
224
+ /markdownLinkResolution: '(.+)'/,
225
+ `markdownLinkResolution: '${linkResolutionStrategy}'`,
226
+ )
227
+ await fs.promises.writeFile(configFilePath, configContent)
228
+
229
+ // setup remote
230
+ execSync(
231
+ `git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
232
+ { stdio: "ignore" },
233
+ )
234
+
235
+ outro(`You're all set! Not sure what to do next? Try:
236
+ • Customizing Quartz a bit more by editing \`quartz.config.ts\`
237
+ • Running \`npx quartz build --serve\` to preview your Quartz locally
238
+ • Hosting your Quartz online (see: https://quartz.jzhao.xyz/hosting)
239
+ `)
240
+ }
241
+
242
+ /**
243
+ * Handles `npx quartz build`
244
+ * @param {*} argv arguments for `build`
245
+ */
246
+ export async function handleBuild(argv) {
247
+ if (argv.serve) {
248
+ argv.watch = true
249
+ }
250
+
251
+ console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`)
252
+ const ctx = await esbuild.context({
253
+ entryPoints: [fp],
254
+ outfile: cacheFile,
255
+ bundle: true,
256
+ keepNames: true,
257
+ minifyWhitespace: true,
258
+ minifySyntax: true,
259
+ platform: "node",
260
+ format: "esm",
261
+ jsx: "automatic",
262
+ jsxImportSource: "preact",
263
+ packages: "external",
264
+ metafile: true,
265
+ sourcemap: true,
266
+ sourcesContent: false,
267
+ plugins: [
268
+ sassPlugin({
269
+ type: "css-text",
270
+ cssImports: true,
271
+ }),
272
+ sassPlugin({
273
+ filter: /\.inline\.scss$/,
274
+ type: "css",
275
+ cssImports: true,
276
+ }),
277
+ {
278
+ name: "inline-script-loader",
279
+ setup(build) {
280
+ build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => {
281
+ let text = await promises.readFile(args.path, "utf8")
282
+
283
+ // remove default exports that we manually inserted
284
+ text = text.replace("export default", "")
285
+ text = text.replace("export", "")
286
+
287
+ const sourcefile = path.relative(path.resolve("."), args.path)
288
+ const resolveDir = path.dirname(sourcefile)
289
+ const transpiled = await esbuild.build({
290
+ stdin: {
291
+ contents: text,
292
+ loader: "ts",
293
+ resolveDir,
294
+ sourcefile,
295
+ },
296
+ write: false,
297
+ bundle: true,
298
+ minify: true,
299
+ platform: "browser",
300
+ format: "esm",
301
+ })
302
+ const rawMod = transpiled.outputFiles[0].text
303
+ return {
304
+ contents: rawMod,
305
+ loader: "text",
306
+ }
307
+ })
308
+ },
309
+ },
310
+ ],
311
+ })
312
+
313
+ const buildMutex = new Mutex()
314
+ let lastBuildMs = 0
315
+ let cleanupBuild = null
316
+ const build = async (clientRefresh) => {
317
+ const buildStart = new Date().getTime()
318
+ lastBuildMs = buildStart
319
+ const release = await buildMutex.acquire()
320
+ if (lastBuildMs > buildStart) {
321
+ release()
322
+ return
323
+ }
324
+
325
+ if (cleanupBuild) {
326
+ console.log(styleText("yellow", "Detected a source code change, doing a hard rebuild..."))
327
+ await cleanupBuild()
328
+ }
329
+
330
+ const result = await ctx.rebuild().catch((err) => {
331
+ console.error(`${styleText("red", "Couldn't parse Quartz configuration:")} ${fp}`)
332
+ console.log(`Reason: ${styleText("gray", err)}`)
333
+ process.exit(1)
334
+ })
335
+ release()
336
+
337
+ if (argv.bundleInfo) {
338
+ const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs"
339
+ const meta = result.metafile.outputs[outputFileName]
340
+ console.log(
341
+ `Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(
342
+ meta.bytes,
343
+ )})`,
344
+ )
345
+ console.log(await esbuild.analyzeMetafile(result.metafile, { color: true }))
346
+ }
347
+
348
+ // bypass module cache
349
+ // https://github.com/nodejs/modules/issues/307
350
+ const { default: buildQuartz } = await import(`../../${cacheFile}?update=${randomUUID()}`)
351
+ // ^ this import is relative, so base "cacheFile" path can't be used
352
+
353
+ cleanupBuild = await buildQuartz(argv, buildMutex, clientRefresh)
354
+ clientRefresh()
355
+ }
356
+
357
+ let clientRefresh = () => {}
358
+ if (argv.serve) {
359
+ const connections = []
360
+ clientRefresh = () => connections.forEach((conn) => conn.send("rebuild"))
361
+
362
+ if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) {
363
+ argv.baseDir = "/" + argv.baseDir
364
+ }
365
+
366
+ await build(clientRefresh)
367
+ const server = http.createServer(async (req, res) => {
368
+ if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) {
369
+ console.log(
370
+ styleText(
371
+ "red",
372
+ `[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`,
373
+ ),
374
+ )
375
+ res.writeHead(404)
376
+ res.end()
377
+ return
378
+ }
379
+
380
+ // strip baseDir prefix
381
+ req.url = req.url?.slice(argv.baseDir.length)
382
+
383
+ const serve = async () => {
384
+ const release = await buildMutex.acquire()
385
+ await serveHandler(req, res, {
386
+ public: argv.output,
387
+ directoryListing: false,
388
+ headers: [
389
+ {
390
+ source: "**/*.*",
391
+ headers: [{ key: "Content-Disposition", value: "inline" }],
392
+ },
393
+ {
394
+ source: "**/*.webp",
395
+ headers: [{ key: "Content-Type", value: "image/webp" }],
396
+ },
397
+ // fixes bug where avif images are displayed as text instead of images (future proof)
398
+ {
399
+ source: "**/*.avif",
400
+ headers: [{ key: "Content-Type", value: "image/avif" }],
401
+ },
402
+ ],
403
+ })
404
+ const status = res.statusCode
405
+ const statusString =
406
+ status >= 200 && status < 300
407
+ ? styleText("green", `[${status}]`)
408
+ : styleText("red", `[${status}]`)
409
+ console.log(statusString + styleText("gray", ` ${argv.baseDir}${req.url}`))
410
+ release()
411
+ }
412
+
413
+ const redirect = (newFp) => {
414
+ newFp = argv.baseDir + newFp
415
+ res.writeHead(302, {
416
+ Location: newFp,
417
+ })
418
+ console.log(
419
+ styleText("yellow", "[302]") +
420
+ styleText("gray", ` ${argv.baseDir}${req.url} -> ${newFp}`),
421
+ )
422
+ res.end()
423
+ }
424
+
425
+ let fp = req.url?.split("?")[0] ?? "/"
426
+
427
+ // handle redirects
428
+ if (fp.endsWith("/")) {
429
+ // /trailing/
430
+ // does /trailing/index.html exist? if so, serve it
431
+ const indexFp = path.posix.join(fp, "index.html")
432
+ if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
433
+ req.url = fp
434
+ return serve()
435
+ }
436
+
437
+ // does /trailing.html exist? if so, redirect to /trailing
438
+ let base = fp.slice(0, -1)
439
+ if (path.extname(base) === "") {
440
+ base += ".html"
441
+ }
442
+ if (fs.existsSync(path.posix.join(argv.output, base))) {
443
+ return redirect(fp.slice(0, -1))
444
+ }
445
+ } else {
446
+ // /regular
447
+ // does /regular.html exist? if so, serve it
448
+ let base = fp
449
+ if (path.extname(base) === "") {
450
+ base += ".html"
451
+ }
452
+ if (fs.existsSync(path.posix.join(argv.output, base))) {
453
+ req.url = fp
454
+ return serve()
455
+ }
456
+
457
+ // does /regular/index.html exist? if so, redirect to /regular/
458
+ let indexFp = path.posix.join(fp, "index.html")
459
+ if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
460
+ return redirect(fp + "/")
461
+ }
462
+ }
463
+
464
+ return serve()
465
+ })
466
+
467
+ server.listen(argv.port)
468
+ const wss = new WebSocketServer({ port: argv.wsPort })
469
+ wss.on("connection", (ws) => connections.push(ws))
470
+ console.log(
471
+ styleText(
472
+ "cyan",
473
+ `Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`,
474
+ ),
475
+ )
476
+ } else {
477
+ await build(clientRefresh)
478
+ ctx.dispose()
479
+ }
480
+
481
+ if (argv.watch) {
482
+ const paths = await globby([
483
+ "**/*.ts",
484
+ "quartz/cli/*.js",
485
+ "quartz/static/**/*",
486
+ "**/*.tsx",
487
+ "**/*.scss",
488
+ "package.json",
489
+ ])
490
+ chokidar
491
+ .watch(paths, { ignoreInitial: true })
492
+ .on("add", () => build(clientRefresh))
493
+ .on("change", () => build(clientRefresh))
494
+ .on("unlink", () => build(clientRefresh))
495
+
496
+ console.log(styleText("gray", "hint: exit with ctrl+c"))
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Handles `npx quartz update`
502
+ * @param {*} argv arguments for `update`
503
+ */
504
+ export async function handleUpdate(argv) {
505
+ const contentFolder = resolveContentPath(argv.directory)
506
+ console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`)
507
+ console.log("Backing up your content")
508
+ execSync(
509
+ `git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
510
+ )
511
+ await stashContentFolder(contentFolder)
512
+ console.log(
513
+ "Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
514
+ )
515
+
516
+ try {
517
+ gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
518
+ } catch {
519
+ console.log(styleText("red", "An error occurred above while pulling updates."))
520
+ await popContentFolder(contentFolder)
521
+ return
522
+ }
523
+
524
+ await popContentFolder(contentFolder)
525
+ console.log("Ensuring dependencies are up to date")
526
+
527
+ /*
528
+ On Windows, if the command `npm` is really `npm.cmd', this call fails
529
+ as it will be unable to find `npm`. This is often the case on systems
530
+ where `npm` is installed via a package manager.
531
+
532
+ This means `npx quartz update` will not actually update dependencies
533
+ on Windows, without a manual `npm i` from the caller.
534
+
535
+ However, by spawning a shell, we are able to call `npm.cmd`.
536
+ See: https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
537
+ */
538
+
539
+ const opts = { stdio: "inherit" }
540
+ if (process.platform === "win32") {
541
+ opts.shell = true
542
+ }
543
+
544
+ const res = spawnSync("npm", ["i"], opts)
545
+ if (res.status === 0) {
546
+ console.log(styleText("green", "Done!"))
547
+ } else {
548
+ console.log(styleText("red", "An error occurred above while installing dependencies."))
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Handles `npx quartz restore`
554
+ * @param {*} argv arguments for `restore`
555
+ */
556
+ export async function handleRestore(argv) {
557
+ const contentFolder = resolveContentPath(argv.directory)
558
+ await popContentFolder(contentFolder)
559
+ }
560
+
561
+ /**
562
+ * Handles `npx quartz sync`
563
+ * @param {*} argv arguments for `sync`
564
+ */
565
+ export async function handleSync(argv) {
566
+ const contentFolder = resolveContentPath(argv.directory)
567
+ console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)}\n`)
568
+ console.log("Backing up your content")
569
+
570
+ if (argv.commit) {
571
+ const contentStat = await fs.promises.lstat(contentFolder)
572
+ if (contentStat.isSymbolicLink()) {
573
+ const linkTarg = await fs.promises.readlink(contentFolder)
574
+ console.log(styleText("yellow", "Detected symlink, trying to dereference before committing"))
575
+
576
+ // stash symlink file
577
+ await stashContentFolder(contentFolder)
578
+
579
+ // follow symlink and copy content
580
+ await fs.promises.cp(linkTarg, contentFolder, {
581
+ recursive: true,
582
+ preserveTimestamps: true,
583
+ })
584
+ }
585
+
586
+ const currentTimestamp = new Date().toLocaleString("en-US", {
587
+ dateStyle: "medium",
588
+ timeStyle: "short",
589
+ })
590
+ const commitMessage = argv.message ?? `Quartz sync: ${currentTimestamp}`
591
+ spawnSync("git", ["add", "."], { stdio: "inherit" })
592
+ spawnSync("git", ["commit", "-m", commitMessage], { stdio: "inherit" })
593
+
594
+ if (contentStat.isSymbolicLink()) {
595
+ // put symlink back
596
+ await popContentFolder(contentFolder)
597
+ }
598
+ }
599
+
600
+ await stashContentFolder(contentFolder)
601
+
602
+ if (argv.pull) {
603
+ console.log(
604
+ "Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.",
605
+ )
606
+ try {
607
+ gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
608
+ } catch {
609
+ console.log(styleText("red", "An error occurred above while pulling updates."))
610
+ await popContentFolder(contentFolder)
611
+ return
612
+ }
613
+ }
614
+
615
+ await popContentFolder(contentFolder)
616
+ if (argv.push) {
617
+ console.log("Pushing your changes")
618
+ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD").toString().trim()
619
+ const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, currentBranch], {
620
+ stdio: "inherit",
621
+ })
622
+ if (res.status !== 0) {
623
+ console.log(
624
+ styleText("red", `An error occurred above while pushing to remote ${ORIGIN_NAME}.`),
625
+ )
626
+ return
627
+ }
628
+ }
629
+
630
+ console.log(styleText("green", "Done!"))
631
+ }
632
+
633
+ // brain overlay (T4): re-export partial-build handler as partialBuildContent.
634
+ // The full machinery lives in build_partial_handler.js (single responsibility).
635
+ // bootstrap-cli.mjs imports this as partialBuildContent and wires it to build-partial.
636
+ export { handlePartialBuild as partialBuildContent } from "./build_partial_handler.js"