tova 0.7.0 → 0.8.2

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 (38) hide show
  1. package/bin/tova.js +192 -10
  2. package/package.json +2 -7
  3. package/src/analyzer/analyzer.js +134 -2
  4. package/src/analyzer/deploy-analyzer.js +44 -0
  5. package/src/codegen/base-codegen.js +1159 -10
  6. package/src/codegen/codegen.js +20 -0
  7. package/src/codegen/deploy-codegen.js +49 -0
  8. package/src/codegen/shared-codegen.js +5 -0
  9. package/src/codegen/wasm-codegen.js +6 -0
  10. package/src/config/edit-toml.js +6 -2
  11. package/src/config/git-resolver.js +128 -0
  12. package/src/config/lock-file.js +57 -0
  13. package/src/config/module-cache.js +58 -0
  14. package/src/config/module-entry.js +37 -0
  15. package/src/config/module-path.js +31 -0
  16. package/src/config/pkg-errors.js +62 -0
  17. package/src/config/resolve.js +17 -0
  18. package/src/config/resolver.js +139 -0
  19. package/src/config/search.js +28 -0
  20. package/src/config/semver.js +72 -0
  21. package/src/config/toml.js +48 -5
  22. package/src/deploy/deploy.js +217 -0
  23. package/src/deploy/infer.js +218 -0
  24. package/src/deploy/provision.js +311 -0
  25. package/src/lsp/server.js +482 -0
  26. package/src/parser/ast.js +24 -0
  27. package/src/parser/concurrency-ast.js +15 -0
  28. package/src/parser/concurrency-parser.js +236 -0
  29. package/src/parser/deploy-ast.js +37 -0
  30. package/src/parser/deploy-parser.js +132 -0
  31. package/src/parser/parser.js +21 -3
  32. package/src/parser/select-ast.js +39 -0
  33. package/src/registry/plugins/concurrency-plugin.js +32 -0
  34. package/src/registry/plugins/deploy-plugin.js +33 -0
  35. package/src/registry/register-all.js +4 -0
  36. package/src/stdlib/inline.js +35 -3
  37. package/src/stdlib/runtime-bridge.js +152 -0
  38. package/src/version.js +1 -1
@@ -0,0 +1,139 @@
1
+ // src/config/resolver.js
2
+ import { selectMinVersion, satisfies, parseSemver, compareSemver } from './semver.js';
3
+
4
+ /**
5
+ * Merges dependency maps from multiple sources, collecting all constraints per module.
6
+ */
7
+ export function mergeDependencies(...depMaps) {
8
+ const merged = {};
9
+ for (const deps of depMaps) {
10
+ for (const [mod, constraint] of Object.entries(deps)) {
11
+ if (!merged[mod]) merged[mod] = [];
12
+ if (Array.isArray(constraint)) {
13
+ merged[mod].push(...constraint);
14
+ } else {
15
+ merged[mod].push(constraint);
16
+ }
17
+ }
18
+ }
19
+ return merged;
20
+ }
21
+
22
+ /**
23
+ * Merges npm dependencies from multiple module configs.
24
+ * For conflicts, picks the highest constraint version.
25
+ */
26
+ export function mergeNpmDeps(moduleConfigs) {
27
+ const merged = {};
28
+ for (const config of moduleConfigs) {
29
+ const prod = config.npm?.prod || {};
30
+ for (const [name, version] of Object.entries(prod)) {
31
+ if (!merged[name]) {
32
+ merged[name] = version;
33
+ } else {
34
+ // Keep whichever specifies a higher minimum
35
+ try {
36
+ const existing = parseSemver(merged[name].replace(/^[\^~>=<]*/, ''));
37
+ const incoming = parseSemver(version.replace(/^[\^~>=<]*/, ''));
38
+ if (compareSemver(incoming, existing) > 0) {
39
+ merged[name] = version;
40
+ }
41
+ } catch {
42
+ merged[name] = version;
43
+ }
44
+ }
45
+ }
46
+ }
47
+ return merged;
48
+ }
49
+
50
+ /**
51
+ * Detects version conflicts — modules where no single version satisfies all constraints.
52
+ */
53
+ export function detectConflicts(constraintMap, availableVersions) {
54
+ const conflicts = [];
55
+ for (const [mod, constraints] of Object.entries(constraintMap)) {
56
+ const versions = availableVersions[mod] || [];
57
+ const resolved = selectMinVersion(versions, constraints);
58
+ if (resolved === null && constraints.length > 1) {
59
+ conflicts.push({
60
+ module: mod,
61
+ constraints,
62
+ available: versions,
63
+ });
64
+ }
65
+ }
66
+ return conflicts;
67
+ }
68
+
69
+ /**
70
+ * Resolves all dependencies to exact versions.
71
+ * Returns a map of modulePath -> { version, sha, source, npmDeps }.
72
+ *
73
+ * This is the high-level orchestrator called by `tova install`.
74
+ * It takes callbacks for I/O operations (fetching tags, reading configs)
75
+ * so the core logic is testable without network access.
76
+ */
77
+ export async function resolveDependencies(rootDeps, options = {}) {
78
+ const {
79
+ getAvailableVersions, // async (modulePath) => ['1.0.0', '1.1.0', ...]
80
+ getModuleConfig, // async (modulePath, version) => { dependencies, npm }
81
+ getVersionSha, // async (modulePath, version) => 'sha...'
82
+ } = options;
83
+
84
+ const resolved = {}; // modulePath -> { version, sha }
85
+ const allConstraints = {}; // modulePath -> [constraints...]
86
+ const allNpmDeps = []; // [{ npm: { prod: {...} } }, ...]
87
+ const queue = [rootDeps]; // queue of dependency maps to process
88
+
89
+ while (queue.length > 0) {
90
+ const deps = queue.shift();
91
+ for (const [mod, constraint] of Object.entries(deps)) {
92
+ if (!allConstraints[mod]) allConstraints[mod] = [];
93
+ allConstraints[mod].push(constraint);
94
+
95
+ // Get available versions
96
+ const versions = await getAvailableVersions(mod);
97
+ const version = selectMinVersion(versions, allConstraints[mod]);
98
+
99
+ if (version === null) {
100
+ const conflicts = detectConflicts(
101
+ { [mod]: allConstraints[mod] },
102
+ { [mod]: versions }
103
+ );
104
+ if (conflicts.length > 0) {
105
+ throw new Error(
106
+ `Version conflict for ${mod}:\n` +
107
+ conflicts[0].constraints.map(c => ` requires ${c}`).join('\n') +
108
+ `\n Available: ${versions.join(', ')}`
109
+ );
110
+ }
111
+ throw new Error(
112
+ `No version of ${mod} satisfies constraint: ${allConstraints[mod].join(', ')}\n` +
113
+ ` Available: ${versions.join(', ') || 'none'}`
114
+ );
115
+ }
116
+
117
+ // Skip if we already resolved this module to the same or higher version
118
+ if (resolved[mod] && compareSemver(resolved[mod].version, version) >= 0) {
119
+ continue;
120
+ }
121
+
122
+ const sha = await getVersionSha(mod, version);
123
+ resolved[mod] = { version, sha, source: `https://${mod}.git` };
124
+
125
+ // Read this module's config for transitive deps
126
+ const config = await getModuleConfig(mod, version);
127
+ if (config) {
128
+ allNpmDeps.push(config);
129
+ if (config.dependencies && Object.keys(config.dependencies).length > 0) {
130
+ queue.push(config.dependencies);
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ const npmDeps = mergeNpmDeps(allNpmDeps);
137
+
138
+ return { resolved, npmDeps };
139
+ }
@@ -0,0 +1,28 @@
1
+ // src/config/search.js
2
+
3
+ export function formatSearchResults(items) {
4
+ if (items.length === 0) return ' No packages found.\n';
5
+ const lines = [];
6
+ for (const item of items) {
7
+ const modulePath = `github.com/${item.full_name}`;
8
+ const stars = item.stargazers_count || 0;
9
+ const desc = item.description || '(no description)';
10
+ const updated = item.updated_at ? item.updated_at.slice(0, 10) : 'unknown';
11
+ lines.push(` ${modulePath}`);
12
+ lines.push(` ${desc}`);
13
+ lines.push(` Stars: ${stars} Updated: ${updated}`);
14
+ lines.push('');
15
+ }
16
+ return lines.join('\n');
17
+ }
18
+
19
+ export async function searchPackages(query) {
20
+ const searchQuery = encodeURIComponent(`${query} topic:tova-package`);
21
+ const url = `https://api.github.com/search/repositories?q=${searchQuery}&sort=stars&per_page=20`;
22
+ const res = await fetch(url, {
23
+ headers: { 'Accept': 'application/vnd.github.v3+json' },
24
+ });
25
+ if (!res.ok) throw new Error(`GitHub search failed: ${res.statusText}`);
26
+ const data = await res.json();
27
+ return data.items || [];
28
+ }
@@ -0,0 +1,72 @@
1
+ // src/config/semver.js
2
+ // Semver utilities for the Tova package manager.
3
+ // Provides parsing, comparison, constraint satisfaction, and minimum version selection.
4
+
5
+ export function parseSemver(str) {
6
+ const s = str.startsWith('v') ? str.slice(1) : str;
7
+ const parts = s.split('.');
8
+ const major = parseInt(parts[0], 10);
9
+ const minor = parseInt(parts[1] || '0', 10);
10
+ const patch = parseInt(parts[2] || '0', 10);
11
+ if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
12
+ throw new Error(`Invalid semver: "${str}"`);
13
+ }
14
+ return { major, minor, patch };
15
+ }
16
+
17
+ export function compareSemver(a, b) {
18
+ const va = typeof a === 'string' ? parseSemver(a) : a;
19
+ const vb = typeof b === 'string' ? parseSemver(b) : b;
20
+ if (va.major !== vb.major) return va.major > vb.major ? 1 : -1;
21
+ if (va.minor !== vb.minor) return va.minor > vb.minor ? 1 : -1;
22
+ if (va.patch !== vb.patch) return va.patch > vb.patch ? 1 : -1;
23
+ return 0;
24
+ }
25
+
26
+ export function parseConstraint(constraint) {
27
+ if (constraint.startsWith('^')) {
28
+ return { type: 'caret', version: parseSemver(constraint.slice(1)) };
29
+ }
30
+ if (constraint.startsWith('~')) {
31
+ return { type: 'tilde', version: parseSemver(constraint.slice(1)) };
32
+ }
33
+ if (constraint.startsWith('>=')) {
34
+ return { type: 'gte', version: parseSemver(constraint.slice(2)) };
35
+ }
36
+ if (constraint.startsWith('>')) {
37
+ return { type: 'gt', version: parseSemver(constraint.slice(1)) };
38
+ }
39
+ return { type: 'exact', version: parseSemver(constraint) };
40
+ }
41
+
42
+ export function satisfies(version, constraint) {
43
+ const v = typeof version === 'string' ? parseSemver(version) : version;
44
+ const c = typeof constraint === 'string' ? parseConstraint(constraint) : constraint;
45
+ switch (c.type) {
46
+ case 'exact':
47
+ return compareSemver(v, c.version) === 0;
48
+ case 'caret':
49
+ if (compareSemver(v, c.version) < 0) return false;
50
+ return v.major === c.version.major;
51
+ case 'tilde':
52
+ if (compareSemver(v, c.version) < 0) return false;
53
+ return v.major === c.version.major && v.minor === c.version.minor;
54
+ case 'gte':
55
+ return compareSemver(v, c.version) >= 0;
56
+ case 'gt':
57
+ return compareSemver(v, c.version) > 0;
58
+ default:
59
+ return false;
60
+ }
61
+ }
62
+
63
+ export function selectMinVersion(versions, constraints) {
64
+ const constraintList = Array.isArray(constraints) ? constraints : [constraints];
65
+ const sorted = [...versions].sort((a, b) => compareSemver(a, b));
66
+ for (const ver of sorted) {
67
+ if (constraintList.every(c => satisfies(ver, c))) {
68
+ return ver;
69
+ }
70
+ }
71
+ return null;
72
+ }
@@ -13,10 +13,10 @@ export function parseTOML(input) {
13
13
  // Skip empty lines and comments
14
14
  if (line === '' || line.startsWith('#')) continue;
15
15
 
16
- // Section header: [section] or [section.subsection]
16
+ // Section header: [section] or [section.subsection] or ["quoted.key"]
17
17
  if (line.startsWith('[') && !line.startsWith('[[')) {
18
- const close = line.indexOf(']');
19
- if (close === -1) {
18
+ const close = line.lastIndexOf(']');
19
+ if (close <= 0) {
20
20
  throw new Error(`TOML parse error on line ${i + 1}: unclosed section header`);
21
21
  }
22
22
  const sectionPath = line.slice(1, close).trim();
@@ -24,8 +24,8 @@ export function parseTOML(input) {
24
24
  throw new Error(`TOML parse error on line ${i + 1}: empty section name`);
25
25
  }
26
26
  current = result;
27
- for (const part of sectionPath.split('.')) {
28
- const key = part.trim();
27
+ const parts = parseSectionPath(sectionPath);
28
+ for (const key of parts) {
29
29
  if (!current[key]) current[key] = {};
30
30
  current = current[key];
31
31
  }
@@ -45,6 +45,49 @@ export function parseTOML(input) {
45
45
  return result;
46
46
  }
47
47
 
48
+ function parseSectionPath(path) {
49
+ const parts = [];
50
+ let i = 0;
51
+ while (i < path.length) {
52
+ // Skip whitespace
53
+ while (i < path.length && path[i] === ' ') i++;
54
+ if (i >= path.length) break;
55
+
56
+ if (path[i] === '"') {
57
+ // Quoted key: read until closing quote
58
+ i++; // skip opening quote
59
+ let key = '';
60
+ while (i < path.length && path[i] !== '"') {
61
+ if (path[i] === '\\') {
62
+ i++;
63
+ key += path[i] || '';
64
+ } else {
65
+ key += path[i];
66
+ }
67
+ i++;
68
+ }
69
+ i++; // skip closing quote
70
+ parts.push(key);
71
+ } else {
72
+ // Bare key: read until dot or end
73
+ let key = '';
74
+ while (i < path.length && path[i] !== '.') {
75
+ key += path[i];
76
+ i++;
77
+ }
78
+ key = key.trim();
79
+ if (key) parts.push(key);
80
+ }
81
+
82
+ // Skip dot separator
83
+ while (i < path.length && (path[i] === ' ' || path[i] === '.')) {
84
+ if (path[i] === '.') { i++; break; }
85
+ i++;
86
+ }
87
+ }
88
+ return parts;
89
+ }
90
+
48
91
  function parseValue(raw, lineNum) {
49
92
  if (raw === '') {
50
93
  throw new Error(`TOML parse error on line ${lineNum}: missing value`);
@@ -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
+ }