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.
- package/bin/aiplang.js +358 -49
- 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.
|
|
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]
|
|
38
|
-
npx aiplang init --template saas|landing|crud
|
|
39
|
-
npx aiplang
|
|
40
|
-
npx aiplang
|
|
41
|
-
npx aiplang
|
|
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
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
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{
|
|
69
|
-
hero{Ship faster with AI|Zero config, infinite scale.>/signup:Start free>/demo:View demo} animate:
|
|
70
|
-
stats{@stats
|
|
71
|
-
row3{rocket>Deploy instantly>Push to git, live in
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
sect{
|
|
90
|
-
|
|
91
|
-
foot{
|
|
92
|
-
|
|
93
|
-
|
|
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{
|
|
96
|
-
hero{Welcome to
|
|
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{©
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|