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 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.aiplang` → see it live.
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 `.aiplang` file describes a complete app: frontend, backend, database, auth, email, jobs.
19
+ A single `.aip` file describes a complete app: frontend, backend, database, auth, email, jobs.
20
20
 
21
- ```aiplang
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.aiplang # full-stack server (Node.js)
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 ← .aiplang → AST parser
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)
@@ -1,338 +1,98 @@
1
- # aiplang — Claude Project Knowledge
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
- You are an aiplang code generator. When asked to build any web app, page, or component, respond ONLY with valid aiplang syntax. No explanation unless asked. No React, no HTML, no other frameworks.
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 DATABASE_URL required
23
- ~db sqlite ./app.db (or postgres $DATABASE_URL)
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 User {
207
- id : uuid : pk auto
208
- name : text : required
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 POST /api/auth/register {
217
- ~validate name required | email required email | password min=8
218
- ~unique User email $body.email | 409
219
- ~hash password
220
- insert User($body)
221
- return jwt($inserted) 201
222
- }
223
-
224
- api POST /api/auth/login {
225
- $user = User.findBy(email=$body.email)
226
- ~check password $body.password $user.password | 401
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
- api GET /api/users {
231
- ~guard admin
232
- return User.paginate(1, 20)
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
- nav{MySaaS>/pricing:Pricing>/login:Sign in>/signup:Get started}
245
- hero{Ship faster with AI|Zero config. Deploy in seconds.>/signup:Start free>/demo:View demo} animate:blur-in
246
- stats{@stats:Users|99.9%:Uptime|$49:Starting price}
247
- row3{rocket>Deploy instantly>Push to git, live in seconds.|shield>Enterprise ready>SOC2, GDPR, SSO built-in.|chart>Full observability>Real-time errors and performance.} animate:stagger
248
- testimonial{Sarah Chen, CEO @ Acme|"Cut deployment time by 90%."|img:https://i.pravatar.cc/64?img=47} animate:fade-up
249
- pricing{Starter>Free>3 projects>/signup:Get started|Pro>$29/mo>Unlimited>/signup:Start trial|Enterprise>Custom>SSO>/contact:Talk}
250
- faq{How to start?>Sign up free, no credit card.|Cancel anytime?>Yes, one click, no questions.}
251
- foot{© 2025 MySaaS>/privacy:Privacy>/terms:Terms}
252
-
253
- ---
254
-
255
- %dashboard dark /dashboard
256
-
257
- @users = []
258
- @stats = {}
259
- ~mount GET /api/users => @users
260
- ~mount GET /api/stats => @stats
261
- ~interval 30000 GET /api/stats => @stats
262
-
263
- nav{MySaaS>/settings:Settings>/logout:Sign out}
264
- stats{@stats:Total users|@stats:Active|$0:MRR}
265
- sect{User database}
266
- table @users { Name:name | Email:email | Plan:plan | Status:status | edit PUT /api/users/{id} | delete /api/users/{id} | empty: No users yet. }
267
- sect{Add user}
268
- form POST /api/users => @users.push($result) { Full name:text:Alice Johnson | Email:email:alice@co.com | Plan:select:starter,pro,enterprise }
269
- foot{MySaaS Dashboard © 2025}
270
-
271
- ---
272
-
273
- %login dark /login
274
-
275
- nav{MySaaS>/signup:Create account}
276
- hero{Welcome back|Sign in to continue.}
277
- form POST /api/auth/login => redirect /dashboard { Email:email:you@company.com | Password:password: }
278
- foot{© 2025 MySaaS>/signup:Create account}
279
-
280
- ---
281
-
282
- %signup dark /signup
283
-
284
- nav{MySaaS>/login:Sign in}
285
- hero{Start for free|No credit card required.}
286
- form POST /api/auth/register => redirect /dashboard { Full name:text:Alice | Email:email:alice@co.com | Password:password: }
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.0'
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.aiplang use a local .aiplang file as template
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.aiplang start full-stack server (API + DB + frontend)
48
- PORT=8080 aiplang start app.aiplang custom port
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 .aiplang file as template
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 .aiplang file
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 ~/.aiplang/templates/<name>.aiplang
79
+ // Custom templates stored at ~/.aip/templates/<name>.aip
80
80
  // ─────────────────────────────────────────────────────────────────
81
81
 
82
- const TEMPLATES_DIR = path.join(require('os').homedir(), '.aiplang', 'templates')
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.aiplang to get started.>/signup:Get started} animate:fade-up
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.aiplang or --template /abs/path.aiplang
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: ~/.aiplang/templates/<name>.aiplang
243
- const customPath = path.join(TEMPLATES_DIR, tplName + '.aiplang')
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('.aiplang')).map(f=>f.replace('.aiplang',''))
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('.aiplang')).map(f=>f.replace('.aiplang',''))
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.aiplang
299
- const sources = ['pages', 'app.aiplang', 'index.aiplang']
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 .aiplang files found. Use --from <file> to specify source.\n'); process.exit(1) }
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('.aiplang'))
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 + '.aiplang')
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 + '.aiplang')
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 + '.aiplang')
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 + '.aiplang')
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}.aiplang`
358
- const customPath = path.join(TEMPLATES_DIR, tname + '.aiplang')
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.aiplang
387
+ // Full-stack project: single app.aip
388
388
  fs.mkdirSync(dir, { recursive: true })
389
- fs.writeFileSync(path.join(dir, 'app.aiplang'), tplSrc)
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.aiplang', start: 'npx aiplang start app.aiplang' },
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.aiplang\n\`\`\`\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.aip\n\`\`\`\n`)
398
398
  const label = tplName !== 'default' ? ` (template: ${tplName})` : ''
399
- console.log(`\n ✓ Created ${name}/${label}\n\n app.aiplang ← full-stack app (backend + frontend)\n\n Next:\n cd ${name} && npx aiplang start app.aiplang\n`)
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/*.aiplang
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}.aiplang`), block.trim())
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.aiplang'), tplSrc)
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.aiplang ← edit this\n\n Next:\n cd ${name} && npx aiplang serve\n`)
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}.aiplang`)
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('.aiplang')).forEach(f=>files.push(path.join(input,f)))
454
- } else if(input.endsWith('.aiplang')&&fs.existsSync(input)){ files.push(input) }
455
- if(!files.length){console.error(`\n ✗ No .aiplang files in: ${input}\n`);process.exit(1)}
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('.aiplang')).forEach(f=>fs.copyFileSync(path.join('public',f),path.join(outDir,f)))
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','.aiplang':'text/plain','.json':'application/json','.wasm':'application/wasm','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.ico':'image/x-icon'}
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('.aiplang')).forEach(f=>{
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('.aiplang')){const c=path.join(root,'pages',path.basename(p));if(fs.existsSync(c))fp=c}
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 .aiplang files and browser refreshes.\n Ctrl+C to stop.\n`))
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.aiplang>\n`)
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.0",
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": "^6.9.0",
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 fn = new Function(
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
- .aiplang-root{min-height:100vh}
961
- .aiplang-theme-dark{background:#030712;color:#f1f5f9}
962
- .aiplang-theme-light{background:#fff;color:#0f172a}
963
- .aiplang-theme-acid{background:#000;color:#a3e635}
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
- .aiplang-theme-dark .fx-nav{border-bottom:1px solid #1e293b;background:rgba(3,7,18,.85)}
966
- .aiplang-theme-light .fx-nav{border-bottom:1px solid #e2e8f0;background:rgba(255,255,255,.85)}
967
- .aiplang-theme-acid .fx-nav{border-bottom:1px solid #1a2e05;background:rgba(0,0,0,.9)}
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
- .aiplang-theme-dark .fx-nav-link{color:#cbd5e1}
973
- .aiplang-theme-light .fx-nav-link{color:#475569}
974
- .aiplang-theme-acid .fx-nav-link{color:#86efac}
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
- .aiplang-theme-dark .fx-sub{color:#94a3b8}
980
- .aiplang-theme-light .fx-sub{color:#475569}
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
- .aiplang-theme-dark .fx-cta{background:#2563eb;color:#fff;box-shadow:0 8px 24px rgba(37,99,235,.35)}
984
- .aiplang-theme-light .fx-cta{background:#2563eb;color:#fff}
985
- .aiplang-theme-acid .fx-cta{background:#a3e635;color:#000;font-weight:800}
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
- .aiplang-theme-dark .fx-stat-lbl{color:#64748b}
990
- .aiplang-theme-light .fx-stat-lbl{color:#94a3b8}
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
- .aiplang-theme-dark .fx-card{background:#0f172a;border:1px solid #1e293b}
998
- .aiplang-theme-light .fx-card{background:#f8fafc;border:1px solid #e2e8f0}
999
- .aiplang-theme-acid .fx-card{background:#0a0f00;border:1px solid #1a2e05}
1000
- .aiplang-theme-dark .fx-card:hover{box-shadow:0 20px 40px rgba(0,0,0,.5)}
1001
- .aiplang-theme-light .fx-card:hover{box-shadow:0 20px 40px rgba(0,0,0,.08)}
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
- .aiplang-theme-dark .fx-card-body{color:#64748b}
1006
- .aiplang-theme-light .fx-card-body{color:#475569}
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
- .aiplang-theme-dark .fx-sect-body{color:#64748b}
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
- .aiplang-theme-dark .fx-form{background:#0f172a;border:1px solid #1e293b}
1016
- .aiplang-theme-light .fx-form{background:#f8fafc;border:1px solid #e2e8f0}
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
- .aiplang-theme-dark .fx-label{color:#94a3b8}
1020
- .aiplang-theme-light .fx-label{color:#475569}
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
- .aiplang-theme-dark .fx-input{background:#020617;border:1px solid #1e293b;color:#f1f5f9}
1024
- .aiplang-theme-dark .fx-input::placeholder{color:#334155}
1025
- .aiplang-theme-light .fx-input{background:#fff;border:1px solid #cbd5e1;color:#0f172a}
1026
- .aiplang-theme-acid .fx-input{background:#000;border:1px solid #1a2e05;color:#a3e635}
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
- .aiplang-theme-dark .fx-btn{background:#2563eb;color:#fff;box-shadow:0 4px 14px rgba(37,99,235,.4)}
1031
- .aiplang-theme-light .fx-btn{background:#2563eb;color:#fff}
1032
- .aiplang-theme-acid .fx-btn{background:#a3e635;color:#000;font-weight:800}
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
- .aiplang-theme-dark .fx-th{color:#475569;border-bottom:1px solid #1e293b}
1040
- .aiplang-theme-light .fx-th{color:#94a3b8;border-bottom:1px solid #e2e8f0}
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
- .aiplang-theme-dark .fx-tr:hover{background:#0f172a}
1044
- .aiplang-theme-light .fx-tr:hover{background:#f8fafc}
1045
- .aiplang-theme-dark .fx-td{border-bottom:1px solid #0f172a}
1046
- .aiplang-theme-light .fx-td{border-bottom:1px solid #f1f5f9}
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
- .aiplang-theme-dark .fx-list-item{background:#0f172a;border:1px solid #1e293b}
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
- .aiplang-theme-dark .fx-alert{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:#fca5a5}
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
- .aiplang-theme-dark .fx-footer{border-top:1px solid #1e293b}
1059
- .aiplang-theme-light .fx-footer{border-top:1px solid #e2e8f0}
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
- .aiplang-theme-dark .fx-footer-text{color:#334155}
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
- // Token from cookie (set by login) or fallback to localStorage for compat
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, {method, headers:{'Content-Type':'application/json','Authorization':'Bearer '+getAdminToken()},body:body?JSON.stringify(body):undefined})
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('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:document.getElementById('email').value,password:document.getElementById('pass').value})})
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.token){
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) { const a=req.headers.authorization; return a?.startsWith('Bearer ')?a.slice(7):null }
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 .aiplang app definition
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.0',
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.aiplang> [port]'); process.exit(1) }
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