@aifabrix/builder 2.60.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/api/configuration.api.js +131 -0
- package/lib/api/system-secrets.api.js +72 -0
- 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/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +2 -2
- package/lib/cli/setup-app.js +5 -0
- package/lib/cli/setup-environment.js +156 -0
- 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/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 +5 -1
- package/lib/core/secrets-env-content.js +5 -1
- 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/generator/index.js +3 -0
- 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-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 +14 -2
- package/lib/schema/external-datasource.schema.json +23 -3
- package/lib/schema/infra.parameter.yaml +388 -38
- package/lib/utils/compose-generate-docker-compose.js +7 -12
- 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/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/prepare-local-data-mount.js +58 -0
- 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 +4 -3
- package/templates/agent-kit/agent-kit.yaml +1 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
- package/templates/agent-kit/skills/shared/interaction.md +83 -0
- package/templates/agent-kit/skills/shared/status.md +3 -12
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/builder-api/env.template +5 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +10 -12
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/miso-controller/application.yaml +74 -2
- package/templates/applications/miso-controller/env.template +42 -3
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/main.json +4 -4
- package/templates/python/docker-compose.hbs +1 -1
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
- /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,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
|
+
};
|
|
@@ -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,7 +79,7 @@ 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
84
|
// `build.target` selects a stage of a multi-stage Dockerfile, so one source tree can
|
|
85
85
|
// produce more than one image. Absent, Docker builds the last stage as before.
|
|
@@ -92,7 +92,7 @@ async function executeDockerImageBuild(appName, options, buildConfig) {
|
|
|
92
92
|
primary,
|
|
93
93
|
{ ...options, buildArgs, tagAliases: aliases, target }
|
|
94
94
|
);
|
|
95
|
-
await postBuildTasks(appName, buildConfig);
|
|
95
|
+
await postBuildTasks(appName, buildConfig, options);
|
|
96
96
|
logger.log(formatSuccessParagraph('Build completed successfully!'));
|
|
97
97
|
return tags.map((tag) => `${effectiveImageName}:${tag}`).join(', ');
|
|
98
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) => {
|
|
@@ -118,6 +118,79 @@ Typical workflow:
|
|
|
118
118
|
|
|
119
119
|
Subcommands:
|
|
120
120
|
deploy <env> Provision/update environment in Miso Controller (see env deploy --help)
|
|
121
|
+
secret Installation configuration on the Controller (push, list)
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
const SECRET_GROUP_HELP_AFTER = `
|
|
125
|
+
Installation configuration held by the Controller. Push updates declared variables and secrets;
|
|
126
|
+
list reads them for one application or every authorized application.
|
|
127
|
+
|
|
128
|
+
Scope, compared with the other two stores:
|
|
129
|
+
aifabrix secret your own local secrets.local.yaml (and the shared store)
|
|
130
|
+
aifabrix credential a Connected System's credentials on the Dataplane
|
|
131
|
+
aifabrix env secret this installation's secrets on the Controller
|
|
132
|
+
|
|
133
|
+
Secret values are write-only: they can be set and never read back. Listing shows values only
|
|
134
|
+
for rows the Controller authoritatively classifies as non-sensitive variables.
|
|
135
|
+
|
|
136
|
+
The environment is a parameter. Omit it to use the one in config.yaml
|
|
137
|
+
(aifabrix auth set-environment <env>).
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
$ aifabrix env secret list --app builder-api
|
|
141
|
+
$ aifabrix env secret list dev --app mori-controller
|
|
142
|
+
$ aifabrix env secret push --app mori-controller
|
|
143
|
+
$ af env secret push miso --app mori-controller --dry-run
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
const SECRET_PUSH_HELP_AFTER = `
|
|
147
|
+
Reads values from the application's ignored .env and matches each variable name to the
|
|
148
|
+
Controller's declaration. Non-sensitive variables use configuration PUT; secrets use the
|
|
149
|
+
protected system-secret PUT. Undeclared .env names are skipped.
|
|
150
|
+
|
|
151
|
+
Secrets: pass paths only. The value is read from the file and never taken from the command
|
|
152
|
+
line, so it cannot reach shell history or a process list. No value is printed or logged on
|
|
153
|
+
any path, including failure.
|
|
154
|
+
|
|
155
|
+
Environment: omit to use the configured one, or name it explicitly.
|
|
156
|
+
--key is the variable name from .env, not the Controller declaration key.
|
|
157
|
+
Without --key, an interactive terminal asks before pushing all declared values. Automation
|
|
158
|
+
and AI usage must pass --yes explicitly. Dry-run never prompts and never writes.
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
Preview one value from mori-controller's default .env, then push it to dev:
|
|
162
|
+
$ aifabrix env secret push dev --app mori-controller --key NOTIFICATION_EVENT_WEBHOOK_SECRET --dry-run
|
|
163
|
+
$ aifabrix env secret push dev --app mori-controller --key NOTIFICATION_EVENT_WEBHOOK_SECRET
|
|
164
|
+
|
|
165
|
+
Preview every declared configuration value found in the app's default .env:
|
|
166
|
+
$ aifabrix env secret push dev --app mori-controller --dry-run
|
|
167
|
+
|
|
168
|
+
Push every declared value after reviewing the dry run (interactive confirmation):
|
|
169
|
+
$ aifabrix env secret push dev --app mori-controller
|
|
170
|
+
|
|
171
|
+
Push every declared value non-interactively after explicit approval:
|
|
172
|
+
$ aifabrix env secret push dev --app mori-controller --yes
|
|
173
|
+
|
|
174
|
+
Preview values from a different ignored env file:
|
|
175
|
+
$ af env secret push dev --app mori-controller --file ./path/to/ignored.env --dry-run
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
const SECRET_LIST_HELP_AFTER = `
|
|
179
|
+
Shows declared configuration in a compact table. Without --app, the Application column
|
|
180
|
+
lists every authorized owner; with --app, only that exact owner is shown. Non-sensitive
|
|
181
|
+
variable values are shown. Secret values, references, vault names and ciphertext are never shown.
|
|
182
|
+
|
|
183
|
+
Environment: omit to use the configured one, or name it explicitly.
|
|
184
|
+
--app optionally identifies one exact owner; * means the literal wildcard owner.
|
|
185
|
+
--key searches that owner's secret key and environment-variable names by substring.
|
|
186
|
+
The table has Status, Name, Type and Value headers (plus Application without --app).
|
|
187
|
+
Rows use ✔ for configured, ⚠ for required missing, and ⏭ for optional unset.
|
|
188
|
+
|
|
189
|
+
Examples:
|
|
190
|
+
$ aifabrix env secret list dev
|
|
191
|
+
$ aifabrix env secret list --app mori-controller
|
|
192
|
+
$ aifabrix env secret list dev --app builder-api
|
|
193
|
+
$ aifabrix env secret list --app mori-controller --key policy
|
|
121
194
|
`;
|
|
122
195
|
|
|
123
196
|
function setupEnvironmentCommands(program) {
|
|
@@ -136,6 +209,89 @@ function setupEnvironmentCommands(program) {
|
|
|
136
209
|
.option('--no-poll', 'Do not poll for status'))
|
|
137
210
|
.addHelpText('after', DEPLOY_EXAMPLES)
|
|
138
211
|
.action(deployEnvHandler);
|
|
212
|
+
|
|
213
|
+
setupEnvSecretCommands(env);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {string|undefined} envKey - Positional environment
|
|
218
|
+
* @param {Object} options - CLI options
|
|
219
|
+
* @returns {Promise<void>} Exits non-zero on failure
|
|
220
|
+
*/
|
|
221
|
+
async function envSecretPushHandler(envKey, options) {
|
|
222
|
+
try {
|
|
223
|
+
const { resolveEnvSecretContext, resolveAppPaths } = require('../commands/env-secret-context');
|
|
224
|
+
const { runEnvSecretPush } = require('../commands/env-secret-push-run');
|
|
225
|
+
const { confirmBulkSecretPush } = require('../commands/env-secret-push-confirm');
|
|
226
|
+
const base = await resolveEnvSecretContext(envKey, options);
|
|
227
|
+
const paths = resolveAppPaths(options.app, options);
|
|
228
|
+
const confirmed = await confirmBulkSecretPush(base, options);
|
|
229
|
+
if (!confirmed) return;
|
|
230
|
+
const { exitCode } = await runEnvSecretPush({ ...base, ...paths }, {
|
|
231
|
+
key: options.key,
|
|
232
|
+
application: options.app,
|
|
233
|
+
dryRun: options.dryRun === true
|
|
234
|
+
});
|
|
235
|
+
if (exitCode !== 0) {
|
|
236
|
+
process.exit(exitCode);
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
handleCommandError(error, 'env secret push');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @param {string|undefined} envKey - Positional environment
|
|
246
|
+
* @param {Object} options - CLI options
|
|
247
|
+
* @returns {Promise<void>} Exits non-zero on failure
|
|
248
|
+
*/
|
|
249
|
+
async function envSecretListHandler(envKey, options) {
|
|
250
|
+
try {
|
|
251
|
+
const { resolveEnvSecretContext } = require('../commands/env-secret-context');
|
|
252
|
+
const { runEnvSecretList } = require('../commands/env-secret-list');
|
|
253
|
+
const ctx = await resolveEnvSecretContext(envKey, options);
|
|
254
|
+
const { exitCode } = await runEnvSecretList(ctx, { key: options.key, application: options.app });
|
|
255
|
+
if (exitCode !== 0) {
|
|
256
|
+
process.exit(exitCode);
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
handleCommandError(error, 'env secret list');
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* `aifabrix env secret` — installation secrets on the Controller.
|
|
266
|
+
* @param {import('commander').Command} env - The env command group
|
|
267
|
+
* @returns {void}
|
|
268
|
+
*/
|
|
269
|
+
function setupEnvSecretCommands(env) {
|
|
270
|
+
const secret = env
|
|
271
|
+
.command('secret')
|
|
272
|
+
.description('Installation configuration on the Controller (secret values remain write-only)')
|
|
273
|
+
.addHelpText('after', SECRET_GROUP_HELP_AFTER);
|
|
274
|
+
|
|
275
|
+
// The API takes the environment in the path, so both commands accept it; omitted, it falls
|
|
276
|
+
// back to the one in config.yaml. Push and list have distinct resource contracts.
|
|
277
|
+
secret
|
|
278
|
+
.command('push [env]')
|
|
279
|
+
.description('Push declared configuration from an app env file (values are never arguments)')
|
|
280
|
+
.option('--app <appKey>', 'Exact application owner of the configuration declarations')
|
|
281
|
+
.option('--file <path>', 'Env file to read values from (default: the app\'s .env)')
|
|
282
|
+
.option('--key <variable>', 'Push one .env variable name')
|
|
283
|
+
.option('--dry-run', 'Show what would be pushed and write nothing')
|
|
284
|
+
.option('-y, --yes', 'Confirm bulk push without prompting (automation / AI usage)')
|
|
285
|
+
.addHelpText('after', SECRET_PUSH_HELP_AFTER)
|
|
286
|
+
.action(envSecretPushHandler);
|
|
287
|
+
|
|
288
|
+
secret
|
|
289
|
+
.command('list [env]')
|
|
290
|
+
.description('List configuration types, safe values and status by application')
|
|
291
|
+
.option('--app <appKey>', 'Only this exact application owner (* is the wildcard owner)')
|
|
292
|
+
.option('--key <text>', 'Search key and environment-variable names')
|
|
293
|
+
.addHelpText('after', SECRET_LIST_HELP_AFTER)
|
|
294
|
+
.action(envSecretListHandler);
|
|
139
295
|
}
|
|
140
296
|
|
|
141
297
|
module.exports = { setupEnvironmentCommands, deployEnvHandler, addMarketplaceOptions };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `datasource capability upsert` command registration.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Explicit upsert candidate authoring CLI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
const {
|
|
11
|
+
formatBlockingError,
|
|
12
|
+
headerKeyValue,
|
|
13
|
+
infoLine,
|
|
14
|
+
formatWarningLine,
|
|
15
|
+
formatNextActions
|
|
16
|
+
} = require('../utils/cli-test-layout-chalk');
|
|
17
|
+
const { runCapabilityUpsert } = require('../datasource/capability/run-capability-upsert');
|
|
18
|
+
const { printCapabilitySuccessFooter } = require('./datasource-capability-output');
|
|
19
|
+
|
|
20
|
+
const HELP = `
|
|
21
|
+
Authors a manifest-declared upsert candidate only when create, update, business
|
|
22
|
+
identity, and an exact vendor lookup are already declared. This is static
|
|
23
|
+
authoring eligibility, not live readiness certification.
|
|
24
|
+
|
|
25
|
+
$ aifabrix datasource capability upsert my-datasource --dry-run --json
|
|
26
|
+
$ aifabrix datasource capability upsert ./datasource.yaml --identity domain
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
function printHumanResult(result) {
|
|
30
|
+
if (result.dryRun) {
|
|
31
|
+
logger.log(infoLine('Dry run — planned JSON Patch operations:'));
|
|
32
|
+
logger.log('');
|
|
33
|
+
logger.log(JSON.stringify(result.patchOperations, null, 2));
|
|
34
|
+
} else {
|
|
35
|
+
if (result.backupPath) logger.log(headerKeyValue('Backup:', result.backupPath));
|
|
36
|
+
printCapabilitySuccessFooter(result.resolvedPath, result.updatedSections);
|
|
37
|
+
}
|
|
38
|
+
logger.log(formatWarningLine('Candidate authored — live readiness is not yet verified'));
|
|
39
|
+
logger.log(formatNextActions(result.nextActions));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printUpsertIssues(issues) {
|
|
43
|
+
(issues || []).forEach((issue) => {
|
|
44
|
+
const code = issue.code ? `[${issue.code}] ` : '';
|
|
45
|
+
logger.log(formatWarningLine(`${code}${issue.message}`));
|
|
46
|
+
const details = [
|
|
47
|
+
['Consequence', issue.consequence],
|
|
48
|
+
['Can AI fix?', issue.aiFixable === true ? 'Yes, with an authorized local manifest change' : 'No automatic fix is safe'],
|
|
49
|
+
['Safe action', issue.remediation],
|
|
50
|
+
['Prevention', issue.prevention],
|
|
51
|
+
['Residual risk', issue.residualRisk]
|
|
52
|
+
];
|
|
53
|
+
details.forEach(([label, value]) => {
|
|
54
|
+
const values = Array.isArray(value) ? value : [value];
|
|
55
|
+
values.filter(Boolean).forEach((item) => logger.log(` ${label}: ${item}`));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runUpsertAction(fileOrKey, options) {
|
|
61
|
+
const result = await runCapabilityUpsert({
|
|
62
|
+
fileOrKey,
|
|
63
|
+
as: options.as,
|
|
64
|
+
create: options.create,
|
|
65
|
+
update: options.update,
|
|
66
|
+
identity: options.identity,
|
|
67
|
+
test: Boolean(options.test),
|
|
68
|
+
dryRun: Boolean(options.dryRun),
|
|
69
|
+
overwrite: Boolean(options.overwrite),
|
|
70
|
+
noBackup: Boolean(options.noBackup)
|
|
71
|
+
});
|
|
72
|
+
if (options.json) {
|
|
73
|
+
logger.log(JSON.stringify(result, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
printHumanResult(result);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setupCapabilityUpsertCommand(cap) {
|
|
80
|
+
cap.command('upsert <file-or-key>')
|
|
81
|
+
.description('Author an upsert candidate from declared identity and exact OpenAPI coverage')
|
|
82
|
+
.option('--as <key>', 'Target capability key', 'upsert')
|
|
83
|
+
.option('--create <key>', 'Existing create-shaped capability', 'create')
|
|
84
|
+
.option('--update <key>', 'Existing update-shaped capability', 'update')
|
|
85
|
+
.option('--identity <fields>', 'Comma-separated mapped business identity fields')
|
|
86
|
+
.option('--test', 'Add proof scaffolding when existing safe fixture facts are sufficient')
|
|
87
|
+
.option('--dry-run', 'Print JSON Patch operations; do not write')
|
|
88
|
+
.option('--overwrite', 'Replace only the target upsert slice after full analysis')
|
|
89
|
+
.option('--no-backup', 'Skip backup copy under integration/<app>/backup/')
|
|
90
|
+
.option('--json', 'Print a machine-readable result envelope')
|
|
91
|
+
.addHelpText('after', HELP)
|
|
92
|
+
.action(async(fileOrKey, options) => {
|
|
93
|
+
try {
|
|
94
|
+
await runUpsertAction(fileOrKey, options);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (options.json) {
|
|
97
|
+
logger.log(JSON.stringify({ eligible: false, issues: error.analysis?.issues || [], error: error.message }, null, 2));
|
|
98
|
+
} else {
|
|
99
|
+
logger.error(formatBlockingError(`capability upsert failed: ${error.message}`));
|
|
100
|
+
printUpsertIssues(error.analysis?.issues);
|
|
101
|
+
}
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
setupCapabilityUpsertCommand,
|
|
109
|
+
runUpsertAction,
|
|
110
|
+
printHumanResult,
|
|
111
|
+
printUpsertIssues
|
|
112
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Datasource capability subcommands (copy,
|
|
2
|
+
* Datasource capability subcommands (copy, create, upsert, remove, diff, edit, validate).
|
|
3
3
|
*
|
|
4
4
|
* @fileoverview Nested `aifabrix datasource capability` CLI (copy, relate, …)
|
|
5
5
|
* @author AI Fabrix Team
|
|
@@ -26,6 +26,7 @@ const pathMod = require('path');
|
|
|
26
26
|
const { printCapabilitySuccessFooter } = require('./datasource-capability-output');
|
|
27
27
|
const { setupCapabilityRelateCommand } = require('./datasource-capability-relate-cli');
|
|
28
28
|
const { setupCapabilityDimensionCommand } = require('./datasource-capability-dimension-cli');
|
|
29
|
+
const { setupCapabilityUpsertCommand } = require('./datasource-capability-upsert-cli');
|
|
29
30
|
|
|
30
31
|
const CAP_COPY_HELP = `
|
|
31
32
|
Examples:
|
|
@@ -386,7 +387,7 @@ function setupDatasourceCapabilityCommands(datasource) {
|
|
|
386
387
|
const cap = datasource
|
|
387
388
|
.command('capability')
|
|
388
389
|
.description(
|
|
389
|
-
'Copy, remove, relate
|
|
390
|
+
'Copy, create, upsert, remove, relate, diff, edit, or validate datasource capability slices'
|
|
390
391
|
);
|
|
391
392
|
if (typeof cap.alias === 'function') {
|
|
392
393
|
cap.alias('cap');
|
|
@@ -395,6 +396,7 @@ function setupDatasourceCapabilityCommands(datasource) {
|
|
|
395
396
|
setupCapabilityCopyCommand(cap);
|
|
396
397
|
setupCapabilityRemoveCommand(cap);
|
|
397
398
|
setupCapabilityCreateCommand(cap);
|
|
399
|
+
setupCapabilityUpsertCommand(cap);
|
|
398
400
|
setupCapabilityRelateCommand(cap);
|
|
399
401
|
setupCapabilityDimensionCommand(cap);
|
|
400
402
|
setupCapabilityDiffCommand(cap);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve where `aifabrix env secret` reads from and writes to.
|
|
3
|
+
*
|
|
4
|
+
* The controller URL is resolved once and printed before any write, because pushing into
|
|
5
|
+
* the wrong installation is silent and unrecoverable — no value can be read back to show
|
|
6
|
+
* what was overwritten.
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview env secret: controller, environment and file resolution
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
18
|
+
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
19
|
+
const { normalizeControllerUrl, resolveEnvironment } = require('../core/config');
|
|
20
|
+
const { getAppPath, getIntegrationPath } = require('../utils/paths');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} controllerUrl - Raw controller URL
|
|
24
|
+
* @returns {Promise<Object>} Auth config carrying a bearer token
|
|
25
|
+
* @throws {Error} When no usable token is stored
|
|
26
|
+
*/
|
|
27
|
+
async function resolveAuth(controllerUrl) {
|
|
28
|
+
const normalized = normalizeControllerUrl(controllerUrl);
|
|
29
|
+
const deviceToken = await getOrRefreshDeviceToken(normalized);
|
|
30
|
+
if (deviceToken && deviceToken.token) {
|
|
31
|
+
return { token: deviceToken.token, controllerUrl: deviceToken.controller || normalized };
|
|
32
|
+
}
|
|
33
|
+
throw new Error('Not signed in to the Controller. Run: aifabrix login');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Locate the application directory and env file holding the values.
|
|
38
|
+
* @param {string} appKey - Application key
|
|
39
|
+
* @param {Object} options - `file` overrides the env file path
|
|
40
|
+
* @returns {{ appPath: string, envFilePath: string }} Resolved paths
|
|
41
|
+
* @throws {Error} When the application cannot be located
|
|
42
|
+
*/
|
|
43
|
+
function resolveAppPaths(appKey, options = {}) {
|
|
44
|
+
if (!appKey) {
|
|
45
|
+
throw new Error('--app is required: the catalog keys are read from that application env.template');
|
|
46
|
+
}
|
|
47
|
+
// A platform application lives under builder/<appKey>/; an external system under
|
|
48
|
+
// integration/<systemKey>/. Prefer the candidate holding the default .env, then an existing
|
|
49
|
+
// application directory. An explicit --file supplies values but does not change ownership.
|
|
50
|
+
const candidates = [safePath(getAppPath, appKey), safePath(getIntegrationPath, appKey)]
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
const appPath = candidates.find(dir => fs.existsSync(path.join(dir, '.env'))) ||
|
|
53
|
+
candidates.find(dir => fs.existsSync(dir)) || candidates[0];
|
|
54
|
+
if (!appPath) {
|
|
55
|
+
throw new Error(`Application not found: ${appKey}`);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
appPath,
|
|
59
|
+
envFilePath: options.file ? path.resolve(options.file) : path.join(appPath, '.env')
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {Function} resolver - Path helper
|
|
65
|
+
* @param {string} appKey - Application key
|
|
66
|
+
* @returns {string} Resolved path, or '' when the helper cannot resolve it
|
|
67
|
+
*/
|
|
68
|
+
function safePath(resolver, appKey) {
|
|
69
|
+
try {
|
|
70
|
+
return resolver(appKey) || '';
|
|
71
|
+
} catch {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build the context both subcommands share.
|
|
78
|
+
*
|
|
79
|
+
* The environment is the one recorded in config.yaml, not something a command accepts. It is
|
|
80
|
+
* changed deliberately with `aifabrix auth set-environment`, so a per-command override would
|
|
81
|
+
* let any string be treated as an environment and fail confusingly against the API.
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} [options] - CLI options
|
|
84
|
+
* @returns {Promise<Object>} controllerUrl, authConfig, envKey
|
|
85
|
+
*/
|
|
86
|
+
async function resolveEnvSecretContext(envKey, options = {}) {
|
|
87
|
+
const controllerUrl = await resolveControllerUrl();
|
|
88
|
+
if (!controllerUrl) {
|
|
89
|
+
throw new Error('No Controller URL configured. Run: aifabrix auth set-controller <url>');
|
|
90
|
+
}
|
|
91
|
+
const auth = await resolveAuth(controllerUrl);
|
|
92
|
+
// The API takes the environment in the path and accepts `*` for every installed
|
|
93
|
+
// environment. Omitted, it falls back to the one recorded in config.yaml.
|
|
94
|
+
const explicit = envKey === undefined || envKey === null ? '' : String(envKey).trim();
|
|
95
|
+
const resolvedEnv = explicit || await resolveEnvironment(options);
|
|
96
|
+
if (!resolvedEnv) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'No environment given and none configured. Pass one, use * for all, ' +
|
|
99
|
+
'or set a default with: aifabrix auth set-environment <env>'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
controllerUrl: auth.controllerUrl || controllerUrl,
|
|
104
|
+
authConfig: { token: auth.token },
|
|
105
|
+
envKey: resolvedEnv
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
resolveAuth,
|
|
111
|
+
resolveAppPaths,
|
|
112
|
+
resolveEnvSecretContext
|
|
113
|
+
};
|