world-building-framework 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +104 -2
- package/dist/cli/check.d.ts +13 -0
- package/dist/cli/check.js +11 -0
- package/dist/cli/commands/create.d.ts +26 -0
- package/dist/cli/commands/create.js +35 -0
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.js +119 -0
- package/dist/cli/commands/list-variants.d.ts +1 -0
- package/dist/cli/commands/list-variants.js +7 -0
- package/dist/cli/commands/publish.d.ts +24 -0
- package/dist/cli/commands/publish.js +71 -0
- package/dist/cli/commands/refresh.d.ts +29 -0
- package/dist/cli/commands/refresh.js +70 -0
- package/dist/cli/commands/render.d.ts +14 -0
- package/dist/cli/commands/render.js +27 -0
- package/dist/cli/commands/schema.d.ts +2 -0
- package/dist/cli/commands/schema.js +9 -0
- package/dist/cli/commands/search.d.ts +9 -0
- package/dist/cli/commands/search.js +20 -0
- package/dist/cli/commands/update.d.ts +23 -0
- package/dist/cli/commands/update.js +69 -0
- package/dist/cli/commands/validate.d.ts +21 -0
- package/dist/cli/commands/validate.js +44 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +157 -0
- package/dist/cli/io.d.ts +4 -0
- package/dist/cli/io.js +25 -0
- package/dist/cli/managed-files.d.ts +35 -0
- package/dist/cli/managed-files.js +80 -0
- package/dist/cli/quartz-runner.d.ts +22 -0
- package/dist/cli/quartz-runner.js +76 -0
- package/dist/cli/quartz-setup.d.ts +10 -0
- package/dist/cli/quartz-setup.js +59 -0
- package/dist/cli/vault-path.d.ts +7 -0
- package/dist/cli/vault-path.js +44 -0
- package/dist/core/config/build-variants.d.ts +5 -0
- package/dist/core/config/build-variants.js +13 -0
- package/dist/core/config/index.d.ts +4 -0
- package/dist/core/config/index.js +4 -0
- package/dist/core/config/load.d.ts +4 -0
- package/dist/core/config/load.js +25 -0
- package/dist/core/config/schema.d.ts +229 -0
- package/dist/core/config/schema.js +23 -0
- package/dist/core/config/validate-options.d.ts +4 -0
- package/dist/core/config/validate-options.js +4 -0
- package/dist/core/graph/build.d.ts +8 -0
- package/dist/core/graph/build.js +42 -0
- package/dist/core/graph/causal.d.ts +22 -0
- package/dist/core/graph/causal.js +106 -0
- package/dist/core/graph/dating.d.ts +18 -0
- package/dist/core/graph/dating.js +30 -0
- package/dist/core/graph/entities.d.ts +13 -0
- package/dist/core/graph/entities.js +22 -0
- package/dist/core/graph/index.d.ts +7 -0
- package/dist/core/graph/index.js +7 -0
- package/dist/core/graph/queries.d.ts +3 -0
- package/dist/core/graph/queries.js +8 -0
- package/dist/core/graph/types.d.ts +14 -0
- package/dist/core/graph/types.js +1 -0
- package/dist/core/graph/versioning.d.ts +15 -0
- package/dist/core/graph/versioning.js +50 -0
- package/dist/core/id.d.ts +3 -0
- package/dist/core/id.js +9 -0
- package/dist/core/lint/after-loops.d.ts +15 -0
- package/dist/core/lint/after-loops.js +34 -0
- package/dist/core/lint/dangling-links.d.ts +9 -0
- package/dist/core/lint/dangling-links.js +46 -0
- package/dist/core/lint/events-without-entities.d.ts +4 -0
- package/dist/core/lint/events-without-entities.js +9 -0
- package/dist/core/lint/forked-versions.d.ts +14 -0
- package/dist/core/lint/forked-versions.js +29 -0
- package/dist/core/lint/index.d.ts +6 -0
- package/dist/core/lint/index.js +6 -0
- package/dist/core/lint/lint.d.ts +24 -0
- package/dist/core/lint/lint.js +50 -0
- package/dist/core/lint/target-types.d.ts +9 -0
- package/dist/core/lint/target-types.js +47 -0
- package/dist/core/query/dates.d.ts +70 -0
- package/dist/core/query/dates.js +43 -0
- package/dist/core/query/index.d.ts +2 -0
- package/dist/core/query/index.js +2 -0
- package/dist/core/query/types.d.ts +75 -0
- package/dist/core/query/types.js +12 -0
- package/dist/core/schema/describe.d.ts +8 -0
- package/dist/core/schema/describe.js +51 -0
- package/dist/core/schema/fields.d.ts +249 -0
- package/dist/core/schema/fields.js +43 -0
- package/dist/core/schema/index.d.ts +4 -0
- package/dist/core/schema/index.js +4 -0
- package/dist/core/schema/validate.d.ts +22 -0
- package/dist/core/schema/validate.js +34 -0
- package/dist/core/schema/wikilink.d.ts +8 -0
- package/dist/core/schema/wikilink.js +23 -0
- package/dist/core/search/build.d.ts +11 -0
- package/dist/core/search/build.js +26 -0
- package/dist/core/search/columns.d.ts +9 -0
- package/dist/core/search/columns.js +14 -0
- package/dist/core/search/index.d.ts +3 -0
- package/dist/core/search/index.js +3 -0
- package/dist/core/search/search.d.ts +3 -0
- package/dist/core/search/search.js +35 -0
- package/dist/core/search/types.d.ts +18 -0
- package/dist/core/search/types.js +1 -0
- package/dist/core/vault/index.d.ts +6 -0
- package/dist/core/vault/index.js +6 -0
- package/dist/core/vault/parse.d.ts +8 -0
- package/dist/core/vault/parse.js +11 -0
- package/dist/core/vault/read.d.ts +13 -0
- package/dist/core/vault/read.js +62 -0
- package/dist/core/vault/relink.d.ts +10 -0
- package/dist/core/vault/relink.js +33 -0
- package/dist/core/vault/serialize.d.ts +4 -0
- package/dist/core/vault/serialize.js +4 -0
- package/dist/core/vault/types.d.ts +5 -0
- package/dist/core/vault/types.js +1 -0
- package/dist/core/vault/write.d.ts +18 -0
- package/dist/core/vault/write.js +37 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/render-pipeline/build.d.ts +9 -0
- package/dist/render-pipeline/build.js +45 -0
- package/dist/render-pipeline/filter.d.ts +21 -0
- package/dist/render-pipeline/filter.js +80 -0
- package/dist/render-pipeline/index.d.ts +10 -0
- package/dist/render-pipeline/index.js +10 -0
- package/dist/render-pipeline/merge.d.ts +11 -0
- package/dist/render-pipeline/merge.js +21 -0
- package/dist/render-pipeline/quartz-compat.d.ts +1 -0
- package/dist/render-pipeline/quartz-compat.js +18 -0
- package/dist/render-pipeline/rewrite.d.ts +8 -0
- package/dist/render-pipeline/rewrite.js +23 -0
- package/dist/render-pipeline/slugs.d.ts +7 -0
- package/dist/render-pipeline/slugs.js +30 -0
- package/dist/render-pipeline/sync.d.ts +9 -0
- package/dist/render-pipeline/sync.js +18 -0
- package/dist/render-pipeline/types.d.ts +8 -0
- package/dist/render-pipeline/types.js +1 -0
- package/dist/render-pipeline/write.d.ts +3 -0
- package/dist/render-pipeline/write.js +12 -0
- package/package.json +46 -4
- package/skills/world-vault/SKILL.md +100 -0
- package/templates/AGENTS.md +11 -0
- package/templates/gitignore +5 -0
- package/templates/publish.yml +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Copyright (c) 2026 Marius Faust. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and its source code are proprietary. No license is granted to
|
|
4
|
+
use, copy, modify, merge, publish, distribute, sublicense, or sell any part of
|
|
5
|
+
it, in source or compiled form, without the prior written permission of the
|
|
6
|
+
copyright holder.
|
|
7
|
+
|
|
8
|
+
Being able to view this repository or download this software does not grant
|
|
9
|
+
any such permission.
|
package/README.md
CHANGED
|
@@ -1,3 +1,105 @@
|
|
|
1
|
-
#
|
|
1
|
+
# World Building Framework
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build a fictional world as a folder of Markdown files, and publish it as a wiki on GitHub Pages: one site per audience, such as a player wiki and a GM wiki. You write with any AI agent, through Obsidian, or by hand. The `wbf` command checks every change, so links stay valid and history is kept.
|
|
4
|
+
|
|
5
|
+
## Start a world
|
|
6
|
+
|
|
7
|
+
You need [Node.js 22](https://nodejs.org/) or newer and git.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
mkdir my-world
|
|
11
|
+
cd my-world
|
|
12
|
+
git init
|
|
13
|
+
npx world-building-framework init --agent claude
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`init` sets the folder up as a world. `--agent claude` also installs skills for Claude Code. Leave it out if you use another agent; the generated `AGENTS.md` works with any agent.
|
|
18
|
+
|
|
19
|
+
`init` never overwrites a file you already have. It creates:
|
|
20
|
+
- `package.json`, with the framework version pinned
|
|
21
|
+
- `world.config.json`
|
|
22
|
+
- `.gitignore`
|
|
23
|
+
- `AGENTS.md`
|
|
24
|
+
- a GitHub Pages workflow
|
|
25
|
+
- Obsidian settings that hide tooling folders
|
|
26
|
+
|
|
27
|
+
Then put it on GitHub:
|
|
28
|
+
|
|
29
|
+
1. Create a GitHub repo, commit everything (including `package-lock.json` and `.wbf/managed.json`), and push.
|
|
30
|
+
2. In the repo on GitHub: **Settings → Pages → Source: GitHub Actions**. On a free account, Pages needs the repo to be public.
|
|
31
|
+
3. Every push to your default branch now publishes the wiki to `https://<you>.github.io/<repo>/wiki/`, with a landing page at `https://<you>.github.io/<repo>/`.
|
|
32
|
+
|
|
33
|
+
## Writing your world
|
|
34
|
+
|
|
35
|
+
**With an AI agent:** ask it to add or change things ("add a character called Alea, a ship's captain in Port Kavas"). It uses the `wbf` command through the installed skill or `AGENTS.md`.
|
|
36
|
+
|
|
37
|
+
**From the command line,** inside your world folder:
|
|
38
|
+
|
|
39
|
+
| Command | What it does |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `npx wbf schema` | Shows which fields files can have |
|
|
42
|
+
| `npx wbf search <terms>` | Keyword search, returns each file's UUID |
|
|
43
|
+
| `npx wbf create` | Creates a file from JSON on stdin; checks it first |
|
|
44
|
+
| `npx wbf update --id <uuid>` | Replaces a file's content, for corrections |
|
|
45
|
+
| `npx wbf validate` | Checks the whole world |
|
|
46
|
+
| `npx wbf render --variant wiki --build` | Builds one wiki locally |
|
|
47
|
+
| `npx wbf publish --output site` | Builds every wiki, as the workflow does |
|
|
48
|
+
| `npx wbf --help` | Every command and option |
|
|
49
|
+
|
|
50
|
+
Run `npx wbf` only inside a world folder. Elsewhere it would fetch an unrelated npm package that happens to be called `wbf`.
|
|
51
|
+
|
|
52
|
+
**In Obsidian:** open the world folder as a vault to browse and read. Create and change files through `wbf`, so they get valid UUID names and checked links.
|
|
53
|
+
|
|
54
|
+
## How a world is organized
|
|
55
|
+
|
|
56
|
+
- **Files are named by UUID,** e.g. `7c1e9b2a-….md`. The readable name is the `title` field, so two characters can share a name.
|
|
57
|
+
- **Two kinds of file:** `type: entity` (characters, places, factions, …) and `type: event` (things that happen).
|
|
58
|
+
- **Links** look like `[[<uuid>|Display Name]]`. Any frontmatter field holding one is a relation, whatever you call it.
|
|
59
|
+
- **Events are ordered by `after`**, a link to the event(s) they follow. A `date: {year, day}` is optional.
|
|
60
|
+
- **Changes over time** become new files: a new version of an entity points at the old one with `supersedes`, and at the event that changed it with `causedBy`. The wiki shows the whole history on one page.
|
|
61
|
+
|
|
62
|
+
## world.config.json
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"title": "The Kavas Chronicles",
|
|
67
|
+
"entityTypes": ["character", "location", "faction"],
|
|
68
|
+
"buildVariants": {
|
|
69
|
+
"player-wiki": { "tags": ["player"], "dateRange": { "to": { "year": 150 } } },
|
|
70
|
+
"gm-wiki": {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **`title`:** shown on every wiki page and the landing page.
|
|
76
|
+
- **`entityTypes`:** the allowed `entityType` values. Leave it out to allow any.
|
|
77
|
+
- **`buildVariants`:** one wiki per entry, published at `/<name>/`. `init` starts you with one unfiltered `wiki`.
|
|
78
|
+
- `tags`: a file must carry all of them to be included.
|
|
79
|
+
- `dateRange`: limits events by in-world date. An undated event is placed by its `after` links and only included if it surely falls inside the range.
|
|
80
|
+
- `{}` includes everything.
|
|
81
|
+
|
|
82
|
+
**Every wiki is public.** Anyone who knows or guesses a variant's address can open it. Variants decide what each wiki contains; they don't lock anything.
|
|
83
|
+
|
|
84
|
+
## Preview locally
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
npx wbf render --variant wiki --build
|
|
88
|
+
cd .wbf/quartz
|
|
89
|
+
node ./quartz/bootstrap-cli.mjs build --serve
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The first build downloads Quartz, the site generator, into `.wbf/quartz/`.
|
|
93
|
+
|
|
94
|
+
## Updating the framework
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
npm install world-building-framework@latest
|
|
98
|
+
npx wbf refresh
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`refresh` updates the files `init` generated (the workflow, `AGENTS.md`, skills) to the new version. It leaves any file you've edited alone and lists it. `npx wbf init --agent <name>` adds skills for another agent later.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
Proprietary, all rights reserved. See [LICENSE](LICENSE). No use, copying or distribution without written permission.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ValidateOptions, type ValidationIssue } from "../core/schema/index.js";
|
|
2
|
+
import { type LintContext } from "../core/lint/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Every check a file gets before it's written, or when validated: schema
|
|
5
|
+
* validation plus lint against the whole vault (`context`, from
|
|
6
|
+
* createLintContext). Shared by create, update and validate so they can
|
|
7
|
+
* never disagree. Pass `id` for a file that already exists in the vault.
|
|
8
|
+
*/
|
|
9
|
+
export declare function checkFile(file: {
|
|
10
|
+
id?: string;
|
|
11
|
+
frontmatter: Record<string, unknown>;
|
|
12
|
+
body: string;
|
|
13
|
+
}, options: ValidateOptions, context: LintContext): ValidationIssue[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { validateFrontmatter } from "../core/schema/index.js";
|
|
2
|
+
import { lintFile } from "../core/lint/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Every check a file gets before it's written, or when validated: schema
|
|
5
|
+
* validation plus lint against the whole vault (`context`, from
|
|
6
|
+
* createLintContext). Shared by create, update and validate so they can
|
|
7
|
+
* never disagree. Pass `id` for a file that already exists in the vault.
|
|
8
|
+
*/
|
|
9
|
+
export function checkFile(file, options, context) {
|
|
10
|
+
return [...validateFrontmatter(file.frontmatter, options).issues, ...lintFile(file, context)];
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type ValidationIssue } from "../../core/schema/index.js";
|
|
3
|
+
/** {"frontmatter": {...}, "body": "..."} — the input shape for both create and update. */
|
|
4
|
+
export declare const fileInputSchema: z.ZodObject<{
|
|
5
|
+
frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
6
|
+
body: z.ZodDefault<z.ZodString>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
frontmatter: Record<string, unknown>;
|
|
9
|
+
body: string;
|
|
10
|
+
}, {
|
|
11
|
+
frontmatter: Record<string, unknown>;
|
|
12
|
+
body?: string | undefined;
|
|
13
|
+
}>;
|
|
14
|
+
export interface CreateOutcome {
|
|
15
|
+
written: boolean;
|
|
16
|
+
id?: string;
|
|
17
|
+
path?: string;
|
|
18
|
+
/** Errors (when not written) and warnings (which never block the write). */
|
|
19
|
+
issues?: ValidationIssue[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Lints the candidate against the whole vault before writing, same as `validate` —
|
|
23
|
+
* so a dangling link blocks the write here too instead of silently becoming plain
|
|
24
|
+
* text in the rendered wiki. Lint warnings are returned but don't block.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runCreate(vaultOption: string | undefined, rawInput: unknown): Promise<CreateOutcome>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createVaultFile, readVault } from "../../core/vault/index.js";
|
|
3
|
+
import { hasErrors } from "../../core/schema/index.js";
|
|
4
|
+
import { loadWorldConfig, validateOptionsFromConfig } from "../../core/config/index.js";
|
|
5
|
+
import { resolveVaultDir } from "../vault-path.js";
|
|
6
|
+
import { createLintContext } from "../../core/lint/index.js";
|
|
7
|
+
import { checkFile } from "../check.js";
|
|
8
|
+
/** {"frontmatter": {...}, "body": "..."} — the input shape for both create and update. */
|
|
9
|
+
export const fileInputSchema = z.object({
|
|
10
|
+
frontmatter: z.record(z.string(), z.unknown()),
|
|
11
|
+
body: z.string().default(""),
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Lints the candidate against the whole vault before writing, same as `validate` —
|
|
15
|
+
* so a dangling link blocks the write here too instead of silently becoming plain
|
|
16
|
+
* text in the rendered wiki. Lint warnings are returned but don't block.
|
|
17
|
+
*/
|
|
18
|
+
export async function runCreate(vaultOption, rawInput) {
|
|
19
|
+
const vaultDir = resolveVaultDir(vaultOption);
|
|
20
|
+
const input = fileInputSchema.parse(rawInput);
|
|
21
|
+
const config = await loadWorldConfig(vaultDir);
|
|
22
|
+
const options = validateOptionsFromConfig(config);
|
|
23
|
+
const files = await readVault(vaultDir);
|
|
24
|
+
const issues = checkFile(input, options, createLintContext(files));
|
|
25
|
+
if (hasErrors(issues)) {
|
|
26
|
+
return { written: false, issues };
|
|
27
|
+
}
|
|
28
|
+
const result = await createVaultFile(vaultDir, input.frontmatter, input.body, options);
|
|
29
|
+
if (!result.written) {
|
|
30
|
+
return { written: false, issues: result.issues };
|
|
31
|
+
}
|
|
32
|
+
return issues.length > 0
|
|
33
|
+
? { written: true, id: result.id, path: result.path, issues }
|
|
34
|
+
: { written: true, id: result.id, path: result.path };
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface InitOptions {
|
|
2
|
+
/** Folder to set up; defaults to the current folder. */
|
|
3
|
+
dir?: string;
|
|
4
|
+
/** The world's display title; defaults to one derived from the folder name. */
|
|
5
|
+
title?: string;
|
|
6
|
+
/** Agents to install skills for (see AGENT_SKILL_DIRS). */
|
|
7
|
+
agents?: string[];
|
|
8
|
+
/** Branch the publish workflow runs on; defaults to the git repo's current branch, else "main". */
|
|
9
|
+
branch?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface InitOutcome {
|
|
12
|
+
dir: string;
|
|
13
|
+
created: string[];
|
|
14
|
+
/** Existing files init added to (package.json dependency, .gitignore entries). */
|
|
15
|
+
updated: string[];
|
|
16
|
+
/** Existing files init left alone. Managed ones among them aren't kept current by `refresh`. */
|
|
17
|
+
skipped: string[];
|
|
18
|
+
nextSteps: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Sets a folder up as a world repo (claude-dev/DISTRIBUTION_PLAN.md, component 4).
|
|
22
|
+
* Never overwrites an existing file: it only adds the framework dependency to an
|
|
23
|
+
* existing package.json and missing lines to an existing .gitignore, and reports
|
|
24
|
+
* everything it left alone.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runInit(options?: InitOptions): Promise<InitOutcome>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { WORLD_CONFIG_FILENAME } from "../../core/config/index.js";
|
|
6
|
+
import { AGENT_SKILL_DIRS, contentHash, readFrameworkVersion, readManifest, renderManagedFiles, writeManifest, writeRepoFile, PACKAGE_ROOT, } from "../managed-files.js";
|
|
7
|
+
const PACKAGE_NAME = "world-building-framework";
|
|
8
|
+
function titleFromFolder(dir) {
|
|
9
|
+
const words = path.basename(path.resolve(dir)).split(/[-_\s]+/).filter(Boolean);
|
|
10
|
+
return words.map((word) => word[0].toUpperCase() + word.slice(1)).join(" ") || "My World";
|
|
11
|
+
}
|
|
12
|
+
function detectBranch(dir) {
|
|
13
|
+
const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: dir, encoding: "utf8" });
|
|
14
|
+
const branch = result.status === 0 ? result.stdout.trim() : "";
|
|
15
|
+
return branch && branch !== "HEAD" ? branch : "main";
|
|
16
|
+
}
|
|
17
|
+
function packageNameFromFolder(dir) {
|
|
18
|
+
const name = path.basename(path.resolve(dir)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
19
|
+
return name || "my-world";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sets a folder up as a world repo (claude-dev/DISTRIBUTION_PLAN.md, component 4).
|
|
23
|
+
* Never overwrites an existing file: it only adds the framework dependency to an
|
|
24
|
+
* existing package.json and missing lines to an existing .gitignore, and reports
|
|
25
|
+
* everything it left alone.
|
|
26
|
+
*/
|
|
27
|
+
export async function runInit(options = {}) {
|
|
28
|
+
const dir = path.resolve(options.dir ?? process.cwd());
|
|
29
|
+
const agents = [...new Set(options.agents ?? [])];
|
|
30
|
+
for (const agent of agents) {
|
|
31
|
+
if (!AGENT_SKILL_DIRS[agent]) {
|
|
32
|
+
throw new Error(`Unknown agent "${agent}". Supported: ${Object.keys(AGENT_SKILL_DIRS).join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const version = await readFrameworkVersion();
|
|
36
|
+
const outcome = { dir, created: [], updated: [], skipped: [], nextSteps: [] };
|
|
37
|
+
const exists = (rel) => existsSync(path.join(dir, rel));
|
|
38
|
+
// package.json: create, or add the framework dependency to an existing one.
|
|
39
|
+
if (!exists("package.json")) {
|
|
40
|
+
const pkg = { name: packageNameFromFolder(dir), private: true, dependencies: { [PACKAGE_NAME]: version } };
|
|
41
|
+
await writeRepoFile(dir, "package.json", JSON.stringify(pkg, null, 2) + "\n");
|
|
42
|
+
outcome.created.push("package.json");
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const pkg = JSON.parse(await readFile(path.join(dir, "package.json"), "utf8"));
|
|
46
|
+
const deps = (pkg.dependencies ?? {});
|
|
47
|
+
if (deps[PACKAGE_NAME]) {
|
|
48
|
+
outcome.skipped.push("package.json");
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
pkg.dependencies = { ...deps, [PACKAGE_NAME]: version };
|
|
52
|
+
await writeFile(path.join(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
53
|
+
outcome.updated.push("package.json");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// world.config.json: one unfiltered variant, so a first publish shows everything; edit it to add filtered ones.
|
|
57
|
+
if (!exists(WORLD_CONFIG_FILENAME)) {
|
|
58
|
+
const config = { title: options.title ?? titleFromFolder(dir), entityTypes: [], buildVariants: { wiki: {} } };
|
|
59
|
+
await writeRepoFile(dir, WORLD_CONFIG_FILENAME, JSON.stringify(config, null, 2) + "\n");
|
|
60
|
+
outcome.created.push(WORLD_CONFIG_FILENAME);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
outcome.skipped.push(WORLD_CONFIG_FILENAME);
|
|
64
|
+
}
|
|
65
|
+
// .gitignore: create, or append the lines it's missing.
|
|
66
|
+
const ignoreTemplate = await readFile(path.join(PACKAGE_ROOT, "templates", "gitignore"), "utf8");
|
|
67
|
+
if (!exists(".gitignore")) {
|
|
68
|
+
await writeRepoFile(dir, ".gitignore", ignoreTemplate);
|
|
69
|
+
outcome.created.push(".gitignore");
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const current = await readFile(path.join(dir, ".gitignore"), "utf8");
|
|
73
|
+
const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
74
|
+
const missing = ignoreTemplate.split(/\r?\n/).filter((line) => line && !line.startsWith("#") && !present.has(line));
|
|
75
|
+
if (missing.length > 0) {
|
|
76
|
+
const separator = current.endsWith("\n") || current === "" ? "" : "\n";
|
|
77
|
+
await writeFile(path.join(dir, ".gitignore"), current + separator + missing.join("\n") + "\n", "utf8");
|
|
78
|
+
outcome.updated.push(".gitignore");
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
outcome.skipped.push(".gitignore");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Obsidian: hide tooling folders, so it doesn't list package READMEs.
|
|
85
|
+
if (!exists(".obsidian/app.json")) {
|
|
86
|
+
await writeRepoFile(dir, ".obsidian/app.json", JSON.stringify({ userIgnoreFilters: ["node_modules/", ".wbf/"] }, null, 2) + "\n");
|
|
87
|
+
outcome.created.push(".obsidian/app.json");
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
outcome.skipped.push(".obsidian/app.json");
|
|
91
|
+
}
|
|
92
|
+
// Managed files (workflow, AGENTS.md, skills): written only where nothing exists yet, and recorded so `refresh` can update them.
|
|
93
|
+
const previous = await readManifest(dir);
|
|
94
|
+
const settings = {
|
|
95
|
+
branch: options.branch ?? previous?.branch ?? detectBranch(dir),
|
|
96
|
+
agents: [...new Set([...(previous?.agents ?? []), ...agents])],
|
|
97
|
+
};
|
|
98
|
+
const manifest = { frameworkVersion: version, ...settings, files: { ...(previous?.files ?? {}) } };
|
|
99
|
+
for (const [rel, content] of await renderManagedFiles(version, settings)) {
|
|
100
|
+
if (exists(rel)) {
|
|
101
|
+
if (!manifest.files[rel])
|
|
102
|
+
outcome.skipped.push(rel);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
await writeRepoFile(dir, rel, content);
|
|
106
|
+
manifest.files[rel] = contentHash(content);
|
|
107
|
+
outcome.created.push(rel);
|
|
108
|
+
}
|
|
109
|
+
await writeManifest(dir, manifest);
|
|
110
|
+
outcome.nextSteps = [
|
|
111
|
+
"npm install",
|
|
112
|
+
"Commit everything, including package-lock.json and .wbf/managed.json.",
|
|
113
|
+
"On GitHub: Settings → Pages → Source: GitHub Actions. Then push to publish.",
|
|
114
|
+
];
|
|
115
|
+
if (agents.length === 0) {
|
|
116
|
+
outcome.nextSteps.push(`Optional: install skills for your AI agent with npx wbf init --agent <${Object.keys(AGENT_SKILL_DIRS).join("|")}>.`);
|
|
117
|
+
}
|
|
118
|
+
return outcome;
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runListVariants(vaultOption: string | undefined): Promise<string[]>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { loadWorldConfig, listBuildVariants } from "../../core/config/index.js";
|
|
2
|
+
import { resolveVaultDir } from "../vault-path.js";
|
|
3
|
+
export async function runListVariants(vaultOption) {
|
|
4
|
+
const vaultDir = resolveVaultDir(vaultOption);
|
|
5
|
+
const config = await loadWorldConfig(vaultDir);
|
|
6
|
+
return listBuildVariants(config);
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface PublishOptions {
|
|
2
|
+
quartzDir?: string;
|
|
3
|
+
outputDir: string;
|
|
4
|
+
/** The combined site's public URL (e.g. https://user.github.io/repo). Each variant is built with `<baseUrl>/<variant>`. Omit to leave Quartz's configured baseUrl untouched. */
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PublishedVariant {
|
|
8
|
+
name: string;
|
|
9
|
+
pagesWritten: number;
|
|
10
|
+
outputDir: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PublishOutcome {
|
|
13
|
+
variants: PublishedVariant[];
|
|
14
|
+
landingPage: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function generateLandingPageHtml(variantNames: string[], title: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Builds every declared build variant (subpath topology: each gets its own
|
|
19
|
+
* subfolder of one combined output tree) plus a landing page linking to them.
|
|
20
|
+
* Refuses to publish a vault with zero declared variants rather than falling
|
|
21
|
+
* back to an unfiltered build — that must always be a deliberate choice
|
|
22
|
+
* (REQUIREMENTS.md), never an implicit default.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runPublish(vaultOption: string | undefined, options: PublishOptions): Promise<PublishOutcome>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
3
|
+
import { readVault } from "../../core/vault/index.js";
|
|
4
|
+
import { loadWorldConfig, listBuildVariants, getBuildVariant, worldTitle } from "../../core/config/index.js";
|
|
5
|
+
import { buildDerivedTree, syncDerivedTreeToQuartz } from "../../render-pipeline/index.js";
|
|
6
|
+
import { resolveVaultDir } from "../vault-path.js";
|
|
7
|
+
import { resolveQuartzDir, runQuartzBuild, setQuartzBaseUrl, setQuartzPageTitle, normalizeBaseUrl, } from "../quartz-runner.js";
|
|
8
|
+
import { ensureQuartz } from "../quartz-setup.js";
|
|
9
|
+
function escapeHtml(text) {
|
|
10
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
11
|
+
}
|
|
12
|
+
export function generateLandingPageHtml(variantNames, title) {
|
|
13
|
+
const items = variantNames
|
|
14
|
+
.map((name) => ` <li><a href="./${escapeHtml(name)}/">${escapeHtml(name)}</a></li>`)
|
|
15
|
+
.join("\n");
|
|
16
|
+
return `<!doctype html>
|
|
17
|
+
<html>
|
|
18
|
+
<head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head>
|
|
19
|
+
<body>
|
|
20
|
+
<h1>${escapeHtml(title)}</h1>
|
|
21
|
+
<ul>
|
|
22
|
+
${items}
|
|
23
|
+
</ul>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Builds every declared build variant (subpath topology: each gets its own
|
|
30
|
+
* subfolder of one combined output tree) plus a landing page linking to them.
|
|
31
|
+
* Refuses to publish a vault with zero declared variants rather than falling
|
|
32
|
+
* back to an unfiltered build — that must always be a deliberate choice
|
|
33
|
+
* (REQUIREMENTS.md), never an implicit default.
|
|
34
|
+
*/
|
|
35
|
+
export async function runPublish(vaultOption, options) {
|
|
36
|
+
const vaultDir = resolveVaultDir(vaultOption);
|
|
37
|
+
const quartzDir = resolveQuartzDir(options.quartzDir);
|
|
38
|
+
const outputRoot = path.resolve(process.cwd(), options.outputDir);
|
|
39
|
+
const config = await loadWorldConfig(vaultDir);
|
|
40
|
+
const variantNames = listBuildVariants(config);
|
|
41
|
+
if (variantNames.length === 0) {
|
|
42
|
+
throw new Error("This vault has no declared build variants (world.config.json's buildVariants) — nothing to publish. " +
|
|
43
|
+
"An unfiltered build must be deliberately configured as its own named variant, never an implicit default.");
|
|
44
|
+
}
|
|
45
|
+
await ensureQuartz(quartzDir);
|
|
46
|
+
const files = await readVault(vaultDir);
|
|
47
|
+
const variants = [];
|
|
48
|
+
const title = worldTitle(config);
|
|
49
|
+
await setQuartzPageTitle(quartzDir, title);
|
|
50
|
+
for (const name of variantNames) {
|
|
51
|
+
const query = getBuildVariant(config, name);
|
|
52
|
+
const pages = buildDerivedTree(files, query);
|
|
53
|
+
await syncDerivedTreeToQuartz(path.join(quartzDir, "content"), pages);
|
|
54
|
+
if (options.baseUrl) {
|
|
55
|
+
await setQuartzBaseUrl(quartzDir, `${normalizeBaseUrl(options.baseUrl)}/${name}`);
|
|
56
|
+
}
|
|
57
|
+
const variantOutputDir = path.join(outputRoot, name);
|
|
58
|
+
await runQuartzBuild(quartzDir, variantOutputDir);
|
|
59
|
+
// GitHub Pages only reads CNAME (custom-domain config) at the actual site
|
|
60
|
+
// root, never in a subfolder — verified hands-on that Quartz writes one into
|
|
61
|
+
// every variant's own output regardless, carrying whatever placeholder
|
|
62
|
+
// baseUrl the local Quartz setup happens to have. Harmless as dead weight,
|
|
63
|
+
// but wrong/confusing content, so don't ship it per-variant.
|
|
64
|
+
await rm(path.join(variantOutputDir, "CNAME"), { force: true });
|
|
65
|
+
variants.push({ name, pagesWritten: pages.length, outputDir: variantOutputDir });
|
|
66
|
+
}
|
|
67
|
+
await mkdir(outputRoot, { recursive: true });
|
|
68
|
+
const landingPage = path.join(outputRoot, "index.html");
|
|
69
|
+
await writeFile(landingPage, generateLandingPageHtml(variantNames, title), "utf8");
|
|
70
|
+
return { variants, landingPage };
|
|
71
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface RefreshOptions {
|
|
2
|
+
/** World repo root; defaults to the current folder. */
|
|
3
|
+
dir?: string;
|
|
4
|
+
/** Also overwrite files that were edited, deleted, or never generated by wbf. */
|
|
5
|
+
force?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface KeptFile {
|
|
8
|
+
path: string;
|
|
9
|
+
reason: "edited" | "deleted" | "not-managed";
|
|
10
|
+
}
|
|
11
|
+
export interface RefreshOutcome {
|
|
12
|
+
fromVersion: string;
|
|
13
|
+
toVersion: string;
|
|
14
|
+
created: string[];
|
|
15
|
+
updated: string[];
|
|
16
|
+
unchanged: string[];
|
|
17
|
+
/** Left alone so nothing of yours is lost; `--force` overwrites them. */
|
|
18
|
+
kept: KeptFile[];
|
|
19
|
+
/** Recorded before, but no longer generated by this framework version. The file itself stays. */
|
|
20
|
+
retired: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Brings the files wbf generated (publish workflow, AGENTS.md, agent skills)
|
|
24
|
+
* up to the installed framework version (claude-dev/DISTRIBUTION_PLAN.md,
|
|
25
|
+
* component 4). A file is only overwritten while its content still matches the
|
|
26
|
+
* hash recorded when wbf last wrote it — so anything you edited, deleted, or
|
|
27
|
+
* wrote yourself is kept and reported instead.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runRefresh(options?: RefreshOptions): Promise<RefreshOutcome>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { MANIFEST_PATH, contentHash, readFrameworkVersion, readManifest, renderManagedFiles, writeManifest, writeRepoFile, } from "../managed-files.js";
|
|
5
|
+
/**
|
|
6
|
+
* Brings the files wbf generated (publish workflow, AGENTS.md, agent skills)
|
|
7
|
+
* up to the installed framework version (claude-dev/DISTRIBUTION_PLAN.md,
|
|
8
|
+
* component 4). A file is only overwritten while its content still matches the
|
|
9
|
+
* hash recorded when wbf last wrote it — so anything you edited, deleted, or
|
|
10
|
+
* wrote yourself is kept and reported instead.
|
|
11
|
+
*/
|
|
12
|
+
export async function runRefresh(options = {}) {
|
|
13
|
+
const dir = path.resolve(options.dir ?? process.cwd());
|
|
14
|
+
const manifest = await readManifest(dir);
|
|
15
|
+
if (!manifest) {
|
|
16
|
+
throw new Error(`No ${MANIFEST_PATH} in ${dir} — run wbf init here first.`);
|
|
17
|
+
}
|
|
18
|
+
const toVersion = await readFrameworkVersion();
|
|
19
|
+
const outcome = {
|
|
20
|
+
fromVersion: manifest.frameworkVersion,
|
|
21
|
+
toVersion,
|
|
22
|
+
created: [],
|
|
23
|
+
updated: [],
|
|
24
|
+
unchanged: [],
|
|
25
|
+
kept: [],
|
|
26
|
+
retired: [],
|
|
27
|
+
};
|
|
28
|
+
const rendered = await renderManagedFiles(toVersion, { branch: manifest.branch, agents: manifest.agents });
|
|
29
|
+
const files = {};
|
|
30
|
+
for (const [rel, content] of rendered) {
|
|
31
|
+
const newHash = contentHash(content);
|
|
32
|
+
const recorded = manifest.files[rel];
|
|
33
|
+
const target = path.join(dir, ...rel.split("/"));
|
|
34
|
+
if (!existsSync(target)) {
|
|
35
|
+
if (recorded && !options.force) {
|
|
36
|
+
outcome.kept.push({ path: rel, reason: "deleted" });
|
|
37
|
+
files[rel] = recorded;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
await writeRepoFile(dir, rel, content);
|
|
41
|
+
files[rel] = newHash;
|
|
42
|
+
outcome.created.push(rel);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const currentHash = contentHash(await readFile(target, "utf8"));
|
|
46
|
+
if (!recorded && !options.force) {
|
|
47
|
+
outcome.kept.push({ path: rel, reason: "not-managed" });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (recorded && currentHash !== recorded && !options.force) {
|
|
51
|
+
outcome.kept.push({ path: rel, reason: "edited" });
|
|
52
|
+
files[rel] = recorded;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (currentHash === newHash) {
|
|
56
|
+
files[rel] = newHash;
|
|
57
|
+
outcome.unchanged.push(rel);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
await writeRepoFile(dir, rel, content);
|
|
61
|
+
files[rel] = newHash;
|
|
62
|
+
outcome.updated.push(rel);
|
|
63
|
+
}
|
|
64
|
+
for (const rel of Object.keys(manifest.files)) {
|
|
65
|
+
if (!rendered.has(rel))
|
|
66
|
+
outcome.retired.push(rel);
|
|
67
|
+
}
|
|
68
|
+
await writeManifest(dir, { ...manifest, frameworkVersion: toVersion, files });
|
|
69
|
+
return outcome;
|
|
70
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface RenderOptions {
|
|
2
|
+
variant: string;
|
|
3
|
+
quartzDir?: string;
|
|
4
|
+
/** Absolute or cwd-relative; resolved against process.cwd() before being passed to Quartz's -o flag. */
|
|
5
|
+
outputDir?: string;
|
|
6
|
+
build?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface RenderOutcome {
|
|
9
|
+
variant: string;
|
|
10
|
+
pagesWritten: number;
|
|
11
|
+
quartzContentDir: string;
|
|
12
|
+
built: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function runRender(vaultOption: string | undefined, options: RenderOptions): Promise<RenderOutcome>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readVault } from "../../core/vault/index.js";
|
|
3
|
+
import { loadWorldConfig, getBuildVariant, worldTitle } from "../../core/config/index.js";
|
|
4
|
+
import { buildDerivedTree, syncDerivedTreeToQuartz } from "../../render-pipeline/index.js";
|
|
5
|
+
import { resolveVaultDir } from "../vault-path.js";
|
|
6
|
+
import { resolveQuartzDir, runQuartzBuild, setQuartzPageTitle } from "../quartz-runner.js";
|
|
7
|
+
import { ensureQuartz } from "../quartz-setup.js";
|
|
8
|
+
export async function runRender(vaultOption, options) {
|
|
9
|
+
const vaultDir = resolveVaultDir(vaultOption);
|
|
10
|
+
const quartzDir = resolveQuartzDir(options.quartzDir);
|
|
11
|
+
const config = await loadWorldConfig(vaultDir);
|
|
12
|
+
const query = getBuildVariant(config, options.variant);
|
|
13
|
+
const files = await readVault(vaultDir);
|
|
14
|
+
const pages = buildDerivedTree(files, query);
|
|
15
|
+
// Before syncing: a fresh clone must not land on top of a content folder we just wrote.
|
|
16
|
+
if (options.build)
|
|
17
|
+
await ensureQuartz(quartzDir);
|
|
18
|
+
const quartzContentDir = path.join(quartzDir, "content");
|
|
19
|
+
await syncDerivedTreeToQuartz(quartzContentDir, pages);
|
|
20
|
+
if (options.build) {
|
|
21
|
+
// Same title as publish, so a local preview looks like the real site.
|
|
22
|
+
await setQuartzPageTitle(quartzDir, worldTitle(config));
|
|
23
|
+
const outputDir = options.outputDir ? path.resolve(process.cwd(), options.outputDir) : undefined;
|
|
24
|
+
await runQuartzBuild(quartzDir, outputDir);
|
|
25
|
+
}
|
|
26
|
+
return { variant: options.variant, pagesWritten: pages.length, quartzContentDir, built: Boolean(options.build) };
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describeSchema } from "../../core/schema/index.js";
|
|
2
|
+
import { loadWorldConfig } from "../../core/config/index.js";
|
|
3
|
+
import { findVaultDir } from "../vault-path.js";
|
|
4
|
+
/** Vault is optional here — the framework-fixed schema is useful even with no vault selected. */
|
|
5
|
+
export async function runSchema(vaultOption) {
|
|
6
|
+
const vaultDir = findVaultDir(vaultOption);
|
|
7
|
+
const config = vaultDir ? await loadWorldConfig(vaultDir) : {};
|
|
8
|
+
return describeSchema(config.entityTypes);
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SearchOptions } from "../../core/search/index.js";
|
|
2
|
+
export interface SearchResultSummary {
|
|
3
|
+
id: string;
|
|
4
|
+
score: number;
|
|
5
|
+
title?: string;
|
|
6
|
+
entityType?: string;
|
|
7
|
+
tags?: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function runSearch(vaultOption: string | undefined, query: string, options: SearchOptions): Promise<SearchResultSummary[]>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readVault } from "../../core/vault/index.js";
|
|
2
|
+
import { buildSearchIndex, closeSearchIndex, search } from "../../core/search/index.js";
|
|
3
|
+
import { resolveVaultDir } from "../vault-path.js";
|
|
4
|
+
export async function runSearch(vaultOption, query, options) {
|
|
5
|
+
const vaultDir = resolveVaultDir(vaultOption);
|
|
6
|
+
const files = await readVault(vaultDir);
|
|
7
|
+
const index = buildSearchIndex(files);
|
|
8
|
+
try {
|
|
9
|
+
return search(index, query, options).map((result) => ({
|
|
10
|
+
id: result.id,
|
|
11
|
+
score: result.score,
|
|
12
|
+
title: typeof result.file.frontmatter.title === "string" ? result.file.frontmatter.title : undefined,
|
|
13
|
+
entityType: typeof result.file.frontmatter.entityType === "string" ? result.file.frontmatter.entityType : undefined,
|
|
14
|
+
tags: Array.isArray(result.file.frontmatter.tags) ? result.file.frontmatter.tags : undefined,
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
closeSearchIndex(index);
|
|
19
|
+
}
|
|
20
|
+
}
|