@biaoo/tiangong-wiki 0.2.0 → 0.2.2
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/README.md +39 -50
- package/README.zh-CN.md +39 -50
- package/SKILL.md +75 -107
- package/assets/templates/achievement.md +8 -8
- package/assets/templates/bridge.md +8 -8
- package/assets/templates/concept.md +14 -18
- package/assets/templates/faq.md +8 -10
- package/assets/templates/lesson.md +8 -8
- package/assets/templates/method.md +16 -8
- package/assets/templates/misconception.md +10 -10
- package/assets/templates/person.md +8 -8
- package/assets/templates/research-note.md +10 -10
- package/assets/templates/resume.md +11 -10
- package/assets/templates/source-summary.md +8 -12
- package/assets/tiangong-wiki-framework.png +0 -0
- package/assets/wiki.config.default.json +6 -3
- package/dist/commands/asset.js +21 -0
- package/dist/commands/skill.js +78 -0
- package/dist/commands/template.js +30 -0
- package/dist/core/cli-env.js +34 -5
- package/dist/core/global-config.js +61 -0
- package/dist/core/onboarding.js +252 -102
- package/dist/core/workflow-context.js +58 -21
- package/dist/core/workspace-skills.js +496 -60
- package/dist/daemon/server.js +8 -0
- package/dist/index.js +36 -1
- package/dist/operations/asset.js +81 -0
- package/dist/operations/query.js +25 -1
- package/dist/operations/template-lint.js +160 -0
- package/dist/utils/asset.js +75 -0
- package/dist/utils/errors.js +6 -0
- package/package.json +2 -1
- package/references/cli-interface.md +32 -1
- package/references/template-design-guide.md +125 -113
- package/references/{env.md → troubleshooting.md} +64 -33
- package/references/vault-to-wiki-instruction.md +109 -51
- package/references/wiki-maintenance-instruction.md +15 -15
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import packageJson from "../package.json" with { type: "json" };
|
|
4
|
+
import { registerAssetCommand } from "./commands/asset.js";
|
|
4
5
|
import { registerCheckConfigCommand } from "./commands/check-config.js";
|
|
5
6
|
import { registerCreateCommand } from "./commands/create.js";
|
|
6
7
|
import { registerDaemonCommand } from "./commands/daemon.js";
|
|
@@ -17,6 +18,7 @@ import { registerListCommand } from "./commands/list.js";
|
|
|
17
18
|
import { registerPageInfoCommand } from "./commands/page-info.js";
|
|
18
19
|
import { registerSearchCommand } from "./commands/search.js";
|
|
19
20
|
import { registerSetupCommand } from "./commands/setup.js";
|
|
21
|
+
import { registerSkillCommand } from "./commands/skill.js";
|
|
20
22
|
import { registerStatCommand } from "./commands/stat.js";
|
|
21
23
|
import { registerSyncCommand } from "./commands/sync.js";
|
|
22
24
|
import { registerTemplateCommand } from "./commands/template.js";
|
|
@@ -27,12 +29,39 @@ import { loadRuntimeConfig } from "./core/runtime.js";
|
|
|
27
29
|
import { embedPendingPages } from "./core/sync.js";
|
|
28
30
|
import { processVaultQueueBatch } from "./core/vault-processing.js";
|
|
29
31
|
import { handleCliError, writeJson } from "./utils/output.js";
|
|
32
|
+
function extractEnvFileOption(argv) {
|
|
33
|
+
const nextArgv = argv.slice(0, 2);
|
|
34
|
+
let envFile = null;
|
|
35
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
36
|
+
const arg = argv[index];
|
|
37
|
+
if (arg === "--env-file") {
|
|
38
|
+
const value = argv[index + 1];
|
|
39
|
+
if (!value || value.startsWith("-")) {
|
|
40
|
+
throw new Error("--env-file requires a value");
|
|
41
|
+
}
|
|
42
|
+
envFile = value;
|
|
43
|
+
index += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg.startsWith("--env-file=")) {
|
|
47
|
+
const value = arg.slice("--env-file=".length);
|
|
48
|
+
if (!value) {
|
|
49
|
+
throw new Error("--env-file requires a value");
|
|
50
|
+
}
|
|
51
|
+
envFile = value;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
nextArgv.push(arg);
|
|
55
|
+
}
|
|
56
|
+
return { envFile, argv: nextArgv };
|
|
57
|
+
}
|
|
30
58
|
function buildProgram() {
|
|
31
59
|
const program = new Command();
|
|
32
60
|
program
|
|
33
61
|
.name("tiangong-wiki")
|
|
34
62
|
.description("Tiangong Wiki — local-first indexing and query CLI")
|
|
35
63
|
.version(packageJson.version)
|
|
64
|
+
.option("--env-file <path>", "Load runtime environment from a specific .wiki.env file")
|
|
36
65
|
.showHelpAfterError();
|
|
37
66
|
let runtimeConfig;
|
|
38
67
|
try {
|
|
@@ -42,10 +71,12 @@ function buildProgram() {
|
|
|
42
71
|
runtimeConfig = undefined;
|
|
43
72
|
}
|
|
44
73
|
registerSetupCommand(program);
|
|
74
|
+
registerSkillCommand(program);
|
|
45
75
|
registerInitCommand(program);
|
|
46
76
|
registerDoctorCommand(program);
|
|
47
77
|
registerSyncCommand(program);
|
|
48
78
|
registerCheckConfigCommand(program);
|
|
79
|
+
registerAssetCommand(program);
|
|
49
80
|
registerFindCommand(program, runtimeConfig);
|
|
50
81
|
registerSearchCommand(program);
|
|
51
82
|
registerFtsCommand(program);
|
|
@@ -77,9 +108,13 @@ function buildProgram() {
|
|
|
77
108
|
return program;
|
|
78
109
|
}
|
|
79
110
|
try {
|
|
111
|
+
const { envFile, argv } = extractEnvFileOption(process.argv);
|
|
112
|
+
if (envFile) {
|
|
113
|
+
process.env.WIKI_ENV_FILE = envFile;
|
|
114
|
+
}
|
|
80
115
|
applyCliEnvironment(process.env, process.cwd());
|
|
81
116
|
const program = buildProgram();
|
|
82
|
-
await program.parseAsync(
|
|
117
|
+
await program.parseAsync(argv);
|
|
83
118
|
}
|
|
84
119
|
catch (error) {
|
|
85
120
|
handleCliError(error);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { copyFileSync, renameSync, unlinkSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRuntimePaths } from "../core/paths.js";
|
|
4
|
+
import { AppError } from "../utils/errors.js";
|
|
5
|
+
import { findCandidates, nextAvailableName, resolveAssetDir, toPosixSlashes, toSlug, validateSlug, validateSourceFile, } from "../utils/asset.js";
|
|
6
|
+
import { ensureDirSync, pathExistsSync } from "../utils/fs.js";
|
|
7
|
+
export function saveAsset(env, sourceFile, options = {}) {
|
|
8
|
+
const assetType = options.type ?? "image";
|
|
9
|
+
const { wikiRoot } = resolveRuntimePaths(env);
|
|
10
|
+
const absSource = path.resolve(sourceFile);
|
|
11
|
+
validateSourceFile(absSource);
|
|
12
|
+
const sourceExt = path.extname(absSource).replace(/^\./, "").toLowerCase();
|
|
13
|
+
if (!sourceExt) {
|
|
14
|
+
throw new AppError("Source file has no extension", "config");
|
|
15
|
+
}
|
|
16
|
+
const name = options.name ? (validateSlug(options.name), options.name) : toSlug(path.basename(absSource, path.extname(absSource)));
|
|
17
|
+
const assetDir = resolveAssetDir(wikiRoot, assetType);
|
|
18
|
+
ensureDirSync(assetDir);
|
|
19
|
+
const finalName = nextAvailableName(assetDir, name, sourceExt);
|
|
20
|
+
const finalPath = path.join(assetDir, finalName);
|
|
21
|
+
const tmpName = `.tmp-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${sourceExt}`;
|
|
22
|
+
const tmpPath = path.join(assetDir, tmpName);
|
|
23
|
+
try {
|
|
24
|
+
copyFileSync(absSource, tmpPath);
|
|
25
|
+
renameSync(tmpPath, finalPath);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
try {
|
|
29
|
+
if (pathExistsSync(tmpPath)) {
|
|
30
|
+
unlinkSync(tmpPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// ignore cleanup errors
|
|
35
|
+
}
|
|
36
|
+
throw new AppError(`Failed to save asset: ${err instanceof Error ? err.message : String(err)}`, "runtime");
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
assetPath: toPosixSlashes(path.relative(wikiRoot, finalPath)),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function refAsset(env, assetPathOrName, options) {
|
|
43
|
+
const assetType = options.type ?? "image";
|
|
44
|
+
const { wikiRoot, wikiPath } = resolveRuntimePaths(env);
|
|
45
|
+
// Normalize: if no directory separator, treat as filename and prepend type dir
|
|
46
|
+
const assetRelPath = assetPathOrName.includes("/") || assetPathOrName.includes("\\")
|
|
47
|
+
? assetPathOrName
|
|
48
|
+
: toPosixSlashes(path.join(resolveAssetDir("", assetType), assetPathOrName));
|
|
49
|
+
const absAssetPath = path.join(wikiRoot, assetRelPath);
|
|
50
|
+
const absPageDir = path.dirname(path.join(wikiPath, options.page));
|
|
51
|
+
if (pathExistsSync(absAssetPath)) {
|
|
52
|
+
const rel = path.relative(absPageDir, absAssetPath);
|
|
53
|
+
return {
|
|
54
|
+
relativePath: toPosixSlashes(rel),
|
|
55
|
+
assetPath: toPosixSlashes(assetRelPath),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Try candidate matching
|
|
59
|
+
const assetDir = resolveAssetDir(wikiRoot, assetType);
|
|
60
|
+
const fileName = path.basename(assetPathOrName);
|
|
61
|
+
const ext = path.extname(fileName).replace(/^\./, "");
|
|
62
|
+
const baseName = path.basename(fileName, path.extname(fileName));
|
|
63
|
+
if (ext && baseName) {
|
|
64
|
+
const candidates = findCandidates(assetDir, baseName, ext);
|
|
65
|
+
if (candidates.length > 0) {
|
|
66
|
+
return {
|
|
67
|
+
match: "candidates",
|
|
68
|
+
message: `${fileName} not found, but similar files exist`,
|
|
69
|
+
candidates: candidates.map((c) => {
|
|
70
|
+
const cAssetPath = toPosixSlashes(path.relative(wikiRoot, path.join(assetDir, c)));
|
|
71
|
+
const cRelPath = path.relative(absPageDir, path.join(assetDir, c));
|
|
72
|
+
return {
|
|
73
|
+
relativePath: toPosixSlashes(cRelPath),
|
|
74
|
+
assetPath: cAssetPath,
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw new AppError(`Asset not found: ${assetPathOrName}`, "not_found");
|
|
81
|
+
}
|
package/dist/operations/query.js
CHANGED
|
@@ -9,7 +9,7 @@ import { readAllPages } from "../core/sync.js";
|
|
|
9
9
|
import { getVaultQueueSnapshot } from "../core/vault-processing.js";
|
|
10
10
|
import { camelToSnake } from "../utils/case.js";
|
|
11
11
|
import { AppError } from "../utils/errors.js";
|
|
12
|
-
import { listFilesRecursiveSync } from "../utils/fs.js";
|
|
12
|
+
import { listFilesRecursiveSync, pathExistsSync } from "../utils/fs.js";
|
|
13
13
|
import { normalizeFtsQuery } from "../utils/segmenter.js";
|
|
14
14
|
function parsePositiveLimit(value, label, fallback) {
|
|
15
15
|
const normalized = value ?? fallback;
|
|
@@ -609,6 +609,30 @@ export function runLint(env = process.env, options = {}) {
|
|
|
609
609
|
if (page.unregisteredFields.length > 0) {
|
|
610
610
|
addLintItem(result.info, page.page.id, "unregistered_fields", `Unregistered fields: ${page.unregisteredFields.join(", ")}`);
|
|
611
611
|
}
|
|
612
|
+
// Check for broken image references in page body
|
|
613
|
+
const imageRefPattern = /!\[[^\]]*\]\(([^)]+)\)/g;
|
|
614
|
+
let imageMatch;
|
|
615
|
+
while ((imageMatch = imageRefPattern.exec(page.body)) !== null) {
|
|
616
|
+
let refPath = imageMatch[1].trim();
|
|
617
|
+
// Strip optional markdown title: 
|
|
618
|
+
const titleMatch = refPath.match(/^(\S+)\s+"[^"]*"$/);
|
|
619
|
+
if (titleMatch)
|
|
620
|
+
refPath = titleMatch[1];
|
|
621
|
+
// Skip non-local references
|
|
622
|
+
if (!refPath || refPath.startsWith("http://") || refPath.startsWith("https://")
|
|
623
|
+
|| refPath.startsWith("data:") || refPath.startsWith("#"))
|
|
624
|
+
continue;
|
|
625
|
+
// Decode URL-encoded paths (e.g., %20 → space)
|
|
626
|
+
try {
|
|
627
|
+
refPath = decodeURIComponent(refPath);
|
|
628
|
+
}
|
|
629
|
+
catch { /* use as-is */ }
|
|
630
|
+
// Resolve relative to the page file's directory
|
|
631
|
+
const absRefPath = path.resolve(path.dirname(filePath), refPath);
|
|
632
|
+
if (!pathExistsSync(absRefPath)) {
|
|
633
|
+
addLintItem(result.warnings, page.page.id, "broken_image_ref", `Image not found: ${refPath}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
612
636
|
}
|
|
613
637
|
const draftCount = indexedPages.filter((page) => page.status === "draft").length;
|
|
614
638
|
const pendingEmbeddings = indexedPages.filter((page) => page.embeddingStatus !== "done").length;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import matter from "gray-matter";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getTemplate, resolveTemplateFilePath } from "../core/config.js";
|
|
4
|
+
import { loadRuntimeConfig } from "../core/runtime.js";
|
|
5
|
+
import { AppError } from "../utils/errors.js";
|
|
6
|
+
import { pathExistsSync, readTextFileSync } from "../utils/fs.js";
|
|
7
|
+
const REQUIRED_TEMPLATE_FIELDS = [
|
|
8
|
+
"pageType",
|
|
9
|
+
"title",
|
|
10
|
+
"status",
|
|
11
|
+
"visibility",
|
|
12
|
+
"sourceRefs",
|
|
13
|
+
"relatedPages",
|
|
14
|
+
"tags",
|
|
15
|
+
"createdAt",
|
|
16
|
+
"updatedAt",
|
|
17
|
+
];
|
|
18
|
+
const FIXED_TEMPLATE_FIELDS = new Set([
|
|
19
|
+
...REQUIRED_TEMPLATE_FIELDS,
|
|
20
|
+
"nodeId",
|
|
21
|
+
]);
|
|
22
|
+
function ensureTemplateLintLevel(value) {
|
|
23
|
+
const normalized = (value ?? "info").toLowerCase();
|
|
24
|
+
if (normalized === "error" || normalized === "warning" || normalized === "info") {
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
throw new AppError(`Invalid lint level: ${value}`, "config");
|
|
28
|
+
}
|
|
29
|
+
function isPlainObject(value) {
|
|
30
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
function hasField(data, field) {
|
|
33
|
+
return Object.prototype.hasOwnProperty.call(data, field);
|
|
34
|
+
}
|
|
35
|
+
function collectBodySections(markdown) {
|
|
36
|
+
return markdown
|
|
37
|
+
.split(/\r?\n/)
|
|
38
|
+
.map((line) => line.trim())
|
|
39
|
+
.filter((line) => /^##\s+\S+/.test(line));
|
|
40
|
+
}
|
|
41
|
+
function addItem(collection, pageType, template, check, message) {
|
|
42
|
+
collection.push({ pageType, template, check, message });
|
|
43
|
+
}
|
|
44
|
+
export function runTemplateLint(env = process.env, options = {}) {
|
|
45
|
+
const level = ensureTemplateLintLevel(options.level);
|
|
46
|
+
const { paths, config } = loadRuntimeConfig(env);
|
|
47
|
+
if (options.pageType) {
|
|
48
|
+
getTemplate(config, options.pageType);
|
|
49
|
+
}
|
|
50
|
+
const pageTypes = (options.pageType ? [options.pageType] : Object.keys(config.templates)).sort((left, right) => left.localeCompare(right));
|
|
51
|
+
const result = {
|
|
52
|
+
errors: [],
|
|
53
|
+
warnings: [],
|
|
54
|
+
info: [],
|
|
55
|
+
summary: {
|
|
56
|
+
templates: pageTypes.length,
|
|
57
|
+
errors: 0,
|
|
58
|
+
warnings: 0,
|
|
59
|
+
info: 0,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
for (const pageType of pageTypes) {
|
|
63
|
+
const templateConfig = config.templates[pageType];
|
|
64
|
+
const templatePath = resolveTemplateFilePath(config, paths.wikiRoot, pageType);
|
|
65
|
+
const templateLabel = templateConfig.file;
|
|
66
|
+
if (!pathExistsSync(templatePath)) {
|
|
67
|
+
addItem(result.errors, pageType, templateLabel, "template_file_missing", `Template file not found: ${path.relative(paths.wikiRoot, templatePath)}`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = matter(readTextFileSync(templatePath));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
addItem(result.errors, pageType, templateLabel, "template_parse_error", error instanceof Error ? error.message : String(error));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!isPlainObject(parsed.data)) {
|
|
79
|
+
addItem(result.errors, pageType, templateLabel, "invalid_frontmatter", "Template frontmatter must be a YAML object.");
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const data = parsed.data;
|
|
83
|
+
const registeredFields = new Set([
|
|
84
|
+
...FIXED_TEMPLATE_FIELDS,
|
|
85
|
+
...Object.keys(config.customColumns),
|
|
86
|
+
...Object.keys(config.commonEdges),
|
|
87
|
+
...Object.keys(templateConfig.columns),
|
|
88
|
+
...Object.keys(templateConfig.edges),
|
|
89
|
+
]);
|
|
90
|
+
const missingRequiredFields = REQUIRED_TEMPLATE_FIELDS.filter((field) => !hasField(data, field));
|
|
91
|
+
if (missingRequiredFields.length > 0) {
|
|
92
|
+
addItem(result.errors, pageType, templateLabel, "missing_required_fields", `Missing required template fields: ${missingRequiredFields.join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
if (data.pageType !== pageType) {
|
|
95
|
+
addItem(result.errors, pageType, templateLabel, "template_page_type_mismatch", `Frontmatter pageType must be "${pageType}", got "${String(data.pageType ?? "")}"`);
|
|
96
|
+
}
|
|
97
|
+
const unregisteredFields = Object.keys(data)
|
|
98
|
+
.filter((field) => !registeredFields.has(field))
|
|
99
|
+
.sort();
|
|
100
|
+
if (unregisteredFields.length > 0) {
|
|
101
|
+
addItem(result.errors, pageType, templateLabel, "unregistered_template_fields", `Fields present in template frontmatter but not declared in schema: ${unregisteredFields.join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
const unregisteredSummaryFields = templateConfig.summaryFields
|
|
104
|
+
.filter((field) => !registeredFields.has(field))
|
|
105
|
+
.sort();
|
|
106
|
+
if (unregisteredSummaryFields.length > 0) {
|
|
107
|
+
addItem(result.errors, pageType, templateLabel, "summary_fields_unregistered", `summaryFields reference undeclared fields: ${unregisteredSummaryFields.join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
const missingCommonEdgeFields = Object.keys(config.commonEdges)
|
|
110
|
+
.filter((field) => !hasField(data, field))
|
|
111
|
+
.sort();
|
|
112
|
+
if (missingCommonEdgeFields.length > 0) {
|
|
113
|
+
addItem(result.warnings, pageType, templateLabel, "common_edge_fields_missing", `Common edge fields missing from template frontmatter: ${missingCommonEdgeFields.join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
const missingDeclaredFields = [...Object.keys(templateConfig.columns), ...Object.keys(templateConfig.edges)]
|
|
116
|
+
.filter((field) => !hasField(data, field))
|
|
117
|
+
.sort();
|
|
118
|
+
if (missingDeclaredFields.length > 0) {
|
|
119
|
+
addItem(result.warnings, pageType, templateLabel, "declared_fields_missing", `Fields declared in config but absent from template frontmatter: ${missingDeclaredFields.join(", ")}`);
|
|
120
|
+
}
|
|
121
|
+
const arrayBackedFields = [...Object.keys(config.commonEdges), ...Object.keys(templateConfig.edges)].sort();
|
|
122
|
+
const nonArrayEdgeFields = arrayBackedFields.filter((field) => hasField(data, field) && !Array.isArray(data[field]));
|
|
123
|
+
if (nonArrayEdgeFields.length > 0) {
|
|
124
|
+
addItem(result.errors, pageType, templateLabel, "edge_fields_not_array", `Edge fields must be arrays in template frontmatter: ${nonArrayEdgeFields.join(", ")}`);
|
|
125
|
+
}
|
|
126
|
+
const sections = collectBodySections(parsed.content);
|
|
127
|
+
if (sections.length < 2) {
|
|
128
|
+
addItem(result.warnings, pageType, templateLabel, "body_sections_min", `Template body should contain at least 2 level-2 sections, found ${sections.length}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
result.summary = {
|
|
132
|
+
templates: pageTypes.length,
|
|
133
|
+
errors: result.errors.length,
|
|
134
|
+
warnings: level === "warning" || level === "info" ? result.warnings.length : 0,
|
|
135
|
+
info: level === "info" ? result.info.length : 0,
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
errors: result.errors,
|
|
139
|
+
warnings: level === "warning" || level === "info" ? result.warnings : [],
|
|
140
|
+
info: level === "info" ? result.info : [],
|
|
141
|
+
summary: result.summary,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function renderTemplateLintResult(result) {
|
|
145
|
+
const lines = [`tiangong-wiki template lint: ${result.summary.templates} templates checked`, ""];
|
|
146
|
+
const sections = [
|
|
147
|
+
{ label: "ERROR", items: result.errors },
|
|
148
|
+
{ label: "WARN", items: result.warnings },
|
|
149
|
+
{ label: "INFO", items: result.info },
|
|
150
|
+
];
|
|
151
|
+
for (const section of sections) {
|
|
152
|
+
for (const item of section.items) {
|
|
153
|
+
lines.push(` ${section.label.padEnd(5)} ${item.pageType} (${item.template})`);
|
|
154
|
+
lines.push(` ${item.message}`);
|
|
155
|
+
lines.push("");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
lines.push(`Summary: ${result.summary.errors} errors, ${result.summary.warnings} warnings, ${result.summary.info} info`);
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AppError } from "./errors.js";
|
|
4
|
+
import { pathExistsSync } from "./fs.js";
|
|
5
|
+
export const ASSET_TYPE_DIRS = {
|
|
6
|
+
image: "assets/images",
|
|
7
|
+
};
|
|
8
|
+
const SLUG_PATTERN = /^[a-z0-9-]{1,80}$/;
|
|
9
|
+
const MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024; // 20MB
|
|
10
|
+
export function resolveAssetDir(wikiRoot, assetType) {
|
|
11
|
+
const subdir = ASSET_TYPE_DIRS[assetType];
|
|
12
|
+
if (!subdir) {
|
|
13
|
+
throw new AppError(`Unsupported asset type: ${assetType}. Supported: ${Object.keys(ASSET_TYPE_DIRS).join(", ")}`, "config");
|
|
14
|
+
}
|
|
15
|
+
return path.join(wikiRoot, subdir);
|
|
16
|
+
}
|
|
17
|
+
export function validateSlug(name) {
|
|
18
|
+
if (!SLUG_PATTERN.test(name)) {
|
|
19
|
+
throw new AppError(`Invalid asset name: ${name} (must match [a-z0-9-], 1-80 chars)`, "config");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function toSlug(rawName) {
|
|
23
|
+
const slug = rawName
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
26
|
+
.replace(/^-+|-+$/g, "")
|
|
27
|
+
.slice(0, 80);
|
|
28
|
+
if (!slug) {
|
|
29
|
+
throw new AppError("Cannot derive a valid slug from the source filename", "config");
|
|
30
|
+
}
|
|
31
|
+
return slug;
|
|
32
|
+
}
|
|
33
|
+
export function validateSourceFile(filePath) {
|
|
34
|
+
if (!pathExistsSync(filePath)) {
|
|
35
|
+
throw new AppError(`Source file not found: ${filePath}`, "not_found");
|
|
36
|
+
}
|
|
37
|
+
const stat = statSync(filePath);
|
|
38
|
+
if (!stat.isFile()) {
|
|
39
|
+
throw new AppError(`Source path is not a regular file: ${filePath}`, "config");
|
|
40
|
+
}
|
|
41
|
+
if (stat.size > MAX_FILE_SIZE_BYTES) {
|
|
42
|
+
const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
|
|
43
|
+
throw new AppError(`File too large: ${sizeMB}MB (max 20MB)`, "config");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function nextAvailableName(dir, name, ext) {
|
|
47
|
+
const base = `${name}.${ext}`;
|
|
48
|
+
if (!pathExistsSync(path.join(dir, base))) {
|
|
49
|
+
return base;
|
|
50
|
+
}
|
|
51
|
+
for (let i = 1;; i++) {
|
|
52
|
+
const candidate = `${name}-${i}.${ext}`;
|
|
53
|
+
if (!pathExistsSync(path.join(dir, candidate))) {
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function findCandidates(dir, name, ext) {
|
|
59
|
+
if (!pathExistsSync(dir)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const pattern = new RegExp(`^${escapeRegExp(name)}(-\\d+)?\\.${escapeRegExp(ext)}$`);
|
|
63
|
+
try {
|
|
64
|
+
return readdirSync(dir).filter((entry) => pattern.test(entry)).sort();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function escapeRegExp(s) {
|
|
71
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
72
|
+
}
|
|
73
|
+
export function toPosixSlashes(p) {
|
|
74
|
+
return p.replace(/\\/g, "/");
|
|
75
|
+
}
|
package/dist/utils/errors.js
CHANGED
|
@@ -10,11 +10,17 @@ export class AppError extends Error {
|
|
|
10
10
|
this.details = details;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
function isPromptAbortError(error) {
|
|
14
|
+
return error.name === "ExitPromptError" || error.message.startsWith("User force closed the prompt");
|
|
15
|
+
}
|
|
13
16
|
export function asAppError(error) {
|
|
14
17
|
if (error instanceof AppError) {
|
|
15
18
|
return error;
|
|
16
19
|
}
|
|
17
20
|
if (error instanceof Error) {
|
|
21
|
+
if (isPromptAbortError(error)) {
|
|
22
|
+
return new AppError("Prompt cancelled by user.", "runtime");
|
|
23
|
+
}
|
|
18
24
|
return new AppError(error.message, "runtime");
|
|
19
25
|
}
|
|
20
26
|
return new AppError(String(error), "runtime");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@biaoo/tiangong-wiki",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Local-first wiki index and query engine for Markdown knowledge pages (Tiangong Wiki).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"@antv/g6": "^5.1.0",
|
|
44
44
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
45
45
|
"@fontsource/space-grotesk": "^5.2.10",
|
|
46
|
+
"@inquirer/prompts": "^7.10.1",
|
|
46
47
|
"@openai/codex-sdk": "^0.118.0",
|
|
47
48
|
"adm-zip": "^0.5.17",
|
|
48
49
|
"better-sqlite3": "^12.8.0",
|
|
@@ -13,6 +13,13 @@ npx @biaoo/tiangong-wiki <command> [options]
|
|
|
13
13
|
npm run dev -- <command> [options]
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
Global workspace resolution priority:
|
|
17
|
+
|
|
18
|
+
1. `--env-file <path>`
|
|
19
|
+
2. `WIKI_ENV_FILE`
|
|
20
|
+
3. nearest `.wiki.env` found by walking upward from the current directory
|
|
21
|
+
4. the global default workspace config written by `tiangong-wiki setup`
|
|
22
|
+
|
|
16
23
|
---
|
|
17
24
|
|
|
18
25
|
## Command Overview
|
|
@@ -20,6 +27,7 @@ npm run dev -- <command> [options]
|
|
|
20
27
|
| Command | Description |
|
|
21
28
|
| --- | --- |
|
|
22
29
|
| `setup` | Interactive configuration wizard — writes `.wiki.env` and scaffolds workspace |
|
|
30
|
+
| `skill` | Inspect and update workspace-local managed skills |
|
|
23
31
|
| `doctor` | Diagnose configuration, paths, embedding, and daemon health |
|
|
24
32
|
| `init` | Initialize workspace assets and run the first sync |
|
|
25
33
|
| `sync` | Incrementally sync pages, embeddings, and vault metadata |
|
|
@@ -56,6 +64,7 @@ Interactive step-by-step wizard that:
|
|
|
56
64
|
- Records `WIKI_PATH`, `VAULT_PATH`, `WIKI_DB_PATH`, `WIKI_CONFIG_PATH`, `WIKI_TEMPLATES_PATH`
|
|
57
65
|
- Optionally configures `EMBEDDING_*` and Synology vault settings
|
|
58
66
|
- Writes `.wiki.env` in the current working directory
|
|
67
|
+
- Writes a global default workspace config pointing to that `.wiki.env`
|
|
59
68
|
- Scaffolds `wiki/pages/`, `vault/`, `wiki.config.json`, and `templates/`
|
|
60
69
|
|
|
61
70
|
After setup, run `tiangong-wiki doctor` then `tiangong-wiki init` to complete initialization.
|
|
@@ -63,11 +72,12 @@ After setup, run `tiangong-wiki doctor` then `tiangong-wiki init` to complete in
|
|
|
63
72
|
### doctor
|
|
64
73
|
|
|
65
74
|
```
|
|
66
|
-
tiangong-wiki doctor [--probe] [--format text|json]
|
|
75
|
+
tiangong-wiki doctor [--env-file <path>] [--probe] [--format text|json]
|
|
67
76
|
```
|
|
68
77
|
|
|
69
78
|
| Option | Description |
|
|
70
79
|
| --- | --- |
|
|
80
|
+
| `--env-file` | Load a specific `.wiki.env` before running the command |
|
|
71
81
|
| `--probe` | Additionally test remote services (embedding endpoint, Synology NAS) |
|
|
72
82
|
| `--format` | Output format: `text` (default) or `json` |
|
|
73
83
|
|
|
@@ -196,16 +206,37 @@ Creates a page from the corresponding template in `wiki/templates/`, fills front
|
|
|
196
206
|
|
|
197
207
|
Output: `{ created, filePath }`.
|
|
198
208
|
|
|
209
|
+
### skill
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
tiangong-wiki skill add <source> --skill <name> [--force] [--format text|json]
|
|
213
|
+
tiangong-wiki skill status [name] [--format text|json]
|
|
214
|
+
tiangong-wiki skill update [name] [--all] [--force] [--format text|json]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- `add` — Install a skill from any repo URL or local path via the external `npx skills add <source> --skill <name> -a codex -y` flow, then start tracking it as a managed workspace-local skill
|
|
218
|
+
- `status` — Inspect managed workspace-local skills such as `tiangong-wiki-skill`, parser skills declared in `WIKI_PARSER_SKILLS`, and repo/path-sourced skills previously added via `skill add`
|
|
219
|
+
- `update` — Install missing managed skills, refresh skills that have newer source content, and refuse to overwrite local conflicts unless `--force` is passed
|
|
220
|
+
|
|
221
|
+
The command distinguishes at least four states:
|
|
222
|
+
|
|
223
|
+
- `up_to_date` — Installed content matches the current managed source
|
|
224
|
+
- `update_available` — Upstream changed and local files still match the managed baseline
|
|
225
|
+
- `conflict` — Local files differ from the managed baseline, so update will be skipped unless `--force` is used
|
|
226
|
+
- `missing` — Skill is absent or unreadable and can be installed
|
|
227
|
+
|
|
199
228
|
### template
|
|
200
229
|
|
|
201
230
|
```
|
|
202
231
|
tiangong-wiki template list [--format text|json]
|
|
203
232
|
tiangong-wiki template show <pageType> [--format text|json]
|
|
233
|
+
tiangong-wiki template lint [pageType] [--level error|warning|info] [--format text|json]
|
|
204
234
|
tiangong-wiki template create --type <pageType> --title <title>
|
|
205
235
|
```
|
|
206
236
|
|
|
207
237
|
- `list` — Show registered templates
|
|
208
238
|
- `show` — Display template content for a specific type
|
|
239
|
+
- `lint` — Validate template frontmatter, schema declarations, summaryFields, and minimum body structure
|
|
209
240
|
- `create` — Generate a new template file in `wiki/templates/` and register it in `wiki.config.json`
|
|
210
241
|
|
|
211
242
|
### type
|