@aifabrix/builder 2.59.0 → 2.61.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -11
- package/docs/README.md +80 -0
- package/docs/builder-help/evidence-patterns.json +155 -0
- package/docs/builder-help/golden-examples/crm-company.json +30 -0
- package/docs/builder-help/golden-examples/crm-deal.json +29 -0
- package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
- package/docs/builder-help/golden-examples/document-storage.json +30 -0
- package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
- package/docs/builder-help/golden-examples/repository-template.json +29 -0
- package/docs/builder-help/golden-examples/service-ticket.json +29 -0
- package/docs/builder-help/platform-roles.json +98 -0
- package/docs/builder-help/resource-type-catalog.json +402 -0
- package/lib/agent-kit/git-identity.js +180 -0
- package/lib/agent-kit/setup.js +3 -0
- package/lib/agent-kit/start.js +45 -1
- package/lib/api/configuration.api.js +131 -0
- package/lib/api/role-assistant-test-job.api.js +60 -0
- package/lib/api/system-secrets.api.js +72 -0
- package/lib/api/work-search.api.js +13 -6
- package/lib/app/deploy.js +8 -1
- package/lib/app/run-docker-fallback.js +6 -1
- package/lib/app/run-helpers.js +2 -1
- package/lib/app/run-parameter-sync.js +142 -0
- package/lib/app/show-display.js +1 -0
- package/lib/app/show-online.js +15 -0
- package/lib/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +6 -3
- package/lib/cli/setup-app.js +19 -1
- package/lib/cli/setup-environment.js +156 -0
- package/lib/cli/setup-utility.js +24 -1
- package/lib/commands/datasource-capability-upsert-cli.js +112 -0
- package/lib/commands/datasource-capability.js +4 -2
- package/lib/commands/env-secret-context.js +113 -0
- package/lib/commands/env-secret-list.js +200 -0
- package/lib/commands/env-secret-push-confirm.js +54 -0
- package/lib/commands/env-secret-push-run.js +227 -0
- package/lib/commands/env-secret-push.js +168 -0
- package/lib/commands/repair-datasource-apply.js +2 -0
- package/lib/commands/repair-datasource-keyed-document.js +122 -0
- package/lib/commands/repair-datasource-run.js +1 -0
- package/lib/commands/role-assistant.js +7 -0
- package/lib/commands/setup-modes.js +1 -8
- package/lib/commands/setup-prompts.js +2 -180
- package/lib/commands/verify-operations-skip-e2e.js +25 -1
- package/lib/commands/verify-operations-steps.js +13 -1
- package/lib/commands/wizard-config-normalizer.js +7 -4
- package/lib/commands/wizard-core.js +5 -157
- package/lib/commands/wizard-file-saving.js +163 -0
- package/lib/core/env-platform-expand.js +97 -0
- package/lib/core/secrets-env-content.js +42 -4
- package/lib/core/secrets-env-write.js +10 -3
- package/lib/core/secrets-load.js +4 -2
- package/lib/datasource/binary-documents-validator.js +190 -0
- package/lib/datasource/capability/run-capability-upsert.js +202 -0
- package/lib/datasource/capability/upsert-ingredients.js +291 -0
- package/lib/datasource/capability/upsert-operations.js +138 -0
- package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
- package/lib/datasource/validate.js +12 -5
- package/lib/deployment/installation/azure-infra-stage.js +3 -1
- package/lib/deployment/installation/infra-catalog.js +2 -5
- package/lib/generator/builders.js +17 -0
- package/lib/generator/helpers.js +23 -2
- package/lib/generator/index.js +13 -6
- package/lib/lifecycle/product-model.js +4 -3
- package/lib/lifecycle/report-display.js +3 -2
- package/lib/parameters/infra-parameter-catalog.js +1 -1
- package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
- package/lib/programmatic/builder-help-governance.js +1 -1
- package/lib/programmatic/builder-help.js +1 -1
- package/lib/role-assistant/test-cases-search.js +3 -0
- package/lib/role-assistant/test-job-runner.js +125 -0
- package/lib/role-assistant/test-runner-search.js +44 -2
- package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
- package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
- package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
- package/lib/role-assistant/test-runner-workhub.js +28 -19
- package/lib/schema/application-schema.json +205 -2
- package/lib/schema/external-datasource.schema.json +23 -3
- package/lib/schema/infra-parameter.schema.json +139 -33
- package/lib/schema/infra.parameter.yaml +416 -66
- package/lib/schema/infrastructure-schema.json +10 -35
- package/lib/utils/compose-generate-docker-compose.js +16 -9
- package/lib/utils/datasource-binary-evidence.js +92 -0
- package/lib/utils/datasource-test-run-capability-scope.js +44 -1
- package/lib/utils/datasource-test-run-debug-display.js +2 -0
- package/lib/utils/datasource-test-run-display.js +8 -2
- package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
- package/lib/utils/datasource-test-run-tty-log.js +2 -0
- package/lib/utils/docker-build.js +29 -8
- package/lib/utils/docker-manifest-public-port.js +36 -0
- package/lib/utils/env-copy.js +11 -10
- package/lib/utils/external-system-system-test-tty.js +3 -2
- package/lib/utils/image-tags.js +2 -2
- package/lib/utils/platform-kv-ref.js +1 -1
- package/lib/utils/platform-resolution.js +226 -0
- package/lib/utils/prepare-local-data-mount.js +58 -0
- package/lib/utils/resolve-docker-image-ref.js +8 -3
- package/lib/utils/secrets-helpers.js +0 -1
- package/lib/utils/system-secret-mapping.js +125 -0
- package/lib/utils/test-log-writer.js +2 -1
- package/lib/validation/external-manifest-validator.js +5 -0
- package/lib/validation/openapi-contract-surface-validator.js +3 -1
- package/lib/validation/validate-external-file.js +5 -1
- package/package.json +5 -4
- package/templates/README.md +2 -1
- package/templates/agent-kit/agent-kit.yaml +4 -1
- package/templates/agent-kit/instructions/AGENTKIT.md +1 -1
- package/templates/agent-kit/instructions/root.AGENTS.md +2 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +35 -2
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +6 -2
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +28 -2
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +28 -1
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
- package/templates/agent-kit/skills/shared/feedback.md +31 -0
- package/templates/agent-kit/skills/shared/hosts.md +13 -3
- package/templates/agent-kit/skills/shared/interaction.md +83 -0
- package/templates/agent-kit/skills/shared/status.md +76 -0
- package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/builder-api/env.template +5 -1
- package/templates/applications/dataplane/application.yaml +30 -2
- package/templates/applications/dataplane/env.template +36 -5
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/miso-controller/application.yaml +96 -1
- package/templates/applications/miso-controller/env.template +63 -37
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/main.json +73 -225
- package/templates/python/Dockerfile.hbs +2 -0
- package/templates/python/docker-compose.hbs +1 -1
- package/templates/typescript/Dockerfile.hbs +2 -0
- package/templates/typescript/docker-compose.hbs +12 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -34
- /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameter-backed installation configuration API.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Read safe configuration metadata from Miso Controller
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { ApiClient } = require('./index');
|
|
12
|
+
|
|
13
|
+
const PAGE_SIZE = 100;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} envKey - Environment key
|
|
17
|
+
* @returns {string} Configuration collection endpoint
|
|
18
|
+
*/
|
|
19
|
+
function configurationEndpoint(envKey) {
|
|
20
|
+
return `/api/v1/environments/${encodeURIComponent(envKey)}/configuration`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} meta - Pagination metadata
|
|
25
|
+
* @param {number} requestedPage - Requested one-based page
|
|
26
|
+
* @param {boolean} hasRows - Whether the page contains rows
|
|
27
|
+
* @returns {number} Validated total page count
|
|
28
|
+
*/
|
|
29
|
+
function validatePageMeta(meta, requestedPage, hasRows) {
|
|
30
|
+
const currentPage = Number(meta && meta.currentPage);
|
|
31
|
+
const totalPages = Number(meta && meta.totalPages);
|
|
32
|
+
if (!Number.isInteger(currentPage) || currentPage !== requestedPage) {
|
|
33
|
+
throw new Error('Configuration list response has invalid pagination metadata');
|
|
34
|
+
}
|
|
35
|
+
if (!Number.isInteger(totalPages) || totalPages < 0 || (hasRows && totalPages === 0)) {
|
|
36
|
+
throw new Error('Configuration list response has invalid pagination metadata');
|
|
37
|
+
}
|
|
38
|
+
return totalPages;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {Object} response - API response
|
|
43
|
+
* @param {number} requestedPage - Requested one-based page
|
|
44
|
+
* @returns {{ rows: Object[], totalPages: number }} Validated page
|
|
45
|
+
*/
|
|
46
|
+
function readConfigurationPage(response, requestedPage) {
|
|
47
|
+
if (response && response.success === false) {
|
|
48
|
+
throw new Error(response.formattedError || response.error || 'Configuration list request failed');
|
|
49
|
+
}
|
|
50
|
+
const body = response && response.data !== undefined ? response.data : response;
|
|
51
|
+
if (!body || !Array.isArray(body.data) || !body.meta) {
|
|
52
|
+
throw new Error('Configuration list response is not a paginated result');
|
|
53
|
+
}
|
|
54
|
+
const totalPages = validatePageMeta(body.meta, requestedPage, body.data.length > 0);
|
|
55
|
+
return { rows: body.data, totalPages };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* List configuration declarations, optionally for one exact owner.
|
|
60
|
+
*
|
|
61
|
+
* GET /api/v1/environments/{envKey}/configuration
|
|
62
|
+
* @requiresPermission {Controller} controller:monitor or admin:read
|
|
63
|
+
* @async
|
|
64
|
+
* @function listEnvironmentConfiguration
|
|
65
|
+
* @param {string} controllerUrl - Controller base URL
|
|
66
|
+
* @param {Object} authConfig - Authentication configuration
|
|
67
|
+
* @param {string} envKey - Environment key
|
|
68
|
+
* @param {string} [application] - Exact owning application key
|
|
69
|
+
* @param {Object} [options] - Optional safe search
|
|
70
|
+
* @param {string} [options.search] - Partial key/name search
|
|
71
|
+
* @returns {Promise<Object[]>} Safe configuration metadata rows
|
|
72
|
+
*/
|
|
73
|
+
async function listEnvironmentConfiguration(controllerUrl, authConfig, envKey, application, options = {}) {
|
|
74
|
+
if (!controllerUrl || typeof controllerUrl !== 'string') {
|
|
75
|
+
throw new Error('controllerUrl is required and must be a string');
|
|
76
|
+
}
|
|
77
|
+
const environment = typeof envKey === 'string' ? envKey.trim() : '';
|
|
78
|
+
const owner = typeof application === 'string' ? application.trim() : '';
|
|
79
|
+
if (!environment) {
|
|
80
|
+
throw new Error('envKey is required and must be a string');
|
|
81
|
+
}
|
|
82
|
+
const client = new ApiClient(controllerUrl, authConfig);
|
|
83
|
+
const common = {
|
|
84
|
+
pageSize: PAGE_SIZE,
|
|
85
|
+
sort: owner ? 'key' : 'application'
|
|
86
|
+
};
|
|
87
|
+
if (owner) common.application = owner;
|
|
88
|
+
if (options.search) common.search = String(options.search).trim();
|
|
89
|
+
const rows = [];
|
|
90
|
+
for (let page = 1;; page += 1) {
|
|
91
|
+
const response = await client.get(configurationEndpoint(environment), {
|
|
92
|
+
params: { ...common, page }
|
|
93
|
+
});
|
|
94
|
+
const result = readConfigurationPage(response, page);
|
|
95
|
+
rows.push(...result.rows);
|
|
96
|
+
if (page >= result.totalPages) return rows;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Update one authoritatively non-sensitive configuration declaration.
|
|
102
|
+
* @param {string} controllerUrl - Controller base URL
|
|
103
|
+
* @param {Object} authConfig - Authentication configuration
|
|
104
|
+
* @param {string} envKey - Environment key
|
|
105
|
+
* @param {string} application - Exact application owner
|
|
106
|
+
* @param {string} key - Configuration declaration key
|
|
107
|
+
* @param {Object} update - Value and concurrency revision
|
|
108
|
+
* @param {string} update.value - Non-sensitive value
|
|
109
|
+
* @param {string} update.revision - Opaque revision from the configuration list
|
|
110
|
+
* @returns {Promise<Object>} Safe updated metadata
|
|
111
|
+
*/
|
|
112
|
+
async function putEnvironmentConfiguration(
|
|
113
|
+
controllerUrl, authConfig, envKey, application, key, update
|
|
114
|
+
) {
|
|
115
|
+
const value = update && update.value;
|
|
116
|
+
const revision = update && update.revision;
|
|
117
|
+
const required = { controllerUrl, envKey, application, key, revision };
|
|
118
|
+
for (const [name, input] of Object.entries(required)) {
|
|
119
|
+
if (!input || typeof input !== 'string') throw new Error(`${name} is required`);
|
|
120
|
+
}
|
|
121
|
+
if (typeof value !== 'string') throw new Error('value must be a string');
|
|
122
|
+
const client = new ApiClient(controllerUrl, authConfig);
|
|
123
|
+
const endpoint = `${configurationEndpoint(envKey)}/${encodeURIComponent(application)}/` +
|
|
124
|
+
encodeURIComponent(key);
|
|
125
|
+
return await client.put(endpoint, { body: { value }, headers: { 'If-Match': revision } });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
listEnvironmentConfiguration,
|
|
130
|
+
putEnvironmentConfiguration
|
|
131
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dataplane asynchronous Role Assistant test job API.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Enqueue and poll published Role Assistant cases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { createDataplaneApiClient } = require('./index');
|
|
10
|
+
const { throwIfApiFailed } = require('./api-response-errors');
|
|
11
|
+
const { unwrapApiData } = require('../utils/external-system-readiness-core');
|
|
12
|
+
|
|
13
|
+
function jobPath(systemIdOrKey, suffix = '') {
|
|
14
|
+
const key = encodeURIComponent(String(systemIdOrKey || '').trim());
|
|
15
|
+
return `/api/v1/external/systems/${key}/role-assistant-tests/jobs${suffix}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function unwrapJob(res) {
|
|
19
|
+
const value = unwrapApiData(res);
|
|
20
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && value.data) {
|
|
21
|
+
return value.data;
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function enqueueRoleAssistantTestJob(
|
|
27
|
+
dataplaneUrl,
|
|
28
|
+
authConfig,
|
|
29
|
+
systemIdOrKey,
|
|
30
|
+
body
|
|
31
|
+
) {
|
|
32
|
+
const client = createDataplaneApiClient(dataplaneUrl, authConfig);
|
|
33
|
+
const path = jobPath(systemIdOrKey);
|
|
34
|
+
const res = await client.post(path, { body, timeoutMs: 60000 });
|
|
35
|
+
throwIfApiFailed(res, {
|
|
36
|
+
endpointUrl: `${String(dataplaneUrl || '').replace(/\/$/, '')}${path}`
|
|
37
|
+
});
|
|
38
|
+
return unwrapJob(res);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function getRoleAssistantTestJob(
|
|
42
|
+
dataplaneUrl,
|
|
43
|
+
authConfig,
|
|
44
|
+
systemIdOrKey,
|
|
45
|
+
jobId
|
|
46
|
+
) {
|
|
47
|
+
const suffix = `/${encodeURIComponent(String(jobId || '').trim())}`;
|
|
48
|
+
const client = createDataplaneApiClient(dataplaneUrl, authConfig);
|
|
49
|
+
const path = jobPath(systemIdOrKey, suffix);
|
|
50
|
+
const res = await client.get(path, { timeoutMs: 60000 });
|
|
51
|
+
throwIfApiFailed(res, {
|
|
52
|
+
endpointUrl: `${String(dataplaneUrl || '').replace(/\/$/, '')}${path}`
|
|
53
|
+
});
|
|
54
|
+
return unwrapJob(res);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
enqueueRoleAssistantTestJob,
|
|
59
|
+
getRoleAssistantTestJob
|
|
60
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System secrets API (Miso Controller).
|
|
3
|
+
*
|
|
4
|
+
* Deliberately narrow: a value can be written and never read back, and only against a
|
|
5
|
+
* catalog key the installation already defines. There is no create endpoint, so this module
|
|
6
|
+
* must not grow one — a key that does not exist is a catalog change in aifabrix-miso, not
|
|
7
|
+
* something the CLI can fix.
|
|
8
|
+
*
|
|
9
|
+
* The PUT request body marks `value` as writeOnly. Nothing here returns a value, and no
|
|
10
|
+
* caller should construct a response shape that could carry one.
|
|
11
|
+
*
|
|
12
|
+
* @fileoverview System secrets API functions (put-only over catalog keys)
|
|
13
|
+
* @author AI Fabrix Team
|
|
14
|
+
* @version 1.0.0
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { ApiClient } = require('./index');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} envKey - Environment key
|
|
23
|
+
* @returns {string} Collection endpoint for that environment
|
|
24
|
+
*/
|
|
25
|
+
function systemSecretsEndpoint(envKey) {
|
|
26
|
+
return `/api/v1/environments/${encodeURIComponent(envKey)}/system-secrets`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Set one system secret value. The value is write-only: it cannot be read back, and this
|
|
31
|
+
* function returns the server response unchanged without echoing what was sent.
|
|
32
|
+
*
|
|
33
|
+
* PUT /api/v1/environments/{envKey}/system-secrets/{catalogKey}
|
|
34
|
+
* @requiresPermission {Miso} controller:admin
|
|
35
|
+
* @async
|
|
36
|
+
* @function putSystemSecret
|
|
37
|
+
* @param {string} controllerUrl - Controller base URL
|
|
38
|
+
* @param {Object} authConfig - Authentication configuration
|
|
39
|
+
* @param {string} envKey - Environment key
|
|
40
|
+
* @param {string} catalogKey - Catalog key the installation already defines
|
|
41
|
+
* @param {string} value - Secret value, read from a file by the caller
|
|
42
|
+
* @param {string} revision - Opaque revision returned by the configuration list
|
|
43
|
+
* @returns {Promise<Object>} Server response; carries no value
|
|
44
|
+
* @throws {Error} If the request fails. The message must never include the value
|
|
45
|
+
*/
|
|
46
|
+
async function putSystemSecret(controllerUrl, authConfig, envKey, catalogKey, value, revision) {
|
|
47
|
+
if (!controllerUrl || typeof controllerUrl !== 'string') {
|
|
48
|
+
throw new Error('controllerUrl is required and must be a string');
|
|
49
|
+
}
|
|
50
|
+
if (!envKey || typeof envKey !== 'string') {
|
|
51
|
+
throw new Error('envKey is required and must be a string');
|
|
52
|
+
}
|
|
53
|
+
if (!catalogKey || typeof catalogKey !== 'string') {
|
|
54
|
+
throw new Error('catalogKey is required and must be a string');
|
|
55
|
+
}
|
|
56
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
57
|
+
// Names the key, never the value, because this message reaches the terminal.
|
|
58
|
+
throw new Error(`No value supplied for ${catalogKey}`);
|
|
59
|
+
}
|
|
60
|
+
if (!revision || typeof revision !== 'string') {
|
|
61
|
+
throw new Error(`No configuration revision supplied for ${catalogKey}`);
|
|
62
|
+
}
|
|
63
|
+
const client = new ApiClient(controllerUrl, authConfig);
|
|
64
|
+
return await client.put(`${systemSecretsEndpoint(envKey)}/${encodeURIComponent(catalogKey)}`, {
|
|
65
|
+
body: { value },
|
|
66
|
+
headers: { 'If-Match': revision }
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
putSystemSecret
|
|
72
|
+
};
|
|
@@ -121,16 +121,23 @@ async function getWorkRoles(dataplaneUrl, authConfig) {
|
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* GET-equivalent work.skills for one roleRef.
|
|
124
|
-
*
|
|
125
|
-
*
|
|
124
|
+
* Defaults to detail=full so search.resourceTypes (executable grammar) is present.
|
|
125
|
+
* A caller may request progressive resource/operation detail for skills-only checks.
|
|
126
126
|
* @requiresPermission {Dataplane} Authenticated (oauth2: [])
|
|
127
127
|
* @async
|
|
128
128
|
*/
|
|
129
|
-
async function getWorkSkills(dataplaneUrl, authConfig, roleRef) {
|
|
130
|
-
|
|
129
|
+
async function getWorkSkills(dataplaneUrl, authConfig, roleRef, options = {}) {
|
|
130
|
+
const args = {
|
|
131
131
|
roleRef: String(roleRef || '').trim(),
|
|
132
|
-
detail: 'full'
|
|
133
|
-
}
|
|
132
|
+
detail: String(options.detail || 'full').trim()
|
|
133
|
+
};
|
|
134
|
+
if (typeof options.resourceType === 'string' && options.resourceType.trim()) {
|
|
135
|
+
args.resourceType = options.resourceType.trim();
|
|
136
|
+
}
|
|
137
|
+
if (typeof options.operation === 'string' && options.operation.trim()) {
|
|
138
|
+
args.operation = options.operation.trim();
|
|
139
|
+
}
|
|
140
|
+
return executeLayerBTool(dataplaneUrl, authConfig, LAYER_B_TOOLS.skills, args);
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
/**
|
package/lib/app/deploy.js
CHANGED
|
@@ -268,7 +268,14 @@ async function executeStandardDeployment(appName, options) {
|
|
|
268
268
|
const controllerUrl = config.controllerUrl || 'unknown';
|
|
269
269
|
const appExists = await checkApplicationExists(appName, controllerUrl, config.envKey, config.auth);
|
|
270
270
|
|
|
271
|
-
|
|
271
|
+
// A controller deployment ships tokens plus the platforms/environments sections, so the
|
|
272
|
+
// Controller resolves them per installation and can promote dev -> tst without regenerating.
|
|
273
|
+
// Local runs and an explicit --resource-group resolve here instead.
|
|
274
|
+
const resolveHere = Boolean(options.local || options.resourceGroup);
|
|
275
|
+
const { manifest, manifestPath } = await generateAndValidateManifest(appName, {
|
|
276
|
+
...options,
|
|
277
|
+
...(resolveHere ? { target: { environment: config.envKey, resourceGroup: options.resourceGroup } } : {})
|
|
278
|
+
});
|
|
272
279
|
applyManifestOverrides(manifest, options);
|
|
273
280
|
validateImageIsPullable(manifest.image, appName);
|
|
274
281
|
displayDeploymentInfo(manifest, manifestPath);
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
const chalk = require('chalk');
|
|
11
|
+
const { prepareLocalDataMount } = require('../utils/prepare-local-data-mount');
|
|
12
|
+
const { resolveLocalDataMountPathForCompose } = require('../utils/compose-generate-docker-compose');
|
|
11
13
|
const logger = require('../utils/logger');
|
|
12
14
|
const { execWithDockerEnv } = require('../utils/docker-exec');
|
|
13
15
|
const { getContainerPort } = require('../utils/port-resolver');
|
|
@@ -87,7 +89,10 @@ async function executeDockerRunUp(o) {
|
|
|
87
89
|
];
|
|
88
90
|
|
|
89
91
|
if (needsStorage) {
|
|
90
|
-
|
|
92
|
+
const localPath = resolveLocalDataMountPathForCompose({ appName, appConfig });
|
|
93
|
+
if (localPath) prepareLocalDataMount(localPath, runEnvPath);
|
|
94
|
+
const source = localPath || storageVolumeName(appName, appConfig.developerId);
|
|
95
|
+
parts.push(`-v ${shellSingleQuote(source)}:/mnt/data`);
|
|
91
96
|
}
|
|
92
97
|
if (runEnvPath && typeof runEnvPath === 'string') {
|
|
93
98
|
parts.push(`--env-file ${shellSingleQuote(runEnvPath)}`);
|
package/lib/app/run-helpers.js
CHANGED
|
@@ -368,6 +368,8 @@ async function prepareEnvironment(appName, appConfig, options) {
|
|
|
368
368
|
developerId,
|
|
369
369
|
{ runEnvKey, effectiveEnvironmentScopedResources }
|
|
370
370
|
);
|
|
371
|
+
const { hydrateMissingLocalParameters } = require('./run-parameter-sync');
|
|
372
|
+
await hydrateMissingLocalParameters(appName, runEnvPath);
|
|
371
373
|
|
|
372
374
|
const composeOptions = {
|
|
373
375
|
...options,
|
|
@@ -438,4 +440,3 @@ module.exports = {
|
|
|
438
440
|
displayRunStatus,
|
|
439
441
|
cleanApplicationsDir: runEnvCompose.cleanApplicationsDir
|
|
440
442
|
};
|
|
441
|
-
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hydrate missing application-owned parameters before a local governed run.
|
|
3
|
+
*
|
|
4
|
+
* Local/database controllers deliberately leave database generators to Builder. In
|
|
5
|
+
* client-credentials mode the application cannot start until those values are in its
|
|
6
|
+
* bootstrap snapshot, so Builder fills only unresolved declared rows through the
|
|
7
|
+
* application's own client token. Existing Controller values are never overwritten.
|
|
8
|
+
*
|
|
9
|
+
* @fileoverview Local run parameter hydration for governed bootstrap
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs').promises;
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { ApiClient } = require('../api');
|
|
17
|
+
const { getToken } = require('../api/auth.api');
|
|
18
|
+
const { getControllerDeploymentType } = require('../api/controller-health.api');
|
|
19
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
20
|
+
const { getBuilderPath } = require('../utils/paths');
|
|
21
|
+
const { mapEnvToCatalogKeys, parseEnvAssignments } = require('../utils/system-secret-mapping');
|
|
22
|
+
const { getInfraParameterCatalog } = require('../parameters/infra-parameter-catalog');
|
|
23
|
+
|
|
24
|
+
const LOCAL_DEPLOYMENTS = new Set(['local', 'database']);
|
|
25
|
+
const IDENTITY_NAMES = new Set([
|
|
26
|
+
'MISO_CLIENTID',
|
|
27
|
+
'MISO_CLIENT_ID',
|
|
28
|
+
'MISO_CLIENTSECRET',
|
|
29
|
+
'MISO_CLIENT_SECRET'
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function statusOf(response) {
|
|
33
|
+
const value = response && (response.status || response.statusCode || response.response?.status);
|
|
34
|
+
return Number.isInteger(value) ? value : 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function failed(response) {
|
|
38
|
+
return Boolean(response && (response.success === false || statusOf(response) >= 400));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tokenFrom(response) {
|
|
42
|
+
const body = response && response.data !== undefined ? response.data : response;
|
|
43
|
+
const payload = body && body.data !== undefined ? body.data : body;
|
|
44
|
+
const token = payload && payload.token;
|
|
45
|
+
if (!token || typeof token !== 'string') {
|
|
46
|
+
throw new Error('Controller client-token response is invalid');
|
|
47
|
+
}
|
|
48
|
+
return token;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function credentialPair(env) {
|
|
52
|
+
const clientId = env.get('MISO_CLIENTID') || env.get('MISO_CLIENT_ID');
|
|
53
|
+
const clientSecret = env.get('MISO_CLIENTSECRET') || env.get('MISO_CLIENT_SECRET');
|
|
54
|
+
if (!clientId || !clientSecret) {
|
|
55
|
+
throw new Error('MISO client credentials are required for governed local configuration');
|
|
56
|
+
}
|
|
57
|
+
return { clientId, clientSecret };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function uniqueRows(mapping, catalog = getInfraParameterCatalog()) {
|
|
61
|
+
const rows = new Map();
|
|
62
|
+
for (const row of mapping.pushable) {
|
|
63
|
+
if (IDENTITY_NAMES.has(row.name)) continue;
|
|
64
|
+
const generatorType = catalog.findEntryForKey(row.catalogKey)?.generator?.type;
|
|
65
|
+
if (generatorType !== 'databaseUrl' && generatorType !== 'databasePassword') continue;
|
|
66
|
+
const prior = rows.get(row.catalogKey);
|
|
67
|
+
if (prior && prior.value !== row.value) {
|
|
68
|
+
throw new Error(`Conflicting local values resolve to parameter '${row.catalogKey}'`);
|
|
69
|
+
}
|
|
70
|
+
rows.set(row.catalogKey, row);
|
|
71
|
+
}
|
|
72
|
+
return [...rows.values()];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function isMissing(client, key) {
|
|
76
|
+
const response = await client.get(`/api/v1/parameters/${encodeURIComponent(key)}`);
|
|
77
|
+
if (!failed(response)) return false;
|
|
78
|
+
if (statusOf(response) === 404) return true;
|
|
79
|
+
throw new Error(`Unable to inspect governed parameter '${key}'`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function writeMissing(client, row) {
|
|
83
|
+
const response = await client.put(`/api/v1/parameters/${encodeURIComponent(row.catalogKey)}`, {
|
|
84
|
+
body: { value: row.value }
|
|
85
|
+
});
|
|
86
|
+
if (failed(response)) {
|
|
87
|
+
throw new Error(`Unable to initialize governed parameter '${row.catalogKey}'`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function resolveSyncContext(appName, runEnvPath, deps) {
|
|
92
|
+
const readFile = deps.readFile || fs.readFile;
|
|
93
|
+
const envContent = await readFile(runEnvPath, 'utf8');
|
|
94
|
+
const env = parseEnvAssignments(envContent);
|
|
95
|
+
if (env.get('MISO_AUTH_MODE') !== 'client-credentials') return null;
|
|
96
|
+
const controllerUrl = await (deps.resolveControllerUrl || resolveControllerUrl)();
|
|
97
|
+
if (!controllerUrl) throw new Error('Controller URL is required for governed local configuration');
|
|
98
|
+
const deploymentType = await (deps.getControllerDeploymentType || getControllerDeploymentType)(controllerUrl);
|
|
99
|
+
if (!LOCAL_DEPLOYMENTS.has(String(deploymentType || '').toLowerCase())) return null;
|
|
100
|
+
const templatePath = path.join((deps.getBuilderPath || getBuilderPath)(appName), 'env.template');
|
|
101
|
+
const templateContent = await readFile(templatePath, 'utf8');
|
|
102
|
+
const catalog = (deps.getInfraParameterCatalog || getInfraParameterCatalog)();
|
|
103
|
+
return {
|
|
104
|
+
controllerUrl,
|
|
105
|
+
credentials: credentialPair(env),
|
|
106
|
+
rows: uniqueRows(mapEnvToCatalogKeys(envContent, templateContent), catalog)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Initialize unresolved local parameter rows needed by a client-credentials snapshot.
|
|
112
|
+
* @param {string} appName Application key
|
|
113
|
+
* @param {string} runEnvPath Resolved app-only run env
|
|
114
|
+
* @param {Object} [deps] Test dependencies
|
|
115
|
+
* @returns {Promise<number>} Number of rows initialized
|
|
116
|
+
*/
|
|
117
|
+
async function hydrateMissingLocalParameters(appName, runEnvPath, deps = {}) {
|
|
118
|
+
const context = await resolveSyncContext(appName, runEnvPath, deps);
|
|
119
|
+
if (!context || context.rows.length === 0) return 0;
|
|
120
|
+
const response = await (deps.getToken || getToken)(
|
|
121
|
+
context.credentials.clientId,
|
|
122
|
+
context.credentials.clientSecret,
|
|
123
|
+
context.controllerUrl
|
|
124
|
+
);
|
|
125
|
+
const client = deps.client || new ApiClient(context.controllerUrl, {
|
|
126
|
+
type: 'client-token', token: tokenFrom(response)
|
|
127
|
+
});
|
|
128
|
+
let initialized = 0;
|
|
129
|
+
for (const row of context.rows) {
|
|
130
|
+
if (!(await isMissing(client, row.catalogKey))) continue;
|
|
131
|
+
await writeMissing(client, row);
|
|
132
|
+
initialized += 1;
|
|
133
|
+
}
|
|
134
|
+
return initialized;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
credentialPair,
|
|
139
|
+
hydrateMissingLocalParameters,
|
|
140
|
+
tokenFrom,
|
|
141
|
+
uniqueRows
|
|
142
|
+
};
|
package/lib/app/show-display.js
CHANGED
|
@@ -234,6 +234,7 @@ function logExternalSystemMain(ext) {
|
|
|
234
234
|
logger.log(` Version: ${ext.version ?? '—'}`);
|
|
235
235
|
logger.log(` Credential: ${formatCredentialShowLine(ext)}`);
|
|
236
236
|
logger.log(` Status: ${formatDataplaneStatusDisplay(ext.status)}`);
|
|
237
|
+
logger.log(` System link: ${ext.systemUrl ?? '—'}`);
|
|
237
238
|
logger.log(` API docs: ${ext.openApiDocsPageUrl ?? ext.apiDocumentUrl ?? '—'}`);
|
|
238
239
|
logger.log(` MCP server: ${ext.mcpServerUrl ?? '—'}`);
|
|
239
240
|
logger.log(` OpenAPI spec: ${ext.apiDocumentUrl ?? '—'}`);
|
package/lib/app/show-online.js
CHANGED
|
@@ -89,6 +89,7 @@ function buildOnlineSummaryFromDataplane(appKey, controllerUrl, dataplaneUrl, ex
|
|
|
89
89
|
|
|
90
90
|
async function finishOnlineShow(summary, appKey, json, permissionsOnly, verifyCert, authBundle) {
|
|
91
91
|
await enrichExternalShowSummary(summary, appKey, verifyCert, authBundle);
|
|
92
|
+
attachSystemUrl(summary);
|
|
92
93
|
attachRunDefaultsFromUserConfig(summary, appKey, await getConfig());
|
|
93
94
|
await attachDeclarativeUrlsToShowApplication(summary, appKey);
|
|
94
95
|
if (json) {
|
|
@@ -100,6 +101,20 @@ async function finishOnlineShow(summary, appKey, json, permissionsOnly, verifyCe
|
|
|
100
101
|
displayShow(summary, { permissionsOnly: !!permissionsOnly });
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Expose the Dataplane system browser route for humans and agents.
|
|
106
|
+
* Preserve the deployment base path (for example /data).
|
|
107
|
+
* @param {Object} summary - Online application summary
|
|
108
|
+
*/
|
|
109
|
+
function attachSystemUrl(summary) {
|
|
110
|
+
const ext = summary.externalSystem;
|
|
111
|
+
if (!summary.isExternal || !ext || ext.error || !ext.dataplaneUrl) return;
|
|
112
|
+
const baseUrl = ext.dataplaneUrl.replace(/\/+$/, '');
|
|
113
|
+
const systemKey = ext.systemKey || summary.appKey;
|
|
114
|
+
const section = /^(ra-|ek-)/.test(systemKey) ? 'enterprise-knowledge' : 'connected-systems';
|
|
115
|
+
ext.systemUrl = `${baseUrl}/${section}/${encodeURIComponent(systemKey)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
async function runOnlineFromDataplane(appKey, json, permissionsOnly, verifyCert, authConfig, authResult) {
|
|
104
119
|
const environment = await resolveEnvironment();
|
|
105
120
|
const dataplaneUrl = await resolveDataplaneUrl(authResult.actualControllerUrl, environment, authConfig);
|
|
@@ -52,10 +52,13 @@ function applyMisoControllerUiBuildArgs(buildArgs, envMap) {
|
|
|
52
52
|
*
|
|
53
53
|
* @param {string} appName
|
|
54
54
|
* @param {string} [contextPath] Docker build context (repo root for miso-controller)
|
|
55
|
+
* @param {Object} [options] - `runEnvKey`: environment key for manifest {NAME} resolution
|
|
56
|
+
* (defaults to the current CLI environment inside generateEnvContent)
|
|
55
57
|
* @returns {Promise<Object.<string, string>>}
|
|
56
58
|
*/
|
|
57
|
-
async function resolveDockerBuildArgsForApp(appName, contextPath) {
|
|
58
|
-
const
|
|
59
|
+
async function resolveDockerBuildArgsForApp(appName, contextPath, options = {}) {
|
|
60
|
+
const runEnvKey = options && options.runEnvKey ? String(options.runEnvKey) : undefined;
|
|
61
|
+
const envMap = await secretsEnvWrite.resolveAndGetEnvMap(appName, { environment: 'docker', runEnvKey });
|
|
59
62
|
const { getBashPrefixedProcessEnvOverlay } = require('../utils/bash-secret-env');
|
|
60
63
|
const bashOverlay = await getBashPrefixedProcessEnvOverlay(null, appName);
|
|
61
64
|
const buildArgs = {};
|
package/lib/build/index.js
CHANGED
|
@@ -191,12 +191,13 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
|
|
|
191
191
|
* @param {Object} options - Build options
|
|
192
192
|
*/
|
|
193
193
|
|
|
194
|
-
async function postBuildTasks(appName, buildConfig) {
|
|
194
|
+
async function postBuildTasks(appName, buildConfig, options = {}) {
|
|
195
195
|
try {
|
|
196
196
|
// Validate that env.template + secrets resolve cleanly, but never write <appPath>/.env or
|
|
197
197
|
// envOutputPath. Build args still flow through resolveAndGetEnvMap (in-memory). Run
|
|
198
198
|
// `aifabrix resolve <app>` to materialize an on-disk .env.
|
|
199
|
-
|
|
199
|
+
const runEnvKey = options && options.env ? String(options.env) : undefined;
|
|
200
|
+
await secrets.generateEnvFile(appName, buildConfig.secrets, 'docker', false, { noWrite: true, runEnvKey });
|
|
200
201
|
logger.log(formatSuccessLine('Env resolution validated (in-memory only; run "aifabrix resolve ' + appName + '" for on-disk .env)'));
|
|
201
202
|
} catch (error) {
|
|
202
203
|
logger.log(formatWarningLine(`Could not resolve env: ${error.message}`));
|
|
@@ -79,17 +79,20 @@ async function executeDockerImageBuild(appName, options, buildConfig) {
|
|
|
79
79
|
buildHelpers
|
|
80
80
|
);
|
|
81
81
|
await ensureRunSecretsForApp(appName);
|
|
82
|
-
const buildArgs = await resolveDockerBuildArgsForApp(appName, contextPath);
|
|
82
|
+
const buildArgs = await resolveDockerBuildArgsForApp(appName, contextPath, { runEnvKey: options.env });
|
|
83
83
|
const { primary, aliases, tags } = options.tagPlan;
|
|
84
|
+
// `build.target` selects a stage of a multi-stage Dockerfile, so one source tree can
|
|
85
|
+
// produce more than one image. Absent, Docker builds the last stage as before.
|
|
86
|
+
const target = buildConfig && buildConfig.target;
|
|
84
87
|
await dockerBuild.executeDockerBuildWithTag(
|
|
85
88
|
effectiveImageName,
|
|
86
89
|
imageName,
|
|
87
90
|
dockerfilePath,
|
|
88
91
|
contextPath,
|
|
89
92
|
primary,
|
|
90
|
-
{ ...options, buildArgs, tagAliases: aliases }
|
|
93
|
+
{ ...options, buildArgs, tagAliases: aliases, target }
|
|
91
94
|
);
|
|
92
|
-
await postBuildTasks(appName, buildConfig);
|
|
95
|
+
await postBuildTasks(appName, buildConfig, options);
|
|
93
96
|
logger.log(formatSuccessParagraph('Build completed successfully!'));
|
|
94
97
|
return tags.map((tag) => `${effectiveImageName}:${tag}`).join(', ');
|
|
95
98
|
}
|
package/lib/cli/setup-app.js
CHANGED
|
@@ -243,6 +243,10 @@ function registerBuildCommand(program) {
|
|
|
243
243
|
'Image tag or comma-separated tags (default: latest); {version} expands to app.version, e.g. "latest,{version}"'
|
|
244
244
|
)
|
|
245
245
|
.option('-r, --registry <registry>', 'Registry host for the image name (overrides dev set-registry and application.yaml)')
|
|
246
|
+
.option(
|
|
247
|
+
'-e, --env <env>',
|
|
248
|
+
'Environment key used to resolve manifest {NAME} values (dev|tst|pro); defaults to the current CLI environment'
|
|
249
|
+
)
|
|
246
250
|
.option(
|
|
247
251
|
'--base',
|
|
248
252
|
'Build/run the manifest base image (no -devN suffix). Default local build uses a developer-scoped repository when developer-id > 0.'
|
|
@@ -255,6 +259,7 @@ function registerBuildCommand(program) {
|
|
|
255
259
|
Examples:
|
|
256
260
|
$ aifabrix build myapp
|
|
257
261
|
$ aifabrix build myapp --tag v1.0.0 # also tags latest
|
|
262
|
+
$ aifabrix build myapp --env dev # resolve manifest {NAME} values for dev
|
|
258
263
|
$ aifabrix build myapp --base --from-dev --tag 'latest,{version}'
|
|
259
264
|
$ aifabrix build myapp --registry myregistry.azurecr.io`)
|
|
260
265
|
.action(async(appName, options) => {
|
|
@@ -362,7 +367,7 @@ function setupDockerfileGenerateCommand(program) {
|
|
|
362
367
|
}
|
|
363
368
|
|
|
364
369
|
function setupDeployCommand(program) {
|
|
365
|
-
program.command('deploy <appKey|systemKey>')
|
|
370
|
+
addDeployTargetOptions(program.command('deploy <appKey|systemKey>'))
|
|
366
371
|
.description('Deploy builder app or external system via Miso Controller (Azure or --local)')
|
|
367
372
|
.addHelpText('after', DEPLOY_HELP_AFTER)
|
|
368
373
|
.option('--local', 'Send manifest to controller then run app locally (app: same as aifabrix run <app>; external: restart dataplane)')
|
|
@@ -410,6 +415,19 @@ function setupDeployCommand(program) {
|
|
|
410
415
|
});
|
|
411
416
|
}
|
|
412
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Deployment-target options. A controller deployment normally ships {NAME} tokens plus the
|
|
420
|
+
* platforms/environments sections; these flags force local resolution instead.
|
|
421
|
+
* @param {Command} command - Commander command
|
|
422
|
+
* @returns {Command} The command
|
|
423
|
+
*/
|
|
424
|
+
function addDeployTargetOptions(command) {
|
|
425
|
+
return command.option(
|
|
426
|
+
'-g, --resource-group <name>',
|
|
427
|
+
'Target Azure resource group; resolves platforms values in the generated manifest instead of shipping tokens'
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
413
431
|
function setupPushDeployDockerfileCommands(program) {
|
|
414
432
|
program.command('push <appKey>')
|
|
415
433
|
.description('Push builder appKey image to Azure Container Registry')
|