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.
- package/bin/tova.js +192 -10
- package/package.json +2 -7
- package/src/analyzer/analyzer.js +134 -2
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/codegen/base-codegen.js +1159 -10
- package/src/codegen/codegen.js +20 -0
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/wasm-codegen.js +6 -0
- package/src/config/edit-toml.js +6 -2
- package/src/config/git-resolver.js +128 -0
- package/src/config/lock-file.js +57 -0
- package/src/config/module-cache.js +58 -0
- package/src/config/module-entry.js +37 -0
- package/src/config/module-path.js +31 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +17 -0
- package/src/config/resolver.js +139 -0
- package/src/config/search.js +28 -0
- package/src/config/semver.js +72 -0
- package/src/config/toml.js +48 -5
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +311 -0
- package/src/lsp/server.js +482 -0
- package/src/parser/ast.js +24 -0
- package/src/parser/concurrency-ast.js +15 -0
- package/src/parser/concurrency-parser.js +236 -0
- package/src/parser/deploy-ast.js +37 -0
- package/src/parser/deploy-parser.js +132 -0
- package/src/parser/parser.js +21 -3
- package/src/parser/select-ast.js +39 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/register-all.js +4 -0
- package/src/stdlib/inline.js +35 -3
- package/src/stdlib/runtime-bridge.js +152 -0
- 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
|
+
}
|
package/src/config/toml.js
CHANGED
|
@@ -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.
|
|
19
|
-
if (close
|
|
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
|
-
|
|
28
|
-
|
|
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
|
+
}
|