aiplang 2.2.0 → 2.3.0

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 (2) hide show
  1. package/bin/aiplang.js +358 -49
  2. package/package.json +1 -1
package/bin/aiplang.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs')
5
5
  const path = require('path')
6
6
  const http = require('http')
7
7
 
8
- const VERSION = '2.2.0'
8
+ const VERSION = '2.3.0'
9
9
  const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
10
10
  const cmd = process.argv[2]
11
11
  const args = process.argv.slice(3)
@@ -34,17 +34,32 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
34
34
  AI-first web language — full apps in ~20 lines.
35
35
 
36
36
  Usage:
37
- npx aiplang init [name] create project
38
- npx aiplang init --template saas|landing|crud
39
- npx aiplang serve [dir] dev server + hot reload
40
- npx aiplang build [dir/file] compile static HTML
41
- npx aiplang new <page> new page template
37
+ npx aiplang init [name] create project (default template)
38
+ npx aiplang init [name] --template <t> use template: saas|landing|crud|dashboard|portfolio|blog
39
+ npx aiplang init [name] --template ./my.flux use a local .flux file as template
40
+ npx aiplang init [name] --template my-custom use a saved custom template
41
+ npx aiplang serve [dir] dev server + hot reload
42
+ npx aiplang build [dir/file] compile → static HTML
43
+ npx aiplang new <page> new page template
42
44
  npx aiplang --version
43
45
 
44
46
  Full-stack:
45
47
  npx aiplang start app.flux start full-stack server (API + DB + frontend)
46
48
  PORT=8080 aiplang start app.flux custom port
47
49
 
50
+ Templates:
51
+ npx aiplang template list list all templates (built-in + custom)
52
+ npx aiplang template save <n> save current project as template
53
+ npx aiplang template save <n> --from <f> save a specific .flux file as template
54
+ npx aiplang template edit <n> open template in editor
55
+ npx aiplang template show <n> print template source
56
+ npx aiplang template export <n> export template to .flux file
57
+ npx aiplang template remove <n> delete a custom template
58
+
59
+ Custom template variables:
60
+ {{name}} project name
61
+ {{year}} current year
62
+
48
63
  Customization:
49
64
  ~theme accent=#7c3aed radius=1.5rem font=Syne bg=#000 text=#fff
50
65
  hero{...} animate:fade-up
@@ -59,66 +74,360 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
59
74
  }
60
75
  if (cmd==='--version'||cmd==='-v') { console.log(`aiplang v${VERSION}`); process.exit(0) }
61
76
 
62
- // ── Templates ────────────────────────────────────────────────────
63
- const TEMPLATES = {
64
- saas: (n,y) => `# ${n}
77
+ // ─────────────────────────────────────────────────────────────────
78
+ // TEMPLATE SYSTEM
79
+ // Custom templates stored at ~/.aiplang/templates/<name>.flux
80
+ // ─────────────────────────────────────────────────────────────────
81
+
82
+ const TEMPLATES_DIR = path.join(require('os').homedir(), '.aiplang', 'templates')
83
+
84
+ function ensureTemplatesDir() {
85
+ if (!fs.existsSync(TEMPLATES_DIR)) fs.mkdirSync(TEMPLATES_DIR, { recursive: true })
86
+ }
87
+
88
+ // Built-in templates (interpolate {{name}} and {{year}})
89
+ const BUILTIN_TEMPLATES = {
90
+ saas: `# {{name}}
91
+ ~db sqlite ./app.db
92
+ ~auth jwt $JWT_SECRET expire=7d
93
+ ~admin /admin
94
+
95
+ model User {
96
+ id : uuid : pk auto
97
+ name : text : required
98
+ email : text : required unique
99
+ password : text : required hashed
100
+ plan : enum : free,starter,pro : default=free
101
+ role : enum : user,admin : default=user
102
+ ~soft-delete
103
+ }
104
+
105
+ api POST /api/auth/register {
106
+ ~validate name required | email required email | password min=8
107
+ ~unique User email $body.email | 409
108
+ ~hash password
109
+ insert User($body)
110
+ return jwt($inserted) 201
111
+ }
112
+
113
+ api POST /api/auth/login {
114
+ $user = User.findBy(email=$body.email)
115
+ ~check password $body.password $user.password | 401
116
+ return jwt($user) 200
117
+ }
118
+
119
+ api GET /api/me {
120
+ ~guard auth
121
+ return $auth.user
122
+ }
123
+
124
+ api GET /api/stats {
125
+ return User.count()
126
+ }
127
+
65
128
  %home dark /
66
129
  @stats = {}
67
130
  ~mount GET /api/stats => @stats
68
- nav{${n}>/features:Features>/pricing:Pricing>/login:Sign in}
69
- hero{Ship faster with AI|Zero config, infinite scale.>/signup:Start free>/demo:View demo} animate:fade-up
70
- stats{@stats.users:Users|@stats.mrr:MRR|@stats.uptime:Uptime}
71
- row3{rocket>Deploy instantly>Push to git, live in 3 seconds.|shield>Enterprise ready>SOC2, GDPR, SSO built-in.|chart>Full observability>Real-time errors and performance.} animate:stagger
72
- testimonial{Sarah Chen, CEO @ Acme|"Cut deployment time by 90%. Absolutely game-changing."|img:https://i.pravatar.cc/64?img=47}
73
- foot{© ${y} ${n}>/privacy:Privacy>/terms:Terms}`,
74
-
75
- landing: (n,y) => `# ${n}
76
- %home dark /
77
- nav{${n}>/about:About>/login:Sign in}
78
- hero{The future is now|${n} — built for the next generation.>/signup:Get started for free} animate:blur-in
79
- row3{rocket>Fast>Zero config, instant results.|bolt>Simple>One command to deploy.|globe>Global>CDN in 180+ countries.}
80
- foot{© ${y} ${n}}`,
131
+ nav{{{name}}>/pricing:Pricing>/login:Sign in}
132
+ hero{Ship faster with AI|Zero config, infinite scale.>/signup:Start free>/demo:View demo} animate:blur-in
133
+ stats{@stats:Users|99.9%:Uptime|$0:Start free}
134
+ row3{rocket>Deploy instantly>Push to git, live in seconds.|shield>Enterprise ready>SOC2, GDPR built-in.|chart>Full observability>Real-time errors.} animate:stagger
135
+ pricing{Free>$0/mo>3 projects>/signup:Get started|Pro>$29/mo>Unlimited>/signup:Start trial|Enterprise>Custom>SSO + SLA>/contact:Talk}
136
+ testimonial{Sarah Chen, CEO @ Acme|"Cut deployment time by 90%."|img:https://i.pravatar.cc/64?img=47} animate:fade-up
137
+ foot{© {{year}} {{name}}>/privacy:Privacy>/terms:Terms}
138
+
139
+ ---
140
+
141
+ %login dark /login
142
+ nav{{{name}}>/signup:Create account}
143
+ hero{Welcome back|Sign in to continue.}
144
+ form POST /api/auth/login => redirect /dashboard { Email:email | Password:password }
145
+ foot{© {{year}} {{name}}}
81
146
 
82
- crud: (n,y) => `# ${n}
83
- %users dark /users
147
+ ---
148
+
149
+ %signup dark /signup
150
+ nav{{{name}}>/login:Sign in}
151
+ hero{Start for free|No credit card required.}
152
+ form POST /api/auth/register => redirect /dashboard { Name:text | Email:email | Password:password }
153
+ foot{© {{year}} {{name}}}
154
+
155
+ ---
156
+
157
+ %dashboard dark /dashboard
84
158
  @users = []
159
+ @stats = {}
85
160
  ~mount GET /api/users => @users
86
- nav{${n}>/users:Users>/settings:Settings}
87
- sect{User Management}
88
- table @users { Name:name | Email:email | Plan:plan | Status:status | edit PUT /api/users/{id} | delete /api/users/{id} | empty: No users yet. }
89
- sect{Add User}
90
- form POST /api/users => @users.push($result) { Full name:text:Alice Johnson | Email:email:alice@company.com | Plan:select:starter,pro,enterprise }
91
- foot{© ${y} ${n}}`,
92
-
93
- default: (n,y) => `# ${n}
161
+ ~mount GET /api/stats => @stats
162
+ nav{{{name}}>/logout:Sign out}
163
+ stats{@stats:Total users}
164
+ sect{Users}
165
+ table @users { Name:name | Email:email | Plan:plan | edit PUT /api/users/{id} | delete /api/users/{id} | empty: No users yet. }
166
+ foot{{{name}} Dashboard}`,
167
+
168
+ landing: `# {{name}}
169
+ %home dark /
170
+ nav{{{name}}>/about:About>/contact:Contact}
171
+ hero{The future is now|{{name}} — built for the next generation.>/signup:Get started for free} animate:blur-in
172
+ row3{rocket>Fast>Zero config, instant results.|bolt>Simple>One command to deploy.|globe>Global>CDN in 180+ countries.}
173
+ foot{© {{year}} {{name}}}`,
174
+
175
+ crud: `# {{name}}
176
+ %items dark /
177
+ @items = []
178
+ ~mount GET /api/items => @items
179
+ nav{{{name}}>/items:Items>/settings:Settings}
180
+ sect{Manage Items}
181
+ table @items { Name:name | Status:status | edit PUT /api/items/{id} | delete /api/items/{id} | empty: No items yet. }
182
+ form POST /api/items => @items.push($result) { Name:text:Item name | Status:select:active,inactive }
183
+ foot{© {{year}} {{name}}}`,
184
+
185
+ blog: `# {{name}}
186
+ %home dark /
187
+ @posts = []
188
+ ~mount GET /api/posts => @posts
189
+ nav{{{name}}>/about:About}
190
+ hero{{{name}}|A blog about things that matter.} animate:fade-up
191
+ table @posts { Title:title | Date:created_at | empty: No posts yet. }
192
+ foot{© {{year}} {{name}}}`,
193
+
194
+ portfolio: `# {{name}}
195
+ ~theme accent=#f59e0b radius=2rem font=Syne bg=#0c0a09 text=#fafaf9
196
+
197
+ %home dark /
198
+ nav{{{name}}>/work:Work>/contact:Contact}
199
+ hero{Design & code.|Creative work for bold brands.>/work:See my work} animate:blur-in
200
+ row3{globe>10+ countries>Clients from 3 continents.|star>50+ projects>From startups to Fortune 500.|check>On time>98% on-schedule delivery.} animate:stagger
201
+ gallery{https://images.unsplash.com/photo-1518770660439?w=600|https://images.unsplash.com/photo-1561070791-2526d30994b5?w=600|https://images.unsplash.com/photo-1558655146?w=600}
202
+ testimonial{Marco Rossi, CEO|"Exceptional work from start to finish."|img:https://i.pravatar.cc/64?img=11}
203
+ foot{© {{year}} {{name}}>/github:GitHub>/linkedin:LinkedIn}`,
204
+
205
+ dashboard: `# {{name}}
206
+ %main dark /
207
+ @stats = {}
208
+ @items = []
209
+ ~mount GET /api/stats => @stats
210
+ ~mount GET /api/items => @items
211
+ ~interval 30000 GET /api/stats => @stats
212
+ nav{{{name}}>/logout:Sign out}
213
+ stats{@stats.total:Total|@stats.active:Active|@stats.revenue:Revenue}
214
+ sect{Recent Items}
215
+ table @items { Name:name | Status:status | Date:created_at | edit PUT /api/items/{id} | delete /api/items/{id} | empty: No data. }
216
+ sect{Add Item}
217
+ form POST /api/items => @items.push($result) { Name:text | Status:select:active,inactive }
218
+ foot{{{name}}}`,
219
+
220
+ default: `# {{name}}
94
221
  %home dark /
95
- nav{${n}>/login:Sign in}
96
- hero{Welcome to ${n}|Edit pages/home.flux to get started.>/signup:Get started} animate:fade-up
222
+ nav{{{name}}>/login:Sign in}
223
+ hero{Welcome to {{name}}|Edit pages/home.flux to get started.>/signup:Get started} animate:fade-up
97
224
  row3{rocket>Fast>Renders in under 1ms.|bolt>AI-native>Written by Claude in seconds.|globe>Deploy anywhere>Static files. Any host.}
98
- foot{© ${y} ${n}}`,
225
+ foot{© {{year}} {{name}}}`,
226
+ }
227
+
228
+ function applyTemplateVars(src, name, year) {
229
+ return src.replace(/\{\{name\}\}/g, name).replace(/\{\{year\}\}/g, year)
230
+ }
231
+
232
+ function getTemplate(tplName, name, year) {
233
+ ensureTemplatesDir()
234
+
235
+ // 1. Local file path: --template ./my-template.flux or --template /abs/path.flux
236
+ if (tplName.startsWith('./') || tplName.startsWith('../') || tplName.startsWith('/')) {
237
+ const full = path.resolve(tplName)
238
+ if (!fs.existsSync(full)) { console.error(`\n ✗ Template file not found: ${full}\n`); process.exit(1) }
239
+ return applyTemplateVars(fs.readFileSync(full, 'utf8'), name, year)
240
+ }
241
+
242
+ // 2. User custom template: ~/.aiplang/templates/<name>.flux
243
+ const customPath = path.join(TEMPLATES_DIR, tplName + '.flux')
244
+ if (fs.existsSync(customPath)) {
245
+ return applyTemplateVars(fs.readFileSync(customPath, 'utf8'), name, year)
246
+ }
247
+
248
+ // 3. Built-in template
249
+ const builtin = BUILTIN_TEMPLATES[tplName]
250
+ if (builtin) return applyTemplateVars(builtin, name, year)
251
+
252
+ // Not found — show what's available
253
+ const customs = fs.existsSync(TEMPLATES_DIR)
254
+ ? fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.flux')).map(f=>f.replace('.flux',''))
255
+ : []
256
+ const all = [...Object.keys(BUILTIN_TEMPLATES).filter(k=>k!=='default'), ...customs]
257
+ console.error(`\n ✗ Template "${tplName}" not found.\n Available: ${all.join(', ')}\n`)
258
+ process.exit(1)
259
+ }
260
+
261
+ function listTemplates() {
262
+ ensureTemplatesDir()
263
+ const builtins = Object.keys(BUILTIN_TEMPLATES).filter(k=>k!=='default')
264
+ const customs = fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.flux')).map(f=>f.replace('.flux',''))
265
+ console.log(`\n aiplang templates\n`)
266
+ console.log(` Built-in:`)
267
+ builtins.forEach(t => console.log(` ${t}`))
268
+ if (customs.length) {
269
+ console.log(`\n Custom (${TEMPLATES_DIR}):`)
270
+ customs.forEach(t => console.log(` ${t} ✓`))
271
+ } else {
272
+ console.log(`\n Custom: (none yet — use "aiplang template save <name>" to create one)`)
273
+ }
274
+ console.log()
275
+ }
276
+
277
+ // ── template subcommand ──────────────────────────────────────────
278
+ if (cmd === 'template') {
279
+ const sub = args[0]
280
+ ensureTemplatesDir()
281
+
282
+ // aiplang template list
283
+ if (!sub || sub === 'list' || sub === 'ls') {
284
+ listTemplates(); process.exit(0)
285
+ }
286
+
287
+ // aiplang template save <name> [--from <file>]
288
+ if (sub === 'save' || sub === 'add') {
289
+ const tname = args[1]
290
+ if (!tname) { console.error('\n ✗ Usage: aiplang template save <name> [--from <file>]\n'); process.exit(1) }
291
+ const fromIdx = args.indexOf('--from')
292
+ let src
293
+ if (fromIdx !== -1 && args[fromIdx+1]) {
294
+ const fp = path.resolve(args[fromIdx+1])
295
+ if (!fs.existsSync(fp)) { console.error(`\n ✗ File not found: ${fp}\n`); process.exit(1) }
296
+ src = fs.readFileSync(fp, 'utf8')
297
+ } else {
298
+ // Auto-detect: use pages/ directory or app.flux
299
+ const sources = ['pages', 'app.flux', 'index.flux']
300
+ const found = sources.find(s => fs.existsSync(s))
301
+ if (!found) { console.error('\n ✗ No .flux files found. Use --from <file> to specify source.\n'); process.exit(1) }
302
+ if (fs.statSync(found).isDirectory()) {
303
+ src = fs.readdirSync(found).filter(f=>f.endsWith('.flux'))
304
+ .map(f => fs.readFileSync(path.join(found,f),'utf8')).join('\n---\n')
305
+ } else {
306
+ src = fs.readFileSync(found, 'utf8')
307
+ }
308
+ }
309
+ const dest = path.join(TEMPLATES_DIR, tname + '.flux')
310
+ fs.writeFileSync(dest, src)
311
+ console.log(`\n ✓ Template saved: ${tname}\n ${dest}\n\n Use it: aiplang init my-app --template ${tname}\n`)
312
+ process.exit(0)
313
+ }
314
+
315
+ // aiplang template remove <name>
316
+ if (sub === 'remove' || sub === 'rm' || sub === 'delete') {
317
+ const tname = args[1]
318
+ if (!tname) { console.error('\n ✗ Usage: aiplang template remove <name>\n'); process.exit(1) }
319
+ const dest = path.join(TEMPLATES_DIR, tname + '.flux')
320
+ if (!fs.existsSync(dest)) { console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1) }
321
+ fs.unlinkSync(dest)
322
+ console.log(`\n ✓ Removed template: ${tname}\n`); process.exit(0)
323
+ }
324
+
325
+ // aiplang template edit <name>
326
+ if (sub === 'edit' || sub === 'open') {
327
+ const tname = args[1]
328
+ if (!tname) { console.error('\n ✗ Usage: aiplang template edit <name>\n'); process.exit(1) }
329
+ let dest = path.join(TEMPLATES_DIR, tname + '.flux')
330
+ if (!fs.existsSync(dest)) {
331
+ // create from built-in if exists
332
+ const builtin = BUILTIN_TEMPLATES[tname]
333
+ if (builtin) { fs.writeFileSync(dest, builtin); console.log(`\n ✓ Copied built-in "${tname}" to custom templates.\n`) }
334
+ else { console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1) }
335
+ }
336
+ const editor = process.env.EDITOR || process.env.VISUAL || 'code'
337
+ try { require('child_process').spawnSync(editor, [dest], { stdio: 'inherit' }) }
338
+ catch { console.log(`\n Template path: ${dest}\n Open it in your editor.\n`) }
339
+ process.exit(0)
340
+ }
341
+
342
+ // aiplang template show <name>
343
+ if (sub === 'show' || sub === 'cat') {
344
+ const tname = args[1] || 'default'
345
+ const customPath = path.join(TEMPLATES_DIR, tname + '.flux')
346
+ if (fs.existsSync(customPath)) { console.log(fs.readFileSync(customPath,'utf8')); process.exit(0) }
347
+ const builtin = BUILTIN_TEMPLATES[tname]
348
+ if (builtin) { console.log(builtin); process.exit(0) }
349
+ console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1)
350
+ }
351
+
352
+ // aiplang template export <name> [--out <file>]
353
+ if (sub === 'export') {
354
+ const tname = args[1]
355
+ if (!tname) { console.error('\n ✗ Usage: aiplang template export <name>\n'); process.exit(1) }
356
+ const outIdx = args.indexOf('--out')
357
+ const outFile = outIdx !== -1 ? args[outIdx+1] : `./${tname}.flux`
358
+ const customPath = path.join(TEMPLATES_DIR, tname + '.flux')
359
+ const src = fs.existsSync(customPath) ? fs.readFileSync(customPath,'utf8') : BUILTIN_TEMPLATES[tname]
360
+ if (!src) { console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1) }
361
+ fs.writeFileSync(outFile, src)
362
+ console.log(`\n ✓ Exported "${tname}" → ${outFile}\n`)
363
+ process.exit(0)
364
+ }
365
+
366
+ console.error(`\n ✗ Unknown template command: ${sub}\n Commands: list, save, remove, edit, show, export\n`)
367
+ process.exit(1)
99
368
  }
100
369
 
101
370
  // ── Init ─────────────────────────────────────────────────────────
102
371
  if (cmd==='init') {
103
372
  const tplIdx = args.indexOf('--template')
104
- const tplName = tplIdx!==-1 ? args[tplIdx+1] : 'default'
373
+ const tplName = tplIdx !== -1 ? args[tplIdx+1] : 'default'
105
374
  const name = args.find(a=>!a.startsWith('--')&&a!==tplName)||'aiplang-app'
106
375
  const dir = path.resolve(name), year = new Date().getFullYear()
376
+
107
377
  if (fs.existsSync(dir)) { console.error(`\n ✗ Directory "${name}" already exists.\n`); process.exit(1) }
108
- fs.mkdirSync(path.join(dir,'pages'), {recursive:true})
109
- fs.mkdirSync(path.join(dir,'public'), {recursive:true})
110
- for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
111
- const src=path.join(RUNTIME_DIR,f); if(fs.existsSync(src)) fs.copyFileSync(src,path.join(dir,'public',f))
378
+
379
+ // Get template source (built-in, custom, or file path)
380
+ const tplSrc = getTemplate(tplName, name, year)
381
+
382
+ // Check if template has full-stack backend (models/api blocks)
383
+ const isFullStack = tplSrc.includes('\nmodel ') || tplSrc.includes('\napi ')
384
+ const isMultiFile = tplSrc.includes('\n---\n')
385
+
386
+ if (isFullStack) {
387
+ // Full-stack project: single app.flux
388
+ fs.mkdirSync(dir, { recursive: true })
389
+ fs.writeFileSync(path.join(dir, 'app.flux'), tplSrc)
390
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
391
+ name, version:'0.1.0',
392
+ scripts: { dev: 'npx aiplang start app.flux', start: 'npx aiplang start app.flux' },
393
+ devDependencies: { 'aiplang': `^${VERSION}` }
394
+ }, null, 2))
395
+ fs.writeFileSync(path.join(dir, '.env.example'), 'JWT_SECRET=change-me-in-production\n# STRIPE_SECRET_KEY=sk_test_...\n# AWS_ACCESS_KEY_ID=...\n# AWS_SECRET_ACCESS_KEY=...\n# S3_BUCKET=...\n')
396
+ fs.writeFileSync(path.join(dir, '.gitignore'), '*.db\nnode_modules/\ndist/\n.env\nuploads/\n')
397
+ fs.writeFileSync(path.join(dir, 'README.md'), `# ${name}\n\nGenerated with [aiplang](https://npmjs.com/package/aiplang) v${VERSION}\n\n## Run\n\n\`\`\`bash\nnpx aiplang start app.flux\n\`\`\`\n`)
398
+ const label = tplName !== 'default' ? ` (template: ${tplName})` : ''
399
+ console.log(`\n ✓ Created ${name}/${label}\n\n app.flux ← full-stack app (backend + frontend)\n\n Next:\n cd ${name} && npx aiplang start app.flux\n`)
400
+ } else if (isMultiFile) {
401
+ // Multi-page SSG project: pages/*.flux
402
+ fs.mkdirSync(path.join(dir,'pages'), {recursive:true})
403
+ fs.mkdirSync(path.join(dir,'public'), {recursive:true})
404
+ for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
405
+ const src=path.join(RUNTIME_DIR,f); if(fs.existsSync(src)) fs.copyFileSync(src,path.join(dir,'public',f))
406
+ }
407
+ const pageBlocks = tplSrc.split('\n---\n')
408
+ pageBlocks.forEach((block, i) => {
409
+ const m = block.match(/^%([a-zA-Z0-9_-]+)/m)
410
+ const pageName = m ? m[1] : (i === 0 ? 'home' : `page${i}`)
411
+ fs.writeFileSync(path.join(dir,'pages',`${pageName}.flux`), block.trim())
412
+ })
413
+ fs.writeFileSync(path.join(dir,'package.json'), JSON.stringify({name,version:'0.1.0',scripts:{dev:'npx aiplang serve',build:'npx aiplang build pages/ --out dist/'},devDependencies:{'aiplang':`^${VERSION}`}},null,2))
414
+ fs.writeFileSync(path.join(dir,'.gitignore'),'dist/\nnode_modules/\n')
415
+ const label = tplName !== 'default' ? ` (template: ${tplName})` : ''
416
+ const files = fs.readdirSync(path.join(dir,'pages')).map(f=>f).join(', ')
417
+ console.log(`\n ✓ Created ${name}/${label}\n\n pages/{${files}} ← edit these\n\n Next:\n cd ${name} && npx aiplang serve\n`)
418
+ } else {
419
+ // Single-page SSG project
420
+ fs.mkdirSync(path.join(dir,'pages'), {recursive:true})
421
+ fs.mkdirSync(path.join(dir,'public'), {recursive:true})
422
+ for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
423
+ const src=path.join(RUNTIME_DIR,f); if(fs.existsSync(src)) fs.copyFileSync(src,path.join(dir,'public',f))
424
+ }
425
+ fs.writeFileSync(path.join(dir,'pages','home.flux'), tplSrc)
426
+ fs.writeFileSync(path.join(dir,'package.json'), JSON.stringify({name,version:'0.1.0',scripts:{dev:'npx aiplang serve',build:'npx aiplang build pages/ --out dist/'},devDependencies:{'aiplang':`^${VERSION}`}},null,2))
427
+ fs.writeFileSync(path.join(dir,'.gitignore'),'dist/\nnode_modules/\n')
428
+ const label = tplName !== 'default' ? ` (template: ${tplName})` : ''
429
+ console.log(`\n ✓ Created ${name}/${label}\n\n pages/home.flux ← edit this\n\n Next:\n cd ${name} && npx aiplang serve\n`)
112
430
  }
113
- fs.writeFileSync(path.join(dir,'public','index.html'),`<!DOCTYPE html>
114
- <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${name}</title>
115
- <meta http-equiv="refresh" content="0; url=/">
116
- </head><body><p>Run <code>npx aiplang serve</code> to start the dev server.</p></body></html>`)
117
- fs.writeFileSync(path.join(dir,'pages','home.flux'), (TEMPLATES[tplName]||TEMPLATES.default)(name, year))
118
- fs.writeFileSync(path.join(dir,'package.json'), JSON.stringify({name,version:'0.1.0',scripts:{dev:'npx aiplang serve',build:'npx aiplang build pages/ --out dist/'},devDependencies:{'aiplang':`^${VERSION}`}},null,2))
119
- fs.writeFileSync(path.join(dir,'.gitignore'),'dist/\nnode_modules/\n')
120
- const label = tplName!=='default' ? ` (template: ${tplName})` : ''
121
- console.log(`\n ✓ Created ${name}/${label}\n\n pages/home.flux ← edit this\n\n Next:\n cd ${name} && npx aiplang serve\n`)
122
431
  process.exit(0)
123
432
  }
124
433
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "AI-first full-stack language. Frontend + Backend + DB + Auth in one file. Competes with Laravel.",
5
5
  "keywords": [
6
6
  "aiplang",