@enterpriseai/cli 3.6.10 → 3.7.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/dist/commands/init.js +66 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/provision.js +20 -3
- package/dist/commands/provision.js.map +1 -1
- package/dist/commands/vertical.d.ts +95 -0
- package/dist/commands/vertical.d.ts.map +1 -1
- package/dist/commands/vertical.js +695 -5
- package/dist/commands/vertical.js.map +1 -1
- package/dist/lib/api.d.ts +79 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +22 -0
- package/dist/lib/api.js.map +1 -1
- package/package.json +2 -2
- package/resources/linked-sources.json +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { Command } from 'commander';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { resolveCommandContext, normalizeFormat, makeSpinner } from '../lib/context.js';
|
|
11
|
-
import { PlatformAPIClient } from '../lib/api.js';
|
|
11
|
+
import { PlatformAPIClient, } from '../lib/api.js';
|
|
12
12
|
import { resolveActiveTenantContext, resolveMainCompanyTenantId, resolvePublicApiUrl, } from '../lib/tenant-context.js';
|
|
13
13
|
import { findProjectRoot, patchEnvFile } from '../lib/config.js';
|
|
14
14
|
import { errMsg, isRecord, normalizeChildTenantDisplayNameOption, normalizeChildTenantSlugOption, toObjectTypeSlug, } from '../lib/utils.js';
|
|
@@ -17,6 +17,8 @@ const VERTICAL_ENROLLMENT_TYPE = 'tenant-vertical-enrollment';
|
|
|
17
17
|
const DEFAULT_VERTICAL_SOURCE = ['eai', 'cli'].join('-');
|
|
18
18
|
const APP_KEY_ENV = 'EAI_APP_KEY';
|
|
19
19
|
const LEGACY_VERTICAL_KEY_ENV = 'EAI_VERTICAL_KEY';
|
|
20
|
+
const DEFAULT_SOURCE_UNKNOWN_GITHUB_OIDC_AUDIENCE = 'api://enterprise-ai-publicapi/source-unknown';
|
|
21
|
+
const DEFAULT_SOURCE_UNKNOWN_RUNTIME_PATH = 'eai.runtime.json';
|
|
20
22
|
export function buildVerticalEnrollmentData(name, tenantId, options) {
|
|
21
23
|
const displayName = name.trim();
|
|
22
24
|
const verticalKey = (options.key || toObjectTypeSlug(displayName)).trim();
|
|
@@ -36,6 +38,289 @@ export function buildVerticalEnrollmentData(name, tenantId, options) {
|
|
|
36
38
|
...(options.appUrl ? { appUrl: options.appUrl } : {}),
|
|
37
39
|
};
|
|
38
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse the GitHub repository identifier accepted by Admin Portal export jobs.
|
|
43
|
+
*/
|
|
44
|
+
export function parseRepositorySlug(repo) {
|
|
45
|
+
const normalized = repo
|
|
46
|
+
.trim()
|
|
47
|
+
.replace(/^git@github\.com:/, '')
|
|
48
|
+
.replace(/^https:\/\/github\.com\//, '')
|
|
49
|
+
.replace(/\.git$/, '');
|
|
50
|
+
const [owner, name, extra] = normalized.split('/');
|
|
51
|
+
if (!owner || !name || extra) {
|
|
52
|
+
throw new Error('Repository must be in owner/name form.');
|
|
53
|
+
}
|
|
54
|
+
return { owner, name };
|
|
55
|
+
}
|
|
56
|
+
function defaultRepoUrl(owner, name) {
|
|
57
|
+
return `https://github.com/${owner}/${name}`;
|
|
58
|
+
}
|
|
59
|
+
function normaliseOptionalString(value) {
|
|
60
|
+
const normalized = value?.trim();
|
|
61
|
+
return normalized || undefined;
|
|
62
|
+
}
|
|
63
|
+
function assertSha256Digest(value, field) {
|
|
64
|
+
if (!/^sha256:[a-fA-F0-9]{64}$/.test(value)) {
|
|
65
|
+
throw new Error(`${field} must be a sha256:<64 hex chars> digest.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function assertGitCommitSha(value, field) {
|
|
69
|
+
if (!/^[a-fA-F0-9]{40}$/.test(value)) {
|
|
70
|
+
throw new Error(`${field} must be a 40 character git commit SHA.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function buildSchemaProvenance(options) {
|
|
74
|
+
const schemaDigest = normaliseOptionalString(options.schemaDigest);
|
|
75
|
+
const validatorDigest = normaliseOptionalString(options.validatorDigest);
|
|
76
|
+
const templateVersion = normaliseOptionalString(options.templateVersion);
|
|
77
|
+
const baseTemplateSha = normaliseOptionalString(options.baseTemplateSha);
|
|
78
|
+
const approvedSourceSha = normaliseOptionalString(options.approvedSourceSha);
|
|
79
|
+
const approvedReleaseId = normaliseOptionalString(options.approvedRelease);
|
|
80
|
+
const hasAny = schemaDigest ||
|
|
81
|
+
validatorDigest ||
|
|
82
|
+
templateVersion ||
|
|
83
|
+
baseTemplateSha ||
|
|
84
|
+
approvedSourceSha ||
|
|
85
|
+
approvedReleaseId;
|
|
86
|
+
if (!hasAny)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (!schemaDigest || !validatorDigest) {
|
|
89
|
+
throw new Error('Schema provenance requires --schema-digest and --validator-digest.');
|
|
90
|
+
}
|
|
91
|
+
if (!templateVersion) {
|
|
92
|
+
throw new Error('Schema provenance requires --template-version.');
|
|
93
|
+
}
|
|
94
|
+
if (!baseTemplateSha && !approvedSourceSha && !approvedReleaseId) {
|
|
95
|
+
throw new Error('Schema provenance requires --base-template-sha, --approved-source-sha, or --approved-release.');
|
|
96
|
+
}
|
|
97
|
+
assertSha256Digest(schemaDigest, '--schema-digest');
|
|
98
|
+
assertSha256Digest(validatorDigest, '--validator-digest');
|
|
99
|
+
if (baseTemplateSha)
|
|
100
|
+
assertGitCommitSha(baseTemplateSha, '--base-template-sha');
|
|
101
|
+
if (approvedSourceSha)
|
|
102
|
+
assertGitCommitSha(approvedSourceSha, '--approved-source-sha');
|
|
103
|
+
return {
|
|
104
|
+
templateVersion,
|
|
105
|
+
...(baseTemplateSha ? { baseTemplateSha } : {}),
|
|
106
|
+
...(approvedSourceSha ? { approvedSourceSha } : {}),
|
|
107
|
+
...(approvedReleaseId ? { approvedReleaseId } : {}),
|
|
108
|
+
schemaDigest,
|
|
109
|
+
validatorDigest,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build the PublicAPI payload used to store source-unknown app repo metadata.
|
|
114
|
+
*/
|
|
115
|
+
export function buildSourceUnknownRegistrationData(options) {
|
|
116
|
+
const repo = parseRepositorySlug(options.repo);
|
|
117
|
+
const defaultBranch = (options.branch || 'main').trim();
|
|
118
|
+
if (!defaultBranch) {
|
|
119
|
+
throw new Error('Default branch is required.');
|
|
120
|
+
}
|
|
121
|
+
const schemaProvenance = buildSchemaProvenance(options);
|
|
122
|
+
return {
|
|
123
|
+
repoOwner: repo.owner,
|
|
124
|
+
repoName: repo.name,
|
|
125
|
+
repoUrl: options.repoUrl?.trim() || defaultRepoUrl(repo.owner, repo.name),
|
|
126
|
+
defaultBranch,
|
|
127
|
+
workflowPath: options.workflow?.trim() || '.github/workflows/eai-app.yml',
|
|
128
|
+
ref: options.ref?.trim() || `refs/heads/${defaultBranch}`,
|
|
129
|
+
...(options.commit?.trim() ? { commitSha: options.commit.trim() } : {}),
|
|
130
|
+
configPath: options.config?.trim() || 'src/eai.config/index.ts',
|
|
131
|
+
runtimePath: options.runtime?.trim() || DEFAULT_SOURCE_UNKNOWN_RUNTIME_PATH,
|
|
132
|
+
sourceMode: 'source-unknown',
|
|
133
|
+
...(schemaProvenance ? { schemaProvenance } : {}),
|
|
134
|
+
validationSummary: {
|
|
135
|
+
status: 'registered_by_cli',
|
|
136
|
+
appValidated: !options.skipValidate,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function buildSourceUnknownAdoptObservedData(options) {
|
|
141
|
+
const registration = buildSourceUnknownRegistrationData(options);
|
|
142
|
+
const activeUrl = options.url.trim();
|
|
143
|
+
const environment = (options.environment || 'production').trim();
|
|
144
|
+
if (!activeUrl) {
|
|
145
|
+
throw new Error('Observed app URL is required.');
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
new URL(activeUrl);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
throw new Error('Observed app URL must be an absolute URL.');
|
|
152
|
+
}
|
|
153
|
+
if (!environment) {
|
|
154
|
+
throw new Error('Observed environment is required.');
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
...registration,
|
|
158
|
+
adoptionMode: 'adopted-observed',
|
|
159
|
+
observedDeployment: {
|
|
160
|
+
environment,
|
|
161
|
+
activeUrl,
|
|
162
|
+
status: 'adopted_observed',
|
|
163
|
+
observedAt: options.observedAt?.trim() || new Date().toISOString(),
|
|
164
|
+
...(options.deploymentId?.trim() ? { deploymentId: options.deploymentId.trim() } : {}),
|
|
165
|
+
...(options.imageDigest?.trim() ? { imageDigest: options.imageDigest.trim() } : {}),
|
|
166
|
+
...(options.configHash?.trim() ? { configHash: options.configHash.trim() } : {}),
|
|
167
|
+
},
|
|
168
|
+
validationSummary: {
|
|
169
|
+
status: 'adopted_observed_by_cli',
|
|
170
|
+
appValidated: !options.skipValidate,
|
|
171
|
+
destructiveOperationsBlocked: true,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export function buildSourceUnknownWorkflowSetupData(options) {
|
|
176
|
+
const environment = (options.environment || 'preview').trim();
|
|
177
|
+
if (!environment) {
|
|
178
|
+
throw new Error('Workflow setup environment is required.');
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
environment,
|
|
182
|
+
workflowPath: options.workflow?.trim() || '.github/workflows/eai-app.yml',
|
|
183
|
+
...(options.ref?.trim() ? { ref: options.ref.trim() } : {}),
|
|
184
|
+
...(options.commit?.trim() ? { commitSha: options.commit.trim() } : {}),
|
|
185
|
+
...(options.configHash?.trim() ? { configHash: options.configHash.trim() } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export function buildSourceUnknownWorkflowEvidenceData(options) {
|
|
189
|
+
const repo = parseRepositorySlug(options.repo);
|
|
190
|
+
const operationId = options.operationId?.trim();
|
|
191
|
+
const nonce = options.nonce?.trim();
|
|
192
|
+
const environment = (options.environment || 'preview').trim();
|
|
193
|
+
const defaultBranch = (options.branch || 'main').trim();
|
|
194
|
+
const workflowPath = options.workflow?.trim() || '.github/workflows/eai-app.yml';
|
|
195
|
+
const ref = options.ref?.trim() || `refs/heads/${defaultBranch}`;
|
|
196
|
+
const commitSha = options.commit?.trim();
|
|
197
|
+
const configHash = options.configHash?.trim();
|
|
198
|
+
const artifactDigest = options.artifactDigest?.trim();
|
|
199
|
+
const imageDigest = options.imageDigest?.trim();
|
|
200
|
+
if (!operationId)
|
|
201
|
+
throw new Error('Workflow evidence operation ID is required.');
|
|
202
|
+
if (!nonce)
|
|
203
|
+
throw new Error('Workflow evidence nonce is required.');
|
|
204
|
+
if (!environment)
|
|
205
|
+
throw new Error('Workflow evidence environment is required.');
|
|
206
|
+
if (!defaultBranch)
|
|
207
|
+
throw new Error('Default branch is required.');
|
|
208
|
+
if (!commitSha)
|
|
209
|
+
throw new Error('Workflow evidence commit SHA is required.');
|
|
210
|
+
if (!configHash)
|
|
211
|
+
throw new Error('Workflow evidence config hash is required.');
|
|
212
|
+
if (!artifactDigest)
|
|
213
|
+
throw new Error('Workflow evidence artifact digest is required.');
|
|
214
|
+
if (!imageDigest)
|
|
215
|
+
throw new Error('Workflow evidence image digest is required.');
|
|
216
|
+
assertSha256Digest(artifactDigest, '--artifact-digest');
|
|
217
|
+
assertSha256Digest(imageDigest, '--image-digest');
|
|
218
|
+
const schemaProvenance = buildSchemaProvenance(options);
|
|
219
|
+
if (!schemaProvenance) {
|
|
220
|
+
throw new Error('Workflow evidence requires schema provenance: provide --template-version, --schema-digest, --validator-digest, and an approved source anchor.');
|
|
221
|
+
}
|
|
222
|
+
const workflowRun = {};
|
|
223
|
+
if (options.workflowRunId?.trim())
|
|
224
|
+
workflowRun.id = options.workflowRunId.trim();
|
|
225
|
+
if (options.workflowRunAttempt?.trim()) {
|
|
226
|
+
const attempt = Number(options.workflowRunAttempt.trim());
|
|
227
|
+
workflowRun.attempt = Number.isFinite(attempt) ? attempt : options.workflowRunAttempt.trim();
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
operationId,
|
|
231
|
+
nonce,
|
|
232
|
+
environment,
|
|
233
|
+
workflowPath,
|
|
234
|
+
ref,
|
|
235
|
+
commitSha,
|
|
236
|
+
configHash,
|
|
237
|
+
artifactDigest,
|
|
238
|
+
imageDigest,
|
|
239
|
+
schemaProvenance,
|
|
240
|
+
...(Object.keys(workflowRun).length > 0 ? { workflowRun } : {}),
|
|
241
|
+
oidcClaims: {
|
|
242
|
+
repository: `${repo.owner}/${repo.name}`,
|
|
243
|
+
ref,
|
|
244
|
+
sha: commitSha,
|
|
245
|
+
workflow_ref: `${repo.owner}/${repo.name}/${workflowPath}@${ref}`,
|
|
246
|
+
...(workflowRun.id ? { run_id: String(workflowRun.id) } : {}),
|
|
247
|
+
...(workflowRun.attempt ? { run_attempt: String(workflowRun.attempt) } : {}),
|
|
248
|
+
},
|
|
249
|
+
validationSummary: {
|
|
250
|
+
status: 'passed_by_cli',
|
|
251
|
+
appValidated: !options.skipValidate,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
export function buildSourceUnknownDeploymentData(options) {
|
|
256
|
+
const operationId = options.operationId?.trim();
|
|
257
|
+
const environment = (options.environment || 'preview').trim();
|
|
258
|
+
const repo = options.repo?.trim() ? parseRepositorySlug(options.repo) : undefined;
|
|
259
|
+
const workflowRunId = normaliseOptionalString(options.workflowRunId);
|
|
260
|
+
const artifactDigest = options.artifactDigest?.trim();
|
|
261
|
+
const imageDigest = options.imageDigest?.trim();
|
|
262
|
+
if (!operationId)
|
|
263
|
+
throw new Error('Deployment handoff operation ID is required.');
|
|
264
|
+
if (!environment)
|
|
265
|
+
throw new Error('Deployment handoff environment is required.');
|
|
266
|
+
if (artifactDigest)
|
|
267
|
+
assertSha256Digest(artifactDigest, '--artifact-digest');
|
|
268
|
+
if (imageDigest)
|
|
269
|
+
assertSha256Digest(imageDigest, '--image-digest');
|
|
270
|
+
const deploymentTarget = {};
|
|
271
|
+
const targetKind = normaliseOptionalString(options.targetKind);
|
|
272
|
+
const releaseChannel = normaliseOptionalString(options.releaseChannel);
|
|
273
|
+
if (targetKind)
|
|
274
|
+
deploymentTarget.kind = targetKind;
|
|
275
|
+
if (releaseChannel)
|
|
276
|
+
deploymentTarget.releaseChannel = releaseChannel;
|
|
277
|
+
return {
|
|
278
|
+
operationId,
|
|
279
|
+
environment,
|
|
280
|
+
...(repo ? { repoOwner: repo.owner, repoName: repo.name } : {}),
|
|
281
|
+
...(options.workflow?.trim() ? { workflowPath: options.workflow.trim() } : {}),
|
|
282
|
+
...(options.ref?.trim() ? { ref: options.ref.trim() } : {}),
|
|
283
|
+
...(options.commit?.trim() ? { commitSha: options.commit.trim() } : {}),
|
|
284
|
+
...(workflowRunId ? { workflowRunId } : {}),
|
|
285
|
+
...(options.configHash?.trim() ? { configHash: options.configHash.trim() } : {}),
|
|
286
|
+
...(artifactDigest ? { artifactDigest } : {}),
|
|
287
|
+
...(imageDigest ? { imageDigest } : {}),
|
|
288
|
+
...(Object.keys(deploymentTarget).length > 0 ? { deploymentTarget } : {}),
|
|
289
|
+
validationSummary: {
|
|
290
|
+
status: 'deployment_requested_by_cli',
|
|
291
|
+
appValidated: !options.skipValidate,
|
|
292
|
+
requiresTenantInfra: true,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
async function resolveGitHubOidcToken(options) {
|
|
297
|
+
const explicitToken = normaliseOptionalString(options.githubOidcToken);
|
|
298
|
+
if (explicitToken)
|
|
299
|
+
return explicitToken;
|
|
300
|
+
const envToken = normaliseOptionalString(process.env['GITHUB_ID_TOKEN']);
|
|
301
|
+
if (envToken)
|
|
302
|
+
return envToken;
|
|
303
|
+
const requestUrlEnvKey = ['ACTIONS', 'ID', 'TOKEN', 'REQUEST', 'URL'].join('_');
|
|
304
|
+
const requestTokenEnvKey = ['ACTIONS', 'ID', 'TOKEN', 'REQUEST', 'TOKEN'].join('_');
|
|
305
|
+
const requestUrl = normaliseOptionalString(process.env[requestUrlEnvKey]);
|
|
306
|
+
const requestToken = normaliseOptionalString(process.env[requestTokenEnvKey]);
|
|
307
|
+
if (!requestUrl || !requestToken) {
|
|
308
|
+
throw new Error('Workflow evidence requires --github-oidc-token or GitHub Actions OIDC request environment variables.');
|
|
309
|
+
}
|
|
310
|
+
const audience = normaliseOptionalString(options.githubOidcAudience) || DEFAULT_SOURCE_UNKNOWN_GITHUB_OIDC_AUDIENCE;
|
|
311
|
+
const separator = requestUrl.includes('?') ? '&' : '?';
|
|
312
|
+
const response = await fetch(`${requestUrl}${separator}audience=${encodeURIComponent(audience)}`, {
|
|
313
|
+
headers: { Authorization: `Bearer ${requestToken}` },
|
|
314
|
+
});
|
|
315
|
+
if (!response.ok) {
|
|
316
|
+
throw new Error(`GitHub OIDC token request failed: ${response.status} ${response.statusText}`);
|
|
317
|
+
}
|
|
318
|
+
const payload = await response.json();
|
|
319
|
+
if (!isRecord(payload) || typeof payload.value !== 'string' || !payload.value.trim()) {
|
|
320
|
+
throw new Error('GitHub OIDC token response did not include a token value.');
|
|
321
|
+
}
|
|
322
|
+
return payload.value.trim();
|
|
323
|
+
}
|
|
39
324
|
function extractDocs(payload) {
|
|
40
325
|
if (!isRecord(payload))
|
|
41
326
|
return [];
|
|
@@ -75,8 +360,8 @@ async function resolveAppManagementContext(options) {
|
|
|
75
360
|
tenantId: context.activeTenant.id,
|
|
76
361
|
};
|
|
77
362
|
}
|
|
78
|
-
async function validateVerticalEnrollment(verticalKey,
|
|
79
|
-
const res = await
|
|
363
|
+
async function validateVerticalEnrollment(verticalKey, client) {
|
|
364
|
+
const res = await client.listResources(VERTICAL_ENROLLMENT_TYPE, {
|
|
80
365
|
limit: 1,
|
|
81
366
|
where: { verticalKey },
|
|
82
367
|
});
|
|
@@ -195,6 +480,411 @@ verticalCommand
|
|
|
195
480
|
out.info(`App tenant: ${chalk.cyan(immediateParentTenantId)}`);
|
|
196
481
|
}
|
|
197
482
|
});
|
|
483
|
+
verticalCommand
|
|
484
|
+
.command('connect-existing <key>')
|
|
485
|
+
.description('Register an existing app repository for managed deployment')
|
|
486
|
+
.requiredOption('--repo <owner/repo>', 'GitHub repository to connect')
|
|
487
|
+
.option('--tenant-id <id>', 'Run against a specific company tenant')
|
|
488
|
+
.option('--repo-url <url>', 'Repository URL when it differs from https://github.com/owner/repo')
|
|
489
|
+
.option('--branch <branch>', 'Default branch', 'main')
|
|
490
|
+
.option('--workflow <path>', 'GitHub Actions workflow path', '.github/workflows/eai-app.yml')
|
|
491
|
+
.option('--ref <ref>', 'Approved git ref (defaults to refs/heads/<branch>)')
|
|
492
|
+
.option('--commit <sha>', 'Current commit SHA to bind')
|
|
493
|
+
.option('--config <path>', 'eai.config path', 'src/eai.config/index.ts')
|
|
494
|
+
.option('--runtime <path>', 'eai.runtime path', DEFAULT_SOURCE_UNKNOWN_RUNTIME_PATH)
|
|
495
|
+
.option('--template-version <version>', 'Approved schema/template version')
|
|
496
|
+
.option('--base-template-sha <sha>', 'Base eai-app-template commit SHA when known')
|
|
497
|
+
.option('--approved-source-sha <sha>', 'Approved source commit SHA for non-template apps')
|
|
498
|
+
.option('--approved-release <id>', 'Approved schema/validator release identifier')
|
|
499
|
+
.option('--schema-digest <digest>', 'Approved schema digest in sha256:<hex> form')
|
|
500
|
+
.option('--validator-digest <digest>', 'Approved validator digest in sha256:<hex> form')
|
|
501
|
+
.option('--skip-validate', 'Skip app lookup', false)
|
|
502
|
+
.option('--format <format>', 'Output format (text|json)', 'text')
|
|
503
|
+
.option('--json', 'Output raw JSON (deprecated, use --format json)', false)
|
|
504
|
+
.action(async (key, options) => {
|
|
505
|
+
const ctx = await resolveAppManagementContext({ tenantId: options.tenantId, interactive: !options.tenantId });
|
|
506
|
+
const companyTenantId = options.tenantId
|
|
507
|
+
? ctx.tenantId
|
|
508
|
+
: await resolveMainCompanyTenantId(ctx.publicApiUrl, ctx.tenantId);
|
|
509
|
+
const client = new PlatformAPIClient(ctx.publicApiUrl, companyTenantId);
|
|
510
|
+
const format = normalizeFormat(options);
|
|
511
|
+
const appKey = key.trim();
|
|
512
|
+
if (!appKey) {
|
|
513
|
+
fail('App key is required.');
|
|
514
|
+
}
|
|
515
|
+
let registration;
|
|
516
|
+
try {
|
|
517
|
+
registration = buildSourceUnknownRegistrationData(options);
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
fail(errMsg(err));
|
|
521
|
+
}
|
|
522
|
+
if (!options.skipValidate) {
|
|
523
|
+
await validateVerticalEnrollment(appKey, client);
|
|
524
|
+
}
|
|
525
|
+
const spinner = makeSpinner(format, `Connecting ${appKey} to ${registration.repoOwner}/${registration.repoName}...`);
|
|
526
|
+
const res = await client.registerSourceUnknownApp(companyTenantId, appKey, registration);
|
|
527
|
+
const payload = await readResponsePayload(res);
|
|
528
|
+
if (!res.ok) {
|
|
529
|
+
spinner?.fail('Failed to connect existing app repository');
|
|
530
|
+
fail(isRecord(payload) && typeof payload.message === 'string' ? payload.message : `${res.status} ${res.statusText}`);
|
|
531
|
+
}
|
|
532
|
+
if (format === 'json') {
|
|
533
|
+
out.json({
|
|
534
|
+
tenantId: companyTenantId,
|
|
535
|
+
appKey,
|
|
536
|
+
sourceMode: 'source-unknown',
|
|
537
|
+
repository: {
|
|
538
|
+
owner: registration.repoOwner,
|
|
539
|
+
name: registration.repoName,
|
|
540
|
+
url: registration.repoUrl,
|
|
541
|
+
},
|
|
542
|
+
request: registration,
|
|
543
|
+
response: payload,
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
spinner?.succeed(`Connected existing repository for ${chalk.cyan(appKey)}`);
|
|
548
|
+
out.info(`Repository: ${chalk.cyan(`${registration.repoOwner}/${registration.repoName}`)}`);
|
|
549
|
+
out.info(`Branch: ${chalk.cyan(String(registration.defaultBranch))} · Ref: ${chalk.dim(String(registration.ref))}`);
|
|
550
|
+
out.info(`Workflow: ${chalk.cyan(String(registration.workflowPath))}`);
|
|
551
|
+
});
|
|
552
|
+
verticalCommand
|
|
553
|
+
.command('adopt-observed <key>')
|
|
554
|
+
.description('Import an already-running app as read-only observed infrastructure')
|
|
555
|
+
.requiredOption('--repo <owner/repo>', 'GitHub repository to connect')
|
|
556
|
+
.requiredOption('--url <url>', 'Currently active observed app URL')
|
|
557
|
+
.option('--tenant-id <id>', 'Run against a specific company tenant')
|
|
558
|
+
.option('--repo-url <url>', 'Repository URL when it differs from https://github.com/owner/repo')
|
|
559
|
+
.option('--environment <environment>', 'Observed deployment environment', 'production')
|
|
560
|
+
.option('--branch <branch>', 'Default branch', 'main')
|
|
561
|
+
.option('--workflow <path>', 'GitHub Actions workflow path', '.github/workflows/eai-app.yml')
|
|
562
|
+
.option('--ref <ref>', 'Approved git ref (defaults to refs/heads/<branch>)')
|
|
563
|
+
.option('--commit <sha>', 'Current commit SHA to bind')
|
|
564
|
+
.option('--config <path>', 'eai.config path', 'src/eai.config/index.ts')
|
|
565
|
+
.option('--runtime <path>', 'eai.runtime path', DEFAULT_SOURCE_UNKNOWN_RUNTIME_PATH)
|
|
566
|
+
.option('--template-version <version>', 'Approved schema/template version')
|
|
567
|
+
.option('--base-template-sha <sha>', 'Base eai-app-template commit SHA when known')
|
|
568
|
+
.option('--approved-source-sha <sha>', 'Approved source commit SHA for non-template apps')
|
|
569
|
+
.option('--approved-release <id>', 'Approved schema/validator release identifier')
|
|
570
|
+
.option('--schema-digest <digest>', 'Approved schema digest in sha256:<hex> form')
|
|
571
|
+
.option('--validator-digest <digest>', 'Approved validator digest in sha256:<hex> form')
|
|
572
|
+
.option('--deployment-id <id>', 'Observed deployment identifier')
|
|
573
|
+
.option('--image-digest <digest>', 'Observed immutable image digest')
|
|
574
|
+
.option('--config-hash <hash>', 'Observed config hash')
|
|
575
|
+
.option('--observed-at <iso>', 'Observation timestamp')
|
|
576
|
+
.option('--skip-validate', 'Skip app lookup', false)
|
|
577
|
+
.option('--format <format>', 'Output format (text|json)', 'text')
|
|
578
|
+
.option('--json', 'Output raw JSON (deprecated, use --format json)', false)
|
|
579
|
+
.action(async (key, options) => {
|
|
580
|
+
const ctx = await resolveAppManagementContext({ tenantId: options.tenantId, interactive: !options.tenantId });
|
|
581
|
+
const companyTenantId = options.tenantId
|
|
582
|
+
? ctx.tenantId
|
|
583
|
+
: await resolveMainCompanyTenantId(ctx.publicApiUrl, ctx.tenantId);
|
|
584
|
+
const client = new PlatformAPIClient(ctx.publicApiUrl, companyTenantId);
|
|
585
|
+
const format = normalizeFormat(options);
|
|
586
|
+
const appKey = key.trim();
|
|
587
|
+
if (!appKey) {
|
|
588
|
+
fail('App key is required.');
|
|
589
|
+
}
|
|
590
|
+
let registration;
|
|
591
|
+
try {
|
|
592
|
+
registration = buildSourceUnknownAdoptObservedData(options);
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
fail(errMsg(err));
|
|
596
|
+
}
|
|
597
|
+
if (!options.skipValidate) {
|
|
598
|
+
await validateVerticalEnrollment(appKey, client);
|
|
599
|
+
}
|
|
600
|
+
const spinner = makeSpinner(format, `Adopting observed app ${appKey} from ${registration.repoOwner}/${registration.repoName}...`);
|
|
601
|
+
const res = await client.registerSourceUnknownApp(companyTenantId, appKey, registration);
|
|
602
|
+
const payload = await readResponsePayload(res);
|
|
603
|
+
if (!res.ok) {
|
|
604
|
+
spinner?.fail('Failed to adopt observed app');
|
|
605
|
+
fail(isRecord(payload) && typeof payload.message === 'string' ? payload.message : `${res.status} ${res.statusText}`);
|
|
606
|
+
}
|
|
607
|
+
if (format === 'json') {
|
|
608
|
+
out.json({
|
|
609
|
+
tenantId: companyTenantId,
|
|
610
|
+
appKey,
|
|
611
|
+
sourceMode: 'source-unknown',
|
|
612
|
+
adoptionMode: 'adopted-observed',
|
|
613
|
+
repository: {
|
|
614
|
+
owner: registration.repoOwner,
|
|
615
|
+
name: registration.repoName,
|
|
616
|
+
url: registration.repoUrl,
|
|
617
|
+
},
|
|
618
|
+
observedDeployment: registration.observedDeployment,
|
|
619
|
+
request: registration,
|
|
620
|
+
response: payload,
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
spinner?.succeed(`Adopted observed app ${chalk.cyan(appKey)}`);
|
|
625
|
+
out.info(`Repository: ${chalk.cyan(`${registration.repoOwner}/${registration.repoName}`)}`);
|
|
626
|
+
out.info(`Observed URL: ${chalk.cyan(String(registration.observedDeployment?.activeUrl))}`);
|
|
627
|
+
out.info('Destructive operations remain blocked until a managed redeploy verifies control.');
|
|
628
|
+
});
|
|
629
|
+
verticalCommand
|
|
630
|
+
.command('workflow-setup <key>')
|
|
631
|
+
.description('Issue source-unknown workflow setup operation and nonce')
|
|
632
|
+
.option('--tenant-id <id>', 'Run against a specific company tenant')
|
|
633
|
+
.option('--environment <environment>', 'Deployment environment to bind', 'preview')
|
|
634
|
+
.option('--workflow <path>', 'GitHub Actions workflow path', '.github/workflows/eai-app.yml')
|
|
635
|
+
.option('--ref <ref>', 'Approved git ref')
|
|
636
|
+
.option('--commit <sha>', 'Current commit SHA to bind')
|
|
637
|
+
.option('--config-hash <hash>', 'Validated config hash to bind')
|
|
638
|
+
.option('--skip-validate', 'Skip app lookup', false)
|
|
639
|
+
.option('--format <format>', 'Output format (text|json)', 'text')
|
|
640
|
+
.option('--json', 'Output raw JSON (deprecated, use --format json)', false)
|
|
641
|
+
.action(async (key, options) => {
|
|
642
|
+
const ctx = await resolveAppManagementContext({ tenantId: options.tenantId, interactive: !options.tenantId });
|
|
643
|
+
const companyTenantId = options.tenantId
|
|
644
|
+
? ctx.tenantId
|
|
645
|
+
: await resolveMainCompanyTenantId(ctx.publicApiUrl, ctx.tenantId);
|
|
646
|
+
const client = new PlatformAPIClient(ctx.publicApiUrl, companyTenantId);
|
|
647
|
+
const format = normalizeFormat(options);
|
|
648
|
+
const appKey = key.trim();
|
|
649
|
+
if (!appKey) {
|
|
650
|
+
fail('App key is required.');
|
|
651
|
+
}
|
|
652
|
+
let setupRequest;
|
|
653
|
+
try {
|
|
654
|
+
setupRequest = buildSourceUnknownWorkflowSetupData(options);
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
fail(errMsg(err));
|
|
658
|
+
}
|
|
659
|
+
if (!options.skipValidate) {
|
|
660
|
+
await validateVerticalEnrollment(appKey, client);
|
|
661
|
+
}
|
|
662
|
+
const spinner = makeSpinner(format, `Issuing workflow setup for ${appKey}...`);
|
|
663
|
+
const res = await client.setupSourceUnknownWorkflow(companyTenantId, appKey, setupRequest);
|
|
664
|
+
const payload = await readResponsePayload(res);
|
|
665
|
+
if (!res.ok) {
|
|
666
|
+
spinner?.fail('Failed to issue source-unknown workflow setup');
|
|
667
|
+
fail(isRecord(payload) && typeof payload.message === 'string' ? payload.message : `${res.status} ${res.statusText}`);
|
|
668
|
+
}
|
|
669
|
+
if (format === 'json') {
|
|
670
|
+
out.json({
|
|
671
|
+
tenantId: companyTenantId,
|
|
672
|
+
appKey,
|
|
673
|
+
sourceMode: 'source-unknown',
|
|
674
|
+
request: setupRequest,
|
|
675
|
+
response: payload,
|
|
676
|
+
});
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
spinner?.succeed(`Issued workflow setup for ${chalk.cyan(appKey)}`);
|
|
680
|
+
if (isRecord(payload)) {
|
|
681
|
+
const operationId = typeof payload.operationId === 'string' ? payload.operationId : '<unknown>';
|
|
682
|
+
const nonce = typeof payload.nonce === 'string' ? payload.nonce : '<returned in JSON response>';
|
|
683
|
+
const expiresAt = typeof payload.expiresAt === 'string' ? payload.expiresAt : '<unknown>';
|
|
684
|
+
const setup = isRecord(payload.setup) ? payload.setup : {};
|
|
685
|
+
const workflowPath = typeof setup.workflowPath === 'string' ? setup.workflowPath : setupRequest.workflowPath;
|
|
686
|
+
const evidencePath = typeof setup.publicApiEvidencePath === 'string' ? setup.publicApiEvidencePath : undefined;
|
|
687
|
+
out.info(`Operation: ${chalk.cyan(operationId)}`);
|
|
688
|
+
out.info(`Nonce: ${chalk.cyan(nonce)} ${chalk.dim('(displayed once)')}`);
|
|
689
|
+
out.info(`Expires: ${chalk.cyan(expiresAt)}`);
|
|
690
|
+
out.info(`Workflow: ${chalk.cyan(String(workflowPath))}`);
|
|
691
|
+
if (evidencePath) {
|
|
692
|
+
out.info(`Evidence endpoint: ${chalk.cyan(evidencePath)}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
verticalCommand
|
|
697
|
+
.command('workflow-evidence <key>')
|
|
698
|
+
.description('Submit source-unknown workflow evidence for a setup operation')
|
|
699
|
+
.requiredOption('--repo <owner/repo>', 'GitHub repository submitting evidence')
|
|
700
|
+
.requiredOption('--operation-id <id>', 'Source-unknown workflow setup operation ID')
|
|
701
|
+
.requiredOption('--nonce <nonce>', 'One-time setup nonce')
|
|
702
|
+
.requiredOption('--commit <sha>', 'Workflow commit SHA')
|
|
703
|
+
.requiredOption('--config-hash <hash>', 'Validated config hash')
|
|
704
|
+
.requiredOption('--artifact-digest <digest>', 'Workflow artifact digest in sha256:<hex> form')
|
|
705
|
+
.requiredOption('--image-digest <digest>', 'Immutable image digest in sha256:<hex> form')
|
|
706
|
+
.option('--tenant-id <id>', 'Run against a specific company tenant')
|
|
707
|
+
.option('--environment <environment>', 'Deployment environment to bind', 'preview')
|
|
708
|
+
.option('--branch <branch>', 'Default branch', 'main')
|
|
709
|
+
.option('--workflow <path>', 'GitHub Actions workflow path', '.github/workflows/eai-app.yml')
|
|
710
|
+
.option('--ref <ref>', 'Approved git ref (defaults to refs/heads/<branch>)')
|
|
711
|
+
.option('--template-version <version>', 'Approved schema/template version')
|
|
712
|
+
.option('--base-template-sha <sha>', 'Base eai-app-template commit SHA when known')
|
|
713
|
+
.option('--approved-source-sha <sha>', 'Approved source commit SHA for non-template apps')
|
|
714
|
+
.option('--approved-release <id>', 'Approved schema/validator release identifier')
|
|
715
|
+
.option('--schema-digest <digest>', 'Approved schema digest in sha256:<hex> form')
|
|
716
|
+
.option('--validator-digest <digest>', 'Approved validator digest in sha256:<hex> form')
|
|
717
|
+
.option('--workflow-run-id <id>', 'GitHub Actions run ID')
|
|
718
|
+
.option('--workflow-run-attempt <n>', 'GitHub Actions run attempt')
|
|
719
|
+
.option('--github-oidc-token <token>', 'GitHub Actions OIDC token for workflow evidence submission')
|
|
720
|
+
.option('--github-oidc-audience <audience>', 'GitHub Actions OIDC audience', DEFAULT_SOURCE_UNKNOWN_GITHUB_OIDC_AUDIENCE)
|
|
721
|
+
.option('--skip-validate', 'Skip app lookup', false)
|
|
722
|
+
.option('--format <format>', 'Output format (text|json)', 'text')
|
|
723
|
+
.option('--json', 'Output raw JSON (deprecated, use --format json)', false)
|
|
724
|
+
.action(async (key, options) => {
|
|
725
|
+
const ctx = await resolveAppManagementContext({ tenantId: options.tenantId, interactive: !options.tenantId });
|
|
726
|
+
const companyTenantId = options.tenantId
|
|
727
|
+
? ctx.tenantId
|
|
728
|
+
: await resolveMainCompanyTenantId(ctx.publicApiUrl, ctx.tenantId);
|
|
729
|
+
const client = new PlatformAPIClient(ctx.publicApiUrl, companyTenantId);
|
|
730
|
+
const format = normalizeFormat(options);
|
|
731
|
+
const appKey = key.trim();
|
|
732
|
+
if (!appKey) {
|
|
733
|
+
fail('App key is required.');
|
|
734
|
+
}
|
|
735
|
+
let evidenceRequest;
|
|
736
|
+
try {
|
|
737
|
+
evidenceRequest = buildSourceUnknownWorkflowEvidenceData(options);
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
fail(errMsg(err));
|
|
741
|
+
}
|
|
742
|
+
if (!options.skipValidate) {
|
|
743
|
+
await validateVerticalEnrollment(appKey, client);
|
|
744
|
+
}
|
|
745
|
+
let githubOidcToken;
|
|
746
|
+
try {
|
|
747
|
+
githubOidcToken = await resolveGitHubOidcToken(options);
|
|
748
|
+
}
|
|
749
|
+
catch (err) {
|
|
750
|
+
fail(errMsg(err));
|
|
751
|
+
}
|
|
752
|
+
const spinner = makeSpinner(format, `Submitting workflow evidence for ${appKey}...`);
|
|
753
|
+
const res = await client.submitSourceUnknownWorkflowEvidence(companyTenantId, appKey, evidenceRequest, githubOidcToken);
|
|
754
|
+
const payload = await readResponsePayload(res);
|
|
755
|
+
if (!res.ok) {
|
|
756
|
+
spinner?.fail('Failed to submit source-unknown workflow evidence');
|
|
757
|
+
fail(isRecord(payload) && typeof payload.message === 'string' ? payload.message : `${res.status} ${res.statusText}`);
|
|
758
|
+
}
|
|
759
|
+
if (format === 'json') {
|
|
760
|
+
out.json({
|
|
761
|
+
tenantId: companyTenantId,
|
|
762
|
+
appKey,
|
|
763
|
+
sourceMode: 'source-unknown',
|
|
764
|
+
request: evidenceRequest,
|
|
765
|
+
response: payload,
|
|
766
|
+
});
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
spinner?.succeed(`Submitted workflow evidence for ${chalk.cyan(appKey)}`);
|
|
770
|
+
out.info(`Operation: ${chalk.cyan(evidenceRequest.operationId)}`);
|
|
771
|
+
out.info(`Artifact: ${chalk.cyan(evidenceRequest.artifactDigest)}`);
|
|
772
|
+
out.info(`Image: ${chalk.cyan(evidenceRequest.imageDigest)}`);
|
|
773
|
+
});
|
|
774
|
+
verticalCommand
|
|
775
|
+
.command('deploy-source-unknown <key>')
|
|
776
|
+
.description('Request a TenantInfra deployment handoff for a source-unknown app')
|
|
777
|
+
.requiredOption('--operation-id <id>', 'Accepted source-unknown workflow evidence operation ID')
|
|
778
|
+
.option('--tenant-id <id>', 'Run against a specific company tenant')
|
|
779
|
+
.option('--environment <environment>', 'Deployment environment to bind', 'preview')
|
|
780
|
+
.option('--repo <owner/name>', 'GitHub repository that produced the deployment evidence')
|
|
781
|
+
.option('--workflow <path>', 'GitHub Actions workflow path')
|
|
782
|
+
.option('--ref <ref>', 'Approved git ref')
|
|
783
|
+
.option('--commit <sha>', 'Workflow commit SHA')
|
|
784
|
+
.option('--workflow-run-id <id>', 'GitHub Actions workflow run ID')
|
|
785
|
+
.option('--config-hash <hash>', 'Validated config hash')
|
|
786
|
+
.option('--artifact-digest <digest>', 'Workflow artifact digest in sha256:<hex> form')
|
|
787
|
+
.option('--image-digest <digest>', 'Immutable image digest in sha256:<hex> form')
|
|
788
|
+
.option('--target-kind <kind>', 'Deployment backend target kind', 'tenantinfra')
|
|
789
|
+
.option('--release-channel <channel>', 'Release channel to request', 'preview')
|
|
790
|
+
.option('--skip-validate', 'Skip app lookup', false)
|
|
791
|
+
.option('--format <format>', 'Output format (text|json)', 'text')
|
|
792
|
+
.option('--json', 'Output raw JSON (deprecated, use --format json)', false)
|
|
793
|
+
.action(async (key, options) => {
|
|
794
|
+
const ctx = await resolveAppManagementContext({ tenantId: options.tenantId, interactive: !options.tenantId });
|
|
795
|
+
const companyTenantId = options.tenantId
|
|
796
|
+
? ctx.tenantId
|
|
797
|
+
: await resolveMainCompanyTenantId(ctx.publicApiUrl, ctx.tenantId);
|
|
798
|
+
const client = new PlatformAPIClient(ctx.publicApiUrl, companyTenantId);
|
|
799
|
+
const format = normalizeFormat(options);
|
|
800
|
+
const appKey = key.trim();
|
|
801
|
+
if (!appKey) {
|
|
802
|
+
fail('App key is required.');
|
|
803
|
+
}
|
|
804
|
+
let deploymentRequest;
|
|
805
|
+
try {
|
|
806
|
+
deploymentRequest = buildSourceUnknownDeploymentData(options);
|
|
807
|
+
}
|
|
808
|
+
catch (err) {
|
|
809
|
+
fail(errMsg(err));
|
|
810
|
+
}
|
|
811
|
+
if (!options.skipValidate) {
|
|
812
|
+
await validateVerticalEnrollment(appKey, client);
|
|
813
|
+
}
|
|
814
|
+
const spinner = makeSpinner(format, `Requesting source-unknown deployment handoff for ${appKey}...`);
|
|
815
|
+
const res = await client.requestSourceUnknownDeployment(companyTenantId, appKey, deploymentRequest);
|
|
816
|
+
const payload = await readResponsePayload(res);
|
|
817
|
+
if (!res.ok) {
|
|
818
|
+
spinner?.fail('Failed to request source-unknown deployment handoff');
|
|
819
|
+
fail(isRecord(payload) && typeof payload.message === 'string' ? payload.message : `${res.status} ${res.statusText}`);
|
|
820
|
+
}
|
|
821
|
+
if (format === 'json') {
|
|
822
|
+
out.json({
|
|
823
|
+
tenantId: companyTenantId,
|
|
824
|
+
appKey,
|
|
825
|
+
sourceMode: 'source-unknown',
|
|
826
|
+
request: deploymentRequest,
|
|
827
|
+
response: payload,
|
|
828
|
+
});
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
spinner?.succeed(`Recorded deployment handoff for ${chalk.cyan(appKey)}`);
|
|
832
|
+
if (isRecord(payload)) {
|
|
833
|
+
const status = typeof payload.status === 'string' ? payload.status : 'unknown';
|
|
834
|
+
const requestId = typeof payload.deploymentRequestId === 'string' ? payload.deploymentRequestId : '<unknown>';
|
|
835
|
+
const requiresTenantInfra = payload.requiresTenantInfra === true ? 'required' : 'not required';
|
|
836
|
+
out.info(`Status: ${chalk.cyan(status)}`);
|
|
837
|
+
out.info(`Request: ${chalk.cyan(requestId)}`);
|
|
838
|
+
out.info(`TenantInfra: ${chalk.cyan(requiresTenantInfra)}`);
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
verticalCommand
|
|
842
|
+
.command('deploy-source-unknown-status <key>')
|
|
843
|
+
.description('Read the latest source-unknown deployment handoff status')
|
|
844
|
+
.option('--tenant-id <id>', 'Run against a specific company tenant')
|
|
845
|
+
.option('--skip-validate', 'Skip app lookup', false)
|
|
846
|
+
.option('--format <format>', 'Output format (text|json)', 'text')
|
|
847
|
+
.option('--json', 'Output raw JSON (deprecated, use --format json)', false)
|
|
848
|
+
.action(async (key, options) => {
|
|
849
|
+
const ctx = await resolveAppManagementContext({ tenantId: options.tenantId, interactive: !options.tenantId });
|
|
850
|
+
const companyTenantId = options.tenantId
|
|
851
|
+
? ctx.tenantId
|
|
852
|
+
: await resolveMainCompanyTenantId(ctx.publicApiUrl, ctx.tenantId);
|
|
853
|
+
const client = new PlatformAPIClient(ctx.publicApiUrl, companyTenantId);
|
|
854
|
+
const format = normalizeFormat(options);
|
|
855
|
+
const appKey = key.trim();
|
|
856
|
+
if (!appKey) {
|
|
857
|
+
fail('App key is required.');
|
|
858
|
+
}
|
|
859
|
+
if (!options.skipValidate) {
|
|
860
|
+
await validateVerticalEnrollment(appKey, client);
|
|
861
|
+
}
|
|
862
|
+
const spinner = makeSpinner(format, `Reading source-unknown deployment handoff status for ${appKey}...`);
|
|
863
|
+
const res = await client.getLatestSourceUnknownDeployment(companyTenantId, appKey);
|
|
864
|
+
const payload = await readResponsePayload(res);
|
|
865
|
+
if (!res.ok) {
|
|
866
|
+
spinner?.fail('Failed to read source-unknown deployment handoff status');
|
|
867
|
+
fail(isRecord(payload) && typeof payload.message === 'string' ? payload.message : `${res.status} ${res.statusText}`);
|
|
868
|
+
}
|
|
869
|
+
if (format === 'json') {
|
|
870
|
+
out.json({
|
|
871
|
+
tenantId: companyTenantId,
|
|
872
|
+
appKey,
|
|
873
|
+
sourceMode: 'source-unknown',
|
|
874
|
+
response: payload,
|
|
875
|
+
});
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
spinner?.succeed(`Read deployment handoff status for ${chalk.cyan(appKey)}`);
|
|
879
|
+
if (isRecord(payload)) {
|
|
880
|
+
const status = typeof payload.status === 'string' ? payload.status : 'unknown';
|
|
881
|
+
const requestId = typeof payload.deploymentRequestId === 'string' ? payload.deploymentRequestId : '<none>';
|
|
882
|
+
const requiresTenantInfra = payload.requiresTenantInfra === true ? 'required' : 'not required';
|
|
883
|
+
out.info(`Status: ${chalk.cyan(status)}`);
|
|
884
|
+
out.info(`Request: ${chalk.cyan(requestId)}`);
|
|
885
|
+
out.info(`TenantInfra: ${chalk.cyan(requiresTenantInfra)}`);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
198
888
|
verticalCommand
|
|
199
889
|
.command('select <key>')
|
|
200
890
|
.description('Set EAI_APP_KEY in the current project .env.local')
|
|
@@ -210,7 +900,7 @@ verticalCommand
|
|
|
210
900
|
fail('App key is required.');
|
|
211
901
|
}
|
|
212
902
|
if (!options.skipValidate) {
|
|
213
|
-
await validateVerticalEnrollment(verticalKey, ctx);
|
|
903
|
+
await validateVerticalEnrollment(verticalKey, ctx.client);
|
|
214
904
|
}
|
|
215
905
|
await patchEnvFile(ctx.root, {
|
|
216
906
|
[APP_KEY_ENV]: verticalKey,
|
|
@@ -247,7 +937,7 @@ verticalCommand
|
|
|
247
937
|
fail('App key is required.');
|
|
248
938
|
}
|
|
249
939
|
if (!options.skipValidate) {
|
|
250
|
-
await validateVerticalEnrollment(verticalKey, ctx);
|
|
940
|
+
await validateVerticalEnrollment(verticalKey, ctx.client);
|
|
251
941
|
}
|
|
252
942
|
const spinner = makeSpinner(format, options.dryRun
|
|
253
943
|
? `Planning app storage readiness for ${verticalKey}...`
|