@numa-tech/numa 1.13.1 → 1.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -6
- package/dist/application-onboarding/client.d.ts +42 -3
- package/dist/application-onboarding/client.js +41 -2
- package/dist/application-onboarding/client.js.map +1 -1
- package/dist/application-onboarding/commands.d.ts +25 -1
- package/dist/application-onboarding/commands.js +461 -17
- package/dist/application-onboarding/commands.js.map +1 -1
- package/dist/application-onboarding/schemas.d.ts +881 -0
- package/dist/application-onboarding/schemas.js +329 -4
- package/dist/application-onboarding/schemas.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.js +201 -6
- package/dist/command-catalog.js.map +1 -1
- package/dist/gitops/schemas.js +2 -1
- package/dist/gitops/schemas.js.map +1 -1
- package/dist/jenkins-jobs/client.d.ts +319 -0
- package/dist/jenkins-jobs/client.js +136 -0
- package/dist/jenkins-jobs/client.js.map +1 -0
- package/dist/jenkins-jobs/commands.d.ts +6 -0
- package/dist/jenkins-jobs/commands.js +269 -0
- package/dist/jenkins-jobs/commands.js.map +1 -0
- package/dist/jenkins-jobs/schemas.d.ts +359 -0
- package/dist/jenkins-jobs/schemas.js +119 -0
- package/dist/jenkins-jobs/schemas.js.map +1 -0
- package/dist/publications/client.d.ts +181 -0
- package/dist/publications/client.js +147 -0
- package/dist/publications/client.js.map +1 -0
- package/dist/publications/commands.d.ts +8 -0
- package/dist/publications/commands.js +225 -0
- package/dist/publications/commands.js.map +1 -0
- package/dist/publications/scanner.d.ts +23 -0
- package/dist/publications/scanner.js +247 -0
- package/dist/publications/scanner.js.map +1 -0
- package/dist/publications/schemas.d.ts +164 -0
- package/dist/publications/schemas.js +90 -0
- package/dist/publications/schemas.js.map +1 -0
- package/dist/repositories/commands.js +23 -1
- package/dist/repositories/commands.js.map +1 -1
- package/package.json +4 -4
- package/skills/numa-create-application/SKILL.md +158 -0
- package/skills/numa-create-application/agents/openai.yaml +4 -0
- package/skills/numa-create-application/evals/evals.json +61 -0
- package/skills/numa-create-application/references/checklist.md +126 -0
- package/skills/numa-create-application/references/inference-rules.md +69 -0
- package/skills/numa-create-application/references/numa-cli.md +216 -0
- package/skills/numa-create-application/references/promote-workflows.md +105 -0
- package/skills/numa-create-application/scripts/inspect-project.mjs +243 -0
|
@@ -4,9 +4,10 @@ import { loadAppConfig } from "../app-config.js";
|
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
5
|
import { registerApplicationCandidateCommands } from "../application-candidates/commands.js";
|
|
6
6
|
import { SourceRepositoryClient } from "../repositories/client.js";
|
|
7
|
+
import { scanProjectPublication } from "../publications/scanner.js";
|
|
7
8
|
import { OnboardingClient } from "./client.js";
|
|
8
9
|
import { OnboardingError, sanitizeOnboardingOutput } from "./errors.js";
|
|
9
|
-
import { OnboardingModeSchema, OnboardingEventOutputSchema, OnboardingSessionStateSchema, OnboardingSpecSchema, ScmProviderSchema } from "./schemas.js";
|
|
10
|
+
import { OnboardingModeSchema, OnboardingEventOutputSchema, OnboardingFirstBuildActionRequestSchema, OnboardingJenkinsBindingActionRequestSchema, OnboardingObserverScopeActionRequestSchema, OnboardingPublicationPlanActionRequestSchema, OnboardingSessionStateSchema, OnboardingSpecSchema, ScmProviderSchema } from "./schemas.js";
|
|
10
11
|
import { readScmAdminConnectionInput, readScmCredentialBundle } from "./scm-credentials.js";
|
|
11
12
|
import { runOnboardingTui } from "./tui.js";
|
|
12
13
|
const TERMINAL_STATES = new Set(["COMPLETED", "FAILED", "CANCELLED"]);
|
|
@@ -188,7 +189,12 @@ export function scmCredentialUpdate(version, authType, credential) {
|
|
|
188
189
|
}
|
|
189
190
|
function scmConnectionLine(connection) {
|
|
190
191
|
const enabled = Object.entries(connection.capabilities).filter(([, value]) => value).map(([key]) => key);
|
|
191
|
-
|
|
192
|
+
const codeupCloneReadiness = connection.provider === "CODEUP"
|
|
193
|
+
? `\tcodeup-clone:${connection.codeupCloneCredentialConfigured == null
|
|
194
|
+
? "unknown"
|
|
195
|
+
: connection.codeupCloneCredentialConfigured ? "ready" : "missing"}`
|
|
196
|
+
: "";
|
|
197
|
+
return `${connection.code}\t${connection.provider}\t${connection.health}\t${connection.organizationKey}\t${enabled.join(",") || "no-capabilities"}${codeupCloneReadiness}`;
|
|
192
198
|
}
|
|
193
199
|
function repositoryLine(repository) {
|
|
194
200
|
return `${repository.externalId}\t${repository.fullName ?? repository.name}\t${repository.defaultBranch ?? "-"}\t${repository.headRevision ?? "-"}${repository.archived ? "\tARCHIVED" : ""}`;
|
|
@@ -211,6 +217,56 @@ function idempotencyKey(value, operation = "--apply") {
|
|
|
211
217
|
}
|
|
212
218
|
return candidate;
|
|
213
219
|
}
|
|
220
|
+
function stableActionText(value, label, maximum = 253) {
|
|
221
|
+
const candidate = value?.trim() ?? "";
|
|
222
|
+
if (!candidate || candidate.length > maximum || /[\u0000-\u001f\u007f]/u.test(candidate)) {
|
|
223
|
+
throw new OnboardingError(`${label} must be 1-${maximum} printable characters.`, "onboarding_input_invalid");
|
|
224
|
+
}
|
|
225
|
+
return candidate;
|
|
226
|
+
}
|
|
227
|
+
function sha256(value, label) {
|
|
228
|
+
const candidate = value?.trim() ?? "";
|
|
229
|
+
if (!/^[0-9a-f]{64}$/u.test(candidate)) {
|
|
230
|
+
throw new OnboardingError(`${label} must be the exact lower-case SHA-256 locked by the server.`, "onboarding_input_invalid");
|
|
231
|
+
}
|
|
232
|
+
return candidate;
|
|
233
|
+
}
|
|
234
|
+
function collectParameter(value, previous) {
|
|
235
|
+
return [...previous, value];
|
|
236
|
+
}
|
|
237
|
+
function buildParameters(values) {
|
|
238
|
+
if (!values?.length)
|
|
239
|
+
return undefined;
|
|
240
|
+
if (values.length > 100)
|
|
241
|
+
throw new OnboardingError("At most 100 first-build parameters are allowed.", "onboarding_input_invalid");
|
|
242
|
+
const result = {};
|
|
243
|
+
for (const value of values) {
|
|
244
|
+
const separator = value.indexOf("=");
|
|
245
|
+
const key = separator > 0 ? value.slice(0, separator).trim() : "";
|
|
246
|
+
const parameterValue = separator > 0 ? value.slice(separator + 1) : "";
|
|
247
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]{0,127}$/u.test(key) || parameterValue.length > 2_000
|
|
248
|
+
|| /[\u0000-\u001f\u007f]/u.test(parameterValue)) {
|
|
249
|
+
throw new OnboardingError("Each --parameter must be printable KEY=VALUE with a stable key.", "onboarding_input_invalid");
|
|
250
|
+
}
|
|
251
|
+
const normalizedKey = key.replace(/[^A-Za-z0-9]/gu, "").toLowerCase();
|
|
252
|
+
if (["secret", "password", "passwd", "credential", "token", "privatekey", "apikey", "accesskey"].some((part) => normalizedKey.includes(part))
|
|
253
|
+
|| /\b(?:Bearer|Basic)\s+\S+/iu.test(parameterValue)
|
|
254
|
+
|| /\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|LTAI[A-Za-z0-9]{12,})\b/u.test(parameterValue)
|
|
255
|
+
|| /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/u.test(parameterValue)) {
|
|
256
|
+
throw new OnboardingError("First-build parameters cannot contain credential-like keys or values.", "onboarding_secret_not_allowed");
|
|
257
|
+
}
|
|
258
|
+
if (Object.hasOwn(result, key))
|
|
259
|
+
throw new OnboardingError(`Duplicate first-build parameter: ${key}`, "onboarding_input_invalid");
|
|
260
|
+
result[key] = parameterValue;
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
function parsedActionInput(schema, value) {
|
|
265
|
+
const parsed = schema.safeParse(value);
|
|
266
|
+
if (!parsed.success)
|
|
267
|
+
throw new OnboardingError(`Invalid onboarding action input: ${parsed.error.message}`, "onboarding_input_invalid");
|
|
268
|
+
return parsed.data;
|
|
269
|
+
}
|
|
214
270
|
export async function readOnboardingSpec(path) {
|
|
215
271
|
try {
|
|
216
272
|
const info = await stat(path);
|
|
@@ -239,6 +295,9 @@ export async function executeOnboardingSpec(client, spec, options = {}) {
|
|
|
239
295
|
if (!options.apply && options.idempotencyKey) {
|
|
240
296
|
throw new OnboardingError("--idempotency-key is only valid with --apply.", "onboarding_input_invalid");
|
|
241
297
|
}
|
|
298
|
+
if (spec.schemaVersion === 3 && spec.projectStrategy === "PUBLISH_PROJECT" && !options.sessionNo) {
|
|
299
|
+
throw new OnboardingError("schemaVersion=3 PUBLISH_PROJECT must reuse the session created by `numa app begin`; provide --session <session-no>.", "onboarding_session_required");
|
|
300
|
+
}
|
|
242
301
|
const key = options.apply ? idempotencyKey(options.idempotencyKey) : undefined;
|
|
243
302
|
const draftSession = options.sessionNo
|
|
244
303
|
? await client.getSession(options.sessionNo)
|
|
@@ -264,9 +323,17 @@ export async function executeOnboardingSpec(client, spec, options = {}) {
|
|
|
264
323
|
if (!planHash) {
|
|
265
324
|
throw new OnboardingError("The server did not return a plan hash; apply was not submitted.", "onboarding_plan_missing");
|
|
266
325
|
}
|
|
267
|
-
const
|
|
326
|
+
const attempt = await executeOnboardingSessionMutation(client, session.sessionNo, () => client.apply(session.sessionNo, { planHash, idempotencyKey: key }), (event) => hasOnboardingApplyRecoveryEvidence(event, planHash));
|
|
327
|
+
if (!attempt.result) {
|
|
328
|
+
return {
|
|
329
|
+
session: attempt.inspection.session,
|
|
330
|
+
preflight,
|
|
331
|
+
idempotencyKey: key,
|
|
332
|
+
recoveredFromStatus: true
|
|
333
|
+
};
|
|
334
|
+
}
|
|
268
335
|
session = await client.getSession(session.sessionNo);
|
|
269
|
-
return { session, preflight, task, idempotencyKey: key };
|
|
336
|
+
return { session, preflight, task: attempt.result, idempotencyKey: key, recoveredFromStatus: false };
|
|
270
337
|
}
|
|
271
338
|
function defaultSleep(milliseconds, signal) {
|
|
272
339
|
return new Promise((resolve, reject) => {
|
|
@@ -316,7 +383,7 @@ export async function inspectOnboardingSession(client, sessionNo, afterSequence
|
|
|
316
383
|
.filter((event) => event.sequenceNo > afterSequence)
|
|
317
384
|
.map(publicOnboardingEvent)
|
|
318
385
|
.sort((left, right) => left.sequenceNo - right.sequenceNo);
|
|
319
|
-
const lastSequence = events.reduce((maximum, event) => Math.max(maximum, event.sequenceNo), afterSequence);
|
|
386
|
+
const lastSequence = events.reduce((maximum, event) => Math.max(maximum, event.sequenceNo), Math.max(afterSequence, eventPage.nextSequence ?? afterSequence));
|
|
320
387
|
return {
|
|
321
388
|
session,
|
|
322
389
|
events,
|
|
@@ -324,6 +391,192 @@ export async function inspectOnboardingSession(client, sessionNo, afterSequence
|
|
|
324
391
|
execution: { ...onboardingExecutionView(session, events), lastSequence }
|
|
325
392
|
};
|
|
326
393
|
}
|
|
394
|
+
function mutationResultMayBeUnknown(error) {
|
|
395
|
+
if (error instanceof OnboardingError) {
|
|
396
|
+
return error.retryable
|
|
397
|
+
|| (error.status != null && error.status >= 500)
|
|
398
|
+
|| (error.code === "onboarding_invalid_response" && error.status != null
|
|
399
|
+
&& error.status >= 200 && error.status < 300);
|
|
400
|
+
}
|
|
401
|
+
return error instanceof TypeError;
|
|
402
|
+
}
|
|
403
|
+
export function hasOnboardingApplyRecoveryEvidence(event, planHash) {
|
|
404
|
+
return event.output?.taskNo != null && event.output.planHash === planHash;
|
|
405
|
+
}
|
|
406
|
+
export function hasJenkinsBindingRecoveryEvidence(_event, _bindingId) {
|
|
407
|
+
// jenkinsBindingId alone does not prove CONFIRMED status or the requested
|
|
408
|
+
// optimistic-lock version; unknown confirmations therefore fail closed.
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
export function hasObserverScopeRecoveryEvidence(_event) {
|
|
412
|
+
// The reviewed event projection currently lacks serviceBindingId and exact
|
|
413
|
+
// observer coordinates, so no event can prove this request's full intent.
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
export function hasFirstBuildRecoveryEvidence(_event, _input) {
|
|
417
|
+
// Aggregated task output can repeat an older decision on an unrelated new
|
|
418
|
+
// event and does not carry the action idempotency key or full build intent.
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
export async function executeOnboardingSessionMutation(client, sessionNo, mutate, hasNewEvidence) {
|
|
422
|
+
// A pre-write cursor is required: without it, a matching field or broad
|
|
423
|
+
// session state that predates this request could be misreported as recovery.
|
|
424
|
+
const baseline = await inspectOnboardingSession(client, sessionNo);
|
|
425
|
+
try {
|
|
426
|
+
return { result: await mutate(), recoveredFromStatus: false };
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
if (!mutationResultMayBeUnknown(error))
|
|
430
|
+
throw error;
|
|
431
|
+
let inspection;
|
|
432
|
+
try {
|
|
433
|
+
inspection = await inspectOnboardingSession(client, sessionNo, baseline.lastSequence);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// Preserve the mutation error below; a failed read must never trigger a write replay.
|
|
437
|
+
}
|
|
438
|
+
if (inspection && inspection.events.some((event) => event.sequenceNo > baseline.lastSequence && hasNewEvidence(event, baseline, inspection))) {
|
|
439
|
+
return { inspection, recoveredFromStatus: true };
|
|
440
|
+
}
|
|
441
|
+
const original = error instanceof OnboardingError ? error : undefined;
|
|
442
|
+
throw new OnboardingError("The mutation result is unknown. Status recovery was attempted; inspect the same session before reusing the original idempotency key.", "onboarding_result_unknown", original?.status, original?.requestId, true, {
|
|
443
|
+
sessionNo,
|
|
444
|
+
recoveryCommand: `numa app inspect ${sessionNo} --json`,
|
|
445
|
+
originalCode: original?.code ?? "network_error"
|
|
446
|
+
}, original?.retryAfterSeconds);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function assertPublicationSessionBinding(result, sessionNo, resolved) {
|
|
450
|
+
if (result.session.sessionNo !== sessionNo
|
|
451
|
+
|| result.publicationPlan.consumerType !== "APPLICATION_ONBOARDING"
|
|
452
|
+
|| result.publicationPlan.consumerRef !== sessionNo
|
|
453
|
+
|| (resolved && !result.replayed)) {
|
|
454
|
+
throw new OnboardingError("The publication response is not a replay bound to the same APPLICATION_ONBOARDING session.", "onboarding_invalid_response", 200);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function publicationResolutionPending(error) {
|
|
458
|
+
return error instanceof OnboardingError
|
|
459
|
+
&& error.status === 404
|
|
460
|
+
&& error.code === "ONBOARDING_PUBLICATION_PLAN_NOT_RESOLVED"
|
|
461
|
+
&& error.retryable;
|
|
462
|
+
}
|
|
463
|
+
export async function executePublicationAttachmentWithRecovery(client, sessionNo, clientRequestId, mutate, options = {}) {
|
|
464
|
+
try {
|
|
465
|
+
const result = await mutate();
|
|
466
|
+
assertPublicationSessionBinding(result, sessionNo, false);
|
|
467
|
+
return { result, recoveredFromStatus: false };
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
if (!mutationResultMayBeUnknown(error))
|
|
471
|
+
throw error;
|
|
472
|
+
const delaysMs = options.delaysMs ?? [0, 200, 500, 1_000];
|
|
473
|
+
const sleep = options.sleep ?? ((milliseconds) => defaultSleep(milliseconds));
|
|
474
|
+
let resolutionCode;
|
|
475
|
+
for (const delayMs of delaysMs) {
|
|
476
|
+
if (delayMs > 0)
|
|
477
|
+
await sleep(delayMs);
|
|
478
|
+
try {
|
|
479
|
+
const result = await client.resolvePublicationPlan(sessionNo, clientRequestId);
|
|
480
|
+
assertPublicationSessionBinding(result, sessionNo, true);
|
|
481
|
+
return { result, recoveredFromStatus: true };
|
|
482
|
+
}
|
|
483
|
+
catch (resolutionError) {
|
|
484
|
+
resolutionCode = resolutionError instanceof OnboardingError ? resolutionError.code : "network_error";
|
|
485
|
+
if (publicationResolutionPending(resolutionError) || mutationResultMayBeUnknown(resolutionError))
|
|
486
|
+
continue;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const original = error instanceof OnboardingError ? error : undefined;
|
|
491
|
+
throw new OnboardingError("The publication attachment result is unknown. Continue read-only resolution with the original session and client-request-id; do not replay the write based on this error.", "onboarding_publication_result_unknown", original?.status, original?.requestId, true, {
|
|
492
|
+
sessionNo,
|
|
493
|
+
clientRequestId,
|
|
494
|
+
recoveryCommand: `numa app publication resolve ${sessionNo} --client-request-id ${clientRequestId} --json`,
|
|
495
|
+
originalCode: original?.code ?? "network_error",
|
|
496
|
+
resolutionCode
|
|
497
|
+
}, original?.retryAfterSeconds);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function sessionSummary(session) {
|
|
501
|
+
return {
|
|
502
|
+
sessionNo: session.sessionNo,
|
|
503
|
+
mode: session.mode,
|
|
504
|
+
state: session.state,
|
|
505
|
+
currentStep: session.currentStep,
|
|
506
|
+
revision: session.revision,
|
|
507
|
+
planHash: session.planHash
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function recoveredMutationPayload(inspection, idempotencyKeyValue) {
|
|
511
|
+
return {
|
|
512
|
+
ok: true,
|
|
513
|
+
session: sessionSummary(inspection.session),
|
|
514
|
+
execution: inspection.execution,
|
|
515
|
+
recoveredFromStatus: true,
|
|
516
|
+
mutationResponseReceived: false,
|
|
517
|
+
idempotencyKey: idempotencyKeyValue,
|
|
518
|
+
recovery: { inspectCommand: `numa app inspect ${inspection.session.sessionNo} --json` }
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
export function onboardingPublicationAttachmentView(result, source, idempotencyKeyValue, recoveredFromStatus = false) {
|
|
522
|
+
const plan = result.publicationPlan;
|
|
523
|
+
return {
|
|
524
|
+
ok: true,
|
|
525
|
+
session: sessionSummary(result.session),
|
|
526
|
+
publicationPlan: {
|
|
527
|
+
planNo: plan.planNo,
|
|
528
|
+
state: plan.state,
|
|
529
|
+
planHash: plan.planHash,
|
|
530
|
+
repositoryKey: plan.repositoryKey,
|
|
531
|
+
consumerType: plan.consumerType,
|
|
532
|
+
consumerRef: plan.consumerRef,
|
|
533
|
+
publicationMode: plan.publicationMode,
|
|
534
|
+
branch: plan.branch,
|
|
535
|
+
expectedHead: plan.expectedHead,
|
|
536
|
+
fileCount: plan.manifest.length,
|
|
537
|
+
warningCount: plan.warnings.length,
|
|
538
|
+
blockerCount: plan.blockers.length,
|
|
539
|
+
expiresAt: plan.expiresAt
|
|
540
|
+
},
|
|
541
|
+
scan: {
|
|
542
|
+
schemaVersion: source.schemaVersion,
|
|
543
|
+
sourceRevision: source.sourceRevision,
|
|
544
|
+
sourceTreeDigest: source.sourceTreeDigest,
|
|
545
|
+
clean: source.clean,
|
|
546
|
+
fileCount: source.fileCount,
|
|
547
|
+
totalBytes: source.totalBytes,
|
|
548
|
+
executableFileCount: source.executableFileCount
|
|
549
|
+
},
|
|
550
|
+
replayed: result.replayed,
|
|
551
|
+
recoveredFromStatus,
|
|
552
|
+
idempotencyKey: idempotencyKeyValue
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
export function onboardingPublicationResolutionView(result, clientRequestId) {
|
|
556
|
+
const plan = result.publicationPlan;
|
|
557
|
+
return {
|
|
558
|
+
ok: true,
|
|
559
|
+
session: sessionSummary(result.session),
|
|
560
|
+
publicationPlan: {
|
|
561
|
+
planNo: plan.planNo,
|
|
562
|
+
state: plan.state,
|
|
563
|
+
planHash: plan.planHash,
|
|
564
|
+
repositoryKey: plan.repositoryKey,
|
|
565
|
+
consumerType: plan.consumerType,
|
|
566
|
+
consumerRef: plan.consumerRef,
|
|
567
|
+
publicationMode: plan.publicationMode,
|
|
568
|
+
branch: plan.branch,
|
|
569
|
+
expectedHead: plan.expectedHead,
|
|
570
|
+
fileCount: plan.manifest.length,
|
|
571
|
+
warningCount: plan.warnings.length,
|
|
572
|
+
blockerCount: plan.blockers.length,
|
|
573
|
+
expiresAt: plan.expiresAt
|
|
574
|
+
},
|
|
575
|
+
replayed: result.replayed,
|
|
576
|
+
recoveredFromStatus: true,
|
|
577
|
+
clientRequestId
|
|
578
|
+
};
|
|
579
|
+
}
|
|
327
580
|
export async function executeOnboardingAction(client, sessionNo, action) {
|
|
328
581
|
await client[action](sessionNo);
|
|
329
582
|
return inspectOnboardingSession(client, sessionNo);
|
|
@@ -353,6 +606,7 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
353
606
|
const client = () => dependencies.clientFactory?.() ?? new OnboardingClient(loadConfig("platform"));
|
|
354
607
|
const isInteractive = dependencies.interactive ?? (() => Boolean(process.stdin.isTTY && process.stdout.isTTY));
|
|
355
608
|
const tui = dependencies.runTui ?? runOnboardingTui;
|
|
609
|
+
const scanPublication = dependencies.scanPublication ?? scanProjectPublication;
|
|
356
610
|
const sleep = dependencies.sleep ?? defaultSleep;
|
|
357
611
|
const app = program.command("app")
|
|
358
612
|
.description("创建新应用或把存量代码库接入 DevOps 平台")
|
|
@@ -363,6 +617,15 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
363
617
|
结果不明时用相同 idempotency-key 重试或 inspect/resume,禁止新建重复会话。
|
|
364
618
|
`);
|
|
365
619
|
registerApplicationCandidateCommands(app);
|
|
620
|
+
addExamples(app.command("capabilities")
|
|
621
|
+
.description("读取服务端 Application Onboarding 能力自述;命令存在不代表端到端能力可用")
|
|
622
|
+
.action(async (_options, command) => {
|
|
623
|
+
const capabilities = await client().getCapabilities();
|
|
624
|
+
await output(command, { ok: true, capabilities }, JSON.stringify(stableOutput(capabilities), null, 2));
|
|
625
|
+
}), ["numa app capabilities --json"], [
|
|
626
|
+
"与 `numa commands --json`、Repository capability 及各 child control plane 的只读能力共同形成运行时能力矩阵。",
|
|
627
|
+
"HTTP 501 表示服务端能力未发布;不得因为本地命令存在就声称可执行。"
|
|
628
|
+
]);
|
|
366
629
|
addExamples(app.command("options <type>")
|
|
367
630
|
.description("按名称查询应用所需的团队、系统、业务域或环境 ID")
|
|
368
631
|
.option("--search <text>", "按 code 或名称搜索")
|
|
@@ -388,7 +651,8 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
388
651
|
live probe 与连接变更由服务端校验平台管理员权限和乐观锁 version。
|
|
389
652
|
provider credential 只能来自权限受限文件或重定向 stdin;禁止通过 argv、环境变量或普通 JSON 输出传递。
|
|
390
653
|
Codeup 云 AK/Role 只能引用已有 aliyunAccountId/aliyunAssumeRoleId,Numa 不读取或保存云 AK。
|
|
391
|
-
|
|
654
|
+
Codeup accessToken 只用于 OpenAPI;含 100755 文件的 Publication 另需 HTTPS cloneUsername/clonePassword。
|
|
655
|
+
三者只从 chmod 600 文件或重定向 stdin 写入;未提供时使用服务端 Aliyun identity,不提交空 credential bundle。
|
|
392
656
|
`);
|
|
393
657
|
addExamples(scm.command("list")
|
|
394
658
|
.description("列出当前用户可见的安全 SCM 连接摘要")
|
|
@@ -541,7 +805,7 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
541
805
|
if (credential) {
|
|
542
806
|
connection = await onboarding.rotateAdminScmCredential(connection.code, scmCredentialUpdate(connection.version, connection.authType, credential));
|
|
543
807
|
}
|
|
544
|
-
await output(command, { ok: true, connection }, `${connection.code}\t${connection.status}\tversion:${connection.version}\tcredential:${connection.credentialConfigured ? "configured" : "missing"}`);
|
|
808
|
+
await output(command, { ok: true, connection }, `${connection.code}\t${connection.status}\tversion:${connection.version}\tcredential:${connection.credentialConfigured ? "configured" : "missing"}\tcodeup-at:${connection.codeupAccessTokenConfigured ? "ready" : "missing"}\tcodeup-clone:${connection.codeupCloneCredentialConfigured ? "ready" : "missing"}`);
|
|
545
809
|
}), [
|
|
546
810
|
"numa app scm create --input github.json --credential-file github-app.private.json --json",
|
|
547
811
|
"numa app scm create --input codeup.json --json"
|
|
@@ -565,7 +829,7 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
565
829
|
await output(command, { ok: true, connection }, `${connection.code}\t${connection.status}\tversion:${connection.version}`);
|
|
566
830
|
}), ["numa app scm update github-main --input github.json --revision 4 --yes --json"]);
|
|
567
831
|
addExamples(scm.command("rotate <code>")
|
|
568
|
-
.description("
|
|
832
|
+
.description("平台管理员轮换 GitHub 凭据或 Codeup OpenAPI/HTTPS clone 凭据;不会回显 credential")
|
|
569
833
|
.requiredOption("--revision <number>", "当前乐观锁 version")
|
|
570
834
|
.option("--credential-file <file>", "仅 owner 可读(chmod 600)的 provider credential JSON")
|
|
571
835
|
.option("--credential-stdin", "从重定向 stdin 读取 provider credential JSON")
|
|
@@ -587,7 +851,7 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
587
851
|
await output(command, { ok: true, connection }, `${connection.code}\tcredential:configured\tversion:${connection.version}`);
|
|
588
852
|
}), [
|
|
589
853
|
"numa app scm rotate github-main --revision 4 --credential-file github-app.private.json --yes --json",
|
|
590
|
-
"numa app scm rotate codeup-main --revision 2 --credential-stdin --yes --json < codeup
|
|
854
|
+
"numa app scm rotate codeup-main --revision 2 --credential-stdin --yes --json < codeup.private.json"
|
|
591
855
|
]);
|
|
592
856
|
addExamples(scm.command("disable <code>")
|
|
593
857
|
.description("平台管理员软禁用连接;服务端会阻止仍被非终态会话使用的连接")
|
|
@@ -603,11 +867,27 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
603
867
|
await onboarding.disableAdminScmConnection(code, version);
|
|
604
868
|
await output(command, { ok: true, code, disabled: true, previousVersion: version }, `${code}\tDISABLED`);
|
|
605
869
|
}), ["numa app scm disable github-main --revision 5 --yes --json"]);
|
|
870
|
+
addExamples(app.command("begin")
|
|
871
|
+
.description("幂等创建空 onboarding session,为 consumer-bound Repository plan 获取 sessionNo")
|
|
872
|
+
.requiredOption("--mode <mode>", "new|existing")
|
|
873
|
+
.requiredOption("--idempotency-key <key>", "同一 actor/mode 结果未知时复用")
|
|
874
|
+
.option("--yes", "确认创建服务端草稿会话")
|
|
875
|
+
.action(async (options, command) => {
|
|
876
|
+
requireConfirmation(options.yes, "Beginning an onboarding session");
|
|
877
|
+
const key = idempotencyKey(options.idempotencyKey, "onboarding begin");
|
|
878
|
+
const session = await client().createSession(parseOnboardingMode(options.mode), key);
|
|
879
|
+
await output(command, { ok: true, session, idempotencyKey: key }, sessionLine(session));
|
|
880
|
+
}), [
|
|
881
|
+
"numa app begin --mode new --idempotency-key begin-code-index-onboarding --yes --json"
|
|
882
|
+
], [
|
|
883
|
+
"先取得 sessionNo,再创建 consumer=APPLICATION_ONBOARDING、consumerRef=sessionNo 的 Repository plan。",
|
|
884
|
+
"相同 actor/key/mode 安全返回原 session;不同 mode 冲突,不会创建第二个会话。"
|
|
885
|
+
]);
|
|
606
886
|
addExamples(app.command("onboard")
|
|
607
887
|
.description("启动入驻向导,或按 JSON spec 创建、预检和执行")
|
|
608
888
|
.option("--mode <mode>", "new|existing;如有 --spec 必须与文件一致")
|
|
609
889
|
.option("--session <session-no>", "更新已有草稿并跨 Web/CLI 恢复;不创建新会话")
|
|
610
|
-
.option("--spec <file>", "schemaVersion=1
|
|
890
|
+
.option("--spec <file>", "schemaVersion=1(兼容)、2(Repository CP)或 3(Promote 全链路)的 onboarding JSON 文件")
|
|
611
891
|
.option("--plan", "保存草稿并执行服务端预检,不产生外部副作用")
|
|
612
892
|
.option("--apply", "保存草稿、预检并提交服务端异步执行")
|
|
613
893
|
.option("--idempotency-key <key>", "apply 幂等键;结果不明时必须复用")
|
|
@@ -632,11 +912,13 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
632
912
|
idempotencyKey: options.idempotencyKey
|
|
633
913
|
});
|
|
634
914
|
const execution = onboardingExecutionView(result.session);
|
|
635
|
-
const text = result.
|
|
636
|
-
?
|
|
637
|
-
: result.
|
|
638
|
-
?
|
|
639
|
-
:
|
|
915
|
+
const text = result.recoveredFromStatus
|
|
916
|
+
? `写响应未知,已从同一会话状态恢复:\n${executionText(result.session)}\nidempotency-key: ${result.idempotencyKey}`
|
|
917
|
+
: result.task
|
|
918
|
+
? `入驻执行已提交:\n${executionText(result.session)}\nidempotency-key: ${result.idempotencyKey}`
|
|
919
|
+
: result.preflight
|
|
920
|
+
? `预检完成: ${sessionLine(result.session)}\nplan: ${result.preflight.planHash ?? "-"}\nblocking: ${result.preflight.blockingErrors.length}`
|
|
921
|
+
: `草稿已保存: ${sessionLine(result.session)}`;
|
|
640
922
|
await output(command, { ok: true, ...result, execution }, text);
|
|
641
923
|
}), [
|
|
642
924
|
"numa app onboard",
|
|
@@ -648,6 +930,163 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
648
930
|
"未指定 --plan/--apply 时只保存服务端草稿。",
|
|
649
931
|
"完整 spec 保存的 currentStep 为稳定字符串 REVIEW。"
|
|
650
932
|
]);
|
|
933
|
+
const publicationAction = app.command("publication")
|
|
934
|
+
.description("为 schemaVersion=3 会话附加受控本地源码 Publication 计划");
|
|
935
|
+
addExamples(publicationAction.command("resolve <session-no>")
|
|
936
|
+
.description("按原 clientRequestId 只读恢复 publication attach 的确定结果")
|
|
937
|
+
.requiredOption("--client-request-id <id>", "原 attach 使用的稳定 child request ID")
|
|
938
|
+
.action(async (sessionNo, options, command) => {
|
|
939
|
+
const clientRequestId = stableActionText(options.clientRequestId, "client-request-id", 160);
|
|
940
|
+
const result = await client().resolvePublicationPlan(sessionNo, clientRequestId);
|
|
941
|
+
assertPublicationSessionBinding(result, sessionNo, true);
|
|
942
|
+
await output(command, onboardingPublicationResolutionView(result, clientRequestId), `${result.publicationPlan.planNo}\t${result.publicationPlan.state}\t${result.publicationPlan.planHash}\trecovered-from-status`);
|
|
943
|
+
}), [
|
|
944
|
+
"numa app publication resolve <session-no> --client-request-id onboarding-code-index-publication --json"
|
|
945
|
+
], [
|
|
946
|
+
"固定 GET resolve,不上传 bundle、不重放 attach;404 ONBOARDING_PUBLICATION_PLAN_NOT_RESOLVED 可安全稍后重查。",
|
|
947
|
+
"compact 输出不含 manifest path、文件内容、完整 session draft/plan 或 SCM credential。"
|
|
948
|
+
]);
|
|
949
|
+
addExamples(publicationAction.command("attach <session-no>")
|
|
950
|
+
.description("扫描 clean Git HEAD 并创建绑定当前 onboarding planHash 的 child plan;不写远端 Git")
|
|
951
|
+
.requiredOption("--source <directory>", "本地 Git worktree 根目录")
|
|
952
|
+
.requiredOption("--commit-message <message>", "受审提交消息,1-500字符")
|
|
953
|
+
.requiredOption("--client-request-id <id>", "稳定 child request ID")
|
|
954
|
+
.requiredOption("--idempotency-key <key>", "结果未知时必须复用;须与 client-request-id 相同")
|
|
955
|
+
.option("--yes", "确认上传受限源码 bundle 并附加 child plan")
|
|
956
|
+
.action(async (sessionNo, options, command) => {
|
|
957
|
+
requireConfirmation(options.yes, "Attaching a project publication plan");
|
|
958
|
+
const key = idempotencyKey(options.idempotencyKey, "publication attach");
|
|
959
|
+
const clientRequestId = stableActionText(options.clientRequestId, "client-request-id", 160);
|
|
960
|
+
if (key !== clientRequestId) {
|
|
961
|
+
throw new OnboardingError("idempotency-key must exactly match client-request-id for publication attachment.", "onboarding_input_invalid");
|
|
962
|
+
}
|
|
963
|
+
const onboarding = client();
|
|
964
|
+
const session = await onboarding.getSession(sessionNo);
|
|
965
|
+
if (session.state !== "WAITING_EXTERNAL" || session.currentStep !== "ATTACH_PUBLICATION_PLAN") {
|
|
966
|
+
throw new OnboardingError("Publication attachment requires the same parent session to be WAITING_EXTERNAL at ATTACH_PUBLICATION_PLAN.", "onboarding_publication_session_invalid", undefined, undefined, false, { sessionNo: session.sessionNo, state: session.state, currentStep: session.currentStep ?? null });
|
|
967
|
+
}
|
|
968
|
+
const source = await scanPublication(options.source);
|
|
969
|
+
const input = parsedActionInput(OnboardingPublicationPlanActionRequestSchema, {
|
|
970
|
+
parentPlanHash: sha256(session.planHash, "session planHash"),
|
|
971
|
+
clientRequestId,
|
|
972
|
+
sourceRevision: source.sourceRevision,
|
|
973
|
+
sourceTreeDigest: source.sourceTreeDigest,
|
|
974
|
+
clean: true,
|
|
975
|
+
commitMessage: options.commitMessage,
|
|
976
|
+
files: source.requestFiles
|
|
977
|
+
});
|
|
978
|
+
const attempt = await executePublicationAttachmentWithRecovery(onboarding, session.sessionNo, clientRequestId, () => onboarding.attachPublicationPlan(session.sessionNo, input, key));
|
|
979
|
+
const result = attempt.result;
|
|
980
|
+
await output(command, onboardingPublicationAttachmentView(result, source, key, attempt.recoveredFromStatus), `${result.publicationPlan.planNo}\t${result.publicationPlan.state}\t${result.publicationPlan.planHash}\t${attempt.recoveredFromStatus ? "recovered-from-status" : `replayed:${result.replayed}`}`);
|
|
981
|
+
}), [
|
|
982
|
+
"numa app publication attach <session-no> --source ./code-index --commit-message 'feat: publish code-index source' --client-request-id onboarding-code-index-publication --idempotency-key onboarding-code-index-publication --yes --json"
|
|
983
|
+
], [
|
|
984
|
+
"只读取 clean Git HEAD;compact 输出不含 sourceRoot、manifest path、contentBase64、完整 session draft/plan 或 SCM credential。",
|
|
985
|
+
"POST 500/504、连接中断或成功响应无法解析时,CLI 以原 sessionNo/clientRequestId 轮询 GET resolve;绝不自动重放 attach。",
|
|
986
|
+
"该动作只创建 child plan;onboarding worker 仍会等待显式执行和验证。"
|
|
987
|
+
]);
|
|
988
|
+
const jenkinsBindingAction = app.command("jenkins-binding")
|
|
989
|
+
.description("确认 Job Control Plane 产生的候选 Jenkins binding");
|
|
990
|
+
addExamples(jenkinsBindingAction.command("confirm <session-no>")
|
|
991
|
+
.requiredOption("--binding-id <id>", "会话事件返回的候选 binding ID")
|
|
992
|
+
.requiredOption("--expected-version <number>", "候选 binding 当前乐观锁版本")
|
|
993
|
+
.requiredOption("--idempotency-key <key>", "稳定 key;写响应未知时仅用于取证,不自动重发")
|
|
994
|
+
.option("--yes", "确认该 Jenkins job 与应用/tier 的绑定关系")
|
|
995
|
+
.action(async (sessionNo, options, command) => {
|
|
996
|
+
requireConfirmation(options.yes, "Confirming a Jenkins binding");
|
|
997
|
+
const key = idempotencyKey(options.idempotencyKey, "jenkins binding confirmation");
|
|
998
|
+
const input = parsedActionInput(OnboardingJenkinsBindingActionRequestSchema, {
|
|
999
|
+
bindingId: positiveInteger(options.bindingId, "binding-id"),
|
|
1000
|
+
expectedVersion: nonNegativeInteger(options.expectedVersion, "expected-version", 0)
|
|
1001
|
+
});
|
|
1002
|
+
const onboarding = client();
|
|
1003
|
+
const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.confirmJenkinsBinding(sessionNo, input, key), (event) => hasJenkinsBindingRecoveryEvidence(event, input.bindingId));
|
|
1004
|
+
if (!attempt.result) {
|
|
1005
|
+
await output(command, recoveredMutationPayload(attempt.inspection, key), `${input.bindingId}\trecovered-from-status\t${attempt.inspection.session.state}`);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const result = attempt.result;
|
|
1009
|
+
await output(command, { ok: true, ...result, idempotencyKey: key }, `${result.binding.id}\t${result.binding.confirmationStatus}\tv${result.binding.version}\treplayed:${result.replayed}`);
|
|
1010
|
+
}), [
|
|
1011
|
+
"numa app jenkins-binding confirm <session-no> --binding-id 42 --expected-version 0 --idempotency-key confirm-code-index-binding --yes --json"
|
|
1012
|
+
], ["确认不会触发 scan 或 build;Job Control Plane 已独立验证配置和 inventory generation。"]);
|
|
1013
|
+
const observerScopeAction = app.command("observer-scope")
|
|
1014
|
+
.description("把服务绑定关联到 observer 可验证的精确 Flux/Kubernetes 坐标");
|
|
1015
|
+
addExamples(observerScopeAction.command("configure <session-no>")
|
|
1016
|
+
.requiredOption("--service-binding-id <id>", "会话锁定的 GitOps service binding ID")
|
|
1017
|
+
.requiredOption("--kustomization-namespace <name>", "Flux Kustomization namespace")
|
|
1018
|
+
.requiredOption("--kustomization-name <name>", "Flux Kustomization name")
|
|
1019
|
+
.requiredOption("--helm-release-namespace <name>", "HelmRelease namespace")
|
|
1020
|
+
.requiredOption("--helm-release-name <name>", "HelmRelease name")
|
|
1021
|
+
.requiredOption("--workload-namespace <name>", "最终 workload namespace")
|
|
1022
|
+
.requiredOption("--workload-kind <kind>", "最终 workload kind")
|
|
1023
|
+
.requiredOption("--workload-name <name>", "最终 workload name")
|
|
1024
|
+
.requiredOption("--expected-version <number>", "scope 当前版本;首次创建使用服务端事件给出的版本")
|
|
1025
|
+
.requiredOption("--idempotency-key <key>", "稳定 key;写响应未知时仅用于取证,不自动重发")
|
|
1026
|
+
.option("--yes", "确认精确运行态坐标")
|
|
1027
|
+
.action(async (sessionNo, options, command) => {
|
|
1028
|
+
requireConfirmation(options.yes, "Configuring an observer scope");
|
|
1029
|
+
const key = idempotencyKey(options.idempotencyKey, "observer scope configuration");
|
|
1030
|
+
const input = parsedActionInput(OnboardingObserverScopeActionRequestSchema, {
|
|
1031
|
+
serviceBindingId: positiveInteger(options.serviceBindingId, "service-binding-id"),
|
|
1032
|
+
kustomizationNamespace: stableActionText(options.kustomizationNamespace, "kustomization-namespace"),
|
|
1033
|
+
kustomizationName: stableActionText(options.kustomizationName, "kustomization-name"),
|
|
1034
|
+
helmReleaseNamespace: stableActionText(options.helmReleaseNamespace, "helm-release-namespace"),
|
|
1035
|
+
helmReleaseName: stableActionText(options.helmReleaseName, "helm-release-name"),
|
|
1036
|
+
workloadNamespace: stableActionText(options.workloadNamespace, "workload-namespace"),
|
|
1037
|
+
workloadKind: stableActionText(options.workloadKind, "workload-kind", 160),
|
|
1038
|
+
workloadName: stableActionText(options.workloadName, "workload-name"),
|
|
1039
|
+
expectedVersion: nonNegativeInteger(options.expectedVersion, "expected-version", 0)
|
|
1040
|
+
});
|
|
1041
|
+
const onboarding = client();
|
|
1042
|
+
const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.configureObserverScope(sessionNo, input, key), hasObserverScopeRecoveryEvidence);
|
|
1043
|
+
if (!attempt.result) {
|
|
1044
|
+
await output(command, recoveredMutationPayload(attempt.inspection, key), `${String(attempt.inspection.execution.eventOutput.observerScopeNo)}\trecovered-from-status\t${attempt.inspection.session.state}`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const result = attempt.result;
|
|
1048
|
+
await output(command, { ok: true, ...result, idempotencyKey: key }, `${result.scope.scopeNo}\t${result.scope.status}\t${result.scope.evidenceFreshness}\t${result.scope.lastDriftState ?? "-"}`);
|
|
1049
|
+
}), [
|
|
1050
|
+
"numa app observer-scope configure <session-no> --service-binding-id 17 --kustomization-namespace flux-system --kustomization-name prd --helm-release-namespace prd --helm-release-name code-index --workload-namespace prd --workload-kind Deployment --workload-name code-index --expected-version 0 --idempotency-key scope-code-index-prd --yes --json"
|
|
1051
|
+
], ["服务端强制 serviceBindingId 属于该会话;CLI 不允许任意 cluster/path/YAML。"]);
|
|
1052
|
+
const firstBuildAction = app.command("first-build")
|
|
1053
|
+
.description("在全部结构与运行证据就绪后,独立决定首次 Jenkins build");
|
|
1054
|
+
addExamples(firstBuildAction.command("decide <session-no>")
|
|
1055
|
+
.requiredOption("--decision <decision>", "trigger|defer")
|
|
1056
|
+
.option("--branch <branch>", "TRIGGER 的分支;缺省由服务端锁定配置决定")
|
|
1057
|
+
.option("--parameter <KEY=VALUE>", "非敏感构建参数,可重复", collectParameter, [])
|
|
1058
|
+
.option("--approve-id <id>", "生产 TRIGGER 的审批记录 ID")
|
|
1059
|
+
.option("--production-reason <reason>", "生产 TRIGGER 的受审原因(至少10字符)")
|
|
1060
|
+
.requiredOption("--idempotency-key <key>", "稳定 key;写响应未知时仅用于取证,不自动重发")
|
|
1061
|
+
.option("--yes", "确认记录决定;TRIGGER 会真正提交 Jenkins build")
|
|
1062
|
+
.action(async (sessionNo, options, command) => {
|
|
1063
|
+
requireConfirmation(options.yes, "Recording the first-build decision");
|
|
1064
|
+
const decision = options.decision.trim().toUpperCase();
|
|
1065
|
+
const input = parsedActionInput(OnboardingFirstBuildActionRequestSchema, {
|
|
1066
|
+
decision,
|
|
1067
|
+
branch: options.branch?.trim() || undefined,
|
|
1068
|
+
parameters: buildParameters(options.parameter),
|
|
1069
|
+
approveId: options.approveId?.trim() || undefined,
|
|
1070
|
+
productionReason: options.productionReason?.trim() || undefined
|
|
1071
|
+
});
|
|
1072
|
+
const key = idempotencyKey(options.idempotencyKey, "first-build decision");
|
|
1073
|
+
const onboarding = client();
|
|
1074
|
+
const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.decideFirstBuild(sessionNo, input, key), (event) => hasFirstBuildRecoveryEvidence(event, input));
|
|
1075
|
+
if (!attempt.result) {
|
|
1076
|
+
await output(command, recoveredMutationPayload(attempt.inspection, key), `${decision}\trecovered-from-status\t${attempt.inspection.session.state}`);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const result = attempt.result;
|
|
1080
|
+
await output(command, { ok: true, ...result, idempotencyKey: key }, result.build
|
|
1081
|
+
? `${result.decision}\t${result.build.requestId}\t${result.build.state}\treplayed:${result.replayed}`
|
|
1082
|
+
: `${result.decision}\tno-build\treplayed:${result.replayed}`);
|
|
1083
|
+
}), [
|
|
1084
|
+
"numa app first-build decide <session-no> --decision defer --idempotency-key defer-code-index-first-build --yes --json",
|
|
1085
|
+
"numa app first-build decide <session-no> --decision trigger --branch main --idempotency-key trigger-code-index-first-build --yes --json"
|
|
1086
|
+
], [
|
|
1087
|
+
"DEFER 永不触发 build,且会话保持 WAITING_EXTERNAL。",
|
|
1088
|
+
"生产 TRIGGER 仍由服务端强制 ops-admin 与受审生产依据;参数禁止凭据式 key/value。"
|
|
1089
|
+
]);
|
|
651
1090
|
addExamples(app.command("resume <session-no>")
|
|
652
1091
|
.description("让服务端重新检查外部等待条件,不在客户端推进状态")
|
|
653
1092
|
.action(async (sessionNo, _options, command) => {
|
|
@@ -664,11 +1103,16 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
664
1103
|
throw new OnboardingError("Session has no server plan hash; run onboarding preflight first.", "onboarding_plan_missing");
|
|
665
1104
|
}
|
|
666
1105
|
const key = idempotencyKey(options.idempotencyKey);
|
|
667
|
-
const
|
|
1106
|
+
const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.apply(sessionNo, { planHash: before.planHash, idempotencyKey: key }), (event) => hasOnboardingApplyRecoveryEvidence(event, before.planHash));
|
|
1107
|
+
if (!attempt.result) {
|
|
1108
|
+
await output(command, recoveredMutationPayload(attempt.inspection, key), `写响应未知,已从同一会话状态恢复:\n${executionText(attempt.inspection.session, attempt.inspection.events)}\nidempotency-key: ${key}`);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const task = attempt.result;
|
|
668
1112
|
const session = await onboarding.getSession(sessionNo);
|
|
669
1113
|
await output(command, { ok: true, session, task, idempotencyKey: key, execution: onboardingExecutionView(session) }, `入驻执行已提交:\n${executionText(session)}\nidempotency-key: ${key}`);
|
|
670
1114
|
}), ["numa app apply 8d15ee89-43f0-4378-a7aa-51f09c5b42fc --idempotency-key app-order-20260819 --json"], [
|
|
671
|
-
"用于先 --plan
|
|
1115
|
+
"用于先 --plan 后的首次显式 apply;响应不明时只读 inspect/status,证据不足返回 onboarding_result_unknown,不要重发 apply。"
|
|
672
1116
|
]);
|
|
673
1117
|
addExamples(app.command("inspect <session-no>")
|
|
674
1118
|
.description("查询会话;watch 从最后 event sequence 持续恢复")
|