aiplang 2.6.0 → 2.6.2
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/README.md +5 -5
- package/aiplang-knowledge.md +86 -326
- package/bin/aiplang.js +43 -43
- package/package.json +5 -5
- package/runtime/aiplang-runtime.js +49 -52
- package/server/server.js +22 -16
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ cd my-app
|
|
|
8
8
|
npx aiplang serve
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Ask Claude to generate a page → paste into `pages/home.
|
|
11
|
+
Ask Claude to generate a page → paste into `pages/home.aip` → see it live.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -16,9 +16,9 @@ Ask Claude to generate a page → paste into `pages/home.aiplang` → see it liv
|
|
|
16
16
|
|
|
17
17
|
**aiplang** is a web language designed to be generated by AI (Claude), not written by humans.
|
|
18
18
|
|
|
19
|
-
A single `.
|
|
19
|
+
A single `.aip` file describes a complete app: frontend, backend, database, auth, email, jobs.
|
|
20
20
|
|
|
21
|
-
```
|
|
21
|
+
```aip
|
|
22
22
|
~db sqlite ./app.db
|
|
23
23
|
~auth jwt $JWT_SECRET expire=7d
|
|
24
24
|
~admin /admin
|
|
@@ -83,7 +83,7 @@ npx aiplang init --template landing # landing page template
|
|
|
83
83
|
npx aiplang init --template crud # CRUD app template
|
|
84
84
|
npx aiplang serve # dev server + hot reload → localhost:3000
|
|
85
85
|
npx aiplang build pages/ --out dist/ # compile → static HTML
|
|
86
|
-
npx aiplang start app.
|
|
86
|
+
npx aiplang start app.aip # full-stack server (Node.js)
|
|
87
87
|
npx aiplang new dashboard # create new page template
|
|
88
88
|
```
|
|
89
89
|
|
|
@@ -117,7 +117,7 @@ aiplang/
|
|
|
117
117
|
│ ├── server/server.js ← full-stack Node.js server
|
|
118
118
|
│ └── aiplang-knowledge.md ← Claude Project knowledge file
|
|
119
119
|
├── aiplang-go/ ← Go compiler + server (v2)
|
|
120
|
-
│ ├── compiler/compiler.go ← .
|
|
120
|
+
│ ├── compiler/compiler.go ← .aip → AST parser
|
|
121
121
|
│ ├── server/server.go ← Go HTTP server
|
|
122
122
|
│ └── cmd/aiplangd/main.go ← binary entrypoint
|
|
123
123
|
├── docs/ ← GitHub Pages (aiplang.io)
|
package/aiplang-knowledge.md
CHANGED
|
@@ -1,338 +1,98 @@
|
|
|
1
|
-
# aiplang —
|
|
1
|
+
# aiplang — syntax reference
|
|
2
|
+
File extension: `.aip` (e.g. `app.aip`, `home.aip`)
|
|
3
|
+
Generate ONLY aiplang code. No React, HTML, or other frameworks. No explanations unless asked.
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## aiplang syntax reference
|
|
8
|
-
|
|
9
|
-
### File structure
|
|
10
|
-
```
|
|
11
|
-
~theme ... (optional global theme vars)
|
|
12
|
-
%id theme /route (page declaration — required)
|
|
13
|
-
@var = default (reactive state)
|
|
14
|
-
~mount GET /api => @var (fetch on load)
|
|
15
|
-
~interval 10000 GET /api => @var (polling)
|
|
16
|
-
blocks...
|
|
17
|
-
--- (page separator)
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### Backend (full-stack)
|
|
5
|
+
## File structure
|
|
21
6
|
```
|
|
22
|
-
~env
|
|
23
|
-
~db sqlite ./app.db
|
|
24
|
-
~auth jwt $JWT_SECRET expire=7d
|
|
25
|
-
~mail smtp host=smtp.mailgun.com user=$MAIL_USER pass=$MAIL_PASS
|
|
26
|
-
~admin /admin (auto admin panel)
|
|
27
|
-
~middleware cors | rate-limit 100/min | log
|
|
28
|
-
|
|
29
|
-
model User {
|
|
30
|
-
id : uuid : pk auto
|
|
31
|
-
name : text : required
|
|
32
|
-
email : text : required unique
|
|
33
|
-
password : text : required hashed
|
|
34
|
-
plan : enum : starter,pro,enterprise : default=starter
|
|
35
|
-
role : enum : user,admin : default=user
|
|
36
|
-
~soft-delete
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
api POST /api/auth/register {
|
|
40
|
-
~validate name required | email required email | password min=8
|
|
41
|
-
~unique User email $body.email | 409
|
|
42
|
-
~hash password
|
|
43
|
-
insert User($body)
|
|
44
|
-
~mail $inserted.email "Welcome!" "Your account is ready."
|
|
45
|
-
return jwt($inserted) 201
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
api GET /api/users {
|
|
49
|
-
~guard admin
|
|
50
|
-
~query page=1 limit=20
|
|
51
|
-
return User.paginate($page, $limit)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
api DELETE /api/users/:id {
|
|
55
|
-
~guard auth | admin
|
|
56
|
-
delete User($id)
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Page declaration
|
|
61
|
-
`%id theme /route`
|
|
62
|
-
- themes: `dark` | `light` | `acid` | `#bg,#text,#accent`
|
|
63
|
-
|
|
64
|
-
### Global theme
|
|
65
|
-
`~theme accent=#7c3aed radius=1.5rem font=Syne bg=#0a0a0a text=#fff surface=#111 navbg=#000 spacing=6rem`
|
|
66
|
-
|
|
67
|
-
### State & data
|
|
68
|
-
```
|
|
69
|
-
@users = []
|
|
70
|
-
@stats = {}
|
|
71
|
-
~mount GET /api/users => @users
|
|
72
|
-
~interval 30000 GET /api/stats => @stats
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### S3 Storage
|
|
76
|
-
```
|
|
77
|
-
~s3 $AWS_ACCESS_KEY_ID secret=$AWS_SECRET_ACCESS_KEY bucket=$S3_BUCKET region=us-east-1
|
|
78
|
-
~s3 bucket=my-bucket region=us-east-1 prefix=uploads/ maxSize=5mb allow=image/jpeg,image/png,application/pdf
|
|
79
|
-
|
|
80
|
-
# Cloudflare R2 compatible
|
|
81
|
-
~s3 $R2_KEY secret=$R2_SECRET bucket=my-bucket endpoint=https://xxx.r2.cloudflarestorage.com
|
|
82
|
-
|
|
83
|
-
# MinIO local
|
|
84
|
-
~s3 $KEY secret=$SECRET bucket=local endpoint=http://localhost:9000
|
|
85
|
-
```
|
|
86
|
-
Auto-generated routes: `POST /api/upload` | `DELETE /api/upload/:key` | `GET /api/upload/presign?key=x`
|
|
87
|
-
|
|
88
|
-
Mock mode in dev: saves to `./uploads/` folder, serves via `/uploads/filename`.
|
|
89
|
-
|
|
90
|
-
### Plugin System
|
|
91
|
-
```
|
|
92
|
-
# Built-in plugins (no file needed)
|
|
93
|
-
~use logger format=tiny
|
|
94
|
-
~use cors origins=https://myapp.com,https://www.myapp.com
|
|
95
|
-
~use rate-limit max=100 window=60s
|
|
96
|
-
~use helmet
|
|
97
|
-
~use compression
|
|
98
|
-
|
|
99
|
-
# Local file plugin
|
|
100
|
-
~plugin ./plugins/my-plugin.js
|
|
101
|
-
|
|
102
|
-
# npm package plugin
|
|
103
|
-
~plugin my-aiplang-plugin
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Plugin interface:
|
|
107
|
-
```js
|
|
108
|
-
// plugins/my-plugin.js
|
|
109
|
-
module.exports = {
|
|
110
|
-
name: 'my-plugin',
|
|
111
|
-
setup(server, app, utils) {
|
|
112
|
-
// server.addRoute('GET', '/api/custom', handler)
|
|
113
|
-
// utils.emit, utils.on, utils.dispatch, utils.dbRun, utils.uuid
|
|
114
|
-
// utils.s3Upload, utils.generateJWT — all available
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Factory with options
|
|
119
|
-
module.exports = (opts) => ({
|
|
120
|
-
name: 'my-plugin',
|
|
121
|
-
setup(server, app, { opts, emit }) { ... }
|
|
122
|
-
})
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### Stripe Payments
|
|
126
|
-
```
|
|
127
|
-
~stripe $STRIPE_SECRET_KEY webhook=$STRIPE_WEBHOOK_SECRET success=/dashboard cancel=/pricing
|
|
128
|
-
~plan starter=price_xxx pro=price_yyy enterprise=price_zzz
|
|
129
|
-
```
|
|
130
|
-
Auto-generated routes: `POST /api/stripe/checkout` | `POST /api/stripe/portal` | `GET /api/stripe/subscription` | `DELETE /api/stripe/subscription` | `POST /api/stripe/webhook`
|
|
131
|
-
|
|
132
|
-
Webhooks handled: `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_failed`, `invoice.payment_succeeded`
|
|
133
|
-
|
|
134
|
-
Guard: `~guard subscribed` — requires active subscription
|
|
135
|
-
|
|
136
|
-
### All blocks
|
|
137
|
-
|
|
138
|
-
**nav** — `nav{Brand>/path:Link>/path:Link}` (auto mobile hamburger)
|
|
139
|
-
|
|
140
|
-
**hero** — `hero{Title|Subtitle>/path:CTA>/path:CTA2}` or `hero{Title|Sub>/path:CTA|img:https://url}` (split layout)
|
|
141
|
-
|
|
142
|
-
**stats** — `stats{@stats.users:Users|@stats.mrr:MRR|99.9%:Uptime}`
|
|
143
|
-
|
|
144
|
-
**rowN** — `row3{rocket>Fast>Zero config.|shield>Secure>SOC2.|chart>Smart>Real-time.} animate:stagger`
|
|
145
|
-
|
|
146
|
-
**sect** — `sect{Title|Optional body text}`
|
|
147
|
-
|
|
148
|
-
**table** — `table @users { Name:name | Email:email | Plan:plan | edit PUT /api/users/{id} | delete /api/users/{id} | empty: No data. }`
|
|
149
|
-
|
|
150
|
-
**form** — `form POST /api/users => @users.push($result) { Name:text:Alice | Email:email | Plan:select:starter,pro,enterprise }`
|
|
151
|
-
|
|
152
|
-
**form with redirect** — `form POST /api/auth/login => redirect /dashboard { Email:email | Password:password }`
|
|
153
|
-
|
|
154
|
-
**pricing** — `pricing{Starter>Free>3 projects>/signup:Get started|Pro>$29/mo>Unlimited>/signup:Start trial|Enterprise>Custom>SSO>/contact:Talk}`
|
|
155
|
-
|
|
156
|
-
**faq** — `faq{How to start?>Sign up free.|Cancel anytime?>Yes, one click.}`
|
|
157
|
-
|
|
158
|
-
**testimonial** — `testimonial{Alice Chen, CEO @ Acme|"Changed how we ship."|img:https://i.pravatar.cc/64?img=5}`
|
|
159
|
-
|
|
160
|
-
**gallery** — `gallery{https://img1.jpg | https://img2.jpg | https://img3.jpg}`
|
|
161
|
-
|
|
162
|
-
**btn** — `btn{Export CSV > GET /api/export}` or `btn{Delete all > DELETE /api/items > confirm:Are you sure?}`
|
|
163
|
-
|
|
164
|
-
**select** — `select @filterVar { All | Active | Inactive }`
|
|
165
|
-
|
|
166
|
-
**raw** — `raw{<div style="...">Any HTML, embeds, custom components</div>}`
|
|
167
|
-
|
|
168
|
-
**if** — `if @user { sect{Welcome back!} }`
|
|
169
|
-
|
|
170
|
-
**foot** — `foot{© 2025 AppName>/privacy:Privacy>/terms:Terms}`
|
|
171
|
-
|
|
172
|
-
### Block modifiers (suffix on any block)
|
|
173
|
-
```
|
|
174
|
-
hero{...} animate:blur-in
|
|
175
|
-
row3{...} animate:stagger class:my-section
|
|
176
|
-
sect{...} animate:fade-up
|
|
177
|
-
```
|
|
178
|
-
Animations: `fade-up` `fade-in` `blur-in` `slide-left` `slide-right` `zoom-in` `stagger`
|
|
179
|
-
|
|
180
|
-
### Multiple pages
|
|
181
|
-
```
|
|
182
|
-
%home dark /
|
|
183
|
-
nav{...}
|
|
184
|
-
hero{...}
|
|
185
|
-
---
|
|
186
|
-
%dashboard dark /dashboard
|
|
187
|
-
@users = []
|
|
188
|
-
~mount GET /api/users => @users
|
|
189
|
-
table @users { ... }
|
|
190
|
-
---
|
|
191
|
-
%login dark /login
|
|
192
|
-
form POST /api/auth/login => redirect /dashboard { ... }
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
---
|
|
196
|
-
|
|
197
|
-
## Complete examples
|
|
198
|
-
|
|
199
|
-
### SaaS with 4 pages
|
|
200
|
-
```
|
|
201
|
-
~theme accent=#2563eb
|
|
202
|
-
~db sqlite ./app.db
|
|
7
|
+
~env VAR required # env validation
|
|
8
|
+
~db sqlite ./app.db # or: postgres $DATABASE_URL
|
|
203
9
|
~auth jwt $JWT_SECRET expire=7d
|
|
10
|
+
~mail smtp host=x user=$U pass=$P
|
|
11
|
+
~s3 $KEY secret=$S bucket=$B region=us-east-1 prefix=uploads/ maxSize=10mb
|
|
12
|
+
~stripe $KEY webhook=$WH success=/ok cancel=/pricing
|
|
13
|
+
~plan free=price_x pro=price_y
|
|
204
14
|
~admin /admin
|
|
15
|
+
~use cors origins=https://x.com
|
|
16
|
+
~use rate-limit max=100 window=60s
|
|
17
|
+
~use helmet | ~use logger | ~use compression
|
|
18
|
+
~plugin ./my-plugin.js
|
|
205
19
|
|
|
206
|
-
model
|
|
207
|
-
id
|
|
208
|
-
|
|
209
|
-
email : text : required unique
|
|
210
|
-
password : text : required hashed
|
|
211
|
-
plan : enum : starter,pro,enterprise : default=starter
|
|
212
|
-
role : enum : user,admin : default=user
|
|
20
|
+
model Name {
|
|
21
|
+
id : uuid : pk auto
|
|
22
|
+
field : type : modifier
|
|
213
23
|
~soft-delete
|
|
24
|
+
~belongs OtherModel
|
|
214
25
|
}
|
|
26
|
+
# types: uuid text int float bool timestamp json enum
|
|
27
|
+
# modifiers: pk auto required unique hashed default=val index
|
|
215
28
|
|
|
216
|
-
api
|
|
217
|
-
~
|
|
218
|
-
~
|
|
219
|
-
~
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
29
|
+
api METHOD /path/:id {
|
|
30
|
+
~guard auth | admin | subscribed | owner
|
|
31
|
+
~validate field required | field email | field min=8 | field numeric
|
|
32
|
+
~query page=1 limit=20
|
|
33
|
+
~unique Model field $body.field | 409
|
|
34
|
+
~hash field
|
|
35
|
+
~check password $body.pw $user.pw | 401
|
|
36
|
+
~mail $user.email "Subject" "Body"
|
|
37
|
+
~dispatch jobName $body
|
|
38
|
+
~emit event.name $body
|
|
39
|
+
$var = Model.findBy(field=$body.field)
|
|
40
|
+
insert Model($body)
|
|
41
|
+
update Model($id, $body)
|
|
42
|
+
delete Model($id)
|
|
43
|
+
restore Model($id)
|
|
44
|
+
return $inserted | $updated | $auth.user | Model.all(order=created_at desc)
|
|
45
|
+
return Model.paginate($page, $limit)
|
|
46
|
+
return Model.count() | Model.sum(field) | Model.avg(field)
|
|
227
47
|
return jwt($user) 200
|
|
228
48
|
}
|
|
229
49
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
api GET /api/stats {
|
|
236
|
-
return User.count()
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
%home dark /
|
|
240
|
-
|
|
241
|
-
@stats = {}
|
|
242
|
-
~mount GET /api/stats => @stats
|
|
50
|
+
%id theme /route # dark | light | acid | #bg,#text,#accent
|
|
51
|
+
~theme accent=#hex bg=#hex text=#hex font=Name radius=1rem surface=#hex navbg=#hex
|
|
52
|
+
@var = [] # state: [] or {} or "string" or 0
|
|
53
|
+
~mount GET /api => @var
|
|
54
|
+
~interval 10000 GET /api => @var
|
|
243
55
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
foot{© 2025 MySaaS>/login:Already have an account?}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
### Landing page with custom theme
|
|
291
|
-
```
|
|
292
|
-
~theme accent=#f59e0b radius=2rem font=Syne bg=#0c0a09 text=#fafaf9 surface=#1c1917
|
|
293
|
-
|
|
294
|
-
%home dark /
|
|
295
|
-
|
|
296
|
-
nav{Acme Studio>/work:Work>/blog:Blog>/contact:Contact}
|
|
297
|
-
hero{We build things that matter|Creative studio based in São Paulo.>/work:View our work>/contact:Get in touch|img:https://images.unsplash.com/photo-1497366216548?w=800} animate:fade-in
|
|
298
|
-
row3{globe>Global clients>Teams in 30+ countries.|star>Award winning>12 design awards.|check>On-time delivery>98% on schedule.} animate:stagger
|
|
299
|
-
testimonial{Marco Silva, CTO @ FinTech BR|"From prototype to production in 6 weeks."|img:https://i.pravatar.cc/64?img=12}
|
|
300
|
-
gallery{https://images.unsplash.com/photo-1600880292203?w=400|https://images.unsplash.com/photo-1522202176988?w=400|https://images.unsplash.com/photo-1497366412874?w=400}
|
|
301
|
-
foot{© 2025 Acme Studio>/privacy:Privacy>/instagram:Instagram}
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
---
|
|
305
|
-
|
|
306
|
-
## Generation rules
|
|
307
|
-
|
|
308
|
-
1. Always start with `%id theme /route`
|
|
309
|
-
2. Use `dark` theme unless specified otherwise
|
|
310
|
-
3. For dynamic data, always declare `@var = []` or `@var = {}` and use `~mount`
|
|
311
|
-
4. Tables with data should always have `edit` and `delete` unless readonly
|
|
312
|
-
5. Forms should have `=> @list.push($result)` or `=> redirect /path`
|
|
313
|
-
6. Use real icon names: bolt rocket shield chart star check globe gear fire money bell mail user
|
|
314
|
-
7. Multiple pages separated by `---`
|
|
315
|
-
8. Add `animate:fade-up` or `animate:stagger` to key sections
|
|
316
|
-
9. `~theme` always comes before `%` declarations
|
|
317
|
-
10. Never generate explanations — only aiplang code
|
|
318
|
-
11. For full-stack apps, add `~db`, `~auth`, `model` and `api` blocks before pages
|
|
319
|
-
|
|
320
|
-
---
|
|
321
|
-
|
|
322
|
-
## Running
|
|
323
|
-
|
|
324
|
-
```bash
|
|
325
|
-
# Install
|
|
326
|
-
npm install -g aiplang
|
|
327
|
-
|
|
328
|
-
# Frontend only (static site)
|
|
329
|
-
aiplang serve # dev → localhost:3000
|
|
330
|
-
aiplang build pages/ # compile → dist/
|
|
331
|
-
|
|
332
|
-
# Full-stack (Node.js backend)
|
|
333
|
-
aiplang start app.aiplang
|
|
334
|
-
|
|
335
|
-
# Go binary (production, v2)
|
|
336
|
-
aiplangd dev app.aiplang
|
|
337
|
-
aiplangd build app.aiplang
|
|
338
|
-
```
|
|
56
|
+
blocks...
|
|
57
|
+
--- # page separator
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## All blocks
|
|
61
|
+
```
|
|
62
|
+
nav{Brand>/path:Link>/path:Link}
|
|
63
|
+
hero{Title|Sub>/path:CTA} | hero{Title|Sub>/path:CTA|img:https://url}
|
|
64
|
+
stats{@val:Label|99%:Uptime|$0:Free}
|
|
65
|
+
row2{icon>Title>Body} | row3{...} | row4{...}
|
|
66
|
+
sect{Title|Optional body}
|
|
67
|
+
table @list { Col:field | edit PUT /api/{id} | delete /api/{id} | empty: msg }
|
|
68
|
+
form POST /api => @list.push($result) { Label:type:placeholder | Label:select:a,b,c }
|
|
69
|
+
form POST /api => redirect /path { Label:type | Label:password }
|
|
70
|
+
pricing{Name>Price>Desc>/path:CTA|Name>Price>Desc>/path:CTA}
|
|
71
|
+
faq{Question?>Answer.|Q2?>A2.}
|
|
72
|
+
testimonial{Name, Role @ Co|"Quote."|img:https://url}
|
|
73
|
+
gallery{https://img1|https://img2|https://img3}
|
|
74
|
+
btn{Label > METHOD /api/path} | btn{Label > DELETE /api > confirm:Sure?}
|
|
75
|
+
select @filterVar { All | Active | Inactive }
|
|
76
|
+
if @var { blocks }
|
|
77
|
+
raw{<div>any HTML</div>}
|
|
78
|
+
foot{© 2025 Name>/path:Link}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Block modifiers (any block)
|
|
82
|
+
`animate:fade-up | fade-in | blur-in | slide-left | slide-right | zoom-in | stagger`
|
|
83
|
+
`class:my-class`
|
|
84
|
+
|
|
85
|
+
## S3 auto-routes
|
|
86
|
+
`POST /api/upload` · `DELETE /api/upload/:key` · `GET /api/upload/presign?key=x`
|
|
87
|
+
|
|
88
|
+
## Stripe auto-routes
|
|
89
|
+
`POST /api/stripe/checkout` · `POST /api/stripe/portal` · `GET /api/stripe/subscription`
|
|
90
|
+
|
|
91
|
+
## Rules
|
|
92
|
+
1. Dark theme default
|
|
93
|
+
2. `@var = []` + `~mount` for all dynamic data
|
|
94
|
+
3. Tables always have `edit` + `delete` unless readonly
|
|
95
|
+
4. Forms: `=> @list.push($result)` or `=> redirect /path`
|
|
96
|
+
5. `~theme` before `%` declarations
|
|
97
|
+
6. Separate pages with `---`
|
|
98
|
+
7. Full-stack: `~db` + `~auth` + `model` + `api` before pages
|
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.6.
|
|
8
|
+
const VERSION = '2.6.2'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -36,7 +36,7 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
|
|
|
36
36
|
Usage:
|
|
37
37
|
npx aiplang init [name] create project (default template)
|
|
38
38
|
npx aiplang init [name] --template <t> use template: saas|landing|crud|dashboard|portfolio|blog
|
|
39
|
-
npx aiplang init [name] --template ./my.
|
|
39
|
+
npx aiplang init [name] --template ./my.aip use a local .aip file as template
|
|
40
40
|
npx aiplang init [name] --template my-custom use a saved custom template
|
|
41
41
|
npx aiplang serve [dir] dev server + hot reload
|
|
42
42
|
npx aiplang build [dir/file] compile → static HTML
|
|
@@ -44,16 +44,16 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
|
|
|
44
44
|
npx aiplang --version
|
|
45
45
|
|
|
46
46
|
Full-stack:
|
|
47
|
-
npx aiplang start app.
|
|
48
|
-
PORT=8080 aiplang start app.
|
|
47
|
+
npx aiplang start app.aip start full-stack server (API + DB + frontend)
|
|
48
|
+
PORT=8080 aiplang start app.aip custom port
|
|
49
49
|
|
|
50
50
|
Templates:
|
|
51
51
|
npx aiplang template list list all templates (built-in + custom)
|
|
52
52
|
npx aiplang template save <n> save current project as template
|
|
53
|
-
npx aiplang template save <n> --from <f> save a specific .
|
|
53
|
+
npx aiplang template save <n> --from <f> save a specific .aip file as template
|
|
54
54
|
npx aiplang template edit <n> open template in editor
|
|
55
55
|
npx aiplang template show <n> print template source
|
|
56
|
-
npx aiplang template export <n> export template to .
|
|
56
|
+
npx aiplang template export <n> export template to .aip file
|
|
57
57
|
npx aiplang template remove <n> delete a custom template
|
|
58
58
|
|
|
59
59
|
Custom template variables:
|
|
@@ -76,10 +76,10 @@ if (cmd==='--version'||cmd==='-v') { console.log(`aiplang v${VERSION}`); process
|
|
|
76
76
|
|
|
77
77
|
// ─────────────────────────────────────────────────────────────────
|
|
78
78
|
// TEMPLATE SYSTEM
|
|
79
|
-
// Custom templates stored at ~/.
|
|
79
|
+
// Custom templates stored at ~/.aip/templates/<name>.aip
|
|
80
80
|
// ─────────────────────────────────────────────────────────────────
|
|
81
81
|
|
|
82
|
-
const TEMPLATES_DIR = path.join(require('os').homedir(), '.
|
|
82
|
+
const TEMPLATES_DIR = path.join(require('os').homedir(), '.aip', 'templates')
|
|
83
83
|
|
|
84
84
|
function ensureTemplatesDir() {
|
|
85
85
|
if (!fs.existsSync(TEMPLATES_DIR)) fs.mkdirSync(TEMPLATES_DIR, { recursive: true })
|
|
@@ -220,7 +220,7 @@ foot{{{name}}}`,
|
|
|
220
220
|
default: `# {{name}}
|
|
221
221
|
%home dark /
|
|
222
222
|
nav{{{name}}>/login:Sign in}
|
|
223
|
-
hero{Welcome to {{name}}|Edit pages/home.
|
|
223
|
+
hero{Welcome to {{name}}|Edit pages/home.aip to get started.>/signup:Get started} animate:fade-up
|
|
224
224
|
row3{rocket>Fast>Renders in under 1ms.|bolt>AI-native>Written by Claude in seconds.|globe>Deploy anywhere>Static files. Any host.}
|
|
225
225
|
foot{© {{year}} {{name}}}`,
|
|
226
226
|
}
|
|
@@ -232,15 +232,15 @@ function applyTemplateVars(src, name, year) {
|
|
|
232
232
|
function getTemplate(tplName, name, year) {
|
|
233
233
|
ensureTemplatesDir()
|
|
234
234
|
|
|
235
|
-
// 1. Local file path: --template ./my-template.
|
|
235
|
+
// 1. Local file path: --template ./my-template.aip or --template /abs/path.aip
|
|
236
236
|
if (tplName.startsWith('./') || tplName.startsWith('../') || tplName.startsWith('/')) {
|
|
237
237
|
const full = path.resolve(tplName)
|
|
238
238
|
if (!fs.existsSync(full)) { console.error(`\n ✗ Template file not found: ${full}\n`); process.exit(1) }
|
|
239
239
|
return applyTemplateVars(fs.readFileSync(full, 'utf8'), name, year)
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
// 2. User custom template: ~/.
|
|
243
|
-
const customPath = path.join(TEMPLATES_DIR, tplName + '.
|
|
242
|
+
// 2. User custom template: ~/.aip/templates/<name>.aip
|
|
243
|
+
const customPath = path.join(TEMPLATES_DIR, tplName + '.aip')
|
|
244
244
|
if (fs.existsSync(customPath)) {
|
|
245
245
|
return applyTemplateVars(fs.readFileSync(customPath, 'utf8'), name, year)
|
|
246
246
|
}
|
|
@@ -251,7 +251,7 @@ function getTemplate(tplName, name, year) {
|
|
|
251
251
|
|
|
252
252
|
// Not found — show what's available
|
|
253
253
|
const customs = fs.existsSync(TEMPLATES_DIR)
|
|
254
|
-
? fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.
|
|
254
|
+
? fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.aip')).map(f=>f.replace('.aip',''))
|
|
255
255
|
: []
|
|
256
256
|
const all = [...Object.keys(BUILTIN_TEMPLATES).filter(k=>k!=='default'), ...customs]
|
|
257
257
|
console.error(`\n ✗ Template "${tplName}" not found.\n Available: ${all.join(', ')}\n`)
|
|
@@ -261,7 +261,7 @@ function getTemplate(tplName, name, year) {
|
|
|
261
261
|
function listTemplates() {
|
|
262
262
|
ensureTemplatesDir()
|
|
263
263
|
const builtins = Object.keys(BUILTIN_TEMPLATES).filter(k=>k!=='default')
|
|
264
|
-
const customs = fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.
|
|
264
|
+
const customs = fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.aip')).map(f=>f.replace('.aip',''))
|
|
265
265
|
console.log(`\n aiplang templates\n`)
|
|
266
266
|
console.log(` Built-in:`)
|
|
267
267
|
builtins.forEach(t => console.log(` ${t}`))
|
|
@@ -295,18 +295,18 @@ if (cmd === 'template') {
|
|
|
295
295
|
if (!fs.existsSync(fp)) { console.error(`\n ✗ File not found: ${fp}\n`); process.exit(1) }
|
|
296
296
|
src = fs.readFileSync(fp, 'utf8')
|
|
297
297
|
} else {
|
|
298
|
-
// Auto-detect: use pages/ directory or app.
|
|
299
|
-
const sources = ['pages', 'app.
|
|
298
|
+
// Auto-detect: use pages/ directory or app.aip
|
|
299
|
+
const sources = ['pages', 'app.aip', 'index.aip']
|
|
300
300
|
const found = sources.find(s => fs.existsSync(s))
|
|
301
|
-
if (!found) { console.error('\n ✗ No .
|
|
301
|
+
if (!found) { console.error('\n ✗ No .aip files found. Use --from <file> to specify source.\n'); process.exit(1) }
|
|
302
302
|
if (fs.statSync(found).isDirectory()) {
|
|
303
|
-
src = fs.readdirSync(found).filter(f=>f.endsWith('.
|
|
303
|
+
src = fs.readdirSync(found).filter(f=>f.endsWith('.aip'))
|
|
304
304
|
.map(f => fs.readFileSync(path.join(found,f),'utf8')).join('\n---\n')
|
|
305
305
|
} else {
|
|
306
306
|
src = fs.readFileSync(found, 'utf8')
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
|
-
const dest = path.join(TEMPLATES_DIR, tname + '.
|
|
309
|
+
const dest = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
310
310
|
fs.writeFileSync(dest, src)
|
|
311
311
|
console.log(`\n ✓ Template saved: ${tname}\n ${dest}\n\n Use it: aiplang init my-app --template ${tname}\n`)
|
|
312
312
|
process.exit(0)
|
|
@@ -316,7 +316,7 @@ if (cmd === 'template') {
|
|
|
316
316
|
if (sub === 'remove' || sub === 'rm' || sub === 'delete') {
|
|
317
317
|
const tname = args[1]
|
|
318
318
|
if (!tname) { console.error('\n ✗ Usage: aiplang template remove <name>\n'); process.exit(1) }
|
|
319
|
-
const dest = path.join(TEMPLATES_DIR, tname + '.
|
|
319
|
+
const dest = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
320
320
|
if (!fs.existsSync(dest)) { console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1) }
|
|
321
321
|
fs.unlinkSync(dest)
|
|
322
322
|
console.log(`\n ✓ Removed template: ${tname}\n`); process.exit(0)
|
|
@@ -326,7 +326,7 @@ if (cmd === 'template') {
|
|
|
326
326
|
if (sub === 'edit' || sub === 'open') {
|
|
327
327
|
const tname = args[1]
|
|
328
328
|
if (!tname) { console.error('\n ✗ Usage: aiplang template edit <name>\n'); process.exit(1) }
|
|
329
|
-
let dest = path.join(TEMPLATES_DIR, tname + '.
|
|
329
|
+
let dest = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
330
330
|
if (!fs.existsSync(dest)) {
|
|
331
331
|
// create from built-in if exists
|
|
332
332
|
const builtin = BUILTIN_TEMPLATES[tname]
|
|
@@ -342,7 +342,7 @@ if (cmd === 'template') {
|
|
|
342
342
|
// aiplang template show <name>
|
|
343
343
|
if (sub === 'show' || sub === 'cat') {
|
|
344
344
|
const tname = args[1] || 'default'
|
|
345
|
-
const customPath = path.join(TEMPLATES_DIR, tname + '.
|
|
345
|
+
const customPath = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
346
346
|
if (fs.existsSync(customPath)) { console.log(fs.readFileSync(customPath,'utf8')); process.exit(0) }
|
|
347
347
|
const builtin = BUILTIN_TEMPLATES[tname]
|
|
348
348
|
if (builtin) { console.log(builtin); process.exit(0) }
|
|
@@ -354,8 +354,8 @@ if (cmd === 'template') {
|
|
|
354
354
|
const tname = args[1]
|
|
355
355
|
if (!tname) { console.error('\n ✗ Usage: aiplang template export <name>\n'); process.exit(1) }
|
|
356
356
|
const outIdx = args.indexOf('--out')
|
|
357
|
-
const outFile = outIdx !== -1 ? args[outIdx+1] : `./${tname}.
|
|
358
|
-
const customPath = path.join(TEMPLATES_DIR, tname + '.
|
|
357
|
+
const outFile = outIdx !== -1 ? args[outIdx+1] : `./${tname}.aip`
|
|
358
|
+
const customPath = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
359
359
|
const src = fs.existsSync(customPath) ? fs.readFileSync(customPath,'utf8') : BUILTIN_TEMPLATES[tname]
|
|
360
360
|
if (!src) { console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1) }
|
|
361
361
|
fs.writeFileSync(outFile, src)
|
|
@@ -384,21 +384,21 @@ if (cmd==='init') {
|
|
|
384
384
|
const isMultiFile = tplSrc.includes('\n---\n')
|
|
385
385
|
|
|
386
386
|
if (isFullStack) {
|
|
387
|
-
// Full-stack project: single app.
|
|
387
|
+
// Full-stack project: single app.aip
|
|
388
388
|
fs.mkdirSync(dir, { recursive: true })
|
|
389
|
-
fs.writeFileSync(path.join(dir, 'app.
|
|
389
|
+
fs.writeFileSync(path.join(dir, 'app.aip'), tplSrc)
|
|
390
390
|
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
|
|
391
391
|
name, version:'0.1.0',
|
|
392
|
-
scripts: { dev: 'npx aiplang start app.
|
|
392
|
+
scripts: { dev: 'npx aiplang start app.aip', start: 'npx aiplang start app.aip' },
|
|
393
393
|
devDependencies: { 'aiplang': `^${VERSION}` }
|
|
394
394
|
}, null, 2))
|
|
395
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
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.
|
|
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.aip\n\`\`\`\n`)
|
|
398
398
|
const label = tplName !== 'default' ? ` (template: ${tplName})` : ''
|
|
399
|
-
console.log(`\n ✓ Created ${name}/${label}\n\n app.
|
|
399
|
+
console.log(`\n ✓ Created ${name}/${label}\n\n app.aip ← full-stack app (backend + frontend)\n\n Next:\n cd ${name} && npx aiplang start app.aip\n`)
|
|
400
400
|
} else if (isMultiFile) {
|
|
401
|
-
// Multi-page SSG project: pages/*.
|
|
401
|
+
// Multi-page SSG project: pages/*.aip
|
|
402
402
|
fs.mkdirSync(path.join(dir,'pages'), {recursive:true})
|
|
403
403
|
fs.mkdirSync(path.join(dir,'public'), {recursive:true})
|
|
404
404
|
for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
|
|
@@ -408,7 +408,7 @@ if (cmd==='init') {
|
|
|
408
408
|
pageBlocks.forEach((block, i) => {
|
|
409
409
|
const m = block.match(/^%([a-zA-Z0-9_-]+)/m)
|
|
410
410
|
const pageName = m ? m[1] : (i === 0 ? 'home' : `page${i}`)
|
|
411
|
-
fs.writeFileSync(path.join(dir,'pages',`${pageName}.
|
|
411
|
+
fs.writeFileSync(path.join(dir,'pages',`${pageName}.aip`), block.trim())
|
|
412
412
|
})
|
|
413
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
414
|
fs.writeFileSync(path.join(dir,'.gitignore'),'dist/\nnode_modules/\n')
|
|
@@ -422,11 +422,11 @@ if (cmd==='init') {
|
|
|
422
422
|
for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
|
|
423
423
|
const src=path.join(RUNTIME_DIR,f); if(fs.existsSync(src)) fs.copyFileSync(src,path.join(dir,'public',f))
|
|
424
424
|
}
|
|
425
|
-
fs.writeFileSync(path.join(dir,'pages','home.
|
|
425
|
+
fs.writeFileSync(path.join(dir,'pages','home.aip'), tplSrc)
|
|
426
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
427
|
fs.writeFileSync(path.join(dir,'.gitignore'),'dist/\nnode_modules/\n')
|
|
428
428
|
const label = tplName !== 'default' ? ` (template: ${tplName})` : ''
|
|
429
|
-
console.log(`\n ✓ Created ${name}/${label}\n\n pages/home.
|
|
429
|
+
console.log(`\n ✓ Created ${name}/${label}\n\n pages/home.aip ← edit this\n\n Next:\n cd ${name} && npx aiplang serve\n`)
|
|
430
430
|
}
|
|
431
431
|
process.exit(0)
|
|
432
432
|
}
|
|
@@ -435,7 +435,7 @@ if (cmd==='init') {
|
|
|
435
435
|
if (cmd==='new') {
|
|
436
436
|
const name=args[0]; if(!name){console.error('\n ✗ Usage: aiplang new <page>\n');process.exit(1)}
|
|
437
437
|
const dir=fs.existsSync('pages')?'pages':'.'
|
|
438
|
-
const file=path.join(dir,`${name}.
|
|
438
|
+
const file=path.join(dir,`${name}.aip`)
|
|
439
439
|
if(fs.existsSync(file)){console.error(`\n ✗ ${file} exists.\n`);process.exit(1)}
|
|
440
440
|
const cap=name.charAt(0).toUpperCase()+name.slice(1)
|
|
441
441
|
fs.writeFileSync(file,`# ${name}\n%${name} dark /${name}\n\nnav{AppName>/home:Home}\nhero{${cap}|Description.>/action:Get started}\nfoot{© ${new Date().getFullYear()} AppName}\n`)
|
|
@@ -450,9 +450,9 @@ if (cmd==='build') {
|
|
|
450
450
|
const input=args.filter((a,i)=>!a.startsWith('--')&&i!==outIdx+1)[0]||'pages/'
|
|
451
451
|
const files=[]
|
|
452
452
|
if(fs.existsSync(input)&&fs.statSync(input).isDirectory()){
|
|
453
|
-
fs.readdirSync(input).filter(f=>f.endsWith('.
|
|
454
|
-
} else if(input.endsWith('.
|
|
455
|
-
if(!files.length){console.error(`\n ✗ No .
|
|
453
|
+
fs.readdirSync(input).filter(f=>f.endsWith('.aip')).forEach(f=>files.push(path.join(input,f)))
|
|
454
|
+
} else if(input.endsWith('.aip')&&fs.existsSync(input)){ files.push(input) }
|
|
455
|
+
if(!files.length){console.error(`\n ✗ No .aip files in: ${input}\n`);process.exit(1)}
|
|
456
456
|
const src=files.map(f=>fs.readFileSync(f,'utf8')).join('\n---\n')
|
|
457
457
|
const pages=parsePages(src)
|
|
458
458
|
if(!pages.length){console.error('\n ✗ No pages found.\n');process.exit(1)}
|
|
@@ -471,7 +471,7 @@ if (cmd==='build') {
|
|
|
471
471
|
}
|
|
472
472
|
const hf=path.join(RUNTIME_DIR,'aiplang-hydrate.js')
|
|
473
473
|
if(fs.existsSync(hf)){const dst=path.join(outDir,'aiplang-hydrate.js');fs.copyFileSync(hf,dst);total+=fs.statSync(dst).size;console.log(` ✓ ${dst.padEnd(40)} ${hSize(fs.statSync(dst).size)}`)}
|
|
474
|
-
if(fs.existsSync('public'))fs.readdirSync('public').filter(f=>!f.endsWith('.
|
|
474
|
+
if(fs.existsSync('public'))fs.readdirSync('public').filter(f=>!f.endsWith('.aip')).forEach(f=>fs.copyFileSync(path.join('public',f),path.join(outDir,f)))
|
|
475
475
|
console.log(`\n ${pages.length} page(s) — ${hSize(total)} total\n\n Preview: npx serve ${outDir}\n Deploy: Vercel, Netlify, S3, any static host\n`)
|
|
476
476
|
process.exit(0)
|
|
477
477
|
}
|
|
@@ -480,13 +480,13 @@ if (cmd==='build') {
|
|
|
480
480
|
if (cmd==='serve'||cmd==='dev') {
|
|
481
481
|
const root=path.resolve(args[0]||'.')
|
|
482
482
|
const port=parseInt(process.env.PORT||'3000')
|
|
483
|
-
const MIME={'.html':'text/html;charset=utf-8','.js':'application/javascript','.css':'text/css','.
|
|
483
|
+
const MIME={'.html':'text/html;charset=utf-8','.js':'application/javascript','.css':'text/css','.aip':'text/plain','.json':'application/json','.wasm':'application/wasm','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.ico':'image/x-icon'}
|
|
484
484
|
let clients=[]
|
|
485
485
|
const mtimes={}
|
|
486
486
|
setInterval(()=>{
|
|
487
487
|
const pd=path.join(root,'pages')
|
|
488
488
|
if(!fs.existsSync(pd))return
|
|
489
|
-
fs.readdirSync(pd).filter(f=>f.endsWith('.
|
|
489
|
+
fs.readdirSync(pd).filter(f=>f.endsWith('.aip')).forEach(f=>{
|
|
490
490
|
const fp=path.join(pd,f),mt=fs.statSync(fp).mtimeMs
|
|
491
491
|
if(mtimes[fp]&&mtimes[fp]!==mt)clients.forEach(c=>{try{c.write('data: reload\n\n')}catch{}})
|
|
492
492
|
mtimes[fp]=mt
|
|
@@ -501,7 +501,7 @@ if (cmd==='serve'||cmd==='dev') {
|
|
|
501
501
|
let p=req.url.split('?')[0];if(p==='/') p='/index.html'
|
|
502
502
|
let fp=null
|
|
503
503
|
for(const c of [path.join(root,'public',p),path.join(root,p)]){if(fs.existsSync(c)&&fs.statSync(c).isFile()){fp=c;break}}
|
|
504
|
-
if(!fp&&p.endsWith('.
|
|
504
|
+
if(!fp&&p.endsWith('.aip')){const c=path.join(root,'pages',path.basename(p));if(fs.existsSync(c))fp=c}
|
|
505
505
|
if(!fp){res.writeHead(404);res.end('Not found');return}
|
|
506
506
|
let content=fs.readFileSync(fp)
|
|
507
507
|
if(path.extname(fp)==='.html'){
|
|
@@ -510,7 +510,7 @@ if (cmd==='serve'||cmd==='dev') {
|
|
|
510
510
|
}
|
|
511
511
|
res.writeHead(200,{'Content-Type':MIME[path.extname(fp)]||'application/octet-stream','Access-Control-Allow-Origin':'*'})
|
|
512
512
|
res.end(content)
|
|
513
|
-
}).listen(port,()=>console.log(`\n ✓ aiplang dev server\n\n → http://localhost:${port}\n\n Hot reload ON — edit .
|
|
513
|
+
}).listen(port,()=>console.log(`\n ✓ aiplang dev server\n\n → http://localhost:${port}\n\n Hot reload ON — edit .aip files and browser refreshes.\n Ctrl+C to stop.\n`))
|
|
514
514
|
return
|
|
515
515
|
}
|
|
516
516
|
|
|
@@ -518,7 +518,7 @@ if (cmd==='serve'||cmd==='dev') {
|
|
|
518
518
|
if (cmd === 'start' || cmd === 'run') {
|
|
519
519
|
const aipFile = args[0]
|
|
520
520
|
if (!aipFile || !fs.existsSync(aipFile)) {
|
|
521
|
-
console.error(`\n ✗ Usage: aiplang start <app.
|
|
521
|
+
console.error(`\n ✗ Usage: aiplang start <app.aip>\n`)
|
|
522
522
|
process.exit(1)
|
|
523
523
|
}
|
|
524
524
|
const port = parseInt(process.env.PORT || args[1] || '3000')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiplang",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "AI-first full-stack language. Frontend + Backend + DB + Auth in one file. Competes with Laravel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aiplang",
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"node": ">=16"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
+
"@aws-sdk/client-s3": "^3.0.0",
|
|
42
|
+
"@aws-sdk/s3-request-presigner": "^3.0.0",
|
|
41
43
|
"bcryptjs": "^2.4.3",
|
|
42
44
|
"jsonwebtoken": "^9.0.2",
|
|
43
|
-
"nodemailer": "^
|
|
45
|
+
"nodemailer": "^8.0.3",
|
|
44
46
|
"sql.js": "^1.10.3",
|
|
45
|
-
"stripe": "^14.0.0"
|
|
46
|
-
"@aws-sdk/client-s3": "^3.0.0",
|
|
47
|
-
"@aws-sdk/s3-request-presigner": "^3.0.0"
|
|
47
|
+
"stripe": "^14.0.0"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -100,14 +100,11 @@ class State {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
_recompute() {
|
|
103
|
+
// Safe computed: only supports @var.path expressions, no arbitrary code
|
|
103
104
|
if (!this._computedExprs) return
|
|
104
105
|
for (const [name, expr] of Object.entries(this._computedExprs)) {
|
|
105
106
|
try {
|
|
106
|
-
const
|
|
107
|
-
...Object.keys(this._data).map(k => '_'+k),
|
|
108
|
-
`try { return (${expr}) } catch(e) { return null }`
|
|
109
|
-
)
|
|
110
|
-
const val = fn(...Object.keys(this._data).map(k => this._data[k]))
|
|
107
|
+
const val = this.eval(expr.trim())
|
|
111
108
|
if (JSON.stringify(this._computed[name]) !== JSON.stringify(val)) {
|
|
112
109
|
this._computed[name] = val
|
|
113
110
|
this._notify('$'+name)
|
|
@@ -957,108 +954,108 @@ html{scroll-behavior:smooth}
|
|
|
957
954
|
body{font-family:-apple-system,'Segoe UI',system-ui,sans-serif;-webkit-font-smoothing:antialiased}
|
|
958
955
|
a{text-decoration:none;color:inherit}
|
|
959
956
|
input,button,select{font-family:inherit}
|
|
960
|
-
.
|
|
961
|
-
.
|
|
962
|
-
.
|
|
963
|
-
.
|
|
957
|
+
.aip-root{min-height:100vh}
|
|
958
|
+
.aip-theme-dark{background:#030712;color:#f1f5f9}
|
|
959
|
+
.aip-theme-light{background:#fff;color:#0f172a}
|
|
960
|
+
.aip-theme-acid{background:#000;color:#a3e635}
|
|
964
961
|
.fx-nav{display:flex;align-items:center;justify-content:space-between;padding:1rem 2.5rem;position:sticky;top:0;z-index:50;backdrop-filter:blur(12px)}
|
|
965
|
-
.
|
|
966
|
-
.
|
|
967
|
-
.
|
|
962
|
+
.aip-theme-dark .fx-nav{border-bottom:1px solid #1e293b;background:rgba(3,7,18,.85)}
|
|
963
|
+
.aip-theme-light .fx-nav{border-bottom:1px solid #e2e8f0;background:rgba(255,255,255,.85)}
|
|
964
|
+
.aip-theme-acid .fx-nav{border-bottom:1px solid #1a2e05;background:rgba(0,0,0,.9)}
|
|
968
965
|
.fx-brand{font-size:1.25rem;font-weight:800;letter-spacing:-.03em}
|
|
969
966
|
.fx-nav-links{display:flex;align-items:center;gap:1.75rem}
|
|
970
967
|
.fx-nav-link{font-size:.875rem;font-weight:500;opacity:.65;transition:opacity .15s;cursor:pointer}
|
|
971
968
|
.fx-nav-link:hover{opacity:1}
|
|
972
|
-
.
|
|
973
|
-
.
|
|
974
|
-
.
|
|
969
|
+
.aip-theme-dark .fx-nav-link{color:#cbd5e1}
|
|
970
|
+
.aip-theme-light .fx-nav-link{color:#475569}
|
|
971
|
+
.aip-theme-acid .fx-nav-link{color:#86efac}
|
|
975
972
|
.fx-hero{display:flex;align-items:center;justify-content:center;min-height:92vh;padding:4rem 1.5rem}
|
|
976
973
|
.fx-hero-inner{max-width:56rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.5rem}
|
|
977
974
|
.fx-title{font-size:clamp(2.5rem,8vw,5.5rem);font-weight:900;letter-spacing:-.04em;line-height:1}
|
|
978
975
|
.fx-sub{font-size:clamp(1rem,2vw,1.25rem);line-height:1.75;max-width:40rem}
|
|
979
|
-
.
|
|
980
|
-
.
|
|
976
|
+
.aip-theme-dark .fx-sub{color:#94a3b8}
|
|
977
|
+
.aip-theme-light .fx-sub{color:#475569}
|
|
981
978
|
.fx-cta{display:inline-flex;align-items:center;padding:.875rem 2.5rem;border-radius:.75rem;font-weight:700;font-size:1rem;letter-spacing:-.01em;transition:transform .15s,box-shadow .15s;margin:.25rem;cursor:pointer}
|
|
982
979
|
.fx-cta:hover{transform:translateY(-1px)}
|
|
983
|
-
.
|
|
984
|
-
.
|
|
985
|
-
.
|
|
980
|
+
.aip-theme-dark .fx-cta{background:#2563eb;color:#fff;box-shadow:0 8px 24px rgba(37,99,235,.35)}
|
|
981
|
+
.aip-theme-light .fx-cta{background:#2563eb;color:#fff}
|
|
982
|
+
.aip-theme-acid .fx-cta{background:#a3e635;color:#000;font-weight:800}
|
|
986
983
|
.fx-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:3rem;padding:5rem 2.5rem;text-align:center}
|
|
987
984
|
.fx-stat-val{font-size:clamp(2.5rem,5vw,4rem);font-weight:900;letter-spacing:-.04em;line-height:1}
|
|
988
985
|
.fx-stat-lbl{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.1em;margin-top:.5rem}
|
|
989
|
-
.
|
|
990
|
-
.
|
|
986
|
+
.aip-theme-dark .fx-stat-lbl{color:#64748b}
|
|
987
|
+
.aip-theme-light .fx-stat-lbl{color:#94a3b8}
|
|
991
988
|
.fx-grid{display:grid;gap:1.25rem;padding:1rem 2.5rem 5rem}
|
|
992
989
|
.fx-grid-2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
|
|
993
990
|
.fx-grid-3{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
|
|
994
991
|
.fx-grid-4{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}
|
|
995
992
|
.fx-card{border-radius:1rem;padding:1.75rem;transition:transform .2s,box-shadow .2s}
|
|
996
993
|
.fx-card:hover{transform:translateY(-2px)}
|
|
997
|
-
.
|
|
998
|
-
.
|
|
999
|
-
.
|
|
1000
|
-
.
|
|
1001
|
-
.
|
|
994
|
+
.aip-theme-dark .fx-card{background:#0f172a;border:1px solid #1e293b}
|
|
995
|
+
.aip-theme-light .fx-card{background:#f8fafc;border:1px solid #e2e8f0}
|
|
996
|
+
.aip-theme-acid .fx-card{background:#0a0f00;border:1px solid #1a2e05}
|
|
997
|
+
.aip-theme-dark .fx-card:hover{box-shadow:0 20px 40px rgba(0,0,0,.5)}
|
|
998
|
+
.aip-theme-light .fx-card:hover{box-shadow:0 20px 40px rgba(0,0,0,.08)}
|
|
1002
999
|
.fx-icon{font-size:2rem;margin-bottom:1rem}
|
|
1003
1000
|
.fx-card-title{font-size:1.0625rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.5rem}
|
|
1004
1001
|
.fx-card-body{font-size:.875rem;line-height:1.65}
|
|
1005
|
-
.
|
|
1006
|
-
.
|
|
1002
|
+
.aip-theme-dark .fx-card-body{color:#64748b}
|
|
1003
|
+
.aip-theme-light .fx-card-body{color:#475569}
|
|
1007
1004
|
.fx-card-link{font-size:.8125rem;font-weight:600;display:inline-block;margin-top:1rem;opacity:.6;transition:opacity .15s}
|
|
1008
1005
|
.fx-card-link:hover{opacity:1}
|
|
1009
1006
|
.fx-sect{padding:5rem 2.5rem}
|
|
1010
1007
|
.fx-sect-title{font-size:clamp(1.75rem,4vw,3rem);font-weight:800;letter-spacing:-.04em;margin-bottom:1.5rem;text-align:center}
|
|
1011
1008
|
.fx-sect-body{font-size:1rem;line-height:1.75;text-align:center;max-width:48rem;margin:0 auto}
|
|
1012
|
-
.
|
|
1009
|
+
.aip-theme-dark .fx-sect-body{color:#64748b}
|
|
1013
1010
|
.fx-form-wrap{padding:3rem 2.5rem;display:flex;justify-content:center}
|
|
1014
1011
|
.fx-form{width:100%;max-width:28rem;border-radius:1.25rem;padding:2.5rem}
|
|
1015
|
-
.
|
|
1016
|
-
.
|
|
1012
|
+
.aip-theme-dark .fx-form{background:#0f172a;border:1px solid #1e293b}
|
|
1013
|
+
.aip-theme-light .fx-form{background:#f8fafc;border:1px solid #e2e8f0}
|
|
1017
1014
|
.fx-field{margin-bottom:1.25rem}
|
|
1018
1015
|
.fx-label{display:block;font-size:.8125rem;font-weight:600;margin-bottom:.5rem}
|
|
1019
|
-
.
|
|
1020
|
-
.
|
|
1016
|
+
.aip-theme-dark .fx-label{color:#94a3b8}
|
|
1017
|
+
.aip-theme-light .fx-label{color:#475569}
|
|
1021
1018
|
.fx-input{width:100%;padding:.75rem 1rem;border-radius:.625rem;font-size:.9375rem;outline:none;transition:box-shadow .15s;background:transparent}
|
|
1022
1019
|
.fx-input:focus{box-shadow:0 0 0 3px rgba(37,99,235,.35)}
|
|
1023
|
-
.
|
|
1024
|
-
.
|
|
1025
|
-
.
|
|
1026
|
-
.
|
|
1020
|
+
.aip-theme-dark .fx-input{background:#020617;border:1px solid #1e293b;color:#f1f5f9}
|
|
1021
|
+
.aip-theme-dark .fx-input::placeholder{color:#334155}
|
|
1022
|
+
.aip-theme-light .fx-input{background:#fff;border:1px solid #cbd5e1;color:#0f172a}
|
|
1023
|
+
.aip-theme-acid .fx-input{background:#000;border:1px solid #1a2e05;color:#a3e635}
|
|
1027
1024
|
.fx-btn{width:100%;padding:.875rem 1.5rem;border:none;border-radius:.625rem;font-size:.9375rem;font-weight:700;cursor:pointer;margin-top:.5rem;transition:transform .15s,opacity .15s;letter-spacing:-.01em}
|
|
1028
1025
|
.fx-btn:hover{transform:translateY(-1px)}
|
|
1029
1026
|
.fx-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
|
1030
|
-
.
|
|
1031
|
-
.
|
|
1032
|
-
.
|
|
1027
|
+
.aip-theme-dark .fx-btn{background:#2563eb;color:#fff;box-shadow:0 4px 14px rgba(37,99,235,.4)}
|
|
1028
|
+
.aip-theme-light .fx-btn{background:#2563eb;color:#fff}
|
|
1029
|
+
.aip-theme-acid .fx-btn{background:#a3e635;color:#000;font-weight:800}
|
|
1033
1030
|
.fx-form-msg{font-size:.8125rem;padding:.5rem 0;min-height:1.5rem;text-align:center}
|
|
1034
1031
|
.fx-form-err{color:#f87171}
|
|
1035
1032
|
.fx-form-ok{color:#4ade80}
|
|
1036
1033
|
.fx-table-wrap{overflow-x:auto;padding:0 2.5rem 4rem}
|
|
1037
1034
|
.fx-table{width:100%;border-collapse:collapse;font-size:.875rem}
|
|
1038
1035
|
.fx-th{text-align:left;padding:.875rem 1.25rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em}
|
|
1039
|
-
.
|
|
1040
|
-
.
|
|
1036
|
+
.aip-theme-dark .fx-th{color:#475569;border-bottom:1px solid #1e293b}
|
|
1037
|
+
.aip-theme-light .fx-th{color:#94a3b8;border-bottom:1px solid #e2e8f0}
|
|
1041
1038
|
.fx-tr{transition:background .1s}
|
|
1042
1039
|
.fx-td{padding:.875rem 1.25rem}
|
|
1043
|
-
.
|
|
1044
|
-
.
|
|
1045
|
-
.
|
|
1046
|
-
.
|
|
1040
|
+
.aip-theme-dark .fx-tr:hover{background:#0f172a}
|
|
1041
|
+
.aip-theme-light .fx-tr:hover{background:#f8fafc}
|
|
1042
|
+
.aip-theme-dark .fx-td{border-bottom:1px solid #0f172a}
|
|
1043
|
+
.aip-theme-light .fx-td{border-bottom:1px solid #f1f5f9}
|
|
1047
1044
|
.fx-td-empty{padding:2rem 1.25rem;text-align:center;opacity:.4}
|
|
1048
1045
|
.fx-list-wrap{padding:1rem 2.5rem 4rem;display:flex;flex-direction:column;gap:.75rem}
|
|
1049
1046
|
.fx-list-item{border-radius:.75rem;padding:1.25rem 1.5rem}
|
|
1050
|
-
.
|
|
1047
|
+
.aip-theme-dark .fx-list-item{background:#0f172a;border:1px solid #1e293b}
|
|
1051
1048
|
.fx-list-field{font-size:.9375rem;line-height:1.5}
|
|
1052
1049
|
.fx-list-link{font-size:.8125rem;font-weight:600;opacity:.6;transition:opacity .15s}
|
|
1053
1050
|
.fx-list-link:hover{opacity:1}
|
|
1054
1051
|
.fx-alert{padding:1rem 2.5rem;font-size:.9375rem;font-weight:500;border-radius:.75rem;margin:1rem 2.5rem}
|
|
1055
|
-
.
|
|
1052
|
+
.aip-theme-dark .fx-alert{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:#fca5a5}
|
|
1056
1053
|
.fx-if-wrap{display:contents}
|
|
1057
1054
|
.fx-footer{padding:3rem 2.5rem;text-align:center}
|
|
1058
|
-
.
|
|
1059
|
-
.
|
|
1055
|
+
.aip-theme-dark .fx-footer{border-top:1px solid #1e293b}
|
|
1056
|
+
.aip-theme-light .fx-footer{border-top:1px solid #e2e8f0}
|
|
1060
1057
|
.fx-footer-text{font-size:.8125rem}
|
|
1061
|
-
.
|
|
1058
|
+
.aip-theme-dark .fx-footer-text{color:#334155}
|
|
1062
1059
|
.fx-footer-link{font-size:.8125rem;margin:0 .75rem;opacity:.5;transition:opacity .15s}
|
|
1063
1060
|
.fx-footer-link:hover{opacity:1}
|
|
1064
1061
|
`
|
package/server/server.js
CHANGED
|
@@ -752,13 +752,14 @@ td{padding:.875rem 1.25rem;border-bottom:1px solid rgba(255,255,255,.04);color:#
|
|
|
752
752
|
</div>
|
|
753
753
|
<script>
|
|
754
754
|
const prefix = '${prefix}'
|
|
755
|
-
//
|
|
756
|
-
function getAdminToken() {
|
|
757
|
-
const cookie = document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('aiplang_admin='))
|
|
758
|
-
return cookie ? cookie.split('=')[1] : (localStorage.getItem('admin_token') || '')
|
|
759
|
-
}
|
|
755
|
+
// API calls use credentials:include — HttpOnly cookie is sent automatically
|
|
760
756
|
async function api(method, path, body) {
|
|
761
|
-
const r = await fetch(prefix + '/api' + path, {
|
|
757
|
+
const r = await fetch(prefix + '/api' + path, {
|
|
758
|
+
method,
|
|
759
|
+
headers: {'Content-Type':'application/json'},
|
|
760
|
+
credentials: 'same-origin',
|
|
761
|
+
body: body ? JSON.stringify(body) : undefined
|
|
762
|
+
})
|
|
762
763
|
return r.json()
|
|
763
764
|
}
|
|
764
765
|
async function loadModel(name, page=1) {
|
|
@@ -794,13 +795,9 @@ function renderAdminLogin(prefix) {
|
|
|
794
795
|
<button onclick="login()">Sign in</button></div>
|
|
795
796
|
<script>
|
|
796
797
|
async function login(){
|
|
797
|
-
const r=await fetch('/
|
|
798
|
+
const r=await fetch('${prefix}/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:document.getElementById('email').value,password:document.getElementById('pass').value}),credentials:'same-origin'})
|
|
798
799
|
const d=await r.json()
|
|
799
|
-
if(d.
|
|
800
|
-
localStorage.setItem('admin_token',d.token);
|
|
801
|
-
document.cookie='aiplang_admin='+d.token+';path=/;SameSite=Strict;max-age=86400';
|
|
802
|
-
location.href='${prefix}'
|
|
803
|
-
}
|
|
800
|
+
if(d.ok){location.href='${prefix}'}
|
|
804
801
|
else document.getElementById('err').textContent=d.error||'Invalid credentials'
|
|
805
802
|
}
|
|
806
803
|
document.addEventListener('keydown',e=>{if(e.key==='Enter')login()})
|
|
@@ -891,7 +888,16 @@ function matchRoute(pattern, reqPath) {
|
|
|
891
888
|
for(let i=0;i<pp.length;i++){if(pp[i].startsWith(':'))params[pp[i].slice(1)]=rp[i];else if(pp[i]!==rp[i])return null}
|
|
892
889
|
return params
|
|
893
890
|
}
|
|
894
|
-
function extractToken(req) {
|
|
891
|
+
function extractToken(req) {
|
|
892
|
+
// 1. Bearer token from Authorization header
|
|
893
|
+
const a = req.headers.authorization
|
|
894
|
+
if (a?.startsWith('Bearer ')) return a.slice(7)
|
|
895
|
+
// 2. HttpOnly cookie (admin panel)
|
|
896
|
+
const cookies = req.headers['cookie'] || ''
|
|
897
|
+
const adminCookie = cookies.split(';').map(s=>s.trim()).find(s=>s.startsWith('aiplang_admin='))
|
|
898
|
+
if (adminCookie) return adminCookie.slice('aiplang_admin='.length)
|
|
899
|
+
return null
|
|
900
|
+
}
|
|
895
901
|
const MAX_BODY_BYTES = parseInt(process.env.MAX_BODY_BYTES || '1048576') // 1MB default
|
|
896
902
|
async function parseBody(req) {
|
|
897
903
|
return new Promise((resolve) => {
|
|
@@ -1206,7 +1212,7 @@ function getMime(filename) {
|
|
|
1206
1212
|
// name: 'my-plugin',
|
|
1207
1213
|
// setup(server, app, utils) {
|
|
1208
1214
|
// // server = AiplangServer instance (.addRoute, .models)
|
|
1209
|
-
// // app = parsed .
|
|
1215
|
+
// // app = parsed .aip app definition
|
|
1210
1216
|
// // utils = { uuid, now, emit, on, dispatch, resolveEnv, dbRun, dbAll, dbGet }
|
|
1211
1217
|
// }
|
|
1212
1218
|
// }
|
|
@@ -1429,7 +1435,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1429
1435
|
|
|
1430
1436
|
// Health
|
|
1431
1437
|
srv.addRoute('GET', '/health', (req, res) => res.json(200, {
|
|
1432
|
-
status:'ok', version:'2.6.
|
|
1438
|
+
status:'ok', version:'2.6.2',
|
|
1433
1439
|
models: app.models.map(m=>m.name),
|
|
1434
1440
|
routes: app.apis.length, pages: app.pages.length,
|
|
1435
1441
|
admin: app.admin?.prefix || null,
|
|
@@ -1456,7 +1462,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1456
1462
|
module.exports = { startServer, parseApp, Model, getDB, dispatch, on, emit, sendMail, setupStripe, registerStripeRoutes, setupS3, registerS3Routes, s3Upload, s3Delete, s3PresignedUrl, PLUGIN_UTILS }
|
|
1457
1463
|
if (require.main === module) {
|
|
1458
1464
|
const f=process.argv[2], p=parseInt(process.argv[3]||process.env.PORT||'3000')
|
|
1459
|
-
if (!f) { console.error('Usage: node server.js <app.
|
|
1465
|
+
if (!f) { console.error('Usage: node server.js <app.aip> [port]'); process.exit(1) }
|
|
1460
1466
|
startServer(f, p).catch(e=>{console.error(e);process.exit(1)})
|
|
1461
1467
|
}
|
|
1462
1468
|
|