@@ -14,6 +14,7 @@ import {
1414 writeSkillLock ,
1515} from "bailian-cli-core" ;
1616import { emitBare , emitResult , formatTable } from "bailian-cli-runtime" ;
17+ import { planFanoutLinks , summarizeAgents } from "./dry-run-plan.ts" ;
1718
1819interface UpdateOutcome {
1920 name : string ;
@@ -22,6 +23,14 @@ interface UpdateOutcome {
2223 reason ?: string ;
2324}
2425
26+ interface UpdatePlanItem {
27+ name : string ;
28+ status : "update" | "up-to-date" | "skipped" | "failed" ;
29+ publishedAt ?: string ;
30+ reason ?: string ;
31+ links ?: ReturnType < typeof planFanoutLinks > ;
32+ }
33+
2534/** Max number of skills downloading/installing at the same time. */
2635const UPDATE_CONCURRENCY = 3 ;
2736
@@ -61,10 +70,11 @@ export default defineCommand({
6170 const index = await fetchSkillsIndex ( ) ;
6271 const lock = readSkillLock ( ) ;
6372 const disk = new Set ( listSkillDirsOnDisk ( ) ) ;
64-
6573 const agents = detectInstalledAgents ( ) ;
74+
6675 const results : UpdateOutcome [ ] = [ ] ;
6776 const targets : string [ ] = [ ] ;
77+
6878 if ( requested === "all" ) {
6979 // Default: only process skills already installed in lock; reinstall only if version changed or local dir is missing
7080 for ( const [ name , locked ] of Object . entries ( lock . skills ) ) {
@@ -78,6 +88,11 @@ export default defineCommand({
7888 continue ;
7989 }
8090 if ( entry . contentHash === locked . contentHash && disk . has ( name ) ) {
91+ if ( ctx . settings . dryRun ) {
92+ // 真实路径仍会 fan-out 自愈;dry-run 只展示目标路径与存在性,不写盘
93+ results . push ( { name, status : "up-to-date" , publishedAt : locked . publishedAt } ) ;
94+ continue ;
95+ }
8196 // Self-healing: content unchanged, but still fill fan-out links for agents
8297 // detected since the last install (and refresh recorded copies); the merged
8398 // ledger keeps paths of unvisited agents reclaimable by bl skill remove
@@ -103,6 +118,67 @@ export default defineCommand({
103118 }
104119 }
105120
121+ if ( ctx . settings . dryRun ) {
122+ const plan : UpdatePlanItem [ ] = results . map ( ( result ) => {
123+ if ( result . status === "up-to-date" ) {
124+ return {
125+ name : result . name ,
126+ status : "up-to-date" as const ,
127+ publishedAt : result . publishedAt ,
128+ links : planFanoutLinks ( result . name , agents ) ,
129+ } ;
130+ }
131+ if ( result . status === "skipped" ) {
132+ return {
133+ name : result . name ,
134+ status : "skipped" as const ,
135+ publishedAt : result . publishedAt ,
136+ reason : result . reason ,
137+ } ;
138+ }
139+ return {
140+ name : result . name ,
141+ status : "failed" as const ,
142+ publishedAt : result . publishedAt ,
143+ reason : result . reason ,
144+ } ;
145+ } ) ;
146+
147+ for ( const name of targets ) {
148+ const entry = index . skills [ name ] ;
149+ if ( ! entry ) {
150+ plan . push ( { name, status : "failed" , reason : "skill not found in registry" } ) ;
151+ continue ;
152+ }
153+ plan . push ( {
154+ name,
155+ status : "update" ,
156+ publishedAt : entry . publishedAt ,
157+ links : planFanoutLinks ( name , agents ) ,
158+ } ) ;
159+ }
160+
161+ emitResult (
162+ {
163+ action : "skill.update" ,
164+ registry : getSkillRegistryBaseUrl ( ) ,
165+ agents : summarizeAgents ( agents ) ,
166+ skills : plan ,
167+ } ,
168+ format ,
169+ ) ;
170+
171+ const failed = plan . filter ( ( item ) => item . status === "failed" ) ;
172+ if ( failed . length > 0 ) {
173+ throw new BailianError (
174+ `${ failed . length } skill(s) failed to update` ,
175+ ExitCode . GENERAL ,
176+ "Check the reason for failed skills in the output; network failures can be retried with bl skill update" ,
177+ ) ;
178+ }
179+ return ;
180+ }
181+
106182 const tasks = targets . map ( ( name ) => async ( ) : Promise < UpdateOutcome > => {
107183 const entry = index . skills [ name ] ;
108184 if ( ! entry ) {
0 commit comments