@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,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview aifabrix dev init – onboard with Builder Server (issue-cert, save cert, get settings, add SSH key).
|
|
3
|
+
* Auth: first call (issue-cert) uses no client cert; all other calls (getSettings, addSshKey, and every other dev API) send the client certificate.
|
|
4
|
+
* @author AI Fabrix Team
|
|
5
|
+
* @version 2.0.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs').promises;
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const config = require('../core/config');
|
|
12
|
+
const { getConfigDirForPaths } = require('../utils/paths');
|
|
13
|
+
const { generateCSR, getCertDir, readClientCertPem, readClientKeyPem, getCertValidNotAfter } = require('../utils/dev-cert-helper');
|
|
14
|
+
const { getOrCreatePublicKeyContent } = require('../utils/ssh-key-helper');
|
|
15
|
+
const devApi = require('../api/dev.api');
|
|
16
|
+
const logger = require('../utils/logger');
|
|
17
|
+
const {
|
|
18
|
+
isSslUntrustedError,
|
|
19
|
+
fetchInstallCa,
|
|
20
|
+
installCaPlatform,
|
|
21
|
+
promptInstallCa
|
|
22
|
+
} = require('../utils/dev-ca-install');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensure the Builder Server is trusted: run health check; on SSL untrusted error,
|
|
26
|
+
* optionally fetch and install CA, then retry.
|
|
27
|
+
* @param {string} baseUrl - Builder Server base URL
|
|
28
|
+
* @param {Object} options - Commander options (yes, y, no-install-ca)
|
|
29
|
+
* @returns {Promise<void>}
|
|
30
|
+
*/
|
|
31
|
+
async function ensureServerTrusted(baseUrl, options) {
|
|
32
|
+
const skipInstall = options['no-install-ca'];
|
|
33
|
+
const autoInstall = options.yes || options.y;
|
|
34
|
+
try {
|
|
35
|
+
await devApi.getHealth(baseUrl);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (!isSslUntrustedError(err)) throw err;
|
|
38
|
+
const manualUrl = `${baseUrl.replace(/\/+$/, '')}/install-ca`;
|
|
39
|
+
if (skipInstall) {
|
|
40
|
+
throw new Error(`Server certificate not trusted. Install CA manually: ${manualUrl}`);
|
|
41
|
+
}
|
|
42
|
+
if (!autoInstall) {
|
|
43
|
+
const install = await promptInstallCa();
|
|
44
|
+
if (!install) {
|
|
45
|
+
throw new Error(`Server certificate not trusted. Install CA manually: ${manualUrl}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
logger.log(chalk.gray(' Downloading and installing CA...'));
|
|
49
|
+
const caPem = await fetchInstallCa(baseUrl);
|
|
50
|
+
await installCaPlatform(caPem, baseUrl);
|
|
51
|
+
logger.log(chalk.gray(' CA installed. Retrying...'));
|
|
52
|
+
await devApi.getHealth(baseUrl);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate init options and return normalized baseUrl and devId.
|
|
58
|
+
* @param {Object} options - Commander options
|
|
59
|
+
* @returns {{ baseUrl: string, devId: string }}
|
|
60
|
+
*/
|
|
61
|
+
function validateInitOptions(options) {
|
|
62
|
+
const devId = options.developerId || options['developer-id'];
|
|
63
|
+
const server = options.server;
|
|
64
|
+
const pin = options.pin;
|
|
65
|
+
|
|
66
|
+
if (!devId || typeof devId !== 'string' || !/^[0-9]+$/.test(devId)) {
|
|
67
|
+
throw new Error('--developer-id is required and must be a non-empty digit string (e.g. 01)');
|
|
68
|
+
}
|
|
69
|
+
if (!server || typeof server !== 'string' || !server.trim()) {
|
|
70
|
+
throw new Error('--server is required and must be the Builder Server base URL (e.g. https://dev.aifabrix.dev)');
|
|
71
|
+
}
|
|
72
|
+
if (!pin || typeof pin !== 'string' || !pin.trim()) {
|
|
73
|
+
throw new Error('--pin is required (one-time PIN from your admin)');
|
|
74
|
+
}
|
|
75
|
+
return { baseUrl: server.trim().replace(/\/+$/, ''), devId };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Request certificate from Builder Server; map API errors to user messages.
|
|
80
|
+
* @param {string} baseUrl - Builder Server base URL
|
|
81
|
+
* @param {string} devId - Developer ID
|
|
82
|
+
* @param {string} pin - One-time PIN
|
|
83
|
+
* @param {string} csrPem - PEM CSR
|
|
84
|
+
* @returns {Promise<Object>} IssueCertResponseDto
|
|
85
|
+
*/
|
|
86
|
+
async function requestCertificate(baseUrl, devId, pin, csrPem) {
|
|
87
|
+
try {
|
|
88
|
+
return await devApi.issueCert(baseUrl, {
|
|
89
|
+
developerId: devId,
|
|
90
|
+
pin: pin.trim(),
|
|
91
|
+
csr: csrPem
|
|
92
|
+
});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err.status === 401) {
|
|
95
|
+
throw new Error('Invalid or expired PIN. Ask your admin for a new PIN (aifabrix dev pin <developerId>).');
|
|
96
|
+
}
|
|
97
|
+
if (err.status === 404) {
|
|
98
|
+
throw new Error(`Developer ${devId} not found on the server.`);
|
|
99
|
+
}
|
|
100
|
+
if (err.status === 503) {
|
|
101
|
+
throw new Error('Certificate signing is temporarily unavailable. Try again later.');
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Normalize PEM string: turn literal \n (backslash-n) into real newlines so Docker/OpenSSL accept it.
|
|
109
|
+
* Some servers return JSON with escaped newlines in the PEM string.
|
|
110
|
+
* @param {string} pem - PEM string (certificate or CA)
|
|
111
|
+
* @returns {string} PEM with real newlines
|
|
112
|
+
*/
|
|
113
|
+
function normalizePemNewlines(pem) {
|
|
114
|
+
if (typeof pem !== 'string') return pem;
|
|
115
|
+
return pem.replace(/\\n/g, '\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Save certificate, key, and optional CA to cert dir; set developer-id in config.
|
|
120
|
+
* Remote Docker requires ca.pem in the cert dir; if the server provides it (e.g. issue-cert
|
|
121
|
+
* response caCertificate or ca), it is saved so DOCKER_CERT_PATH works.
|
|
122
|
+
* @param {string} configDir - Config directory
|
|
123
|
+
* @param {string} devId - Developer ID
|
|
124
|
+
* @param {string} certificatePem - Issued certificate PEM
|
|
125
|
+
* @param {string} keyPem - Private key PEM
|
|
126
|
+
* @param {string} [caPem] - Optional CA certificate PEM (for remote Docker TLS)
|
|
127
|
+
*/
|
|
128
|
+
async function saveCertAndConfig(configDir, devId, certificatePem, keyPem, caPem) {
|
|
129
|
+
const certDir = getCertDir(configDir, devId);
|
|
130
|
+
await fs.mkdir(certDir, { recursive: true });
|
|
131
|
+
const certNormalized = normalizePemNewlines(certificatePem);
|
|
132
|
+
const keyNormalized = normalizePemNewlines(keyPem);
|
|
133
|
+
await fs.writeFile(path.join(certDir, 'cert.pem'), certNormalized, { mode: 0o600 });
|
|
134
|
+
await fs.writeFile(path.join(certDir, 'key.pem'), keyNormalized, { mode: 0o600 });
|
|
135
|
+
if (caPem && typeof caPem === 'string' && caPem.trim()) {
|
|
136
|
+
const caNormalized = normalizePemNewlines(caPem.trim());
|
|
137
|
+
await fs.writeFile(path.join(certDir, 'ca.pem'), caNormalized, { mode: 0o600 });
|
|
138
|
+
logger.log(chalk.green(' ✓ Certificate and CA saved to ') + chalk.cyan(path.join(certDir, 'cert.pem')));
|
|
139
|
+
} else {
|
|
140
|
+
logger.log(chalk.green(' ✓ Certificate saved to ') + chalk.cyan(path.join(certDir, 'cert.pem')));
|
|
141
|
+
}
|
|
142
|
+
await config.setDeveloperId(devId);
|
|
143
|
+
logger.log(chalk.green(' ✓ Developer ID set to ') + chalk.cyan(devId));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Message for 400 Bad Request: nginx often forwards X-Client-Cert with literal newlines.
|
|
148
|
+
* @returns {string} Hint for server-side nginx fix
|
|
149
|
+
*/
|
|
150
|
+
function getBadRequestHint() {
|
|
151
|
+
return 'Bad Request (400) often means the server\'s nginx is forwarding the client certificate with literal newlines in X-Client-Cert. On the server, use nginx njs to escape newlines (see .cursor/plans/builder-cli.md §5).';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Log a one-line hint for cert troubleshooting (curl test and docs).
|
|
156
|
+
* @param {string} configDir - Config directory
|
|
157
|
+
* @param {string} devId - Developer ID
|
|
158
|
+
* @param {string} baseUrl - Builder Server base URL
|
|
159
|
+
*/
|
|
160
|
+
function logCertTroubleshootingHint(configDir, devId, baseUrl) {
|
|
161
|
+
const certDir = getCertDir(configDir, devId);
|
|
162
|
+
const certPath = path.join(certDir, 'cert.pem');
|
|
163
|
+
const keyPath = path.join(certDir, 'key.pem');
|
|
164
|
+
logger.log(chalk.gray(` Test with: curl -v --cert ${certPath} --key ${keyPath} ${baseUrl}/api/dev/settings`));
|
|
165
|
+
logger.log(chalk.gray(' See .cursor/plans/builder-cli.md §5 for 200 vs 401 vs 400 and nginx/server fix.'));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Register SSH public key with Builder Server for Mutagen sync.
|
|
170
|
+
* @param {string} baseUrl - Builder Server base URL
|
|
171
|
+
* @param {string} clientCertPem - Client certificate PEM
|
|
172
|
+
* @param {string} clientKeyPem - Client private key PEM (for mTLS)
|
|
173
|
+
* @param {string} devId - Developer ID
|
|
174
|
+
*/
|
|
175
|
+
async function registerSshKey(baseUrl, clientCertPem, clientKeyPem, devId) {
|
|
176
|
+
const publicKey = getOrCreatePublicKeyContent();
|
|
177
|
+
try {
|
|
178
|
+
await devApi.addSshKey(baseUrl, clientCertPem, devId, {
|
|
179
|
+
publicKey,
|
|
180
|
+
label: 'aifabrix-init'
|
|
181
|
+
}, clientKeyPem);
|
|
182
|
+
logger.log(chalk.green(' ✓ SSH key registered'));
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (err.status === 409) {
|
|
185
|
+
logger.log(chalk.yellow(' ⚠ SSH key already registered'));
|
|
186
|
+
} else {
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Run SSH key registration step (log, register, handle 400 hint).
|
|
194
|
+
* @param {string} baseUrl - Builder Server base URL
|
|
195
|
+
* @param {Object} issueResponse - IssueCert response (certificate)
|
|
196
|
+
* @param {string} keyPem - Client key PEM
|
|
197
|
+
* @param {string} configDir - Config directory
|
|
198
|
+
* @param {string} devId - Developer ID
|
|
199
|
+
* @private
|
|
200
|
+
*/
|
|
201
|
+
async function _runSshKeyRegistrationStep(baseUrl, issueResponse, keyPem, configDir, devId) {
|
|
202
|
+
logger.log(chalk.gray(' Registering SSH key for Mutagen sync...'));
|
|
203
|
+
try {
|
|
204
|
+
if (keyPem && typeof keyPem === 'string') {
|
|
205
|
+
logger.log(chalk.gray(' Using client certificate for TLS'));
|
|
206
|
+
}
|
|
207
|
+
await registerSshKey(baseUrl, issueResponse.certificate, keyPem, devId);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
const msg = err.status === 400 ? getBadRequestHint() : (err.message || String(err));
|
|
210
|
+
logger.log(chalk.yellow(' ⚠ Could not register SSH key: ' + msg));
|
|
211
|
+
logCertTroubleshootingHint(configDir, devId, baseUrl);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Apply settings from issue-cert response or fetch via getSettings; merge into config.
|
|
217
|
+
* @param {string} baseUrl - Builder Server base URL
|
|
218
|
+
* @param {string} devId - Developer ID
|
|
219
|
+
* @param {Object} issueResponse - IssueCert response (certificate, settings)
|
|
220
|
+
* @param {string} keyPem - Client key PEM
|
|
221
|
+
*/
|
|
222
|
+
async function applySettingsFromServer(baseUrl, devId, issueResponse, keyPem) {
|
|
223
|
+
const configDir = getConfigDirForPaths();
|
|
224
|
+
if (issueResponse.settings && typeof issueResponse.settings === 'object') {
|
|
225
|
+
await config.mergeRemoteSettings(issueResponse.settings);
|
|
226
|
+
logger.log(chalk.green(' ✓ Config updated from server (issue-cert response)'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
logger.log(chalk.gray(' Fetching settings...'));
|
|
230
|
+
try {
|
|
231
|
+
if (keyPem && typeof keyPem === 'string') {
|
|
232
|
+
logger.log(chalk.gray(' Using client certificate for TLS'));
|
|
233
|
+
}
|
|
234
|
+
const settings = await devApi.getSettings(baseUrl, issueResponse.certificate, keyPem);
|
|
235
|
+
await config.mergeRemoteSettings(settings);
|
|
236
|
+
logger.log(chalk.green(' ✓ Config updated from server'));
|
|
237
|
+
} catch (err) {
|
|
238
|
+
const msg = err.status === 400 ? getBadRequestHint() : (err.message || String(err));
|
|
239
|
+
logger.log(chalk.yellow(' ⚠ Could not fetch settings (server may not support cert yet): ' + msg));
|
|
240
|
+
logCertTroubleshootingHint(configDir, devId, baseUrl);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Run dev init: validate PIN via issue-cert, save certificate, fetch settings, add SSH key.
|
|
246
|
+
* @param {Object} options - Commander options (devId, server, pin)
|
|
247
|
+
* @returns {Promise<void>}
|
|
248
|
+
*/
|
|
249
|
+
async function runDevInit(options) {
|
|
250
|
+
const { baseUrl, devId } = validateInitOptions(options);
|
|
251
|
+
logger.log(chalk.blue('\n🔐 Onboarding with Builder Server...\n'));
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
await ensureServerTrusted(baseUrl, options);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
throw new Error(`Cannot reach Builder Server at ${baseUrl}. Check URL and network. ${err.message}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
logger.log(chalk.gray(' Generating certificate request...'));
|
|
260
|
+
const { csrPem, keyPem } = generateCSR(devId);
|
|
261
|
+
|
|
262
|
+
logger.log(chalk.gray(' Requesting certificate (issue-cert)...'));
|
|
263
|
+
const issueResponse = await requestCertificate(baseUrl, devId, options.pin, csrPem);
|
|
264
|
+
|
|
265
|
+
const configDir = getConfigDirForPaths();
|
|
266
|
+
const caPem = issueResponse.caCertificate || issueResponse.ca;
|
|
267
|
+
await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
|
|
268
|
+
|
|
269
|
+
await config.setRemoteServer(baseUrl);
|
|
270
|
+
|
|
271
|
+
await applySettingsFromServer(baseUrl, devId, issueResponse, keyPem);
|
|
272
|
+
await _runSshKeyRegistrationStep(baseUrl, issueResponse, keyPem, configDir, devId);
|
|
273
|
+
logger.log(chalk.green('\n✓ Onboarding complete. You can use remote Docker and Mutagen sync.\n'));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Days before cert expiry at which we auto-refresh on dev refresh. */
|
|
277
|
+
const CERT_REFRESH_DAYS = 14;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* True if the cert in certDir expires within CERT_REFRESH_DAYS (or we cannot read expiry).
|
|
281
|
+
* @param {string} certDir - Certificate directory
|
|
282
|
+
* @returns {boolean}
|
|
283
|
+
*/
|
|
284
|
+
function shouldRefreshDevCert(certDir) {
|
|
285
|
+
const validNotAfter = getCertValidNotAfter(certDir);
|
|
286
|
+
if (!validNotAfter) return true;
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
const threshold = now + CERT_REFRESH_DAYS * 24 * 60 * 60 * 1000;
|
|
289
|
+
return validNotAfter.getTime() < threshold;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Refresh developer certificate: create PIN (with current cert), issue new cert, save and apply settings.
|
|
294
|
+
* @param {{ serverUrl: string, clientCertPem: string }} auth - Current auth from getRemoteDevAuth
|
|
295
|
+
* @returns {Promise<void>}
|
|
296
|
+
*/
|
|
297
|
+
async function runCertificateRefresh(auth) {
|
|
298
|
+
const devId = await config.getDeveloperId();
|
|
299
|
+
if (!devId) throw new Error('developer-id not set in config.');
|
|
300
|
+
const configDir = getConfigDirForPaths();
|
|
301
|
+
logger.log(chalk.blue('\n🔄 Refreshing certificate (create PIN + issue-cert)...\n'));
|
|
302
|
+
const pinRes = await devApi.createPin(auth.serverUrl, auth.clientCertPem, devId);
|
|
303
|
+
const pin = pinRes.pin;
|
|
304
|
+
if (!pin || typeof pin !== 'string') throw new Error('Server did not return a PIN.');
|
|
305
|
+
logger.log(chalk.gray(' Generating new certificate request...'));
|
|
306
|
+
const { csrPem, keyPem } = generateCSR(devId);
|
|
307
|
+
logger.log(chalk.gray(' Requesting new certificate (issue-cert)...'));
|
|
308
|
+
const issueResponse = await requestCertificate(auth.serverUrl, devId, pin, csrPem);
|
|
309
|
+
const caPem = issueResponse.caCertificate || issueResponse.ca;
|
|
310
|
+
await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
|
|
311
|
+
await applySettingsFromServer(auth.serverUrl, devId, issueResponse, keyPem);
|
|
312
|
+
logger.log(chalk.green('✓ Certificate refreshed and config updated from server.\n'));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Fetch settings from Builder Server and merge into config (GET /api/dev/settings).
|
|
317
|
+
* If certificate expires within CERT_REFRESH_DAYS (or --cert), refresh cert first (create PIN + issue-cert).
|
|
318
|
+
* @param {Object} [options] - Commander options; options.cert = true forces cert refresh
|
|
319
|
+
* @returns {Promise<void>}
|
|
320
|
+
* @throws {Error} If remote server or certificate not configured, or getSettings fails
|
|
321
|
+
*/
|
|
322
|
+
async function runDevRefresh(options = {}) {
|
|
323
|
+
const { getRemoteDevAuth } = require('../utils/remote-dev-auth');
|
|
324
|
+
const auth = await getRemoteDevAuth();
|
|
325
|
+
if (!auth) {
|
|
326
|
+
throw new Error('Remote server is not configured. Set remote-server and run "aifabrix dev init" first.');
|
|
327
|
+
}
|
|
328
|
+
const devId = await config.getDeveloperId();
|
|
329
|
+
const configDir = getConfigDirForPaths();
|
|
330
|
+
const certDir = getCertDir(configDir, devId);
|
|
331
|
+
const clientCertPem = readClientCertPem(certDir);
|
|
332
|
+
const clientKeyPem = readClientKeyPem(certDir);
|
|
333
|
+
if (!clientCertPem) {
|
|
334
|
+
throw new Error('Client certificate not found. Run "aifabrix dev init" first.');
|
|
335
|
+
}
|
|
336
|
+
const forceCertRefresh = Boolean(options.cert);
|
|
337
|
+
if (forceCertRefresh || shouldRefreshDevCert(certDir)) {
|
|
338
|
+
await runCertificateRefresh(auth);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
logger.log(chalk.blue('\n🔄 Fetching settings from Builder Server...\n'));
|
|
342
|
+
const settings = await devApi.getSettings(auth.serverUrl, clientCertPem, clientKeyPem || undefined);
|
|
343
|
+
await config.mergeRemoteSettings(settings);
|
|
344
|
+
logger.log(chalk.green('✓ Config updated from server. Run "aifabrix dev config" to verify.\n'));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = { runDevInit, runDevRefresh };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes system file authentication.security and configuration keyvault entries.
|
|
3
|
+
* @fileoverview Repair auth/config KV_* names and path-style kv:// values
|
|
4
|
+
* @author AI Fabrix Team
|
|
5
|
+
* @version 2.0.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const { systemKeyToKvPrefix, securityKeyToVar, kvEnvKeyToPath } = require('../utils/credential-secrets-env');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns true if a kv value looks like legacy format (KeyVault suffix or no path segments).
|
|
14
|
+
* @param {string} val - Value from authentication.security or configuration
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function isLegacyKvValue(val) {
|
|
18
|
+
if (typeof val !== 'string' || !val.trim().toLowerCase().startsWith('kv://')) return false;
|
|
19
|
+
const after = val.trim().slice(5); // after 'kv://'
|
|
20
|
+
return after.includes('KeyVault') || !after.includes('/');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalizes authentication.security keyvault entries to path-style kv:// values (kv://systemKey/variable).
|
|
25
|
+
* @param {Object} security - authentication.security object (mutated)
|
|
26
|
+
* @param {string} prefix - KV prefix (e.g. 'HUBSPOT')
|
|
27
|
+
* @param {string} systemKey - System key for path namespace
|
|
28
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
29
|
+
* @returns {boolean} True if any change was made
|
|
30
|
+
*/
|
|
31
|
+
function normalizeSecuritySection(security, prefix, systemKey, changes) {
|
|
32
|
+
let updated = false;
|
|
33
|
+
for (const key of Object.keys(security)) {
|
|
34
|
+
const val = security[key];
|
|
35
|
+
if (typeof val !== 'string' || !isLegacyKvValue(val)) continue;
|
|
36
|
+
const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
|
|
37
|
+
const pathVal = kvEnvKeyToPath(envName, systemKey);
|
|
38
|
+
if (pathVal) {
|
|
39
|
+
security[key] = pathVal;
|
|
40
|
+
changes.push(`authentication.security.${key}: normalized to path-style ${pathVal}`);
|
|
41
|
+
updated = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return updated;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes configuration array keyvault entries to canonical KV_* names and path-style values.
|
|
49
|
+
* @param {Object[]} config - configuration array (mutated)
|
|
50
|
+
* @param {string} prefix - KV prefix (e.g. 'HUBSPOT')
|
|
51
|
+
* @param {string} systemKey - System key for path namespace
|
|
52
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
53
|
+
* @returns {boolean} True if any change was made
|
|
54
|
+
*/
|
|
55
|
+
function normalizeConfigurationSection(config, prefix, systemKey, changes) {
|
|
56
|
+
let updated = false;
|
|
57
|
+
for (let i = 0; i < config.length; i++) {
|
|
58
|
+
const entry = config[i];
|
|
59
|
+
if (!entry || !entry.name || (entry.location !== 'keyvault' && !String(entry.name).startsWith('KV_'))) continue;
|
|
60
|
+
const afterPrefix = entry.name.startsWith(`KV_${prefix}_`)
|
|
61
|
+
? entry.name.slice(`KV_${prefix}_`.length)
|
|
62
|
+
: entry.name.replace(/^KV_[A-Z0-9]+_/, '');
|
|
63
|
+
const normalizedVar = afterPrefix.replace(/_/g, '').toUpperCase();
|
|
64
|
+
const canonicalName = `KV_${prefix}_${normalizedVar}`;
|
|
65
|
+
const pathVal = kvEnvKeyToPath(canonicalName, systemKey);
|
|
66
|
+
if (!pathVal) continue;
|
|
67
|
+
const pathValWithoutPrefix = pathVal.replace(/^kv:\/\//, '');
|
|
68
|
+
const valueLegacy = typeof entry.value === 'string' && (entry.value.includes('KeyVault') || !entry.value.includes('/'));
|
|
69
|
+
if (entry.name !== canonicalName || (valueLegacy && entry.value !== pathValWithoutPrefix)) {
|
|
70
|
+
config[i] = { ...entry, name: canonicalName, value: pathValWithoutPrefix, location: 'keyvault' };
|
|
71
|
+
changes.push(`configuration: normalized ${entry.name} → ${canonicalName}, value → path-style`);
|
|
72
|
+
updated = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return updated;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Normalizes system file authentication.security and configuration keyvault entries to canonical
|
|
80
|
+
* KV_* names and path-style kv:// values so upload validation and env.template align.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} systemParsed - Parsed system config (mutated)
|
|
83
|
+
* @param {string} systemKey - System key (e.g. 'hubspot')
|
|
84
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
85
|
+
* @returns {boolean} True if any change was made
|
|
86
|
+
*/
|
|
87
|
+
function normalizeSystemFileAuthAndConfig(systemParsed, systemKey, changes) {
|
|
88
|
+
const prefix = systemKeyToKvPrefix(systemKey);
|
|
89
|
+
if (!prefix) return false;
|
|
90
|
+
const security = systemParsed.authentication?.security;
|
|
91
|
+
let updated = (security && typeof security === 'object' && normalizeSecuritySection(security, prefix, systemKey, changes));
|
|
92
|
+
const config = systemParsed.configuration;
|
|
93
|
+
if (Array.isArray(config)) {
|
|
94
|
+
updated = normalizeConfigurationSection(config, prefix, systemKey, changes) || updated;
|
|
95
|
+
}
|
|
96
|
+
return updated;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { normalizeSystemFileAuthAndConfig, isLegacyKvValue };
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize datasource keys and filenames to canonical form during repair.
|
|
3
|
+
*
|
|
4
|
+
* Key: <systemKey>-<resourceType> or <systemKey>-<resourceType>-2, -3 for duplicates.
|
|
5
|
+
* Filename: <systemKey>-datasource-<suffix>.<ext> where suffix = key without leading systemKey-.
|
|
6
|
+
* Skips keys/filenames that already match the valid pattern (e.g. customer-extra, customer-1).
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview Datasource key and filename normalization for repair
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 2.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns suffix from a canonical-format filename: <systemKey>-datasource-<suffix>.<ext>.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} fileName - Filename
|
|
23
|
+
* @param {string} systemKey - System key
|
|
24
|
+
* @returns {string|null} Suffix or null if not in canonical format
|
|
25
|
+
*/
|
|
26
|
+
function suffixFromCanonicalFilename(fileName, systemKey) {
|
|
27
|
+
const base = path.basename(fileName);
|
|
28
|
+
const ext = path.extname(fileName);
|
|
29
|
+
const withoutExt = base.slice(0, -ext.length);
|
|
30
|
+
const prefix = `${systemKey}-datasource-`;
|
|
31
|
+
if (!withoutExt.startsWith(prefix)) return null;
|
|
32
|
+
return withoutExt.slice(prefix.length) || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns true if the key already matches canonical form and should not be changed.
|
|
37
|
+
* Valid: <systemKey>-<resourceType> or <systemKey>-<resourceType>-<extra> (e.g. customer-extra, customer-1).
|
|
38
|
+
* When fileName is provided and is canonical, key may be just the suffix (e.g. record-storage).
|
|
39
|
+
* Invalid (will normalize): key ending with redundant -datasource (e.g. hubspot-demo-companies-datasource).
|
|
40
|
+
*
|
|
41
|
+
* @param {string} key - Datasource key
|
|
42
|
+
* @param {string} systemKey - System key
|
|
43
|
+
* @param {string} [fileName] - Optional filename; if canonical, key can be suffix-only
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isKeyAlreadyCanonical(key, systemKey, fileName) {
|
|
47
|
+
if (!key || !systemKey) return false;
|
|
48
|
+
if (fileName && isFilenameAlreadyCanonical(fileName, systemKey)) {
|
|
49
|
+
const suffixFromFile = suffixFromCanonicalFilename(fileName, systemKey);
|
|
50
|
+
if (suffixFromFile && (key === suffixFromFile || key === `${systemKey}-${suffixFromFile}`)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!key.startsWith(systemKey + '-')) return false;
|
|
55
|
+
const suffix = key.slice(systemKey.length + 1);
|
|
56
|
+
if (!suffix) return false;
|
|
57
|
+
if (suffix.endsWith('-datasource')) return false;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Derives resourceType slug from key: strip systemKey prefix, then strip trailing -datasource if present.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} key - Current datasource key
|
|
65
|
+
* @param {string} systemKey - System key
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function slugFromKey(key, systemKey) {
|
|
69
|
+
if (!key || !systemKey || !key.startsWith(systemKey + '-')) return key || '';
|
|
70
|
+
let suffix = key.slice(systemKey.length + 1);
|
|
71
|
+
if (suffix.endsWith('-datasource')) suffix = suffix.slice(0, -'-datasource'.length);
|
|
72
|
+
return suffix || key;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns canonical filename for a datasource: <systemKey>-datasource-<suffix>.<ext>.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} canonicalKey - Canonical datasource key
|
|
79
|
+
* @param {string} systemKey - System key
|
|
80
|
+
* @param {string} ext - File extension including dot (e.g. .json)
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function canonicalDatasourceFilename(canonicalKey, systemKey, ext) {
|
|
84
|
+
const suffix = canonicalKey.startsWith(systemKey + '-')
|
|
85
|
+
? canonicalKey.slice(systemKey.length + 1)
|
|
86
|
+
: canonicalKey;
|
|
87
|
+
return `${systemKey}-datasource-${suffix}${ext}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns true if filename already matches canonical pattern <systemKey>-datasource-<suffix>.<ext>.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} fileName - Current filename
|
|
94
|
+
* @param {string} systemKey - System key
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
function isFilenameAlreadyCanonical(fileName, systemKey) {
|
|
98
|
+
const base = path.basename(fileName);
|
|
99
|
+
const ext = path.extname(fileName);
|
|
100
|
+
const withoutExt = base.slice(0, -ext.length);
|
|
101
|
+
const prefix = `${systemKey}-datasource-`;
|
|
102
|
+
if (!withoutExt.startsWith(prefix)) return false;
|
|
103
|
+
const suffix = withoutExt.slice(prefix.length);
|
|
104
|
+
if (!suffix || suffix.endsWith('-datasource')) return false;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Normalizes datasource keys and filenames to canonical form. Runs early in repair.
|
|
110
|
+
* Updates file contents (key property), renames files when needed, and updates variables.externalIntegration.dataSources.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} appPath - Application directory path
|
|
113
|
+
* @param {string[]} datasourceFiles - Current list of datasource filenames
|
|
114
|
+
* @param {string} systemKey - System key
|
|
115
|
+
* @param {Object} variables - Application variables (mutated: externalIntegration.dataSources updated)
|
|
116
|
+
* @param {boolean} dryRun - If true, do not write or rename
|
|
117
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
118
|
+
* @returns {{ updated: boolean, datasourceFiles: string[] }} Updated flag and new list of datasource filenames
|
|
119
|
+
*/
|
|
120
|
+
/* eslint-disable max-lines-per-function, max-statements, complexity -- Normalization loops and branching per file */
|
|
121
|
+
function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey, variables, dryRun, changes) {
|
|
122
|
+
if (!datasourceFiles || datasourceFiles.length === 0) {
|
|
123
|
+
return { updated: false, datasourceFiles: datasourceFiles || [] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const slugCounts = new Map();
|
|
127
|
+
const fileInfos = [];
|
|
128
|
+
|
|
129
|
+
for (const fileName of datasourceFiles) {
|
|
130
|
+
const filePath = path.join(appPath, fileName);
|
|
131
|
+
if (!fs.existsSync(filePath)) continue;
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = loadConfigFile(filePath);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
fileInfos.push({ fileName, key: null, skip: true });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const key = (parsed && typeof parsed.key === 'string' && parsed.key.trim()) ? parsed.key.trim() : null;
|
|
140
|
+
if (isKeyAlreadyCanonical(key, systemKey, fileName) && isFilenameAlreadyCanonical(fileName, systemKey)) {
|
|
141
|
+
fileInfos.push({ fileName, key, skip: true });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const slug = slugFromKey(key || fileName, systemKey);
|
|
145
|
+
fileInfos.push({
|
|
146
|
+
fileName,
|
|
147
|
+
parsed,
|
|
148
|
+
key,
|
|
149
|
+
slug,
|
|
150
|
+
canonicalKey: null,
|
|
151
|
+
skip: false
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const info of fileInfos) {
|
|
156
|
+
if (info.skip) continue;
|
|
157
|
+
const slug = info.slug;
|
|
158
|
+
const n = (slugCounts.get(slug) || 0) + 1;
|
|
159
|
+
slugCounts.set(slug, n);
|
|
160
|
+
info.canonicalKey = n === 1 ? `${systemKey}-${slug}` : `${systemKey}-${slug}-${n}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let updated = false;
|
|
164
|
+
const newDatasourceFiles = [];
|
|
165
|
+
|
|
166
|
+
for (const info of fileInfos) {
|
|
167
|
+
if (info.skip) {
|
|
168
|
+
newDatasourceFiles.push(info.fileName);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const { fileName, parsed, canonicalKey } = info;
|
|
172
|
+
const ext = path.extname(fileName);
|
|
173
|
+
const canonicalFileName = canonicalDatasourceFilename(canonicalKey, systemKey, ext);
|
|
174
|
+
|
|
175
|
+
if (parsed.key !== canonicalKey) {
|
|
176
|
+
parsed.key = canonicalKey;
|
|
177
|
+
if (!dryRun) writeConfigFile(path.join(appPath, fileName), parsed);
|
|
178
|
+
changes.push(`${fileName}: key → ${canonicalKey}`);
|
|
179
|
+
updated = true;
|
|
180
|
+
}
|
|
181
|
+
if (fileName !== canonicalFileName) {
|
|
182
|
+
const oldPath = path.join(appPath, fileName);
|
|
183
|
+
const newPath = path.join(appPath, canonicalFileName);
|
|
184
|
+
if (!dryRun && fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
|
|
185
|
+
fs.renameSync(oldPath, newPath);
|
|
186
|
+
}
|
|
187
|
+
changes.push(`Renamed ${fileName} → ${canonicalFileName}`);
|
|
188
|
+
updated = true;
|
|
189
|
+
newDatasourceFiles.push(canonicalFileName);
|
|
190
|
+
} else {
|
|
191
|
+
newDatasourceFiles.push(fileName);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (updated && variables.externalIntegration && Array.isArray(variables.externalIntegration.dataSources)) {
|
|
196
|
+
variables.externalIntegration.dataSources = [...newDatasourceFiles].sort();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { updated, datasourceFiles: newDatasourceFiles };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
normalizeDatasourceKeysAndFilenames,
|
|
204
|
+
isKeyAlreadyCanonical,
|
|
205
|
+
slugFromKey,
|
|
206
|
+
canonicalDatasourceFilename,
|
|
207
|
+
isFilenameAlreadyCanonical
|
|
208
|
+
};
|