@@ -9,25 +9,37 @@ const __filename = fileURLToPath(import.meta.url);
99const repoRoot = path . resolve ( path . dirname ( __filename ) , "../../.." ) ;
1010const registryRoot = path . join ( repoRoot , "registry" ) ;
1111const modulesRoot = path . join ( registryRoot , "modules" ) ;
12+ const presetsRoot = path . join ( registryRoot , "presets" ) ;
1213
1314const requiredModuleFields = [ "name" , "type" , "category" , "title" , "description" , "files" , "status" ] ;
15+ const requiredPresetFields = [ "name" , "title" , "description" , "modules" , "status" ] ;
1416
1517function usage ( ) {
1618 console . log ( `StackFoundry
1719
1820Usage:
1921 stackfoundry list
22+ stackfoundry presets
2023 stackfoundry validate
2124 stackfoundry build
25+ stackfoundry add preset <name> [--target <dir>] [--dry-run] [--force]
2226 stackfoundry add <module> [--target <dir>] [--dry-run] [--force]
2327 stackfoundry diff <module> [--target <dir>]
2428` ) ;
2529}
2630
2731function parseArgs ( argv ) {
28- const [ command , moduleName , ...rest ] = argv ;
32+ const [ command , firstArg , ...tail ] = argv ;
33+ let moduleName = firstArg ;
34+ let presetName ;
35+ let rest = tail ;
2936 const flags = { target : process . cwd ( ) , dryRun : false , force : false } ;
3037
38+ if ( command === "add" && firstArg === "preset" ) {
39+ presetName = rest . shift ( ) ;
40+ moduleName = undefined ;
41+ }
42+
3143 for ( let i = 0 ; i < rest . length ; i += 1 ) {
3244 const arg = rest [ i ] ;
3345 if ( arg === "--target" ) {
@@ -41,7 +53,7 @@ function parseArgs(argv) {
4153 }
4254 }
4355
44- return { command, moduleName, flags } ;
56+ return { command, moduleName, presetName , flags } ;
4557}
4658
4759async function readJson ( filePath ) {
@@ -82,6 +94,14 @@ async function getModule(name) {
8294 } ;
8395}
8496
97+ async function getPreset ( name ) {
98+ const presetPath = path . join ( presetsRoot , `${ name } .json` ) ;
99+ if ( ! existsSync ( presetPath ) ) {
100+ throw new Error ( `Unknown preset: ${ name } ` ) ;
101+ }
102+ return readJson ( presetPath ) ;
103+ }
104+
85105async function validateModule ( moduleDir ) {
86106 const manifestPath = path . join ( moduleDir , "module.json" ) ;
87107 const manifest = await readJson ( manifestPath ) ;
@@ -132,6 +152,22 @@ async function validateRegistry() {
132152 }
133153 }
134154
155+ if ( existsSync ( presetsRoot ) ) {
156+ for ( const file of await readdir ( presetsRoot ) ) {
157+ if ( ! file . endsWith ( ".json" ) ) continue ;
158+ const preset = await readJson ( path . join ( presetsRoot , file ) ) ;
159+ for ( const field of requiredPresetFields ) {
160+ if ( ! ( field in preset ) ) errors . push ( `${ file } : missing field: ${ field } ` ) ;
161+ }
162+ if ( ! Array . isArray ( preset . modules ) ) errors . push ( `${ file } : modules must be an array` ) ;
163+ for ( const moduleName of preset . modules ?? [ ] ) {
164+ if ( ! existsSync ( path . join ( modulesRoot , moduleName , "module.json" ) ) ) {
165+ errors . push ( `${ file } : unknown module ${ moduleName } ` ) ;
166+ }
167+ }
168+ }
169+ }
170+
135171 return errors ;
136172}
137173
@@ -143,6 +179,15 @@ async function listModules() {
143179 }
144180}
145181
182+ async function listPresets ( ) {
183+ if ( ! existsSync ( presetsRoot ) ) return ;
184+ const files = ( await readdir ( presetsRoot ) ) . filter ( ( file ) => file . endsWith ( ".json" ) ) . sort ( ) ;
185+ for ( const file of files ) {
186+ const preset = await readJson ( path . join ( presetsRoot , file ) ) ;
187+ console . log ( `${ preset . name . padEnd ( 18 ) } ${ preset . status . padEnd ( 12 ) } ${ preset . description } ` ) ;
188+ }
189+ }
190+
146191async function loadInstallManifest ( target ) {
147192 const filePath = path . join ( target , ".stackfoundry" , "installed.json" ) ;
148193 if ( ! existsSync ( filePath ) ) return { modules : { } } ;
@@ -155,8 +200,15 @@ async function saveInstallManifest(target, manifest) {
155200 await writeFile ( path . join ( dir , "installed.json" ) , `${ JSON . stringify ( manifest , null , 2 ) } \n` ) ;
156201}
157202
158- async function addModule ( name , flags ) {
203+ async function addModule ( name , flags , visited = new Set ( ) ) {
204+ if ( visited . has ( name ) ) return ;
205+ visited . add ( name ) ;
206+
159207 const { dir, manifest } = await getModule ( name ) ;
208+ for ( const dependency of manifest . registryDependencies ) {
209+ await addModule ( dependency , flags , visited ) ;
210+ }
211+
160212 const filesDir = path . join ( dir , "files" ) ;
161213 const sourceFiles = await listFiles ( filesDir ) ;
162214 const installed = await loadInstallManifest ( flags . target ) ;
@@ -217,6 +269,16 @@ async function addModule(name, flags) {
217269 }
218270}
219271
272+ async function addPreset ( name , flags ) {
273+ if ( ! name ) throw new Error ( "add preset requires a preset name" ) ;
274+ const preset = await getPreset ( name ) ;
275+ const visited = new Set ( ) ;
276+ console . log ( `${ flags . dryRun ? "would install" : "installing" } preset ${ preset . name } ` ) ;
277+ for ( const moduleName of preset . modules ) {
278+ await addModule ( moduleName , flags , visited ) ;
279+ }
280+ }
281+
220282async function diffModule ( name , flags ) {
221283 const { dir } = await getModule ( name ) ;
222284 const filesDir = path . join ( dir , "files" ) ;
@@ -259,6 +321,15 @@ async function buildRegistry() {
259321
260322 await writeFile ( path . join ( outputDir , "registry.json" ) , `${ JSON . stringify ( builtRegistry , null , 2 ) } \n` ) ;
261323
324+ const presetsOutputDir = path . join ( outputDir , "presets" ) ;
325+ await mkdir ( presetsOutputDir , { recursive : true } ) ;
326+ if ( existsSync ( presetsRoot ) ) {
327+ for ( const file of ( await readdir ( presetsRoot ) ) . filter ( ( entry ) => entry . endsWith ( ".json" ) ) . sort ( ) ) {
328+ await copyFile ( path . join ( presetsRoot , file ) , path . join ( presetsOutputDir , file ) ) ;
329+ console . log ( `built public/r/presets/${ file } ` ) ;
330+ }
331+ }
332+
262333 for ( const item of registry . items ) {
263334 const { dir, manifest } = await getModule ( item . name ) ;
264335 const filesDir = path . join ( dir , "files" ) ;
@@ -300,14 +371,15 @@ async function buildRegistry() {
300371}
301372
302373async function main ( ) {
303- const { command, moduleName, flags } = parseArgs ( process . argv . slice ( 2 ) ) ;
374+ const { command, moduleName, presetName , flags } = parseArgs ( process . argv . slice ( 2 ) ) ;
304375
305376 if ( ! command || command === "help" || command === "--help" ) {
306377 usage ( ) ;
307378 return ;
308379 }
309380
310381 if ( command === "list" ) return listModules ( ) ;
382+ if ( command === "presets" ) return listPresets ( ) ;
311383 if ( command === "build" ) return buildRegistry ( ) ;
312384 if ( command === "validate" ) {
313385 const errors = await validateRegistry ( ) ;
@@ -320,6 +392,7 @@ async function main() {
320392 return ;
321393 }
322394
395+ if ( command === "add" && presetName ) return addPreset ( presetName , flags ) ;
323396 if ( ! moduleName ) throw new Error ( `${ command } requires a module name` ) ;
324397 if ( command === "add" ) return addModule ( moduleName , flags ) ;
325398 if ( command === "diff" ) return diffModule ( moduleName , flags ) ;
0 commit comments