@@ -156,6 +156,15 @@ export const getSyncPlan = async (
156156 filteredSources . map ( async ( source ) => {
157157 const lockEntry = lockData ?. sources ?. [ source . id ] ;
158158 const rulesSha256 = computeRulesSha ( source , defaults ) ;
159+ if ( options . install ) {
160+ return buildInstallResult ( {
161+ source,
162+ lockEntry,
163+ defaults,
164+ resolvedCacheDir,
165+ rulesSha256,
166+ } ) ;
167+ }
159168 if ( options . offline ) {
160169 return buildOfflineResult ( {
161170 source,
@@ -356,6 +365,32 @@ const buildOfflineResult = async (params: {
356365 } ;
357366} ;
358367
368+ const buildInstallResult = async ( params : {
369+ source : DocsCacheResolvedSource ;
370+ lockEntry : DocsCacheLock [ "sources" ] [ string ] | undefined ;
371+ defaults : DocsCacheDefaults ;
372+ resolvedCacheDir : string ;
373+ rulesSha256 : string ;
374+ } ) : Promise < SyncResult > => {
375+ const { source, lockEntry, defaults, resolvedCacheDir, rulesSha256 } = params ;
376+ const docsPresent = await hasDocs ( resolvedCacheDir , source . id ) ;
377+ const resolvedCommit = lockEntry ?. resolvedCommit ?? "missing" ;
378+ const base = buildSyncResultBase ( {
379+ source,
380+ lockEntry,
381+ defaults,
382+ resolvedCommit,
383+ rulesSha256,
384+ } ) ;
385+ if ( ! lockEntry ) {
386+ return { ...base , status : "missing" } ;
387+ }
388+ if ( lockEntry . rulesSha256 !== rulesSha256 ) {
389+ return { ...base , status : "changed" } ;
390+ }
391+ return { ...base , status : docsPresent ? "up-to-date" : "changed" } ;
392+ } ;
393+
359394const buildOnlineResult = async ( params : {
360395 source : DocsCacheResolvedSource ;
361396 lockEntry : DocsCacheLock [ "sources" ] [ string ] | undefined ;
@@ -734,6 +769,55 @@ const reportVerifyFailures = (
734769 }
735770} ;
736771
772+ const assertInstallLock = ( plan : SyncPlan ) => {
773+ if ( ! plan . lockData ) {
774+ throw new Error (
775+ "Install requires docs-lock.json. Run docs-cache sync first." ,
776+ ) ;
777+ }
778+ const missing = plan . sources . filter (
779+ ( source ) => ! plan . lockData ?. sources [ source . id ] ,
780+ ) ;
781+ if ( missing . length > 0 ) {
782+ throw new Error (
783+ `Install failed: lock is missing source(s): ${ missing
784+ . map ( ( source ) => source . id )
785+ . join (
786+ ", " ,
787+ ) } . Run docs-cache update or docs-cache sync to refresh the lock.`,
788+ ) ;
789+ }
790+ const changed = plan . results . filter (
791+ ( result ) => result . lockRulesSha256 !== result . rulesSha256 ,
792+ ) ;
793+ const driftedSources = plan . sources . filter ( ( source ) => {
794+ const lockEntry = plan . lockData ?. sources [ source . id ] ;
795+ return lockEntry ?. repo !== source . repo || lockEntry . ref !== source . ref ;
796+ } ) ;
797+ changed . push (
798+ ...driftedSources
799+ . filter ( ( source ) => ! changed . some ( ( result ) => result . id === source . id ) )
800+ . map ( ( source ) => {
801+ const result = plan . results . find ( ( entry ) => entry . id === source . id ) ;
802+ if ( ! result ) {
803+ throw new Error (
804+ `Install failed: source ${ source . id } is missing from plan.` ,
805+ ) ;
806+ }
807+ return result ;
808+ } ) ,
809+ ) ;
810+ if ( changed . length > 0 ) {
811+ throw new Error (
812+ `Install failed: lock is out of date for source(s): ${ changed
813+ . map ( ( result ) => result . id )
814+ . join (
815+ ", " ,
816+ ) } . Run docs-cache update or docs-cache sync to refresh the lock.`,
817+ ) ;
818+ }
819+ } ;
820+
737821const finalizeSync = async ( params : {
738822 plan : SyncPlan ;
739823 previous : Awaited < ReturnType < typeof readLock > > | null ;
@@ -743,8 +827,15 @@ const finalizeSync = async (params: {
743827 warningCount : number ;
744828} ) => {
745829 const { plan, previous, reporter, options, startTime, warningCount } = params ;
746- const lock = await buildLock ( plan , previous ) ;
747- await writeLock ( plan . lockPath , lock ) ;
830+ const lock = options . install ? previous : await buildLock ( plan , previous ) ;
831+ if ( ! lock ) {
832+ throw new Error (
833+ "Install requires docs-lock.json. Run docs-cache sync first." ,
834+ ) ;
835+ }
836+ if ( ! options . install ) {
837+ await writeLock ( plan . lockPath , lock ) ;
838+ }
748839 const { totalBytes, totalFiles } = summarizePlan ( plan ) ;
749840 if ( reporter ) {
750841 const summary = `${ symbols . info } ${ formatBytes ( totalBytes ) } · ${ totalFiles } files` ;
@@ -806,8 +897,8 @@ const createJobRunner = (params: {
806897
807898 const fetch = await runFetch ( {
808899 sourceId : source . id ,
809- repo : source . repo ,
810- ref : source . ref ,
900+ repo : options . install ? ( lockEntry ?. repo ?? source . repo ) : source . repo ,
901+ ref : options . install ? ( lockEntry ?. ref ?? source . ref ) : source . ref ,
811902 resolvedCommit : result . resolvedCommit ,
812903 cacheDir : plan . cacheDir ,
813904 include : source . include ?? defaults . include ,
@@ -855,6 +946,9 @@ const createJobRunner = (params: {
855946} ;
856947
857948export const runSync = async ( options : SyncOptions , deps : SyncDeps = { } ) => {
949+ if ( options . install && options . lockOnly ) {
950+ throw new Error ( "Install does not support lockOnly." ) ;
951+ }
858952 const startTime = process . hrtime . bigint ( ) ;
859953 let warningCount = 0 ;
860954 const plan = await getSyncPlan ( options , deps ) ;
@@ -865,6 +959,9 @@ export const runSync = async (options: SyncOptions, deps: SyncDeps = {}) => {
865959 ! options . json && ! isSilentMode ( ) && process . stdout . isTTY && ! isTestRunner ;
866960 const reporter = useLiveOutput ? new TaskReporter ( ) : null ;
867961 const previous = plan . lockData ;
962+ if ( options . install ) {
963+ assertInstallLock ( plan ) ;
964+ }
868965 const requiredMissing = plan . results . filter ( ( result ) => {
869966 const source = plan . sources . find ( ( entry ) => entry . id === result . id ) ;
870967 return result . status === "missing" && ( source ?. required ?? true ) ;
0 commit comments