@@ -28,6 +28,12 @@ Usage:
2828 stackfoundry add <registry-item-url-or-file> [--target <dir>] [--dry-run] [--force]
2929 stackfoundry diff <module> [--target <dir>]
3030 stackfoundry diff <registry-item-url-or-file> [--target <dir>]
31+
32+ Examples:
33+ stackfoundry add api-keys --target ./app
34+ stackfoundry add preset vendor-examples --target ./app
35+ stackfoundry add https://stackfoundry.dev/r/api-keys.json --target ./app
36+ stackfoundry add public/r/vendor-examples.json --target ./app
3137` ) ;
3238}
3339
@@ -443,6 +449,62 @@ async function addPreset(name, flags) {
443449 }
444450}
445451
452+ async function addRegistryItem ( specifier , flags , visited = new Set ( ) ) {
453+ const item = await readRegistryItem ( specifier ) ;
454+ const itemId = isHttpUrl ( specifier ) ? specifier : path . resolve ( specifier ) ;
455+
456+ if ( visited . has ( itemId ) ) return ;
457+ visited . add ( itemId ) ;
458+
459+ if ( item . type !== "registry:block" ) {
460+ throw new Error ( `${ item . name ?? specifier } : expected registry:block` ) ;
461+ }
462+
463+ for ( const dependency of item . registryDependencies ?? [ ] ) {
464+ await addRegistryItem ( dependency , flags , visited ) ;
465+ }
466+
467+ const installed = await loadInstallManifest ( flags . target ) ;
468+ const installedFiles = { } ;
469+ console . log ( `${ flags . dryRun ? "would install" : "installing" } registry item ${ item . name } ` ) ;
470+
471+ for ( const file of item . files ?? [ ] ) {
472+ if ( ! file . content ) throw new Error ( `${ item . name } : ${ file . path } is missing embedded content` ) ;
473+
474+ const relative = file . target ?? file . path ;
475+ const sourceHash = createHash ( "sha256" ) . update ( file . content ) . digest ( "hex" ) ;
476+ await writeFileWithSafety ( {
477+ sourceHash,
478+ content : file . content ,
479+ relative,
480+ target : flags . target ,
481+ flags,
482+ } ) ;
483+ installedFiles [ relative ] = sourceHash ;
484+ }
485+
486+ const envVars = Object . keys ( item . envVars ?? { } ) ;
487+ if ( envVars . length > 0 ) {
488+ const relative = `.env.stackfoundry.${ item . name } .example` ;
489+ const content = envVars . map ( ( key ) => `${ key } =` ) . join ( "\n" ) + "\n" ;
490+ const sourceHash = createHash ( "sha256" ) . update ( content ) . digest ( "hex" ) ;
491+ await writeFileWithSafety ( { sourceHash, content, relative, target : flags . target , flags } ) ;
492+ installedFiles [ relative ] = sourceHash ;
493+ }
494+
495+ if ( ! flags . dryRun ) {
496+ installed . modules [ item . name ] = {
497+ installedAt : new Date ( ) . toISOString ( ) ,
498+ files : installedFiles ,
499+ dependencies : item . dependencies ?? [ ] ,
500+ devDependencies : item . devDependencies ?? [ ] ,
501+ env : envVars ,
502+ source : specifier ,
503+ } ;
504+ await saveInstallManifest ( flags . target , installed ) ;
505+ }
506+ }
507+
446508async function diffModule ( name , flags ) {
447509 const { dir } = await getModule ( name ) ;
448510 const filesDir = path . join ( dir , "files" ) ;
@@ -468,6 +530,37 @@ async function diffModule(name, flags) {
468530 process . exitCode = changes > 0 ? 1 : 0 ;
469531}
470532
533+ async function diffRegistryItem ( specifier , flags , visited = new Set ( ) ) {
534+ const item = await readRegistryItem ( specifier ) ;
535+ const itemId = isHttpUrl ( specifier ) ? specifier : path . resolve ( specifier ) ;
536+ if ( visited . has ( itemId ) ) return ;
537+ visited . add ( itemId ) ;
538+
539+ let changes = 0 ;
540+ for ( const dependency of item . registryDependencies ?? [ ] ) {
541+ await diffRegistryItem ( dependency , flags , visited ) ;
542+ }
543+
544+ for ( const file of item . files ?? [ ] ) {
545+ const relative = file . target ?? file . path ;
546+ const dest = path . join ( flags . target , relative ) ;
547+ if ( ! existsSync ( dest ) ) {
548+ console . log ( `missing ${ relative } ` ) ;
549+ changes += 1 ;
550+ continue ;
551+ }
552+ const sourceHash = createHash ( "sha256" ) . update ( file . content ?? "" ) . digest ( "hex" ) ;
553+ if ( ( await hashFile ( dest ) ) !== sourceHash ) {
554+ console . log ( `changed ${ relative } ` ) ;
555+ changes += 1 ;
556+ } else {
557+ console . log ( `same ${ relative } ` ) ;
558+ }
559+ }
560+
561+ process . exitCode = changes > 0 ? 1 : process . exitCode ;
562+ }
563+
471564async function buildRegistry ( ) {
472565 const outputDir = path . join ( repoRoot , "public" , "r" ) ;
473566 await mkdir ( outputDir , { recursive : true } ) ;
@@ -605,7 +698,9 @@ async function main() {
605698
606699 if ( command === "add" && presetName ) return addPreset ( presetName , flags ) ;
607700 if ( ! moduleName ) throw new Error ( `${ command } requires a module name` ) ;
701+ if ( command === "add" && isRegistryItemSpecifier ( moduleName ) ) return addRegistryItem ( moduleName , flags ) ;
608702 if ( command === "add" ) return addModule ( moduleName , flags ) ;
703+ if ( command === "diff" && isRegistryItemSpecifier ( moduleName ) ) return diffRegistryItem ( moduleName , flags ) ;
609704 if ( command === "diff" ) return diffModule ( moduleName , flags ) ;
610705
611706 throw new Error ( `Unknown command: ${ command } ` ) ;
0 commit comments