@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.
Files changed (198) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +7 -5
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/integration/hubspot/test.js +1 -1
  16. package/jest.config.manual.js +2 -1
  17. package/lib/api/credential.api.js +40 -0
  18. package/lib/api/dev.api.js +423 -0
  19. package/lib/api/external-test.api.js +111 -0
  20. package/lib/api/index.js +42 -19
  21. package/lib/api/pipeline.api.js +66 -120
  22. package/lib/api/types/credential.types.js +23 -0
  23. package/lib/api/types/dev.types.js +140 -0
  24. package/lib/api/types/pipeline.types.js +37 -0
  25. package/lib/api/wizard-platform.api.js +61 -0
  26. package/lib/api/wizard.api.js +34 -1
  27. package/lib/app/config.js +44 -11
  28. package/lib/app/down.js +2 -1
  29. package/lib/app/index.js +12 -1
  30. package/lib/app/prompts.js +44 -29
  31. package/lib/app/push.js +36 -12
  32. package/lib/app/readme.js +9 -6
  33. package/lib/app/run-env-compose.js +264 -0
  34. package/lib/app/run-helpers.js +121 -118
  35. package/lib/app/run.js +148 -28
  36. package/lib/app/show-display.js +1 -1
  37. package/lib/app/show.js +5 -2
  38. package/lib/build/index.js +11 -3
  39. package/lib/cli/setup-app.js +172 -15
  40. package/lib/cli/setup-credential-deployment.js +31 -6
  41. package/lib/cli/setup-dev.js +206 -16
  42. package/lib/cli/setup-environment.js +16 -6
  43. package/lib/cli/setup-external-system.js +89 -24
  44. package/lib/cli/setup-infra.js +82 -15
  45. package/lib/cli/setup-secrets.js +52 -5
  46. package/lib/cli/setup-utility.js +129 -24
  47. package/lib/commands/app-install.js +172 -0
  48. package/lib/commands/app-shell.js +75 -0
  49. package/lib/commands/app-test.js +282 -0
  50. package/lib/commands/app.js +1 -1
  51. package/lib/commands/credential-env.js +162 -0
  52. package/lib/commands/credential-list.js +17 -22
  53. package/lib/commands/credential-push.js +96 -0
  54. package/lib/commands/datasource.js +77 -6
  55. package/lib/commands/dev-cli-handlers.js +141 -0
  56. package/lib/commands/dev-down.js +114 -0
  57. package/lib/commands/dev-init.js +347 -0
  58. package/lib/commands/repair-auth-config.js +99 -0
  59. package/lib/commands/repair-datasource-keys.js +208 -0
  60. package/lib/commands/repair-datasource.js +235 -0
  61. package/lib/commands/repair-env-template.js +348 -0
  62. package/lib/commands/repair-internal.js +85 -0
  63. package/lib/commands/repair-rbac.js +158 -0
  64. package/lib/commands/repair.js +507 -0
  65. package/lib/commands/secrets-list.js +118 -0
  66. package/lib/commands/secrets-remove.js +97 -0
  67. package/lib/commands/secrets-set.js +30 -17
  68. package/lib/commands/secrets-validate.js +50 -0
  69. package/lib/commands/test-e2e-external.js +165 -0
  70. package/lib/commands/up-dataplane.js +2 -2
  71. package/lib/commands/up-miso.js +0 -25
  72. package/lib/commands/upload.js +96 -40
  73. package/lib/commands/wizard-core-helpers.js +226 -4
  74. package/lib/commands/wizard-core.js +67 -29
  75. package/lib/commands/wizard-dataplane.js +1 -1
  76. package/lib/commands/wizard-entity-selection.js +43 -0
  77. package/lib/commands/wizard-headless.js +44 -5
  78. package/lib/commands/wizard-helpers.js +7 -3
  79. package/lib/commands/wizard.js +86 -64
  80. package/lib/core/admin-secrets.js +96 -0
  81. package/lib/core/config.js +7 -1
  82. package/lib/core/secrets-ensure.js +378 -0
  83. package/lib/core/secrets-env-write.js +157 -0
  84. package/lib/core/secrets.js +176 -89
  85. package/lib/datasource/deploy.js +12 -3
  86. package/lib/datasource/field-reference-validator.js +91 -0
  87. package/lib/datasource/test-e2e.js +219 -0
  88. package/lib/datasource/test-integration.js +154 -0
  89. package/lib/datasource/validate.js +21 -3
  90. package/lib/deployment/deployer.js +7 -5
  91. package/lib/deployment/environment-config.js +137 -0
  92. package/lib/deployment/environment.js +21 -98
  93. package/lib/deployment/push.js +32 -2
  94. package/lib/external-system/download.js +188 -203
  95. package/lib/external-system/generator.js +204 -56
  96. package/lib/external-system/test-auth.js +7 -3
  97. package/lib/external-system/test-execution.js +2 -1
  98. package/lib/external-system/test-system-level.js +73 -0
  99. package/lib/external-system/test.js +56 -19
  100. package/lib/generator/external-controller-manifest.js +29 -2
  101. package/lib/generator/external-schema-utils.js +1 -1
  102. package/lib/generator/external.js +10 -3
  103. package/lib/generator/index.js +177 -25
  104. package/lib/generator/split-readme.js +1 -0
  105. package/lib/generator/split-variables.js +7 -1
  106. package/lib/generator/split.js +194 -54
  107. package/lib/generator/wizard-prompts-secondary.js +294 -0
  108. package/lib/generator/wizard-prompts.js +105 -106
  109. package/lib/generator/wizard-readme.js +88 -0
  110. package/lib/generator/wizard.js +155 -158
  111. package/lib/infrastructure/compose.js +11 -1
  112. package/lib/infrastructure/helpers.js +103 -20
  113. package/lib/infrastructure/index.js +98 -12
  114. package/lib/infrastructure/services.js +88 -22
  115. package/lib/schema/application-schema.json +32 -8
  116. package/lib/schema/external-datasource.schema.json +49 -26
  117. package/lib/schema/external-system.schema.json +509 -411
  118. package/lib/schema/wizard-config.schema.json +16 -0
  119. package/lib/utils/api.js +41 -13
  120. package/lib/utils/app-register-auth.js +25 -3
  121. package/lib/utils/auth-headers.js +8 -7
  122. package/lib/utils/cli-utils.js +20 -0
  123. package/lib/utils/compose-generator.js +77 -76
  124. package/lib/utils/compose-handlebars-helpers.js +54 -0
  125. package/lib/utils/compose-vector-helper.js +18 -0
  126. package/lib/utils/config-format-preference.js +51 -0
  127. package/lib/utils/config-format.js +36 -0
  128. package/lib/utils/config-paths.js +127 -2
  129. package/lib/utils/configuration-env-resolver.js +179 -0
  130. package/lib/utils/credential-display.js +83 -0
  131. package/lib/utils/credential-secrets-env.js +357 -0
  132. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  133. package/lib/utils/deployment-validation-helpers.js +4 -4
  134. package/lib/utils/dev-ca-install.js +139 -0
  135. package/lib/utils/dev-cert-helper.js +122 -0
  136. package/lib/utils/device-code-helpers.js +224 -0
  137. package/lib/utils/device-code.js +37 -336
  138. package/lib/utils/docker-build.js +40 -8
  139. package/lib/utils/env-copy.js +103 -13
  140. package/lib/utils/env-map.js +35 -5
  141. package/lib/utils/env-template.js +6 -5
  142. package/lib/utils/error-formatters/http-status-errors.js +20 -2
  143. package/lib/utils/error-formatters/permission-errors.js +0 -1
  144. package/lib/utils/error-formatters/validation-errors.js +0 -1
  145. package/lib/utils/external-readme.js +56 -29
  146. package/lib/utils/external-system-display.js +59 -1
  147. package/lib/utils/external-system-test-helpers.js +21 -8
  148. package/lib/utils/external-system-validators.js +3 -0
  149. package/lib/utils/file-upload.js +20 -50
  150. package/lib/utils/help-builder.js +16 -2
  151. package/lib/utils/infra-status.js +80 -45
  152. package/lib/utils/local-secrets.js +7 -52
  153. package/lib/utils/mutagen-install.js +195 -0
  154. package/lib/utils/mutagen.js +146 -0
  155. package/lib/utils/paths.js +128 -37
  156. package/lib/utils/port-resolver.js +28 -16
  157. package/lib/utils/remote-dev-auth.js +38 -0
  158. package/lib/utils/remote-docker-env.js +43 -0
  159. package/lib/utils/remote-secrets-loader.js +60 -0
  160. package/lib/utils/secrets-canonical.js +93 -0
  161. package/lib/utils/secrets-generator.js +114 -6
  162. package/lib/utils/secrets-helpers.js +108 -114
  163. package/lib/utils/secrets-path.js +2 -2
  164. package/lib/utils/secrets-utils.js +52 -1
  165. package/lib/utils/secrets-validation.js +84 -0
  166. package/lib/utils/ssh-key-helper.js +116 -0
  167. package/lib/utils/test-log-writer.js +56 -0
  168. package/lib/utils/token-manager-messages.js +90 -0
  169. package/lib/utils/token-manager.js +29 -36
  170. package/lib/utils/variable-transformer.js +3 -3
  171. package/lib/validation/env-template-auth.js +157 -0
  172. package/lib/validation/env-template-kv.js +41 -0
  173. package/lib/validation/external-manifest-validator.js +25 -0
  174. package/lib/validation/external-system-auth-rules.js +86 -0
  175. package/lib/validation/validate-batch.js +149 -0
  176. package/lib/validation/validate-datasource-keys-api.js +33 -0
  177. package/lib/validation/validate-display.js +94 -16
  178. package/lib/validation/validate.js +25 -12
  179. package/lib/validation/validator.js +72 -9
  180. package/lib/validation/wizard-datasource-validation.js +50 -0
  181. package/package.json +8 -3
  182. package/scripts/install-local.js +34 -15
  183. package/templates/README.md +0 -1
  184. package/templates/applications/README.md.hbs +4 -4
  185. package/templates/applications/dataplane/application.yaml +6 -5
  186. package/templates/applications/dataplane/env.template +15 -10
  187. package/templates/applications/dataplane/rbac.yaml +2 -2
  188. package/templates/applications/keycloak/env.template +2 -0
  189. package/templates/applications/miso-controller/application.yaml +1 -0
  190. package/templates/applications/miso-controller/env.template +12 -10
  191. package/templates/external-system/README.md.hbs +65 -25
  192. package/templates/external-system/deploy.js.hbs +4 -2
  193. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  194. package/templates/external-system/external-system.json.hbs +1 -18
  195. package/templates/infra/compose.yaml.hbs +6 -0
  196. package/templates/python/docker-compose.hbs +49 -23
  197. package/templates/typescript/docker-compose.hbs +48 -22
  198. 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
+ };
@@ -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 for a developer.
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} Absolute path to developer-specific app directory
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
- * Gets the builder folder path for regular applications.
324
- * When AIFABRIX_BUILDER_DIR is set (e.g. by up-miso/up-dataplane from config aifabrix-env-config),
325
- * uses that as builder root; otherwise uses project root so deploy works when run from integration/<app>.
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