create-mantiq 0.5.23 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +188 -12
  3. package/src/templates.ts +23 -5
  4. package/src/terminal.ts +62 -0
  5. package/stubs/auth/api/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
  6. package/stubs/auth/api/routes/api.ts.stub +24 -0
  7. package/stubs/auth/api/tests/feature/token-auth.test.ts.stub +69 -0
  8. package/stubs/auth/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
  9. package/stubs/auth/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
  10. package/stubs/auth/web/app/Http/Controllers/AuthController.ts.stub +43 -0
  11. package/stubs/auth/web/app/Http/Controllers/PageController.ts.stub +66 -0
  12. package/stubs/auth/web/routes/web.ts.stub +25 -0
  13. package/stubs/auth/web/svelte/src/App.svelte.stub +77 -0
  14. package/stubs/auth/web/svelte/src/pages.ts.stub +17 -0
  15. package/stubs/auth/web/tests/feature/auth.test.ts.stub +69 -0
  16. package/stubs/auth/web/vue/src/App.vue.stub +74 -0
  17. package/stubs/auth/web/vue/src/pages.ts.stub +17 -0
  18. package/stubs/manifest.json +582 -0
  19. package/stubs/noauth/app/Http/Controllers/PageController.ts.stub +41 -0
  20. package/stubs/noauth/app/Models/User.ts.stub +5 -0
  21. package/stubs/noauth/database/migrations/001_create_users_table.ts.stub +17 -0
  22. package/stubs/noauth/routes/api.ts.stub +16 -0
  23. package/stubs/noauth/routes/web.ts.stub +15 -0
  24. package/stubs/noauth/svelte/src/App.svelte.stub +68 -0
  25. package/stubs/noauth/svelte/src/pages.ts.stub +7 -0
  26. package/stubs/noauth/vue/src/App.vue.stub +62 -0
  27. package/stubs/noauth/vue/src/pages.ts.stub +7 -0
  28. package/stubs/tailwind-only/react/src/components/layout/app-sidebar.tsx.stub +68 -0
  29. package/stubs/tailwind-only/react/src/components/layout/authenticated-layout.tsx.stub +57 -0
  30. package/stubs/tailwind-only/react/src/components/layout/header.tsx.stub +52 -0
  31. package/stubs/tailwind-only/react/src/components/layout/main.tsx.stub +21 -0
  32. package/stubs/tailwind-only/react/src/components/layout/nav-group.tsx.stub +185 -0
  33. package/stubs/tailwind-only/react/src/components/layout/nav-user.tsx.stub +106 -0
  34. package/stubs/tailwind-only/react/src/components/layout/sidebar-data.ts.stub +58 -0
  35. package/stubs/tailwind-only/react/src/components/layout/theme-toggle.tsx.stub +36 -0
  36. package/stubs/tailwind-only/react/src/components/layout/top-nav.tsx.stub +72 -0
  37. package/stubs/tailwind-only/react/src/lib/utils.ts.stub +6 -0
  38. package/stubs/tailwind-only/react/src/pages/Dashboard.tsx.stub +205 -0
  39. package/stubs/tailwind-only/react/src/pages/Login.tsx.stub +122 -0
  40. package/stubs/tailwind-only/react/src/pages/Register.tsx.stub +137 -0
  41. package/stubs/tailwind-only/react/src/pages/Users.tsx.stub +426 -0
  42. package/stubs/tailwind-only/react/src/pages/account/layout.tsx.stub +64 -0
  43. package/stubs/tailwind-only/react/src/pages/account/preferences.tsx.stub +80 -0
  44. package/stubs/tailwind-only/react/src/pages/account/profile.tsx.stub +67 -0
  45. package/stubs/tailwind-only/react/src/pages/account/security.tsx.stub +91 -0
  46. package/stubs/tailwind-only/react/src/style.css.stub +14 -0
  47. package/stubs/tailwind-only/svelte/src/lib/components/layout/app-sidebar.svelte.stub +104 -0
  48. package/stubs/tailwind-only/svelte/src/lib/components/layout/authenticated-layout.svelte.stub +51 -0
  49. package/stubs/tailwind-only/svelte/src/lib/components/layout/header.svelte.stub +66 -0
  50. package/stubs/tailwind-only/svelte/src/lib/components/layout/main.svelte.stub +26 -0
  51. package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-group.svelte.stub +131 -0
  52. package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-user.svelte.stub +104 -0
  53. package/stubs/tailwind-only/svelte/src/lib/components/layout/sidebar-data.ts.stub +57 -0
  54. package/stubs/tailwind-only/svelte/src/lib/components/layout/theme-toggle.svelte.stub +28 -0
  55. package/stubs/tailwind-only/svelte/src/lib/components/layout/top-nav.svelte.stub +99 -0
  56. package/stubs/tailwind-only/svelte/src/lib/utils.ts.stub +6 -0
  57. package/stubs/tailwind-only/svelte/src/pages/Dashboard.svelte.stub +192 -0
  58. package/stubs/tailwind-only/svelte/src/pages/Login.svelte.stub +120 -0
  59. package/stubs/tailwind-only/svelte/src/pages/Register.svelte.stub +134 -0
  60. package/stubs/tailwind-only/svelte/src/pages/Users.svelte.stub +293 -0
  61. package/stubs/tailwind-only/svelte/src/pages/account/Layout.svelte.stub +61 -0
  62. package/stubs/tailwind-only/svelte/src/pages/account/Preferences.svelte.stub +81 -0
  63. package/stubs/tailwind-only/svelte/src/pages/account/Profile.svelte.stub +76 -0
  64. package/stubs/tailwind-only/svelte/src/pages/account/Security.svelte.stub +140 -0
  65. package/stubs/tailwind-only/svelte/src/style.css.stub +127 -0
  66. package/stubs/tailwind-only/vue/src/components/layout/AppSidebar.vue.stub +60 -0
  67. package/stubs/tailwind-only/vue/src/components/layout/AuthenticatedLayout.vue.stub +73 -0
  68. package/stubs/tailwind-only/vue/src/components/layout/Header.vue.stub +54 -0
  69. package/stubs/tailwind-only/vue/src/components/layout/Main.vue.stub +22 -0
  70. package/stubs/tailwind-only/vue/src/components/layout/NavGroup.vue.stub +107 -0
  71. package/stubs/tailwind-only/vue/src/components/layout/NavUser.vue.stub +104 -0
  72. package/stubs/tailwind-only/vue/src/components/layout/ThemeToggle.vue.stub +28 -0
  73. package/stubs/tailwind-only/vue/src/components/layout/TopNav.vue.stub +86 -0
  74. package/stubs/tailwind-only/vue/src/components/layout/sidebar-data.ts.stub +57 -0
  75. package/stubs/tailwind-only/vue/src/lib/utils.ts.stub +7 -0
  76. package/stubs/tailwind-only/vue/src/pages/Dashboard.vue.stub +195 -0
  77. package/stubs/tailwind-only/vue/src/pages/Login.vue.stub +121 -0
  78. package/stubs/tailwind-only/vue/src/pages/Register.vue.stub +137 -0
  79. package/stubs/tailwind-only/vue/src/pages/Users.vue.stub +401 -0
  80. package/stubs/tailwind-only/vue/src/pages/account/Layout.vue.stub +56 -0
  81. package/stubs/tailwind-only/vue/src/pages/account/Preferences.vue.stub +81 -0
  82. package/stubs/tailwind-only/vue/src/pages/account/Profile.vue.stub +69 -0
  83. package/stubs/tailwind-only/vue/src/pages/account/Security.vue.stub +85 -0
  84. package/stubs/tailwind-only/vue/src/style.css.stub +26 -0
  85. package/stubs/themes/corporate/react/src/components/layout/app-sidebar.tsx.stub +74 -0
  86. package/stubs/themes/corporate/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  87. package/stubs/themes/corporate/react/src/pages/Dashboard.tsx.stub +310 -0
  88. package/stubs/themes/corporate/react/src/pages/Login.tsx.stub +122 -0
  89. package/stubs/themes/corporate/react/src/pages/Register.tsx.stub +144 -0
  90. package/stubs/themes/corporate/react/src/style.css.stub +135 -0
  91. package/stubs/themes/corporate/svelte/src/pages/Dashboard.svelte.stub +271 -0
  92. package/stubs/themes/corporate/svelte/src/pages/Login.svelte.stub +117 -0
  93. package/stubs/themes/corporate/svelte/src/pages/Register.svelte.stub +138 -0
  94. package/stubs/themes/corporate/svelte/src/style.css.stub +134 -0
  95. package/stubs/themes/corporate/vue/src/pages/Dashboard.vue.stub +325 -0
  96. package/stubs/themes/corporate/vue/src/pages/Login.vue.stub +118 -0
  97. package/stubs/themes/corporate/vue/src/pages/Register.vue.stub +139 -0
  98. package/stubs/themes/corporate/vue/src/style.css.stub +134 -0
  99. package/stubs/themes/minimal/react/src/components/layout/app-sidebar.tsx.stub +68 -0
  100. package/stubs/themes/minimal/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  101. package/stubs/themes/minimal/react/src/pages/Dashboard.tsx.stub +166 -0
  102. package/stubs/themes/minimal/react/src/pages/Login.tsx.stub +95 -0
  103. package/stubs/themes/minimal/react/src/pages/Register.tsx.stub +73 -0
  104. package/stubs/themes/minimal/react/src/style.css.stub +142 -0
  105. package/stubs/themes/minimal/svelte/src/pages/Dashboard.svelte.stub +149 -0
  106. package/stubs/themes/minimal/svelte/src/pages/Login.svelte.stub +90 -0
  107. package/stubs/themes/minimal/svelte/src/pages/Register.svelte.stub +70 -0
  108. package/stubs/themes/minimal/svelte/src/style.css.stub +142 -0
  109. package/stubs/themes/minimal/vue/src/pages/Dashboard.vue.stub +163 -0
  110. package/stubs/themes/minimal/vue/src/pages/Login.vue.stub +91 -0
  111. package/stubs/themes/minimal/vue/src/pages/Register.vue.stub +73 -0
  112. package/stubs/themes/minimal/vue/src/style.css.stub +142 -0
  113. package/stubs/themes/starter/react/src/components/layout/app-sidebar.tsx.stub +74 -0
  114. package/stubs/themes/starter/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  115. package/stubs/themes/starter/react/src/pages/Dashboard.tsx.stub +236 -0
  116. package/stubs/themes/starter/react/src/pages/Login.tsx.stub +131 -0
  117. package/stubs/themes/starter/react/src/pages/Register.tsx.stub +145 -0
  118. package/stubs/themes/starter/react/src/style.css.stub +141 -0
  119. package/stubs/themes/starter/svelte/src/pages/Dashboard.svelte.stub +212 -0
  120. package/stubs/themes/starter/svelte/src/pages/Login.svelte.stub +126 -0
  121. package/stubs/themes/starter/svelte/src/pages/Register.svelte.stub +139 -0
  122. package/stubs/themes/starter/svelte/src/style.css.stub +141 -0
  123. package/stubs/themes/starter/vue/src/pages/Dashboard.vue.stub +228 -0
  124. package/stubs/themes/starter/vue/src/pages/Login.vue.stub +127 -0
  125. package/stubs/themes/starter/vue/src/pages/Register.vue.stub +140 -0
  126. package/stubs/themes/starter/vue/src/style.css.stub +141 -0
  127. package/stubs/themes/workspace/react/src/components/layout/app-sidebar.tsx.stub +97 -0
  128. package/stubs/themes/workspace/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  129. package/stubs/themes/workspace/react/src/pages/Dashboard.tsx.stub +304 -0
  130. package/stubs/themes/workspace/react/src/pages/Login.tsx.stub +131 -0
  131. package/stubs/themes/workspace/react/src/pages/Register.tsx.stub +82 -0
  132. package/stubs/themes/workspace/react/src/style.css.stub +138 -0
  133. package/stubs/themes/workspace/svelte/src/pages/Dashboard.svelte.stub +215 -0
  134. package/stubs/themes/workspace/svelte/src/pages/Login.svelte.stub +124 -0
  135. package/stubs/themes/workspace/svelte/src/pages/Register.svelte.stub +76 -0
  136. package/stubs/themes/workspace/svelte/src/style.css.stub +134 -0
  137. package/stubs/themes/workspace/vue/src/pages/Dashboard.vue.stub +220 -0
  138. package/stubs/themes/workspace/vue/src/pages/Login.vue.stub +128 -0
  139. package/stubs/themes/workspace/vue/src/pages/Register.vue.stub +80 -0
  140. package/stubs/themes/workspace/vue/src/style.css.stub +134 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mantiq",
3
- "version": "0.5.23",
3
+ "version": "0.6.1",
4
4
  "description": "Scaffold a new MantiqJS application",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -2,15 +2,17 @@
2
2
  import { existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'node:fs'
3
3
  import { dirname, resolve, join, relative } from 'node:path'
4
4
  import { randomBytes } from 'node:crypto'
5
- import { getTemplates } from './templates.ts'
5
+ import { getTemplates, type Theme } from './templates.ts'
6
6
  import { Terminal } from './terminal.ts'
7
7
 
8
8
  /**
9
9
  * Recursively copy a directory, preserving structure.
10
10
  * Skips node_modules, .git, bun.lock, *.sqlite files.
11
+ * `skipRelPaths` contains relative paths (from root src) to skip.
11
12
  */
12
- async function copyDirectory(src: string, dest: string): Promise<number> {
13
+ async function copyDirectory(src: string, dest: string, skipRelPaths?: Set<string>, rootSrc?: string): Promise<number> {
13
14
  let count = 0
15
+ const root = rootSrc ?? src
14
16
  const entries = readdirSync(src, { withFileTypes: true })
15
17
  for (const entry of entries) {
16
18
  const srcPath = join(src, entry.name)
@@ -20,9 +22,15 @@ async function copyDirectory(src: string, dest: string): Promise<number> {
20
22
  if (['node_modules', '.git', 'bun.lock', 'README.md'].includes(entry.name)) continue
21
23
  if (entry.name.endsWith('.sqlite') || entry.name.endsWith('.sqlite-wal') || entry.name.endsWith('.sqlite-shm')) continue
22
24
 
25
+ // Skip paths in the conditional skip set
26
+ if (skipRelPaths) {
27
+ const rel = relative(root, srcPath)
28
+ if (skipRelPaths.has(rel)) continue
29
+ }
30
+
23
31
  if (entry.isDirectory()) {
24
32
  mkdirSync(destPath, { recursive: true })
25
- count += await copyDirectory(srcPath, destPath)
33
+ count += await copyDirectory(srcPath, destPath, skipRelPaths, root)
26
34
  } else {
27
35
  mkdirSync(dirname(destPath), { recursive: true })
28
36
  await Bun.write(destPath, Bun.file(srcPath))
@@ -74,7 +82,11 @@ if (!projectName) {
74
82
 
75
83
  ${bold('Options:')}
76
84
  --kit=${emerald('react|vue|svelte')} Frontend framework
77
- --ui=${emerald('shadcn')} UI component library (React only)
85
+ --ui=${emerald('shadcn|tailwind')} UI component library
86
+ --theme=${emerald('default|minimal|workspace|corporate|starter')}
87
+ Dashboard theme (shadcn only)
88
+ --auth=${emerald('builtin|none')} Authentication setup
89
+ --with=${emerald('ai')} Optional packages (comma-separated)
78
90
  --no-git Skip git initialization
79
91
  --yes Accept defaults (non-interactive)
80
92
 
@@ -82,6 +94,8 @@ if (!projectName) {
82
94
  bun create mantiq my-app
83
95
  bun create mantiq my-app --kit=react
84
96
  bun create mantiq my-app --kit=react --ui=shadcn
97
+ bun create mantiq my-app --kit=react --auth=none
98
+ bun create mantiq my-app --kit=react --with=ai
85
99
  `)
86
100
  process.exit(1)
87
101
  }
@@ -97,7 +111,15 @@ if (existsSync(projectDir)) {
97
111
  const term = new Terminal()
98
112
 
99
113
  let kit: Kit | undefined = flags['kit'] as Kit | undefined
100
- let ui: 'shadcn' | 'none' = (flags['ui'] as string) === 'shadcn' ? 'shadcn' : 'none'
114
+ let ui: 'shadcn' | 'tailwind' = (flags['ui'] as string) === 'tailwind' ? 'tailwind' : 'shadcn'
115
+ const validThemes = ['default', 'minimal', 'workspace', 'corporate', 'starter'] as const
116
+ let theme: Theme = (validThemes as readonly string[]).includes(flags['theme'] as string)
117
+ ? (flags['theme'] as Theme)
118
+ : 'default'
119
+ let auth: 'builtin' | 'none' = (flags['auth'] as string) === 'none' ? 'none' : 'builtin'
120
+ let optionalPackages: string[] = typeof flags['with'] === 'string'
121
+ ? flags['with'].split(',').map(s => s.trim()).filter(Boolean)
122
+ : []
101
123
 
102
124
  if (!isCI && !kit) {
103
125
  // Show branded header
@@ -108,11 +130,48 @@ if (!isCI && !kit) {
108
130
  { value: 'react', label: 'React' },
109
131
  { value: 'vue', label: 'Vue' },
110
132
  { value: 'svelte', label: 'Svelte' },
111
- { value: 'none', label: 'None', hint: 'API only' },
133
+ { value: 'none', label: 'Vanilla', hint: 'API only' },
112
134
  ])
113
135
 
114
136
  kit = framework === 'none' ? undefined : framework as Kit
115
137
 
138
+ // UI kit selection (only if a frontend framework was chosen)
139
+ if (kit && !flags['ui']) {
140
+ const uiChoice = await term.select('Choose a UI kit', [
141
+ { value: 'shadcn', label: 'shadcn + Tailwind' },
142
+ { value: 'tailwind', label: 'Tailwind only' },
143
+ ])
144
+ ui = uiChoice as 'shadcn' | 'tailwind'
145
+ }
146
+
147
+ // Theme selection (only for shadcn)
148
+ if (kit && ui === 'shadcn' && !flags['theme']) {
149
+ const themeChoice = await term.select('Choose a theme', [
150
+ { value: 'default', label: 'Default', hint: 'emerald, classic admin' },
151
+ { value: 'minimal', label: 'Minimal', hint: 'clean & focused' },
152
+ { value: 'workspace', label: 'Workspace', hint: 'warm & approachable' },
153
+ { value: 'corporate', label: 'Corporate', hint: 'professional & data-rich' },
154
+ { value: 'starter', label: 'Starter', hint: 'bold & marketing-ready' },
155
+ ])
156
+ theme = themeChoice as Theme
157
+ }
158
+
159
+ // Authentication selection
160
+ if (!flags['auth']) {
161
+ const authChoice = await term.select('Authentication', [
162
+ { value: 'builtin', label: 'Built-in', hint: 'session + token auth' },
163
+ { value: 'none', label: 'None' },
164
+ ])
165
+ auth = authChoice as 'builtin' | 'none'
166
+ }
167
+
168
+ // Optional packages selection
169
+ if (!flags['with']) {
170
+ optionalPackages = await term.multiSelect('Optional packages', [
171
+ { value: 'ai', label: 'AI', hint: '@mantiq/ai' },
172
+ ])
173
+ }
174
+
116
175
  } else {
117
176
  // Validate flags
118
177
  if (kit && !validKits.includes(kit as Kit)) {
@@ -130,7 +189,16 @@ let fileCount = 0
130
189
  // Step 1: Copy the skeleton directory as the base
131
190
  const skeletonDir = resolve(import.meta.dir, '..', 'skeleton')
132
191
  if (existsSync(skeletonDir)) {
133
- fileCount += await copyDirectory(skeletonDir, projectDir)
192
+ // Build conditional skip set for skeleton files
193
+ const skeletonSkips = new Set<string>()
194
+ if (auth === 'none') {
195
+ skeletonSkips.add('config/auth.ts')
196
+ skeletonSkips.add('database/migrations/002_create_personal_access_tokens_table.ts')
197
+ }
198
+ if (!optionalPackages.includes('ai')) {
199
+ skeletonSkips.add('config/ai.ts')
200
+ }
201
+ fileCount += await copyDirectory(skeletonDir, projectDir, skeletonSkips)
134
202
  } else {
135
203
  // Fallback: skeleton not bundled (shouldn't happen in published package)
136
204
  console.error(' Skeleton directory not found')
@@ -138,7 +206,7 @@ if (existsSync(skeletonDir)) {
138
206
 
139
207
  // Step 2: Generate dynamic files (package.json, .env — overwrites skeleton versions)
140
208
  const appKey = `base64:${randomBytes(32).toString('base64')}`
141
- const templates = getTemplates({ name: projectName, appKey, kit, ui })
209
+ const templates = getTemplates({ name: projectName, appKey, kit, ui, theme, auth, optionalPackages })
142
210
  for (const [relativePath, content] of Object.entries(templates)) {
143
211
  const fullPath = `${projectDir}/${relativePath}`
144
212
  mkdirSync(dirname(fullPath), { recursive: true })
@@ -156,9 +224,32 @@ if (kit) {
156
224
  const kitManifest = manifest[kit]
157
225
  const sharedManifest = manifest.shared
158
226
 
227
+ // Auth-related shared stubs to skip when auth === 'none'
228
+ const authSharedTargets = new Set([
229
+ 'app/Http/Controllers/AuthController.ts',
230
+ 'app/Http/Controllers/PageController.ts',
231
+ 'app/Http/Requests/LoginRequest.ts',
232
+ 'app/Http/Requests/RegisterRequest.ts',
233
+ 'routes/web.ts',
234
+ 'tests/feature/auth.test.ts',
235
+ ])
236
+
237
+ // Targets that tailwind-only stubs will override — skip from shadcn kit
238
+ const tailwindOnlyManifest = manifest['tailwind-only']?.[kit]
239
+ const tailwindOverrideTargets = new Set<string>()
240
+ if (ui === 'tailwind' && tailwindOnlyManifest?.files) {
241
+ for (const { target } of tailwindOnlyManifest.files) {
242
+ tailwindOverrideTargets.add(target)
243
+ }
244
+ }
245
+
159
246
  // Kit-specific stubs (src/, vite.config.ts, etc.)
160
247
  if (kitManifest?.files) {
161
248
  for (const { stub, target } of kitManifest.files) {
249
+ // Skip components.json when ui === 'tailwind'
250
+ if (ui === 'tailwind' && stub === 'components.json.stub') continue
251
+ // Skip files that will be overridden by tailwind-only stubs
252
+ if (ui === 'tailwind' && tailwindOverrideTargets.has(target)) continue
162
253
  const src = resolve(stubsDir, kit, stub)
163
254
  const dest = resolve(projectDir, target)
164
255
  mkdirSync(dirname(dest), { recursive: true })
@@ -167,6 +258,31 @@ if (kit) {
167
258
  }
168
259
  }
169
260
 
261
+ // Tailwind-only overlay: replace shadcn-dependent components with plain Tailwind versions
262
+ if (ui === 'tailwind' && tailwindOnlyManifest?.files) {
263
+ for (const { stub, target } of tailwindOnlyManifest.files) {
264
+ const src = resolve(stubsDir, 'tailwind-only', kit, stub)
265
+ const dest = resolve(projectDir, target)
266
+ mkdirSync(dirname(dest), { recursive: true })
267
+ await Bun.write(dest, Bun.file(src))
268
+ fileCount++
269
+ }
270
+ }
271
+
272
+ // Theme overlay: replace pages/layouts/styles with theme-specific variants
273
+ if (ui === 'shadcn' && theme !== 'default' && kit) {
274
+ const themeManifest = manifest.themes?.[theme]?.[kit]
275
+ if (themeManifest?.files) {
276
+ for (const { stub, target } of themeManifest.files) {
277
+ const src = resolve(stubsDir, 'themes', theme, kit, stub)
278
+ const dest = resolve(projectDir, target)
279
+ mkdirSync(dirname(dest), { recursive: true })
280
+ await Bun.write(dest, Bun.file(src))
281
+ fileCount++
282
+ }
283
+ }
284
+ }
285
+
170
286
  // Shared stubs (routes, controllers, config — overwrites skeleton versions)
171
287
  if (sharedManifest?.files) {
172
288
  // Build placeholder map from manifest
@@ -178,6 +294,8 @@ if (kit) {
178
294
  }
179
295
 
180
296
  for (const { stub, target } of sharedManifest.files) {
297
+ // Skip auth-related stubs when auth === 'none'
298
+ if (auth === 'none' && authSharedTargets.has(target)) continue
181
299
  const src = resolve(stubsDir, 'shared', stub)
182
300
  const dest = resolve(projectDir, target)
183
301
  mkdirSync(dirname(dest), { recursive: true })
@@ -189,10 +307,43 @@ if (kit) {
189
307
  fileCount++
190
308
  }
191
309
  }
310
+
311
+ // Noauth overlay: replace auth-aware routes/controllers/models when auth === 'none'
312
+ if (auth === 'none') {
313
+ const noauthManifest = manifest.noauth
314
+ if (noauthManifest?.files) {
315
+ for (const { stub, target } of noauthManifest.files) {
316
+ const src = resolve(stubsDir, 'noauth', stub)
317
+ if (existsSync(src)) {
318
+ const dest = resolve(projectDir, target)
319
+ mkdirSync(dirname(dest), { recursive: true })
320
+ let content = await Bun.file(src).text()
321
+ // Apply shared placeholders
322
+ if (sharedManifest?.placeholders) {
323
+ for (const [key, values] of Object.entries(sharedManifest.placeholders)) {
324
+ const value = (values as Record<string, string>)[kit!] ?? ''
325
+ content = content.replaceAll(key, value)
326
+ }
327
+ }
328
+ await Bun.write(dest, content)
329
+ fileCount++
330
+ }
331
+ }
332
+ }
333
+ }
192
334
  }
193
335
  } else {
194
336
  // API-only: overlay token-based auth stubs
195
337
  const stubsDir = resolve(import.meta.dir, '..', 'stubs')
338
+
339
+ // Auth-related API-only stubs to skip when auth === 'none'
340
+ const authApiOnlyTargets = new Set([
341
+ 'app/Http/Controllers/ApiAuthController.ts',
342
+ 'app/Http/Requests/RegisterRequest.ts',
343
+ 'app/Http/Requests/LoginRequest.ts',
344
+ 'tests/feature/token-auth.test.ts',
345
+ ])
346
+
196
347
  const apiOnlyFiles = [
197
348
  { stub: 'api-only/routes/api.ts.stub', target: 'routes/api.ts' },
198
349
  { stub: 'shared/app/Http/Controllers/ApiAuthController.ts.stub', target: 'app/Http/Controllers/ApiAuthController.ts' },
@@ -206,6 +357,8 @@ if (kit) {
206
357
  { stub: 'api-only/tests/feature/token-auth.test.ts.stub', target: 'tests/feature/token-auth.test.ts' },
207
358
  ]
208
359
  for (const { stub, target } of apiOnlyFiles) {
360
+ // Skip auth-related stubs when auth === 'none'
361
+ if (auth === 'none' && authApiOnlyTargets.has(target)) continue
209
362
  const src = resolve(stubsDir, stub)
210
363
  if (existsSync(src)) {
211
364
  const dest = resolve(projectDir, target)
@@ -214,6 +367,26 @@ if (kit) {
214
367
  fileCount++
215
368
  }
216
369
  }
370
+
371
+ // Noauth overlay for API-only
372
+ if (auth === 'none') {
373
+ const manifestPath = resolve(stubsDir, 'manifest.json')
374
+ if (existsSync(manifestPath)) {
375
+ const manifest = JSON.parse(await Bun.file(manifestPath).text())
376
+ const noauthManifest = manifest.noauth
377
+ if (noauthManifest?.files) {
378
+ for (const { stub, target } of noauthManifest.files) {
379
+ const src = resolve(stubsDir, 'noauth', stub)
380
+ if (existsSync(src)) {
381
+ const dest = resolve(projectDir, target)
382
+ mkdirSync(dirname(dest), { recursive: true })
383
+ await Bun.write(dest, Bun.file(src))
384
+ fileCount++
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
217
390
  }
218
391
  console.log(` ${dim(`${fileCount} files created`)}`)
219
392
 
@@ -228,8 +401,8 @@ const install = Bun.spawn(['bun', 'install'], {
228
401
  await install.exited
229
402
  spin.stop('Dependencies installed')
230
403
 
231
- // ── Install shadcn components (React always uses shadcn) ─────────────────────
232
- if (kit === 'react') {
404
+ // ── Install shadcn components ────────────────────────────────────────────────
405
+ if (kit === 'react' && ui === 'shadcn') {
233
406
  const shadcnSpin = term.spinner('Installing shadcn/ui components')
234
407
 
235
408
  const run = async (args: string[]) => {
@@ -287,8 +460,11 @@ if (!noGit) {
287
460
 
288
461
  // ── Done ─────────────────────────────────────────────────────────────────────
289
462
  console.log(`\n ${emerald('✓')} ${bold(projectName)} created\n`)
290
- console.log(` ${dim('Framework')} ${kit ? bold(kit.charAt(0).toUpperCase() + kit.slice(1)) : dim('None (API only)')}`)
291
- if (kit === 'react') console.log(` ${dim('UI Kit')} ${ui === 'shadcn' ? bold('shadcn/ui') : dim('Plain Tailwind')}`)
463
+ console.log(` ${dim('Framework')} ${kit ? bold(kit.charAt(0).toUpperCase() + kit.slice(1)) : dim('Vanilla (API only)')}`)
464
+ if (kit) console.log(` ${dim('UI Kit')} ${ui === 'shadcn' ? bold('shadcn/ui') : bold('Tailwind')}`)
465
+ if (kit && ui === 'shadcn') console.log(` ${dim('Theme')} ${bold(theme.charAt(0).toUpperCase() + theme.slice(1))}`)
466
+ console.log(` ${dim('Auth')} ${auth === 'builtin' ? bold('Built-in') : dim('None')}`)
467
+ if (optionalPackages.length > 0) console.log(` ${dim('Extras')} ${bold(optionalPackages.join(', '))}`)
292
468
  console.log(`\n ${dim('Next steps:')}\n`)
293
469
  console.log(` cd ${projectName}`)
294
470
  console.log(` bun mantiq migrate`)
package/src/templates.ts CHANGED
@@ -1,8 +1,13 @@
1
+ export type Theme = 'default' | 'minimal' | 'workspace' | 'corporate' | 'starter'
2
+
1
3
  export interface TemplateContext {
2
4
  name: string
3
5
  appKey: string
4
6
  kit?: 'react' | 'vue' | 'svelte' | undefined
5
- ui?: 'shadcn' | 'none'
7
+ ui: 'shadcn' | 'tailwind'
8
+ theme: Theme
9
+ auth: 'builtin' | 'none'
10
+ optionalPackages: string[]
6
11
  }
7
12
 
8
13
  /**
@@ -21,7 +26,7 @@ export function getTemplates(ctx: TemplateContext): Record<string, string> {
21
26
 
22
27
  // ── package.json (always dynamic — name + deps) ────────────────────────
23
28
  const baseDeps: Record<string, string> = {
24
- '@mantiq/auth': '^0.5.0',
29
+ ...(ctx.auth !== 'none' ? { '@mantiq/auth': '^0.5.0' } : {}),
25
30
  '@mantiq/cli': '^0.5.0',
26
31
  '@mantiq/core': '^0.5.0',
27
32
  '@mantiq/database': '^0.5.0',
@@ -37,6 +42,7 @@ export function getTemplates(ctx: TemplateContext): Record<string, string> {
37
42
  '@mantiq/notify': '^0.5.0',
38
43
  '@mantiq/search': '^0.5.0',
39
44
  '@mantiq/health': '^0.5.0',
45
+ ...(ctx.optionalPackages.includes('ai') ? { '@mantiq/ai': '^0.5.0' } : {}),
40
46
  }
41
47
 
42
48
  const baseDevDeps: Record<string, string> = {
@@ -60,11 +66,23 @@ export function getTemplates(ctx: TemplateContext): Record<string, string> {
60
66
  ? { 'vue': '^3.5.0', '@vitejs/plugin-vue': '^6.0.0' }
61
67
  : { 'svelte': '^5.0.0', '@sveltejs/vite-plugin-svelte': '^7.0.0' }
62
68
 
69
+ const shadcnOnly = ctx.ui === 'shadcn'
70
+
63
71
  const uiDeps: Record<string, string> = ctx.kit === 'react'
64
- ? { 'clsx': '^2.1.0', 'tailwind-merge': '^2.6.0', 'class-variance-authority': '^0.7.1', 'lucide-react': '^0.577.0', 'radix-ui': '^1.4.0' }
72
+ ? {
73
+ 'clsx': '^2.1.0', 'tailwind-merge': '^2.6.0',
74
+ ...(shadcnOnly ? { 'class-variance-authority': '^0.7.1', 'lucide-react': '^0.577.0', 'radix-ui': '^1.4.0' } : {}),
75
+ }
65
76
  : ctx.kit === 'vue'
66
- ? { 'clsx': '^2.1.0', 'tailwind-merge': '^3.5.0', 'class-variance-authority': '^0.7.1', 'lucide-vue-next': '^0.577.0', 'reka-ui': '^2.9.0', 'tw-animate-css': '^1.4.0', '@tanstack/vue-table': '^8.0.0' }
67
- : { 'clsx': '^2.1.0', 'tailwind-merge': '^2.6.0', 'tailwind-variants': '^3.2.0', 'lucide-svelte': '^0.577.0', '@lucide/svelte': '^0.577.0', 'bits-ui': '^2.16.0' }
77
+ ? {
78
+ 'clsx': '^2.1.0', 'tailwind-merge': '^3.5.0',
79
+ ...(shadcnOnly ? { 'class-variance-authority': '^0.7.1', 'lucide-vue-next': '^0.577.0', 'reka-ui': '^2.9.0' } : {}),
80
+ 'tw-animate-css': '^1.4.0', '@tanstack/vue-table': '^8.0.0',
81
+ }
82
+ : {
83
+ 'clsx': '^2.1.0', 'tailwind-merge': '^2.6.0',
84
+ ...(shadcnOnly ? { 'tailwind-variants': '^3.2.0', 'lucide-svelte': '^0.577.0', '@lucide/svelte': '^0.577.0', 'bits-ui': '^2.16.0' } : {}),
85
+ }
68
86
 
69
87
  Object.assign(baseDeps, {
70
88
  '@mantiq/vite': '^0.5.0',
package/src/terminal.ts CHANGED
@@ -96,6 +96,68 @@ export class Terminal {
96
96
  })
97
97
  }
98
98
 
99
+ /** Checkbox-style multi-select prompt */
100
+ async multiSelect(label: string, options: SelectOption[]): Promise<string[]> {
101
+ let cursor = 0
102
+ const checked = new Set<number>()
103
+
104
+ const render = () => {
105
+ let out = ` ${EMERALD}◆${R} ${BOLD}${label}${R}\n`
106
+ for (let i = 0; i < options.length; i++) {
107
+ const opt = options[i]!
108
+ const active = i === cursor
109
+ const isChecked = checked.has(i)
110
+ const box = isChecked ? `${EMERALD}\u2611${R}` : `${GRAY}\u2610${R}`
111
+ const text = active ? `${WHITE}${BOLD}${opt.label}${R}` : `${GRAY}${opt.label}${R}`
112
+ const hint = opt.hint ? ` ${DIM}${opt.hint}${R}` : ''
113
+ out += ` ${box} ${text}${hint}\n`
114
+ }
115
+ out += `\n`
116
+ return out
117
+ }
118
+
119
+ const lines = options.length + 2
120
+ write(render())
121
+
122
+ if (process.stdin.isTTY) process.stdin.setRawMode(true)
123
+ write(HIDE_CURSOR)
124
+
125
+ return new Promise<string[]>((resolve) => {
126
+ const onData = (buf: Buffer) => {
127
+ const key = buf.toString()
128
+ if (key === '\x1b[A' || key === 'k') {
129
+ cursor = (cursor - 1 + options.length) % options.length
130
+ } else if (key === '\x1b[B' || key === 'j') {
131
+ cursor = (cursor + 1) % options.length
132
+ } else if (key === ' ') {
133
+ if (checked.has(cursor)) checked.delete(cursor)
134
+ else checked.add(cursor)
135
+ } else if (key === '\r' || key === '\n') {
136
+ process.stdin.removeListener('data', onData)
137
+ if (process.stdin.isTTY) process.stdin.setRawMode(false)
138
+ write(SHOW_CURSOR)
139
+ write(UP(lines) + CLEAR_LINE)
140
+ for (let i = 0; i < lines; i++) write(`${CLEAR_LINE}\n`)
141
+ write(UP(lines))
142
+ const selected = [...checked].sort().map(i => options[i]!.label)
143
+ const summary = selected.length > 0 ? selected.join(', ') : 'None'
144
+ write(` ${EMERALD}\u25C7${R} ${label} ${EMERALD}${summary}${R}\n\n`)
145
+ resolve([...checked].sort().map(i => options[i]!.value))
146
+ return
147
+ } else if (key === '\x03') {
148
+ process.stdin.removeListener('data', onData)
149
+ if (process.stdin.isTTY) process.stdin.setRawMode(false)
150
+ write(SHOW_CURSOR + '\n')
151
+ process.exit(0)
152
+ }
153
+ write(UP(lines))
154
+ write(render())
155
+ }
156
+ process.stdin.on('data', onData)
157
+ process.stdin.resume()
158
+ })
159
+ }
160
+
99
161
  /** Yes/No confirm prompt */
100
162
  async confirm(label: string, defaultVal = false): Promise<boolean> {
101
163
  const options: SelectOption[] = [
@@ -0,0 +1,57 @@
1
+ import type { MantiqRequest } from '@mantiq/core'
2
+ import { json, hash, hashCheck, abort } from '@mantiq/core'
3
+ import { auth } from '@mantiq/auth'
4
+ import { User } from '../../Models/User.ts'
5
+
6
+ /**
7
+ * Sanctum-style token authentication for API-only apps.
8
+ * Issues bearer tokens instead of session cookies.
9
+ */
10
+ export class ApiAuthController {
11
+ async register(request: MantiqRequest, data: Record<string, any>): Promise<Response> {
12
+ if (await User.where('email', data.email).first()) abort(422, 'A user with this email already exists.')
13
+
14
+ const user = await User.create({
15
+ name: data.name,
16
+ email: data.email,
17
+ password: await hash(data.password),
18
+ })
19
+
20
+ const { plainTextToken } = await user.createToken(data.device_name ?? 'api')
21
+
22
+ return json({ message: 'Registered.', user: user.toObject(), token: plainTextToken }, 201)
23
+ }
24
+
25
+ async login(request: MantiqRequest, data: Record<string, any>): Promise<Response> {
26
+ const user = await User.where('email', data.email).first()
27
+ if (!user || !(await hashCheck(data.password, user.getAuthPassword()))) {
28
+ abort(401, 'Invalid credentials.')
29
+ }
30
+
31
+ const { plainTextToken } = await user!.createToken(data.device_name ?? 'api')
32
+
33
+ return json({ message: 'Logged in.', user: user!.toObject(), token: plainTextToken })
34
+ }
35
+
36
+ async logout(request: MantiqRequest): Promise<Response> {
37
+ const manager = auth()
38
+ manager.setRequest(request)
39
+ const user = await manager.guard('api').user()
40
+
41
+ if (user) {
42
+ const token = user.currentAccessToken?.()
43
+ if (token) await token.delete()
44
+ }
45
+
46
+ return json({ message: 'Token revoked.' })
47
+ }
48
+
49
+ async user(request: MantiqRequest): Promise<Response> {
50
+ const manager = auth()
51
+ manager.setRequest(request)
52
+ const user = await manager.guard('api').user()
53
+ if (!user) abort(401, 'Unauthenticated.')
54
+
55
+ return json({ user: user!.toObject() })
56
+ }
57
+ }
@@ -0,0 +1,24 @@
1
+ import type { Router } from '@mantiq/core'
2
+ import { json } from '@mantiq/core'
3
+ import { ApiAuthController } from '../app/Http/Controllers/ApiAuthController.ts'
4
+ import { UserController } from '../app/Http/Controllers/UserController.ts'
5
+ import { RegisterRequest } from '../app/Http/Requests/RegisterRequest.ts'
6
+ import { LoginRequest } from '../app/Http/Requests/LoginRequest.ts'
7
+ import { StoreUserRequest } from '../app/Http/Requests/StoreUserRequest.ts'
8
+ import { UpdateUserRequest } from '../app/Http/Requests/UpdateUserRequest.ts'
9
+
10
+ export default function (router: Router) {
11
+ router.get('/ping', () => json({ status: 'ok', timestamp: new Date().toISOString() }))
12
+
13
+ // Token auth — FormRequest auto-validates, controller receives validated data
14
+ router.post('/register', [ApiAuthController, 'register', RegisterRequest])
15
+ router.post('/login', [ApiAuthController, 'login', LoginRequest])
16
+ router.post('/logout', [ApiAuthController, 'logout']).middleware('auth:api')
17
+ router.get('/user', [ApiAuthController, 'user']).middleware('auth:api')
18
+
19
+ // Users CRUD (protected)
20
+ router.get('/users', [UserController, 'index']).middleware('auth:api')
21
+ router.post('/users', [UserController, 'store', StoreUserRequest]).middleware('auth:api')
22
+ router.put('/users/:id', [UserController, 'update', UpdateUserRequest]).middleware('auth:api')
23
+ router.delete('/users/:id', [UserController, 'destroy']).middleware('auth:api')
24
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { TestCase } from '@mantiq/testing'
3
+
4
+ const t = new TestCase()
5
+ t.refreshDatabase = true
6
+ t.setup()
7
+
8
+ const user = {
9
+ name: 'API User',
10
+ email: 'api@example.com',
11
+ password: 'password123',
12
+ }
13
+
14
+ describe('Token Authentication', () => {
15
+ test('can register and receive a token', async () => {
16
+ const res = await t.client.post('/api/register', user)
17
+ res.assertCreated()
18
+ await res.assertJsonHasKey('token')
19
+ const data = await res.json()
20
+ expect(data.token).toContain('|')
21
+ })
22
+
23
+ test('can login and receive a token', async () => {
24
+ await t.client.post('/api/register', user)
25
+ const res = await t.client.post('/api/login', {
26
+ email: user.email,
27
+ password: user.password,
28
+ })
29
+ res.assertOk()
30
+ await res.assertJsonHasKey('token')
31
+ })
32
+
33
+ test('cannot login with wrong credentials', async () => {
34
+ await t.client.post('/api/register', user)
35
+ const res = await t.client.post('/api/login', {
36
+ email: user.email,
37
+ password: 'wrong',
38
+ })
39
+ res.assertUnauthorized()
40
+ })
41
+
42
+ test('can access protected route with bearer token', async () => {
43
+ const regRes = await t.client.post('/api/register', user)
44
+ const { token } = await regRes.json()
45
+
46
+ t.client.withToken(token)
47
+ const res = await t.client.get('/api/user')
48
+ res.assertOk()
49
+ await res.assertJsonPath('user.email', user.email)
50
+ })
51
+
52
+ test('cannot access protected route without token', async () => {
53
+ const res = await t.client.get('/api/user')
54
+ res.assertUnauthorized()
55
+ })
56
+
57
+ test('can logout (revoke token)', async () => {
58
+ const regRes = await t.client.post('/api/register', user)
59
+ const { token } = await regRes.json()
60
+
61
+ t.client.withToken(token)
62
+ const logoutRes = await t.client.post('/api/logout')
63
+ logoutRes.assertOk()
64
+
65
+ // Token should be revoked
66
+ const userRes = await t.client.get('/api/user')
67
+ userRes.assertUnauthorized()
68
+ })
69
+ })
@@ -0,0 +1,10 @@
1
+ import { FormRequest } from '@mantiq/validation'
2
+
3
+ export class LoginRequest extends FormRequest {
4
+ override rules() {
5
+ return {
6
+ email: 'required|email',
7
+ password: 'required|string',
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,11 @@
1
+ import { FormRequest } from '@mantiq/validation'
2
+
3
+ export class RegisterRequest extends FormRequest {
4
+ override rules() {
5
+ return {
6
+ name: 'required|string|max:255',
7
+ email: 'required|email|max:255',
8
+ password: 'required|string|min:6',
9
+ }
10
+ }
11
+ }