naider 1.23.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/naide.js +43 -1
- package/mcp/server.js +1 -1
- package/package.json +1 -1
- package/src/gen.js +342 -16
package/bin/naide.js
CHANGED
|
@@ -8,7 +8,7 @@ import { spawn } from 'child_process';
|
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
9
|
const _require = createRequire(import.meta.url);
|
|
10
10
|
|
|
11
|
-
const NAIDE_VERSION = '1.
|
|
11
|
+
const NAIDE_VERSION = '1.24.0';
|
|
12
12
|
|
|
13
13
|
function crashReport(err, context = {}) {
|
|
14
14
|
const info = [
|
|
@@ -305,6 +305,48 @@ if (files[0] === 'gen' || files[0] === '~~') {
|
|
|
305
305
|
process.exit(0);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// Multi-target generation (D6)
|
|
309
|
+
if (flags.target && flags.target !== 'naide') {
|
|
310
|
+
const { generateForTarget, generateAllTargets } = await import('../src/gen.js');
|
|
311
|
+
if (flags.target === 'all') {
|
|
312
|
+
const result = await generateAllTargets(instruction);
|
|
313
|
+
if (!result.valid) {
|
|
314
|
+
process.stderr.write(` Error: generated NAIDE code is invalid\n`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
const succeeded = Object.entries(result.targets).filter(([, v]) => !v.error);
|
|
318
|
+
const failed = Object.entries(result.targets).filter(([, v]) => v.error);
|
|
319
|
+
process.stderr.write(`\n ── Multi-Target Generation (${succeeded.length}/${Object.keys(result.targets).length} targets) ──\n`);
|
|
320
|
+
for (const [t, v] of succeeded) {
|
|
321
|
+
if (flags.output) {
|
|
322
|
+
const outPath = flags.output.replace(/\.\w+$/, v.ext);
|
|
323
|
+
writeFileSync(outPath, v.code, 'utf-8');
|
|
324
|
+
console.log(` ${t}: ${outPath}`);
|
|
325
|
+
} else {
|
|
326
|
+
console.log(`\n ── ${t} (${v.ext}) ──\n`);
|
|
327
|
+
console.log(v.code);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (failed.length > 0) {
|
|
331
|
+
process.stderr.write(` Failed: ${failed.map(([t, v]) => `${t} (${v.error.slice(0, 50)})`).join(', ')}\n`);
|
|
332
|
+
}
|
|
333
|
+
process.exit(0);
|
|
334
|
+
}
|
|
335
|
+
const result = await generateForTarget(instruction, flags.target);
|
|
336
|
+
if (result.compiled) {
|
|
337
|
+
if (flags.output) {
|
|
338
|
+
writeFileSync(flags.output, result.compiled, 'utf-8');
|
|
339
|
+
console.log(` Generated: ${flags.output} (${flags.target})`);
|
|
340
|
+
} else {
|
|
341
|
+
console.log(result.compiled);
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
process.stderr.write(` Error compiling to ${flags.target}: ${result.compileError || 'unknown'}\n`);
|
|
345
|
+
console.log(result.code);
|
|
346
|
+
}
|
|
347
|
+
process.exit(0);
|
|
348
|
+
}
|
|
349
|
+
|
|
308
350
|
const result = generate(instruction);
|
|
309
351
|
|
|
310
352
|
if (flags.output) {
|
package/mcp/server.js
CHANGED
|
@@ -192,7 +192,7 @@ async function handleMessage(msg) {
|
|
|
192
192
|
respond(msg.id, {
|
|
193
193
|
protocolVersion: '2024-11-05',
|
|
194
194
|
capabilities: { tools: {} },
|
|
195
|
-
serverInfo: { name: 'naide-mcp', version: '1.
|
|
195
|
+
serverInfo: { name: 'naide-mcp', version: '1.24.0' }
|
|
196
196
|
});
|
|
197
197
|
break;
|
|
198
198
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naider",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "NAIDE - Simpler than Python, compiles to 15 targets. AI-specialized language with 35+ built-in functions, syntax sugar (unless/until/repeat/swap/is/isnt), and 47 features. Targets: Node.js, Python, TypeScript, C, C++, Java, Go, Rust, PHP, Ruby, Kotlin, Swift, Dart, C#, Bun.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
package/src/gen.js
CHANGED
|
@@ -1202,13 +1202,48 @@ function passwordResetRoutes() {
|
|
|
1202
1202
|
|
|
1203
1203
|
// ── WebSocket Rooms (B11) ──────────────────────────────────
|
|
1204
1204
|
|
|
1205
|
-
function wsRoomsBlock() {
|
|
1206
|
-
|
|
1205
|
+
function wsRoomsBlock(hasAuth) {
|
|
1206
|
+
const lines = [
|
|
1207
1207
|
`ws "/ws":`,
|
|
1208
|
-
` on "connection" (socket):`,
|
|
1208
|
+
` on "connection" (socket, req):`,
|
|
1209
|
+
];
|
|
1210
|
+
if (hasAuth) {
|
|
1211
|
+
lines.push(
|
|
1212
|
+
` # WS auth: verify token from query string`,
|
|
1213
|
+
` str wsToken = ""`,
|
|
1214
|
+
` if req.url and req.url.includes("token="):`,
|
|
1215
|
+
` str qs = req.url.split("token=")[1] or ""`,
|
|
1216
|
+
` int ampIdx = qs.indexOf("&")`,
|
|
1217
|
+
` if ampIdx > 0:`,
|
|
1218
|
+
` wsToken = qs.slice(0, ampIdx)`,
|
|
1219
|
+
` else:`,
|
|
1220
|
+
` wsToken = qs`,
|
|
1221
|
+
` if wsToken:`,
|
|
1222
|
+
` any wsDecoded = jwt.verify(wsToken, JWT_SECRET)`,
|
|
1223
|
+
` if wsDecoded:`,
|
|
1224
|
+
` socket.user = wsDecoded`,
|
|
1225
|
+
` socket.authenticated = true`,
|
|
1226
|
+
` else:`,
|
|
1227
|
+
` socket.authenticated = false`,
|
|
1228
|
+
` socket.send(JSON.stringify({type: "error", message: "Invalid token"}))`,
|
|
1229
|
+
` socket.close()`,
|
|
1230
|
+
` else:`,
|
|
1231
|
+
` socket.authenticated = false`,
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
lines.push(
|
|
1209
1235
|
` socket.room = "general"`,
|
|
1210
1236
|
` log("Client connected to room: general")`,
|
|
1211
1237
|
` on "message" (data, socket):`,
|
|
1238
|
+
);
|
|
1239
|
+
if (hasAuth) {
|
|
1240
|
+
lines.push(
|
|
1241
|
+
` if not socket.authenticated:`,
|
|
1242
|
+
` socket.send(JSON.stringify({type: "error", message: "Not authenticated"}))`,
|
|
1243
|
+
` ret`,
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
lines.push(
|
|
1212
1247
|
` any msg = JSON.parse(data)`,
|
|
1213
1248
|
` if msg.type == "join":`,
|
|
1214
1249
|
` socket.room = msg.room`,
|
|
@@ -1221,7 +1256,8 @@ function wsRoomsBlock() {
|
|
|
1221
1256
|
` broadcast(msg.data)`,
|
|
1222
1257
|
` on "close" (socket):`,
|
|
1223
1258
|
` log("Client disconnected")`,
|
|
1224
|
-
|
|
1259
|
+
);
|
|
1260
|
+
return lines;
|
|
1225
1261
|
}
|
|
1226
1262
|
|
|
1227
1263
|
// ── Rate Limiting (B12) ────────────────────────────────────
|
|
@@ -1250,6 +1286,33 @@ function loggingBlock() {
|
|
|
1250
1286
|
next()`;
|
|
1251
1287
|
}
|
|
1252
1288
|
|
|
1289
|
+
// ── RBAC Middleware (D1) ──────────────────────────────────
|
|
1290
|
+
|
|
1291
|
+
function rbacBlock() {
|
|
1292
|
+
return `fn requireRole(list allowed):
|
|
1293
|
+
ret fn(req, res, next):
|
|
1294
|
+
if not req.user:
|
|
1295
|
+
ret.status(401) {error: "Not authenticated"}
|
|
1296
|
+
str userRole = req.user.role or "user"
|
|
1297
|
+
if not allowed.includes(userRole):
|
|
1298
|
+
ret.status(403) {error: "Insufficient permissions"}
|
|
1299
|
+
next()`;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function rbacRouteGuards(entity) {
|
|
1303
|
+
const lower = entity.name.toLowerCase();
|
|
1304
|
+
const plural = pluralize(lower);
|
|
1305
|
+
return [
|
|
1306
|
+
`# Role guards for ${plural}`,
|
|
1307
|
+
`del "/api/${plural}/:id" (req, res):`,
|
|
1308
|
+
` str role = req.user.role or "user"`,
|
|
1309
|
+
` if role != "admin" and role != "moderator":`,
|
|
1310
|
+
` ret.status(403) {error: "Only admin/moderator can delete"}`,
|
|
1311
|
+
` ${entity.name}Store.remove(req.params.id)`,
|
|
1312
|
+
` ret {deleted: true}`,
|
|
1313
|
+
];
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1253
1316
|
// ── Caching Headers (C18) ─────────────────────────────────
|
|
1254
1317
|
|
|
1255
1318
|
function cachingRoute() {
|
|
@@ -1288,7 +1351,11 @@ th{background:var(--bg);font-weight:600;font-size:.875rem;color:var(--muted)}
|
|
|
1288
1351
|
.search input{flex:1;padding:.5rem .75rem;border:1px solid var(--border);border-radius:var(--radius)}
|
|
1289
1352
|
.pagination{display:flex;gap:.5rem;justify-content:center;margin-top:1rem;align-items:center}
|
|
1290
1353
|
.toast{position:fixed;top:1rem;right:1rem;background:#22c55e;color:#fff;padding:.75rem 1.5rem;border-radius:var(--radius);opacity:0;transition:opacity .3s;z-index:999}
|
|
1291
|
-
.toast.show{opacity:1}.
|
|
1354
|
+
.toast.show{opacity:1}.toast.error{background:var(--danger)}.toast.warn{background:#f59e0b}
|
|
1355
|
+
.loading{text-align:center;padding:2rem;color:var(--muted)}.loading::after{content:"";display:inline-block;width:1.5rem;height:1.5rem;border:2px solid var(--border);border-top-color:var(--primary);border-radius:50%;animation:spin .6s linear infinite;margin-left:.5rem}
|
|
1356
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
1357
|
+
.error-banner{background:#fef2f2;border:1px solid #fecaca;color:#991b1b;padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;font-size:.875rem}
|
|
1358
|
+
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:100}
|
|
1292
1359
|
.modal{background:var(--card);border-radius:var(--radius);padding:2rem;width:90%;max-width:500px;box-shadow:0 20px 60px rgba(0,0,0,.2)}
|
|
1293
1360
|
.modal h3{margin-bottom:1rem}.modal-actions{display:flex;gap:.5rem;justify-content:flex-end;margin-top:1.5rem}
|
|
1294
1361
|
.badge{display:inline-block;padding:.15rem .5rem;border-radius:99px;font-size:.75rem;font-weight:500;background:var(--border)}
|
|
@@ -1338,30 +1405,36 @@ th{background:var(--bg);font-weight:600;font-size:.875rem;color:var(--muted)}
|
|
|
1338
1405
|
L.push('<script>');
|
|
1339
1406
|
L.push(`const API="";let currentPage={};let _token=null;let _isRegister=false;`);
|
|
1340
1407
|
L.push(`function authHeaders(){const h={"Content-Type":"application/json"};if(_token)h.Authorization="Bearer "+_token;return h}`);
|
|
1341
|
-
L.push(`function toast(m){const t=document.getElementById("toast");t.textContent=m;t.
|
|
1408
|
+
L.push(`function toast(m,type){const t=document.getElementById("toast");t.textContent=m;t.className="toast show"+(type?" "+type:"");setTimeout(()=>{t.classList.remove("show")},2500)}`);
|
|
1409
|
+
L.push(`function showError(msg){toast(msg,"error")}`);
|
|
1410
|
+
L.push(`async function apiFetch(url,opts){try{const r=await fetch(url,opts||{headers:authHeaders()});if(r.status===401){${hasAuth?'showError("Session expired — please log in again");doLogout();':''} return null}if(!r.ok){const e=await r.json().catch(()=>({error:"Request failed"}));showError(e.error||"Error "+r.status);return null}return await r.json()}catch(e){showError("Network error: "+e.message);return null}}`);
|
|
1411
|
+
L.push(`function setLoading(id,on){const el=document.getElementById(id);if(el)el.innerHTML=on?'<div class="loading">Loading...</div>':""}`);
|
|
1412
|
+
L.push(`function showBanner(containerId,msg){const el=document.getElementById(containerId);if(!el)return;let b=el.querySelector(".error-banner");if(!b){b=document.createElement("div");b.className="error-banner";el.prepend(b)}b.textContent=msg;setTimeout(()=>b.remove(),5000)}`);
|
|
1342
1413
|
L.push(`function showSection(name){document.querySelectorAll('[id^="section-"]').forEach(s=>s.classList.add("hidden"));const el=document.getElementById("section-"+name);if(el)el.classList.remove("hidden");document.querySelectorAll("nav a").forEach(a=>a.classList.remove("active"));const n=document.getElementById("nav-"+name);if(n)n.classList.add("active");if(window[name+"Load"])window[name+"Load"]()}`);
|
|
1343
1414
|
if (hasAuth) {
|
|
1344
1415
|
L.push(`function toggleAuthMode(){_isRegister=!_isRegister;document.getElementById("auth-title").textContent=_isRegister?"Register":"Login";document.getElementById("auth-name-group").classList.toggle("hidden",!_isRegister)}`);
|
|
1345
|
-
L.push(`async function doAuth(){const email=document.getElementById("auth-email").value;const password=document.getElementById("auth-password").value;const url=_isRegister?"/api/auth/register":"/api/auth/login";const body=_isRegister?{name:document.getElementById("auth-name").value,email,password}:{email,password};try{const r=await fetch(API+url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});const d=await r.json();if(d.token){_token=d.token;document.getElementById("user-info").textContent="Hi, "+(d.user?.name||email);document.getElementById("user-info").classList.remove("hidden");document.getElementById("logout-btn").classList.remove("hidden");document.getElementById("nav-auth").classList.add("hidden");toast(_isRegister?"Registered!":"Logged in!");showSection("${entities[0].name.toLowerCase()}")}else{
|
|
1346
|
-
L.push(`function doLogout(){_token=null;document.getElementById("user-info").classList.add("hidden");document.getElementById("logout-btn").classList.add("hidden");document.getElementById("nav-auth").classList.remove("hidden");toast("Logged out");showSection("auth")}`);
|
|
1416
|
+
L.push(`async function doAuth(){const email=document.getElementById("auth-email").value;const password=document.getElementById("auth-password").value;if(!email||!password){showError("Email and password are required");return}const url=_isRegister?"/api/auth/register":"/api/auth/login";const body=_isRegister?{name:document.getElementById("auth-name").value,email,password}:{email,password};try{const r=await fetch(API+url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});const d=await r.json();if(!r.ok){showError(d.error||"Auth failed ("+r.status+")");return}if(d.token){_token=d.token;localStorage.setItem("_token",d.token);document.getElementById("user-info").textContent="Hi, "+(d.user?.name||email);document.getElementById("user-info").classList.remove("hidden");document.getElementById("logout-btn").classList.remove("hidden");document.getElementById("nav-auth").classList.add("hidden");toast(_isRegister?"Registered!":"Logged in!");showSection("${entities[0].name.toLowerCase()}")}else{showError(d.error||"No token received")}}catch(e){showError("Network error: "+e.message)}}`);
|
|
1417
|
+
L.push(`function doLogout(){_token=null;localStorage.removeItem("_token");document.getElementById("user-info").classList.add("hidden");document.getElementById("logout-btn").classList.add("hidden");document.getElementById("nav-auth").classList.remove("hidden");toast("Logged out");showSection("auth")}`);
|
|
1418
|
+
L.push(`(function(){const t=localStorage.getItem("_token");if(t){_token=t;document.getElementById("user-info").textContent="Logged in";document.getElementById("user-info").classList.remove("hidden");document.getElementById("logout-btn").classList.remove("hidden");document.getElementById("nav-auth").classList.add("hidden")}})()`);
|
|
1347
1419
|
}
|
|
1348
1420
|
for (const entity of entities) {
|
|
1349
1421
|
const lower = entity.name.toLowerCase();
|
|
1350
1422
|
const plural = pluralize(lower);
|
|
1351
1423
|
const fields = entity.fields.filter(f => f.type !== 'auto');
|
|
1352
1424
|
L.push(`async function ${lower}Load(page){page=page||1;currentPage["${lower}"]=page;const s=document.getElementById("${lower}-search").value;const sort=document.getElementById("${lower}-sort").value;const ord=document.getElementById("${lower}-order").value;`);
|
|
1353
|
-
L.push(`
|
|
1425
|
+
L.push(`setLoading("${lower}-tbody",true);`);
|
|
1426
|
+
L.push(`const d=await apiFetch(API+"/api/${plural}?page="+page+"&limit=10&search="+encodeURIComponent(s)+"&sort="+sort+"&order="+ord);if(!d){setLoading("${lower}-tbody",false);return}const items=d.items||d;`);
|
|
1354
1427
|
L.push(`document.getElementById("${lower}-tbody").innerHTML=items.map(i=>"<tr>${entity.fields.map(f => `<td>"+(i.${f.name}!=null?i.${f.name}:"")+"</td>`).join('')}<td><button class='btn btn-sm btn-primary' onclick='${lower}Edit("+i.id+")'>Edit</button> <button class='btn btn-sm btn-danger' onclick='${lower}Del("+i.id+")'>Del</button></td></tr>").join("");`);
|
|
1355
1428
|
L.push(`const tp=Math.ceil((d.total||items.length)/10);let pg="";for(let p=1;p<=tp;p++)pg+="<button class='btn btn-sm "+(p===page?"btn-primary":"")+"' onclick='${lower}Load("+p+")'>"+p+"</button>";document.getElementById("${lower}-pager").innerHTML=pg}`);
|
|
1356
1429
|
L.push(`function ${lower}ShowModal(id){document.getElementById("${lower}-edit-id").value=id||"";document.getElementById("${lower}-modal-title").textContent=id?"Edit ${entity.name}":"New ${entity.name}";${fields.map(f => f.type === 'bool' ? `document.getElementById("${lower}-f-${f.name}").checked=false` : `document.getElementById("${lower}-f-${f.name}").value=""`).join(';')};document.getElementById("${lower}-modal").classList.remove("hidden")}`);
|
|
1357
|
-
L.push(`async function ${lower}Edit(id){const
|
|
1430
|
+
L.push(`async function ${lower}Edit(id){const d=await apiFetch(API+"/api/${plural}/"+id);if(!d)return;const i=d.data||d;${lower}ShowModal(id);${fields.map(f => f.type === 'bool' ? `document.getElementById("${lower}-f-${f.name}").checked=!!i.${f.name}` : `document.getElementById("${lower}-f-${f.name}").value=i.${f.name}||""`).join(';')}}`);
|
|
1358
1431
|
L.push(`async function ${lower}Save(){const id=document.getElementById("${lower}-edit-id").value;const body={${fields.map(f => {
|
|
1359
1432
|
if (f.type === 'bool') return `${f.name}:document.getElementById("${lower}-f-${f.name}").checked`;
|
|
1360
1433
|
if (f.type === 'int') return `${f.name}:+document.getElementById("${lower}-f-${f.name}").value`;
|
|
1361
1434
|
return `${f.name}:document.getElementById("${lower}-f-${f.name}").value`;
|
|
1362
1435
|
}).join(',')}};`);
|
|
1363
|
-
L.push(`const method=id?"PUT":"POST";const url=API+"/api/${plural}"+(id?"/"+id:"");await
|
|
1364
|
-
L.push(`async function ${lower}Del(id){if(!confirm("Delete this ${lower}?"))return;await
|
|
1436
|
+
L.push(`const method=id?"PUT":"POST";const url=API+"/api/${plural}"+(id?"/"+id:"");const d=await apiFetch(url,{method,headers:authHeaders(),body:JSON.stringify(body)});if(!d){showBanner("section-${lower}","Save failed — check required fields");return}document.getElementById("${lower}-modal").classList.add("hidden");toast(id?"Updated!":"Created!");${lower}Load()}`);
|
|
1437
|
+
L.push(`async function ${lower}Del(id){if(!confirm("Delete this ${lower}?"))return;const d=await apiFetch(API+"/api/${plural}/"+id,{method:"DELETE",headers:authHeaders()});if(!d)return;toast("Deleted!");${lower}Load()}`);
|
|
1365
1438
|
}
|
|
1366
1439
|
const initSection = hasAuth ? 'auth' : entities[0].name.toLowerCase();
|
|
1367
1440
|
L.push(`showSection("${initSection}")`);
|
|
@@ -1501,6 +1574,173 @@ function envLines(intents, params) {
|
|
|
1501
1574
|
return lines;
|
|
1502
1575
|
}
|
|
1503
1576
|
|
|
1577
|
+
// ── Test Runner (D2) ─────────────────────────────────────
|
|
1578
|
+
|
|
1579
|
+
function testRunnerFile(entities, intents, port) {
|
|
1580
|
+
const lines = [];
|
|
1581
|
+
lines.push(`import { describe, it } from 'node:test';`);
|
|
1582
|
+
lines.push(`import assert from 'node:assert/strict';`);
|
|
1583
|
+
lines.push('');
|
|
1584
|
+
lines.push(`const BASE = process.env.TEST_URL || 'http://localhost:${port}';`);
|
|
1585
|
+
lines.push(`const headers = {'Content-Type': 'application/json'};`);
|
|
1586
|
+
lines.push(`let authToken = null;`);
|
|
1587
|
+
lines.push('');
|
|
1588
|
+
lines.push(`async function api(path, opts = {}) {`);
|
|
1589
|
+
lines.push(` const h = {...headers};`);
|
|
1590
|
+
lines.push(` if (authToken) h.Authorization = 'Bearer ' + authToken;`);
|
|
1591
|
+
lines.push(` const r = await fetch(BASE + path, {...opts, headers: {...h, ...opts.headers}});`);
|
|
1592
|
+
lines.push(` const body = await r.json().catch(() => null);`);
|
|
1593
|
+
lines.push(` return {status: r.status, body, ok: r.ok};`);
|
|
1594
|
+
lines.push(`}`);
|
|
1595
|
+
lines.push('');
|
|
1596
|
+
lines.push(`describe('Health', () => {`);
|
|
1597
|
+
lines.push(` it('GET /api/health returns ok', async () => {`);
|
|
1598
|
+
lines.push(` const r = await api('/api/health');`);
|
|
1599
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1600
|
+
lines.push(` assert.equal(r.body.status, 'ok');`);
|
|
1601
|
+
lines.push(` });`);
|
|
1602
|
+
lines.push(`});`);
|
|
1603
|
+
lines.push('');
|
|
1604
|
+
if (intents.has('auth')) {
|
|
1605
|
+
lines.push(`describe('Auth', () => {`);
|
|
1606
|
+
lines.push(` const email = 'test' + Date.now() + '@test.com';`);
|
|
1607
|
+
lines.push(` it('POST /api/auth/register creates user', async () => {`);
|
|
1608
|
+
lines.push(` const r = await api('/api/auth/register', {method: 'POST', body: JSON.stringify({name: 'Test', email, password: 'test1234'})});`);
|
|
1609
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1610
|
+
lines.push(` assert.ok(r.body.token);`);
|
|
1611
|
+
lines.push(` authToken = r.body.token;`);
|
|
1612
|
+
lines.push(` });`);
|
|
1613
|
+
lines.push(` it('POST /api/auth/login returns token', async () => {`);
|
|
1614
|
+
lines.push(` const r = await api('/api/auth/login', {method: 'POST', body: JSON.stringify({email, password: 'test1234'})});`);
|
|
1615
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1616
|
+
lines.push(` assert.ok(r.body.token);`);
|
|
1617
|
+
lines.push(` authToken = r.body.token;`);
|
|
1618
|
+
lines.push(` });`);
|
|
1619
|
+
lines.push(` it('GET /api/auth/me returns current user', async () => {`);
|
|
1620
|
+
lines.push(` const r = await api('/api/auth/me');`);
|
|
1621
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1622
|
+
lines.push(` assert.equal(r.body.email || r.body.data?.email, email);`);
|
|
1623
|
+
lines.push(` });`);
|
|
1624
|
+
lines.push(` it('rejects invalid login', async () => {`);
|
|
1625
|
+
lines.push(` const r = await api('/api/auth/login', {method: 'POST', body: JSON.stringify({email: 'no@no.com', password: 'wrong'})});`);
|
|
1626
|
+
lines.push(` assert.equal(r.status, 401);`);
|
|
1627
|
+
lines.push(` });`);
|
|
1628
|
+
lines.push(`});`);
|
|
1629
|
+
lines.push('');
|
|
1630
|
+
}
|
|
1631
|
+
for (const entity of entities) {
|
|
1632
|
+
const lower = entity.name.toLowerCase();
|
|
1633
|
+
const plural = pluralize(lower);
|
|
1634
|
+
const requiredFields = entity.fields.filter(f => f.modifiers.includes('required') && f.type !== 'auto');
|
|
1635
|
+
const sampleData = {};
|
|
1636
|
+
for (const f of requiredFields) {
|
|
1637
|
+
if (f.type === 'int') sampleData[f.name] = 1;
|
|
1638
|
+
else if (f.type === 'bool') sampleData[f.name] = true;
|
|
1639
|
+
else sampleData[f.name] = 'test_' + f.name;
|
|
1640
|
+
}
|
|
1641
|
+
if (Object.keys(sampleData).length === 0) {
|
|
1642
|
+
const first = entity.fields.find(f => f.type !== 'auto' && f.type !== 'bool');
|
|
1643
|
+
if (first) sampleData[first.name] = first.type === 'int' ? 1 : 'test';
|
|
1644
|
+
}
|
|
1645
|
+
lines.push(`describe('${entity.name} CRUD', () => {`);
|
|
1646
|
+
lines.push(` let createdId;`);
|
|
1647
|
+
lines.push(` it('POST /api/${plural} creates', async () => {`);
|
|
1648
|
+
lines.push(` const r = await api('/api/${plural}', {method: 'POST', body: JSON.stringify(${JSON.stringify(sampleData)})});`);
|
|
1649
|
+
lines.push(` assert.ok(r.ok, 'status ' + r.status);`);
|
|
1650
|
+
lines.push(` createdId = r.body.id || r.body.data?.id;`);
|
|
1651
|
+
lines.push(` assert.ok(createdId, 'has id');`);
|
|
1652
|
+
lines.push(` });`);
|
|
1653
|
+
lines.push(` it('GET /api/${plural} lists', async () => {`);
|
|
1654
|
+
lines.push(` const r = await api('/api/${plural}');`);
|
|
1655
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1656
|
+
lines.push(` const items = r.body.items || r.body;`);
|
|
1657
|
+
lines.push(` assert.ok(Array.isArray(items));`);
|
|
1658
|
+
lines.push(` });`);
|
|
1659
|
+
lines.push(` it('GET /api/${plural}/:id finds', async () => {`);
|
|
1660
|
+
lines.push(` const r = await api('/api/${plural}/' + createdId);`);
|
|
1661
|
+
lines.push(` assert.equal(r.status, 200);`);
|
|
1662
|
+
lines.push(` });`);
|
|
1663
|
+
lines.push(` it('PUT /api/${plural}/:id updates', async () => {`);
|
|
1664
|
+
lines.push(` const r = await api('/api/${plural}/' + createdId, {method: 'PUT', body: JSON.stringify(${JSON.stringify(sampleData)})});`);
|
|
1665
|
+
lines.push(` assert.ok(r.ok);`);
|
|
1666
|
+
lines.push(` });`);
|
|
1667
|
+
lines.push(` it('DELETE /api/${plural}/:id deletes', async () => {`);
|
|
1668
|
+
lines.push(` const r = await api('/api/${plural}/' + createdId, {method: 'DELETE'});`);
|
|
1669
|
+
lines.push(` assert.ok(r.ok);`);
|
|
1670
|
+
lines.push(` });`);
|
|
1671
|
+
lines.push(`});`);
|
|
1672
|
+
lines.push('');
|
|
1673
|
+
}
|
|
1674
|
+
return lines.join('\n');
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// ── DB Migration (D3) ───────────────────────────────────────
|
|
1678
|
+
|
|
1679
|
+
function migrationFile(entities, relationships, m2mRels) {
|
|
1680
|
+
const lines = [];
|
|
1681
|
+
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
1682
|
+
lines.push(`# Migration: ${ts}`);
|
|
1683
|
+
lines.push(`# Auto-generated by NAIDE ~~ engine`);
|
|
1684
|
+
lines.push(`# Run: naide migrate.naide`);
|
|
1685
|
+
lines.push('');
|
|
1686
|
+
lines.push('use "node:fs"');
|
|
1687
|
+
lines.push('');
|
|
1688
|
+
lines.push(`fn migrate():`);
|
|
1689
|
+
for (const entity of entities) {
|
|
1690
|
+
const lower = entity.name.toLowerCase();
|
|
1691
|
+
const plural = pluralize(lower);
|
|
1692
|
+
lines.push(` log("Creating table: ${plural}")`);
|
|
1693
|
+
const cols = entity.fields.map(f => {
|
|
1694
|
+
let col = f.name;
|
|
1695
|
+
if (f.type === 'auto') col += ' INTEGER PRIMARY KEY AUTOINCREMENT';
|
|
1696
|
+
else if (f.type === 'int') col += ' INTEGER';
|
|
1697
|
+
else if (f.type === 'bool') col += ' BOOLEAN DEFAULT false';
|
|
1698
|
+
else col += ' TEXT';
|
|
1699
|
+
if (f.modifiers.includes('required') && f.type !== 'auto') col += ' NOT NULL';
|
|
1700
|
+
if (f.modifiers.includes('unique')) col += ' UNIQUE';
|
|
1701
|
+
return col;
|
|
1702
|
+
});
|
|
1703
|
+
lines.push(` str sql_${lower} = "CREATE TABLE IF NOT EXISTS ${plural} (${cols.join(', ')}"`);
|
|
1704
|
+
const fks = relationships.filter(r => r.child === entity.name);
|
|
1705
|
+
if (fks.length > 0) {
|
|
1706
|
+
for (const fk of fks) {
|
|
1707
|
+
const parentPlural = pluralize(fk.parent.toLowerCase());
|
|
1708
|
+
lines.push(` sql_${lower} = sql_${lower} + ", FOREIGN KEY (${fk.fk}) REFERENCES ${parentPlural}(id)"`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
lines.push(` sql_${lower} = sql_${lower} + ")"`);
|
|
1712
|
+
lines.push(` log(sql_${lower})`);
|
|
1713
|
+
lines.push('');
|
|
1714
|
+
}
|
|
1715
|
+
for (const rel of m2mRels) {
|
|
1716
|
+
const jLower = rel.junction.toLowerCase();
|
|
1717
|
+
lines.push(` log("Creating junction table: ${jLower}")`);
|
|
1718
|
+
const lp = pluralize(rel.left.toLowerCase());
|
|
1719
|
+
const rp = pluralize(rel.right.toLowerCase());
|
|
1720
|
+
lines.push(` str sql_${jLower} = "CREATE TABLE IF NOT EXISTS ${jLower} (id INTEGER PRIMARY KEY AUTOINCREMENT, ${rel.fkLeft} INTEGER NOT NULL, ${rel.fkRight} INTEGER NOT NULL, created TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (${rel.fkLeft}) REFERENCES ${lp}(id), FOREIGN KEY (${rel.fkRight}) REFERENCES ${rp}(id), UNIQUE(${rel.fkLeft}, ${rel.fkRight}))"`);
|
|
1721
|
+
lines.push(` log(sql_${jLower})`);
|
|
1722
|
+
lines.push('');
|
|
1723
|
+
}
|
|
1724
|
+
lines.push(' log("Migration complete")');
|
|
1725
|
+
lines.push('');
|
|
1726
|
+
lines.push(`fn rollback():`);
|
|
1727
|
+
for (const entity of [...entities].reverse()) {
|
|
1728
|
+
const plural = pluralize(entity.name.toLowerCase());
|
|
1729
|
+
lines.push(` log("Dropping table: ${plural}")`);
|
|
1730
|
+
lines.push(` str drop_${entity.name.toLowerCase()} = "DROP TABLE IF EXISTS ${plural}"`);
|
|
1731
|
+
lines.push(` log(drop_${entity.name.toLowerCase()})`);
|
|
1732
|
+
}
|
|
1733
|
+
for (const rel of m2mRels) {
|
|
1734
|
+
const jLower = rel.junction.toLowerCase();
|
|
1735
|
+
lines.push(` str drop_${jLower} = "DROP TABLE IF EXISTS ${jLower}"`);
|
|
1736
|
+
lines.push(` log(drop_${jLower})`);
|
|
1737
|
+
}
|
|
1738
|
+
lines.push(' log("Rollback complete")');
|
|
1739
|
+
lines.push('');
|
|
1740
|
+
lines.push('migrate()');
|
|
1741
|
+
return lines.join('\n') + '\n';
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1504
1744
|
// ── Code Composer ───────────────────────────────────────────
|
|
1505
1745
|
|
|
1506
1746
|
function compose(intents, params) {
|
|
@@ -1531,6 +1771,11 @@ function compose(intents, params) {
|
|
|
1531
1771
|
sections.push(loggingBlock());
|
|
1532
1772
|
}
|
|
1533
1773
|
|
|
1774
|
+
// RBAC middleware (D1)
|
|
1775
|
+
if (hasAuth) {
|
|
1776
|
+
sections.push(rbacBlock());
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1534
1779
|
// Input validation functions (A4)
|
|
1535
1780
|
if (intents.has('schema') && intents.has('crud')) {
|
|
1536
1781
|
for (const entity of entities) {
|
|
@@ -1640,9 +1885,9 @@ function compose(intents, params) {
|
|
|
1640
1885
|
body.push('');
|
|
1641
1886
|
}
|
|
1642
1887
|
|
|
1643
|
-
// WebSocket with rooms (B11)
|
|
1888
|
+
// WebSocket with rooms + auth (B11, D4)
|
|
1644
1889
|
if (intents.has('websocket')) {
|
|
1645
|
-
body.push(...wsRoomsBlock());
|
|
1890
|
+
body.push(...wsRoomsBlock(hasAuth));
|
|
1646
1891
|
body.push('');
|
|
1647
1892
|
}
|
|
1648
1893
|
|
|
@@ -1666,6 +1911,14 @@ function compose(intents, params) {
|
|
|
1666
1911
|
}
|
|
1667
1912
|
}
|
|
1668
1913
|
|
|
1914
|
+
// RBAC role-guarded delete routes (D1)
|
|
1915
|
+
if (hasAuth && intents.has('crud') && intents.has('schema')) {
|
|
1916
|
+
for (const entity of entities) {
|
|
1917
|
+
body.push(...rbacRouteGuards(entity));
|
|
1918
|
+
body.push('');
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1669
1922
|
// File upload
|
|
1670
1923
|
if (intents.has('crud')) {
|
|
1671
1924
|
body.push(...uploadRoute());
|
|
@@ -1866,8 +2119,10 @@ export function generateProject(instruction, options = {}) {
|
|
|
1866
2119
|
dev: 'naide app.naide',
|
|
1867
2120
|
build: 'naide app.naide --emit -o dist/app.mjs',
|
|
1868
2121
|
start: 'node dist/app.mjs',
|
|
1869
|
-
test: '
|
|
2122
|
+
test: 'node --test test.mjs',
|
|
2123
|
+
'test:unit': 'naide test app.naide',
|
|
1870
2124
|
seed: 'naide app.naide --run-seed',
|
|
2125
|
+
migrate: 'naide migrate.naide',
|
|
1871
2126
|
},
|
|
1872
2127
|
dependencies: deps,
|
|
1873
2128
|
};
|
|
@@ -1997,6 +2252,17 @@ CMD ["node", "dist/app.mjs"]
|
|
|
1997
2252
|
// GitHub Actions CI (C16)
|
|
1998
2253
|
files.push({ path: '.github/workflows/ci.yml', content: githubActionsCI(projectName) });
|
|
1999
2254
|
|
|
2255
|
+
// Test runner (D2)
|
|
2256
|
+
if (intents.has('server') && entityObjs.length > 0) {
|
|
2257
|
+
files.push({ path: 'test.mjs', content: testRunnerFile(entityObjs, intents, result.port) });
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// DB migration (D3)
|
|
2261
|
+
if (intents.has('database') && entityObjs.length > 0) {
|
|
2262
|
+
const m2mRels = detectManyToMany(entities.map(e => e));
|
|
2263
|
+
files.push({ path: 'migrate.naide', content: migrationFile(entityObjs, result.relationships || [], m2mRels) });
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2000
2266
|
return { ...result, files };
|
|
2001
2267
|
}
|
|
2002
2268
|
|
|
@@ -2332,7 +2598,8 @@ function buildAnalysis(instruction, intents, params) {
|
|
|
2332
2598
|
if (params.relationships.length > 0) plan.push(`Nested routes: ${params.relationships.map(r => `/${pluralize(r.parent.toLowerCase())}/:id/${pluralize(r.child.toLowerCase())}`).join(', ')}`);
|
|
2333
2599
|
const m2m = detectManyToMany(params.entities.map(e => e.name));
|
|
2334
2600
|
if (m2m.length > 0) plan.push(`M:N relations: ${m2m.map(r => `${r.left}↔${r.right} via ${r.junction}`).join(', ')}`);
|
|
2335
|
-
if (intents.has('
|
|
2601
|
+
if (intents.has('auth')) plan.push('RBAC: requireRole() middleware for admin/moderator/user');
|
|
2602
|
+
if (intents.has('websocket'))plan.push('WebSocket: rooms + broadcast' + (intents.has('auth') ? ' (token auth)' : ''));
|
|
2336
2603
|
if (intents.has('ai')) plan.push('AI endpoint: /api/ask');
|
|
2337
2604
|
if (intents.has('bot')) plan.push(`Bot: ${params.platform || 'discord'}`);
|
|
2338
2605
|
if (intents.has('mail')) plan.push('Mail: SMTP configuration');
|
|
@@ -2388,3 +2655,62 @@ export function generate(instruction, options = {}) {
|
|
|
2388
2655
|
analysis,
|
|
2389
2656
|
};
|
|
2390
2657
|
}
|
|
2658
|
+
|
|
2659
|
+
// ── Multi-Target Generation (D6) ─────────────────────────────
|
|
2660
|
+
|
|
2661
|
+
const TARGET_EXTENSIONS = {
|
|
2662
|
+
node: '.mjs', python: '.py', bun: '.mjs', typescript: '.ts',
|
|
2663
|
+
go: '.go', java: '.java', rust: '.rs', cpp: '.cpp', c: '.c',
|
|
2664
|
+
csharp: '.cs', kotlin: '.kt', swift: '.swift', dart: '.dart',
|
|
2665
|
+
php: '.php', ruby: '.rb',
|
|
2666
|
+
};
|
|
2667
|
+
|
|
2668
|
+
const ALL_TARGETS = Object.keys(TARGET_EXTENSIONS);
|
|
2669
|
+
|
|
2670
|
+
export async function generateForTarget(instruction, target, options = {}) {
|
|
2671
|
+
const result = generate(instruction, options);
|
|
2672
|
+
if (!result.valid) return { ...result, compiled: null, target };
|
|
2673
|
+
|
|
2674
|
+
let compiled;
|
|
2675
|
+
try {
|
|
2676
|
+
const { compile, compileAsync } = await import('./index.js');
|
|
2677
|
+
if (target === 'node') {
|
|
2678
|
+
const r = compile(result.code);
|
|
2679
|
+
compiled = r.js;
|
|
2680
|
+
} else {
|
|
2681
|
+
const r = await compileAsync(result.code, { target });
|
|
2682
|
+
compiled = r.code;
|
|
2683
|
+
}
|
|
2684
|
+
} catch (e) {
|
|
2685
|
+
compiled = null;
|
|
2686
|
+
result.compileError = e.message;
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
return { ...result, compiled, target, extension: TARGET_EXTENSIONS[target] || '.txt' };
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
export async function generateAllTargets(instruction, options = {}) {
|
|
2693
|
+
const result = generate(instruction, options);
|
|
2694
|
+
if (!result.valid) return { ...result, targets: {} };
|
|
2695
|
+
|
|
2696
|
+
const targets = {};
|
|
2697
|
+
try {
|
|
2698
|
+
const { compile, compileAsync } = await import('./index.js');
|
|
2699
|
+
for (const target of ALL_TARGETS) {
|
|
2700
|
+
try {
|
|
2701
|
+
if (target === 'node') {
|
|
2702
|
+
targets[target] = { code: compile(result.code).js, ext: '.mjs' };
|
|
2703
|
+
} else {
|
|
2704
|
+
const r = await compileAsync(result.code, { target });
|
|
2705
|
+
targets[target] = { code: r.code, ext: TARGET_EXTENSIONS[target] };
|
|
2706
|
+
}
|
|
2707
|
+
} catch (e) {
|
|
2708
|
+
targets[target] = { error: e.message, ext: TARGET_EXTENSIONS[target] };
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
} catch (e) {
|
|
2712
|
+
return { ...result, targets: {}, importError: e.message };
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
return { ...result, targets };
|
|
2716
|
+
}
|