@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.
Files changed (37) hide show
  1. package/README.md +39 -50
  2. package/README.zh-CN.md +39 -50
  3. package/SKILL.md +75 -107
  4. package/assets/templates/achievement.md +8 -8
  5. package/assets/templates/bridge.md +8 -8
  6. package/assets/templates/concept.md +14 -18
  7. package/assets/templates/faq.md +8 -10
  8. package/assets/templates/lesson.md +8 -8
  9. package/assets/templates/method.md +16 -8
  10. package/assets/templates/misconception.md +10 -10
  11. package/assets/templates/person.md +8 -8
  12. package/assets/templates/research-note.md +10 -10
  13. package/assets/templates/resume.md +11 -10
  14. package/assets/templates/source-summary.md +8 -12
  15. package/assets/tiangong-wiki-framework.png +0 -0
  16. package/assets/wiki.config.default.json +6 -3
  17. package/dist/commands/asset.js +21 -0
  18. package/dist/commands/skill.js +78 -0
  19. package/dist/commands/template.js +30 -0
  20. package/dist/core/cli-env.js +34 -5
  21. package/dist/core/global-config.js +61 -0
  22. package/dist/core/onboarding.js +252 -102
  23. package/dist/core/workflow-context.js +58 -21
  24. package/dist/core/workspace-skills.js +496 -60
  25. package/dist/daemon/server.js +8 -0
  26. package/dist/index.js +36 -1
  27. package/dist/operations/asset.js +81 -0
  28. package/dist/operations/query.js +25 -1
  29. package/dist/operations/template-lint.js +160 -0
  30. package/dist/utils/asset.js +75 -0
  31. package/dist/utils/errors.js +6 -0
  32. package/package.json +2 -1
  33. package/references/cli-interface.md +32 -1
  34. package/references/template-design-guide.md +125 -113
  35. package/references/{env.md → troubleshooting.md} +64 -33
  36. package/references/vault-to-wiki-instruction.md +109 -51
  37. 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
- 记录真实使用过它的场景、结果、指标或反馈,支撑 effectiveness 的判断。
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
- 先写出这份 resume 面向谁、主打什么定位,以及希望读者一眼记住什么。
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
- 写出最能支撑当前目标 audience 的技能、能力证据和熟练程度。
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
- 汇总最能增强可信度的 achievement、奖项、认证或公开证明材料。
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
- 交代这份来源是什么、来自哪里、为什么值得被单独消化成 wiki 页面。
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
- 说明这份来源会补强、修正或挑战哪些现有 concept、method lesson 页面。
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.
@@ -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")
@@ -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 candidatePath = requestedPath ?? findNearestEnvFile(cwd);
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 = { ...EMPTY_INFO, requestedPath };
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: false,
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: requestedPath === null,
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
+ }