@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.
Files changed (145) hide show
  1. package/README.md +14 -11
  2. package/docs/README.md +80 -0
  3. package/docs/builder-help/evidence-patterns.json +155 -0
  4. package/docs/builder-help/golden-examples/crm-company.json +30 -0
  5. package/docs/builder-help/golden-examples/crm-deal.json +29 -0
  6. package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
  7. package/docs/builder-help/golden-examples/document-storage.json +30 -0
  8. package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
  9. package/docs/builder-help/golden-examples/repository-template.json +29 -0
  10. package/docs/builder-help/golden-examples/service-ticket.json +29 -0
  11. package/docs/builder-help/platform-roles.json +98 -0
  12. package/docs/builder-help/resource-type-catalog.json +402 -0
  13. package/lib/agent-kit/git-identity.js +180 -0
  14. package/lib/agent-kit/setup.js +3 -0
  15. package/lib/agent-kit/start.js +45 -1
  16. package/lib/api/configuration.api.js +131 -0
  17. package/lib/api/role-assistant-test-job.api.js +60 -0
  18. package/lib/api/system-secrets.api.js +72 -0
  19. package/lib/api/work-search.api.js +13 -6
  20. package/lib/app/deploy.js +8 -1
  21. package/lib/app/run-docker-fallback.js +6 -1
  22. package/lib/app/run-helpers.js +2 -1
  23. package/lib/app/run-parameter-sync.js +142 -0
  24. package/lib/app/show-display.js +1 -0
  25. package/lib/app/show-online.js +15 -0
  26. package/lib/build/docker-build-args.js +5 -2
  27. package/lib/build/index.js +3 -2
  28. package/lib/build/standard-docker-build.js +6 -3
  29. package/lib/cli/setup-app.js +19 -1
  30. package/lib/cli/setup-environment.js +156 -0
  31. package/lib/cli/setup-utility.js +24 -1
  32. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  33. package/lib/commands/datasource-capability.js +4 -2
  34. package/lib/commands/env-secret-context.js +113 -0
  35. package/lib/commands/env-secret-list.js +200 -0
  36. package/lib/commands/env-secret-push-confirm.js +54 -0
  37. package/lib/commands/env-secret-push-run.js +227 -0
  38. package/lib/commands/env-secret-push.js +168 -0
  39. package/lib/commands/repair-datasource-apply.js +2 -0
  40. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  41. package/lib/commands/repair-datasource-run.js +1 -0
  42. package/lib/commands/role-assistant.js +7 -0
  43. package/lib/commands/setup-modes.js +1 -8
  44. package/lib/commands/setup-prompts.js +2 -180
  45. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  46. package/lib/commands/verify-operations-steps.js +13 -1
  47. package/lib/commands/wizard-config-normalizer.js +7 -4
  48. package/lib/commands/wizard-core.js +5 -157
  49. package/lib/commands/wizard-file-saving.js +163 -0
  50. package/lib/core/env-platform-expand.js +97 -0
  51. package/lib/core/secrets-env-content.js +42 -4
  52. package/lib/core/secrets-env-write.js +10 -3
  53. package/lib/core/secrets-load.js +4 -2
  54. package/lib/datasource/binary-documents-validator.js +190 -0
  55. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  56. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  57. package/lib/datasource/capability/upsert-operations.js +138 -0
  58. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  59. package/lib/datasource/validate.js +12 -5
  60. package/lib/deployment/installation/azure-infra-stage.js +3 -1
  61. package/lib/deployment/installation/infra-catalog.js +2 -5
  62. package/lib/generator/builders.js +17 -0
  63. package/lib/generator/helpers.js +23 -2
  64. package/lib/generator/index.js +13 -6
  65. package/lib/lifecycle/product-model.js +4 -3
  66. package/lib/lifecycle/report-display.js +3 -2
  67. package/lib/parameters/infra-parameter-catalog.js +1 -1
  68. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  69. package/lib/programmatic/builder-help-governance.js +1 -1
  70. package/lib/programmatic/builder-help.js +1 -1
  71. package/lib/role-assistant/test-cases-search.js +3 -0
  72. package/lib/role-assistant/test-job-runner.js +125 -0
  73. package/lib/role-assistant/test-runner-search.js +44 -2
  74. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  75. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  76. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  77. package/lib/role-assistant/test-runner-workhub.js +28 -19
  78. package/lib/schema/application-schema.json +205 -2
  79. package/lib/schema/external-datasource.schema.json +23 -3
  80. package/lib/schema/infra-parameter.schema.json +139 -33
  81. package/lib/schema/infra.parameter.yaml +416 -66
  82. package/lib/schema/infrastructure-schema.json +10 -35
  83. package/lib/utils/compose-generate-docker-compose.js +16 -9
  84. package/lib/utils/datasource-binary-evidence.js +92 -0
  85. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  86. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  87. package/lib/utils/datasource-test-run-display.js +8 -2
  88. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  89. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  90. package/lib/utils/docker-build.js +29 -8
  91. package/lib/utils/docker-manifest-public-port.js +36 -0
  92. package/lib/utils/env-copy.js +11 -10
  93. package/lib/utils/external-system-system-test-tty.js +3 -2
  94. package/lib/utils/image-tags.js +2 -2
  95. package/lib/utils/platform-kv-ref.js +1 -1
  96. package/lib/utils/platform-resolution.js +226 -0
  97. package/lib/utils/prepare-local-data-mount.js +58 -0
  98. package/lib/utils/resolve-docker-image-ref.js +8 -3
  99. package/lib/utils/secrets-helpers.js +0 -1
  100. package/lib/utils/system-secret-mapping.js +125 -0
  101. package/lib/utils/test-log-writer.js +2 -1
  102. package/lib/validation/external-manifest-validator.js +5 -0
  103. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  104. package/lib/validation/validate-external-file.js +5 -1
  105. package/package.json +5 -4
  106. package/templates/README.md +2 -1
  107. package/templates/agent-kit/agent-kit.yaml +4 -1
  108. package/templates/agent-kit/instructions/AGENTKIT.md +1 -1
  109. package/templates/agent-kit/instructions/root.AGENTS.md +2 -0
  110. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +35 -2
  111. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  112. package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
  113. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +6 -2
  114. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +28 -2
  115. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  116. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +28 -1
  117. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  118. package/templates/agent-kit/skills/shared/feedback.md +31 -0
  119. package/templates/agent-kit/skills/shared/hosts.md +13 -3
  120. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  121. package/templates/agent-kit/skills/shared/status.md +76 -0
  122. package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
  123. package/templates/applications/builder-api/application.yaml +1 -1
  124. package/templates/applications/builder-api/env.template +5 -1
  125. package/templates/applications/dataplane/application.yaml +30 -2
  126. package/templates/applications/dataplane/env.template +36 -5
  127. package/templates/applications/keycloak/application.yaml +6 -1
  128. package/templates/applications/miso-controller/application.yaml +96 -1
  129. package/templates/applications/miso-controller/env.template +63 -37
  130. package/templates/applications/miso-controller/rbac.yaml +17 -0
  131. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  132. package/templates/marketplace/main.json +73 -225
  133. package/templates/python/Dockerfile.hbs +2 -0
  134. package/templates/python/docker-compose.hbs +1 -1
  135. package/templates/typescript/Dockerfile.hbs +2 -0
  136. package/templates/typescript/docker-compose.hbs +12 -0
  137. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -34
  138. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  139. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  140. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  141. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  142. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  143. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  144. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  145. /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
@@ -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 };
@@ -65,6 +65,23 @@ async function handleSplitJsonCommand(appName, options) {
65
65
  return generator.splitDeployJson(deployJsonPath, outputDir);
66
66
  }
67
67
 
68
+ /**
69
+ * Deployment target for manifest generation. A resource group resolves platform values; the
70
+ * environment key resolves the environments layer. Without either, {NAME} tokens are preserved.
71
+ * @param {Object} options - Command options
72
+ * @returns {Promise<{ resourceGroup: string|undefined, environment: string }>}
73
+ */
74
+ async function buildGenerationTarget(options = {}) {
75
+ // Only an explicit target resolves. The default artifact keeps {NAME} tokens and carries the
76
+ // platforms/environments sections, so the Controller resolves them per installation.
77
+ if (!options.resourceGroup && !options.env) return undefined;
78
+ const { resolveEnvironment } = require('../core/config');
79
+ const environment = options.env
80
+ ? String(options.env).trim().toLowerCase()
81
+ : await resolveEnvironment();
82
+ return { resourceGroup: options.resourceGroup, environment };
83
+ }
84
+
68
85
  /**
69
86
  * Logs split-json results
70
87
  * @param {Object} result - Generated file paths
@@ -93,6 +110,11 @@ function logSplitJsonResult(result) {
93
110
  function setupJsonCommand(program) {
94
111
  program.command('json <appKey|systemKey>')
95
112
  .description('Write deployment JSON to disk for version control')
113
+ .option(
114
+ '-g, --resource-group <name>',
115
+ 'Target Azure resource group; resolves platforms/environments values instead of leaving {NAME} tokens'
116
+ )
117
+ .option('-e, --env <env>', 'Deployment environment key for environments resolution (default: current environment)')
96
118
  .addHelpText('after', JSON_HELP_AFTER)
97
119
  .action(async(appName, options) => {
98
120
  try {
@@ -105,7 +127,8 @@ function setupJsonCommand(program) {
105
127
  json: false
106
128
  });
107
129
  const result = await generator.generateDeployJsonWithValidation(appName, {
108
- ...options
130
+ ...options,
131
+ target: await buildGenerationTarget(options)
109
132
  });
110
133
  if (result.success) {
111
134
  const fileName = result.path.includes('application-schema.json') ? 'application-schema.json' : 'deployment JSON';
@@ -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, remove, create, diff, edit, validate).
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 (FK metadata), diff, edit, or validate per-capability slices in datasource JSON'
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
+ };
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Show compact configuration status without exposing sensitive values.
3
+ *
4
+ * @fileoverview aifabrix env secret list
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const logger = require('../utils/logger');
12
+ const { listEnvironmentConfiguration } = require('../api/configuration.api');
13
+ const {
14
+ colorAggregateGlyph,
15
+ metadata,
16
+ sectionTitle
17
+ } = require('../utils/cli-layout-chalk');
18
+
19
+ const FORBIDDEN_FIELDS = ['data', 'ciphertext', 'reference', 'vaultName', 'secretValue'];
20
+ const WIDTHS = Object.freeze({ status: 6, application: 20, name: 28, type: 8, value: 48 });
21
+ const GLYPHS = Object.freeze({ ok: '✔', warn: '⚠', skipped: '⏭' });
22
+
23
+ /**
24
+ * @param {Object} row - Configuration metadata row
25
+ * @param {string} application - Optional expected owner
26
+ * @returns {void} Throws when unsafe or contradictory
27
+ */
28
+ function assertSafeRow(row, application) {
29
+ if (!row || typeof row !== 'object') {
30
+ throw new Error('Configuration list returned invalid or unsafe metadata');
31
+ }
32
+ const owner = String(row.application || '').trim();
33
+ const name = String(row.name || row.key || '').trim();
34
+ const hasValue = Object.prototype.hasOwnProperty.call(row, 'value');
35
+ const safeValue = [!hasValue, row.sensitive === false && typeof row.value === 'string']
36
+ .some(Boolean);
37
+ const forbidden = FORBIDDEN_FIELDS.some(field =>
38
+ Object.prototype.hasOwnProperty.call(row, field)
39
+ );
40
+ const exactOwner = [!application, owner === application].some(Boolean);
41
+ const booleans = ['sensitive', 'configured', 'required']
42
+ .every(field => typeof row[field] === 'boolean');
43
+ const valid = [owner, name, exactOwner, booleans, safeValue, !forbidden].every(Boolean);
44
+ if (!valid) {
45
+ throw new Error('Configuration list returned invalid or unsafe metadata');
46
+ }
47
+ }
48
+
49
+ /**
50
+ * @param {Object} row - Configuration metadata row
51
+ * @param {string} application - Optional expected exact owner
52
+ * @returns {Object} Safe normalized row
53
+ */
54
+ function normalizeRow(row, application) {
55
+ assertSafeRow(row, application);
56
+ const owner = String(row.application).trim();
57
+ const name = String(row.name || row.key).trim();
58
+ const hasValue = Object.prototype.hasOwnProperty.call(row, 'value');
59
+ const state = row.configured ? 'ok' : (row.required ? 'warn' : 'skipped');
60
+ const normalized = {
61
+ application: owner,
62
+ name,
63
+ type: row.sensitive ? 'secret' : 'variable',
64
+ configured: row.configured,
65
+ required: row.required,
66
+ state
67
+ };
68
+ if (!row.sensitive && hasValue) normalized.value = row.value;
69
+ return normalized;
70
+ }
71
+
72
+ /**
73
+ * @param {unknown} value - Cell value
74
+ * @returns {string} Safe single-line cell value
75
+ */
76
+ function cleanCell(value) {
77
+ const noControls = [...String(value ?? '')]
78
+ .map(char => char.charCodeAt(0) <= 31 || char.charCodeAt(0) === 127 ? ' ' : char)
79
+ .join('');
80
+ return noControls.replace(/\s+/g, ' ').trim();
81
+ }
82
+
83
+ /**
84
+ * @param {unknown} value - Cell value
85
+ * @param {number} width - Visible cell width
86
+ * @returns {string} Single-line padded or truncated cell
87
+ */
88
+ function formatCell(value, width) {
89
+ const clean = cleanCell(value);
90
+ const visible = clean.length > width ? `${clean.slice(0, width - 1)}…` : clean;
91
+ return visible.padEnd(width);
92
+ }
93
+
94
+ /**
95
+ * Keep identifiers complete while retaining compact minimum widths.
96
+ * @param {Array<Object>} rows - Safe normalized rows
97
+ * @returns {Object} Widths for this result set
98
+ */
99
+ function tableWidths(rows) {
100
+ return {
101
+ ...WIDTHS,
102
+ application: Math.max(WIDTHS.application, ...rows.map(row => cleanCell(row.application).length)),
103
+ name: Math.max(WIDTHS.name, ...rows.map(row => cleanCell(row.name).length))
104
+ };
105
+ }
106
+
107
+ /**
108
+ * @param {Object} row - Safe normalized row
109
+ * @param {boolean} includeApplication - Include owner column
110
+ * @returns {string} One compact table row
111
+ */
112
+ function formatRow(row, includeApplication, widths = WIDTHS) {
113
+ const glyph = colorAggregateGlyph(row.state, GLYPHS[row.state]);
114
+ const value = row.type === 'variable'
115
+ ? (row.value === '' ? '""' : (row.value ?? ''))
116
+ : '';
117
+ const cells = [glyph + ' '.repeat(widths.status - 1)];
118
+ if (includeApplication) cells.push(formatCell(row.application, widths.application));
119
+ cells.push(formatCell(row.name, widths.name));
120
+ cells.push(formatCell(row.type, widths.type));
121
+ cells.push(formatCell(value, widths.value).trimEnd());
122
+ return ` ${cells.join(' ')}`;
123
+ }
124
+
125
+ /**
126
+ * @param {boolean} includeApplication - Include owner column
127
+ * @returns {string[]} Header and divider
128
+ */
129
+ function tableHeader(includeApplication, widths = WIDTHS) {
130
+ const headers = [formatCell('Status', widths.status)];
131
+ const dividers = ['-'.repeat(widths.status)];
132
+ if (includeApplication) {
133
+ headers.push(formatCell('Application', widths.application));
134
+ dividers.push('-'.repeat(widths.application));
135
+ }
136
+ headers.push(formatCell('Name', widths.name), formatCell('Type', widths.type), 'Value');
137
+ dividers.push('-'.repeat(widths.name), '-'.repeat(widths.type), '-'.repeat(12));
138
+ return [` ${headers.join(' ')}`, ` ${dividers.join(' ')}`];
139
+ }
140
+
141
+ /**
142
+ * @param {Array<Object>} rows - Safe rows
143
+ * @returns {string} Compact totals
144
+ */
145
+ function formatSummary(rows) {
146
+ const configured = rows.filter(row => row.configured).length;
147
+ const requiredMissing = rows.filter(row => !row.configured && row.required).length;
148
+ const optionalUnset = rows.length - configured - requiredMissing;
149
+ const ok = colorAggregateGlyph('ok', GLYPHS.ok);
150
+ const warn = colorAggregateGlyph('warn', GLYPHS.warn);
151
+ const skipped = colorAggregateGlyph('skipped', GLYPHS.skipped);
152
+ const label = rows.length === 1 ? 'entry' : 'entries';
153
+ return `${rows.length} ${label} · ${ok} ${configured} configured · ` +
154
+ `${warn} ${requiredMissing} required missing · ${skipped} ${optionalUnset} optional unset`;
155
+ }
156
+
157
+ /**
158
+ * List Parameter declarations for an environment or exact application owner.
159
+ * @param {Object} ctx - controllerUrl, authConfig, envKey
160
+ * @param {Object} [options] - `api` for injection, application and key search
161
+ * @returns {Promise<{ exitCode: number, rows: Array<Object> }>} Safe normalized rows
162
+ */
163
+ async function runEnvSecretList(ctx, options = {}) {
164
+ const application = String(options.application || '').trim();
165
+ const api = options.api || { listEnvironmentConfiguration };
166
+ const raw = await api.listEnvironmentConfiguration(
167
+ ctx.controllerUrl, ctx.authConfig, ctx.envKey, application || undefined,
168
+ options.key ? { search: String(options.key).trim() } : {}
169
+ );
170
+ if (!Array.isArray(raw)) throw new Error('Configuration list response is not an array');
171
+ const rows = raw.map(row => normalizeRow(row, application)).sort((left, right) =>
172
+ left.application.localeCompare(right.application) || left.name.localeCompare(right.name)
173
+ );
174
+ const title = application ? `${application} (${ctx.envKey})` : ctx.envKey;
175
+ logger.log(sectionTitle(`Configuration: ${title}`));
176
+ logger.log(metadata(`Controller: ${ctx.controllerUrl}`));
177
+ logger.log('');
178
+ if (rows.length === 0) {
179
+ const suffix = options.key ? ` matching "${options.key}"` : '';
180
+ logger.log(metadata(` No configuration entries${suffix}.`));
181
+ return { exitCode: 0, rows };
182
+ }
183
+ const widths = tableWidths(rows);
184
+ tableHeader(!application, widths).forEach(line => logger.log(line));
185
+ rows.forEach(row => logger.log(formatRow(row, !application, widths)));
186
+ logger.log('');
187
+ logger.log(metadata(formatSummary(rows)));
188
+ return { exitCode: 0, rows };
189
+ }
190
+
191
+ module.exports = {
192
+ normalizeRow,
193
+ cleanCell,
194
+ formatCell,
195
+ tableWidths,
196
+ formatRow,
197
+ tableHeader,
198
+ formatSummary,
199
+ runEnvSecretList
200
+ };