tova 0.7.0 → 0.9.4

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.
Files changed (59) hide show
  1. package/bin/tova.js +1312 -139
  2. package/package.json +8 -1
  3. package/src/analyzer/analyzer.js +539 -11
  4. package/src/analyzer/browser-analyzer.js +56 -8
  5. package/src/analyzer/deploy-analyzer.js +44 -0
  6. package/src/analyzer/scope.js +7 -0
  7. package/src/analyzer/server-analyzer.js +33 -1
  8. package/src/codegen/base-codegen.js +1296 -23
  9. package/src/codegen/browser-codegen.js +725 -20
  10. package/src/codegen/codegen.js +87 -5
  11. package/src/codegen/deploy-codegen.js +49 -0
  12. package/src/codegen/server-codegen.js +54 -6
  13. package/src/codegen/shared-codegen.js +5 -0
  14. package/src/codegen/theme-codegen.js +69 -0
  15. package/src/codegen/wasm-codegen.js +6 -0
  16. package/src/config/edit-toml.js +6 -2
  17. package/src/config/git-resolver.js +128 -0
  18. package/src/config/lock-file.js +57 -0
  19. package/src/config/module-cache.js +58 -0
  20. package/src/config/module-entry.js +37 -0
  21. package/src/config/module-path.js +63 -0
  22. package/src/config/pkg-errors.js +62 -0
  23. package/src/config/resolve.js +26 -0
  24. package/src/config/resolver.js +139 -0
  25. package/src/config/search.js +28 -0
  26. package/src/config/semver.js +72 -0
  27. package/src/config/toml.js +61 -6
  28. package/src/deploy/deploy.js +217 -0
  29. package/src/deploy/infer.js +218 -0
  30. package/src/deploy/provision.js +315 -0
  31. package/src/diagnostics/security-scorecard.js +111 -0
  32. package/src/lexer/lexer.js +18 -3
  33. package/src/lsp/server.js +482 -0
  34. package/src/parser/animate-ast.js +45 -0
  35. package/src/parser/ast.js +39 -0
  36. package/src/parser/browser-ast.js +19 -1
  37. package/src/parser/browser-parser.js +221 -4
  38. package/src/parser/concurrency-ast.js +15 -0
  39. package/src/parser/concurrency-parser.js +236 -0
  40. package/src/parser/deploy-ast.js +37 -0
  41. package/src/parser/deploy-parser.js +132 -0
  42. package/src/parser/parser.js +42 -5
  43. package/src/parser/select-ast.js +39 -0
  44. package/src/parser/theme-ast.js +29 -0
  45. package/src/parser/theme-parser.js +70 -0
  46. package/src/registry/plugins/concurrency-plugin.js +32 -0
  47. package/src/registry/plugins/deploy-plugin.js +33 -0
  48. package/src/registry/plugins/theme-plugin.js +20 -0
  49. package/src/registry/register-all.js +6 -0
  50. package/src/runtime/charts.js +547 -0
  51. package/src/runtime/embedded.js +6 -2
  52. package/src/runtime/reactivity.js +60 -0
  53. package/src/runtime/router.js +703 -295
  54. package/src/runtime/table.js +606 -33
  55. package/src/stdlib/inline.js +365 -10
  56. package/src/stdlib/runtime-bridge.js +152 -0
  57. package/src/stdlib/string.js +84 -2
  58. package/src/stdlib/validation.js +1 -1
  59. package/src/version.js +1 -1
@@ -0,0 +1,315 @@
1
+ // Provisioning Script Generator for the Tova language
2
+ // Generates idempotent bash scripts from an infrastructure manifest.
3
+
4
+ /**
5
+ * Generate a complete provisioning shell script from an infrastructure manifest.
6
+ *
7
+ * The script is idempotent — it checks before installing (command -v, dpkg, etc.)
8
+ * and is organized in layers:
9
+ * Layer 1: System (Bun, Caddy, UFW, tova user)
10
+ * Layer 2: Databases (PostgreSQL, Redis — conditional)
11
+ * Layer 3: App directories
12
+ * Layer 5: Caddy config
13
+ * Layer 6: systemd services
14
+ *
15
+ * @param {Object} manifest - Infrastructure manifest from inferInfrastructure()
16
+ * @returns {string} Complete provisioning bash script
17
+ */
18
+ export function generateProvisionScript(manifest) {
19
+ const appName = manifest.name || 'tova-app';
20
+ const lines = [];
21
+
22
+ lines.push('#!/bin/bash');
23
+ lines.push('set -euo pipefail');
24
+ lines.push('');
25
+ lines.push(`# Provisioning script for ${appName}`);
26
+ lines.push(`# Generated by Tova deploy — idempotent, safe to re-run`);
27
+ lines.push('');
28
+
29
+ // ── Layer 1: System ────────────────────────────────────────
30
+ lines.push('# ═══════════════════════════════════════════════════════════');
31
+ lines.push('# Layer 1: System dependencies');
32
+ lines.push('# ═══════════════════════════════════════════════════════════');
33
+ lines.push('');
34
+
35
+ if (manifest.requires && manifest.requires.bun) {
36
+ lines.push('# Install Bun runtime');
37
+ lines.push('if ! command -v bun &>/dev/null; then');
38
+ lines.push(' echo "Installing Bun..."');
39
+ lines.push(' curl -fsSL https://bun.sh/install | bash');
40
+ lines.push(' export PATH="$HOME/.bun/bin:$PATH"');
41
+ lines.push('fi');
42
+ lines.push('');
43
+ }
44
+
45
+ if (manifest.requires && manifest.requires.caddy) {
46
+ lines.push('# Install Caddy web server');
47
+ lines.push('if ! command -v caddy &>/dev/null; then');
48
+ lines.push(' echo "Installing Caddy..."');
49
+ lines.push(' apt-get update -qq');
50
+ lines.push(' apt-get install -y -qq debian-keyring debian-archive-keyring apt-transport-https curl');
51
+ lines.push(' curl -1sLf "https://dl.cloudsmith.io/public/caddy/stable/gpg.key" | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg');
52
+ lines.push(' curl -1sLf "https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt" | tee /etc/apt/sources.list.d/caddy-stable.list');
53
+ lines.push(' apt-get update -qq');
54
+ lines.push(' apt-get install -y -qq caddy');
55
+ lines.push('fi');
56
+ lines.push('');
57
+ }
58
+
59
+ if (manifest.requires && manifest.requires.ufw) {
60
+ lines.push('# Configure UFW firewall');
61
+ lines.push('if command -v ufw &>/dev/null; then');
62
+ lines.push(' ufw allow 22/tcp');
63
+ lines.push(' ufw allow 80/tcp');
64
+ lines.push(' ufw allow 443/tcp');
65
+ lines.push(' echo "y" | ufw enable || true');
66
+ lines.push('fi');
67
+ lines.push('');
68
+ }
69
+
70
+ // Create tova system user
71
+ lines.push('# Create tova system user');
72
+ lines.push('if ! id "tova" &>/dev/null; then');
73
+ lines.push(' useradd --system --create-home --shell /bin/bash tova');
74
+ lines.push('fi');
75
+ lines.push('');
76
+
77
+ // ── Layer 2: Databases ─────────────────────────────────────
78
+ const databases = manifest.databases || [];
79
+ const hasPostgres = databases.some(d => d.engine === 'postgres');
80
+ const hasRedis = databases.some(d => d.engine === 'redis');
81
+
82
+ if (hasPostgres || hasRedis) {
83
+ lines.push('# ═══════════════════════════════════════════════════════════');
84
+ lines.push('# Layer 2: Databases');
85
+ lines.push('# ═══════════════════════════════════════════════════════════');
86
+ lines.push('');
87
+ }
88
+
89
+ if (hasPostgres) {
90
+ const pgDb = databases.find(d => d.engine === 'postgres');
91
+ const dbName = (pgDb && pgDb.config && pgDb.config.name) || `${appName}_db`;
92
+ lines.push('# Install PostgreSQL');
93
+ lines.push('if ! command -v psql &>/dev/null; then');
94
+ lines.push(' echo "Installing PostgreSQL..."');
95
+ lines.push(' apt-get update -qq');
96
+ lines.push(' apt-get install -y -qq postgresql postgresql-contrib');
97
+ lines.push(' systemctl enable postgresql');
98
+ lines.push(' systemctl start postgresql');
99
+ lines.push('fi');
100
+ lines.push('');
101
+ lines.push(`# Create database: ${dbName}`);
102
+ lines.push(`sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname = '${dbName}'" | grep -q 1 || sudo -u postgres createdb "${dbName}"`);
103
+ lines.push('');
104
+ }
105
+
106
+ if (hasRedis) {
107
+ lines.push('# Install Redis');
108
+ lines.push('if ! command -v redis-server &>/dev/null; then');
109
+ lines.push(' echo "Installing Redis..."');
110
+ lines.push(' apt-get update -qq');
111
+ lines.push(' apt-get install -y -qq redis-server');
112
+ lines.push(' systemctl enable redis-server');
113
+ lines.push(' systemctl start redis-server');
114
+ lines.push('fi');
115
+ lines.push('');
116
+ }
117
+
118
+ // ── Layer 3: App directories ───────────────────────────────
119
+ lines.push('# ═══════════════════════════════════════════════════════════');
120
+ lines.push('# Layer 3: App directories');
121
+ lines.push('# ═══════════════════════════════════════════════════════════');
122
+ lines.push('');
123
+ lines.push('mkdir -p /opt/tova/apps');
124
+ lines.push(`APP_DIR="/opt/tova/apps/${appName}"`);
125
+ lines.push('mkdir -p "$APP_DIR/releases"');
126
+ lines.push('mkdir -p "$APP_DIR/shared/logs"');
127
+ lines.push('mkdir -p "$APP_DIR/shared/data"');
128
+ lines.push('chown -R tova:tova /opt/tova');
129
+ lines.push('');
130
+
131
+ // ── Layer 5: Caddy config ──────────────────────────────────
132
+ if (manifest.requires && manifest.requires.caddy && manifest.domain) {
133
+ lines.push('# ═══════════════════════════════════════════════════════════');
134
+ lines.push('# Layer 5: Caddy config');
135
+ lines.push('# ═══════════════════════════════════════════════════════════');
136
+ lines.push('');
137
+ const caddyConfig = generateCaddyConfig(appName, {
138
+ domain: manifest.domain,
139
+ instances: manifest.instances || 1,
140
+ health: manifest.health,
141
+ health_interval: manifest.health_interval,
142
+ health_timeout: manifest.health_timeout,
143
+ hasWebSocket: manifest.hasWebSocket,
144
+ });
145
+ lines.push(`cat > /etc/caddy/Caddyfile <<'CADDY_EOF'`);
146
+ lines.push(caddyConfig);
147
+ lines.push('CADDY_EOF');
148
+ lines.push('');
149
+ lines.push('systemctl reload caddy || systemctl restart caddy');
150
+ lines.push('');
151
+ }
152
+
153
+ // ── Layer 6: systemd services ──────────────────────────────
154
+ lines.push('# ═══════════════════════════════════════════════════════════');
155
+ lines.push('# Layer 6: systemd services');
156
+ lines.push('# ═══════════════════════════════════════════════════════════');
157
+ lines.push('');
158
+ const serviceUnit = generateSystemdService(appName, {
159
+ memory: manifest.memory || '512mb',
160
+ restart_on_failure: manifest.restart_on_failure !== false,
161
+ env: manifest.env || {},
162
+ });
163
+ lines.push(`cat > /etc/systemd/system/${appName}@.service <<'SYSTEMD_EOF'`);
164
+ lines.push(serviceUnit);
165
+ lines.push('SYSTEMD_EOF');
166
+ lines.push('');
167
+ lines.push('systemctl daemon-reload');
168
+
169
+ // Enable and start instances
170
+ const instances = manifest.instances || 1;
171
+ for (let i = 0; i < instances; i++) {
172
+ const port = 3000 + i;
173
+ lines.push(`systemctl enable ${appName}@${port}`);
174
+ }
175
+ lines.push('');
176
+
177
+ lines.push(`echo "Provisioning complete for ${appName}"`);
178
+ return lines.join('\n');
179
+ }
180
+
181
+ /**
182
+ * Parse a memory string like "512mb", "1gb" into bytes for systemd MemoryMax.
183
+ */
184
+ function parseMemory(mem) {
185
+ if (typeof mem === 'number') return mem;
186
+ const str = String(mem).toLowerCase().trim();
187
+ const match = str.match(/^(\d+(?:\.\d+)?)\s*(mb|gb|m|g|kb|k)?$/);
188
+ if (!match) return str;
189
+ const num = parseFloat(match[1]);
190
+ const unit = match[2] || 'mb';
191
+ switch (unit) {
192
+ case 'kb': case 'k': return `${Math.round(num)}K`;
193
+ case 'mb': case 'm': return `${Math.round(num)}M`;
194
+ case 'gb': case 'g': return `${Math.round(num * 1024)}M`;
195
+ default: return str;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Generate a systemd unit template for the application.
201
+ *
202
+ * Uses %i for the instance port (template unit: appName@.service).
203
+ *
204
+ * @param {string} appName - Application name
205
+ * @param {Object} config - { memory, restart_on_failure, env }
206
+ * @returns {string} systemd unit file content
207
+ */
208
+ export function generateSystemdService(appName, config = {}) {
209
+ const memLimit = parseMemory(config.memory || '512mb');
210
+ const restart = config.restart_on_failure !== false ? 'on-failure' : 'no';
211
+ const env = config.env || {};
212
+
213
+ const lines = [];
214
+ lines.push('[Unit]');
215
+ lines.push(`Description=${appName} instance on port %i`);
216
+ lines.push('After=network.target');
217
+ lines.push('');
218
+ lines.push('[Service]');
219
+ lines.push('Type=simple');
220
+ lines.push('User=tova');
221
+ lines.push('Group=tova');
222
+ lines.push(`WorkingDirectory=/opt/tova/apps/${appName}/current`);
223
+ lines.push(`ExecStart=/home/tova/.bun/bin/bun run server.js --port %i`);
224
+ lines.push(`Restart=${restart}`);
225
+ lines.push('RestartSec=5');
226
+ lines.push(`MemoryMax=${memLimit}`);
227
+ lines.push('');
228
+
229
+ // Environment — load secrets from .env.production, then set inline defaults
230
+ lines.push(`EnvironmentFile=-/opt/tova/apps/${appName}/.env.production`);
231
+ lines.push('Environment=NODE_ENV=production');
232
+ lines.push('Environment=PORT=%i');
233
+ for (const [key, value] of Object.entries(env)) {
234
+ if (key !== 'NODE_ENV' && key !== 'PORT') {
235
+ lines.push(`Environment=${key}=${value}`);
236
+ }
237
+ }
238
+ lines.push('');
239
+
240
+ // Logging
241
+ lines.push('StandardOutput=journal');
242
+ lines.push('StandardError=journal');
243
+ lines.push(`SyslogIdentifier=${appName}-%i`);
244
+ lines.push('');
245
+ lines.push('[Install]');
246
+ lines.push('WantedBy=multi-user.target');
247
+
248
+ return lines.join('\n');
249
+ }
250
+
251
+ /**
252
+ * Generate a Caddy site block configuration.
253
+ *
254
+ * @param {string} appName - Application name
255
+ * @param {Object} opts - { domain, instances, health, hasWebSocket }
256
+ * @returns {string} Caddy config block
257
+ */
258
+ export function generateCaddyConfig(appName, opts = {}) {
259
+ const domain = opts.domain || 'localhost';
260
+ const instances = opts.instances || 1;
261
+ const health = opts.health || '/healthz';
262
+ const healthInterval = opts.health_interval || 30;
263
+ const healthTimeout = opts.health_timeout || 5;
264
+ const hasWebSocket = opts.hasWebSocket || false;
265
+
266
+ const lines = [];
267
+ lines.push(`${domain} {`);
268
+
269
+ // Upstream / reverse proxy
270
+ if (instances === 1) {
271
+ lines.push(' reverse_proxy localhost:3000 {');
272
+ } else {
273
+ // Multiple instances with round-robin load balancing
274
+ const upstreams = [];
275
+ for (let i = 0; i < instances; i++) {
276
+ upstreams.push(`localhost:${3000 + i}`);
277
+ }
278
+ lines.push(` reverse_proxy ${upstreams.join(' ')} {`);
279
+ lines.push(' lb_policy round_robin');
280
+ }
281
+
282
+ // Health check
283
+ lines.push(` health_uri ${health}`);
284
+ lines.push(` health_interval ${healthInterval}s`);
285
+ lines.push(` health_timeout ${healthTimeout}s`);
286
+
287
+ lines.push(' }');
288
+
289
+ // WebSocket support
290
+ if (hasWebSocket) {
291
+ lines.push('');
292
+ lines.push(' @websocket {');
293
+ lines.push(' header Connection *Upgrade*');
294
+ lines.push(' header Upgrade websocket');
295
+ lines.push(' }');
296
+ if (instances === 1) {
297
+ lines.push(' reverse_proxy @websocket localhost:3000');
298
+ } else {
299
+ const upstreams = [];
300
+ for (let i = 0; i < instances; i++) {
301
+ upstreams.push(`localhost:${3000 + i}`);
302
+ }
303
+ lines.push(` reverse_proxy @websocket ${upstreams.join(' ')}`);
304
+ }
305
+ }
306
+
307
+ // Logging
308
+ lines.push('');
309
+ lines.push(' log {');
310
+ lines.push(` output file /var/log/caddy/${appName}.log`);
311
+ lines.push(' }');
312
+
313
+ lines.push('}');
314
+ return lines.join('\n');
315
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Security Scorecard — post-compilation security posture summary.
3
+ */
4
+
5
+ export function generateSecurityScorecard(securityConfig, warnings, hasServer, hasEdge) {
6
+ if (!hasServer && !hasEdge) return null;
7
+
8
+ const items = [];
9
+ let score = 10;
10
+
11
+ const warningCodes = new Set((warnings || []).map(w => w.code).filter(Boolean));
12
+
13
+ if (!securityConfig) {
14
+ items.push({ pass: false, label: 'No security block configured', deduction: 3 });
15
+ score -= 3;
16
+ items.push({ pass: false, label: 'No auth configured', deduction: 2 });
17
+ score -= 2;
18
+ items.push({ pass: false, label: 'No CSRF protection', deduction: 1 });
19
+ score -= 1;
20
+ items.push({ pass: false, label: 'No rate limiting', deduction: 1 });
21
+ score -= 1;
22
+ items.push({ pass: false, label: 'No CSP configured', deduction: 1 });
23
+ score -= 1;
24
+ items.push({ pass: false, label: 'No audit logging', deduction: 1 });
25
+ score -= 1;
26
+ return { score: Math.max(0, score), items, format: () => formatScorecard(Math.max(0, score), items) };
27
+ }
28
+
29
+ // Auth
30
+ if (securityConfig.auth) {
31
+ const isCookie = securityConfig.auth.storage === 'cookie';
32
+ const authType = securityConfig.auth.authType || 'JWT';
33
+ if (isCookie) {
34
+ items.push({ pass: true, label: `${authType} auth with HttpOnly cookies` });
35
+ } else {
36
+ items.push({ pass: true, label: `${authType} auth configured` });
37
+ }
38
+ if (warningCodes.has('W_LOCALSTORAGE_TOKEN')) {
39
+ items.push({ pass: false, label: 'Auth tokens in localStorage (XSS vulnerable)', deduction: 1 });
40
+ score -= 1;
41
+ }
42
+ } else {
43
+ items.push({ pass: false, label: 'No auth configured', deduction: 2 });
44
+ score -= 2;
45
+ }
46
+
47
+ // CSRF
48
+ if (securityConfig.csrf && securityConfig.csrf.enabled === false) {
49
+ items.push({ pass: false, label: 'CSRF protection disabled', deduction: 1 });
50
+ score -= 1;
51
+ } else if (securityConfig.auth) {
52
+ items.push({ pass: true, label: 'CSRF enabled with session binding' });
53
+ } else {
54
+ items.push({ pass: false, label: 'No CSRF protection', deduction: 1 });
55
+ score -= 1;
56
+ }
57
+
58
+ // Rate limiting
59
+ if (securityConfig.rateLimit) {
60
+ items.push({ pass: true, label: 'Rate limiting configured' });
61
+ } else if (securityConfig.auth) {
62
+ items.push({ pass: false, label: 'No rate limiting (auth without brute-force protection)', deduction: 1 });
63
+ score -= 1;
64
+ }
65
+
66
+ // CSP
67
+ if (securityConfig.csp) {
68
+ items.push({ pass: true, label: 'Content Security Policy configured' });
69
+ } else {
70
+ items.push({ pass: false, label: 'No CSP configured', deduction: 1 });
71
+ score -= 1;
72
+ }
73
+
74
+ // CORS wildcard
75
+ if (warningCodes.has('W_CORS_WILDCARD')) {
76
+ items.push({ pass: false, label: 'CORS allows wildcard origins', deduction: 1 });
77
+ score -= 1;
78
+ } else if (securityConfig.cors) {
79
+ items.push({ pass: true, label: 'CORS restricted to specific origins' });
80
+ }
81
+
82
+ // Hardcoded secret
83
+ if (warningCodes.has('W_HARDCODED_SECRET')) {
84
+ items.push({ pass: false, label: 'Auth secret hardcoded in source', deduction: 1 });
85
+ score -= 1;
86
+ }
87
+
88
+ // Audit logging
89
+ if (securityConfig.audit) {
90
+ items.push({ pass: true, label: 'Audit logging configured' });
91
+ } else {
92
+ items.push({ pass: false, label: 'No audit logging', deduction: 1 });
93
+ score -= 1;
94
+ }
95
+
96
+ score = Math.max(0, score);
97
+ return { score, items, format: () => formatScorecard(score, items) };
98
+ }
99
+
100
+ function formatScorecard(score, items) {
101
+ const lines = [];
102
+ lines.push(`\x1b[1mSecurity: ${score}/10\x1b[0m`);
103
+ for (const item of items) {
104
+ if (item.pass) {
105
+ lines.push(` \x1b[32m[pass]\x1b[0m ${item.label}`);
106
+ } else {
107
+ lines.push(` \x1b[33m[warn]\x1b[0m ${item.label} (-${item.deduction})`);
108
+ }
109
+ }
110
+ return lines.join('\n');
111
+ }
@@ -899,15 +899,30 @@ export class Lexer {
899
899
  return;
900
900
  }
901
901
 
902
- // Special case: "style {" → read raw CSS block
902
+ // Special case: "style {" or "style(...) {" → read raw CSS block
903
903
  if (value === 'style') {
904
904
  const savedPos = this.pos;
905
905
  const savedLine = this.line;
906
906
  const savedCol = this.column;
907
- // Skip whitespace (including newlines) to check for {
907
+ // Skip whitespace (including newlines) to check for ( or {
908
908
  while (this.pos < this.length && (this.isWhitespace(this.peek()) || this.peek() === '\n')) {
909
909
  this.advance();
910
910
  }
911
+ // Check for style(...) config
912
+ let configPrefix = '';
913
+ if (this.peek() === '(') {
914
+ this.advance(); // skip (
915
+ let configStr = '';
916
+ while (this.pos < this.length && this.peek() !== ')') {
917
+ configStr += this.advance();
918
+ }
919
+ if (this.pos < this.length) this.advance(); // skip )
920
+ configPrefix = '__CONFIG:' + configStr.trim() + '__';
921
+ // Skip whitespace after )
922
+ while (this.pos < this.length && (this.isWhitespace(this.peek()) || this.peek() === '\n')) {
923
+ this.advance();
924
+ }
925
+ }
911
926
  if (this.peek() === '{') {
912
927
  this.advance(); // skip {
913
928
  let depth = 1;
@@ -924,7 +939,7 @@ export class Lexer {
924
939
  if (depth > 0) {
925
940
  this.error('Unterminated style block');
926
941
  }
927
- this.tokens.push(new Token(TokenType.STYLE_BLOCK, css.trim(), startLine, startCol));
942
+ this.tokens.push(new Token(TokenType.STYLE_BLOCK, configPrefix + css.trim(), startLine, startCol));
928
943
  return;
929
944
  }
930
945
  // Not a style block — restore position