@@ -185,20 +185,123 @@ async function addBlocksWorkspace(targetDir: string, options: {
185185 }
186186}
187187
188- const AVAILABLE_TEMPLATES = ['default', 'bare', 'react', 'backend', 'nextjs', 'auth-cognito', 'amplify', 'demo'];
188+ // ─── Template discovery ─────────────────────────────────────────────────────
189+ //
190+ // Templates are the ONLY source of truth for the template list. Each template
191+ // under ` templates / < name > / ` s h i p s a ` p a c k a g e .j s o n ` w i t h :
192+ // {
193+ // "blocksTemplate": "<name>",
194+ // "blocksTemplateDescription": "One-line description shown in --help"
195+ // }
196+ //
197+ // Adding a new template = drop a folder with a package.json containing those
198+ // two fields. The CLI's --help, validation, and error messages all pick it up
199+ // automatically — no other code changes required.
200+ //
201+ // An optional `blocksTemplateOverlayOnly: true` marks a template as an overlay
202+ // that's auto-selected when the CLI detects a matching existing project (e.g.
203+ // `amplify`) rather than a scaffoldable fresh-app starter; `createFreshProject`
204+ // rejects it up front. A folder whose package.json can't be read is still shown
205+ // in --help (flagged) but excluded from `--template` validation.
206+ //
207+ // Display order below is preserved from the previous hardcoded list so that
208+ // `--help` reads in the intended progression (default → bare → richer variants
209+ // → framework variants → overlays → demos). New templates default to
210+ // alphabetical after known ones. Update this array to reorder.
211+
212+ type TemplateInfo = {
213+ name : string ;
214+ description : string ;
215+ valid : boolean ;
216+ overlayOnly : boolean ;
217+ } ;
218+
219+ const TEMPLATE_DISPLAY_ORDER = [ 'default' , 'bare' , 'react' , 'backend' , 'nextjs' , 'auth-cognito' , 'amplify' , 'demo' ] ;
220+
221+ let templateCatalogCache : TemplateInfo [ ] | null = null ;
222+
223+ async function loadTemplateCatalog ( ) : Promise < TemplateInfo [ ] > {
224+ if ( templateCatalogCache ) return templateCatalogCache ;
225+
226+ const templatesDir = join ( __dirname , '../templates' ) ;
227+ const entries = await readdir ( templatesDir , { withFileTypes : true } ) ;
228+ const discovered : TemplateInfo [ ] = [ ] ;
229+
230+ for ( const entry of entries ) {
231+ if ( ! entry . isDirectory ( ) ) continue ;
232+ const pkgPath = join ( templatesDir , entry . name , 'package.json' ) ;
233+ try {
234+ const pkg = JSON . parse ( await readFile ( pkgPath , 'utf-8' ) ) ;
235+ discovered . push ( {
236+ name : entry . name ,
237+ description : typeof pkg . blocksTemplateDescription === 'string'
238+ ? pkg . blocksTemplateDescription
239+ : '(no description — add `blocksTemplateDescription` to this template\'s package.json)' ,
240+ valid : true ,
241+ overlayOnly : pkg . blocksTemplateOverlayOnly === true ,
242+ } ) ;
243+ } catch {
244+ // Template folder without a readable package.json — surface it so it's
245+ // discoverable, but flag that metadata is missing.
246+ discovered . push ( {
247+ name : entry . name ,
248+ description : '(no package.json — template metadata missing)' ,
249+ valid : false ,
250+ overlayOnly : false ,
251+ } ) ;
252+ }
253+ }
254+
255+ // Sort by TEMPLATE_DISPLAY_ORDER, then any unknown templates alphabetically.
256+ discovered . sort ( ( a , b ) => {
257+ const aIdx = TEMPLATE_DISPLAY_ORDER . indexOf ( a . name ) ;
258+ const bIdx = TEMPLATE_DISPLAY_ORDER . indexOf ( b . name ) ;
259+ if ( aIdx === - 1 && bIdx === - 1 ) return a . name . localeCompare ( b . name ) ;
260+ if ( aIdx === - 1 ) return 1 ;
261+ if ( bIdx === - 1 ) return - 1 ;
262+ return aIdx - bIdx ;
263+ } ) ;
264+
265+ templateCatalogCache = discovered ;
266+ return discovered ;
267+ }
189268
190- function validateTemplateName(templateName: string): void {
191- if (!AVAILABLE_TEMPLATES.includes(templateName)) {
269+ async function validateTemplateName ( templateName : string ) : Promise < void > {
270+ const catalog = await loadTemplateCatalog ( ) ;
271+ const names = catalog . filter ( t => t . valid ) . map ( t => t . name ) ;
272+ if ( ! names . includes ( templateName ) ) {
192273 console . error ( `Error: Unknown template "${ templateName } ".` ) ;
193- console.error(` Available templates : ${AVAILABLE_TEMPLATES . join ( ', ' ) } `);
274+ console . error ( `Available templates: ${ names . join ( ', ' ) } ` ) ;
194275 process . exit ( 1 ) ;
195276 }
196277}
197278
198279// ─── Fresh project creation ──────────────────────────────────────────────────
199280
200281async function createFreshProject ( targetDir : string , templateName : string , skipInstall = false ) {
201- validateTemplateName(templateName);
282+ await validateTemplateName ( templateName ) ;
283+
284+ // Overlay templates (e.g. `amplify`, detected via `isAmplifyGen2Project()`)
285+ // are auto-selected when the CLI finds a matching existing project and are
286+ // flagged `blocksTemplateOverlayOnly` in their package.json. They ship only an
287+ // overlay snippet — not a full standalone project (no `src/`, no `index.html`,
288+ // no `gitignore`) — so a fresh scaffold would fail partway through the copy.
289+ // Reject up front. Keying off the catalog flag (not a hardcoded name) means a
290+ // new overlay template needs no change here — just the flag in its package.json.
291+ const overlayEntry = ( await loadTemplateCatalog ( ) ) . find ( t => t . name === templateName ) ;
292+ if ( overlayEntry ?. overlayOnly ) {
293+ console . error ( `Error: The \`${ templateName } \` template is an overlay that's auto-selected` ) ;
294+ console . error ( 'when the CLI detects a compatible existing project. It cannot be' ) ;
295+ console . error ( 'scaffolded as a fresh app.' ) ;
296+ console . error ( '' ) ;
297+ console . error ( 'To scaffold a fresh Blocks app, choose a different template:' ) ;
298+ console . error ( ' npx @aws-blocks/create-blocks-app my-app --template default' ) ;
299+ console . error ( '' ) ;
300+ console . error ( 'To integrate Blocks into an existing project, run this from' ) ;
301+ console . error ( 'the project root:' ) ;
302+ console . error ( ' npx @aws-blocks/create-blocks-app' ) ;
303+ process . exit ( 1 ) ;
304+ }
202305
203306 // Read template package.json to get template name
204307 const templateDir = join ( __dirname , '../templates' , templateName ) ;
@@ -556,7 +659,14 @@ async function integrateWithExistingProject(targetDir: string, templateName = 'd
556659
557660// ─── Main ────────────────────────────────────────────────────────────────────
558661
559- function printUsage() {
662+ async function printUsage ( ) {
663+ const catalog = await loadTemplateCatalog ( ) ;
664+ const names = catalog . map ( t => t . name ) ;
665+ const widest = catalog . length ? Math . max ( ...catalog . map ( t => t . name . length ) ) : 0 ;
666+ const templateList = catalog
667+ . map ( t => ` ${ t . name . padEnd ( widest ) } ${ t . description } ` )
668+ . join ( '\n' ) ;
669+
560670 console . log ( `Usage: create-blocks-app [directory] [options]
561671
562672Create a new AWS Blocks app or add Blocks to an existing project.
@@ -581,15 +691,20 @@ Options:
581691 starter app; when adding to an existing project it
582692 selects which aws-blocks/ workspace to copy, e.g.
583693 "nextjs" for a Next.js dev server (default: "default")
584- Available templates : ${AVAILABLE_TEMPLATES . join ( ', ' ) }
694+
695+ Available templates: ${ names . join ( ', ' ) }
696+
697+ ${ templateList }
698+
585699 --skip-install Skip installing dependencies
586700 -y, --yes Skip confirmation prompts
587701 -h, --help Show this help message
588702
589703Examples:
590- npx @aws - blocks / create - blocks - app Add Blocks to current project
591- npx @aws - blocks / create - blocks - app . Add Blocks to current project
592- npx @aws - blocks / create - blocks - app my - app Create a standalone Blocks starter app
704+ npx @aws-blocks/create-blocks-app Add Blocks to current project
705+ npx @aws-blocks/create-blocks-app . Add Blocks to current project
706+ npx @aws-blocks/create-blocks-app my-app Create a standalone Blocks starter app
707+ npx @aws-blocks/create-blocks-app . --yes --template nextjs Non-interactive scaffold with the Next.js template
593708` ) ;
594709}
595710
@@ -602,7 +717,7 @@ async function create() {
602717
603718 for ( let i = 0 ; i < args . length ; i ++ ) {
604719 if ( args [ i ] === '--help' || args [ i ] === '-h' ) {
605- printUsage();
720+ await printUsage ( ) ;
606721 process . exit ( 0 ) ;
607722 } else if ( args [ i ] === '--template' ) {
608723 const value = args [ i + 1 ] ;
@@ -633,7 +748,7 @@ async function create() {
633748 }
634749 }
635750
636- validateTemplateName ( templateName ) ;
751+ await validateTemplateName ( templateName ) ;
637752
638753 const templatePkgVersion : string = JSON . parse (
639754 await readFile ( join ( __dirname , '../templates' , templateName , 'package.json' ) , 'utf-8' ) ,
0 commit comments