@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
|
@@ -14,18 +14,26 @@ effectiveness:
|
|
|
14
14
|
applicableTo: []
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Purpose & Mechanism
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
State what problem this method solves and why it works. Do not restate the title — explain the underlying mechanism or principle that makes this method effective.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Steps / Procedure
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
List the concrete steps to execute this method. Each step should be actionable and verifiable. If the method has variants, describe the core sequence first, then note variations.
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Applicable Scenarios
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Specify the preconditions, input types, and constraints under which this method is the right choice. Be concrete — name the kind of problem, data, or situation, not just "when appropriate."
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Inapplicable Scenarios
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
State when this method should NOT be used. Name the boundary conditions, failure triggers, or assumptions that must hold. If these break, what goes wrong?
|
|
32
|
+
|
|
33
|
+
## Failure Modes
|
|
34
|
+
|
|
35
|
+
Describe how this method fails when misapplied. What does a bad outcome look like? What early signals indicate the method is not working? This section prevents blind reuse.
|
|
36
|
+
|
|
37
|
+
## Evidence
|
|
38
|
+
|
|
39
|
+
Record real uses of this method with measurable outcomes. Include context, results, and comparison baselines where available. Do not write "it worked well" without specifics — state what was measured and what changed.
|
|
@@ -14,22 +14,22 @@ resolvedAt:
|
|
|
14
14
|
correctedConcepts: []
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## False Belief
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
State the incorrect understanding exactly as it was held — without softening, defending, or pre-correcting it. A reader should be able to recognize this belief if they currently hold it.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Why It Is Wrong
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Identify the specific evidence, counterexample, or reasoning flaw that disproves the false belief. Do not just assert it is wrong — show the proof.
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Correct Understanding
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
State the corrected understanding clearly and precisely. Highlight the key difference from the false belief — what specifically changes?
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Turning Point
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Record the moment, material, experiment, or conversation that caused the actual cognitive shift. If no single turning point exists, this section may be omitted.
|
|
32
32
|
|
|
33
|
-
##
|
|
33
|
+
## Recurrence Prevention
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Write the checks, questions, or heuristics to apply in the future to avoid falling back into this misconception. Each item should be a concrete test, not a vague reminder.
|
|
@@ -14,18 +14,18 @@ context:
|
|
|
14
14
|
contact:
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Identity & Role
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
State this person's name, professional role, organization, and domain expertise. Focus on facts that make this person a reusable knowledge node — not personal relationship notes.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Context & Collaboration
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Describe the projects, domains, or initiatives where this person is relevant. Link to existing wiki pages for shared projects or topics. This section should help graph traversal — a reader should understand why this person node connects to other knowledge.
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Key Contributions
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Record this person's notable contributions, decisions, or expertise that are valuable beyond a single interaction. Write facts and outcomes, not impressions. If contributions are significant enough, they should be separate achievement pages linked via relatedPages.
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Notes
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Brief factual notes for future reference: contact preferences, scheduling constraints, areas of ongoing work. Keep this minimal — this is not a personal diary entry.
|
|
@@ -13,22 +13,22 @@ researchTopic:
|
|
|
13
13
|
stage:
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Research Question
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
State the specific question being investigated and why it matters. A well-scoped question should be falsifiable or at least answerable with evidence.
|
|
19
19
|
|
|
20
|
-
##
|
|
20
|
+
## Literature & Sources
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
List sources consulted so far. For each, note: what it contributes (supports / contradicts / is tangential) and what key claim or data it provides. Link to source-summary pages where they exist.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Working Hypothesis
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
State your current best explanation or framework. Label it explicitly as a hypothesis — this is not a conclusion. Include the assumptions it rests on.
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## Open Questions
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
List unresolved issues: contradictory evidence, data gaps, untested assumptions, or alternative explanations you have not yet ruled out.
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## Next Actions
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Describe the specific next steps: what to read, test, build, or ask. Each action should be concrete enough to execute immediately — "research more" is not an action.
|
|
@@ -11,24 +11,25 @@ createdAt: 2026-04-06
|
|
|
11
11
|
updatedAt: 2026-04-06
|
|
12
12
|
targetAudience:
|
|
13
13
|
lastReviewedAt:
|
|
14
|
+
vaultPath:
|
|
14
15
|
---
|
|
15
16
|
|
|
16
|
-
##
|
|
17
|
+
## Positioning
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
State the target audience for this resume and the core professional identity in one sentence. What should the reader remember after 10 seconds?
|
|
19
20
|
|
|
20
|
-
##
|
|
21
|
+
## Education & Work Background
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
List education and work history in order of relevance to the target audience, not chronological order. Keep entries factual — institution, role, duration, key focus. Do not expand into narratives here.
|
|
23
24
|
|
|
24
|
-
##
|
|
25
|
+
## Core Skills
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
List skills with evidence of proficiency. For each skill, either link to a wiki page that demonstrates it or note the context where it was applied. Do not list skills without supporting evidence.
|
|
27
28
|
|
|
28
|
-
##
|
|
29
|
+
## Selected Projects
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
Choose the most representative projects. For each: state the problem, your specific role, the action taken, and the measurable outcome. If a project has its own wiki page, link to it rather than repeating the description here.
|
|
31
32
|
|
|
32
|
-
##
|
|
33
|
+
## Honors & Achievements
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
Reference existing achievement pages via relatedPages where possible. For items not yet captured as achievement pages, provide a one-line summary with verifiable evidence (certificate, publication, link).
|
|
@@ -14,22 +14,18 @@ vaultPath:
|
|
|
14
14
|
keyFindings: []
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Source Identity
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
State what this source is (document type, author, date, context) and why it was preserved as a source-summary rather than extracted into a more specific knowledge type. If the source contains extractable concepts, methods, or lessons, those should be separate pages — not folded into this summary.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Key Claims
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
List the independently quotable factual claims, data points, or conclusions from this source. Each item should be a self-contained assertion, not a topic heading. Do not paraphrase broadly — capture what the source actually states.
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Knowledge Connections
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Identify which existing wiki pages this source supports, extends, challenges, or contradicts. Name specific pages and describe the relationship. Keep `sourceRefs` and `relatedPages` aligned with the key connections, but do not repeat them mechanically in prose. Use the body only to explain the relationship.
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Evidence Pointers
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
## 重要引用
|
|
34
|
-
|
|
35
|
-
摘录最值得回看的句子、数据点、章节或页码线索,帮助以后快速回源。
|
|
31
|
+
Optionally capture only the evidence needed for fast source recovery: short excerpts, exact data points, and precise locators such as page numbers, section headings, figure labels, or timestamps. Do not restate `sourceRefs`, and do not turn this section into a quote dump. Use it only when exact evidence localization would save future rereading.
|
|
Binary file
|
|
@@ -91,7 +91,8 @@
|
|
|
91
91
|
"file": "templates/source-summary.md",
|
|
92
92
|
"columns": {
|
|
93
93
|
"sourceType": "text",
|
|
94
|
-
"vaultPath": "text"
|
|
94
|
+
"vaultPath": "text",
|
|
95
|
+
"keyFindings": "text"
|
|
95
96
|
},
|
|
96
97
|
"edges": {},
|
|
97
98
|
"summaryFields": [
|
|
@@ -116,7 +117,8 @@
|
|
|
116
117
|
"file": "templates/method.md",
|
|
117
118
|
"columns": {
|
|
118
119
|
"domain": "text",
|
|
119
|
-
"effectiveness": "text"
|
|
120
|
+
"effectiveness": "text",
|
|
121
|
+
"applicableTo": "text"
|
|
120
122
|
},
|
|
121
123
|
"edges": {},
|
|
122
124
|
"summaryFields": [
|
|
@@ -159,7 +161,8 @@
|
|
|
159
161
|
"file": "templates/resume.md",
|
|
160
162
|
"columns": {
|
|
161
163
|
"targetAudience": "text",
|
|
162
|
-
"lastReviewedAt": "text"
|
|
164
|
+
"lastReviewedAt": "text",
|
|
165
|
+
"vaultPath": "text"
|
|
163
166
|
},
|
|
164
167
|
"edges": {},
|
|
165
168
|
"summaryFields": [
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { refAsset, saveAsset } from "../operations/asset.js";
|
|
2
|
+
import { writeJson } from "../utils/output.js";
|
|
3
|
+
export function registerAssetCommand(program) {
|
|
4
|
+
const asset = program.command("asset").description("Manage wiki assets (images, files)");
|
|
5
|
+
asset
|
|
6
|
+
.command("save <source-file>")
|
|
7
|
+
.description("Save a file to wiki assets directory")
|
|
8
|
+
.option("--name <slug>", "Target filename in kebab-case, without extension")
|
|
9
|
+
.option("--type <asset-type>", "Asset type (determines subdirectory)", "image")
|
|
10
|
+
.action(async (sourceFile, options) => {
|
|
11
|
+
writeJson(saveAsset(process.env, sourceFile, options));
|
|
12
|
+
});
|
|
13
|
+
asset
|
|
14
|
+
.command("ref <asset-path-or-name>")
|
|
15
|
+
.description("Compute relative path from a page to an asset")
|
|
16
|
+
.requiredOption("--page <page-id>", "Page ID that will reference this asset")
|
|
17
|
+
.option("--type <asset-type>", "Asset type (determines lookup directory)", "image")
|
|
18
|
+
.action(async (assetPathOrName, options) => {
|
|
19
|
+
writeJson(refAsset(process.env, assetPathOrName, options));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { addManagedSkill, getManagedSkillStatus, updateManagedSkills } from "../core/workspace-skills.js";
|
|
2
|
+
import { AppError } from "../utils/errors.js";
|
|
3
|
+
import { ensureTextOrJson, writeJson, writeText } from "../utils/output.js";
|
|
4
|
+
function renderSkillStatus(payload) {
|
|
5
|
+
return payload.skills.map((item) => `${item.name}: ${item.state}\n ${item.message}`).join("\n");
|
|
6
|
+
}
|
|
7
|
+
function resolveTargetNames(name, all, options = {}) {
|
|
8
|
+
if (name && all) {
|
|
9
|
+
throw new AppError("Pass either a skill name or --all, not both.", "config");
|
|
10
|
+
}
|
|
11
|
+
if (!name && !all && options.requireSelection) {
|
|
12
|
+
throw new AppError("Pass a skill name or --all.", "config");
|
|
13
|
+
}
|
|
14
|
+
if (!name && !all) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return name ? [name] : undefined;
|
|
18
|
+
}
|
|
19
|
+
export function registerSkillCommand(program) {
|
|
20
|
+
const skill = program.command("skill").description("Inspect, install, and update workspace-local managed skills");
|
|
21
|
+
skill
|
|
22
|
+
.command("add")
|
|
23
|
+
.argument("<source>", "Skill source repo URL or local path")
|
|
24
|
+
.requiredOption("--skill <name>", "Skill name")
|
|
25
|
+
.option("--force", "Replace local conflicting changes with the latest managed snapshot")
|
|
26
|
+
.option("--format <format>", "Output format: text or json", "text")
|
|
27
|
+
.action(async (source, options) => {
|
|
28
|
+
const format = ensureTextOrJson(options.format);
|
|
29
|
+
const payload = {
|
|
30
|
+
results: [
|
|
31
|
+
addManagedSkill(process.env, source, options.skill ?? "", {
|
|
32
|
+
force: Boolean(options.force),
|
|
33
|
+
}),
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
if (format === "json") {
|
|
37
|
+
writeJson(payload);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
writeText(payload.results
|
|
41
|
+
.map((item) => `${item.name}: ${item.action} (${item.state})\n ${item.message}`)
|
|
42
|
+
.join("\n"));
|
|
43
|
+
});
|
|
44
|
+
skill
|
|
45
|
+
.command("status")
|
|
46
|
+
.argument("[name]", "Optional skill name")
|
|
47
|
+
.option("--format <format>", "Output format: text or json", "text")
|
|
48
|
+
.action(async (name, options) => {
|
|
49
|
+
const format = ensureTextOrJson(options.format);
|
|
50
|
+
const payload = { skills: getManagedSkillStatus(process.env, name ? [name] : undefined) };
|
|
51
|
+
if (format === "json") {
|
|
52
|
+
writeJson(payload);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
writeText(renderSkillStatus(payload));
|
|
56
|
+
});
|
|
57
|
+
skill
|
|
58
|
+
.command("update")
|
|
59
|
+
.argument("[name]", "Optional skill name")
|
|
60
|
+
.option("--all", "Update all managed skills")
|
|
61
|
+
.option("--force", "Replace local conflicting changes with the latest managed snapshot")
|
|
62
|
+
.option("--format <format>", "Output format: text or json", "text")
|
|
63
|
+
.action(async (name, options) => {
|
|
64
|
+
const format = ensureTextOrJson(options.format);
|
|
65
|
+
const payload = {
|
|
66
|
+
results: updateManagedSkills(process.env, resolveTargetNames(name, Boolean(options.all), { requireSelection: true }), {
|
|
67
|
+
force: Boolean(options.force),
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
if (format === "json") {
|
|
71
|
+
writeJson(payload);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
writeText(payload.results
|
|
75
|
+
.map((item) => `${item.name}: ${item.action} (${item.state})\n ${item.message}`)
|
|
76
|
+
.join("\n"));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
|
|
2
|
+
import { renderTemplateLintResult, runTemplateLint } from "../operations/template-lint.js";
|
|
2
3
|
import { createTemplate, listTemplates, showTemplate } from "../operations/type-template.js";
|
|
3
4
|
import { ensureTextOrJson, writeJson, writeText } from "../utils/output.js";
|
|
4
5
|
export function registerTemplateCommand(program) {
|
|
@@ -45,6 +46,35 @@ export function registerTemplateCommand(program) {
|
|
|
45
46
|
}
|
|
46
47
|
writeText(String(payload.content ?? ""));
|
|
47
48
|
});
|
|
49
|
+
template
|
|
50
|
+
.command("lint")
|
|
51
|
+
.argument("[pageType]", "Optional pageType to lint")
|
|
52
|
+
.option("--level <level>", "error, warning, or info", "info")
|
|
53
|
+
.option("--format <format>", "Output format: text or json", "text")
|
|
54
|
+
.action(async (pageType, options) => {
|
|
55
|
+
const format = ensureTextOrJson(options.format);
|
|
56
|
+
const payload = await executeServerBackedOperation({
|
|
57
|
+
kind: "read",
|
|
58
|
+
local: () => runTemplateLint(process.env, {
|
|
59
|
+
pageType: typeof pageType === "string" ? pageType : undefined,
|
|
60
|
+
level: options.level ?? undefined,
|
|
61
|
+
}),
|
|
62
|
+
remote: (endpoint) => requestDaemonJson({
|
|
63
|
+
endpoint,
|
|
64
|
+
method: "GET",
|
|
65
|
+
path: "/template/lint",
|
|
66
|
+
query: {
|
|
67
|
+
pageType: typeof pageType === "string" ? pageType : undefined,
|
|
68
|
+
level: options.level ?? undefined,
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
if (format === "json") {
|
|
73
|
+
writeJson(payload);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
writeText(renderTemplateLintResult(payload));
|
|
77
|
+
});
|
|
48
78
|
template
|
|
49
79
|
.command("create")
|
|
50
80
|
.requiredOption("--type <pageType>", "New pageType")
|
package/dist/core/cli-env.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { readGlobalConfig } from "./global-config.js";
|
|
2
3
|
import { pathExistsSync, readTextFileSync } from "../utils/fs.js";
|
|
3
4
|
export const DEFAULT_WIKI_ENV_FILE = ".wiki.env";
|
|
4
5
|
const EMPTY_INFO = {
|
|
@@ -6,6 +7,10 @@ const EMPTY_INFO = {
|
|
|
6
7
|
loadedPath: null,
|
|
7
8
|
autoDiscovered: false,
|
|
8
9
|
missingRequestedPath: false,
|
|
10
|
+
missingDefaultPath: false,
|
|
11
|
+
source: "none",
|
|
12
|
+
globalConfigPath: null,
|
|
13
|
+
defaultPath: null,
|
|
9
14
|
loadedKeys: [],
|
|
10
15
|
};
|
|
11
16
|
let lastCliEnvInfo = EMPTY_INFO;
|
|
@@ -86,20 +91,40 @@ export function applyCliEnvironment(targetEnv = process.env, cwd = process.cwd()
|
|
|
86
91
|
const requestedEnvFile = targetEnv.WIKI_ENV_FILE?.trim();
|
|
87
92
|
const requestedPath = requestedEnvFile ? path.resolve(cwd, requestedEnvFile) : null;
|
|
88
93
|
if (!requestedPath && hasExplicitCoreRuntimeEnv(targetEnv)) {
|
|
89
|
-
lastCliEnvInfo = { ...EMPTY_INFO };
|
|
94
|
+
lastCliEnvInfo = { ...EMPTY_INFO, source: "process-env" };
|
|
90
95
|
return lastCliEnvInfo;
|
|
91
96
|
}
|
|
92
|
-
const
|
|
97
|
+
const nearestPath = requestedPath ? null : findNearestEnvFile(cwd);
|
|
98
|
+
const globalConfig = requestedPath || nearestPath ? null : readGlobalConfig(targetEnv);
|
|
99
|
+
const defaultPath = globalConfig ? path.resolve(globalConfig.defaultEnvFile) : null;
|
|
100
|
+
const candidatePath = requestedPath ?? nearestPath ?? defaultPath;
|
|
101
|
+
const source = requestedPath
|
|
102
|
+
? "explicit-env-file"
|
|
103
|
+
: nearestPath
|
|
104
|
+
? "nearest-env-file"
|
|
105
|
+
: defaultPath
|
|
106
|
+
? "global-default-env-file"
|
|
107
|
+
: "none";
|
|
93
108
|
if (!candidatePath) {
|
|
94
|
-
lastCliEnvInfo = {
|
|
109
|
+
lastCliEnvInfo = {
|
|
110
|
+
...EMPTY_INFO,
|
|
111
|
+
requestedPath,
|
|
112
|
+
source,
|
|
113
|
+
globalConfigPath: globalConfig?.configPath ?? null,
|
|
114
|
+
defaultPath,
|
|
115
|
+
};
|
|
95
116
|
return lastCliEnvInfo;
|
|
96
117
|
}
|
|
97
118
|
if (!pathExistsSync(candidatePath)) {
|
|
98
119
|
lastCliEnvInfo = {
|
|
99
120
|
requestedPath: candidatePath,
|
|
100
121
|
loadedPath: null,
|
|
101
|
-
autoDiscovered:
|
|
122
|
+
autoDiscovered: source === "nearest-env-file",
|
|
102
123
|
missingRequestedPath: requestedPath !== null,
|
|
124
|
+
missingDefaultPath: requestedPath === null && source === "global-default-env-file",
|
|
125
|
+
source,
|
|
126
|
+
globalConfigPath: globalConfig?.configPath ?? null,
|
|
127
|
+
defaultPath,
|
|
103
128
|
loadedKeys: [],
|
|
104
129
|
};
|
|
105
130
|
return lastCliEnvInfo;
|
|
@@ -118,8 +143,12 @@ export function applyCliEnvironment(targetEnv = process.env, cwd = process.cwd()
|
|
|
118
143
|
lastCliEnvInfo = {
|
|
119
144
|
requestedPath,
|
|
120
145
|
loadedPath: candidatePath,
|
|
121
|
-
autoDiscovered:
|
|
146
|
+
autoDiscovered: source === "nearest-env-file",
|
|
122
147
|
missingRequestedPath: false,
|
|
148
|
+
missingDefaultPath: false,
|
|
149
|
+
source,
|
|
150
|
+
globalConfigPath: globalConfig?.configPath ?? null,
|
|
151
|
+
defaultPath,
|
|
123
152
|
loadedKeys,
|
|
124
153
|
};
|
|
125
154
|
return lastCliEnvInfo;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AppError } from "../utils/errors.js";
|
|
4
|
+
import { ensureDirSync, pathExistsSync, readTextFileSync, writeTextFileSync } from "../utils/fs.js";
|
|
5
|
+
export const GLOBAL_CONFIG_DIRNAME = "tiangong-wiki";
|
|
6
|
+
export const GLOBAL_CONFIG_FILENAME = "config.json";
|
|
7
|
+
function resolveConfigBaseDir(env = process.env) {
|
|
8
|
+
const xdgConfigHome = env.XDG_CONFIG_HOME?.trim();
|
|
9
|
+
if (xdgConfigHome) {
|
|
10
|
+
return path.resolve(xdgConfigHome);
|
|
11
|
+
}
|
|
12
|
+
const homeDir = env.HOME?.trim() || os.homedir();
|
|
13
|
+
return path.join(homeDir, ".config");
|
|
14
|
+
}
|
|
15
|
+
export function resolveGlobalConfigPath(env = process.env) {
|
|
16
|
+
return path.join(resolveConfigBaseDir(env), GLOBAL_CONFIG_DIRNAME, GLOBAL_CONFIG_FILENAME);
|
|
17
|
+
}
|
|
18
|
+
export function readGlobalConfig(env = process.env) {
|
|
19
|
+
const configPath = resolveGlobalConfigPath(env);
|
|
20
|
+
if (!pathExistsSync(configPath)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(readTextFileSync(configPath));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new AppError(`Failed to parse global CLI config JSON: ${configPath}`, "config", {
|
|
29
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
33
|
+
throw new AppError(`Global CLI config must be an object: ${configPath}`, "config");
|
|
34
|
+
}
|
|
35
|
+
const schemaVersion = parsed.schemaVersion;
|
|
36
|
+
if (!Number.isInteger(schemaVersion) || Number(schemaVersion) < 1) {
|
|
37
|
+
throw new AppError(`Global CLI config schemaVersion must be a positive integer: ${configPath}`, "config");
|
|
38
|
+
}
|
|
39
|
+
const defaultEnvFile = parsed.defaultEnvFile;
|
|
40
|
+
if (typeof defaultEnvFile !== "string" || defaultEnvFile.trim().length === 0) {
|
|
41
|
+
throw new AppError(`Global CLI config defaultEnvFile must be a non-empty string: ${configPath}`, "config");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
configPath,
|
|
45
|
+
defaultEnvFile: path.resolve(defaultEnvFile),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function writeGlobalConfig(defaultEnvFile, env = process.env) {
|
|
49
|
+
const configPath = resolveGlobalConfigPath(env);
|
|
50
|
+
const normalizedEnvFile = path.resolve(defaultEnvFile);
|
|
51
|
+
const payload = {
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
defaultEnvFile: normalizedEnvFile,
|
|
54
|
+
};
|
|
55
|
+
ensureDirSync(path.dirname(configPath));
|
|
56
|
+
writeTextFileSync(configPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
57
|
+
return {
|
|
58
|
+
configPath,
|
|
59
|
+
defaultEnvFile: normalizedEnvFile,
|
|
60
|
+
};
|
|
61
|
+
}
|