55 * 1. 12h throttle: skip if last check was less than 12h ago
66 * 2. Download skills/index.json from public-read OSS, compare bailian-docs-llm-wiki entry version
77 * 3. Same version → only refresh lastChecked
8- * 4. Different version → download skills/bailian-docs-llm-wiki/skill.tar.br → brotli decompress +
9- * tar-stream extract (per-entry path safety check) to same-volume temp dir → renameSync atomic swap
10- * → write state + skill-lock.json record (same ledger as bl skill add; list shows installed)
8+ * 4. Different version → delegate to the shared skill install pipeline
9+ * (installSkill: download + extract + SKILL.md validate + atomic swap;
10+ * linkSkillToAgents: fan-out symlinks to detected agents;
11+ * upsertSkillLockEntry: write lock WITH links so bl skill remove can reclaim correctly)
1112 *
1213 * Protocol: unified skill publishing protocol (FC publish-skills, all skills are isomorphic), entry point is
1314 * skills/index.json, one skill.tar.br per skill (brotli q6).
1415 *
15- * Complements postinstall.js (layer 1, unconditional overwrite on npm install). Extraction and atomic swap
16- * reuse skills/extract.ts (same as bl skill installer), symmetric with publisher tar.pack().
16+ * Complements postinstall.js (layer 1, unconditional overwrite on npm install). Install, extraction,
17+ * validation, fan-out and lock writing all reuse the skills/ module (same as bl skill add), symmetric
18+ * with publisher tar.pack().
1719 *
1820 * Failure strategy: any step failure silently returns without updating lastChecked; next recommend retries immediately.
1921 */
20- import { existsSync , mkdirSync , readFileSync , rmSync , writeFileSync } from "node:fs" ;
22+ import { existsSync , readFileSync , writeFileSync } from "node:fs" ;
2123import { join } from "node:path" ;
2224import { getConfigDir } from "../config/paths.ts" ;
23- import { atomicSwap , extractTarBr } from "../skills/extract.ts" ;
25+ import { detectInstalledAgents , linkSkillToAgents } from "../skills/agents.ts" ;
26+ import { installSkill } from "../skills/installer.ts" ;
2427import { readSkillLock , upsertSkillLockEntry } from "../skills/lock.ts" ;
28+ import type { SkillIndexEntry } from "../skills/types.ts" ;
2529
2630/** Public-read OSS skill registry root (hardcoded, does not use env). */
2731const REGISTRY_BASE_URL = "https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/skills" ;
@@ -30,32 +34,20 @@ const SKILL_DIR_NAME = "skills/bailian-docs-llm-wiki";
3034const STATE_FILE_NAME = "wiki-sync-state.json" ;
3135const MODELS_FILE = "models.jsonl" ;
3236const INDEX_KEY = "index.json" ;
33- const ASSET_NAME = "skill.tar.br" ;
3437
3538const THROTTLE_MS = 12 * 60 * 60 * 1000 ; // 12h
3639const INDEX_TIMEOUT_MS = 3000 ;
37- const DOWNLOAD_TIMEOUT_MS = 30000 ;
3840
3941interface SyncState {
4042 lastChecked : number ;
4143 /** Content fingerprint of the last synced revision; the change-detection token */
4244 contentHash : string ;
4345}
4446
45- /** A single skill entry in skills/index.json (unified publishing protocol) */
46- interface IndexSkillEntry {
47- /** Reserved for the skill's own semantic version; not used for change detection */
48- version ?: string ;
49- publishedAt ?: string ;
50- description ?: string ;
51- contentHash ?: string ;
52- compression ?: string ;
53- }
54-
5547interface SkillsIndex {
5648 version : number ;
5749 updatedAt ?: string ;
58- skills : Record < string , IndexSkillEntry > ;
50+ skills : Record < string , SkillIndexEntry > ;
5951}
6052
6153function getCatalogDir ( ) : string {
@@ -93,17 +85,19 @@ function writeState(state: SyncState): void {
9385
9486/**
9587 * Record this sync in skill-lock.json so the wiki skill shares the same ledger as bl skill
96- * (list shows installed instead of untracked; update can manage subsequent upgrades).
88+ * (list shows installed instead of untracked; update/remove can manage it correctly).
89+ * Includes fan-out links so bl skill remove can reclaim agent symlinks.
9790 * Bookkeeping in the silent channel must be best-effort: failure does not affect sync results.
9891 */
99- function recordWikiInLock ( entry : IndexSkillEntry ) : void {
92+ function recordWikiInLock ( entry : SkillIndexEntry , links : string [ ] ) : void {
10093 try {
10194 upsertSkillLockEntry ( WIKI_SKILL_NAME , {
10295 ...( entry . contentHash ? { contentHash : entry . contentHash } : { } ) ,
10396 ...( entry . publishedAt ? { publishedAt : entry . publishedAt } : { } ) ,
10497 installedAt : new Date ( ) . toISOString ( ) ,
10598 sourceType : "oss" ,
10699 ...( entry . description ? { description : entry . description } : { } ) ,
100+ links,
107101 } ) ;
108102 } catch {
109103 /* Bookkeeping failure does not block sync; next sync or bl skill add will fill it in */
@@ -120,7 +114,7 @@ function wikiLockUpToDate(contentHash: string): boolean {
120114}
121115
122116/** Fetch skills/index.json and extract the wiki skill entry; returns null on any failure */
123- async function fetchIndexEntry ( ) : Promise < IndexSkillEntry | null > {
117+ async function fetchIndexEntry ( ) : Promise < SkillIndexEntry | null > {
124118 try {
125119 const res = await fetch ( `${ REGISTRY_BASE_URL } /${ INDEX_KEY } ` , {
126120 signal : AbortSignal . timeout ( INDEX_TIMEOUT_MS ) ,
@@ -134,16 +128,6 @@ async function fetchIndexEntry(): Promise<IndexSkillEntry | null> {
134128 }
135129}
136130
137- async function downloadBuffer ( url : string ) : Promise < Buffer | null > {
138- try {
139- const res = await fetch ( url , { signal : AbortSignal . timeout ( DOWNLOAD_TIMEOUT_MS ) } ) ;
140- if ( ! res . ok ) return null ;
141- return Buffer . from ( await res . arrayBuffer ( ) ) ;
142- } catch {
143- return null ;
144- }
145- }
146-
147131/**
148132 * Check and sync Wiki data. Runs silently; never throws.
149133 * @returns Whether data was actually updated (for testing/debugging)
@@ -172,29 +156,26 @@ export async function maybeSyncWikiData(): Promise<boolean> {
172156 if ( dataOk && ( ! state || state . contentHash === entry . contentHash ) ) {
173157 writeState ( { lastChecked : now , contentHash : entry . contentHash } ) ;
174158 // Data and content are ready but lock record is missing/stale (e.g. postinstall landed before this mechanism) → backfill
175- if ( ! wikiLockUpToDate ( entry . contentHash ) ) recordWikiInLock ( entry ) ;
159+ if ( ! wikiLockUpToDate ( entry . contentHash ) ) recordWikiInLock ( entry , [ ] ) ;
176160 return false ;
177161 }
178162
179- // 4. Different content: download + extract (with entry path safety check) + atomic swap
180- const tarBuf = await downloadBuffer ( `${ REGISTRY_BASE_URL } /${ WIKI_SKILL_NAME } /${ ASSET_NAME } ` ) ;
181- if ( ! tarBuf ) return false ;
182-
183- const catalogDir = getCatalogDir ( ) ;
184- // Same-volume temp dir: extract here then rename; cross-device rename would EXDEV
185- const tmpDir = `${ catalogDir } .tmp-${ process . pid } -${ Date . now ( ) } ` ;
163+ // 4. Different content or missing data: delegate to the shared skill install pipeline
164+ // (download → extract → SKILL.md validate → atomic swap → fan-out → lock with links)
186165 try {
187- mkdirSync ( tmpDir , { recursive : true } ) ;
188- await extractTarBr ( tarBuf , tmpDir ) ;
189- atomicSwap ( tmpDir , catalogDir ) ;
166+ await installSkill ( WIKI_SKILL_NAME , entry ) ;
167+ const agents = detectInstalledAgents ( ) ;
168+ const linkResults = linkSkillToAgents ( WIKI_SKILL_NAME , agents ) ;
169+ const effectiveLinks = linkResults
170+ . filter ( ( link ) => link . mode !== "skipped" )
171+ . map ( ( link ) => link . path ) ;
172+ recordWikiInLock ( entry , effectiveLinks ) ;
190173 } catch {
191- // Extract/swap failed → clean up temp dir, leave existing data untouched, do not write state
192- if ( existsSync ( tmpDir ) ) rmSync ( tmpDir , { recursive : true , force : true } ) ;
174+ // Install failed → clean exit, leave existing data untouched, do not write state; next recommend retries
193175 return false ;
194176 }
195177
196- // 5. Success: write state + skill-lock.json record (unified bl skill ledger)
178+ // 5. Success: write state
197179 writeState ( { lastChecked : now , contentHash : entry . contentHash } ) ;
198- recordWikiInLock ( entry ) ;
199180 return true ;
200181}
0 commit comments