@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
@@ -1,9 +1,14 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { spawnSync } from "node:child_process";
2
- import { accessSync, constants, lstatSync, realpathSync, rmSync, symlinkSync, unlinkSync } from "node:fs";
3
+ import { accessSync, constants, lstatSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, symlinkSync, unlinkSync, } from "node:fs";
4
+ import os from "node:os";
3
5
  import path from "node:path";
4
6
  import { AppError } from "../utils/errors.js";
5
- import { ensureDirSync, pathExistsSync } from "../utils/fs.js";
7
+ import { copyDirectoryContentsSync, ensureDirSync, pathExistsSync, readTextFileSync, writeTextFileSync } from "../utils/fs.js";
8
+ import { resolveRuntimePaths } from "./paths.js";
9
+ import { toOffsetIso } from "../utils/time.js";
6
10
  export const PARSER_SKILL_SOURCE = "https://github.com/anthropics/skills";
11
+ export const MANAGED_SKILL_STATE_FILE = ".tiangong-wiki-skill.json";
7
12
  export const OPTIONAL_PARSER_SKILLS = [
8
13
  { name: "pdf", summary: "Process PDF files" },
9
14
  { name: "docx", summary: "Process DOCX files" },
@@ -11,6 +16,11 @@ export const OPTIONAL_PARSER_SKILLS = [
11
16
  { name: "xlsx", summary: "Process XLSX/CSV files" },
12
17
  ];
13
18
  const OPTIONAL_PARSER_SKILL_NAMES = new Set(OPTIONAL_PARSER_SKILLS.map((skill) => skill.name));
19
+ const MANAGED_SKILL_SOURCE_KINDS = new Set([
20
+ "workspace-package",
21
+ "curated-parser",
22
+ "external-source",
23
+ ]);
14
24
  function canRead(filePath) {
15
25
  accessSync(filePath, constants.R_OK);
16
26
  return true;
@@ -105,49 +115,142 @@ export function inspectSkillInstall(skillPath, name = path.basename(skillPath))
105
115
  };
106
116
  }
107
117
  }
108
- export function ensureWikiSkillInstall(wikiPath, packageRoot) {
109
- const paths = resolveWorkspaceSkillPaths(wikiPath);
110
- const packageRealPath = realpathSync(packageRoot);
111
- const existing = inspectSkillInstall(paths.wikiSkillPath, "tiangong-wiki-skill");
112
- ensureDirSync(paths.skillsRoot);
113
- if (existing.exists) {
114
- const stats = lstatSync(paths.wikiSkillPath);
115
- if (stats.isSymbolicLink()) {
116
- const currentRealPath = realpathSync(paths.wikiSkillPath);
117
- if (currentRealPath === packageRealPath) {
118
- return {
119
- sourcePath: packageRoot,
120
- skillPath: paths.wikiSkillPath,
121
- status: "linked",
122
- };
123
- }
124
- unlinkSync(paths.wikiSkillPath);
125
- symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
126
- return {
127
- sourcePath: packageRoot,
128
- skillPath: paths.wikiSkillPath,
129
- status: "updated",
130
- };
118
+ function getManagedSkillStatePath(skillPath) {
119
+ return path.join(skillPath, MANAGED_SKILL_STATE_FILE);
120
+ }
121
+ function readManagedSkillMetadata(skillPath) {
122
+ const metadataPath = getManagedSkillStatePath(skillPath);
123
+ if (!pathExistsSync(metadataPath)) {
124
+ return null;
125
+ }
126
+ try {
127
+ const parsed = JSON.parse(readTextFileSync(metadataPath));
128
+ if (parsed.version !== 1 ||
129
+ typeof parsed.skillName !== "string" ||
130
+ typeof parsed.sourceKind !== "string" ||
131
+ !MANAGED_SKILL_SOURCE_KINDS.has(parsed.sourceKind) ||
132
+ typeof parsed.source !== "string" ||
133
+ typeof parsed.installedAt !== "string" ||
134
+ typeof parsed.baselineHash !== "string") {
135
+ return null;
131
136
  }
132
- if (existing.readable) {
133
- rmSync(paths.wikiSkillPath, { recursive: true, force: true });
134
- symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
135
- return {
136
- sourcePath: packageRoot,
137
- skillPath: paths.wikiSkillPath,
138
- status: "updated",
139
- };
137
+ return {
138
+ version: 1,
139
+ skillName: parsed.skillName,
140
+ sourceKind: parsed.sourceKind,
141
+ source: parsed.source,
142
+ installedAt: parsed.installedAt,
143
+ baselineHash: parsed.baselineHash,
144
+ ...(typeof parsed.command === "string" ? { command: parsed.command } : {}),
145
+ };
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
151
+ function writeManagedSkillMetadata(skillPath, metadata) {
152
+ writeTextFileSync(getManagedSkillStatePath(skillPath), `${JSON.stringify(metadata, null, 2)}\n`);
153
+ }
154
+ function hashSkillDirectory(skillPath) {
155
+ const hash = createHash("sha256");
156
+ const visit = (dirPath, relativePrefix) => {
157
+ const entries = readdirSync(dirPath, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
158
+ for (const entry of entries) {
159
+ if (entry.name === MANAGED_SKILL_STATE_FILE) {
160
+ continue;
161
+ }
162
+ const absolutePath = path.join(dirPath, entry.name);
163
+ const relativePath = path.posix.join(relativePrefix, entry.name);
164
+ const stats = lstatSync(absolutePath);
165
+ if (stats.isSymbolicLink()) {
166
+ hash.update(`symlink:${relativePath}:${realpathSync(absolutePath)}\n`);
167
+ continue;
168
+ }
169
+ if (stats.isDirectory()) {
170
+ hash.update(`dir:${relativePath}\n`);
171
+ visit(absolutePath, relativePath);
172
+ continue;
173
+ }
174
+ hash.update(`file:${relativePath}\n`);
175
+ hash.update(readFileSync(absolutePath));
140
176
  }
141
- throw new AppError(`workspace skill path is occupied and cannot be reused: ${paths.wikiSkillPath}`, "config", {
142
- skillName: "tiangong-wiki-skill",
143
- skillPath: paths.wikiSkillPath,
144
- });
177
+ };
178
+ visit(skillPath, "");
179
+ return hash.digest("hex");
180
+ }
181
+ function getWorkspaceRootForSkillPath(skillPath) {
182
+ return path.dirname(path.dirname(path.dirname(skillPath)));
183
+ }
184
+ function replaceSkillDirectory(targetPath, sourcePath) {
185
+ rmSync(targetPath, { recursive: true, force: true });
186
+ ensureDirSync(targetPath);
187
+ copyDirectoryContentsSync(sourcePath, targetPath);
188
+ }
189
+ function createManagedSkillMetadata(descriptor, baselineHash, command) {
190
+ return {
191
+ version: 1,
192
+ skillName: descriptor.name,
193
+ sourceKind: descriptor.sourceKind,
194
+ source: descriptor.source,
195
+ installedAt: toOffsetIso(),
196
+ baselineHash,
197
+ ...(command ? { command } : {}),
198
+ };
199
+ }
200
+ function ensureParserSkillName(rawName) {
201
+ const normalized = rawName.trim().toLowerCase();
202
+ if (!OPTIONAL_PARSER_SKILL_NAMES.has(normalized)) {
203
+ throw new AppError(`Unsupported parser skill: ${rawName}`, "config");
145
204
  }
146
- symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
205
+ return normalized;
206
+ }
207
+ function normalizeCustomSkillName(rawName) {
208
+ const name = rawName.trim();
209
+ if (!name) {
210
+ throw new AppError("Skill name must not be empty.", "config");
211
+ }
212
+ if (name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
213
+ throw new AppError(`Skill name must be a single directory-friendly token, got ${rawName}`, "config");
214
+ }
215
+ if (name === "tiangong-wiki-skill") {
216
+ throw new AppError("Skill name tiangong-wiki-skill is reserved for the workspace package skill.", "config");
217
+ }
218
+ return name;
219
+ }
220
+ function normalizeManagedSource(rawSource) {
221
+ const source = rawSource.trim();
222
+ if (!source) {
223
+ throw new AppError("Skill source must not be empty.", "config");
224
+ }
225
+ return source;
226
+ }
227
+ function createParserDescriptor(workspaceRoot, name, configured) {
147
228
  return {
148
- sourcePath: packageRoot,
229
+ name,
230
+ sourceKind: "curated-parser",
231
+ configured,
232
+ source: PARSER_SKILL_SOURCE,
233
+ skillPath: resolveWorkspaceSkillPath(workspaceRoot, name),
234
+ };
235
+ }
236
+ function createExternalDescriptor(workspaceRoot, name, source) {
237
+ const normalizedName = normalizeCustomSkillName(name);
238
+ return {
239
+ name: normalizedName,
240
+ sourceKind: "external-source",
241
+ configured: true,
242
+ source: normalizeManagedSource(source),
243
+ skillPath: resolveWorkspaceSkillPath(workspaceRoot, normalizedName),
244
+ };
245
+ }
246
+ function createWikiDescriptor(wikiPath, packageRoot) {
247
+ const paths = resolveWorkspaceSkillPaths(wikiPath);
248
+ return {
249
+ name: "tiangong-wiki-skill",
250
+ sourceKind: "workspace-package",
251
+ configured: true,
252
+ source: packageRoot,
149
253
  skillPath: paths.wikiSkillPath,
150
- status: "linked",
151
254
  };
152
255
  }
153
256
  function renderCommand(command, args) {
@@ -155,44 +258,50 @@ function renderCommand(command, args) {
155
258
  .map((part) => (/[A-Za-z0-9_./:@+-]+/.test(part) ? part : JSON.stringify(part)))
156
259
  .join(" ");
157
260
  }
158
- export function buildParserSkillInstallInvocation(skillName) {
261
+ export function buildExternalSkillInstallInvocation(source, skillName) {
159
262
  const command = getNpxCommand();
160
- const args = ["-y", "skills", "add", PARSER_SKILL_SOURCE, "--skill", skillName, "-a", "codex", "-y"];
263
+ const args = ["-y", "skills", "add", source, "--skill", skillName, "-a", "codex", "-y"];
161
264
  return {
162
265
  command,
163
266
  args,
164
267
  rendered: renderCommand(command, args),
165
268
  };
166
269
  }
167
- export function installParserSkill(skillName, workspaceRoot, options = {}) {
168
- const skillPath = resolveWorkspaceSkillPath(workspaceRoot, skillName);
169
- const current = inspectSkillInstall(skillPath, skillName);
170
- const invocation = buildParserSkillInstallInvocation(skillName);
171
- if (current.readable) {
270
+ function installManagedExternalSkill(descriptor, options = {}) {
271
+ if (descriptor.sourceKind === "workspace-package") {
272
+ throw new AppError("Workspace package skills must be installed via ensureWikiSkillInstall.", "config");
273
+ }
274
+ const current = inspectSkillInstall(descriptor.skillPath, descriptor.name);
275
+ const invocation = buildExternalSkillInstallInvocation(descriptor.source, descriptor.name);
276
+ if (current.readable && options.skipIfExists !== false) {
172
277
  return {
173
- name: skillName,
174
- skillPath,
278
+ name: descriptor.name,
279
+ source: descriptor.source,
280
+ skillPath: descriptor.skillPath,
175
281
  skillMdPath: current.skillMdPath,
176
282
  status: "existing",
177
283
  command: invocation.rendered,
178
284
  };
179
285
  }
180
- options.output?.write(`Installing parser skill ${skillName}...\n`);
286
+ const workspaceRoot = getWorkspaceRootForSkillPath(descriptor.skillPath);
287
+ options.output?.write(`Installing skill ${descriptor.name} from ${descriptor.source}...\n`);
181
288
  const result = spawnSync(invocation.command, invocation.args, {
182
289
  cwd: workspaceRoot,
183
290
  env: options.env ?? process.env,
184
291
  encoding: "utf8",
185
292
  });
186
293
  if (result.error) {
187
- throw new AppError(`failed to install parser skill ${skillName}: ${result.error.message}`, "runtime", {
188
- skillName,
294
+ throw new AppError(`failed to install skill ${descriptor.name}: ${result.error.message}`, "runtime", {
295
+ skillName: descriptor.name,
296
+ source: descriptor.source,
189
297
  command: invocation.rendered,
190
298
  cwd: workspaceRoot,
191
299
  });
192
300
  }
193
301
  if (result.status !== 0) {
194
- throw new AppError(`failed to install parser skill ${skillName}`, "runtime", {
195
- skillName,
302
+ throw new AppError(`failed to install skill ${descriptor.name}`, "runtime", {
303
+ skillName: descriptor.name,
304
+ source: descriptor.source,
196
305
  command: invocation.rendered,
197
306
  cwd: workspaceRoot,
198
307
  exitCode: result.status,
@@ -200,21 +309,348 @@ export function installParserSkill(skillName, workspaceRoot, options = {}) {
200
309
  stderr: result.stderr.trim(),
201
310
  });
202
311
  }
203
- const installed = inspectSkillInstall(skillPath, skillName);
312
+ const installed = inspectSkillInstall(descriptor.skillPath, descriptor.name);
204
313
  if (!installed.readable) {
205
- throw new AppError(`parser skill ${skillName} was installed but SKILL.md is missing or unreadable`, "runtime", {
206
- skillName,
314
+ throw new AppError(`skill ${descriptor.name} was installed but SKILL.md is missing or unreadable`, "runtime", {
315
+ skillName: descriptor.name,
316
+ source: descriptor.source,
207
317
  command: invocation.rendered,
208
318
  cwd: workspaceRoot,
209
- skillPath,
319
+ skillPath: descriptor.skillPath,
210
320
  skillMdPath: installed.skillMdPath,
211
321
  });
212
322
  }
323
+ if (options.trackMetadata !== false) {
324
+ writeManagedSkillMetadata(descriptor.skillPath, createManagedSkillMetadata(descriptor, hashSkillDirectory(descriptor.skillPath), invocation.rendered));
325
+ }
213
326
  return {
214
- name: skillName,
215
- skillPath,
327
+ name: descriptor.name,
328
+ source: descriptor.source,
329
+ skillPath: descriptor.skillPath,
216
330
  skillMdPath: installed.skillMdPath,
217
331
  status: "installed",
218
332
  command: invocation.rendered,
219
333
  };
220
334
  }
335
+ function installManagedExternalSkillIntoTempWorkspace(descriptor, options = {}) {
336
+ const tempRoot = mkdtempSync(path.join(os.tmpdir(), "tiangong-wiki-skill-update-"));
337
+ try {
338
+ const result = installManagedExternalSkill({
339
+ ...descriptor,
340
+ skillPath: resolveWorkspaceSkillPath(tempRoot, descriptor.name),
341
+ }, {
342
+ env: options.env,
343
+ trackMetadata: false,
344
+ });
345
+ return {
346
+ hash: hashSkillDirectory(resolveWorkspaceSkillPath(tempRoot, descriptor.name)),
347
+ tempRoot,
348
+ invocation: result.command,
349
+ };
350
+ }
351
+ catch (error) {
352
+ rmSync(tempRoot, { recursive: true, force: true });
353
+ throw error;
354
+ }
355
+ }
356
+ function hasCompatibleManagedMetadata(metadata, descriptor) {
357
+ return metadata !== null &&
358
+ metadata.skillName === descriptor.name &&
359
+ metadata.sourceKind === descriptor.sourceKind &&
360
+ metadata.source === descriptor.source;
361
+ }
362
+ function readManagedSkillDescriptorsFromMetadata(workspaceRoot, options = {}) {
363
+ const skillsRoot = path.join(workspaceRoot, ".agents", "skills");
364
+ if (!pathExistsSync(skillsRoot)) {
365
+ return [];
366
+ }
367
+ const includeKinds = options.includeSourceKinds ? new Set(options.includeSourceKinds) : null;
368
+ const descriptors = [];
369
+ const entries = readdirSync(skillsRoot, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
370
+ for (const entry of entries) {
371
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) {
372
+ continue;
373
+ }
374
+ const skillPath = path.join(skillsRoot, entry.name);
375
+ const metadata = readManagedSkillMetadata(skillPath);
376
+ if (!metadata || metadata.skillName !== entry.name) {
377
+ continue;
378
+ }
379
+ if (includeKinds && !includeKinds.has(metadata.sourceKind)) {
380
+ continue;
381
+ }
382
+ descriptors.push({
383
+ name: metadata.skillName,
384
+ sourceKind: metadata.sourceKind,
385
+ configured: true,
386
+ source: metadata.source,
387
+ skillPath,
388
+ });
389
+ }
390
+ return descriptors;
391
+ }
392
+ function createManagedSkillCatalog(env = process.env) {
393
+ const paths = resolveRuntimePaths(env);
394
+ const workspace = resolveWorkspaceSkillPaths(paths.wikiPath);
395
+ const configuredParserSkills = parseParserSkills(env.WIKI_PARSER_SKILLS, { strict: false });
396
+ return {
397
+ wikiDescriptor: createWikiDescriptor(paths.wikiPath, paths.packageRoot),
398
+ configuredParserDescriptors: configuredParserSkills.map((name) => createParserDescriptor(workspace.workspaceRoot, name, true)),
399
+ discoveredDescriptors: readManagedSkillDescriptorsFromMetadata(workspace.workspaceRoot),
400
+ };
401
+ }
402
+ function resolveManagedSkillDescriptorByName(env, name) {
403
+ const catalog = createManagedSkillCatalog(env);
404
+ if (name === "tiangong-wiki-skill") {
405
+ return catalog.wikiDescriptor;
406
+ }
407
+ const discovered = catalog.discoveredDescriptors.find((descriptor) => descriptor.name === name);
408
+ if (discovered) {
409
+ return discovered;
410
+ }
411
+ if (OPTIONAL_PARSER_SKILL_NAMES.has(name)) {
412
+ return createParserDescriptor(getWorkspaceRootForSkillPath(catalog.wikiDescriptor.skillPath), name, catalog.configuredParserDescriptors.some((descriptor) => descriptor.name === name));
413
+ }
414
+ throw new AppError(`Unknown managed skill: ${name}`, "config");
415
+ }
416
+ function resolveManagedSkillDescriptors(env = process.env, names) {
417
+ if (names && names.length > 0) {
418
+ return names.map((name) => resolveManagedSkillDescriptorByName(env, name));
419
+ }
420
+ const catalog = createManagedSkillCatalog(env);
421
+ const descriptors = new Map();
422
+ for (const descriptor of catalog.discoveredDescriptors) {
423
+ if (descriptor.sourceKind === "external-source") {
424
+ descriptors.set(descriptor.name, descriptor);
425
+ }
426
+ }
427
+ descriptors.set(catalog.wikiDescriptor.name, catalog.wikiDescriptor);
428
+ for (const descriptor of catalog.configuredParserDescriptors) {
429
+ if (!descriptors.has(descriptor.name)) {
430
+ descriptors.set(descriptor.name, descriptor);
431
+ }
432
+ }
433
+ return [...descriptors.values()];
434
+ }
435
+ function getWikiSkillStatus(descriptor) {
436
+ const current = inspectSkillInstall(descriptor.skillPath, descriptor.name);
437
+ if (!current.readable) {
438
+ return {
439
+ ...descriptor,
440
+ state: "missing",
441
+ tracked: false,
442
+ message: "Skill is missing or unreadable.",
443
+ };
444
+ }
445
+ const stats = lstatSync(descriptor.skillPath);
446
+ if (!stats.isSymbolicLink()) {
447
+ return {
448
+ ...descriptor,
449
+ state: "conflict",
450
+ tracked: false,
451
+ message: "Workspace skill path is a local directory, not the managed symlink target.",
452
+ };
453
+ }
454
+ if (realpathSync(descriptor.skillPath) !== realpathSync(descriptor.source)) {
455
+ return {
456
+ ...descriptor,
457
+ state: "update_available",
458
+ tracked: true,
459
+ message: "Symlink points to an older or different package root.",
460
+ };
461
+ }
462
+ return {
463
+ ...descriptor,
464
+ state: "up_to_date",
465
+ tracked: true,
466
+ message: "Symlink points to the current package root.",
467
+ };
468
+ }
469
+ function getExternalManagedSkillStatus(descriptor, env = process.env) {
470
+ const current = inspectSkillInstall(descriptor.skillPath, descriptor.name);
471
+ if (!current.readable) {
472
+ return {
473
+ ...descriptor,
474
+ state: "missing",
475
+ tracked: false,
476
+ message: "Skill is missing or unreadable.",
477
+ };
478
+ }
479
+ const currentHash = hashSkillDirectory(descriptor.skillPath);
480
+ const metadata = readManagedSkillMetadata(descriptor.skillPath);
481
+ const latest = installManagedExternalSkillIntoTempWorkspace(descriptor, { env });
482
+ try {
483
+ if (!hasCompatibleManagedMetadata(metadata, descriptor)) {
484
+ if (currentHash === latest.hash) {
485
+ return {
486
+ ...descriptor,
487
+ state: "up_to_date",
488
+ tracked: false,
489
+ message: "Installed skill matches the latest source snapshot, but no compatible managed baseline metadata exists yet.",
490
+ };
491
+ }
492
+ return {
493
+ ...descriptor,
494
+ state: "conflict",
495
+ tracked: false,
496
+ message: "Installed skill differs from the latest source snapshot, but no compatible managed baseline metadata exists to separate local edits from source changes.",
497
+ };
498
+ }
499
+ if (currentHash === latest.hash) {
500
+ return {
501
+ ...descriptor,
502
+ state: "up_to_date",
503
+ tracked: true,
504
+ message: "Installed skill matches the latest source snapshot.",
505
+ };
506
+ }
507
+ if (currentHash === metadata.baselineHash) {
508
+ return {
509
+ ...descriptor,
510
+ state: "update_available",
511
+ tracked: true,
512
+ message: "A newer source snapshot is available and local files are unchanged.",
513
+ };
514
+ }
515
+ return {
516
+ ...descriptor,
517
+ state: "conflict",
518
+ tracked: true,
519
+ message: metadata.baselineHash === latest.hash
520
+ ? "Local files differ from the managed baseline."
521
+ : "Local files and source snapshot both differ from the managed baseline.",
522
+ };
523
+ }
524
+ finally {
525
+ rmSync(latest.tempRoot, { recursive: true, force: true });
526
+ }
527
+ }
528
+ export function getManagedSkillStatus(env = process.env, names) {
529
+ return resolveManagedSkillDescriptors(env, names)
530
+ .map((descriptor) => descriptor.sourceKind === "workspace-package" ? getWikiSkillStatus(descriptor) : getExternalManagedSkillStatus(descriptor, env))
531
+ .sort((left, right) => left.name.localeCompare(right.name));
532
+ }
533
+ function updateWikiManagedSkill(descriptor, env = process.env, options = {}) {
534
+ const status = getWikiSkillStatus(descriptor);
535
+ if (status.state === "conflict" && !options.force) {
536
+ return {
537
+ ...status,
538
+ action: "skipped",
539
+ message: `${status.message} Re-run with --force to replace it with the managed symlink.`,
540
+ };
541
+ }
542
+ const paths = resolveRuntimePaths(env);
543
+ const installed = ensureWikiSkillInstall(paths.wikiPath, paths.packageRoot);
544
+ const nextStatus = getWikiSkillStatus(descriptor);
545
+ return {
546
+ ...nextStatus,
547
+ action: status.state === "missing" ? "installed" : installed.status === "linked" ? "unchanged" : "updated",
548
+ };
549
+ }
550
+ function updateExternalManagedSkill(descriptor, env = process.env, options = {}) {
551
+ const currentStatus = getExternalManagedSkillStatus(descriptor, env);
552
+ if (currentStatus.state === "missing") {
553
+ installManagedExternalSkill(descriptor, { env });
554
+ return {
555
+ ...getExternalManagedSkillStatus(descriptor, env),
556
+ action: "installed",
557
+ };
558
+ }
559
+ if (currentStatus.state === "conflict" && !options.force) {
560
+ return {
561
+ ...currentStatus,
562
+ action: "skipped",
563
+ message: `${currentStatus.message} Refusing to overwrite local changes without --force.`,
564
+ };
565
+ }
566
+ const latest = installManagedExternalSkillIntoTempWorkspace(descriptor, { env });
567
+ try {
568
+ const latestSkillPath = resolveWorkspaceSkillPath(latest.tempRoot, descriptor.name);
569
+ const currentHash = hashSkillDirectory(descriptor.skillPath);
570
+ const action = currentHash === latest.hash ? "unchanged" : "updated";
571
+ if (action === "updated") {
572
+ replaceSkillDirectory(descriptor.skillPath, latestSkillPath);
573
+ }
574
+ writeManagedSkillMetadata(descriptor.skillPath, createManagedSkillMetadata(descriptor, latest.hash, latest.invocation));
575
+ return {
576
+ ...getExternalManagedSkillStatus(descriptor, env),
577
+ action,
578
+ };
579
+ }
580
+ finally {
581
+ rmSync(latest.tempRoot, { recursive: true, force: true });
582
+ }
583
+ }
584
+ export function updateManagedSkills(env = process.env, names, options = {}) {
585
+ return resolveManagedSkillDescriptors(env, names)
586
+ .map((descriptor) => descriptor.sourceKind === "workspace-package"
587
+ ? updateWikiManagedSkill(descriptor, env, options)
588
+ : updateExternalManagedSkill(descriptor, env, options))
589
+ .sort((left, right) => left.name.localeCompare(right.name));
590
+ }
591
+ export function addManagedSkill(env = process.env, source, skillName, options = {}) {
592
+ const paths = resolveRuntimePaths(env);
593
+ const workspace = resolveWorkspaceSkillPaths(paths.wikiPath);
594
+ return updateExternalManagedSkill(createExternalDescriptor(workspace.workspaceRoot, skillName, source), env, options);
595
+ }
596
+ export function ensureWikiSkillInstall(wikiPath, packageRoot) {
597
+ const paths = resolveWorkspaceSkillPaths(wikiPath);
598
+ const packageRealPath = realpathSync(packageRoot);
599
+ const existing = inspectSkillInstall(paths.wikiSkillPath, "tiangong-wiki-skill");
600
+ ensureDirSync(paths.skillsRoot);
601
+ if (existing.exists) {
602
+ const stats = lstatSync(paths.wikiSkillPath);
603
+ if (stats.isSymbolicLink()) {
604
+ const currentRealPath = realpathSync(paths.wikiSkillPath);
605
+ if (currentRealPath === packageRealPath) {
606
+ return {
607
+ sourcePath: packageRoot,
608
+ skillPath: paths.wikiSkillPath,
609
+ status: "linked",
610
+ };
611
+ }
612
+ unlinkSync(paths.wikiSkillPath);
613
+ symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
614
+ return {
615
+ sourcePath: packageRoot,
616
+ skillPath: paths.wikiSkillPath,
617
+ status: "updated",
618
+ };
619
+ }
620
+ if (existing.readable) {
621
+ rmSync(paths.wikiSkillPath, { recursive: true, force: true });
622
+ symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
623
+ return {
624
+ sourcePath: packageRoot,
625
+ skillPath: paths.wikiSkillPath,
626
+ status: "updated",
627
+ };
628
+ }
629
+ throw new AppError(`workspace skill path is occupied and cannot be reused: ${paths.wikiSkillPath}`, "config", {
630
+ skillName: "tiangong-wiki-skill",
631
+ skillPath: paths.wikiSkillPath,
632
+ });
633
+ }
634
+ symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
635
+ return {
636
+ sourcePath: packageRoot,
637
+ skillPath: paths.wikiSkillPath,
638
+ status: "linked",
639
+ };
640
+ }
641
+ export function buildParserSkillInstallInvocation(skillName) {
642
+ return buildExternalSkillInstallInvocation(PARSER_SKILL_SOURCE, skillName);
643
+ }
644
+ export function installParserSkill(skillName, workspaceRoot, options = {}) {
645
+ const installed = installManagedExternalSkill(createParserDescriptor(workspaceRoot, skillName, true), {
646
+ env: options.env,
647
+ output: options.output,
648
+ });
649
+ return {
650
+ name: skillName,
651
+ skillPath: installed.skillPath,
652
+ skillMdPath: installed.skillMdPath,
653
+ status: installed.status,
654
+ command: installed.command,
655
+ };
656
+ }
@@ -7,6 +7,7 @@ import { exportGraphContent, exportIndexContent } from "../operations/export.js"
7
7
  import { getDashboardGraphOverview, getDashboardLintSummary, getDashboardPageDetail, getDashboardPageSource, getDashboardQueueItemDetail, getDashboardQueueSummary, getDashboardStatus, getDashboardVaultFileDetail, getDashboardVaultSummary, listDashboardLintIssues, listDashboardQueueItems, listDashboardVaultFiles, openDashboardPageSource, openDashboardVaultFile, retryDashboardQueueItem, searchDashboardGraph, } from "../operations/dashboard.js";
8
8
  import { diffVaultFiles, findPages, ftsSearchPages, getPageInfo, getVaultQueue, getWikiStat, listPages, listVaultFiles, renderLintResult, runLint, searchPages, traverseGraph, } from "../operations/query.js";
9
9
  import { createTemplate, listTemplates, listTypes, recommendTypes, showTemplate, showType, } from "../operations/type-template.js";
10
+ import { runTemplateLint } from "../operations/template-lint.js";
10
11
  import { createPage, runSync, runSyncCommand } from "../operations/write.js";
11
12
  import { AppError, asAppError } from "../utils/errors.js";
12
13
  import { pathExistsSync } from "../utils/fs.js";
@@ -726,6 +727,13 @@ export async function runDaemonServer(options) {
726
727
  writeJsonResponse(response, 200, showTemplate(env, pageType));
727
728
  return;
728
729
  }
730
+ if (method === "GET" && pathname === "/template/lint") {
731
+ writeJsonResponse(response, 200, runTemplateLint(env, {
732
+ pageType: url.searchParams.get("pageType") ?? undefined,
733
+ level: url.searchParams.get("level") ?? undefined,
734
+ }));
735
+ return;
736
+ }
729
737
  if (method === "POST" && pathname === "/template/create") {
730
738
  const body = await readJsonBody(request);
731
739
  const result = await runWriteTask("template-create", () => Promise.resolve(createTemplate(env, {