nothumanallowed 13.5.42 → 13.5.43
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/package.json +1 -1
- package/src/commands/ui.mjs +258 -0
- package/src/constants.mjs +1 -1
- package/src/services/web-ui.mjs +139 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "13.5.
|
|
3
|
+
"version": "13.5.43",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands/ui.mjs
CHANGED
|
@@ -3865,6 +3865,264 @@ ${completedHeadings ? `## SECTIONS ALREADY WRITTEN (headings only):\n${completed
|
|
|
3865
3865
|
return;
|
|
3866
3866
|
}
|
|
3867
3867
|
|
|
3868
|
+
// ── WebCraft Sandbox — fullstack preview in ~/.nha/webcraft/<project> ──
|
|
3869
|
+
// POST /api/studio/webcraft/sandbox/start { projectName, files[] }
|
|
3870
|
+
// → SSE stream with log lines, ends with { type:'ready', port, dir }
|
|
3871
|
+
// DELETE /api/studio/webcraft/sandbox → kill running sandbox process
|
|
3872
|
+
if (pathname === '/api/studio/webcraft/sandbox/start' && method === 'POST') {
|
|
3873
|
+
const body = await parseBody(req, 8 * 1024 * 1024); // 8MB max
|
|
3874
|
+
const projName = (body.projectName || 'webcraft-sandbox').replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
3875
|
+
const files = body.files || [];
|
|
3876
|
+
if (!files.length) { sendJSON(res, 400, { error: 'no files' }); return; }
|
|
3877
|
+
|
|
3878
|
+
res.writeHead(200, {
|
|
3879
|
+
'Content-Type': 'text/event-stream',
|
|
3880
|
+
'Cache-Control': 'no-cache',
|
|
3881
|
+
'Connection': 'keep-alive',
|
|
3882
|
+
'Access-Control-Allow-Origin': '*',
|
|
3883
|
+
});
|
|
3884
|
+
const sendLog = (msg) => res.write(`data: ${JSON.stringify({ type: 'log', msg })}\n\n`);
|
|
3885
|
+
const sendReady = (port, dir) => res.write(`data: ${JSON.stringify({ type: 'ready', port, dir })}\n\n`);
|
|
3886
|
+
const sendError = (msg) => res.write(`data: ${JSON.stringify({ type: 'error', msg })}\n\n`);
|
|
3887
|
+
|
|
3888
|
+
try {
|
|
3889
|
+
// Kill previous sandbox if running
|
|
3890
|
+
if (global._wcSandboxProc) {
|
|
3891
|
+
try { global._wcSandboxProc.kill('SIGTERM'); } catch(_) {}
|
|
3892
|
+
global._wcSandboxProc = null;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
const sandboxDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
|
|
3896
|
+
sendLog(`📁 Percorso sandbox: ${sandboxDir}`);
|
|
3897
|
+
fs.mkdirSync(sandboxDir, { recursive: true });
|
|
3898
|
+
|
|
3899
|
+
// Write all generated files
|
|
3900
|
+
sendLog(`📝 Scrittura di ${files.length} file...`);
|
|
3901
|
+
for (const f of files) {
|
|
3902
|
+
const filePath = path.join(sandboxDir, f.name);
|
|
3903
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
3904
|
+
fs.writeFileSync(filePath, f.content, 'utf8');
|
|
3905
|
+
sendLog(` ✓ ${f.name}`);
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
// Inject sandbox db shim — replaces pg with in-memory SQLite-like store
|
|
3909
|
+
const dbShim = `
|
|
3910
|
+
// NHA WebCraft Sandbox DB Shim
|
|
3911
|
+
// Replaces pg.Pool with a simple in-memory store so the sandbox runs without PostgreSQL.
|
|
3912
|
+
// For production, delete this file and restore server/db.js to use pg.Pool.
|
|
3913
|
+
const crypto = require('crypto');
|
|
3914
|
+
const tables = {};
|
|
3915
|
+
function ensureTable(t) { if (!tables[t]) tables[t] = []; }
|
|
3916
|
+
function matchesWhere(row, where) {
|
|
3917
|
+
if (!where) return true;
|
|
3918
|
+
return Object.keys(where).every(k => row[k] == where[k]);
|
|
3919
|
+
}
|
|
3920
|
+
const pool = {
|
|
3921
|
+
query: async function(text, params) {
|
|
3922
|
+
// Parse very simple SQL patterns for auth routes
|
|
3923
|
+
const t = text.trim();
|
|
3924
|
+
const ins = t.match(/^INSERT INTO (\\w+)\\s*\\(([^)]+)\\)/i);
|
|
3925
|
+
if (ins) {
|
|
3926
|
+
const tbl = ins[1]; ensureTable(tbl);
|
|
3927
|
+
const cols = ins[2].split(',').map(c=>c.trim());
|
|
3928
|
+
const row = { id: crypto.randomUUID() };
|
|
3929
|
+
cols.forEach((c,i)=>{ row[c] = params?params[i]:null; });
|
|
3930
|
+
row.created_at = new Date().toISOString();
|
|
3931
|
+
row.updated_at = new Date().toISOString();
|
|
3932
|
+
tables[tbl].push(row);
|
|
3933
|
+
return { rows: [row], rowCount: 1 };
|
|
3934
|
+
}
|
|
3935
|
+
const sel = t.match(/^SELECT (.+) FROM (\\w+)/i);
|
|
3936
|
+
if (sel) {
|
|
3937
|
+
const tbl = sel[2]; ensureTable(tbl);
|
|
3938
|
+
const whereM = t.match(/WHERE (.+?)(?:LIMIT|ORDER|$)/i);
|
|
3939
|
+
let rows = tables[tbl];
|
|
3940
|
+
if (whereM && params) {
|
|
3941
|
+
// Simple $1 = value matching
|
|
3942
|
+
let idx = 0;
|
|
3943
|
+
const conds = whereM[1].split(/AND/i).map(c=>c.trim());
|
|
3944
|
+
const where = {};
|
|
3945
|
+
conds.forEach(c=>{
|
|
3946
|
+
const m = c.match(/(\\w+)\\s*=\\s*\\$\\d+/i);
|
|
3947
|
+
if (m) { where[m[1]] = params[idx++]; }
|
|
3948
|
+
});
|
|
3949
|
+
rows = rows.filter(r=>matchesWhere(r,where));
|
|
3950
|
+
}
|
|
3951
|
+
const lim = t.match(/LIMIT (\\d+)/i);
|
|
3952
|
+
if (lim) rows = rows.slice(0, parseInt(lim[1]));
|
|
3953
|
+
return { rows, rowCount: rows.length };
|
|
3954
|
+
}
|
|
3955
|
+
const upd = t.match(/^UPDATE (\\w+) SET (.+?) WHERE/i);
|
|
3956
|
+
if (upd) {
|
|
3957
|
+
const tbl = upd[1]; ensureTable(tbl);
|
|
3958
|
+
const whereM = t.match(/WHERE (.+?)(?:RETURNING|$)/i);
|
|
3959
|
+
let updated = [];
|
|
3960
|
+
tables[tbl] = tables[tbl].map(row=>{
|
|
3961
|
+
let match = true;
|
|
3962
|
+
if (whereM && params) {
|
|
3963
|
+
const conds = whereM[1].split(/AND/i).map(c=>c.trim());
|
|
3964
|
+
let idx = params.length - conds.length;
|
|
3965
|
+
conds.forEach(c=>{
|
|
3966
|
+
const m = c.match(/(\\w+)\\s*=\\s*\\$\\d+/i);
|
|
3967
|
+
if (m && params[idx] !== undefined) match = match && (row[m[1]] == params[idx++]);
|
|
3968
|
+
});
|
|
3969
|
+
}
|
|
3970
|
+
if (match) { row.updated_at = new Date().toISOString(); updated.push(row); }
|
|
3971
|
+
return row;
|
|
3972
|
+
});
|
|
3973
|
+
return { rows: updated, rowCount: updated.length };
|
|
3974
|
+
}
|
|
3975
|
+
const del = t.match(/^DELETE FROM (\\w+)/i);
|
|
3976
|
+
if (del) {
|
|
3977
|
+
const tbl = del[1]; ensureTable(tbl);
|
|
3978
|
+
tables[tbl] = [];
|
|
3979
|
+
return { rows: [], rowCount: 0 };
|
|
3980
|
+
}
|
|
3981
|
+
return { rows: [], rowCount: 0 };
|
|
3982
|
+
},
|
|
3983
|
+
connect: async function() { return { query: pool.query, release: ()=>{} }; }
|
|
3984
|
+
};
|
|
3985
|
+
pool.query.bind(pool);
|
|
3986
|
+
async function query(text, params) { return pool.query(text, params); }
|
|
3987
|
+
async function transaction(cb) {
|
|
3988
|
+
const client = await pool.connect();
|
|
3989
|
+
try { const r = await cb(client); client.release(); return r; }
|
|
3990
|
+
catch(e) { client.release(); throw e; }
|
|
3991
|
+
}
|
|
3992
|
+
module.exports = { pool, query, transaction };
|
|
3993
|
+
`;
|
|
3994
|
+
fs.writeFileSync(path.join(sandboxDir, 'server', 'db.js'), dbShim, 'utf8');
|
|
3995
|
+
sendLog('🔧 DB shim iniettato (in-memory, no PostgreSQL richiesto)');
|
|
3996
|
+
|
|
3997
|
+
// Patch package.json to remove pg, add only what's needed
|
|
3998
|
+
const pkgPath = path.join(sandboxDir, 'package.json');
|
|
3999
|
+
let pkg = {};
|
|
4000
|
+
try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch(_) {}
|
|
4001
|
+
if (!pkg.dependencies) pkg.dependencies = {};
|
|
4002
|
+
delete pkg.dependencies.pg;
|
|
4003
|
+
// Ensure essentials
|
|
4004
|
+
pkg.dependencies.express = pkg.dependencies.express || '^4.19.0';
|
|
4005
|
+
pkg.dependencies.bcryptjs = pkg.dependencies.bcryptjs || '^2.4.3';
|
|
4006
|
+
pkg.dependencies.jsonwebtoken = pkg.dependencies.jsonwebtoken || '^9.0.0';
|
|
4007
|
+
pkg.dependencies.helmet = pkg.dependencies.helmet || '^7.0.0';
|
|
4008
|
+
pkg.dependencies['express-rate-limit'] = pkg.dependencies['express-rate-limit'] || '^7.0.0';
|
|
4009
|
+
pkg.dependencies.cors = pkg.dependencies.cors || '^2.8.5';
|
|
4010
|
+
pkg.dependencies.dotenv = pkg.dependencies.dotenv || '^16.0.0';
|
|
4011
|
+
pkg.dependencies.nodemailer = pkg.dependencies.nodemailer || '^6.9.0';
|
|
4012
|
+
pkg.dependencies['express-validator'] = pkg.dependencies['express-validator'] || '^7.0.0';
|
|
4013
|
+
delete pkg.dependencies.ioredis;
|
|
4014
|
+
pkg.scripts = pkg.scripts || {};
|
|
4015
|
+
pkg.scripts.start = 'node server/index.js';
|
|
4016
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), 'utf8');
|
|
4017
|
+
sendLog('📦 package.json ottimizzato per sandbox (rimosso pg, redis)');
|
|
4018
|
+
|
|
4019
|
+
// Create minimal .env for sandbox
|
|
4020
|
+
const envContent = [
|
|
4021
|
+
'PORT=0',
|
|
4022
|
+
'NODE_ENV=development',
|
|
4023
|
+
`JWT_SECRET=nha-sandbox-${crypto.randomBytes(16).toString('hex')}`,
|
|
4024
|
+
`JWT_REFRESH_SECRET=nha-sandbox-ref-${crypto.randomBytes(16).toString('hex')}`,
|
|
4025
|
+
'CORS_ORIGIN=*',
|
|
4026
|
+
`BASE_URL=http://localhost`,
|
|
4027
|
+
'SMTP_HOST=localhost',
|
|
4028
|
+
'SMTP_PORT=25',
|
|
4029
|
+
'SMTP_USER=sandbox',
|
|
4030
|
+
'SMTP_PASS=sandbox',
|
|
4031
|
+
'SMTP_FROM=sandbox@localhost',
|
|
4032
|
+
].join('\n');
|
|
4033
|
+
fs.writeFileSync(path.join(sandboxDir, '.env'), envContent, 'utf8');
|
|
4034
|
+
sendLog('⚙️ .env sandbox creato');
|
|
4035
|
+
|
|
4036
|
+
sendLog('');
|
|
4037
|
+
sendLog('📦 Dipendenze che verranno installate:');
|
|
4038
|
+
const deps = Object.keys(pkg.dependencies);
|
|
4039
|
+
deps.forEach(d => sendLog(` • ${d}@${pkg.dependencies[d]}`));
|
|
4040
|
+
sendLog(` Percorso: ${sandboxDir}/node_modules`);
|
|
4041
|
+
sendLog('');
|
|
4042
|
+
sendLog('⏳ npm install in corso...');
|
|
4043
|
+
|
|
4044
|
+
// npm install
|
|
4045
|
+
await new Promise((resolve, reject) => {
|
|
4046
|
+
const npm = exec(`npm install --prefer-offline 2>&1`, { cwd: sandboxDir, timeout: 120000 }, (err, stdout) => {
|
|
4047
|
+
if (err) { sendLog('❌ npm install fallito: ' + err.message); reject(err); }
|
|
4048
|
+
else { resolve(); }
|
|
4049
|
+
});
|
|
4050
|
+
npm.stdout && npm.stdout.on('data', d => { const line = d.toString().trim(); if (line) sendLog(' ' + line); });
|
|
4051
|
+
});
|
|
4052
|
+
sendLog('✅ npm install completato');
|
|
4053
|
+
|
|
4054
|
+
// Find free port
|
|
4055
|
+
const freePort = await new Promise(resolve => {
|
|
4056
|
+
const srv = (await import('net')).default.createServer();
|
|
4057
|
+
srv.listen(0, '127.0.0.1', () => { const p = srv.address().port; srv.close(() => resolve(p)); });
|
|
4058
|
+
});
|
|
4059
|
+
|
|
4060
|
+
// Patch PORT in .env
|
|
4061
|
+
fs.writeFileSync(path.join(sandboxDir, '.env'), envContent.replace('PORT=0', `PORT=${freePort}`), 'utf8');
|
|
4062
|
+
|
|
4063
|
+
sendLog(`🚀 Avvio server sandbox su porta ${freePort}...`);
|
|
4064
|
+
|
|
4065
|
+
// Spawn sandbox server
|
|
4066
|
+
const { spawn } = await import('child_process');
|
|
4067
|
+
const proc = spawn('node', ['server/index.js'], {
|
|
4068
|
+
cwd: sandboxDir,
|
|
4069
|
+
env: { ...process.env, PORT: String(freePort), NODE_ENV: 'development' },
|
|
4070
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4071
|
+
});
|
|
4072
|
+
global._wcSandboxProc = proc;
|
|
4073
|
+
global._wcSandboxPort = freePort;
|
|
4074
|
+
global._wcSandboxDir = sandboxDir;
|
|
4075
|
+
|
|
4076
|
+
proc.stdout.on('data', d => { const l = d.toString().trim(); if (l) sendLog(' [server] ' + l); });
|
|
4077
|
+
proc.stderr.on('data', d => { const l = d.toString().trim(); if (l) sendLog(' [server] ' + l); });
|
|
4078
|
+
|
|
4079
|
+
// Wait for server to be ready (max 10s)
|
|
4080
|
+
await new Promise((resolve, reject) => {
|
|
4081
|
+
let attempts = 0;
|
|
4082
|
+
const tryConnect = () => {
|
|
4083
|
+
import('net').then(({ default: net }) => {
|
|
4084
|
+
const s = net.createConnection(freePort, '127.0.0.1');
|
|
4085
|
+
s.on('connect', () => { s.destroy(); resolve(); });
|
|
4086
|
+
s.on('error', () => {
|
|
4087
|
+
s.destroy();
|
|
4088
|
+
if (++attempts > 20) reject(new Error('Server non risponde dopo 10s'));
|
|
4089
|
+
else setTimeout(tryConnect, 500);
|
|
4090
|
+
});
|
|
4091
|
+
});
|
|
4092
|
+
};
|
|
4093
|
+
setTimeout(tryConnect, 1000);
|
|
4094
|
+
});
|
|
4095
|
+
|
|
4096
|
+
sendLog(`✅ Sandbox pronta!`);
|
|
4097
|
+
sendReady(freePort, sandboxDir);
|
|
4098
|
+
} catch (e) {
|
|
4099
|
+
sendError(e.message);
|
|
4100
|
+
}
|
|
4101
|
+
res.end();
|
|
4102
|
+
logRequest(method, pathname, 200, Date.now() - start);
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
if (pathname === '/api/studio/webcraft/sandbox' && method === 'DELETE') {
|
|
4107
|
+
if (global._wcSandboxProc) {
|
|
4108
|
+
try { global._wcSandboxProc.kill('SIGTERM'); } catch(_) {}
|
|
4109
|
+
global._wcSandboxProc = null;
|
|
4110
|
+
}
|
|
4111
|
+
sendJSON(res, 200, { ok: true });
|
|
4112
|
+
logRequest(method, pathname, 200, Date.now() - start);
|
|
4113
|
+
return;
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
if (pathname === '/api/studio/webcraft/sandbox/status' && method === 'GET') {
|
|
4117
|
+
sendJSON(res, 200, {
|
|
4118
|
+
running: !!global._wcSandboxProc,
|
|
4119
|
+
port: global._wcSandboxPort || null,
|
|
4120
|
+
dir: global._wcSandboxDir || null,
|
|
4121
|
+
});
|
|
4122
|
+
logRequest(method, pathname, 200, Date.now() - start);
|
|
4123
|
+
return;
|
|
4124
|
+
}
|
|
4125
|
+
|
|
3868
4126
|
// ── Studio: Parliament deliberation (SSE streaming) ──────────────────
|
|
3869
4127
|
// Implements the Legion DeliberationEngine protocol adapted for Studio:
|
|
3870
4128
|
// Round 1 outputs already exist (from normal workflow steps).
|
package/src/constants.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
|
|
8
|
-
export const VERSION = '13.5.
|
|
8
|
+
export const VERSION = '13.5.43';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -3148,6 +3148,7 @@ var I18N = {
|
|
|
3148
3148
|
wc_download:'Download Archive', wc_describe_first:'Please describe your project first.',
|
|
3149
3149
|
wc_no_files:'Describe your project and click Generate',
|
|
3150
3150
|
wc_examples_label:'Examples',
|
|
3151
|
+
wc_sandbox_start:'Launch Sandbox',
|
|
3151
3152
|
},
|
|
3152
3153
|
it: {
|
|
3153
3154
|
chat:'Chat', studio:'Studio', settings:'Impostazioni', agents:'Agenti',
|
|
@@ -3184,6 +3185,7 @@ var I18N = {
|
|
|
3184
3185
|
wc_download:'Scarica archivio', wc_describe_first:'Descrivi prima il tuo progetto.',
|
|
3185
3186
|
wc_no_files:'Descrivi il progetto e clicca Genera',
|
|
3186
3187
|
wc_examples_label:'Esempi',
|
|
3188
|
+
wc_sandbox_start:'Avvia Sandbox',
|
|
3187
3189
|
},
|
|
3188
3190
|
es: {
|
|
3189
3191
|
chat:'Chat', studio:'Studio', settings:'Configuración', agents:'Agentes',
|
|
@@ -3219,6 +3221,7 @@ var I18N = {
|
|
|
3219
3221
|
wc_download:'Descargar archivo', wc_describe_first:'Por favor describe tu proyecto primero.',
|
|
3220
3222
|
wc_no_files:'Describe el proyecto y haz clic en Generar',
|
|
3221
3223
|
wc_examples_label:'Ejemplos',
|
|
3224
|
+
wc_sandbox_start:'Iniciar Sandbox',
|
|
3222
3225
|
},
|
|
3223
3226
|
fr: {
|
|
3224
3227
|
chat:'Chat', studio:'Studio', settings:'Paramètres', agents:'Agents',
|
|
@@ -3254,6 +3257,7 @@ var I18N = {
|
|
|
3254
3257
|
wc_download:'T\u00e9l\u00e9charger archive', wc_describe_first:'Veuillez d\u00e9crire votre projet.',
|
|
3255
3258
|
wc_no_files:'D\u00e9crivez le projet et cliquez sur G\u00e9n\u00e9rer',
|
|
3256
3259
|
wc_examples_label:'Exemples',
|
|
3260
|
+
wc_sandbox_start:'Lancer Sandbox',
|
|
3257
3261
|
},
|
|
3258
3262
|
de: {
|
|
3259
3263
|
chat:'Chat', studio:'Studio', settings:'Einstellungen', agents:'Agenten',
|
|
@@ -3289,6 +3293,7 @@ var I18N = {
|
|
|
3289
3293
|
wc_download:'Archiv herunterladen', wc_describe_first:'Bitte beschreibe zuerst dein Projekt.',
|
|
3290
3294
|
wc_no_files:'Beschreibe das Projekt und klicke auf Generieren',
|
|
3291
3295
|
wc_examples_label:'Beispiele',
|
|
3296
|
+
wc_sandbox_start:'Sandbox starten',
|
|
3292
3297
|
},
|
|
3293
3298
|
};
|
|
3294
3299
|
// Fallback to 'en' for unmapped languages
|
|
@@ -6212,8 +6217,17 @@ var wcState = {
|
|
|
6212
6217
|
generatedFiles: [], // [{name, content, lang}]
|
|
6213
6218
|
activeFile: 0,
|
|
6214
6219
|
running: false,
|
|
6215
|
-
projectName: ''
|
|
6220
|
+
projectName: '',
|
|
6221
|
+
rightTab: 'files', // 'files' | 'preview'
|
|
6222
|
+
sandbox: {
|
|
6223
|
+
running: false,
|
|
6224
|
+
port: null,
|
|
6225
|
+
dir: null,
|
|
6226
|
+
logs: [],
|
|
6227
|
+
error: null
|
|
6228
|
+
}
|
|
6216
6229
|
};
|
|
6230
|
+
var wcRightTab = 'files';
|
|
6217
6231
|
|
|
6218
6232
|
function wcEsc(s){return s?String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'):''}
|
|
6219
6233
|
|
|
@@ -6312,14 +6326,20 @@ function renderWebCraft(el) {
|
|
|
6312
6326
|
'</button>' +
|
|
6313
6327
|
|
|
6314
6328
|
(wcState.generatedFiles.length > 0 ?
|
|
6315
|
-
'<button onclick="wcDownloadZip()" style="width:100%;padding:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-size:12px;font-weight:600;cursor:pointer">⇩ '+t('wc_download')+'</button>'
|
|
6329
|
+
'<button onclick="wcDownloadZip()" style="width:100%;padding:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-size:12px;font-weight:600;cursor:pointer">⇩ '+t('wc_download')+'</button>' +
|
|
6330
|
+
'<button onclick="wcStartSandbox()" id="wcSandboxBtn" style="width:100%;padding:10px;background:var(--bg3);border:1px solid var(--green3);border-radius:8px;color:var(--green);font-size:12px;font-weight:600;cursor:pointer">▶ '+t('wc_sandbox_start')+'</button>'
|
|
6331
|
+
: '') +
|
|
6316
6332
|
|
|
6317
6333
|
'</div>' +
|
|
6318
6334
|
|
|
6319
|
-
// RIGHT:
|
|
6335
|
+
// RIGHT: tabs — Files | Preview
|
|
6320
6336
|
'<div style="flex:1;min-width:0;background:var(--bg2);border:1px solid var(--border);border-radius:10px;display:flex;flex-direction:column;height:calc(100vh - 120px);overflow:hidden">' +
|
|
6321
|
-
|
|
6322
|
-
|
|
6337
|
+
// Tab bar — use named functions to avoid quoting issues
|
|
6338
|
+
'<div style="display:flex;border-bottom:1px solid var(--border);flex-shrink:0">' +
|
|
6339
|
+
'<button onclick="wcTabFiles()" style="padding:8px 16px;background:'+(wcRightTab==='preview'?'transparent':'var(--bg3)')+';border:none;border-right:1px solid var(--border);color:'+(wcRightTab==='preview'?'var(--dim)':'var(--text)')+';font-size:11px;font-weight:600;cursor:pointer">📄 File</button>' +
|
|
6340
|
+
'<button onclick="wcTabPreview()" style="padding:8px 16px;background:'+(wcRightTab==='preview'?'var(--bg3)':'transparent')+';border:none;color:'+(wcRightTab==='preview'?'var(--text)':'var(--dim)')+';font-size:11px;font-weight:600;cursor:pointer">🌐 Sandbox</button>' +
|
|
6341
|
+
'</div>' +
|
|
6342
|
+
(wcRightTab === 'preview' ? wcSandboxPanelHtml() : (fileTabsHtml + codeHtml)) +
|
|
6323
6343
|
'</div>' +
|
|
6324
6344
|
|
|
6325
6345
|
'</div>' +
|
|
@@ -6341,6 +6361,9 @@ function wcPickExample(i) {
|
|
|
6341
6361
|
wcState.description = ex.desc;
|
|
6342
6362
|
renderWebCraft(document.getElementById('content'));
|
|
6343
6363
|
}
|
|
6364
|
+
function wcTabFiles() { wcRightTab = 'files'; renderWebCraft(document.getElementById('content')); }
|
|
6365
|
+
function wcTabPreview() { wcRightTab = 'preview'; renderWebCraft(document.getElementById('content')); }
|
|
6366
|
+
function wcOpenSandbox() { if (wcState.sandbox.port) window.open('http://127.0.0.1:' + wcState.sandbox.port, '_blank'); }
|
|
6344
6367
|
function wcUpdateField(i, val) { wcState.authFields[i].label = val; }
|
|
6345
6368
|
function wcUpdateFieldType(i, t) { wcState.authFields[i].type = t; }
|
|
6346
6369
|
function wcToggleRequired(i, v) { wcState.authFields[i].required = v; }
|
|
@@ -6402,6 +6425,7 @@ async function wcGenerate() {
|
|
|
6402
6425
|
{ name: 'server/middleware/sentinel.js', lang: 'javascript', prompt: 'Generate server/middleware/sentinel.js: a lightweight WAF middleware for Express. Check request for: SQL injection patterns (UNION SELECT, DROP TABLE, etc.), XSS patterns (<script, javascript:, onerror=), path traversal (../), oversized payloads (>100KB body). Rate limit by IP using an in-memory sliding window (fallback when Redis unavailable). Log blocked requests with IP, method, path, reason to stderr. Export sentinelMiddleware(req, res, next).' },
|
|
6403
6426
|
{ name: 'server/services/cache.js', lang: 'javascript', prompt: 'Generate server/services/cache.js: Redis/Dragonfly client using ioredis. Connect to REDIS_URL from env. Export: get(key), set(key, value, ttlSeconds), del(key), exists(key). Add circuit breaker pattern: if Redis fails 3+ times in 30s, switch to in-memory LRU fallback (Map with max 1000 entries, LRU eviction). Reconnect Redis in background every 60s. Log circuit state changes. This makes the app resilient when Redis is down.' },
|
|
6404
6427
|
{ name: 'README.md', lang: 'markdown', prompt: 'Generate README.md for "'+projName+'": project description, tech stack (Express, PostgreSQL with circuit breaker, Redis/Dragonfly with LRU fallback, JWT auth, Nodemailer SMTP + SendGrid fallback, Sentinel WAF, BEM CSS), folder structure, setup instructions (clone, npm install, copy .env.example to .env, run migrations with psql, optional: start Redis/Dragonfly, npm run dev), environment variables table (including REDIS_URL), API endpoints table, security notes, email configuration guide.' },
|
|
6428
|
+
{ name: 'nginx.conf', lang: 'nginx', prompt: 'Generate a production-ready nginx.conf for "'+projName+'" running as an Express app on localhost:3000. Include: HTTPS with TLS 1.2/1.3 only, strong cipher suites, HSTS (max-age=31536000; includeSubDomains; preload), X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy (geolocation=(), microphone=(), camera=()), Content-Security-Policy (default-src self, script-src self, style-src self unsafe-inline, img-src self data:, connect-src self, frame-src none, object-src none), rate limiting (limit_req_zone 10r/s burst 20), gzip compression, proxy_pass to http://127.0.0.1:3000 with correct headers (X-Real-IP, X-Forwarded-For, X-Forwarded-Proto), static files served directly from public/ with long cache headers, Certbot/ACME path. Use server_name example.com with a TODO comment. Security rating: A+ on securityheaders.com.' },
|
|
6405
6429
|
];
|
|
6406
6430
|
|
|
6407
6431
|
// Filter by selected blocks
|
|
@@ -6455,6 +6479,116 @@ async function wcCallLLM(sys, user) {
|
|
|
6455
6479
|
return (d && (d.text || d.content || d.result)) || '';
|
|
6456
6480
|
}
|
|
6457
6481
|
|
|
6482
|
+
function wcSandboxPanelHtml() {
|
|
6483
|
+
var sb = wcState.sandbox;
|
|
6484
|
+
if (!wcState.generatedFiles.length) {
|
|
6485
|
+
return '<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--dim);font-size:13px">Genera prima il progetto</div>';
|
|
6486
|
+
}
|
|
6487
|
+
var logsHtml = sb.logs.length
|
|
6488
|
+
? '<div id="wcSbLogs" style="flex:1;overflow-y:auto;padding:10px 14px;font-family:var(--mono);font-size:11px;line-height:1.7;color:var(--dim)">' +
|
|
6489
|
+
sb.logs.map(function(l){ return '<div>'+wcEsc(l)+'</div>'; }).join('') +
|
|
6490
|
+
'</div>'
|
|
6491
|
+
: '<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--dim);font-size:12px">Premi "Avvia Sandbox" per avviare il server locale</div>';
|
|
6492
|
+
|
|
6493
|
+
if (sb.port && !sb.running) {
|
|
6494
|
+
// Server ready — show iframe
|
|
6495
|
+
return '<div style="display:flex;flex-direction:column;flex:1;min-height:0">' +
|
|
6496
|
+
'<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--border);flex-shrink:0">' +
|
|
6497
|
+
'<span style="font-size:11px;color:var(--dim);font-family:var(--mono)">http://127.0.0.1:'+sb.port+'</span>' +
|
|
6498
|
+
'<span style="font-size:10px;color:var(--dim)">— ' + wcEsc(sb.dir || '') + '</span>' +
|
|
6499
|
+
'<button onclick="wcStopSandbox()" style="margin-left:auto;padding:3px 10px;background:var(--bg3);border:1px solid var(--border2);border-radius:5px;color:var(--red);font-size:11px;cursor:pointer">■ Ferma</button>' +
|
|
6500
|
+
'<button onclick="wcOpenSandbox()" style="padding:3px 10px;background:var(--green3);border:none;border-radius:5px;color:var(--bg);font-size:11px;cursor:pointer;font-weight:700">↗ Apri</button>' +
|
|
6501
|
+
'</div>' +
|
|
6502
|
+
'<iframe src="http://127.0.0.1:'+sb.port+'" style="flex:1;border:none;width:100%;background:#fff" sandbox="allow-scripts allow-forms allow-same-origin allow-popups"></iframe>' +
|
|
6503
|
+
'</div>';
|
|
6504
|
+
}
|
|
6505
|
+
|
|
6506
|
+
return '<div style="display:flex;flex-direction:column;flex:1;min-height:0">' +
|
|
6507
|
+
// Info bar
|
|
6508
|
+
(!sb.running && !sb.port ? '<div style="padding:10px 14px;border-bottom:1px solid var(--border);flex-shrink:0;font-size:11px;color:var(--dim);line-height:1.6">' +
|
|
6509
|
+
'<div style="font-weight:700;color:var(--text);margin-bottom:4px">Sandbox locale — cosa succede quando avvii:</div>' +
|
|
6510
|
+
'<div>• I file vengono scritti in <span style="font-family:var(--mono);color:var(--green)">~/.nha/webcraft/'+(wcState.projectName||'project')+'</span></div>' +
|
|
6511
|
+
'<div>• npm install delle dipendenze nella stessa cartella (nessuna installazione globale)</div>' +
|
|
6512
|
+
'<div>• Il server Express parte su una porta locale casuale (es. 45123)</div>' +
|
|
6513
|
+
'<div>• Il DB usa un in-memory store (nessun PostgreSQL richiesto)</div>' +
|
|
6514
|
+
'<div>• I dati sandbox sono temporanei e si azzerano al riavvio</div>' +
|
|
6515
|
+
'<div style="margin-top:6px;color:var(--amber)">⚠ Sandbox solo locale — nessun dato esce dal tuo Mac</div>' +
|
|
6516
|
+
'</div>' : '') +
|
|
6517
|
+
logsHtml +
|
|
6518
|
+
(sb.error ? '<div style="padding:8px 14px;color:var(--red);font-size:11px;font-family:var(--mono);flex-shrink:0">❌ '+wcEsc(sb.error)+'</div>' : '') +
|
|
6519
|
+
'</div>';
|
|
6520
|
+
}
|
|
6521
|
+
|
|
6522
|
+
async function wcStartSandbox() {
|
|
6523
|
+
if (wcState.sandbox.running) return;
|
|
6524
|
+
wcState.sandbox = { running: true, port: null, dir: null, logs: [], error: null };
|
|
6525
|
+
wcState.rightTab = 'preview';
|
|
6526
|
+
wcRightTab = 'preview';
|
|
6527
|
+
renderWebCraft(document.getElementById('content'));
|
|
6528
|
+
|
|
6529
|
+
try {
|
|
6530
|
+
var r = await fetch(API + '/api/studio/webcraft/sandbox/start', {
|
|
6531
|
+
method: 'POST',
|
|
6532
|
+
headers: {'Content-Type':'application/json'},
|
|
6533
|
+
body: JSON.stringify({
|
|
6534
|
+
projectName: wcState.projectName || 'webcraft-sandbox',
|
|
6535
|
+
files: wcState.generatedFiles
|
|
6536
|
+
})
|
|
6537
|
+
});
|
|
6538
|
+
if (!r.ok || !r.body) throw new Error('Sandbox error ' + r.status);
|
|
6539
|
+
var reader = r.body.getReader();
|
|
6540
|
+
var dec = new TextDecoder();
|
|
6541
|
+
var buf = '';
|
|
6542
|
+
while (true) {
|
|
6543
|
+
var chunk = await reader.read();
|
|
6544
|
+
if (chunk.done) break;
|
|
6545
|
+
buf += dec.decode(chunk.value, {stream: true});
|
|
6546
|
+
var _dbl = String.fromCharCode(10)+String.fromCharCode(10);
|
|
6547
|
+
var parts = buf.split(_dbl);
|
|
6548
|
+
buf = parts.pop();
|
|
6549
|
+
for (var i = 0; i < parts.length; i++) {
|
|
6550
|
+
var line = parts[i].trim();
|
|
6551
|
+
if (!line.startsWith('data:')) continue;
|
|
6552
|
+
try {
|
|
6553
|
+
var evt = JSON.parse(line.slice(5).trim());
|
|
6554
|
+
if (evt.type === 'log') {
|
|
6555
|
+
wcState.sandbox.logs.push(evt.msg);
|
|
6556
|
+
// Live update logs only (avoid full re-render on every line)
|
|
6557
|
+
var logsEl = document.getElementById('wcSbLogs');
|
|
6558
|
+
if (logsEl) {
|
|
6559
|
+
var d = document.createElement('div');
|
|
6560
|
+
d.textContent = evt.msg;
|
|
6561
|
+
logsEl.appendChild(d);
|
|
6562
|
+
logsEl.scrollTop = logsEl.scrollHeight;
|
|
6563
|
+
} else {
|
|
6564
|
+
renderWebCraft(document.getElementById('content'));
|
|
6565
|
+
}
|
|
6566
|
+
} else if (evt.type === 'ready') {
|
|
6567
|
+
wcState.sandbox.running = false;
|
|
6568
|
+
wcState.sandbox.port = evt.port;
|
|
6569
|
+
wcState.sandbox.dir = evt.dir;
|
|
6570
|
+
renderWebCraft(document.getElementById('content'));
|
|
6571
|
+
} else if (evt.type === 'error') {
|
|
6572
|
+
wcState.sandbox.running = false;
|
|
6573
|
+
wcState.sandbox.error = evt.msg;
|
|
6574
|
+
renderWebCraft(document.getElementById('content'));
|
|
6575
|
+
}
|
|
6576
|
+
} catch(_) {}
|
|
6577
|
+
}
|
|
6578
|
+
}
|
|
6579
|
+
} catch(e) {
|
|
6580
|
+
wcState.sandbox.running = false;
|
|
6581
|
+
wcState.sandbox.error = e.message;
|
|
6582
|
+
renderWebCraft(document.getElementById('content'));
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
|
|
6586
|
+
async function wcStopSandbox() {
|
|
6587
|
+
await fetch(API + '/api/studio/webcraft/sandbox', {method:'DELETE'});
|
|
6588
|
+
wcState.sandbox = { running: false, port: null, dir: null, logs: [], error: null };
|
|
6589
|
+
renderWebCraft(document.getElementById('content'));
|
|
6590
|
+
}
|
|
6591
|
+
|
|
6458
6592
|
function wcDownloadZip() {
|
|
6459
6593
|
if (!wcState.generatedFiles.length) return;
|
|
6460
6594
|
// Build a real ZIP file (PKZIP format, stored/no compression) — zero dependencies
|