@aifabrix/builder 2.40.2 → 2.42.0
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/.cursor/rules/docs-rules.mdc +30 -0
- package/README.md +7 -5
- package/integration/hubspot/README.md +8 -4
- package/integration/hubspot/application.json +54 -0
- package/integration/hubspot/create-hubspot.js +9 -136
- package/integration/hubspot/env.template +3 -4
- package/integration/hubspot/hubspot-datasource-company.json +343 -5
- package/integration/hubspot/hubspot-datasource-contact.json +413 -5
- package/integration/hubspot/hubspot-datasource-deal.json +341 -4
- package/integration/hubspot/hubspot-datasource-users.json +116 -0
- package/integration/hubspot/hubspot-deploy.json +1250 -108
- package/integration/hubspot/hubspot-system.json +15 -32
- package/integration/hubspot/test-dataplane-down-tests.js +17 -16
- package/integration/hubspot/test-dataplane-down.js +2 -2
- package/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +2 -1
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/external-test.api.js +111 -0
- package/lib/api/index.js +42 -19
- package/lib/api/pipeline.api.js +66 -120
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/api/types/pipeline.types.js +37 -0
- package/lib/api/wizard-platform.api.js +61 -0
- package/lib/api/wizard.api.js +34 -1
- package/lib/app/config.js +44 -11
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +12 -1
- package/lib/app/prompts.js +44 -29
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +9 -6
- package/lib/app/run-env-compose.js +264 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show-display.js +1 -1
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +172 -15
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +206 -16
- package/lib/cli/setup-environment.js +16 -6
- package/lib/cli/setup-external-system.js +89 -24
- package/lib/cli/setup-infra.js +82 -15
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +129 -24
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/credential-env.js +162 -0
- package/lib/commands/credential-list.js +17 -22
- package/lib/commands/credential-push.js +96 -0
- package/lib/commands/datasource.js +77 -6
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +347 -0
- package/lib/commands/repair-auth-config.js +99 -0
- package/lib/commands/repair-datasource-keys.js +208 -0
- package/lib/commands/repair-datasource.js +235 -0
- package/lib/commands/repair-env-template.js +348 -0
- package/lib/commands/repair-internal.js +85 -0
- package/lib/commands/repair-rbac.js +158 -0
- package/lib/commands/repair.js +507 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +96 -40
- package/lib/commands/wizard-core-helpers.js +226 -4
- package/lib/commands/wizard-core.js +67 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +44 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +86 -64
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/config.js +7 -1
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +176 -89
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/deployer.js +7 -5
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +188 -203
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test-execution.js +2 -1
- package/lib/external-system/test-system-level.js +73 -0
- package/lib/external-system/test.js +56 -19
- package/lib/generator/external-controller-manifest.js +29 -2
- package/lib/generator/external-schema-utils.js +1 -1
- package/lib/generator/external.js +10 -3
- package/lib/generator/index.js +177 -25
- package/lib/generator/split-readme.js +1 -0
- package/lib/generator/split-variables.js +7 -1
- package/lib/generator/split.js +194 -54
- package/lib/generator/wizard-prompts-secondary.js +294 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +88 -0
- package/lib/generator/wizard.js +155 -158
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +98 -12
- package/lib/infrastructure/services.js +88 -22
- package/lib/schema/application-schema.json +32 -8
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +509 -411
- package/lib/schema/wizard-config.schema.json +16 -0
- package/lib/utils/api.js +41 -13
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +77 -76
- package/lib/utils/compose-handlebars-helpers.js +54 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +357 -0
- package/lib/utils/dataplane-pipeline-warning.js +28 -0
- package/lib/utils/deployment-validation-helpers.js +4 -4
- package/lib/utils/dev-ca-install.js +139 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +103 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -2
- package/lib/utils/error-formatters/permission-errors.js +0 -1
- package/lib/utils/error-formatters/validation-errors.js +0 -1
- package/lib/utils/external-readme.js +56 -29
- package/lib/utils/external-system-display.js +59 -1
- package/lib/utils/external-system-test-helpers.js +21 -8
- package/lib/utils/external-system-validators.js +3 -0
- package/lib/utils/file-upload.js +20 -50
- package/lib/utils/help-builder.js +16 -2
- package/lib/utils/infra-status.js +80 -45
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +128 -37
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +114 -6
- package/lib/utils/secrets-helpers.js +108 -114
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/env-template-auth.js +157 -0
- package/lib/validation/env-template-kv.js +41 -0
- package/lib/validation/external-manifest-validator.js +25 -0
- package/lib/validation/external-system-auth-rules.js +86 -0
- package/lib/validation/validate-batch.js +149 -0
- package/lib/validation/validate-datasource-keys-api.js +33 -0
- package/lib/validation/validate-display.js +94 -16
- package/lib/validation/validate.js +25 -12
- package/lib/validation/validator.js +72 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +8 -3
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +6 -5
- package/templates/applications/dataplane/env.template +15 -10
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +12 -10
- package/templates/external-system/README.md.hbs +65 -25
- package/templates/external-system/deploy.js.hbs +4 -2
- package/templates/external-system/external-datasource.yaml.hbs +217 -0
- package/templates/external-system/external-system.json.hbs +1 -18
- package/templates/infra/compose.yaml.hbs +6 -0
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
- package/integration/hubspot/application.yaml +0 -37
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutagen binary auto-install: download from GitHub releases into ~/.aifabrix/bin/.
|
|
3
|
+
* Per remote-docker.md: CLI installs Mutagen when missing; never rely on system PATH.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Download and install Mutagen to AI Fabrix bin directory
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const https = require('https');
|
|
13
|
+
const { getAifabrixHome } = require('./paths');
|
|
14
|
+
const { exec } = require('child_process');
|
|
15
|
+
const { promisify } = require('util');
|
|
16
|
+
|
|
17
|
+
const execAsync = promisify(exec);
|
|
18
|
+
const fsPromises = fs.promises;
|
|
19
|
+
|
|
20
|
+
const MUTAGEN_RELEASE_API = 'https://api.github.com/repos/mutagen-io/mutagen/releases/latest';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Platform/arch to Mutagen asset basename (e.g. mutagen_linux_amd64).
|
|
24
|
+
* @returns {string|null} Basename without version or extension, or null if unsupported
|
|
25
|
+
*/
|
|
26
|
+
function getPlatformAssetBasename() {
|
|
27
|
+
const platform = process.platform;
|
|
28
|
+
const arch = process.arch;
|
|
29
|
+
if (platform === 'win32') {
|
|
30
|
+
return arch === 'x64' ? 'mutagen_windows_amd64' : arch === 'arm64' ? 'mutagen_windows_arm64' : null;
|
|
31
|
+
}
|
|
32
|
+
if (platform === 'darwin') {
|
|
33
|
+
return arch === 'x64' ? 'mutagen_darwin_amd64' : arch === 'arm64' ? 'mutagen_darwin_arm64' : null;
|
|
34
|
+
}
|
|
35
|
+
if (platform === 'linux') {
|
|
36
|
+
if (arch === 'x64') return 'mutagen_linux_amd64';
|
|
37
|
+
if (arch === 'arm64') return 'mutagen_linux_arm64';
|
|
38
|
+
if (arch === 'arm') return 'mutagen_linux_arm';
|
|
39
|
+
if (arch === 'ia32') return 'mutagen_linux_386';
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetch latest release info from GitHub API.
|
|
46
|
+
* @returns {Promise<{ tagName: string, assets: Array<{ name: string, browser_download_url: string }> }>}
|
|
47
|
+
* @throws {Error} If request fails or response is invalid
|
|
48
|
+
*/
|
|
49
|
+
function fetchLatestRelease() {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const req = https.get(MUTAGEN_RELEASE_API, {
|
|
52
|
+
headers: { 'User-Agent': 'aifabrix-builder-cli' }
|
|
53
|
+
}, (res) => {
|
|
54
|
+
if (res.statusCode !== 200) {
|
|
55
|
+
reject(new Error(`GitHub API returned ${res.statusCode}`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let body = '';
|
|
59
|
+
res.on('data', chunk => {
|
|
60
|
+
body += chunk;
|
|
61
|
+
});
|
|
62
|
+
res.on('end', () => {
|
|
63
|
+
try {
|
|
64
|
+
const data = JSON.parse(body);
|
|
65
|
+
const tagName = data.tag_name;
|
|
66
|
+
const assets = (data.assets || []).map(a => ({ name: a.name, browser_download_url: a.browser_download_url }));
|
|
67
|
+
if (!tagName || !Array.isArray(assets)) {
|
|
68
|
+
reject(new Error('Invalid GitHub release response'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
resolve({ tagName, assets });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
reject(e);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
req.on('error', reject);
|
|
78
|
+
req.setTimeout(15000, () => {
|
|
79
|
+
req.destroy(); reject(new Error('Request timeout'));
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Download URL to a file.
|
|
86
|
+
* @param {string} url - Download URL
|
|
87
|
+
* @param {string} destPath - Full path to write file
|
|
88
|
+
* @param {(msg: string) => void} [log] - Optional progress logger
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
function downloadToFile(url, destPath, log) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const file = fs.createWriteStream(destPath, { flags: 'w' });
|
|
94
|
+
const req = https.get(url, { headers: { 'User-Agent': 'aifabrix-builder-cli' } }, (res) => {
|
|
95
|
+
if (res.statusCode !== 200) {
|
|
96
|
+
file.close();
|
|
97
|
+
fs.unlink(destPath, () => {});
|
|
98
|
+
reject(new Error(`Download returned ${res.statusCode}`));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
res.pipe(file);
|
|
102
|
+
file.on('finish', () => {
|
|
103
|
+
file.close(() => resolve());
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
req.on('error', (err) => {
|
|
107
|
+
file.close();
|
|
108
|
+
fs.unlink(destPath, () => {});
|
|
109
|
+
reject(err);
|
|
110
|
+
});
|
|
111
|
+
req.setTimeout(120000, () => {
|
|
112
|
+
req.destroy(); reject(new Error('Download timeout'));
|
|
113
|
+
});
|
|
114
|
+
if (typeof log === 'function') log('Downloading Mutagen...');
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract .tar.gz using system tar; find binary and copy to destPath.
|
|
120
|
+
* Mutagen tarballs may have binary at root or inside a single top-level directory.
|
|
121
|
+
* @param {string} archivePath - Path to .tar.gz
|
|
122
|
+
* @param {string} destPath - Final binary path
|
|
123
|
+
* @param {string} binaryName - mutagen or mutagen.exe
|
|
124
|
+
*/
|
|
125
|
+
async function extractAndInstall(archivePath, destPath, binaryName) {
|
|
126
|
+
const tmpDir = path.join(path.dirname(archivePath), `mutagen-extract-${Date.now()}`);
|
|
127
|
+
await fsPromises.mkdir(tmpDir, { recursive: true });
|
|
128
|
+
try {
|
|
129
|
+
await execAsync(`tar -xzf "${archivePath}" -C "${tmpDir}"`, { timeout: 60000 });
|
|
130
|
+
let sourcePath = path.join(tmpDir, binaryName);
|
|
131
|
+
if (!fs.existsSync(sourcePath)) {
|
|
132
|
+
const entries = await fsPromises.readdir(tmpDir, { withFileTypes: true });
|
|
133
|
+
const sub = entries.length === 1 && entries[0].isDirectory() ? path.join(tmpDir, entries[0].name) : tmpDir;
|
|
134
|
+
sourcePath = path.join(sub, binaryName);
|
|
135
|
+
if (!fs.existsSync(sourcePath)) {
|
|
136
|
+
throw new Error(`Binary ${binaryName} not found in archive`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
await fsPromises.copyFile(sourcePath, destPath);
|
|
140
|
+
if (process.platform !== 'win32') {
|
|
141
|
+
await fsPromises.chmod(destPath, 0o755);
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
await fsPromises.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolve install paths: bin dir, binary name, dest path, and archive path.
|
|
150
|
+
* @returns {{ binDir: string, binaryName: string, destPath: string, archivePath: string }}
|
|
151
|
+
*/
|
|
152
|
+
function getInstallPaths() {
|
|
153
|
+
const home = getAifabrixHome();
|
|
154
|
+
const binDir = path.join(home, 'bin');
|
|
155
|
+
const binaryName = process.platform === 'win32' ? 'mutagen.exe' : 'mutagen';
|
|
156
|
+
const destPath = path.join(binDir, binaryName);
|
|
157
|
+
const archivePath = path.join(binDir, `mutagen-dl-${Date.now()}.tar.gz`);
|
|
158
|
+
return { binDir, binaryName, destPath, archivePath };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Download and install Mutagen to ~/.aifabrix/bin/. Uses internal path only (no PATH).
|
|
163
|
+
* @param {(msg: string) => void} [log] - Optional progress logger
|
|
164
|
+
* @returns {Promise<string>} Path to installed binary
|
|
165
|
+
* @throws {Error} If platform unsupported, download fails, or install fails
|
|
166
|
+
*/
|
|
167
|
+
async function installMutagen(log) {
|
|
168
|
+
const basename = getPlatformAssetBasename();
|
|
169
|
+
if (!basename) {
|
|
170
|
+
throw new Error(`Mutagen does not provide a binary for ${process.platform}/${process.arch}. Install manually to ~/.aifabrix/bin/.`);
|
|
171
|
+
}
|
|
172
|
+
const { tagName, assets } = await fetchLatestRelease();
|
|
173
|
+
const version = tagName.replace(/^v/, '');
|
|
174
|
+
const assetName = `${basename}_v${version}.tar.gz`;
|
|
175
|
+
const asset = assets.find(a => a.name === assetName);
|
|
176
|
+
if (!asset) {
|
|
177
|
+
throw new Error(`Mutagen release ${tagName} has no asset ${assetName}. Install manually to ~/.aifabrix/bin/.`);
|
|
178
|
+
}
|
|
179
|
+
const { binDir, binaryName, destPath, archivePath } = getInstallPaths();
|
|
180
|
+
await fsPromises.mkdir(binDir, { recursive: true });
|
|
181
|
+
try {
|
|
182
|
+
await downloadToFile(asset.browser_download_url, archivePath, log);
|
|
183
|
+
if (typeof log === 'function') log('Installing Mutagen...');
|
|
184
|
+
await extractAndInstall(archivePath, destPath, binaryName);
|
|
185
|
+
} finally {
|
|
186
|
+
await fsPromises.unlink(archivePath).catch(() => {});
|
|
187
|
+
}
|
|
188
|
+
return destPath;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
getPlatformAssetBasename,
|
|
193
|
+
fetchLatestRelease,
|
|
194
|
+
installMutagen
|
|
195
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutagen sync – binary path and session helpers (plan 65: sync for dev).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Mutagen binary resolution; session create/resume/terminate
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { getAifabrixHome } = require('./paths');
|
|
12
|
+
const { exec } = require('child_process');
|
|
13
|
+
const { promisify } = require('util');
|
|
14
|
+
|
|
15
|
+
const execAsync = promisify(exec);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Name of the Mutagen binary (platform-specific).
|
|
19
|
+
* @returns {string} mutagen or mutagen.exe
|
|
20
|
+
*/
|
|
21
|
+
function getMutagenBinaryName() {
|
|
22
|
+
return process.platform === 'win32' ? 'mutagen.exe' : 'mutagen';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Preferred path for Mutagen binary (~/.aifabrix/bin/mutagen or mutagen.exe).
|
|
27
|
+
* @returns {string} Absolute path
|
|
28
|
+
*/
|
|
29
|
+
function getMutagenBinPath() {
|
|
30
|
+
const home = getAifabrixHome();
|
|
31
|
+
return path.join(home, 'bin', getMutagenBinaryName());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve path to Mutagen binary. Uses only ~/.aifabrix/bin/ (never system PATH).
|
|
36
|
+
* @returns {Promise<string|null>} Path to binary or null if not installed
|
|
37
|
+
*/
|
|
38
|
+
async function getMutagenPath() {
|
|
39
|
+
const preferred = getMutagenBinPath();
|
|
40
|
+
return fs.existsSync(preferred) ? preferred : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Ensure Mutagen is available: return path if already installed, otherwise download and install
|
|
45
|
+
* to ~/.aifabrix/bin/ then return that path. Per remote-docker.md: CLI installs when missing.
|
|
46
|
+
* @param {(msg: string) => void} [log] - Optional progress logger (e.g. logger.log)
|
|
47
|
+
* @returns {Promise<string>} Path to Mutagen binary
|
|
48
|
+
* @throws {Error} If install fails (unsupported platform, network, etc.)
|
|
49
|
+
*/
|
|
50
|
+
async function ensureMutagenPath(log) {
|
|
51
|
+
const existing = await getMutagenPath();
|
|
52
|
+
if (existing) return existing;
|
|
53
|
+
const installMutagen = require('./mutagen-install').installMutagen;
|
|
54
|
+
await installMutagen(log);
|
|
55
|
+
const pathAfter = getMutagenBinPath();
|
|
56
|
+
if (!fs.existsSync(pathAfter)) {
|
|
57
|
+
throw new Error('Mutagen install did not create binary at ' + pathAfter);
|
|
58
|
+
}
|
|
59
|
+
return pathAfter;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Session name for app: aifabrix-<dev-id>-<app-key>
|
|
64
|
+
* @param {string} developerId - Developer ID
|
|
65
|
+
* @param {string} appKey - App key (e.g. app name)
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function getSessionName(developerId, appKey) {
|
|
69
|
+
return `aifabrix-${developerId}-${appKey}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Remote path for sync and Docker -v: user-mutagen-folder + '/' + relative path.
|
|
74
|
+
* Relative path is remoteSyncPath (normalized) when set, else 'dev/' + appKey.
|
|
75
|
+
* @param {string} userMutagenFolder - From config (no trailing slash)
|
|
76
|
+
* @param {string} appKey - App key (used when relativePathOverride is unset)
|
|
77
|
+
* @param {string} [relativePathOverride] - Optional; when non-empty, used as relative path under user-mutagen-folder (leading slashes stripped)
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function getRemotePath(userMutagenFolder, appKey, relativePathOverride) {
|
|
81
|
+
const base = (userMutagenFolder || '').trim().replace(/\/+$/, '');
|
|
82
|
+
if (!base) return '';
|
|
83
|
+
const raw = typeof relativePathOverride === 'string' ? relativePathOverride.trim() : '';
|
|
84
|
+
const relative = raw ? raw.replace(/^\/+/, '') : '';
|
|
85
|
+
if (relative) return `${base}/${relative}`;
|
|
86
|
+
return `${base}/dev/${appKey}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* SSH URL for Mutagen: sync-ssh-user@sync-ssh-host:remote_path
|
|
91
|
+
* @param {string} syncSshUser - SSH user
|
|
92
|
+
* @param {string} syncSshHost - SSH host
|
|
93
|
+
* @param {string} remotePath - Remote path
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
function getSyncSshUrl(syncSshUser, syncSshHost, remotePath) {
|
|
97
|
+
if (!syncSshUser || !syncSshHost || !remotePath) return '';
|
|
98
|
+
return `${syncSshUser}@${syncSshHost}:${remotePath}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* List sync session names (one per line).
|
|
103
|
+
* @param {string} mutagenPath - Path to mutagen binary
|
|
104
|
+
* @returns {Promise<string[]>}
|
|
105
|
+
*/
|
|
106
|
+
async function listSyncSessionNames(mutagenPath) {
|
|
107
|
+
const { stdout } = await execAsync(`"${mutagenPath}" sync list --template '{{.Name}}'`, {
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
timeout: 5000
|
|
110
|
+
});
|
|
111
|
+
return (stdout || '').trim().split('\n').filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Ensure a sync session exists: create or resume. Idempotent.
|
|
116
|
+
* @param {string} mutagenPath - Path to mutagen binary
|
|
117
|
+
* @param {string} sessionName - Session name (e.g. aifabrix-01-myapp)
|
|
118
|
+
* @param {string} localPath - Local app directory (absolute)
|
|
119
|
+
* @param {string} sshUrl - Remote SSH URL (user@host:path)
|
|
120
|
+
* @returns {Promise<void>}
|
|
121
|
+
* @throws {Error} If create or resume fails
|
|
122
|
+
*/
|
|
123
|
+
async function ensureSyncSession(mutagenPath, sessionName, localPath, sshUrl) {
|
|
124
|
+
const sessions = await listSyncSessionNames(mutagenPath);
|
|
125
|
+
if (sessions.includes(sessionName)) {
|
|
126
|
+
await execAsync(`"${mutagenPath}" sync resume "${sessionName}"`, { timeout: 10000 });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const local = path.resolve(localPath).replace(/\\/g, '/');
|
|
130
|
+
await execAsync(
|
|
131
|
+
`"${mutagenPath}" sync create "${local}" "${sshUrl}" --name "${sessionName}" --sync-mode two-way-resolved`,
|
|
132
|
+
{ timeout: 15000 }
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
getMutagenPath,
|
|
138
|
+
ensureMutagenPath,
|
|
139
|
+
getMutagenBinaryName,
|
|
140
|
+
getMutagenBinPath,
|
|
141
|
+
getSessionName,
|
|
142
|
+
getRemotePath,
|
|
143
|
+
getSyncSshUrl,
|
|
144
|
+
listSyncSessionNames,
|
|
145
|
+
ensureSyncSession
|
|
146
|
+
};
|
package/lib/utils/paths.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Path Utilities for AI Fabrix Builder
|
|
3
|
-
*
|
|
4
|
-
* Centralized helpers for resolving filesystem locations with support for
|
|
5
|
-
* AIFABRIX_HOME override. Defaults to ~/.aifabrix when not specified.
|
|
6
|
-
*
|
|
3
|
+
* Centralized helpers for resolving filesystem locations with AIFABRIX_HOME override.
|
|
7
4
|
* @fileoverview Path resolution utilities with environment overrides
|
|
8
5
|
* @author AI Fabrix Team
|
|
9
6
|
* @version 2.0.0
|
|
10
7
|
*/
|
|
8
|
+
/* eslint-disable max-lines -- Central path resolution; resolveIntegrationAppKeyFromCwd for datasource commands */
|
|
11
9
|
|
|
12
10
|
'use strict';
|
|
13
11
|
|
|
@@ -158,14 +156,8 @@ function getFallbackProjectRoot() {
|
|
|
158
156
|
return path.resolve(__dirname, '..', '..');
|
|
159
157
|
}
|
|
160
158
|
|
|
161
|
-
/**
|
|
162
|
-
* Gets the project root directory by finding package.json
|
|
163
|
-
* Works reliably in all environments including Jest tests and CI
|
|
164
|
-
* @returns {string} Absolute path to project root
|
|
165
|
-
*/
|
|
166
159
|
/**
|
|
167
160
|
* Checks if global PROJECT_ROOT is valid
|
|
168
|
-
* @function checkGlobalProjectRoot
|
|
169
161
|
* @returns {string|null} Valid global root or null
|
|
170
162
|
*/
|
|
171
163
|
function checkGlobalProjectRoot() {
|
|
@@ -195,39 +187,29 @@ function checkGlobalProjectRoot() {
|
|
|
195
187
|
|
|
196
188
|
/**
|
|
197
189
|
* Tries different strategies to find project root
|
|
198
|
-
* @function tryFindProjectRoot
|
|
199
190
|
* @returns {string} Found project root
|
|
200
191
|
*/
|
|
201
192
|
function tryFindProjectRoot() {
|
|
202
|
-
// Strategy 1: Check global.PROJECT_ROOT
|
|
203
193
|
const globalRoot = checkGlobalProjectRoot();
|
|
204
194
|
if (globalRoot) {
|
|
205
195
|
cachedProjectRoot = globalRoot;
|
|
206
196
|
return cachedProjectRoot;
|
|
207
197
|
}
|
|
208
|
-
|
|
209
|
-
// Strategy 2: Walk up from __dirname
|
|
210
198
|
const foundRoot = findProjectRootByWalkingUp(__dirname);
|
|
211
199
|
if (foundRoot && hasPackageJson(foundRoot)) {
|
|
212
200
|
cachedProjectRoot = foundRoot;
|
|
213
201
|
return cachedProjectRoot;
|
|
214
202
|
}
|
|
215
|
-
|
|
216
|
-
// Strategy 3: Try process.cwd()
|
|
217
203
|
const cwdRoot = findProjectRootFromCwd();
|
|
218
204
|
if (cwdRoot && hasPackageJson(cwdRoot)) {
|
|
219
205
|
cachedProjectRoot = cwdRoot;
|
|
220
206
|
return cachedProjectRoot;
|
|
221
207
|
}
|
|
222
|
-
|
|
223
|
-
// Strategy 4: Fallback
|
|
224
208
|
const fallbackRoot = getFallbackProjectRoot();
|
|
225
209
|
if (hasPackageJson(fallbackRoot)) {
|
|
226
210
|
cachedProjectRoot = fallbackRoot;
|
|
227
211
|
return cachedProjectRoot;
|
|
228
212
|
}
|
|
229
|
-
|
|
230
|
-
// Last resort
|
|
231
213
|
cachedProjectRoot = fallbackRoot;
|
|
232
214
|
return cachedProjectRoot;
|
|
233
215
|
}
|
|
@@ -242,10 +224,7 @@ function getProjectRoot() {
|
|
|
242
224
|
}
|
|
243
225
|
|
|
244
226
|
/**
|
|
245
|
-
* Returns the applications base directory
|
|
246
|
-
* Dev 0: <home>/applications
|
|
247
|
-
* Dev > 0: <home>/applications-dev-{id}
|
|
248
|
-
*
|
|
227
|
+
* Returns the applications base directory. Dev 0: <home>/applications; Dev > 0: <home>/applications-dev-{id}
|
|
249
228
|
* @param {number|string} developerId - Developer ID
|
|
250
229
|
* @returns {string} Absolute path to applications base directory
|
|
251
230
|
*/
|
|
@@ -259,19 +238,13 @@ function getApplicationsBaseDir(developerId) {
|
|
|
259
238
|
}
|
|
260
239
|
|
|
261
240
|
/**
|
|
262
|
-
* Returns the developer-specific application directory.
|
|
263
|
-
* Dev 0: points to applications/ (root)
|
|
264
|
-
* Dev > 0: <home>/applications-dev-{id} (root)
|
|
265
|
-
*
|
|
241
|
+
* Returns the developer-specific application directory. Dev 0: applications/; Dev > 0: applications-dev-{id}
|
|
266
242
|
* @param {string} appName - Application name
|
|
267
243
|
* @param {number|string} developerId - Developer ID
|
|
268
|
-
* @returns {string}
|
|
244
|
+
* @returns {string} Developer-specific app directory (root)
|
|
269
245
|
*/
|
|
270
246
|
function getDevDirectory(appName, developerId) {
|
|
271
247
|
const baseDir = getApplicationsBaseDir(developerId);
|
|
272
|
-
// All files should be generated at the root of the applications folder
|
|
273
|
-
// Dev 0: <home>/applications
|
|
274
|
-
// Dev > 0: <home>/applications-dev-{id}
|
|
275
248
|
return baseDir;
|
|
276
249
|
}
|
|
277
250
|
|
|
@@ -292,7 +265,6 @@ function getAppPath(appName, appType) {
|
|
|
292
265
|
|
|
293
266
|
/**
|
|
294
267
|
* Base directory for integration/builder: project root when cwd is inside project, else cwd.
|
|
295
|
-
* So deploy works when run from integration/<app> (e.g. node deploy.js), and tests using temp dirs still work.
|
|
296
268
|
* @returns {string} Directory to resolve integration/ and builder/ from
|
|
297
269
|
*/
|
|
298
270
|
function getIntegrationBuilderBaseDir() {
|
|
@@ -305,9 +277,78 @@ function getIntegrationBuilderBaseDir() {
|
|
|
305
277
|
return cwd;
|
|
306
278
|
}
|
|
307
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Returns the integration root directory (used for listing apps).
|
|
282
|
+
* @returns {string} Absolute path to integration/ directory
|
|
283
|
+
*/
|
|
284
|
+
function getIntegrationRoot() {
|
|
285
|
+
return path.join(getIntegrationBuilderBaseDir(), 'integration');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Returns the builder root directory. Uses AIFABRIX_BUILDER_DIR when set, else project/cwd + builder.
|
|
290
|
+
* @returns {string} Absolute path to builder/ directory
|
|
291
|
+
*/
|
|
292
|
+
function getBuilderRoot() {
|
|
293
|
+
const envDir = process.env.AIFABRIX_BUILDER_DIR && typeof process.env.AIFABRIX_BUILDER_DIR === 'string'
|
|
294
|
+
? process.env.AIFABRIX_BUILDER_DIR.trim()
|
|
295
|
+
: null;
|
|
296
|
+
if (envDir) {
|
|
297
|
+
return path.resolve(envDir);
|
|
298
|
+
}
|
|
299
|
+
return path.join(getIntegrationBuilderBaseDir(), 'builder');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Lists app names (directories) under integration root. Excludes dot-prefixed entries.
|
|
304
|
+
* Returns [] if root does not exist.
|
|
305
|
+
* @returns {string[]} Sorted list of app directory names
|
|
306
|
+
*/
|
|
307
|
+
function listIntegrationAppNames() {
|
|
308
|
+
const root = getIntegrationRoot();
|
|
309
|
+
if (!fs.existsSync(root)) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
const stat = fs.statSync(root);
|
|
313
|
+
if (!stat || typeof stat.isDirectory !== 'function' || !stat.isDirectory()) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
const entries = fs.readdirSync(root);
|
|
317
|
+
return entries
|
|
318
|
+
.filter(name => !name.startsWith('.'))
|
|
319
|
+
.filter(name => {
|
|
320
|
+
const fullPath = path.join(root, name);
|
|
321
|
+
return fs.statSync(fullPath).isDirectory();
|
|
322
|
+
})
|
|
323
|
+
.sort();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Lists app names (directories) under builder root. Excludes dot-prefixed entries.
|
|
328
|
+
* Returns [] if root does not exist.
|
|
329
|
+
* @returns {string[]} Sorted list of app directory names
|
|
330
|
+
*/
|
|
331
|
+
function listBuilderAppNames() {
|
|
332
|
+
const root = getBuilderRoot();
|
|
333
|
+
if (!fs.existsSync(root)) {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
const stat = fs.statSync(root);
|
|
337
|
+
if (!stat || typeof stat.isDirectory !== 'function' || !stat.isDirectory()) {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
const entries = fs.readdirSync(root);
|
|
341
|
+
return entries
|
|
342
|
+
.filter(name => !name.startsWith('.'))
|
|
343
|
+
.filter(name => {
|
|
344
|
+
const fullPath = path.join(root, name);
|
|
345
|
+
return fs.statSync(fullPath).isDirectory();
|
|
346
|
+
})
|
|
347
|
+
.sort();
|
|
348
|
+
}
|
|
349
|
+
|
|
308
350
|
/**
|
|
309
351
|
* Gets the integration folder path for external systems.
|
|
310
|
-
* Uses project root when cwd is inside project so deploy works when run from integration/<app> (e.g. node deploy.js).
|
|
311
352
|
* @param {string} appName - Application name
|
|
312
353
|
* @returns {string} Absolute path to integration directory
|
|
313
354
|
*/
|
|
@@ -320,9 +361,21 @@ function getIntegrationPath(appName) {
|
|
|
320
361
|
}
|
|
321
362
|
|
|
322
363
|
/**
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
364
|
+
* Resolves build.context from application.yaml to an absolute path.
|
|
365
|
+
* Used as the canonical app code directory for local mount and (when remote) Mutagen local path.
|
|
366
|
+
*
|
|
367
|
+
* @param {string} configDir - Directory containing the application config (e.g. builder/<appKey>/)
|
|
368
|
+
* @param {string} [buildContext='.'] - build.context value (relative to configDir)
|
|
369
|
+
* @returns {string} Absolute path to the app code directory
|
|
370
|
+
*/
|
|
371
|
+
function resolveBuildContext(configDir, buildContext) {
|
|
372
|
+
const dir = (configDir && typeof configDir === 'string') ? configDir : '';
|
|
373
|
+
const ctx = (buildContext && typeof buildContext === 'string') ? buildContext : '.';
|
|
374
|
+
return path.resolve(dir, ctx);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Gets the builder folder path. Uses AIFABRIX_BUILDER_DIR when set, else project root.
|
|
326
379
|
* @param {string} appName - Application name
|
|
327
380
|
* @returns {string} Absolute path to builder directory
|
|
328
381
|
*/
|
|
@@ -462,6 +515,37 @@ async function detectAppType(appName, _options = {}) {
|
|
|
462
515
|
if (builderResult) return builderResult;
|
|
463
516
|
throw new Error(`App '${appName}' not found in integration/${appName} or builder/${appName}`);
|
|
464
517
|
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Resolve-specific app path: prefer integration + env.template only (env-only mode).
|
|
521
|
+
* If integration/<appName>/env.template exists, use that directory without requiring application.yaml.
|
|
522
|
+
* Otherwise fall back to detectAppType (integration or builder with full config).
|
|
523
|
+
*
|
|
524
|
+
* @param {string} appName - Application name
|
|
525
|
+
* @returns {Promise<{appPath: string, envOnly: boolean}>} appPath and envOnly (true when only env.template is used)
|
|
526
|
+
* @throws {Error} When app not found in integration or builder
|
|
527
|
+
*/
|
|
528
|
+
async function getResolveAppPath(appName) {
|
|
529
|
+
if (!appName || typeof appName !== 'string') {
|
|
530
|
+
throw new Error('App name is required and must be a string');
|
|
531
|
+
}
|
|
532
|
+
const integrationPath = getIntegrationPath(appName);
|
|
533
|
+
const envTemplatePath = path.join(integrationPath, 'env.template');
|
|
534
|
+
if (fs.existsSync(integrationPath) && fs.existsSync(envTemplatePath)) {
|
|
535
|
+
return { appPath: integrationPath, envOnly: true };
|
|
536
|
+
}
|
|
537
|
+
const result = await detectAppType(appName);
|
|
538
|
+
return { appPath: result.appPath, envOnly: false };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Resolve appKey when cwd is inside integration/<appKey>/. */
|
|
542
|
+
function resolveIntegrationAppKeyFromCwd() {
|
|
543
|
+
const integrationNorm = path.resolve(path.join(getIntegrationBuilderBaseDir(), 'integration'));
|
|
544
|
+
const cwd = path.resolve(process.cwd());
|
|
545
|
+
if (cwd !== integrationNorm && !cwd.startsWith(integrationNorm + path.sep)) return null;
|
|
546
|
+
return path.relative(integrationNorm, cwd).split(path.sep)[0] || null;
|
|
547
|
+
}
|
|
548
|
+
|
|
465
549
|
module.exports = {
|
|
466
550
|
getAifabrixHome,
|
|
467
551
|
getConfigDirForPaths,
|
|
@@ -471,9 +555,16 @@ module.exports = {
|
|
|
471
555
|
getProjectRoot,
|
|
472
556
|
getIntegrationPath,
|
|
473
557
|
getBuilderPath,
|
|
558
|
+
getIntegrationRoot,
|
|
559
|
+
getBuilderRoot,
|
|
560
|
+
listIntegrationAppNames,
|
|
561
|
+
listBuilderAppNames,
|
|
562
|
+
resolveBuildContext,
|
|
474
563
|
getDeployJsonPath,
|
|
475
564
|
resolveApplicationConfigPath,
|
|
476
565
|
detectAppType,
|
|
566
|
+
getResolveAppPath,
|
|
567
|
+
resolveIntegrationAppKeyFromCwd,
|
|
477
568
|
clearProjectRootCache
|
|
478
569
|
};
|
|
479
570
|
|