@aifabrix/builder 2.46.0 → 2.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmrc.token +1 -0
- package/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +17 -0
- package/.temp/validation/summary.txt +5 -0
- package/jest.isolated-projects.js +18 -2
- package/jest.projects.js +24 -0
- package/lib/api/dataplane-health.api.js +19 -7
- package/lib/api/lifecycle.api.js +22 -1
- package/lib/api/resource-types.api.js +107 -0
- package/lib/api/runtime-configuration.api.js +72 -0
- package/lib/api/types/resource-types.types.js +45 -0
- package/lib/api/user-groups.api.js +9 -9
- package/lib/api/users.api.js +8 -8
- package/lib/api/wizard.api.js +10 -2
- package/lib/app/down.js +58 -26
- package/lib/app/index.js +2 -2
- package/lib/app/run-resolve-image.js +2 -1
- package/lib/app/run-stop-containers.js +113 -0
- package/lib/app/run.js +1 -31
- package/lib/app/show-display.js +35 -0
- package/lib/app/show.js +32 -1
- package/lib/build/index.js +17 -5
- package/lib/build/miso-controller-npm-sdk-guard.js +25 -0
- package/lib/channels/add-channel.js +348 -0
- package/lib/channels/channel-artifact-generator.js +141 -0
- package/lib/channels/channel-audit.js +101 -0
- package/lib/channels/channel-auth-context.js +37 -0
- package/lib/channels/channel-credential.js +200 -0
- package/lib/channels/channel-display-name.js +64 -0
- package/lib/channels/channel-external-system.js +106 -0
- package/lib/channels/channel-integration-generator.js +274 -0
- package/lib/channels/channel-probe.js +89 -0
- package/lib/channels/channel-runtime-resolver.js +152 -0
- package/lib/channels/channel-status-display.js +141 -0
- package/lib/channels/channel-targets.js +115 -0
- package/lib/cli/index.js +16 -4
- package/lib/cli/setup-app.js +54 -3
- package/lib/cli/setup-infra-up-dataplane-action.js +2 -2
- package/lib/cli/setup-infra-up-platform-action.js +2 -1
- package/lib/cli/setup-platform.js +1 -1
- package/lib/cli/setup-utility-help-after.js +4 -1
- package/lib/cli/setup-utility-resolve.js +8 -5
- package/lib/commands/app-logs.js +54 -26
- package/lib/commands/app.js +41 -0
- package/lib/commands/auth-status-dataplane-version.js +97 -61
- package/lib/commands/auth-status.js +2 -7
- package/lib/commands/channel.js +58 -0
- package/lib/commands/datasource.js +1 -1
- package/lib/commands/identity-apply-sync.js +49 -8
- package/lib/commands/identity-group-cli.js +66 -26
- package/lib/commands/identity-membership-role-cache.js +58 -37
- package/lib/commands/identity-shared.js +22 -1
- package/lib/commands/identity-user-cli.js +80 -34
- package/lib/commands/repair-auth-apply.js +30 -4
- package/lib/commands/repair-auth-config.js +8 -13
- package/lib/commands/repair-datasource-com-v2.js +15 -0
- package/lib/commands/repair-datasource-service-contract.js +206 -0
- package/lib/commands/repair-datasource.js +53 -18
- package/lib/commands/repair-env-template.js +12 -12
- package/lib/commands/repair-openapi-sync.js +26 -4
- package/lib/commands/repair-steps.js +28 -13
- package/lib/commands/repair-system-alignment.js +7 -4
- package/lib/commands/resource-type.js +371 -0
- package/lib/commands/setup-modes.js +1 -1
- package/lib/commands/setup-prompts-platform-mode.js +24 -21
- package/lib/commands/test-trust-external.js +39 -90
- package/lib/commands/up-common.js +33 -1
- package/lib/commands/up-dataplane.js +8 -4
- package/lib/commands/upload.js +24 -3
- package/lib/commands/verify-trust-command-action.js +4 -1
- package/lib/commands/verify-trust-external.js +12 -20
- package/lib/core/config-setup-platform-mode.js +3 -3
- package/lib/core/setup-platform-mode.js +41 -6
- package/lib/datasource/capability/capability-diff-slice.js +2 -2
- package/lib/datasource/capability/capability-manifest.js +254 -0
- package/lib/datasource/capability/capability-resolve.js +3 -4
- package/lib/datasource/capability/copy-operations.js +5 -3
- package/lib/datasource/capability/remove-operations.js +2 -1
- package/lib/datasource/capability/run-capability-copy.js +32 -4
- package/lib/datasource/capability/run-capability-diff.js +3 -3
- package/lib/datasource/capability/run-capability-dimension.js +2 -4
- package/lib/datasource/capability/run-capability-edit.js +4 -7
- package/lib/datasource/capability/run-capability-relate.js +2 -4
- package/lib/datasource/capability/run-capability-remove.js +2 -4
- package/lib/datasource/capability/validate-capability-slice.js +2 -1
- package/lib/datasource/datasource-identifier.js +2 -1
- package/lib/datasource/datasource-validate-display.js +1 -1
- package/lib/datasource/field-reference-validator.js +24 -3
- package/lib/datasource/orphan-profiles-validator.js +98 -0
- package/lib/datasource/validate.js +41 -23
- package/lib/external-system/download-helpers.js +87 -0
- package/lib/external-system/download.js +61 -122
- package/lib/external-system/generator.js +1 -59
- package/lib/external-system/test.js +10 -15
- package/lib/generator/builders.js +11 -1
- package/lib/generator/external-controller-manifest.js +5 -2
- package/lib/generator/split.js +3 -1
- package/lib/generator/wizard.js +1 -28
- package/lib/identity/fk-reference.js +65 -0
- package/lib/identity/identity-apply-core.js +13 -58
- package/lib/identity/identity-apply-phases.js +5 -1
- package/lib/identity/identity-display.js +243 -0
- package/lib/identity/identity-response-helpers.js +52 -0
- package/lib/identity/identity-sync-errors.js +62 -0
- package/lib/identity/resolve-user-id.js +129 -0
- package/lib/identity/username-default.js +31 -0
- package/lib/lifecycle/certification-progress.js +2 -2
- package/lib/lifecycle/report-display-trust.js +70 -0
- package/lib/lifecycle/report-display.js +14 -13
- package/lib/resolvers/datasource-resolver.js +2 -3
- package/lib/resolvers/resource-type-file.js +159 -0
- package/lib/schema/application-schema.json +16 -0
- package/lib/schema/deployment-rules.yaml +2 -1
- package/lib/schema/external-datasource.schema.json +372 -58
- package/lib/schema/external-system.schema.json +331 -2
- package/lib/schema/type/document-storage.json +9 -5
- package/lib/utils/app-run-containers.js +25 -2
- package/lib/utils/authentication-shape.js +224 -0
- package/lib/utils/build-copy.js +36 -5
- package/lib/utils/build-resolve-image.js +66 -12
- package/lib/utils/celery-companion-apps.js +53 -0
- package/lib/utils/cli-utils.js +4 -0
- package/lib/utils/compose-celery-worker.js +23 -0
- package/lib/utils/compose-generate-docker-compose.js +9 -1
- package/lib/utils/configuration-env-resolver.js +36 -0
- package/lib/utils/credential-secrets-env-kv.js +216 -0
- package/lib/utils/credential-secrets-env.js +52 -262
- package/lib/utils/dataplane-resolver.js +4 -115
- package/lib/utils/declarative-url-ports.js +24 -1
- package/lib/utils/docker-build.js +1 -4
- package/lib/utils/external-env-template.js +10 -3
- package/lib/utils/external-system-validators.js +8 -2
- package/lib/utils/help-builder.js +2 -0
- package/lib/utils/paths.js +25 -0
- package/lib/utils/platform-controller-url.js +2 -1
- package/lib/utils/platform-kv-ref.js +57 -0
- package/lib/utils/runtime-configuration-download.js +189 -0
- package/lib/utils/runtime-configuration-values.js +119 -0
- package/lib/utils/schema-loader.js +19 -4
- package/lib/utils/worker-sidecars.js +142 -0
- package/lib/validation/external-manifest-validator.js +2 -0
- package/lib/validation/runtime-configuration-rules.js +98 -0
- package/lib/validation/validate-display-dimensions.js +154 -0
- package/lib/validation/validate-display-sections.js +276 -0
- package/lib/validation/validate-display-steps.js +67 -0
- package/lib/validation/validate-display.js +24 -402
- package/package.json +1 -1
- package/templates/applications/dataplane/application.yaml +5 -0
- package/templates/applications/dataplane/env.template +26 -13
- package/templates/applications/dataplane/rbac.yaml +108 -1
- package/templates/applications/miso-controller/application.yaml +2 -2
- package/templates/applications/miso-controller/env.template +37 -122
- package/templates/applications/miso-controller/rbac.yaml +28 -13
- package/templates/channels/chatgpt/install.template.md.hbs +49 -0
- package/templates/channels/chatgpt/manifest.template.json.hbs +9 -0
- package/templates/channels/microsoft-copilot/install.template.md.hbs +49 -0
- package/templates/channels/microsoft-copilot/manifest.template.json.hbs +8 -0
- package/templates/channels/slack/install.template.md.hbs +49 -0
- package/templates/channels/slack/manifest.template.json.hbs +66 -0
- package/templates/channels/teams-bot/install.template.md.hbs +49 -0
- package/templates/channels/teams-bot/manifest.template.json.hbs +13 -0
- package/templates/channels/web-embed/install.template.md.hbs +49 -0
- package/templates/channels/web-embed/manifest.template.json.hbs +8 -0
- package/templates/python/docker-compose.hbs +56 -0
- package/templates/typescript/docker-compose.hbs +52 -0
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collect KV_* env vars as secret store items and push to dataplane.
|
|
3
|
-
* Reads integration .env, resolves kv:// in values, scans payload for kv:// refs,
|
|
4
|
-
* and pushes plain values to POST /api/v1/credential/secret.
|
|
5
3
|
*
|
|
6
4
|
* @fileoverview Credential secrets push from .env and payload (Dataplane)
|
|
7
5
|
* @author AI Fabrix Team
|
|
@@ -11,217 +9,45 @@
|
|
|
11
9
|
const fs = require('fs');
|
|
12
10
|
const { loadSecrets } = require('../core/secrets');
|
|
13
11
|
const { storeCredentialSecrets } = require('../api/credential.api');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return systemKey.replace(/-/g, '_').toUpperCase();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Maps authentication security key (camelCase) to env VAR (UPPERCASE, no underscores).
|
|
30
|
-
* Used for canonical KV_<APPKEY>_<VAR> names (e.g. clientId → CLIENTID).
|
|
31
|
-
* @param {string} securityKey - Security key (e.g. 'clientId', 'clientSecret')
|
|
32
|
-
* @returns {string}
|
|
33
|
-
*/
|
|
34
|
-
function securityKeyToVar(securityKey) {
|
|
35
|
-
if (!securityKey || typeof securityKey !== 'string') return '';
|
|
36
|
-
return securityKey.replace(/_/g, '').toUpperCase();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Known single-segment variable suffixes (uppercase) for inferring var vs namespace in env keys */
|
|
40
|
-
const VAR_SUFFIXES = new Set(['ID', 'SECRET', 'KEY', 'TOKEN', 'URL', 'USERNAME', 'PASSWORD']);
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Converts var segment(s) from env key to path-style camelCase (e.g. CLIENT_ID → clientId, CLIENTID → clientId).
|
|
44
|
-
* @param {string[]} varSegments - One or two segments (e.g. ['CLIENT', 'ID'], ['CLIENTID'])
|
|
45
|
-
* @returns {string}
|
|
46
|
-
*/
|
|
47
|
-
function varSegmentsToCamelCase(varSegments) {
|
|
48
|
-
if (!varSegments || varSegments.length === 0) return '';
|
|
49
|
-
if (varSegments.length === 1) {
|
|
50
|
-
const s = varSegments[0].toLowerCase();
|
|
51
|
-
if (s.endsWith('id') && s.length > 2) return s.slice(0, -2) + 'Id';
|
|
52
|
-
if (s.endsWith('secret') && s.length > 6) return s.slice(0, -6) + 'Secret';
|
|
53
|
-
if (s.endsWith('key') && s.length > 3) return s.slice(0, -3) + 'Key';
|
|
54
|
-
if (s.endsWith('token') && s.length > 5) return s.slice(0, -5) + 'Token';
|
|
55
|
-
if (s.endsWith('url') && s.length > 3) return s.slice(0, -3) + 'Url';
|
|
56
|
-
return s;
|
|
57
|
-
}
|
|
58
|
-
return varSegments.map((seg, i) => {
|
|
59
|
-
const lower = seg.toLowerCase();
|
|
60
|
-
return i === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
61
|
-
}).join('');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Builds kv path from segments when systemKey is provided (path = kv://systemKey/variable).
|
|
66
|
-
* @param {string[]} segments - Parsed segments after KV_ prefix
|
|
67
|
-
* @param {string} systemKey - System key (e.g. 'microsoft-teams')
|
|
68
|
-
* @returns {string|null}
|
|
69
|
-
*/
|
|
70
|
-
function kvPathWithSystemKey(segments, systemKey) {
|
|
71
|
-
const prefixInKey = systemKey.replace(/-/g, '_').toUpperCase();
|
|
72
|
-
const prefixSegs = prefixInKey.split('_').filter(Boolean);
|
|
73
|
-
if (segments.length <= prefixSegs.length) return null;
|
|
74
|
-
const prefixMatch = prefixSegs.every((p, i) => segments[i] === p);
|
|
75
|
-
if (!prefixMatch) return null;
|
|
76
|
-
const varSegments = segments.slice(prefixSegs.length);
|
|
77
|
-
const pathVar = varSegmentsToCamelCase(varSegments);
|
|
78
|
-
return pathVar ? `kv://${systemKey}/${pathVar}` : null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Builds kv path from segments when systemKey is not provided (infers namespace and variable).
|
|
83
|
-
* @param {string[]} segments - Parsed segments after KV_ prefix
|
|
84
|
-
* @returns {string|null}
|
|
85
|
-
*/
|
|
86
|
-
function kvPathInferred(segments) {
|
|
87
|
-
if (segments.length === 1) return `kv://${segments[0].toLowerCase()}`;
|
|
88
|
-
const varSegmentCount = (segments.length >= 2 && VAR_SUFFIXES.has(segments[segments.length - 1])) ? 2 : 1;
|
|
89
|
-
const namespace = segments.slice(0, -varSegmentCount).map(s => s.toLowerCase()).join('-');
|
|
90
|
-
const varSegments = segments.slice(-varSegmentCount);
|
|
91
|
-
const pathVar = varSegmentsToCamelCase(varSegments);
|
|
92
|
-
return (namespace && pathVar) ? `kv://${namespace}/${pathVar}` : null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Returns the path segment used in kv://<systemKey>/<segment> for a given security key.
|
|
97
|
-
* Uses the same derivation as env key → path (securityKeyToVar + varSegmentsToCamelCase).
|
|
98
|
-
* @param {string} securityKey - Security key (e.g. 'apiKey', 'clientId', 'clientSecret')
|
|
99
|
-
* @returns {string} Canonical path segment (e.g. 'apiKey', 'clientId')
|
|
100
|
-
*/
|
|
101
|
-
function getKvPathSegmentForSecurityKey(securityKey) {
|
|
102
|
-
if (!securityKey || typeof securityKey !== 'string') return '';
|
|
103
|
-
return varSegmentsToCamelCase([securityKeyToVar(securityKey)]);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Converts KV_* env key to kv:// path in format kv://<system-key>/<variable>.
|
|
108
|
-
* System-key uses hyphens (e.g. microsoft-teams); variable is camelCase (e.g. clientId).
|
|
109
|
-
* When systemKey is provided, uses it as the path namespace; otherwise infers from segments.
|
|
110
|
-
*
|
|
111
|
-
* @param {string} envKey - Env var name (e.g. KV_MICROSOFT_TEAMS_CLIENT_ID)
|
|
112
|
-
* @param {string} [systemKey] - Optional system key (e.g. 'microsoft-teams'); when provided, path is kv://systemKey/variable
|
|
113
|
-
* @returns {string|null} kv:// path or null if invalid
|
|
114
|
-
*/
|
|
115
|
-
function kvEnvKeyToPath(envKey, systemKey) {
|
|
116
|
-
if (!envKey || typeof envKey !== 'string' || !envKey.toUpperCase().startsWith(KV_PREFIX)) return null;
|
|
117
|
-
const rest = envKey.slice(KV_PREFIX.length).trim();
|
|
118
|
-
if (!rest) return null;
|
|
119
|
-
const segments = rest.split('_').filter(Boolean);
|
|
120
|
-
if (segments.length === 0) return null;
|
|
121
|
-
|
|
122
|
-
const hasSystemKey = typeof systemKey === 'string' && systemKey.length > 0;
|
|
123
|
-
if (hasSystemKey) return kvPathWithSystemKey(segments, systemKey);
|
|
124
|
-
return kvPathInferred(segments);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Collects KV_* entries from env map as secret items (key = kv path, value = raw).
|
|
129
|
-
* When value is a kv:// path, uses it as the key so it matches payload paths (e.g. kv://microsoft-teams/clientId).
|
|
130
|
-
* Otherwise derives path from env key via kvEnvKeyToPath(envKey).
|
|
131
|
-
*
|
|
132
|
-
* @param {Object.<string, string>} envMap - Key-value map from .env
|
|
133
|
-
* @returns {Array<{ key: string, value: string }>} Items (key = kv://..., value = raw)
|
|
134
|
-
*/
|
|
135
|
-
function collectKvEnvVarsAsSecretItems(envMap, systemKey) {
|
|
136
|
-
if (!envMap || typeof envMap !== 'object') {
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
const items = [];
|
|
140
|
-
for (const [envKey, rawValue] of Object.entries(envMap)) {
|
|
141
|
-
const value = typeof rawValue === 'string' ? rawValue.trim() : '';
|
|
142
|
-
if (value === '') continue;
|
|
143
|
-
let kvPath = null;
|
|
144
|
-
if (value.startsWith('kv://') && isValidKvPath(value)) {
|
|
145
|
-
kvPath = value;
|
|
146
|
-
}
|
|
147
|
-
if (!kvPath) kvPath = kvEnvKeyToPath(envKey, systemKey) || kvEnvKeyToPath(envKey);
|
|
148
|
-
if (!kvPath) continue;
|
|
149
|
-
items.push({ key: kvPath, value });
|
|
150
|
-
}
|
|
151
|
-
return items;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Resolves a single value if it is a kv:// reference using secrets map.
|
|
156
|
-
* Supports path-style keys (e.g. secrets/foo) and hyphen-style (secrets-foo).
|
|
157
|
-
*
|
|
158
|
-
* @param {Object} secrets - Loaded secrets object
|
|
159
|
-
* @param {string} value - Value (may be "kv://..." or plain)
|
|
160
|
-
* @returns {string|null} Resolved plain value or null if unresolved
|
|
161
|
-
*/
|
|
162
|
-
function resolveKvValue(secrets, value) {
|
|
163
|
-
if (typeof value !== 'string') return null;
|
|
164
|
-
const trimmed = value.trim();
|
|
165
|
-
if (!trimmed.startsWith('kv://')) {
|
|
166
|
-
return trimmed;
|
|
167
|
-
}
|
|
168
|
-
const pathMatch = trimmed.match(/^kv:\/\/([a-zA-Z0-9_\-/]+)$/);
|
|
169
|
-
if (!pathMatch) return null;
|
|
170
|
-
const pathKey = pathMatch[1];
|
|
171
|
-
const resolved = secrets[pathKey];
|
|
172
|
-
if (resolved === undefined) return null;
|
|
173
|
-
return typeof resolved === 'string' ? resolved : String(resolved);
|
|
174
|
-
}
|
|
12
|
+
const {
|
|
13
|
+
collectKvEnvVarsAsSecretItems,
|
|
14
|
+
collectKvRefsFromPayload,
|
|
15
|
+
getKvPathSegmentForSecurityKey,
|
|
16
|
+
isNonEmptySecretValue,
|
|
17
|
+
isValidKvPath,
|
|
18
|
+
kvEnvKeyToPath,
|
|
19
|
+
resolveKvValue,
|
|
20
|
+
securityKeyToVar,
|
|
21
|
+
systemKeyToKvPrefix
|
|
22
|
+
} = require('./credential-secrets-env-kv');
|
|
175
23
|
|
|
176
24
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* @
|
|
180
|
-
* @param {Set<string>} [acc] - Accumulator set (internal)
|
|
181
|
-
* @returns {string[]} Unique kv:// refs (e.g. ["kv://secrets/foo"])
|
|
25
|
+
* Parses .env-style content into key-value map (first = separates key and value).
|
|
26
|
+
* @param {string} content - Raw .env content
|
|
27
|
+
* @returns {Object.<string, string>}
|
|
182
28
|
*/
|
|
183
|
-
function
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (Array.isArray(obj)) {
|
|
196
|
-
for (const item of obj) {
|
|
197
|
-
collectKvRefsFromPayload(item, acc);
|
|
198
|
-
}
|
|
199
|
-
return Array.from(acc);
|
|
200
|
-
}
|
|
201
|
-
if (typeof obj === 'object') {
|
|
202
|
-
for (const v of Object.values(obj)) {
|
|
203
|
-
collectKvRefsFromPayload(v, acc);
|
|
29
|
+
function parseEnvToMap(content) {
|
|
30
|
+
if (!content || typeof content !== 'string') return {};
|
|
31
|
+
const map = {};
|
|
32
|
+
const lines = content.split(/\r?\n/);
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
36
|
+
const eq = trimmed.indexOf('=');
|
|
37
|
+
if (eq > 0) {
|
|
38
|
+
const key = trimmed.substring(0, eq).trim();
|
|
39
|
+
const value = trimmed.substring(eq + 1);
|
|
40
|
+
map[key] = value;
|
|
204
41
|
}
|
|
205
|
-
return Array.from(acc);
|
|
206
42
|
}
|
|
207
|
-
return
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Validates that key is a well-formed kv path (kv:// with alphanumeric/hyphen/slash segments).
|
|
212
|
-
*
|
|
213
|
-
* @param {string} key - kv:// path
|
|
214
|
-
* @returns {boolean}
|
|
215
|
-
*/
|
|
216
|
-
function isValidKvPath(key) {
|
|
217
|
-
return typeof key === 'string' && /^kv:\/\/[a-z0-9][a-z0-9\-/]*$/i.test(key.trim());
|
|
43
|
+
return map;
|
|
218
44
|
}
|
|
219
45
|
|
|
220
46
|
/**
|
|
221
|
-
*
|
|
222
|
-
* @param {
|
|
223
|
-
* @param {
|
|
224
|
-
* @param {
|
|
47
|
+
* @param {string} envFilePath
|
|
48
|
+
* @param {Object} secrets
|
|
49
|
+
* @param {Map<string, string>} itemsByKey
|
|
50
|
+
* @param {string} [systemKey]
|
|
225
51
|
*/
|
|
226
52
|
function buildItemsFromEnv(envFilePath, secrets, itemsByKey, systemKey) {
|
|
227
53
|
if (!envFilePath || typeof envFilePath !== 'string' || !fs.existsSync(envFilePath)) return;
|
|
@@ -231,8 +57,11 @@ function buildItemsFromEnv(envFilePath, secrets, itemsByKey, systemKey) {
|
|
|
231
57
|
const fromEnv = collectKvEnvVarsAsSecretItems(envMap, systemKey);
|
|
232
58
|
for (const { key, value } of fromEnv) {
|
|
233
59
|
const resolved = resolveKvValue(secrets, value);
|
|
234
|
-
|
|
235
|
-
|
|
60
|
+
if (
|
|
61
|
+
isNonEmptySecretValue(resolved) &&
|
|
62
|
+
isValidKvPath(key) &&
|
|
63
|
+
String(resolved).trim() !== key.trim()
|
|
64
|
+
) {
|
|
236
65
|
itemsByKey.set(key, resolved);
|
|
237
66
|
}
|
|
238
67
|
}
|
|
@@ -242,10 +71,9 @@ function buildItemsFromEnv(envFilePath, secrets, itemsByKey, systemKey) {
|
|
|
242
71
|
}
|
|
243
72
|
|
|
244
73
|
/**
|
|
245
|
-
*
|
|
246
|
-
* @param {Object}
|
|
247
|
-
* @param {
|
|
248
|
-
* @param {Map<string, string>} itemsByKey - Mutable map to add items to
|
|
74
|
+
* @param {Object} payload
|
|
75
|
+
* @param {Object} secrets
|
|
76
|
+
* @param {Map<string, string>} itemsByKey
|
|
249
77
|
*/
|
|
250
78
|
function buildItemsFromPayload(payload, secrets, itemsByKey) {
|
|
251
79
|
if (!payload || typeof payload !== 'object') return;
|
|
@@ -255,16 +83,15 @@ function buildItemsFromPayload(payload, secrets, itemsByKey) {
|
|
|
255
83
|
if (existingKeys.has(ref)) continue;
|
|
256
84
|
const pathKey = ref.replace(/^kv:\/\//, '');
|
|
257
85
|
const resolved = secrets[pathKey];
|
|
258
|
-
if (resolved
|
|
86
|
+
if (isNonEmptySecretValue(resolved) && isValidKvPath(ref)) {
|
|
259
87
|
itemsByKey.set(ref, typeof resolved === 'string' ? resolved : String(resolved));
|
|
260
88
|
}
|
|
261
89
|
}
|
|
262
90
|
}
|
|
263
91
|
|
|
264
92
|
/**
|
|
265
|
-
*
|
|
266
|
-
* @param {
|
|
267
|
-
* @param {number} fallback - Fallback when stored not in response
|
|
93
|
+
* @param {Object} res
|
|
94
|
+
* @param {number} fallback
|
|
268
95
|
* @returns {number}
|
|
269
96
|
*/
|
|
270
97
|
function storedCountFromResponse(res, fallback) {
|
|
@@ -274,17 +101,6 @@ function storedCountFromResponse(res, fallback) {
|
|
|
274
101
|
}
|
|
275
102
|
|
|
276
103
|
/**
|
|
277
|
-
* Sends items to dataplane credential API; returns pushed count or warning.
|
|
278
|
-
* @param {string} dataplaneUrl - Dataplane URL
|
|
279
|
-
* @param {Object} authConfig - Auth config
|
|
280
|
-
* @param {Array<{ key: string, value: string }>} items - Items to send
|
|
281
|
-
* @returns {Promise<{ pushed: number, warning?: string }>}
|
|
282
|
-
*/
|
|
283
|
-
/**
|
|
284
|
-
* For bearerToken auth, credential template resolves security.token only.
|
|
285
|
-
* Legacy .env keys KV_*_APIKEY with a plain token wrongly map to kv://.../apiKey.
|
|
286
|
-
* Prefer token path; drop duplicate apiKey when both would be pushed.
|
|
287
|
-
*
|
|
288
104
|
* @param {Map<string, string>} itemsByKey
|
|
289
105
|
* @param {string} [systemKey]
|
|
290
106
|
* @param {Object} [authConfig]
|
|
@@ -309,8 +125,6 @@ function reconcileBearerTokenSecretPaths(itemsByKey, systemKey, authConfig) {
|
|
|
309
125
|
}
|
|
310
126
|
|
|
311
127
|
/**
|
|
312
|
-
* Optional dev/lab dataplane API key (Bearer bypass) for credential secret push only.
|
|
313
|
-
* Does not alter pipeline publish auth when AIFABRIX_DEPLOYMENT_AUTH=client-credentials.
|
|
314
128
|
* @returns {string|null}
|
|
315
129
|
*/
|
|
316
130
|
function resolveLabDataplaneApiKey() {
|
|
@@ -408,6 +222,12 @@ async function handleFailedStoreResponse(dataplaneUrl, authConfig, items, res) {
|
|
|
408
222
|
return storeErrorWarning(res);
|
|
409
223
|
}
|
|
410
224
|
|
|
225
|
+
/**
|
|
226
|
+
* @param {string} dataplaneUrl
|
|
227
|
+
* @param {Object} authConfig
|
|
228
|
+
* @param {Array<{ key: string, value: string }>} items
|
|
229
|
+
* @returns {Promise<{ pushed: number, warning?: string }>}
|
|
230
|
+
*/
|
|
411
231
|
async function sendCredentialSecrets(dataplaneUrl, authConfig, items) {
|
|
412
232
|
try {
|
|
413
233
|
const res = await storeCredentialSecrets(dataplaneUrl, authConfig, items);
|
|
@@ -424,17 +244,10 @@ async function sendCredentialSecrets(dataplaneUrl, authConfig, items) {
|
|
|
424
244
|
}
|
|
425
245
|
|
|
426
246
|
/**
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* @param {string} dataplaneUrl - Dataplane base URL
|
|
432
|
-
* @param {Object} authConfig - Auth config (Bearer)
|
|
433
|
-
* @param {Object} options - Options
|
|
434
|
-
* @param {string} [options.envFilePath] - Path to .env (integration/<systemKey>/.env)
|
|
435
|
-
* @param {string} [options.appName] - App/system name for loadSecrets context
|
|
436
|
-
* @param {Object} [options.payload] - Upload payload { application, dataSources } for kv scan
|
|
437
|
-
* @returns {Promise<{ pushed: number, keys?: string[], skipped?: boolean, warning?: string }>} Count pushed, keys (on success), skipped (when nothing to push), optional warning
|
|
247
|
+
* @param {string} dataplaneUrl
|
|
248
|
+
* @param {Object} authConfig
|
|
249
|
+
* @param {Object} options
|
|
250
|
+
* @returns {Promise<{ pushed: number, keys?: string[], skipped?: boolean, warning?: string }>}
|
|
438
251
|
*/
|
|
439
252
|
async function pushCredentialSecrets(dataplaneUrl, authConfig, options = {}) {
|
|
440
253
|
const { envFilePath, appName, payload } = options;
|
|
@@ -461,29 +274,6 @@ async function pushCredentialSecrets(dataplaneUrl, authConfig, options = {}) {
|
|
|
461
274
|
return sendResult;
|
|
462
275
|
}
|
|
463
276
|
|
|
464
|
-
/**
|
|
465
|
-
* Parses .env-style content into key-value map (first = separates key and value).
|
|
466
|
-
*
|
|
467
|
-
* @param {string} content - Raw .env content
|
|
468
|
-
* @returns {Object.<string, string>}
|
|
469
|
-
*/
|
|
470
|
-
function parseEnvToMap(content) {
|
|
471
|
-
if (!content || typeof content !== 'string') return {};
|
|
472
|
-
const map = {};
|
|
473
|
-
const lines = content.split(/\r?\n/);
|
|
474
|
-
for (const line of lines) {
|
|
475
|
-
const trimmed = line.trim();
|
|
476
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
477
|
-
const eq = trimmed.indexOf('=');
|
|
478
|
-
if (eq > 0) {
|
|
479
|
-
const key = trimmed.substring(0, eq).trim();
|
|
480
|
-
const value = trimmed.substring(eq + 1);
|
|
481
|
-
map[key] = value;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
return map;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
277
|
module.exports = {
|
|
488
278
|
collectKvEnvVarsAsSecretItems,
|
|
489
279
|
collectKvRefsFromPayload,
|
|
@@ -1,123 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dataplane URL Resolver
|
|
3
3
|
*
|
|
4
|
-
* Resolves dataplane URL by discovering from controller
|
|
4
|
+
* Resolves dataplane URL by discovering from controller (application.url in Miso).
|
|
5
|
+
* The controller registration is the source of truth — the CLI does not rewrite ports.
|
|
5
6
|
*
|
|
6
7
|
* @fileoverview Dataplane URL resolution utilities
|
|
7
8
|
* @author AI Fabrix Team
|
|
8
9
|
* @version 2.0.0
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
|
|
14
|
-
const config = require('../core/config');
|
|
15
12
|
const { discoverDataplaneUrl } = require('../commands/wizard-dataplane');
|
|
16
|
-
const pathsUtil = require('./paths');
|
|
17
|
-
const { loadConfigFile } = require('./config-format');
|
|
18
|
-
|
|
19
|
-
/** Matches `templates/applications/dataplane/application.yaml` and `calculateComposePort`. */
|
|
20
|
-
const DEFAULT_DATAPLANE_MANIFEST_PORT = 3001;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {string} url
|
|
24
|
-
* @returns {number|null}
|
|
25
|
-
*/
|
|
26
|
-
function localhostPortFromUrl(url) {
|
|
27
|
-
if (!url || typeof url !== 'string') return null;
|
|
28
|
-
const normalized = url.replace(/127\.0\.0\.1/g, 'localhost');
|
|
29
|
-
const match = normalized.match(/localhost:(\d+)/);
|
|
30
|
-
if (!match) return null;
|
|
31
|
-
const port = parseInt(match[1], 10);
|
|
32
|
-
return Number.isFinite(port) ? port : null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Read dataplane `port` from materialized builder config or bundled template.
|
|
37
|
-
*
|
|
38
|
-
* @returns {number}
|
|
39
|
-
*/
|
|
40
|
-
function loadDataplaneManifestListenPort() {
|
|
41
|
-
const tryLoad = (configPath) => {
|
|
42
|
-
if (!fs.existsSync(configPath)) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
const variables = loadConfigFile(configPath);
|
|
46
|
-
const port = variables && variables.port !== undefined ? Number(variables.port) : NaN;
|
|
47
|
-
return Number.isFinite(port) && port > 0 ? port : null;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const builderPath = pathsUtil.getBuilderPath('dataplane');
|
|
52
|
-
if (fs.existsSync(builderPath)) {
|
|
53
|
-
try {
|
|
54
|
-
const fromBuilder = tryLoad(pathsUtil.resolveApplicationConfigPath(builderPath));
|
|
55
|
-
if (fromBuilder) {
|
|
56
|
-
return fromBuilder;
|
|
57
|
-
}
|
|
58
|
-
} catch {
|
|
59
|
-
// Fall through to bundled template when builder dir exists but config is missing.
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
} catch {
|
|
63
|
-
// ignore
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const templatePath = path.join(
|
|
67
|
-
pathsUtil.getProjectRoot(),
|
|
68
|
-
'templates',
|
|
69
|
-
'applications',
|
|
70
|
-
'dataplane',
|
|
71
|
-
'application.yaml'
|
|
72
|
-
);
|
|
73
|
-
const fromTemplate = tryLoad(templatePath);
|
|
74
|
-
return fromTemplate || DEFAULT_DATAPLANE_MANIFEST_PORT;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Local Docker host port: `application.yaml` `port` (default 3001) + developerId×100.
|
|
79
|
-
* Same rule as {@link calculateComposePort} / `aifabrix up-platform` — not
|
|
80
|
-
* `APP_SERVICE_ENV_DEFAULTS_LOCAL.DATAPLANE_PUBLIC_PORT` (3011), which is for other local profiles.
|
|
81
|
-
*
|
|
82
|
-
* @returns {Promise<string>}
|
|
83
|
-
*/
|
|
84
|
-
async function expectedLocalDataplaneBaseUrl() {
|
|
85
|
-
const developerIdRaw = await config.getDeveloperId();
|
|
86
|
-
const idNum =
|
|
87
|
-
typeof developerIdRaw === 'string'
|
|
88
|
-
? parseInt(developerIdRaw, 10)
|
|
89
|
-
: Number(developerIdRaw);
|
|
90
|
-
const devId = Number.isFinite(idNum) && idNum >= 0 ? idNum : 0;
|
|
91
|
-
const basePort = loadDataplaneManifestListenPort();
|
|
92
|
-
const hostPort = devId === 0 ? basePort : basePort + devId * 100;
|
|
93
|
-
return `http://localhost:${hostPort}`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Controller application.url can lag after dev-id changes; prefer builder host port formula for localhost.
|
|
98
|
-
* @param {string} discoveredUrl
|
|
99
|
-
* @returns {Promise<string>}
|
|
100
|
-
*/
|
|
101
|
-
async function alignDiscoveredDataplaneUrl(discoveredUrl) {
|
|
102
|
-
if (!discoveredUrl || !/localhost|127\.0\.0\.1/i.test(discoveredUrl)) {
|
|
103
|
-
return discoveredUrl;
|
|
104
|
-
}
|
|
105
|
-
try {
|
|
106
|
-
const expectedUrl = await expectedLocalDataplaneBaseUrl();
|
|
107
|
-
const discoveredPort = localhostPortFromUrl(discoveredUrl);
|
|
108
|
-
const expectedPort = localhostPortFromUrl(expectedUrl);
|
|
109
|
-
if (
|
|
110
|
-
discoveredPort !== null &&
|
|
111
|
-
expectedPort !== null &&
|
|
112
|
-
discoveredPort !== expectedPort
|
|
113
|
-
) {
|
|
114
|
-
return expectedUrl;
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
// Keep controller-discovered URL when developer config is unavailable.
|
|
118
|
-
}
|
|
119
|
-
return discoveredUrl;
|
|
120
|
-
}
|
|
121
13
|
|
|
122
14
|
/**
|
|
123
15
|
* @returns {string|null}
|
|
@@ -146,12 +38,9 @@ async function resolveDataplaneUrl(controllerUrl, environment, authConfig, opts
|
|
|
146
38
|
if (envUrl) {
|
|
147
39
|
return envUrl;
|
|
148
40
|
}
|
|
149
|
-
|
|
150
|
-
return alignDiscoveredDataplaneUrl(discovered);
|
|
41
|
+
return discoverDataplaneUrl(controllerUrl, environment, authConfig, opts);
|
|
151
42
|
}
|
|
152
43
|
|
|
153
44
|
module.exports = {
|
|
154
|
-
resolveDataplaneUrl
|
|
155
|
-
expectedLocalDataplaneBaseUrl,
|
|
156
|
-
alignDiscoveredDataplaneUrl
|
|
45
|
+
resolveDataplaneUrl
|
|
157
46
|
};
|
|
@@ -40,8 +40,31 @@ function localHostPort(appPort, developerIdNum) {
|
|
|
40
40
|
return publishedHostPort(appPort, developerIdNum);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Host port for local dataplane Docker (`aifabrix up-dataplane` / `run --env dev|tst`).
|
|
45
|
+
* Matches {@link computeDataplanePortOverride} in up-dataplane.js (+10 dev, +20 tst).
|
|
46
|
+
*
|
|
47
|
+
* @param {number} appPort - Manifest listen port (dataplane default 3001)
|
|
48
|
+
* @param {number} developerIdNum
|
|
49
|
+
* @param {string} [environmentKey] - dev | tst | other
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
function dataplaneDockerHostPort(appPort, developerIdNum, environmentKey = 'dev') {
|
|
53
|
+
const id = Number.isFinite(developerIdNum) && developerIdNum >= 0 ? developerIdNum : 0;
|
|
54
|
+
const baseHostPort = appPort + id * 100;
|
|
55
|
+
const env = String(environmentKey || '').trim().toLowerCase();
|
|
56
|
+
if (env === 'dev') {
|
|
57
|
+
return baseHostPort + 10;
|
|
58
|
+
}
|
|
59
|
+
if (env === 'tst') {
|
|
60
|
+
return baseHostPort + 20;
|
|
61
|
+
}
|
|
62
|
+
return id === 0 ? appPort : baseHostPort;
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
module.exports = {
|
|
44
66
|
parseDeveloperIdNum,
|
|
45
67
|
publishedHostPort,
|
|
46
|
-
localHostPort
|
|
68
|
+
localHostPort,
|
|
69
|
+
dataplaneDockerHostPort
|
|
47
70
|
};
|
|
@@ -264,10 +264,7 @@ async function executeDockerBuildWithTag(effectiveImageName, imageName, dockerfi
|
|
|
264
264
|
|
|
265
265
|
await executeBuild(effectiveImageName, dockerfilePath, contextPath, tag, options);
|
|
266
266
|
|
|
267
|
-
const wantCompatTag =
|
|
268
|
-
effectiveImageName !== imageName &&
|
|
269
|
-
options &&
|
|
270
|
-
options.base === true;
|
|
267
|
+
const wantCompatTag = effectiveImageName !== imageName && imageName;
|
|
271
268
|
if (!wantCompatTag) {
|
|
272
269
|
return;
|
|
273
270
|
}
|
|
@@ -13,6 +13,7 @@ const path = require('path');
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const Handlebars = require('handlebars');
|
|
15
15
|
const { systemKeyToKvPrefix, kvEnvKeyToPath, securityKeyToVar } = require('./credential-secrets-env');
|
|
16
|
+
const { getAuthCredentialTemplate } = require('./authentication-shape');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Builds hint string from portalInput (options → enum, validation → min-max or pattern).
|
|
@@ -63,9 +64,15 @@ function buildAuthSecureVarsFromSystem(system) {
|
|
|
63
64
|
const systemKey = system?.key || 'external-system';
|
|
64
65
|
const prefix = systemKeyToKvPrefix(systemKey);
|
|
65
66
|
if (!prefix) return authSecureVars;
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
67
|
+
const authBlock = system?.authentication || system?.auth;
|
|
68
|
+
const credential = getAuthCredentialTemplate(authBlock);
|
|
69
|
+
const security = credential?.security || authBlock?.security;
|
|
70
|
+
const authMethod = (
|
|
71
|
+
credential?.method ||
|
|
72
|
+
authBlock?.method ||
|
|
73
|
+
authBlock?.type ||
|
|
74
|
+
'apikey'
|
|
75
|
+
).toLowerCase();
|
|
69
76
|
if (security && typeof security === 'object' && Object.keys(security).length > 0) {
|
|
70
77
|
for (const key of Object.keys(security)) {
|
|
71
78
|
const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
|
|
@@ -392,8 +392,7 @@ function validateAgainstSchema(data, schema) {
|
|
|
392
392
|
validateSchema: false
|
|
393
393
|
});
|
|
394
394
|
addFormats(ajv);
|
|
395
|
-
//
|
|
396
|
-
// Adding them up-front keeps validation offline-safe and avoids unresolved $ref failures.
|
|
395
|
+
// Type schemas are referenced by $id from external-system and external-datasource manifests.
|
|
397
396
|
ajv.addSchema(require('../schema/type/document-storage.json'));
|
|
398
397
|
ajv.addSchema(require('../schema/type/message-service.json'));
|
|
399
398
|
ajv.addSchema(require('../schema/type/vector-store.json'));
|
|
@@ -404,6 +403,13 @@ function validateAgainstSchema(data, schema) {
|
|
|
404
403
|
}
|
|
405
404
|
// Normalize schema to handle nullable without type
|
|
406
405
|
const normalizedSchema = normalizeSchema(schemaCopy);
|
|
406
|
+
const schemaId = normalizedSchema.$id || '';
|
|
407
|
+
if (schemaId.includes('external-datasource')) {
|
|
408
|
+
const externalSystemSchema = require('../schema/external-system.schema.json');
|
|
409
|
+
if (!ajv.getSchema(externalSystemSchema.$id)) {
|
|
410
|
+
ajv.addSchema(externalSystemSchema, externalSystemSchema.$id);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
407
413
|
const validate = ajv.compile(normalizedSchema);
|
|
408
414
|
const valid = validate(data);
|
|
409
415
|
|
|
@@ -76,6 +76,7 @@ const CATEGORIES = [
|
|
|
76
76
|
{ name: 'integration-client' },
|
|
77
77
|
{ name: 'dimension' },
|
|
78
78
|
{ name: 'dimension value', term: 'dimension value <command>' },
|
|
79
|
+
{ name: 'resource-type' },
|
|
79
80
|
{ name: 'identity' },
|
|
80
81
|
{ name: 'protection' }
|
|
81
82
|
]
|
|
@@ -101,6 +102,7 @@ const CATEGORIES = [
|
|
|
101
102
|
{ name: 'upload', term: 'upload <systemKey>' },
|
|
102
103
|
{ name: 'delete', term: 'delete <systemKey>' },
|
|
103
104
|
{ name: 'repair', term: 'repair <systemKey>' },
|
|
105
|
+
{ name: 'channel', term: 'channel scaffold <channelTarget>' },
|
|
104
106
|
{ name: 'datasource' },
|
|
105
107
|
{ name: 'datasource load', term: 'datasource load <datasourceKey>' },
|
|
106
108
|
{ name: 'datasource export', term: 'datasource export <datasourceKey>' },
|