@@ -16,17 +16,27 @@ import {
1616 mergeOverrides ,
1717 stripOverrideKeys ,
1818} from './mcp-secrets.js' ;
19- import type { SyncItem , SyncPlan } from './paths.js' ;
19+ import type { ExtraPathPlan , SyncItem , SyncPlan } from './paths.js' ;
2020import { normalizePath } from './paths.js' ;
2121
22- interface ExtraSecretManifestEntry {
22+ type ExtraPathType = 'file' | 'dir' ;
23+
24+ interface ExtraPathManifestItem {
25+ relativePath : string ;
26+ type : ExtraPathType ;
27+ mode ?: number ;
28+ }
29+
30+ interface ExtraPathManifestEntry {
2331 sourcePath : string ;
2432 repoPath : string ;
33+ type ?: ExtraPathType ;
2534 mode ?: number ;
35+ items ?: ExtraPathManifestItem [ ] ;
2636}
2737
28- interface ExtraSecretManifest {
29- entries : ExtraSecretManifestEntry [ ] ;
38+ interface ExtraPathManifest {
39+ entries : ExtraPathManifestEntry [ ] ;
3040}
3141
3242export async function syncRepoToLocal (
@@ -37,7 +47,8 @@ export async function syncRepoToLocal(
3747 await copyItem ( item . repoPath , item . localPath , item . type ) ;
3848 }
3949
40- await applyExtraSecrets ( plan , true ) ;
50+ await applyExtraPaths ( plan , plan . extraConfigs ) ;
51+ await applyExtraPaths ( plan , plan . extraSecrets ) ;
4152
4253 if ( overrides && Object . keys ( overrides ) . length > 0 ) {
4354 await applyOverridesToLocalConfig ( plan , overrides ) ;
@@ -90,7 +101,8 @@ export async function syncLocalToRepo(
90101 await copyItem ( item . localPath , item . repoPath , item . type , true ) ;
91102 }
92103
93- await writeExtraSecretsManifest ( plan ) ;
104+ await writeExtraPathManifest ( plan , plan . extraConfigs ) ;
105+ await writeExtraPathManifest ( plan , plan . extraSecrets ) ;
94106}
95107
96108async function copyItem (
@@ -212,14 +224,14 @@ async function removePath(targetPath: string): Promise<void> {
212224 await fs . rm ( targetPath , { recursive : true , force : true } ) ;
213225}
214226
215- async function applyExtraSecrets ( plan : SyncPlan , fromRepo : boolean ) : Promise < void > {
216- const allowlist = plan . extraSecrets . allowlist ;
227+ async function applyExtraPaths ( plan : SyncPlan , extra : ExtraPathPlan ) : Promise < void > {
228+ const allowlist = extra . allowlist ;
217229 if ( allowlist . length === 0 ) return ;
218230
219- if ( ! ( await pathExists ( plan . extraSecrets . manifestPath ) ) ) return ;
231+ if ( ! ( await pathExists ( extra . manifestPath ) ) ) return ;
220232
221- const manifestContent = await fs . readFile ( plan . extraSecrets . manifestPath , 'utf8' ) ;
222- const manifest = parseJsonc < ExtraSecretManifest > ( manifestContent ) ;
233+ const manifestContent = await fs . readFile ( extra . manifestPath , 'utf8' ) ;
234+ const manifest = parseJsonc < ExtraPathManifest > ( manifestContent ) ;
223235
224236 for ( const entry of manifest . entries ) {
225237 const normalized = normalizePath ( entry . sourcePath , plan . homeDir , plan . platform ) ;
@@ -230,47 +242,136 @@ async function applyExtraSecrets(plan: SyncPlan, fromRepo: boolean): Promise<voi
230242 ? entry . repoPath
231243 : path . join ( plan . repoRoot , entry . repoPath ) ;
232244 const localPath = entry . sourcePath ;
245+ const entryType : ExtraPathType = entry . type ?? 'file' ;
233246
234247 if ( ! ( await pathExists ( repoPath ) ) ) continue ;
235248
236- if ( fromRepo ) {
237- await copyFileWithMode ( repoPath , localPath ) ;
238- if ( entry . mode !== undefined ) {
239- await chmodIfExists ( localPath , entry . mode ) ;
240- }
241- }
249+ await copyItem ( repoPath , localPath , entryType ) ;
250+ await applyExtraPathModes ( localPath , entry ) ;
242251 }
243252}
244253
245- async function writeExtraSecretsManifest ( plan : SyncPlan ) : Promise < void > {
246- const allowlist = plan . extraSecrets . allowlist ;
247- const extraDir = path . join ( path . dirname ( plan . extraSecrets . manifestPath ) , 'extra' ) ;
254+ async function writeExtraPathManifest ( plan : SyncPlan , extra : ExtraPathPlan ) : Promise < void > {
255+ const allowlist = extra . allowlist ;
256+ const extraDir = path . join ( path . dirname ( extra . manifestPath ) , 'extra' ) ;
248257 if ( allowlist . length === 0 ) {
249- await removePath ( plan . extraSecrets . manifestPath ) ;
258+ await removePath ( extra . manifestPath ) ;
250259 await removePath ( extraDir ) ;
251260 return ;
252261 }
253262
254263 await removePath ( extraDir ) ;
255264
256- const entries : ExtraSecretManifestEntry [ ] = [ ] ;
265+ const entries : ExtraPathManifestEntry [ ] = [ ] ;
257266
258- for ( const entry of plan . extraSecrets . entries ) {
267+ for ( const entry of extra . entries ) {
259268 const sourcePath = entry . sourcePath ;
260269 if ( ! ( await pathExists ( sourcePath ) ) ) {
261270 continue ;
262271 }
263272 const stat = await fs . stat ( sourcePath ) ;
264- await copyFileWithMode ( sourcePath , entry . repoPath ) ;
265- entries . push ( {
266- sourcePath,
267- repoPath : path . relative ( plan . repoRoot , entry . repoPath ) ,
268- mode : stat . mode & 0o777 ,
269- } ) ;
273+ if ( stat . isDirectory ( ) ) {
274+ await copyDirRecursive ( sourcePath , entry . repoPath ) ;
275+ const items = await collectExtraPathItems ( sourcePath , sourcePath ) ;
276+ entries . push ( {
277+ sourcePath,
278+ repoPath : path . relative ( plan . repoRoot , entry . repoPath ) ,
279+ type : 'dir' ,
280+ mode : stat . mode & 0o777 ,
281+ items,
282+ } ) ;
283+ continue ;
284+ }
285+ if ( stat . isFile ( ) ) {
286+ await copyFileWithMode ( sourcePath , entry . repoPath ) ;
287+ entries . push ( {
288+ sourcePath,
289+ repoPath : path . relative ( plan . repoRoot , entry . repoPath ) ,
290+ type : 'file' ,
291+ mode : stat . mode & 0o777 ,
292+ } ) ;
293+ }
294+ }
295+
296+ await fs . mkdir ( path . dirname ( extra . manifestPath ) , { recursive : true } ) ;
297+ await writeJsonFile ( extra . manifestPath , { entries } , { jsonc : false } ) ;
298+ }
299+
300+ async function collectExtraPathItems (
301+ sourcePath : string ,
302+ basePath : string
303+ ) : Promise < ExtraPathManifestItem [ ] > {
304+ const items : ExtraPathManifestItem [ ] = [ ] ;
305+ const entries = await fs . readdir ( sourcePath , { withFileTypes : true } ) ;
306+
307+ for ( const entry of entries ) {
308+ const entrySource = path . join ( sourcePath , entry . name ) ;
309+ const relativePath = path . relative ( basePath , entrySource ) ;
310+
311+ if ( entry . isDirectory ( ) ) {
312+ const stat = await fs . stat ( entrySource ) ;
313+ items . push ( {
314+ relativePath,
315+ type : 'dir' ,
316+ mode : stat . mode & 0o777 ,
317+ } ) ;
318+ const nested = await collectExtraPathItems ( entrySource , basePath ) ;
319+ items . push ( ...nested ) ;
320+ continue ;
321+ }
322+
323+ if ( entry . isFile ( ) ) {
324+ const stat = await fs . stat ( entrySource ) ;
325+ items . push ( {
326+ relativePath,
327+ type : 'file' ,
328+ mode : stat . mode & 0o777 ,
329+ } ) ;
330+ }
331+ }
332+
333+ return items ;
334+ }
335+
336+ async function applyExtraPathModes (
337+ targetPath : string ,
338+ entry : ExtraPathManifestEntry
339+ ) : Promise < void > {
340+ if ( entry . mode !== undefined ) {
341+ await chmodIfExists ( targetPath , entry . mode ) ;
342+ }
343+
344+ if ( entry . type !== 'dir' ) {
345+ return ;
346+ }
347+
348+ if ( ! entry . items || entry . items . length === 0 ) {
349+ return ;
350+ }
351+
352+ for ( const item of entry . items ) {
353+ if ( item . mode === undefined ) continue ;
354+ const itemPath = resolveExtraPathItem ( targetPath , item . relativePath ) ;
355+ if ( ! itemPath ) continue ;
356+ await chmodIfExists ( itemPath , item . mode ) ;
357+ }
358+ }
359+
360+ function resolveExtraPathItem ( basePath : string , relativePath : string ) : string | null {
361+ if ( ! relativePath ) return null ;
362+ if ( path . isAbsolute ( relativePath ) ) return null ;
363+
364+ const resolvedBase = path . resolve ( basePath ) ;
365+ const resolvedPath = path . resolve ( basePath , relativePath ) ;
366+ const relative = path . relative ( resolvedBase , resolvedPath ) ;
367+ if ( relative === '..' || relative . startsWith ( `..${ path . sep } ` ) ) {
368+ return null ;
369+ }
370+ if ( path . isAbsolute ( relative ) ) {
371+ return null ;
270372 }
271373
272- await fs . mkdir ( path . dirname ( plan . extraSecrets . manifestPath ) , { recursive : true } ) ;
273- await writeJsonFile ( plan . extraSecrets . manifestPath , { entries } , { jsonc : false } ) ;
374+ return resolvedPath ;
274375}
275376
276377function isDeepEqual ( left : unknown , right : unknown ) : boolean {
0 commit comments