world-building-framework 0.0.0-stage → 0.2.1
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 +20 -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 +29 -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 +34 -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 +21 -0
- package/dist/core/query/types.js +12 -0
- package/dist/core/schema/describe.d.ts +34 -0
- package/dist/core/schema/describe.js +99 -0
- package/dist/core/schema/fields.d.ts +57 -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 +12 -0
- package/dist/render-pipeline/build.js +49 -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 +11 -0
- package/dist/render-pipeline/index.js +11 -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 +8 -0
- package/dist/render-pipeline/slugs.js +31 -0
- package/dist/render-pipeline/start-page.d.ts +9 -0
- package/dist/render-pipeline/start-page.js +44 -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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ValidationIssue } from "../../core/schema/index.js";
|
|
2
|
+
import { type CreateOutcome } from "./create.js";
|
|
3
|
+
export interface UpdateOutcome extends CreateOutcome {
|
|
4
|
+
/** Other files whose links were rewritten to show the new title. */
|
|
5
|
+
relinked?: string[];
|
|
6
|
+
/** Other files whose links couldn't be rewritten, because the file itself fails validation. */
|
|
7
|
+
relinkFailed?: {
|
|
8
|
+
id: string;
|
|
9
|
+
issues: ValidationIssue[];
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Replaces an existing file's frontmatter and body in place — for corrections
|
|
14
|
+
* (typos, a wrong tag, a missing link), not in-world changes, which are a new
|
|
15
|
+
* version file instead (SCHEMA.md rule 8). The input is the complete new
|
|
16
|
+
* content, not a patch. Same checks as create, plus `type` may not change:
|
|
17
|
+
* turning an entity into an event (or back) would break every file linking to it.
|
|
18
|
+
*
|
|
19
|
+
* If the title changes, every link elsewhere whose display text is exactly the
|
|
20
|
+
* old title is rewritten to the new one (see replaceLinkDisplay), so a
|
|
21
|
+
* corrected typo doesn't live on in other files' link text.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runUpdate(vaultOption: string | undefined, id: string, rawInput: unknown): Promise<UpdateOutcome>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readVault, writeVaultFile, replaceLinkDisplay } from "../../core/vault/index.js";
|
|
2
|
+
import { hasErrors } from "../../core/schema/index.js";
|
|
3
|
+
import { loadWorldConfig, validateOptionsFromConfig } from "../../core/config/index.js";
|
|
4
|
+
import { resolveVaultDir } from "../vault-path.js";
|
|
5
|
+
import { createLintContext } from "../../core/lint/index.js";
|
|
6
|
+
import { checkFile } from "../check.js";
|
|
7
|
+
import { fileInputSchema } from "./create.js";
|
|
8
|
+
/**
|
|
9
|
+
* Replaces an existing file's frontmatter and body in place — for corrections
|
|
10
|
+
* (typos, a wrong tag, a missing link), not in-world changes, which are a new
|
|
11
|
+
* version file instead (SCHEMA.md rule 8). The input is the complete new
|
|
12
|
+
* content, not a patch. Same checks as create, plus `type` may not change:
|
|
13
|
+
* turning an entity into an event (or back) would break every file linking to it.
|
|
14
|
+
*
|
|
15
|
+
* If the title changes, every link elsewhere whose display text is exactly the
|
|
16
|
+
* old title is rewritten to the new one (see replaceLinkDisplay), so a
|
|
17
|
+
* corrected typo doesn't live on in other files' link text.
|
|
18
|
+
*/
|
|
19
|
+
export async function runUpdate(vaultOption, id, rawInput) {
|
|
20
|
+
const vaultDir = resolveVaultDir(vaultOption);
|
|
21
|
+
const input = fileInputSchema.parse(rawInput);
|
|
22
|
+
const config = await loadWorldConfig(vaultDir);
|
|
23
|
+
const options = validateOptionsFromConfig(config);
|
|
24
|
+
const files = await readVault(vaultDir);
|
|
25
|
+
const existing = files.find((file) => file.id === id);
|
|
26
|
+
if (!existing)
|
|
27
|
+
throw new Error(`No file with id "${id}" in this vault.`);
|
|
28
|
+
const issues = checkFile({ id, ...input }, options, createLintContext(files));
|
|
29
|
+
if (input.frontmatter.type !== existing.frontmatter.type) {
|
|
30
|
+
issues.unshift({
|
|
31
|
+
path: "type",
|
|
32
|
+
message: `"type" can't change (was ${JSON.stringify(existing.frontmatter.type)}) — create a new file instead`,
|
|
33
|
+
severity: "error",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (hasErrors(issues)) {
|
|
37
|
+
return { written: false, issues };
|
|
38
|
+
}
|
|
39
|
+
const result = await writeVaultFile(vaultDir, id, input.frontmatter, input.body, options);
|
|
40
|
+
if (!result.written) {
|
|
41
|
+
return { written: false, issues: result.issues };
|
|
42
|
+
}
|
|
43
|
+
const outcome = { written: true, id: result.id, path: result.path };
|
|
44
|
+
if (issues.length > 0)
|
|
45
|
+
outcome.issues = issues;
|
|
46
|
+
const oldTitle = existing.frontmatter.title;
|
|
47
|
+
const newTitle = input.frontmatter.title;
|
|
48
|
+
if (typeof oldTitle === "string" && typeof newTitle === "string" && oldTitle !== newTitle) {
|
|
49
|
+
const relinked = [];
|
|
50
|
+
const relinkFailed = [];
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
if (file.id === id)
|
|
53
|
+
continue;
|
|
54
|
+
const replaced = replaceLinkDisplay(file, id, oldTitle, newTitle);
|
|
55
|
+
if (!replaced)
|
|
56
|
+
continue;
|
|
57
|
+
const written = await writeVaultFile(vaultDir, file.id, replaced.frontmatter, replaced.body, options);
|
|
58
|
+
if (written.written)
|
|
59
|
+
relinked.push(file.id);
|
|
60
|
+
else
|
|
61
|
+
relinkFailed.push({ id: file.id, issues: written.issues });
|
|
62
|
+
}
|
|
63
|
+
if (relinked.length > 0)
|
|
64
|
+
outcome.relinked = relinked;
|
|
65
|
+
if (relinkFailed.length > 0)
|
|
66
|
+
outcome.relinkFailed = relinkFailed;
|
|
67
|
+
}
|
|
68
|
+
return outcome;
|
|
69
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ValidationIssue } from "../../core/schema/index.js";
|
|
2
|
+
export interface ValidateOutcome {
|
|
3
|
+
id?: string;
|
|
4
|
+
valid: boolean;
|
|
5
|
+
issues: ValidationIssue[];
|
|
6
|
+
}
|
|
7
|
+
export interface ValidateRequest {
|
|
8
|
+
/** Validate one existing vault file by id. */
|
|
9
|
+
id?: string;
|
|
10
|
+
/** Validate a not-yet-written candidate ({"frontmatter": {...}, "body": "..."}) instead of vault files. */
|
|
11
|
+
candidate?: unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Three modes: validate a candidate that hasn't been written yet (candidate),
|
|
15
|
+
* validate one existing file (id), or validate the whole vault (neither) — an
|
|
16
|
+
* explicit lint pass, distinct from readVault which never validates. All three
|
|
17
|
+
* run both core/schema's structural validation and core/lint's checks
|
|
18
|
+
* (dangling links, events without entities) — the vault is always read in
|
|
19
|
+
* full since dangling-link checking needs every known id as context.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runValidate(vaultOption: string | undefined, request: ValidateRequest): Promise<ValidateOutcome | ValidateOutcome[]>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readVault } from "../../core/vault/index.js";
|
|
3
|
+
import { hasErrors } from "../../core/schema/index.js";
|
|
4
|
+
import { createLintContext } from "../../core/lint/index.js";
|
|
5
|
+
import { checkFile } from "../check.js";
|
|
6
|
+
import { loadWorldConfig, validateOptionsFromConfig } from "../../core/config/index.js";
|
|
7
|
+
import { resolveVaultDir } from "../vault-path.js";
|
|
8
|
+
const candidateInputSchema = z.object({
|
|
9
|
+
frontmatter: z.record(z.string(), z.unknown()),
|
|
10
|
+
body: z.string().optional(),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Three modes: validate a candidate that hasn't been written yet (candidate),
|
|
14
|
+
* validate one existing file (id), or validate the whole vault (neither) — an
|
|
15
|
+
* explicit lint pass, distinct from readVault which never validates. All three
|
|
16
|
+
* run both core/schema's structural validation and core/lint's checks
|
|
17
|
+
* (dangling links, events without entities) — the vault is always read in
|
|
18
|
+
* full since dangling-link checking needs every known id as context.
|
|
19
|
+
*/
|
|
20
|
+
export async function runValidate(vaultOption, request) {
|
|
21
|
+
const vaultDir = resolveVaultDir(vaultOption);
|
|
22
|
+
const config = await loadWorldConfig(vaultDir);
|
|
23
|
+
const options = validateOptionsFromConfig(config);
|
|
24
|
+
const files = await readVault(vaultDir);
|
|
25
|
+
const context = createLintContext(files);
|
|
26
|
+
// Warnings (e.g. an event with no entities) are reported but never make a file invalid.
|
|
27
|
+
const check = (file) => checkFile(file, options, context);
|
|
28
|
+
if (request.candidate !== undefined) {
|
|
29
|
+
const { frontmatter, body } = candidateInputSchema.parse(request.candidate);
|
|
30
|
+
const issues = check({ frontmatter, body: body ?? "" });
|
|
31
|
+
return { valid: !hasErrors(issues), issues };
|
|
32
|
+
}
|
|
33
|
+
if (request.id) {
|
|
34
|
+
const file = files.find((f) => f.id === request.id);
|
|
35
|
+
if (!file)
|
|
36
|
+
throw new Error(`No file with id "${request.id}" in this vault.`);
|
|
37
|
+
const issues = check(file);
|
|
38
|
+
return { id: file.id, valid: !hasErrors(issues), issues };
|
|
39
|
+
}
|
|
40
|
+
return files.map((file) => {
|
|
41
|
+
const issues = check(file);
|
|
42
|
+
return { id: file.id, valid: !hasErrors(issues), issues };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { runSchema } from "./commands/schema.js";
|
|
5
|
+
import { runInit } from "./commands/init.js";
|
|
6
|
+
import { runRefresh } from "./commands/refresh.js";
|
|
7
|
+
import { runCreate } from "./commands/create.js";
|
|
8
|
+
import { runUpdate } from "./commands/update.js";
|
|
9
|
+
import { runValidate } from "./commands/validate.js";
|
|
10
|
+
import { runSearch } from "./commands/search.js";
|
|
11
|
+
import { runRender } from "./commands/render.js";
|
|
12
|
+
import { runListVariants } from "./commands/list-variants.js";
|
|
13
|
+
import { runPublish } from "./commands/publish.js";
|
|
14
|
+
import { readJsonInput } from "./io.js";
|
|
15
|
+
import { resolveQuartzDir } from "./quartz-runner.js";
|
|
16
|
+
import { setupQuartz, isQuartzReady, QUARTZ_VERSION } from "./quartz-setup.js";
|
|
17
|
+
function handle(fn) {
|
|
18
|
+
return async (...args) => {
|
|
19
|
+
try {
|
|
20
|
+
await fn(...args);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
console.error(JSON.stringify({ error: err.message }, null, 2));
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const program = new Command();
|
|
29
|
+
// ../../package.json from both src/cli (dev) and dist/cli (installed package).
|
|
30
|
+
const { version } = createRequire(import.meta.url)("../../package.json");
|
|
31
|
+
program.name("wbf").description("World Building Framework CLI").version(version);
|
|
32
|
+
program
|
|
33
|
+
.command("init")
|
|
34
|
+
.description("Set up the current folder as a world repo: config, publish workflow, AGENTS.md, and agent skills. Never overwrites existing files")
|
|
35
|
+
.option("--dir <path>", "folder to set up (default: current folder)")
|
|
36
|
+
.option("--title <title>", "the world's display title (default: derived from the folder name)")
|
|
37
|
+
.option("--agent <name>", "install skills for this AI agent; repeat for several", (value, previous) => [...previous, value], [])
|
|
38
|
+
.option("--branch <name>", "branch the publish workflow runs on (default: the repo's current branch, else main)")
|
|
39
|
+
.action(handle(async (opts) => {
|
|
40
|
+
const outcome = await runInit({ dir: opts.dir, title: opts.title, agents: opts.agent, branch: opts.branch });
|
|
41
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
42
|
+
}));
|
|
43
|
+
program
|
|
44
|
+
.command("refresh")
|
|
45
|
+
.description("Update the files wbf init generated (workflow, AGENTS.md, skills) to the installed framework version. Keeps files you edited")
|
|
46
|
+
.option("--dir <path>", "world repo root (default: current folder)")
|
|
47
|
+
.option("--force", "also overwrite files that were edited, deleted, or not generated by wbf", false)
|
|
48
|
+
.action(handle(async (opts) => {
|
|
49
|
+
console.log(JSON.stringify(await runRefresh({ dir: opts.dir, force: opts.force }), null, 2));
|
|
50
|
+
}));
|
|
51
|
+
program
|
|
52
|
+
.command("schema")
|
|
53
|
+
.description("Print the current, authoritative vault schema")
|
|
54
|
+
.option("-v, --vault <path>", "vault directory")
|
|
55
|
+
.action(handle(async (opts) => {
|
|
56
|
+
console.log(await runSchema(opts.vault));
|
|
57
|
+
}));
|
|
58
|
+
program
|
|
59
|
+
.command("create")
|
|
60
|
+
.description("Create a new vault file")
|
|
61
|
+
.option("-v, --vault <path>", "vault directory")
|
|
62
|
+
.option("-i, --input <path>", 'JSON input ({"frontmatter": {...}, "body": "..."}); omit or "-" for stdin')
|
|
63
|
+
.action(handle(async (opts) => {
|
|
64
|
+
const input = await readJsonInput(opts.input);
|
|
65
|
+
const outcome = await runCreate(opts.vault, input);
|
|
66
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
67
|
+
if (!outcome.written)
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}));
|
|
70
|
+
program
|
|
71
|
+
.command("update")
|
|
72
|
+
.description("Replace an existing vault file's content (corrections only — in-world changes are a new version)")
|
|
73
|
+
.option("-v, --vault <path>", "vault directory")
|
|
74
|
+
.requiredOption("--id <id>", "id of the file to replace")
|
|
75
|
+
.option("-i, --input <path>", 'complete new content as JSON ({"frontmatter": {...}, "body": "..."}); omit or "-" for stdin')
|
|
76
|
+
.action(handle(async (opts) => {
|
|
77
|
+
const input = await readJsonInput(opts.input);
|
|
78
|
+
const outcome = await runUpdate(opts.vault, opts.id, input);
|
|
79
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
80
|
+
if (!outcome.written)
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}));
|
|
83
|
+
program
|
|
84
|
+
.command("validate")
|
|
85
|
+
.description("Validate vault file(s), or a candidate that hasn't been written yet")
|
|
86
|
+
.option("-v, --vault <path>", "vault directory")
|
|
87
|
+
.option("--id <id>", "validate one existing file by id")
|
|
88
|
+
.option("-i, --input <path>", 'validate a candidate JSON payload ({"frontmatter": {...}, "body": "..."}) instead of vault files')
|
|
89
|
+
.action(handle(async (opts) => {
|
|
90
|
+
const candidate = opts.input !== undefined ? await readJsonInput(opts.input) : undefined;
|
|
91
|
+
const outcome = await runValidate(opts.vault, { id: opts.id, candidate });
|
|
92
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
93
|
+
const allValid = Array.isArray(outcome) ? outcome.every((o) => o.valid) : outcome.valid;
|
|
94
|
+
if (!allValid)
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
}));
|
|
97
|
+
program
|
|
98
|
+
.command("search")
|
|
99
|
+
.description("Keyword search over the vault")
|
|
100
|
+
.argument("<query...>", "search terms")
|
|
101
|
+
.option("-v, --vault <path>", "vault directory")
|
|
102
|
+
.option("-t, --tags <tags>", "comma-separated tags, all required")
|
|
103
|
+
.option("-l, --limit <n>", "max results", (value) => parseInt(value, 10))
|
|
104
|
+
.action(handle(async (queryParts, opts) => {
|
|
105
|
+
const tags = opts.tags ? opts.tags.split(",").map((tag) => tag.trim()) : undefined;
|
|
106
|
+
const results = await runSearch(opts.vault, queryParts.join(" "), { tags, limit: opts.limit });
|
|
107
|
+
console.log(JSON.stringify(results, null, 2));
|
|
108
|
+
}));
|
|
109
|
+
program
|
|
110
|
+
.command("render")
|
|
111
|
+
.description("Build a named build variant's derived tree and sync it into Quartz's content folder")
|
|
112
|
+
.option("-v, --vault <path>", "vault directory")
|
|
113
|
+
.requiredOption("--variant <name>", "named build variant from this vault's world.config.json")
|
|
114
|
+
.option("--quartz-dir <path>", "Quartz project directory (default: ./.wbf/quartz, or WBF_QUARTZ_DIR)")
|
|
115
|
+
.option("--build", "also run `quartz build` after syncing", false)
|
|
116
|
+
.option("--output <dir>", "output directory for the Quartz build (only used with --build)")
|
|
117
|
+
.action(handle(async (opts) => {
|
|
118
|
+
const outcome = await runRender(opts.vault, {
|
|
119
|
+
variant: opts.variant,
|
|
120
|
+
quartzDir: opts.quartzDir,
|
|
121
|
+
build: opts.build,
|
|
122
|
+
outputDir: opts.output,
|
|
123
|
+
});
|
|
124
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
125
|
+
}));
|
|
126
|
+
program
|
|
127
|
+
.command("setup-quartz")
|
|
128
|
+
.description("Fetch, install and initialize Quartz at the pinned version (default location: ./.wbf/quartz). Safe to re-run")
|
|
129
|
+
.option("--quartz-dir <path>", "Quartz project directory (default: ./.wbf/quartz, or WBF_QUARTZ_DIR)")
|
|
130
|
+
.action(handle(async (opts) => {
|
|
131
|
+
const quartzDir = resolveQuartzDir(opts.quartzDir);
|
|
132
|
+
await setupQuartz(quartzDir);
|
|
133
|
+
console.log(JSON.stringify({ quartzDir, version: QUARTZ_VERSION, ready: isQuartzReady(quartzDir) }, null, 2));
|
|
134
|
+
}));
|
|
135
|
+
program
|
|
136
|
+
.command("list-variants")
|
|
137
|
+
.description("List this vault's declared build variants (world.config.json)")
|
|
138
|
+
.option("-v, --vault <path>", "vault directory")
|
|
139
|
+
.action(handle(async (opts) => {
|
|
140
|
+
console.log(JSON.stringify(await runListVariants(opts.vault), null, 2));
|
|
141
|
+
}));
|
|
142
|
+
program
|
|
143
|
+
.command("publish")
|
|
144
|
+
.description("Build every declared build variant into its own subpath of one combined output tree, plus a landing page")
|
|
145
|
+
.option("-v, --vault <path>", "vault directory")
|
|
146
|
+
.option("--quartz-dir <path>", "Quartz project directory (default: ./.wbf/quartz, or WBF_QUARTZ_DIR)")
|
|
147
|
+
.requiredOption("--output <dir>", "combined output directory (each variant gets a subfolder here)")
|
|
148
|
+
.option("--base-url <url>", "public URL of the combined site; each variant is built with <url>/<variant> as Quartz's baseUrl")
|
|
149
|
+
.action(handle(async (opts) => {
|
|
150
|
+
const outcome = await runPublish(opts.vault, {
|
|
151
|
+
quartzDir: opts.quartzDir,
|
|
152
|
+
outputDir: opts.output,
|
|
153
|
+
baseUrl: opts.baseUrl,
|
|
154
|
+
});
|
|
155
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
156
|
+
}));
|
|
157
|
+
await program.parseAsync(process.argv);
|
package/dist/cli/io.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Reads text from a file path, or from stdin when path is omitted or "-". */
|
|
2
|
+
export declare function readTextInput(path: string | undefined): Promise<string>;
|
|
3
|
+
/** Reads and parses a JSON payload from a file path, or from stdin when path is omitted or "-". */
|
|
4
|
+
export declare function readJsonInput(path: string | undefined): Promise<unknown>;
|
package/dist/cli/io.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
async function readStdin() {
|
|
3
|
+
const chunks = [];
|
|
4
|
+
for await (const chunk of process.stdin) {
|
|
5
|
+
chunks.push(chunk);
|
|
6
|
+
}
|
|
7
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
8
|
+
}
|
|
9
|
+
/** Reads text from a file path, or from stdin when path is omitted or "-". */
|
|
10
|
+
export async function readTextInput(path) {
|
|
11
|
+
if (!path || path === "-") {
|
|
12
|
+
return readStdin();
|
|
13
|
+
}
|
|
14
|
+
return readFile(path, "utf8");
|
|
15
|
+
}
|
|
16
|
+
/** Reads and parses a JSON payload from a file path, or from stdin when path is omitted or "-". */
|
|
17
|
+
export async function readJsonInput(path) {
|
|
18
|
+
const raw = await readTextInput(path);
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
throw new Error(`Input is not valid JSON: ${err.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The installed package's root: ../.. from both src/cli (dev) and dist/cli (installed package). */
|
|
2
|
+
export declare const PACKAGE_ROOT: string;
|
|
3
|
+
/**
|
|
4
|
+
* Where each supported agent looks for project skills (open SKILL.md format).
|
|
5
|
+
* Only folders confirmed in the agent's own docs belong here
|
|
6
|
+
* (claude-dev/DISTRIBUTION_PLAN.md, component 6).
|
|
7
|
+
*/
|
|
8
|
+
export declare const AGENT_SKILL_DIRS: Record<string, string>;
|
|
9
|
+
/** Settings that decide how managed files render; stored so `refresh` renders them the same way. */
|
|
10
|
+
export interface ManagedSettings {
|
|
11
|
+
branch: string;
|
|
12
|
+
agents: string[];
|
|
13
|
+
}
|
|
14
|
+
/** The record of generated files, kept in the world repo (committed) at MANIFEST_PATH. */
|
|
15
|
+
export interface Manifest extends ManagedSettings {
|
|
16
|
+
frameworkVersion: string;
|
|
17
|
+
/** Repo-relative path (forward slashes) -> content hash of what wbf last wrote there. */
|
|
18
|
+
files: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
export declare const MANIFEST_PATH = ".wbf/managed.json";
|
|
21
|
+
/**
|
|
22
|
+
* Content hash, ignoring line endings: git may check a file out with CRLF on
|
|
23
|
+
* Windows, and that must not look like the user edited it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function contentHash(content: string): string;
|
|
26
|
+
export declare function readFrameworkVersion(): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* The files wbf owns in a world repo, rendered for this framework version:
|
|
29
|
+
* the publish workflow, AGENTS.md, and the package's skills copied into each
|
|
30
|
+
* chosen agent's skill folder. Keyed by repo-relative path.
|
|
31
|
+
*/
|
|
32
|
+
export declare function renderManagedFiles(version: string, settings: ManagedSettings): Promise<Map<string, string>>;
|
|
33
|
+
export declare function readManifest(rootDir: string): Promise<Manifest | undefined>;
|
|
34
|
+
export declare function writeManifest(rootDir: string, manifest: Manifest): Promise<void>;
|
|
35
|
+
export declare function writeRepoFile(rootDir: string, relPath: string, content: string): Promise<void>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
/** The installed package's root: ../.. from both src/cli (dev) and dist/cli (installed package). */
|
|
7
|
+
export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
8
|
+
/**
|
|
9
|
+
* Where each supported agent looks for project skills (open SKILL.md format).
|
|
10
|
+
* Only folders confirmed in the agent's own docs belong here
|
|
11
|
+
* (claude-dev/DISTRIBUTION_PLAN.md, component 6).
|
|
12
|
+
*/
|
|
13
|
+
export const AGENT_SKILL_DIRS = {
|
|
14
|
+
claude: ".claude/skills",
|
|
15
|
+
};
|
|
16
|
+
export const MANIFEST_PATH = ".wbf/managed.json";
|
|
17
|
+
/**
|
|
18
|
+
* Content hash, ignoring line endings: git may check a file out with CRLF on
|
|
19
|
+
* Windows, and that must not look like the user edited it.
|
|
20
|
+
*/
|
|
21
|
+
export function contentHash(content) {
|
|
22
|
+
return createHash("sha256").update(content.replace(/\r\n/g, "\n")).digest("hex");
|
|
23
|
+
}
|
|
24
|
+
export async function readFrameworkVersion() {
|
|
25
|
+
const raw = await readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8");
|
|
26
|
+
return JSON.parse(raw).version;
|
|
27
|
+
}
|
|
28
|
+
async function template(name, version, branch) {
|
|
29
|
+
const raw = await readFile(path.join(PACKAGE_ROOT, "templates", name), "utf8");
|
|
30
|
+
return raw.replaceAll("__WBF_VERSION__", version).replaceAll("__BRANCH__", branch);
|
|
31
|
+
}
|
|
32
|
+
/** Every file under `dir`, as paths relative to it (forward slashes). */
|
|
33
|
+
async function listFiles(dir, prefix = "") {
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
36
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
37
|
+
if (entry.isDirectory())
|
|
38
|
+
out.push(...(await listFiles(path.join(dir, entry.name), rel)));
|
|
39
|
+
else if (entry.isFile())
|
|
40
|
+
out.push(rel);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The files wbf owns in a world repo, rendered for this framework version:
|
|
46
|
+
* the publish workflow, AGENTS.md, and the package's skills copied into each
|
|
47
|
+
* chosen agent's skill folder. Keyed by repo-relative path.
|
|
48
|
+
*/
|
|
49
|
+
export async function renderManagedFiles(version, settings) {
|
|
50
|
+
const files = new Map();
|
|
51
|
+
files.set(".github/workflows/publish.yml", await template("publish.yml", version, settings.branch));
|
|
52
|
+
files.set("AGENTS.md", await template("AGENTS.md", version, settings.branch));
|
|
53
|
+
const skillsDir = path.join(PACKAGE_ROOT, "skills");
|
|
54
|
+
const skillFiles = await listFiles(skillsDir);
|
|
55
|
+
for (const agent of settings.agents) {
|
|
56
|
+
const target = AGENT_SKILL_DIRS[agent];
|
|
57
|
+
if (!target)
|
|
58
|
+
throw new Error(`Unknown agent "${agent}". Supported: ${Object.keys(AGENT_SKILL_DIRS).join(", ")}`);
|
|
59
|
+
for (const rel of skillFiles) {
|
|
60
|
+
files.set(`${target}/${rel}`, await readFile(path.join(skillsDir, rel), "utf8"));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
export async function readManifest(rootDir) {
|
|
66
|
+
const manifestFile = path.join(rootDir, MANIFEST_PATH);
|
|
67
|
+
if (!existsSync(manifestFile))
|
|
68
|
+
return undefined;
|
|
69
|
+
return JSON.parse(await readFile(manifestFile, "utf8"));
|
|
70
|
+
}
|
|
71
|
+
export async function writeManifest(rootDir, manifest) {
|
|
72
|
+
const manifestFile = path.join(rootDir, MANIFEST_PATH);
|
|
73
|
+
await mkdir(path.dirname(manifestFile), { recursive: true });
|
|
74
|
+
await writeFile(manifestFile, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
75
|
+
}
|
|
76
|
+
export async function writeRepoFile(rootDir, relPath, content) {
|
|
77
|
+
const target = path.join(rootDir, ...relPath.split("/"));
|
|
78
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
79
|
+
await writeFile(target, content, "utf8");
|
|
80
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where Quartz's working copy lives: --quartz-dir, then WBF_QUARTZ_DIR, then
|
|
3
|
+
* `.wbf/quartz` in the current folder — in a world repo, that's the world's
|
|
4
|
+
* own root (claude-dev/DISTRIBUTION_PLAN.md, component 3).
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveQuartzDir(cliOption: string | undefined): string;
|
|
7
|
+
/** Quartz's own baseUrl form: no protocol, no trailing slash (matches what `quartz create -b` stores). */
|
|
8
|
+
export declare function normalizeBaseUrl(url: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Sets `baseUrl`. It must be right per variant: Quartz uses its path part for
|
|
11
|
+
* absolute asset URLs (e.g. the 404 page), so a variant served at /<variant>/
|
|
12
|
+
* needs that subpath in its own baseUrl.
|
|
13
|
+
*/
|
|
14
|
+
export declare function setQuartzBaseUrl(quartzDir: string, baseUrl: string): Promise<void>;
|
|
15
|
+
/** Sets `pageTitle`, the site name shown in every page's header. Quoted, so `:` or `"` in a title can't break the YAML. */
|
|
16
|
+
export declare function setQuartzPageTitle(quartzDir: string, title: string): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Runs a Quartz build. `outputDir`, if given, must be absolute — it's passed
|
|
19
|
+
* straight through to Quartz's own `-o` flag, which resolves relative paths
|
|
20
|
+
* against its cwd (quartzDir), not the caller's.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runQuartzBuild(quartzDir: string, outputDir?: string): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
/**
|
|
5
|
+
* Where Quartz's working copy lives: --quartz-dir, then WBF_QUARTZ_DIR, then
|
|
6
|
+
* `.wbf/quartz` in the current folder — in a world repo, that's the world's
|
|
7
|
+
* own root (claude-dev/DISTRIBUTION_PLAN.md, component 3).
|
|
8
|
+
*/
|
|
9
|
+
export function resolveQuartzDir(cliOption) {
|
|
10
|
+
return cliOption ?? process.env.WBF_QUARTZ_DIR ?? path.join(process.cwd(), ".wbf", "quartz");
|
|
11
|
+
}
|
|
12
|
+
/** Quartz's own baseUrl form: no protocol, no trailing slash (matches what `quartz create -b` stores). */
|
|
13
|
+
export function normalizeBaseUrl(url) {
|
|
14
|
+
return url.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Replaces one `key: value` line in Quartz's quartz.config.yaml. `quartz build`
|
|
18
|
+
* has no flags for these settings — it only ever reads them from config — and
|
|
19
|
+
* quartz/ is fetched fresh in CI, so anything that must survive a build has to
|
|
20
|
+
* be written here right before it. `yamlValue` is written as-is.
|
|
21
|
+
*/
|
|
22
|
+
async function setQuartzConfigLine(quartzDir, key, yamlValue) {
|
|
23
|
+
const configPath = path.join(quartzDir, "quartz.config.yaml");
|
|
24
|
+
const lineRe = new RegExp(`^(\\s*${key}:).*$`, "m");
|
|
25
|
+
const raw = await readFile(configPath, "utf8");
|
|
26
|
+
if (!lineRe.test(raw)) {
|
|
27
|
+
throw new Error(`No ${key} line found in ${configPath} — has Quartz been initialized (wbf setup-quartz)?`);
|
|
28
|
+
}
|
|
29
|
+
await writeFile(configPath, raw.replace(lineRe, (_line, prefix) => `${prefix} ${yamlValue}`), "utf8");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sets `baseUrl`. It must be right per variant: Quartz uses its path part for
|
|
33
|
+
* absolute asset URLs (e.g. the 404 page), so a variant served at /<variant>/
|
|
34
|
+
* needs that subpath in its own baseUrl.
|
|
35
|
+
*/
|
|
36
|
+
export function setQuartzBaseUrl(quartzDir, baseUrl) {
|
|
37
|
+
return setQuartzConfigLine(quartzDir, "baseUrl", normalizeBaseUrl(baseUrl));
|
|
38
|
+
}
|
|
39
|
+
/** Sets `pageTitle`, the site name shown in every page's header. Quoted, so `:` or `"` in a title can't break the YAML. */
|
|
40
|
+
export function setQuartzPageTitle(quartzDir, title) {
|
|
41
|
+
return setQuartzConfigLine(quartzDir, "pageTitle", JSON.stringify(title));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Runs one Quartz step. Its output goes to our stderr, never stdout: the CLI's
|
|
45
|
+
* own JSON result is the only thing on stdout, so scripts and agents can parse it.
|
|
46
|
+
*/
|
|
47
|
+
function spawnStep(command, args, options) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const child = spawn(command, args, { stdio: ["ignore", process.stderr, process.stderr], ...options });
|
|
50
|
+
child.on("error", reject);
|
|
51
|
+
child.on("exit", (code) => {
|
|
52
|
+
if (code === 0)
|
|
53
|
+
resolve();
|
|
54
|
+
else
|
|
55
|
+
reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Runs a Quartz build. `outputDir`, if given, must be absolute — it's passed
|
|
61
|
+
* straight through to Quartz's own `-o` flag, which resolves relative paths
|
|
62
|
+
* against its cwd (quartzDir), not the caller's.
|
|
63
|
+
*/
|
|
64
|
+
export async function runQuartzBuild(quartzDir, outputDir) {
|
|
65
|
+
// Quartz's package.json wires plugin installation as a "prebuild" npm lifecycle
|
|
66
|
+
// hook, which never fires when bootstrap-cli.mjs is invoked directly (as we do
|
|
67
|
+
// here) instead of via `npm run build` — verified hands-on: skipping this step
|
|
68
|
+
// left `.quartz/plugins` missing and the build failed. Replicate that hook
|
|
69
|
+
// explicitly rather than relying on invoking it the one specific way it happens
|
|
70
|
+
// to fire.
|
|
71
|
+
await spawnStep("npm", ["run", "install-plugins"], { cwd: quartzDir, shell: true });
|
|
72
|
+
const buildArgs = ["./quartz/bootstrap-cli.mjs", "build"];
|
|
73
|
+
if (outputDir)
|
|
74
|
+
buildArgs.push("-o", outputDir);
|
|
75
|
+
await spawnStep("node", buildArgs, { cwd: quartzDir });
|
|
76
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const QUARTZ_VERSION = "f1fba3fc55cbf60a60a5d09c95a49c042cdab63a";
|
|
2
|
+
/** A Quartz working copy is usable once it's cloned, installed and initialized. */
|
|
3
|
+
export declare function isQuartzReady(quartzDir: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Fetches Quartz at the pinned commit into quartzDir, installs its dependencies
|
|
6
|
+
* and initializes it. Each step is skipped if already done, so it's safe to re-run.
|
|
7
|
+
*/
|
|
8
|
+
export declare function setupQuartz(quartzDir: string): Promise<void>;
|
|
9
|
+
/** Sets Quartz up first if it isn't ready yet — so render --build and publish work on a fresh checkout. */
|
|
10
|
+
export declare function ensureQuartz(quartzDir: string): Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
// Pinned to a specific commit (not a moving branch, and deliberately NOT the
|
|
6
|
+
// "v5.0.0" tag) for reproducible builds. The v5.0.0 tag itself has at least two
|
|
7
|
+
// real bugs found hands-on — a missing .quartz/plugins folder (its own
|
|
8
|
+
// package.json's plugin-install step never runs outside `npm run build`) and a
|
|
9
|
+
// crash importing a raw .scss file — both absent on this later commit, which is
|
|
10
|
+
// the exact state we validated end-to-end (see render-pipeline's
|
|
11
|
+
// quartz-compat.ts for the one real fix that *was* needed against it).
|
|
12
|
+
export const QUARTZ_VERSION = "f1fba3fc55cbf60a60a5d09c95a49c042cdab63a";
|
|
13
|
+
const QUARTZ_REPO = "https://github.com/jackyzha0/quartz.git";
|
|
14
|
+
/** A Quartz working copy is usable once it's cloned, installed and initialized. */
|
|
15
|
+
export function isQuartzReady(quartzDir) {
|
|
16
|
+
return (existsSync(path.join(quartzDir, "quartz", "bootstrap-cli.mjs")) &&
|
|
17
|
+
existsSync(path.join(quartzDir, "node_modules")) &&
|
|
18
|
+
existsSync(path.join(quartzDir, "quartz.config.yaml")));
|
|
19
|
+
}
|
|
20
|
+
// shell:true is only needed to resolve npm's .cmd shim on Windows — using it for
|
|
21
|
+
// plain executables (git, node) breaks argument quoting for paths containing
|
|
22
|
+
// spaces, since the args array gets flattened into a shell command line.
|
|
23
|
+
// Child output goes to stderr, so a command's own JSON result on stdout stays parseable.
|
|
24
|
+
function run(command, args, cwd) {
|
|
25
|
+
const result = spawnSync(command, args, { cwd, stdio: ["ignore", 2, 2], shell: command === "npm" });
|
|
26
|
+
if (result.status !== 0) {
|
|
27
|
+
throw new Error(`Quartz setup failed: ${command} ${args.join(" ")} exited with code ${result.status ?? "unknown"}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fetches Quartz at the pinned commit into quartzDir, installs its dependencies
|
|
32
|
+
* and initializes it. Each step is skipped if already done, so it's safe to re-run.
|
|
33
|
+
*/
|
|
34
|
+
export async function setupQuartz(quartzDir) {
|
|
35
|
+
if (!existsSync(path.join(quartzDir, "quartz", "bootstrap-cli.mjs"))) {
|
|
36
|
+
// git clone --branch only accepts a branch/tag name, not an arbitrary commit —
|
|
37
|
+
// this is the standard init/fetch/checkout pattern for shallow-cloning one
|
|
38
|
+
// specific commit (GitHub supports fetching a bare SHA for public repos).
|
|
39
|
+
console.error(`Cloning Quartz @ ${QUARTZ_VERSION} into ${quartzDir} ...`);
|
|
40
|
+
await mkdir(quartzDir, { recursive: true });
|
|
41
|
+
run("git", ["init", quartzDir]);
|
|
42
|
+
run("git", ["remote", "add", "origin", QUARTZ_REPO], quartzDir);
|
|
43
|
+
run("git", ["fetch", "--depth", "1", "origin", QUARTZ_VERSION], quartzDir);
|
|
44
|
+
run("git", ["checkout", "FETCH_HEAD"], quartzDir);
|
|
45
|
+
}
|
|
46
|
+
if (!existsSync(path.join(quartzDir, "node_modules"))) {
|
|
47
|
+
console.error("Installing Quartz's own dependencies ...");
|
|
48
|
+
run("npm", ["install"], quartzDir);
|
|
49
|
+
}
|
|
50
|
+
if (!existsSync(path.join(quartzDir, "quartz.config.yaml"))) {
|
|
51
|
+
console.error("Initializing Quartz (empty content folder, non-interactive) ...");
|
|
52
|
+
run("node", ["./quartz/bootstrap-cli.mjs", "create", "-X", "new", "-l", "shortest", "-t", "default", "-b", "localhost"], quartzDir);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Sets Quartz up first if it isn't ready yet — so render --build and publish work on a fresh checkout. */
|
|
56
|
+
export async function ensureQuartz(quartzDir) {
|
|
57
|
+
if (!isQuartzReady(quartzDir))
|
|
58
|
+
await setupQuartz(quartzDir);
|
|
59
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Like resolveVaultDir, but returns undefined instead of throwing — for commands
|
|
3
|
+
* that are useful without a vault (e.g. `schema`).
|
|
4
|
+
*/
|
|
5
|
+
export declare function findVaultDir(cliOption: string | undefined): string | undefined;
|
|
6
|
+
/** Resolves which vault to operate on: explicit flag, then WBF_VAULT_DIR, then the current folder if it has a world.config.json, then a single directory under ./vaults/. */
|
|
7
|
+
export declare function resolveVaultDir(cliOption: string | undefined): string;
|