@@ -37,13 +37,37 @@ function pkgVersion(): string {
3737 return `^${ getCliVersion ( ) } ` ;
3838}
3939
40+ /**
41+ * Convert an npm package name into a valid ObjectStack namespace identifier.
42+ *
43+ * Namespace rules (from `ManifestSchema` in `@objectstack/spec`):
44+ * - 2-20 chars, `^[a-z][a-z0-9_]{1,19}$`
45+ * - Reserved: `base`, `system`, `sys`
46+ *
47+ * npm names allow hyphens/dots/scopes (e.g. `@acme/my-app`); identifiers don't.
48+ * We strip the scope, replace separators with `_`, lowercase, prefix a leading
49+ * digit, truncate to 20, and pad short names so the result always satisfies
50+ * the regex. Reserved names get a `_app` suffix.
51+ */
52+ export function sanitizeNamespace ( name : string ) : string {
53+ let s = name . replace ( / ^ @ [ ^ / ] + \/ / , '' ) ; // drop npm scope
54+ s = s . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 ] + / g, '_' ) ; // separators → _
55+ s = s . replace ( / ^ _ + | _ + $ / g, '' ) ; // trim underscores
56+ if ( ! s ) s = 'app' ;
57+ if ( / ^ [ 0 - 9 ] / . test ( s ) ) s = 'a' + s ; // must start with a letter
58+ if ( s . length < 2 ) s = ( s + '_app' ) . slice ( 0 , 20 ) ;
59+ if ( s . length > 20 ) s = s . slice ( 0 , 20 ) . replace ( / _ + $ / , '' ) ;
60+ if ( [ 'base' , 'system' , 'sys' ] . includes ( s ) ) s = ( s + '_app' ) . slice ( 0 , 20 ) ;
61+ return s ;
62+ }
63+
4064export const TEMPLATES : Record < string , {
4165 description : string ;
4266 dependencies : Record < string , string > ;
4367 devDependencies : Record < string , string > ;
4468 scripts : Record < string , string > ;
45- configContent : ( name : string ) => string ;
46- srcFiles : Record < string , ( name : string ) => string > ;
69+ configContent : ( name : string , namespace : string ) => string ;
70+ srcFiles : Record < string , ( name : string , namespace : string ) => string > ;
4771} > = {
4872 app : {
4973 description : 'Full application with objects, views, and actions' ,
@@ -69,13 +93,13 @@ export const TEMPLATES: Record<string, {
6993 validate : 'objectstack validate' ,
7094 typecheck : 'tsc --noEmit' ,
7195 } ,
72- configContent : ( name : string ) => `import { defineStack } from '@objectstack/spec';
96+ configContent : ( name : string , namespace : string ) => `import { defineStack } from '@objectstack/spec';
7397import * as objects from './src/objects';
7498
7599export default defineStack({
76100 manifest: {
77- id: 'com.example.${ name } ',
78- namespace: '${ name } ',
101+ id: 'com.example.${ namespace } ',
102+ namespace: '${ namespace } ',
79103 version: '0.1.0',
80104 type: 'app',
81105 name: '${ toTitleCase ( name ) } ',
@@ -86,13 +110,13 @@ export default defineStack({
86110});
87111` ,
88112 srcFiles : {
89- 'src/objects/index.ts' : ( name ) => `export { default as ${ toCamelCase ( name ) } } from './${ name } ';
113+ 'src/objects/index.ts' : ( _name , namespace ) => `export { default as ${ toCamelCase ( namespace ) } Item } from './${ namespace } _item ';
90114` ,
91- 'src/objects/__name__ .ts' : ( name ) => `import * as Data from '@objectstack/spec/data';
115+ 'src/objects/__name___item .ts' : ( _name , namespace ) => `import * as Data from '@objectstack/spec/data';
92116
93- const ${ toCamelCase ( name ) } : Data.Object = {
94- name: '${ name } ',
95- label: '${ toTitleCase ( name ) } ',
117+ const ${ toCamelCase ( namespace ) } Item : Data.Object = {
118+ name: '${ namespace } _item ',
119+ label: '${ toTitleCase ( namespace ) } Item ',
96120 ownership: 'own',
97121 fields: {
98122 name: {
@@ -117,7 +141,7 @@ const ${toCamelCase(name)}: Data.Object = {
117141 },
118142};
119143
120- export default ${ toCamelCase ( name ) } ;
144+ export default ${ toCamelCase ( namespace ) } Item ;
121145` ,
122146 } ,
123147 } ,
@@ -142,13 +166,13 @@ export default ${toCamelCase(name)};
142166 test : 'vitest run' ,
143167 typecheck : 'tsc --noEmit' ,
144168 } ,
145- configContent : ( name : string ) => `import { defineStack } from '@objectstack/spec';
169+ configContent : ( name : string , namespace : string ) => `import { defineStack } from '@objectstack/spec';
146170import * as objects from './src/objects';
147171
148172export default defineStack({
149173 manifest: {
150174 id: 'com.objectstack.plugin-${ name } ',
151- namespace: 'plugin_ ${ name } ',
175+ namespace: '${ namespace } ',
152176 version: '0.1.0',
153177 type: 'plugin',
154178 name: '${ toTitleCase ( name ) } Plugin',
@@ -159,13 +183,13 @@ export default defineStack({
159183});
160184` ,
161185 srcFiles : {
162- 'src/objects/index.ts' : ( name ) => `export { default as ${ toCamelCase ( name ) } } from './${ name } ';
186+ 'src/objects/index.ts' : ( _name , namespace ) => `export { default as ${ toCamelCase ( namespace ) } Item } from './${ namespace } _item ';
163187` ,
164- 'src/objects/__name__ .ts' : ( name ) => `import * as Data from '@objectstack/spec/data';
188+ 'src/objects/__name___item .ts' : ( _name , namespace ) => `import * as Data from '@objectstack/spec/data';
165189
166- const ${ toCamelCase ( name ) } : Data.Object = {
167- name: '${ name } ',
168- label: '${ toTitleCase ( name ) } ',
190+ const ${ toCamelCase ( namespace ) } Item : Data.Object = {
191+ name: '${ namespace } _item ',
192+ label: '${ toTitleCase ( namespace ) } Item ',
169193 ownership: 'own',
170194 fields: {
171195 name: {
@@ -176,7 +200,7 @@ const ${toCamelCase(name)}: Data.Object = {
176200 },
177201};
178202
179- export default ${ toCamelCase ( name ) } ;
203+ export default ${ toCamelCase ( namespace ) } Item ;
180204` ,
181205 } ,
182206 } ,
@@ -199,12 +223,12 @@ export default ${toCamelCase(name)};
199223 validate : 'objectstack validate' ,
200224 typecheck : 'tsc --noEmit' ,
201225 } ,
202- configContent : ( name : string ) => `import { defineStack } from '@objectstack/spec';
226+ configContent : ( name : string , namespace : string ) => `import { defineStack } from '@objectstack/spec';
203227
204228export default defineStack({
205229 manifest: {
206- id: 'com.example.${ name } ',
207- namespace: '${ name } ',
230+ id: 'com.example.${ namespace } ',
231+ namespace: '${ namespace } ',
208232 version: '0.1.0',
209233 type: 'app',
210234 name: '${ toTitleCase ( name ) } ',
@@ -343,7 +367,14 @@ export default class Init extends Command {
343367 this . error ( 'objectstack.config.ts already exists' ) ;
344368 }
345369
370+ // Convert the npm-name (which allows hyphens, dots, scopes) into a
371+ // valid ObjectStack namespace identifier. Threaded into every template
372+ // function so object names use `${namespace}_${shortName}` form and
373+ // satisfy `defineStack()` validation.
374+ const namespace = sanitizeNamespace ( projectName ) ;
375+
346376 printKV ( 'Project' , projectName ) ;
377+ printKV ( 'Namespace' , namespace ) ;
347378 printKV ( 'Template' , `${ flags . template } — ${ template . description } ` ) ;
348379 printKV ( 'Directory' , targetDir ) ;
349380 console . log ( '' ) ;
@@ -374,7 +405,7 @@ export default class Init extends Command {
374405 }
375406
376407 // 2. Create objectstack.config.ts
377- const configContent = template . configContent ( projectName ) ;
408+ const configContent = template . configContent ( projectName , namespace ) ;
378409 fs . writeFileSync ( path . join ( targetDir , 'objectstack.config.ts' ) , configContent ) ;
379410 createdFiles . push ( 'objectstack.config.ts' ) ;
380411
@@ -400,17 +431,20 @@ export default class Init extends Command {
400431 createdFiles . push ( 'tsconfig.json' ) ;
401432 }
402433
403- // 4. Create src files
434+ // 4. Create src files. File paths use `__name__` as a placeholder for
435+ // the namespace (NOT the npm name) so generated identifiers stay snake
436+ // _case even when the project name contains hyphens (e.g. `my-app` →
437+ // namespace `my_app` → `src/objects/my_app_item.ts`).
404438 for ( const [ filePath , contentFn ] of Object . entries ( template . srcFiles ) ) {
405- const resolvedPath = filePath . replace ( ' __name__' , projectName ) ;
439+ const resolvedPath = filePath . replace ( / _ _ n a m e _ _ / g , namespace ) ;
406440 const fullPath = path . join ( targetDir , resolvedPath ) ;
407441 const dir = path . dirname ( fullPath ) ;
408442
409443 if ( ! fs . existsSync ( dir ) ) {
410444 fs . mkdirSync ( dir , { recursive : true } ) ;
411445 }
412446
413- fs . writeFileSync ( fullPath , contentFn ( projectName ) ) ;
447+ fs . writeFileSync ( fullPath , contentFn ( projectName , namespace ) ) ;
414448 createdFiles . push ( resolvedPath ) ;
415449 }
416450
@@ -442,6 +476,30 @@ export default class Init extends Command {
442476 }
443477 }
444478
479+ // Self-test the scaffold so we catch template regressions (e.g. an
480+ // invalid namespace or object name) before the user discovers them by
481+ // running `objectstack dev`. Only runs when deps are present —
482+ // `defineStack()` validation lives in `@objectstack/spec`.
483+ if ( installSucceeded ) {
484+ printStep ( 'Validating scaffold...' ) ;
485+ try {
486+ const { bundleRequire } = await import ( 'bundle-require' ) ;
487+ const { mod } = await bundleRequire ( {
488+ filepath : path . join ( targetDir , 'objectstack.config.ts' ) ,
489+ cwd : targetDir ,
490+ } ) ;
491+ const stack = mod . default ?? mod ;
492+ if ( ! stack ?. manifest ?. namespace ) {
493+ throw new Error ( 'Rendered config has no manifest.namespace' ) ;
494+ }
495+ printSuccess ( `Scaffold validated (namespace: ${ stack . manifest . namespace } )` ) ;
496+ } catch ( err : any ) {
497+ printError ( `Scaffold validation failed: ${ err . message || err } ` ) ;
498+ console . log ( chalk . dim ( ' This is a CLI bug — please report it at https://github.com/objectstack-ai/framework/issues' ) ) ;
499+ this . error ( 'Scaffold validation failed' ) ;
500+ }
501+ }
502+
445503 if ( ! installAttempted || installSucceeded ) {
446504 printSuccess ( 'Project initialized!' ) ;
447505 console . log ( '' ) ;
0 commit comments