aiplang 2.6.1 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/aiplang-knowledge.md +104 -62
- package/bin/aiplang.js +60 -43
- package/package.json +1 -1
- package/runtime/aiplang-runtime.js +47 -47
- package/server/server.js +3 -3
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,97 +1,139 @@
|
|
|
1
|
-
# aiplang —
|
|
2
|
-
Generate ONLY
|
|
1
|
+
# aiplang — LLM reference
|
|
2
|
+
Generate ONLY `.aip` syntax. Zero explanation. Zero React. Zero HTML frameworks.
|
|
3
3
|
|
|
4
|
-
##
|
|
4
|
+
## Config directives (before model/api/pages)
|
|
5
5
|
```
|
|
6
|
-
~env VAR required
|
|
7
|
-
~db sqlite ./app.db
|
|
6
|
+
~env VAR required # fail if missing
|
|
7
|
+
~db sqlite ./app.db # or: postgres $DATABASE_URL
|
|
8
8
|
~auth jwt $JWT_SECRET expire=7d
|
|
9
|
-
~mail smtp host=x user=$U pass=$P
|
|
10
|
-
~s3 $KEY secret=$S bucket=$B region=us-east-1 prefix=
|
|
9
|
+
~mail smtp host=x user=$U pass=$P from=no-reply@x.com
|
|
10
|
+
~s3 $KEY secret=$S bucket=$B region=us-east-1 prefix=up/ maxSize=10mb allow=image/jpeg,image/png
|
|
11
11
|
~stripe $KEY webhook=$WH success=/ok cancel=/pricing
|
|
12
12
|
~plan free=price_x pro=price_y
|
|
13
13
|
~admin /admin
|
|
14
14
|
~use cors origins=https://x.com
|
|
15
15
|
~use rate-limit max=100 window=60s
|
|
16
16
|
~use helmet | ~use logger | ~use compression
|
|
17
|
-
~plugin ./
|
|
17
|
+
~plugin ./plugins/custom.js
|
|
18
|
+
```
|
|
18
19
|
|
|
20
|
+
## Model
|
|
21
|
+
```
|
|
19
22
|
model Name {
|
|
20
|
-
id
|
|
21
|
-
field
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
id : uuid : pk auto
|
|
24
|
+
field : text : required
|
|
25
|
+
email : text : required unique
|
|
26
|
+
password : text : required hashed
|
|
27
|
+
count : int : default=0
|
|
28
|
+
price : float
|
|
29
|
+
active : bool : default=true
|
|
30
|
+
at : timestamp
|
|
31
|
+
data : json
|
|
32
|
+
kind : enum : a,b,c : default=a
|
|
33
|
+
~soft-delete # adds deleted_at, filters from queries
|
|
34
|
+
~belongs OtherModel # adds other_model_id FK
|
|
24
35
|
}
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
```
|
|
37
|
+
Types: `uuid text int float bool timestamp json enum`
|
|
38
|
+
Modifiers: `pk auto required unique hashed default=val index`
|
|
27
39
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
~
|
|
40
|
+
## API — ALWAYS one blank line between ops, return is last
|
|
41
|
+
```
|
|
42
|
+
api POST /path {
|
|
43
|
+
~guard auth # auth | admin | subscribed | owner
|
|
44
|
+
~validate field required | field email | field min=8 | field max=100 | field numeric | field in:a,b,c
|
|
32
45
|
~unique Model field $body.field | 409
|
|
33
|
-
~hash field
|
|
34
|
-
~check password $body.pw $user.pw | 401
|
|
46
|
+
~hash field # bcrypt the field before insert
|
|
47
|
+
~check password $body.pw $user.pw | 401 # bcrypt compare
|
|
48
|
+
~query page=1 limit=20 # query params with defaults
|
|
35
49
|
~mail $user.email "Subject" "Body"
|
|
36
50
|
~dispatch jobName $body
|
|
37
51
|
~emit event.name $body
|
|
38
|
-
$var = Model.findBy(field=$body.field)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return
|
|
45
|
-
return
|
|
52
|
+
$var = Model.findBy(field=$body.field) # assign to $var
|
|
53
|
+
$var = Model.find($params.id)
|
|
54
|
+
insert Model($body) # sets $inserted
|
|
55
|
+
update Model($params.id, $body) # sets $updated
|
|
56
|
+
delete Model($params.id) # soft or hard delete
|
|
57
|
+
restore Model($params.id)
|
|
58
|
+
return $inserted 201 # status optional, default 200
|
|
59
|
+
return $updated
|
|
60
|
+
return $var
|
|
61
|
+
return $auth.user
|
|
62
|
+
return jwt($inserted) 201 # return JWT token
|
|
46
63
|
return jwt($user) 200
|
|
64
|
+
return Model.all(order=created_at desc)
|
|
65
|
+
return Model.paginate($page, $limit)
|
|
66
|
+
return Model.count()
|
|
67
|
+
return Model.sum(field)
|
|
68
|
+
return Model.findBy(field=$body.field)
|
|
47
69
|
}
|
|
70
|
+
```
|
|
48
71
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
~
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
## Pages
|
|
73
|
+
```
|
|
74
|
+
%id theme /route # id=any, theme=dark|light|acid|#bg,#text,#accent, route=/path
|
|
75
|
+
~theme accent=#hex bg=#hex text=#hex font=Name radius=1rem surface=#hex navbg=#hex spacing=5rem
|
|
76
|
+
@list = [] # reactive state — [] for arrays, {} for objects, "str", 0
|
|
77
|
+
@obj = {}
|
|
78
|
+
~mount GET /api/path => @list # fetch on page load → assign to @list
|
|
79
|
+
~mount GET /api/path => @obj
|
|
80
|
+
~interval 15000 GET /api/path => @obj # repeat every N ms (always pair with ~mount)
|
|
57
81
|
```
|
|
58
82
|
|
|
59
83
|
## All blocks
|
|
60
84
|
```
|
|
61
85
|
nav{Brand>/path:Link>/path:Link}
|
|
62
|
-
hero{Title|
|
|
63
|
-
|
|
86
|
+
hero{Title|Subtitle>/path:CTA}
|
|
87
|
+
hero{Title|Sub>/path:CTA|img:https://url} # split layout with image
|
|
88
|
+
stats{@obj.field:Label|99%:Uptime|$0:Free}
|
|
64
89
|
row2{icon>Title>Body} | row3{...} | row4{...}
|
|
65
90
|
sect{Title|Optional body}
|
|
66
|
-
table @list { Col:field | edit PUT /api/{id} | delete /api/{id} | empty: msg }
|
|
67
|
-
form POST /api => @list.push($result) { Label:type:placeholder | Label:select:a,b,c }
|
|
68
|
-
form POST /api => redirect /
|
|
69
|
-
pricing{Name>
|
|
70
|
-
faq{Question?>Answer
|
|
71
|
-
testimonial{Name, Role
|
|
72
|
-
gallery{https://img1|https://img2|https://img3}
|
|
73
|
-
btn{Label > METHOD /api/path}
|
|
91
|
+
table @list { Col:field | Col:field | edit PUT /api/path/{id} | delete /api/path/{id} | empty: msg }
|
|
92
|
+
form POST /api/path => @list.push($result) { Label:type:placeholder | Label:select:a,b,c }
|
|
93
|
+
form POST /api/path => redirect /dashboard { Label:type | Label:password }
|
|
94
|
+
pricing{Name>$0/mo>Desc>/path:CTA | Name>$19/mo>Desc>/path:CTA}
|
|
95
|
+
faq{Question?>Answer. | Q2?>A2.}
|
|
96
|
+
testimonial{Name, Role|"Quote."|img:https://url}
|
|
97
|
+
gallery{https://img1 | https://img2 | https://img3}
|
|
98
|
+
btn{Label > METHOD /api/path}
|
|
99
|
+
btn{Label > DELETE /api/path > confirm:Are you sure?}
|
|
74
100
|
select @filterVar { All | Active | Inactive }
|
|
75
101
|
if @var { blocks }
|
|
76
|
-
raw{<div>any HTML</div>}
|
|
102
|
+
raw{<div>any HTML or embed</div>}
|
|
77
103
|
foot{© 2025 Name>/path:Link}
|
|
78
104
|
```
|
|
79
105
|
|
|
80
|
-
##
|
|
81
|
-
`animate:fade-up
|
|
82
|
-
`class:my-class`
|
|
106
|
+
## Animate any block
|
|
107
|
+
`block{...} animate:fade-up` · `animate:fade-in` · `animate:blur-in` · `animate:stagger` · `animate:slide-left` · `animate:zoom-in`
|
|
83
108
|
|
|
84
|
-
##
|
|
85
|
-
|
|
109
|
+
## Multiple pages — separate with ---
|
|
110
|
+
```
|
|
111
|
+
%home dark /
|
|
112
|
+
nav{...}
|
|
113
|
+
hero{...}
|
|
114
|
+
---
|
|
115
|
+
%dashboard dark /dashboard
|
|
116
|
+
@data = []
|
|
117
|
+
~mount GET /api/data => @data
|
|
118
|
+
table @data { ... }
|
|
119
|
+
---
|
|
120
|
+
%login dark /login
|
|
121
|
+
form POST /api/auth/login => redirect /dashboard { Email:email | Password:password }
|
|
122
|
+
```
|
|
86
123
|
|
|
87
|
-
##
|
|
88
|
-
`
|
|
124
|
+
## Icons (use in row blocks)
|
|
125
|
+
`rocket bolt shield chart star check globe gear fire money bell mail user lock eye tag search home`
|
|
89
126
|
|
|
90
|
-
##
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
127
|
+
## S3 auto-routes (generated when ~s3 configured)
|
|
128
|
+
`POST /api/upload` · `DELETE /api/upload/:key` · `GET /api/upload/presign?key=x&expires=3600`
|
|
129
|
+
|
|
130
|
+
## Stripe auto-routes (generated when ~stripe configured)
|
|
131
|
+
`POST /api/stripe/checkout {plan,email}` · `POST /api/stripe/portal` · `GET /api/stripe/subscription` · `POST /api/stripe/webhook`
|
|
132
|
+
Webhooks auto-handled: checkout.completed → sets user.plan, subscription.deleted → resets to free
|
|
133
|
+
|
|
134
|
+
## Run
|
|
135
|
+
```bash
|
|
136
|
+
npx aiplang start app.aip # full-stack
|
|
137
|
+
npx aiplang serve # frontend dev
|
|
138
|
+
npx aiplang build pages/ # static build
|
|
139
|
+
```
|
package/bin/aiplang.js
CHANGED
|
@@ -5,7 +5,7 @@ const fs = require('fs')
|
|
|
5
5
|
const path = require('path')
|
|
6
6
|
const http = require('http')
|
|
7
7
|
|
|
8
|
-
const VERSION = '2.
|
|
8
|
+
const VERSION = '2.7.0'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -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 })
|
|
@@ -121,6 +121,23 @@ api GET /api/me {
|
|
|
121
121
|
return $auth.user
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
api GET /api/users {
|
|
125
|
+
~guard admin
|
|
126
|
+
~query page=1
|
|
127
|
+
return User.paginate($page, 20)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
api PUT /api/users/:id {
|
|
131
|
+
~guard admin
|
|
132
|
+
update User($id, $body)
|
|
133
|
+
return $updated
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
api DELETE /api/users/:id {
|
|
137
|
+
~guard admin
|
|
138
|
+
delete User($id)
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
api GET /api/stats {
|
|
125
142
|
return User.count()
|
|
126
143
|
}
|
|
@@ -220,7 +237,7 @@ foot{{{name}}}`,
|
|
|
220
237
|
default: `# {{name}}
|
|
221
238
|
%home dark /
|
|
222
239
|
nav{{{name}}>/login:Sign in}
|
|
223
|
-
hero{Welcome to {{name}}|Edit pages/home.
|
|
240
|
+
hero{Welcome to {{name}}|Edit pages/home.aip to get started.>/signup:Get started} animate:fade-up
|
|
224
241
|
row3{rocket>Fast>Renders in under 1ms.|bolt>AI-native>Written by Claude in seconds.|globe>Deploy anywhere>Static files. Any host.}
|
|
225
242
|
foot{© {{year}} {{name}}}`,
|
|
226
243
|
}
|
|
@@ -232,15 +249,15 @@ function applyTemplateVars(src, name, year) {
|
|
|
232
249
|
function getTemplate(tplName, name, year) {
|
|
233
250
|
ensureTemplatesDir()
|
|
234
251
|
|
|
235
|
-
// 1. Local file path: --template ./my-template.
|
|
252
|
+
// 1. Local file path: --template ./my-template.aip or --template /abs/path.aip
|
|
236
253
|
if (tplName.startsWith('./') || tplName.startsWith('../') || tplName.startsWith('/')) {
|
|
237
254
|
const full = path.resolve(tplName)
|
|
238
255
|
if (!fs.existsSync(full)) { console.error(`\n ✗ Template file not found: ${full}\n`); process.exit(1) }
|
|
239
256
|
return applyTemplateVars(fs.readFileSync(full, 'utf8'), name, year)
|
|
240
257
|
}
|
|
241
258
|
|
|
242
|
-
// 2. User custom template: ~/.
|
|
243
|
-
const customPath = path.join(TEMPLATES_DIR, tplName + '.
|
|
259
|
+
// 2. User custom template: ~/.aip/templates/<name>.aip
|
|
260
|
+
const customPath = path.join(TEMPLATES_DIR, tplName + '.aip')
|
|
244
261
|
if (fs.existsSync(customPath)) {
|
|
245
262
|
return applyTemplateVars(fs.readFileSync(customPath, 'utf8'), name, year)
|
|
246
263
|
}
|
|
@@ -251,7 +268,7 @@ function getTemplate(tplName, name, year) {
|
|
|
251
268
|
|
|
252
269
|
// Not found — show what's available
|
|
253
270
|
const customs = fs.existsSync(TEMPLATES_DIR)
|
|
254
|
-
? fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.
|
|
271
|
+
? fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.aip')).map(f=>f.replace('.aip',''))
|
|
255
272
|
: []
|
|
256
273
|
const all = [...Object.keys(BUILTIN_TEMPLATES).filter(k=>k!=='default'), ...customs]
|
|
257
274
|
console.error(`\n ✗ Template "${tplName}" not found.\n Available: ${all.join(', ')}\n`)
|
|
@@ -261,7 +278,7 @@ function getTemplate(tplName, name, year) {
|
|
|
261
278
|
function listTemplates() {
|
|
262
279
|
ensureTemplatesDir()
|
|
263
280
|
const builtins = Object.keys(BUILTIN_TEMPLATES).filter(k=>k!=='default')
|
|
264
|
-
const customs = fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.
|
|
281
|
+
const customs = fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.aip')).map(f=>f.replace('.aip',''))
|
|
265
282
|
console.log(`\n aiplang templates\n`)
|
|
266
283
|
console.log(` Built-in:`)
|
|
267
284
|
builtins.forEach(t => console.log(` ${t}`))
|
|
@@ -295,18 +312,18 @@ if (cmd === 'template') {
|
|
|
295
312
|
if (!fs.existsSync(fp)) { console.error(`\n ✗ File not found: ${fp}\n`); process.exit(1) }
|
|
296
313
|
src = fs.readFileSync(fp, 'utf8')
|
|
297
314
|
} else {
|
|
298
|
-
// Auto-detect: use pages/ directory or app.
|
|
299
|
-
const sources = ['pages', 'app.
|
|
315
|
+
// Auto-detect: use pages/ directory or app.aip
|
|
316
|
+
const sources = ['pages', 'app.aip', 'index.aip']
|
|
300
317
|
const found = sources.find(s => fs.existsSync(s))
|
|
301
|
-
if (!found) { console.error('\n ✗ No .
|
|
318
|
+
if (!found) { console.error('\n ✗ No .aip files found. Use --from <file> to specify source.\n'); process.exit(1) }
|
|
302
319
|
if (fs.statSync(found).isDirectory()) {
|
|
303
|
-
src = fs.readdirSync(found).filter(f=>f.endsWith('.
|
|
320
|
+
src = fs.readdirSync(found).filter(f=>f.endsWith('.aip'))
|
|
304
321
|
.map(f => fs.readFileSync(path.join(found,f),'utf8')).join('\n---\n')
|
|
305
322
|
} else {
|
|
306
323
|
src = fs.readFileSync(found, 'utf8')
|
|
307
324
|
}
|
|
308
325
|
}
|
|
309
|
-
const dest = path.join(TEMPLATES_DIR, tname + '.
|
|
326
|
+
const dest = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
310
327
|
fs.writeFileSync(dest, src)
|
|
311
328
|
console.log(`\n ✓ Template saved: ${tname}\n ${dest}\n\n Use it: aiplang init my-app --template ${tname}\n`)
|
|
312
329
|
process.exit(0)
|
|
@@ -316,7 +333,7 @@ if (cmd === 'template') {
|
|
|
316
333
|
if (sub === 'remove' || sub === 'rm' || sub === 'delete') {
|
|
317
334
|
const tname = args[1]
|
|
318
335
|
if (!tname) { console.error('\n ✗ Usage: aiplang template remove <name>\n'); process.exit(1) }
|
|
319
|
-
const dest = path.join(TEMPLATES_DIR, tname + '.
|
|
336
|
+
const dest = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
320
337
|
if (!fs.existsSync(dest)) { console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1) }
|
|
321
338
|
fs.unlinkSync(dest)
|
|
322
339
|
console.log(`\n ✓ Removed template: ${tname}\n`); process.exit(0)
|
|
@@ -326,7 +343,7 @@ if (cmd === 'template') {
|
|
|
326
343
|
if (sub === 'edit' || sub === 'open') {
|
|
327
344
|
const tname = args[1]
|
|
328
345
|
if (!tname) { console.error('\n ✗ Usage: aiplang template edit <name>\n'); process.exit(1) }
|
|
329
|
-
let dest = path.join(TEMPLATES_DIR, tname + '.
|
|
346
|
+
let dest = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
330
347
|
if (!fs.existsSync(dest)) {
|
|
331
348
|
// create from built-in if exists
|
|
332
349
|
const builtin = BUILTIN_TEMPLATES[tname]
|
|
@@ -342,7 +359,7 @@ if (cmd === 'template') {
|
|
|
342
359
|
// aiplang template show <name>
|
|
343
360
|
if (sub === 'show' || sub === 'cat') {
|
|
344
361
|
const tname = args[1] || 'default'
|
|
345
|
-
const customPath = path.join(TEMPLATES_DIR, tname + '.
|
|
362
|
+
const customPath = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
346
363
|
if (fs.existsSync(customPath)) { console.log(fs.readFileSync(customPath,'utf8')); process.exit(0) }
|
|
347
364
|
const builtin = BUILTIN_TEMPLATES[tname]
|
|
348
365
|
if (builtin) { console.log(builtin); process.exit(0) }
|
|
@@ -354,8 +371,8 @@ if (cmd === 'template') {
|
|
|
354
371
|
const tname = args[1]
|
|
355
372
|
if (!tname) { console.error('\n ✗ Usage: aiplang template export <name>\n'); process.exit(1) }
|
|
356
373
|
const outIdx = args.indexOf('--out')
|
|
357
|
-
const outFile = outIdx !== -1 ? args[outIdx+1] : `./${tname}.
|
|
358
|
-
const customPath = path.join(TEMPLATES_DIR, tname + '.
|
|
374
|
+
const outFile = outIdx !== -1 ? args[outIdx+1] : `./${tname}.aip`
|
|
375
|
+
const customPath = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
359
376
|
const src = fs.existsSync(customPath) ? fs.readFileSync(customPath,'utf8') : BUILTIN_TEMPLATES[tname]
|
|
360
377
|
if (!src) { console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1) }
|
|
361
378
|
fs.writeFileSync(outFile, src)
|
|
@@ -384,21 +401,21 @@ if (cmd==='init') {
|
|
|
384
401
|
const isMultiFile = tplSrc.includes('\n---\n')
|
|
385
402
|
|
|
386
403
|
if (isFullStack) {
|
|
387
|
-
// Full-stack project: single app.
|
|
404
|
+
// Full-stack project: single app.aip
|
|
388
405
|
fs.mkdirSync(dir, { recursive: true })
|
|
389
|
-
fs.writeFileSync(path.join(dir, 'app.
|
|
406
|
+
fs.writeFileSync(path.join(dir, 'app.aip'), tplSrc)
|
|
390
407
|
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
|
|
391
408
|
name, version:'0.1.0',
|
|
392
|
-
scripts: { dev: 'npx aiplang start app.
|
|
409
|
+
scripts: { dev: 'npx aiplang start app.aip', start: 'npx aiplang start app.aip' },
|
|
393
410
|
devDependencies: { 'aiplang': `^${VERSION}` }
|
|
394
411
|
}, null, 2))
|
|
395
412
|
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
413
|
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.
|
|
414
|
+
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
415
|
const label = tplName !== 'default' ? ` (template: ${tplName})` : ''
|
|
399
|
-
console.log(`\n ✓ Created ${name}/${label}\n\n app.
|
|
416
|
+
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
417
|
} else if (isMultiFile) {
|
|
401
|
-
// Multi-page SSG project: pages/*.
|
|
418
|
+
// Multi-page SSG project: pages/*.aip
|
|
402
419
|
fs.mkdirSync(path.join(dir,'pages'), {recursive:true})
|
|
403
420
|
fs.mkdirSync(path.join(dir,'public'), {recursive:true})
|
|
404
421
|
for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
|
|
@@ -408,7 +425,7 @@ if (cmd==='init') {
|
|
|
408
425
|
pageBlocks.forEach((block, i) => {
|
|
409
426
|
const m = block.match(/^%([a-zA-Z0-9_-]+)/m)
|
|
410
427
|
const pageName = m ? m[1] : (i === 0 ? 'home' : `page${i}`)
|
|
411
|
-
fs.writeFileSync(path.join(dir,'pages',`${pageName}.
|
|
428
|
+
fs.writeFileSync(path.join(dir,'pages',`${pageName}.aip`), block.trim())
|
|
412
429
|
})
|
|
413
430
|
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
431
|
fs.writeFileSync(path.join(dir,'.gitignore'),'dist/\nnode_modules/\n')
|
|
@@ -422,11 +439,11 @@ if (cmd==='init') {
|
|
|
422
439
|
for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
|
|
423
440
|
const src=path.join(RUNTIME_DIR,f); if(fs.existsSync(src)) fs.copyFileSync(src,path.join(dir,'public',f))
|
|
424
441
|
}
|
|
425
|
-
fs.writeFileSync(path.join(dir,'pages','home.
|
|
442
|
+
fs.writeFileSync(path.join(dir,'pages','home.aip'), tplSrc)
|
|
426
443
|
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
444
|
fs.writeFileSync(path.join(dir,'.gitignore'),'dist/\nnode_modules/\n')
|
|
428
445
|
const label = tplName !== 'default' ? ` (template: ${tplName})` : ''
|
|
429
|
-
console.log(`\n ✓ Created ${name}/${label}\n\n pages/home.
|
|
446
|
+
console.log(`\n ✓ Created ${name}/${label}\n\n pages/home.aip ← edit this\n\n Next:\n cd ${name} && npx aiplang serve\n`)
|
|
430
447
|
}
|
|
431
448
|
process.exit(0)
|
|
432
449
|
}
|
|
@@ -435,7 +452,7 @@ if (cmd==='init') {
|
|
|
435
452
|
if (cmd==='new') {
|
|
436
453
|
const name=args[0]; if(!name){console.error('\n ✗ Usage: aiplang new <page>\n');process.exit(1)}
|
|
437
454
|
const dir=fs.existsSync('pages')?'pages':'.'
|
|
438
|
-
const file=path.join(dir,`${name}.
|
|
455
|
+
const file=path.join(dir,`${name}.aip`)
|
|
439
456
|
if(fs.existsSync(file)){console.error(`\n ✗ ${file} exists.\n`);process.exit(1)}
|
|
440
457
|
const cap=name.charAt(0).toUpperCase()+name.slice(1)
|
|
441
458
|
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 +467,9 @@ if (cmd==='build') {
|
|
|
450
467
|
const input=args.filter((a,i)=>!a.startsWith('--')&&i!==outIdx+1)[0]||'pages/'
|
|
451
468
|
const files=[]
|
|
452
469
|
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 .
|
|
470
|
+
fs.readdirSync(input).filter(f=>f.endsWith('.aip')).forEach(f=>files.push(path.join(input,f)))
|
|
471
|
+
} else if(input.endsWith('.aip')&&fs.existsSync(input)){ files.push(input) }
|
|
472
|
+
if(!files.length){console.error(`\n ✗ No .aip files in: ${input}\n`);process.exit(1)}
|
|
456
473
|
const src=files.map(f=>fs.readFileSync(f,'utf8')).join('\n---\n')
|
|
457
474
|
const pages=parsePages(src)
|
|
458
475
|
if(!pages.length){console.error('\n ✗ No pages found.\n');process.exit(1)}
|
|
@@ -471,7 +488,7 @@ if (cmd==='build') {
|
|
|
471
488
|
}
|
|
472
489
|
const hf=path.join(RUNTIME_DIR,'aiplang-hydrate.js')
|
|
473
490
|
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('.
|
|
491
|
+
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
492
|
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
493
|
process.exit(0)
|
|
477
494
|
}
|
|
@@ -480,13 +497,13 @@ if (cmd==='build') {
|
|
|
480
497
|
if (cmd==='serve'||cmd==='dev') {
|
|
481
498
|
const root=path.resolve(args[0]||'.')
|
|
482
499
|
const port=parseInt(process.env.PORT||'3000')
|
|
483
|
-
const MIME={'.html':'text/html;charset=utf-8','.js':'application/javascript','.css':'text/css','.
|
|
500
|
+
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
501
|
let clients=[]
|
|
485
502
|
const mtimes={}
|
|
486
503
|
setInterval(()=>{
|
|
487
504
|
const pd=path.join(root,'pages')
|
|
488
505
|
if(!fs.existsSync(pd))return
|
|
489
|
-
fs.readdirSync(pd).filter(f=>f.endsWith('.
|
|
506
|
+
fs.readdirSync(pd).filter(f=>f.endsWith('.aip')).forEach(f=>{
|
|
490
507
|
const fp=path.join(pd,f),mt=fs.statSync(fp).mtimeMs
|
|
491
508
|
if(mtimes[fp]&&mtimes[fp]!==mt)clients.forEach(c=>{try{c.write('data: reload\n\n')}catch{}})
|
|
492
509
|
mtimes[fp]=mt
|
|
@@ -501,7 +518,7 @@ if (cmd==='serve'||cmd==='dev') {
|
|
|
501
518
|
let p=req.url.split('?')[0];if(p==='/') p='/index.html'
|
|
502
519
|
let fp=null
|
|
503
520
|
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('.
|
|
521
|
+
if(!fp&&p.endsWith('.aip')){const c=path.join(root,'pages',path.basename(p));if(fs.existsSync(c))fp=c}
|
|
505
522
|
if(!fp){res.writeHead(404);res.end('Not found');return}
|
|
506
523
|
let content=fs.readFileSync(fp)
|
|
507
524
|
if(path.extname(fp)==='.html'){
|
|
@@ -510,7 +527,7 @@ if (cmd==='serve'||cmd==='dev') {
|
|
|
510
527
|
}
|
|
511
528
|
res.writeHead(200,{'Content-Type':MIME[path.extname(fp)]||'application/octet-stream','Access-Control-Allow-Origin':'*'})
|
|
512
529
|
res.end(content)
|
|
513
|
-
}).listen(port,()=>console.log(`\n ✓ aiplang dev server\n\n → http://localhost:${port}\n\n Hot reload ON — edit .
|
|
530
|
+
}).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
531
|
return
|
|
515
532
|
}
|
|
516
533
|
|
|
@@ -518,7 +535,7 @@ if (cmd==='serve'||cmd==='dev') {
|
|
|
518
535
|
if (cmd === 'start' || cmd === 'run') {
|
|
519
536
|
const aipFile = args[0]
|
|
520
537
|
if (!aipFile || !fs.existsSync(aipFile)) {
|
|
521
|
-
console.error(`\n ✗ Usage: aiplang start <app.
|
|
538
|
+
console.error(`\n ✗ Usage: aiplang start <app.aip>\n`)
|
|
522
539
|
process.exit(1)
|
|
523
540
|
}
|
|
524
541
|
const port = parseInt(process.env.PORT || args[1] || '3000')
|
package/package.json
CHANGED
|
@@ -954,108 +954,108 @@ html{scroll-behavior:smooth}
|
|
|
954
954
|
body{font-family:-apple-system,'Segoe UI',system-ui,sans-serif;-webkit-font-smoothing:antialiased}
|
|
955
955
|
a{text-decoration:none;color:inherit}
|
|
956
956
|
input,button,select{font-family:inherit}
|
|
957
|
-
.
|
|
958
|
-
.
|
|
959
|
-
.
|
|
960
|
-
.
|
|
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}
|
|
961
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)}
|
|
962
|
-
.
|
|
963
|
-
.
|
|
964
|
-
.
|
|
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)}
|
|
965
965
|
.fx-brand{font-size:1.25rem;font-weight:800;letter-spacing:-.03em}
|
|
966
966
|
.fx-nav-links{display:flex;align-items:center;gap:1.75rem}
|
|
967
967
|
.fx-nav-link{font-size:.875rem;font-weight:500;opacity:.65;transition:opacity .15s;cursor:pointer}
|
|
968
968
|
.fx-nav-link:hover{opacity:1}
|
|
969
|
-
.
|
|
970
|
-
.
|
|
971
|
-
.
|
|
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}
|
|
972
972
|
.fx-hero{display:flex;align-items:center;justify-content:center;min-height:92vh;padding:4rem 1.5rem}
|
|
973
973
|
.fx-hero-inner{max-width:56rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.5rem}
|
|
974
974
|
.fx-title{font-size:clamp(2.5rem,8vw,5.5rem);font-weight:900;letter-spacing:-.04em;line-height:1}
|
|
975
975
|
.fx-sub{font-size:clamp(1rem,2vw,1.25rem);line-height:1.75;max-width:40rem}
|
|
976
|
-
.
|
|
977
|
-
.
|
|
976
|
+
.aip-theme-dark .fx-sub{color:#94a3b8}
|
|
977
|
+
.aip-theme-light .fx-sub{color:#475569}
|
|
978
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}
|
|
979
979
|
.fx-cta:hover{transform:translateY(-1px)}
|
|
980
|
-
.
|
|
981
|
-
.
|
|
982
|
-
.
|
|
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}
|
|
983
983
|
.fx-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:3rem;padding:5rem 2.5rem;text-align:center}
|
|
984
984
|
.fx-stat-val{font-size:clamp(2.5rem,5vw,4rem);font-weight:900;letter-spacing:-.04em;line-height:1}
|
|
985
985
|
.fx-stat-lbl{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.1em;margin-top:.5rem}
|
|
986
|
-
.
|
|
987
|
-
.
|
|
986
|
+
.aip-theme-dark .fx-stat-lbl{color:#64748b}
|
|
987
|
+
.aip-theme-light .fx-stat-lbl{color:#94a3b8}
|
|
988
988
|
.fx-grid{display:grid;gap:1.25rem;padding:1rem 2.5rem 5rem}
|
|
989
989
|
.fx-grid-2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
|
|
990
990
|
.fx-grid-3{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
|
|
991
991
|
.fx-grid-4{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}
|
|
992
992
|
.fx-card{border-radius:1rem;padding:1.75rem;transition:transform .2s,box-shadow .2s}
|
|
993
993
|
.fx-card:hover{transform:translateY(-2px)}
|
|
994
|
-
.
|
|
995
|
-
.
|
|
996
|
-
.
|
|
997
|
-
.
|
|
998
|
-
.
|
|
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)}
|
|
999
999
|
.fx-icon{font-size:2rem;margin-bottom:1rem}
|
|
1000
1000
|
.fx-card-title{font-size:1.0625rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.5rem}
|
|
1001
1001
|
.fx-card-body{font-size:.875rem;line-height:1.65}
|
|
1002
|
-
.
|
|
1003
|
-
.
|
|
1002
|
+
.aip-theme-dark .fx-card-body{color:#64748b}
|
|
1003
|
+
.aip-theme-light .fx-card-body{color:#475569}
|
|
1004
1004
|
.fx-card-link{font-size:.8125rem;font-weight:600;display:inline-block;margin-top:1rem;opacity:.6;transition:opacity .15s}
|
|
1005
1005
|
.fx-card-link:hover{opacity:1}
|
|
1006
1006
|
.fx-sect{padding:5rem 2.5rem}
|
|
1007
1007
|
.fx-sect-title{font-size:clamp(1.75rem,4vw,3rem);font-weight:800;letter-spacing:-.04em;margin-bottom:1.5rem;text-align:center}
|
|
1008
1008
|
.fx-sect-body{font-size:1rem;line-height:1.75;text-align:center;max-width:48rem;margin:0 auto}
|
|
1009
|
-
.
|
|
1009
|
+
.aip-theme-dark .fx-sect-body{color:#64748b}
|
|
1010
1010
|
.fx-form-wrap{padding:3rem 2.5rem;display:flex;justify-content:center}
|
|
1011
1011
|
.fx-form{width:100%;max-width:28rem;border-radius:1.25rem;padding:2.5rem}
|
|
1012
|
-
.
|
|
1013
|
-
.
|
|
1012
|
+
.aip-theme-dark .fx-form{background:#0f172a;border:1px solid #1e293b}
|
|
1013
|
+
.aip-theme-light .fx-form{background:#f8fafc;border:1px solid #e2e8f0}
|
|
1014
1014
|
.fx-field{margin-bottom:1.25rem}
|
|
1015
1015
|
.fx-label{display:block;font-size:.8125rem;font-weight:600;margin-bottom:.5rem}
|
|
1016
|
-
.
|
|
1017
|
-
.
|
|
1016
|
+
.aip-theme-dark .fx-label{color:#94a3b8}
|
|
1017
|
+
.aip-theme-light .fx-label{color:#475569}
|
|
1018
1018
|
.fx-input{width:100%;padding:.75rem 1rem;border-radius:.625rem;font-size:.9375rem;outline:none;transition:box-shadow .15s;background:transparent}
|
|
1019
1019
|
.fx-input:focus{box-shadow:0 0 0 3px rgba(37,99,235,.35)}
|
|
1020
|
-
.
|
|
1021
|
-
.
|
|
1022
|
-
.
|
|
1023
|
-
.
|
|
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}
|
|
1024
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}
|
|
1025
1025
|
.fx-btn:hover{transform:translateY(-1px)}
|
|
1026
1026
|
.fx-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
|
1027
|
-
.
|
|
1028
|
-
.
|
|
1029
|
-
.
|
|
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}
|
|
1030
1030
|
.fx-form-msg{font-size:.8125rem;padding:.5rem 0;min-height:1.5rem;text-align:center}
|
|
1031
1031
|
.fx-form-err{color:#f87171}
|
|
1032
1032
|
.fx-form-ok{color:#4ade80}
|
|
1033
1033
|
.fx-table-wrap{overflow-x:auto;padding:0 2.5rem 4rem}
|
|
1034
1034
|
.fx-table{width:100%;border-collapse:collapse;font-size:.875rem}
|
|
1035
1035
|
.fx-th{text-align:left;padding:.875rem 1.25rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em}
|
|
1036
|
-
.
|
|
1037
|
-
.
|
|
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}
|
|
1038
1038
|
.fx-tr{transition:background .1s}
|
|
1039
1039
|
.fx-td{padding:.875rem 1.25rem}
|
|
1040
|
-
.
|
|
1041
|
-
.
|
|
1042
|
-
.
|
|
1043
|
-
.
|
|
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}
|
|
1044
1044
|
.fx-td-empty{padding:2rem 1.25rem;text-align:center;opacity:.4}
|
|
1045
1045
|
.fx-list-wrap{padding:1rem 2.5rem 4rem;display:flex;flex-direction:column;gap:.75rem}
|
|
1046
1046
|
.fx-list-item{border-radius:.75rem;padding:1.25rem 1.5rem}
|
|
1047
|
-
.
|
|
1047
|
+
.aip-theme-dark .fx-list-item{background:#0f172a;border:1px solid #1e293b}
|
|
1048
1048
|
.fx-list-field{font-size:.9375rem;line-height:1.5}
|
|
1049
1049
|
.fx-list-link{font-size:.8125rem;font-weight:600;opacity:.6;transition:opacity .15s}
|
|
1050
1050
|
.fx-list-link:hover{opacity:1}
|
|
1051
1051
|
.fx-alert{padding:1rem 2.5rem;font-size:.9375rem;font-weight:500;border-radius:.75rem;margin:1rem 2.5rem}
|
|
1052
|
-
.
|
|
1052
|
+
.aip-theme-dark .fx-alert{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:#fca5a5}
|
|
1053
1053
|
.fx-if-wrap{display:contents}
|
|
1054
1054
|
.fx-footer{padding:3rem 2.5rem;text-align:center}
|
|
1055
|
-
.
|
|
1056
|
-
.
|
|
1055
|
+
.aip-theme-dark .fx-footer{border-top:1px solid #1e293b}
|
|
1056
|
+
.aip-theme-light .fx-footer{border-top:1px solid #e2e8f0}
|
|
1057
1057
|
.fx-footer-text{font-size:.8125rem}
|
|
1058
|
-
.
|
|
1058
|
+
.aip-theme-dark .fx-footer-text{color:#334155}
|
|
1059
1059
|
.fx-footer-link{font-size:.8125rem;margin:0 .75rem;opacity:.5;transition:opacity .15s}
|
|
1060
1060
|
.fx-footer-link:hover{opacity:1}
|
|
1061
1061
|
`
|
package/server/server.js
CHANGED
|
@@ -1212,7 +1212,7 @@ function getMime(filename) {
|
|
|
1212
1212
|
// name: 'my-plugin',
|
|
1213
1213
|
// setup(server, app, utils) {
|
|
1214
1214
|
// // server = AiplangServer instance (.addRoute, .models)
|
|
1215
|
-
// // app = parsed .
|
|
1215
|
+
// // app = parsed .aip app definition
|
|
1216
1216
|
// // utils = { uuid, now, emit, on, dispatch, resolveEnv, dbRun, dbAll, dbGet }
|
|
1217
1217
|
// }
|
|
1218
1218
|
// }
|
|
@@ -1435,7 +1435,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1435
1435
|
|
|
1436
1436
|
// Health
|
|
1437
1437
|
srv.addRoute('GET', '/health', (req, res) => res.json(200, {
|
|
1438
|
-
status:'ok', version:'2.
|
|
1438
|
+
status:'ok', version:'2.7.0',
|
|
1439
1439
|
models: app.models.map(m=>m.name),
|
|
1440
1440
|
routes: app.apis.length, pages: app.pages.length,
|
|
1441
1441
|
admin: app.admin?.prefix || null,
|
|
@@ -1462,7 +1462,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1462
1462
|
module.exports = { startServer, parseApp, Model, getDB, dispatch, on, emit, sendMail, setupStripe, registerStripeRoutes, setupS3, registerS3Routes, s3Upload, s3Delete, s3PresignedUrl, PLUGIN_UTILS }
|
|
1463
1463
|
if (require.main === module) {
|
|
1464
1464
|
const f=process.argv[2], p=parseInt(process.argv[3]||process.env.PORT||'3000')
|
|
1465
|
-
if (!f) { console.error('Usage: node server.js <app.
|
|
1465
|
+
if (!f) { console.error('Usage: node server.js <app.aip> [port]'); process.exit(1) }
|
|
1466
1466
|
startServer(f, p).catch(e=>{console.error(e);process.exit(1)})
|
|
1467
1467
|
}
|
|
1468
1468
|
|