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,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"
|