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,217 @@
1
+ // CLI deploy command entry point for the Tova language
2
+ // Provides argument parsing and deploy orchestration.
3
+ // SSH execution is stubbed for now — will be wired in integration.
4
+
5
+ import { inferInfrastructure } from './infer.js';
6
+
7
+ /**
8
+ * Parse CLI deploy arguments into a config object.
9
+ *
10
+ * tova deploy prod --plan
11
+ * tova deploy prod --rollback
12
+ * tova deploy prod --logs --since "1 hour ago"
13
+ * tova deploy prod --status
14
+ * tova deploy prod --ssh
15
+ * tova deploy prod --setup-git
16
+ * tova deploy --list --server root@example.com
17
+ * tova deploy prod --remove
18
+ * tova deploy prod --logs --instance 1
19
+ */
20
+ export function parseDeployArgs(args) {
21
+ const result = {
22
+ envName: null,
23
+ plan: false,
24
+ rollback: false,
25
+ logs: false,
26
+ status: false,
27
+ ssh: false,
28
+ setupGit: false,
29
+ remove: false,
30
+ list: false,
31
+ server: null,
32
+ since: null,
33
+ instance: null,
34
+ };
35
+
36
+ for (let i = 0; i < args.length; i++) {
37
+ const arg = args[i];
38
+ switch (arg) {
39
+ case '--plan': result.plan = true; break;
40
+ case '--rollback': result.rollback = true; break;
41
+ case '--logs': result.logs = true; break;
42
+ case '--status': result.status = true; break;
43
+ case '--ssh': result.ssh = true; break;
44
+ case '--setup-git': result.setupGit = true; break;
45
+ case '--remove': result.remove = true; break;
46
+ case '--list': result.list = true; break;
47
+ case '--server': result.server = args[++i]; break;
48
+ case '--since': result.since = args[++i]; break;
49
+ case '--instance': result.instance = parseInt(args[++i], 10); break;
50
+ default:
51
+ if (!arg.startsWith('--') && !result.envName) {
52
+ result.envName = arg;
53
+ }
54
+ break;
55
+ }
56
+ }
57
+ return result;
58
+ }
59
+
60
+ /**
61
+ * Print a deploy plan to the console.
62
+ * Shows the infrastructure that would be provisioned.
63
+ *
64
+ * @param {Object} infra - Infrastructure manifest from inferInfrastructure()
65
+ */
66
+ export function printPlan(infra) {
67
+ const lines = [];
68
+ lines.push('');
69
+ lines.push(' Deploy Plan');
70
+ lines.push(' ──────────────────────────────────────');
71
+ lines.push('');
72
+
73
+ if (infra.name) lines.push(` Environment: ${infra.name}`);
74
+ if (infra.server) lines.push(` Server: ${infra.server}`);
75
+ if (infra.domain) lines.push(` Domain: ${infra.domain}`);
76
+ lines.push(` Instances: ${infra.instances}`);
77
+ lines.push(` Memory: ${infra.memory}`);
78
+ lines.push(` Branch: ${infra.branch}`);
79
+ lines.push(` Health: ${infra.health} (every ${infra.health_interval}s)`);
80
+ lines.push(` Keep: ${infra.keep_releases} releases`);
81
+ lines.push('');
82
+
83
+ // Required services
84
+ const services = [];
85
+ if (infra.requires.bun) services.push('Bun');
86
+ if (infra.requires.caddy) services.push('Caddy');
87
+ if (infra.requires.ufw) services.push('UFW');
88
+ if (services.length > 0) {
89
+ lines.push(` Services: ${services.join(', ')}`);
90
+ }
91
+
92
+ // Databases
93
+ if (infra.databases.length > 0) {
94
+ const dbNames = infra.databases.map(d => d.engine);
95
+ lines.push(` Databases: ${dbNames.join(', ')}`);
96
+ }
97
+
98
+ // Features
99
+ const features = [];
100
+ if (infra.hasWebSocket) features.push('WebSocket');
101
+ if (infra.hasSSE) features.push('SSE');
102
+ if (infra.hasBrowser) features.push('Static assets');
103
+ if (features.length > 0) {
104
+ lines.push(` Features: ${features.join(', ')}`);
105
+ }
106
+
107
+ // Required secrets
108
+ if (infra.requiredSecrets.length > 0) {
109
+ lines.push(` Secrets: ${infra.requiredSecrets.join(', ')}`);
110
+ }
111
+
112
+ // Env variables
113
+ const envKeys = Object.keys(infra.env || {});
114
+ if (envKeys.length > 0) {
115
+ lines.push(` Env vars: ${envKeys.join(', ')}`);
116
+ }
117
+
118
+ lines.push('');
119
+ lines.push(' ──────────────────────────────────────');
120
+ lines.push('');
121
+
122
+ console.log(lines.join('\n'));
123
+ }
124
+
125
+ /**
126
+ * Main deploy orchestrator.
127
+ *
128
+ * Compiles the project, infers infrastructure, and (eventually) executes
129
+ * SSH deployment. For now the SSH parts are stubbed.
130
+ *
131
+ * @param {Object} ast - Parsed program AST
132
+ * @param {Object} buildResult - Codegen output
133
+ * @param {Object} deployArgs - Parsed CLI args from parseDeployArgs()
134
+ * @param {string} projectDir - Absolute path to the project directory
135
+ * @returns {Object} result with plan, infra, and status
136
+ */
137
+ export async function deploy(ast, buildResult, deployArgs, projectDir) {
138
+ // Infer full infrastructure manifest from AST
139
+ const infra = inferInfrastructure(ast);
140
+
141
+ // Override environment name from CLI args
142
+ if (deployArgs.envName) {
143
+ infra.name = deployArgs.envName;
144
+ }
145
+
146
+ // If deploy config exists in build result, merge it
147
+ if (buildResult.deploy && buildResult.deploy[deployArgs.envName]) {
148
+ const envConfig = buildResult.deploy[deployArgs.envName];
149
+ if (envConfig.server) infra.server = envConfig.server;
150
+ if (envConfig.domain) infra.domain = envConfig.domain;
151
+ if (envConfig.instances) infra.instances = envConfig.instances;
152
+ if (envConfig.memory) infra.memory = envConfig.memory;
153
+ if (envConfig.branch) infra.branch = envConfig.branch;
154
+ }
155
+
156
+ // Plan mode — just show what would be deployed
157
+ if (deployArgs.plan) {
158
+ printPlan(infra);
159
+ return { action: 'plan', infra };
160
+ }
161
+
162
+ // Rollback mode — stub
163
+ if (deployArgs.rollback) {
164
+ console.log(` Rolling back ${deployArgs.envName}...`);
165
+ // TODO: SSH into server, symlink previous release
166
+ return { action: 'rollback', infra };
167
+ }
168
+
169
+ // Logs mode — stub
170
+ if (deployArgs.logs) {
171
+ const since = deployArgs.since || '1 hour ago';
172
+ const instance = deployArgs.instance !== null ? ` (instance ${deployArgs.instance})` : '';
173
+ console.log(` Fetching logs for ${deployArgs.envName}${instance} since ${since}...`);
174
+ // TODO: SSH into server, journalctl/tail logs
175
+ return { action: 'logs', infra };
176
+ }
177
+
178
+ // Status mode — stub
179
+ if (deployArgs.status) {
180
+ console.log(` Checking status of ${deployArgs.envName}...`);
181
+ // TODO: SSH into server, check systemd service status
182
+ return { action: 'status', infra };
183
+ }
184
+
185
+ // SSH mode — stub
186
+ if (deployArgs.ssh) {
187
+ console.log(` Opening SSH session to ${deployArgs.envName}...`);
188
+ // TODO: spawn interactive SSH session
189
+ return { action: 'ssh', infra };
190
+ }
191
+
192
+ // Setup git push-to-deploy — stub
193
+ if (deployArgs.setupGit) {
194
+ console.log(` Setting up git push-to-deploy for ${deployArgs.envName}...`);
195
+ // TODO: SSH into server, configure bare repo + post-receive hook
196
+ return { action: 'setup-git', infra };
197
+ }
198
+
199
+ // Remove deployment — stub
200
+ if (deployArgs.remove) {
201
+ console.log(` Removing deployment ${deployArgs.envName}...`);
202
+ // TODO: SSH into server, stop services, remove files
203
+ return { action: 'remove', infra };
204
+ }
205
+
206
+ // List deployments — stub
207
+ if (deployArgs.list) {
208
+ console.log(' Listing deployments...');
209
+ // TODO: SSH into server, list ~/apps/
210
+ return { action: 'list', infra };
211
+ }
212
+
213
+ // Default: full deploy — stub
214
+ console.log(` Deploying to ${deployArgs.envName}...`);
215
+ // TODO: rsync build, run provision script, restart services
216
+ return { action: 'deploy', infra };
217
+ }
@@ -0,0 +1,218 @@
1
+ // Infrastructure Inference Engine for the Tova language
2
+ // Walks the entire program AST and produces a complete infrastructure manifest
3
+ // by combining explicit deploy block config with inferred requirements from
4
+ // server, browser, and security blocks.
5
+
6
+ import { DeployCodegen } from '../codegen/deploy-codegen.js';
7
+
8
+ const MANIFEST_DEFAULTS = {
9
+ name: null,
10
+ server: null,
11
+ domain: null,
12
+ instances: 1,
13
+ memory: '512mb',
14
+ branch: 'main',
15
+ health: '/healthz',
16
+ health_interval: 30,
17
+ health_timeout: 5,
18
+ restart_on_failure: true,
19
+ keep_releases: 5,
20
+ env: {},
21
+ databases: [],
22
+ requires: { bun: false, caddy: false, ufw: false },
23
+ hasWebSocket: false,
24
+ hasSSE: false,
25
+ hasBrowser: false,
26
+ requiredSecrets: [],
27
+ blockTypes: [],
28
+ };
29
+
30
+ /**
31
+ * Walk an AST node tree recursively and invoke a visitor callback on each node.
32
+ */
33
+ function walkNode(node, visitor) {
34
+ if (!node || typeof node !== 'object') return;
35
+ if (node.type) visitor(node);
36
+
37
+ // Walk arrays (e.g., body, entries, arguments)
38
+ for (const key of Object.keys(node)) {
39
+ const val = node[key];
40
+ if (Array.isArray(val)) {
41
+ for (const item of val) {
42
+ walkNode(item, visitor);
43
+ }
44
+ } else if (val && typeof val === 'object' && val.type) {
45
+ walkNode(val, visitor);
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Collect all env() call arguments from a node tree.
52
+ * env("JWT_SECRET") => CallExpression with callee.name === "env" and args[0].value
53
+ */
54
+ function collectEnvCalls(node) {
55
+ const secrets = [];
56
+ walkNode(node, (n) => {
57
+ if (
58
+ n.type === 'CallExpression' &&
59
+ n.callee &&
60
+ n.callee.type === 'Identifier' &&
61
+ n.callee.name === 'env' &&
62
+ n.arguments &&
63
+ n.arguments.length > 0
64
+ ) {
65
+ const arg = n.arguments[0];
66
+ if (arg && (arg.value !== undefined)) {
67
+ secrets.push(typeof arg.value === 'string' ? arg.value : String(arg.value));
68
+ }
69
+ }
70
+ });
71
+ return secrets;
72
+ }
73
+
74
+ /**
75
+ * Infer infrastructure requirements from the full program AST.
76
+ *
77
+ * @param {Object} ast - Program AST with ast.body array of top-level blocks
78
+ * @returns {Object} Complete infrastructure manifest
79
+ */
80
+ export function inferInfrastructure(ast) {
81
+ const manifest = JSON.parse(JSON.stringify(MANIFEST_DEFAULTS));
82
+ const blockTypes = new Set();
83
+ const deployBlocks = [];
84
+ const inferredDatabases = [];
85
+ const secretsSet = new Set();
86
+
87
+ if (!ast || !ast.body) return manifest;
88
+
89
+ for (const node of ast.body) {
90
+ switch (node.type) {
91
+ case 'DeployBlock': {
92
+ blockTypes.add('deploy');
93
+ deployBlocks.push(node);
94
+ break;
95
+ }
96
+
97
+ case 'ServerBlock': {
98
+ blockTypes.add('server');
99
+ manifest.requires.bun = true;
100
+ manifest.requires.caddy = true;
101
+ manifest.requires.ufw = true;
102
+
103
+ // Scan server block body for specific declarations
104
+ if (node.body && Array.isArray(node.body)) {
105
+ for (const stmt of node.body) {
106
+ if (stmt.type === 'DbDeclaration') {
107
+ // Server-block db is always SQLite (bun:sqlite)
108
+ const dbConfig = {};
109
+ if (stmt.config && typeof stmt.config === 'object') {
110
+ for (const [k, v] of Object.entries(stmt.config)) {
111
+ dbConfig[k] = v && v.value !== undefined ? v.value : v;
112
+ }
113
+ }
114
+ inferredDatabases.push({ engine: 'sqlite', config: dbConfig });
115
+ }
116
+ if (stmt.type === 'WebSocketDeclaration') {
117
+ manifest.hasWebSocket = true;
118
+ }
119
+ if (stmt.type === 'SseDeclaration') {
120
+ manifest.hasSSE = true;
121
+ }
122
+ // Also check inside route groups
123
+ if (stmt.type === 'RouteGroupDeclaration' && stmt.body) {
124
+ for (const inner of stmt.body) {
125
+ if (inner.type === 'WebSocketDeclaration') {
126
+ manifest.hasWebSocket = true;
127
+ }
128
+ if (inner.type === 'SseDeclaration') {
129
+ manifest.hasSSE = true;
130
+ }
131
+ if (inner.type === 'DbDeclaration') {
132
+ const dbConfig = {};
133
+ if (inner.config && typeof inner.config === 'object') {
134
+ for (const [k, v] of Object.entries(inner.config)) {
135
+ dbConfig[k] = v && v.value !== undefined ? v.value : v;
136
+ }
137
+ }
138
+ inferredDatabases.push({ engine: 'sqlite', config: dbConfig });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ break;
145
+ }
146
+
147
+ case 'BrowserBlock': {
148
+ blockTypes.add('browser');
149
+ manifest.hasBrowser = true;
150
+ break;
151
+ }
152
+
153
+ case 'SecurityBlock': {
154
+ blockTypes.add('security');
155
+ // Scan for env() calls to find required secrets
156
+ if (node.body && Array.isArray(node.body)) {
157
+ for (const stmt of node.body) {
158
+ if (stmt.type === 'SecurityAuthDeclaration') {
159
+ // Walk the config looking for env() calls
160
+ if (stmt.config && typeof stmt.config === 'object') {
161
+ for (const [, value] of Object.entries(stmt.config)) {
162
+ const secrets = collectEnvCalls(value);
163
+ for (const s of secrets) secretsSet.add(s);
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ break;
170
+ }
171
+ }
172
+ }
173
+
174
+ // Merge explicit deploy config via DeployCodegen
175
+ if (deployBlocks.length > 0) {
176
+ const deployConfig = DeployCodegen.mergeDeployBlocks(deployBlocks);
177
+ // Apply deploy config fields to manifest
178
+ if (deployConfig.name) manifest.name = deployConfig.name;
179
+ if (deployConfig.server) manifest.server = deployConfig.server;
180
+ if (deployConfig.domain) manifest.domain = deployConfig.domain;
181
+ if (deployConfig.instances !== undefined) manifest.instances = deployConfig.instances;
182
+ if (deployConfig.memory) manifest.memory = deployConfig.memory;
183
+ if (deployConfig.branch) manifest.branch = deployConfig.branch;
184
+ if (deployConfig.health) manifest.health = deployConfig.health;
185
+ if (deployConfig.health_interval !== undefined) manifest.health_interval = deployConfig.health_interval;
186
+ if (deployConfig.health_timeout !== undefined) manifest.health_timeout = deployConfig.health_timeout;
187
+ if (deployConfig.restart_on_failure !== undefined) manifest.restart_on_failure = deployConfig.restart_on_failure;
188
+ if (deployConfig.keep_releases !== undefined) manifest.keep_releases = deployConfig.keep_releases;
189
+ if (deployConfig.env && Object.keys(deployConfig.env).length > 0) {
190
+ manifest.env = { ...manifest.env, ...deployConfig.env };
191
+ }
192
+ // Declared databases from deploy block
193
+ if (deployConfig.databases && deployConfig.databases.length > 0) {
194
+ manifest.databases = [...deployConfig.databases];
195
+ }
196
+ }
197
+
198
+ // Merge inferred databases with declared ones (avoid duplicates by engine name)
199
+ const declaredEngines = new Set(manifest.databases.map(d => d.engine));
200
+ for (const inferred of inferredDatabases) {
201
+ if (!declaredEngines.has(inferred.engine)) {
202
+ manifest.databases.push(inferred);
203
+ declaredEngines.add(inferred.engine);
204
+ }
205
+ }
206
+
207
+ // If server block is present, ensure bun/caddy/ufw are required
208
+ if (blockTypes.has('server')) {
209
+ manifest.requires.bun = true;
210
+ manifest.requires.caddy = true;
211
+ manifest.requires.ufw = true;
212
+ }
213
+
214
+ manifest.requiredSecrets = [...secretsSet].sort();
215
+ manifest.blockTypes = [...blockTypes].sort();
216
+
217
+ return manifest;
218
+ }