@numa-tech/numa 1.14.29 → 1.14.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +35 -201
  2. package/dist/application-onboarding/client.d.ts +3 -17
  3. package/dist/application-onboarding/client.js +1 -14
  4. package/dist/application-onboarding/client.js.map +1 -1
  5. package/dist/application-onboarding/commands.d.ts +0 -2
  6. package/dist/application-onboarding/commands.js +5 -76
  7. package/dist/application-onboarding/commands.js.map +1 -1
  8. package/dist/application-onboarding/schemas.d.ts +28 -301
  9. package/dist/application-onboarding/schemas.js +13 -91
  10. package/dist/application-onboarding/schemas.js.map +1 -1
  11. package/dist/cli.js +0 -2
  12. package/dist/cli.js.map +1 -1
  13. package/dist/command-catalog.js +6 -336
  14. package/dist/command-catalog.js.map +1 -1
  15. package/dist/gitops/client.d.ts +0 -38
  16. package/dist/gitops/client.js +1 -9
  17. package/dist/gitops/client.js.map +1 -1
  18. package/dist/gitops/commands.js +1 -20
  19. package/dist/gitops/commands.js.map +1 -1
  20. package/dist/gitops/schemas.d.ts +0 -112
  21. package/dist/gitops/schemas.js +0 -38
  22. package/dist/gitops/schemas.js.map +1 -1
  23. package/dist/jenkins/commands.d.ts +0 -4
  24. package/dist/jenkins/commands.js +0 -10
  25. package/dist/jenkins/commands.js.map +1 -1
  26. package/dist/jenkins-jobs/commands.d.ts +0 -2
  27. package/dist/jenkins-jobs/commands.js +0 -2
  28. package/dist/jenkins-jobs/commands.js.map +1 -1
  29. package/dist/pipeline/commands.js +0 -2
  30. package/dist/pipeline/commands.js.map +1 -1
  31. package/dist/platform-profile.d.ts +2 -0
  32. package/dist/platform-profile.js +2 -1
  33. package/dist/platform-profile.js.map +1 -1
  34. package/package.json +4 -4
  35. package/skills/numa-create-application/SKILL.md +30 -163
  36. package/skills/numa-create-application/agents/openai.yaml +2 -2
  37. package/skills/numa-create-application/evals/evals.json +9 -150
  38. package/skills/numa-create-application/references/checklist.md +22 -139
  39. package/skills/numa-create-application/references/numa-cli.md +27 -257
  40. package/skills/numa-create-application/references/promote-workflows.md +22 -188
  41. package/skills/numa-create-application/scripts/inspect-project.mjs +1 -1
  42. package/skills/numa-jenkins-deployment/SKILL.md +21 -132
  43. package/skills/numa-jenkins-deployment/agents/openai.yaml +2 -2
  44. package/skills/numa-jenkins-deployment/evals/evals.json +9 -59
  45. package/skills/numa-jenkins-deployment/references/cli-contract.md +22 -101
  46. package/skills/numa-jenkins-deployment/references/diagnostics.md +2 -2
  47. package/skills/numa-jenkins-deployment/references/release-policy.md +19 -85
  48. package/skills/numa-local-flux-deploy/SKILL.md +1 -1
  49. package/skills/numa-local-flux-deploy/references/local-flux-release.md +1 -1
  50. package/dist/integration-bootstrap/client.d.ts +0 -61
  51. package/dist/integration-bootstrap/client.js +0 -293
  52. package/dist/integration-bootstrap/client.js.map +0 -1
  53. package/dist/integration-bootstrap/commands.d.ts +0 -86
  54. package/dist/integration-bootstrap/commands.js +0 -615
  55. package/dist/integration-bootstrap/commands.js.map +0 -1
  56. package/dist/integration-bootstrap/schemas.d.ts +0 -831
  57. package/dist/integration-bootstrap/schemas.js +0 -565
  58. package/dist/integration-bootstrap/schemas.js.map +0 -1
  59. package/dist/jenkins-gitops-rollouts/client.d.ts +0 -197
  60. package/dist/jenkins-gitops-rollouts/client.js +0 -126
  61. package/dist/jenkins-gitops-rollouts/client.js.map +0 -1
  62. package/dist/jenkins-gitops-rollouts/commands.d.ts +0 -50
  63. package/dist/jenkins-gitops-rollouts/commands.js +0 -358
  64. package/dist/jenkins-gitops-rollouts/commands.js.map +0 -1
  65. package/dist/jenkins-gitops-rollouts/schemas.d.ts +0 -217
  66. package/dist/jenkins-gitops-rollouts/schemas.js +0 -131
  67. package/dist/jenkins-gitops-rollouts/schemas.js.map +0 -1
  68. package/dist/platform-build-job-replans/client.d.ts +0 -291
  69. package/dist/platform-build-job-replans/client.js +0 -148
  70. package/dist/platform-build-job-replans/client.js.map +0 -1
  71. package/dist/platform-build-job-replans/commands.d.ts +0 -6
  72. package/dist/platform-build-job-replans/commands.js +0 -373
  73. package/dist/platform-build-job-replans/commands.js.map +0 -1
  74. package/dist/platform-build-job-replans/schemas.d.ts +0 -147
  75. package/dist/platform-build-job-replans/schemas.js +0 -151
  76. package/dist/platform-build-job-replans/schemas.js.map +0 -1
  77. package/dist/platform-build-rollouts/client.d.ts +0 -346
  78. package/dist/platform-build-rollouts/client.js +0 -153
  79. package/dist/platform-build-rollouts/client.js.map +0 -1
  80. package/dist/platform-build-rollouts/commands.d.ts +0 -98
  81. package/dist/platform-build-rollouts/commands.js +0 -465
  82. package/dist/platform-build-rollouts/commands.js.map +0 -1
  83. package/dist/platform-build-rollouts/schemas.d.ts +0 -348
  84. package/dist/platform-build-rollouts/schemas.js +0 -257
  85. package/dist/platform-build-rollouts/schemas.js.map +0 -1
  86. package/dist/platform-builds/client.d.ts +0 -147
  87. package/dist/platform-builds/client.js +0 -115
  88. package/dist/platform-builds/client.js.map +0 -1
  89. package/dist/platform-builds/commands.d.ts +0 -7
  90. package/dist/platform-builds/commands.js +0 -226
  91. package/dist/platform-builds/commands.js.map +0 -1
  92. package/dist/platform-builds/schemas.d.ts +0 -127
  93. package/dist/platform-builds/schemas.js +0 -66
  94. package/dist/platform-builds/schemas.js.map +0 -1
@@ -1,615 +0,0 @@
1
- import process from "node:process";
2
- import { lstat } from "node:fs/promises";
3
- import { resolve } from "node:path";
4
- import { setTimeout as delay } from "node:timers/promises";
5
- import open from "open";
6
- import { loadAppConfig } from "../app-config.js";
7
- import { loadConfig } from "../config.js";
8
- import { PipelineError, sanitizePipelineOutput } from "../pipeline/errors.js";
9
- import { writePrivateJson } from "../private-file.js";
10
- import { AuthorizationClient } from "../authorization/client.js";
11
- import { waitForKeycloakBootstrap } from "../authorization/commands.js";
12
- import { IntegrationBootstrapClient, integrationBootstrapClientRequestId, integrationBootstrapIdempotencyKey } from "./client.js";
13
- const PLAN_TERMINAL = new Set(["SUCCEEDED", "FAILED", "EXPIRED"]);
14
- const ACTION_TERMINAL = new Set(["UNKNOWN", "SUCCEEDED", "FAILED", "EXPIRED"]);
15
- function normalizedBrowserOptions(options) {
16
- return {
17
- ...options,
18
- noOpen: options.noOpen === true || options.open === false
19
- };
20
- }
21
- function outputPath(value) {
22
- if (value !== value.trim() || value.length === 0 || /[\u0000-\u001f\u007f]/u.test(value)) {
23
- throw new PipelineError("output must be a non-empty local path without control characters.", "INTEGRATION_BOOTSTRAP_INPUT_INVALID");
24
- }
25
- return resolve(value);
26
- }
27
- async function pathExists(path) {
28
- try {
29
- await lstat(path);
30
- return true;
31
- }
32
- catch (error) {
33
- if (error.code === "ENOENT")
34
- return false;
35
- throw error;
36
- }
37
- }
38
- export async function executeIntegrationBootstrapTrustAnchorExport(api, options) {
39
- const destination = outputPath(options.output);
40
- if (!options.force && await pathExists(destination)) {
41
- throw new PipelineError("Trust-anchor output already exists; choose another path or pass --force to replace it.", "INTEGRATION_BOOTSTRAP_TRUST_ANCHOR_OUTPUT_EXISTS");
42
- }
43
- const document = await api.getTrustAnchorConfiguration();
44
- await writePrivateJson(destination, document.configuration);
45
- return { ...document, outputPath: destination };
46
- }
47
- function snake(key) {
48
- return key.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toLowerCase();
49
- }
50
- export function stableIntegrationBootstrapOutput(value) {
51
- const safe = sanitizePipelineOutput(value);
52
- if (Array.isArray(safe))
53
- return safe.map(stableIntegrationBootstrapOutput);
54
- if (safe && typeof safe === "object") {
55
- return Object.fromEntries(Object.entries(safe).map(([key, item]) => [snake(key), stableIntegrationBootstrapOutput(item)]));
56
- }
57
- return safe;
58
- }
59
- async function jsonEnabled(command) {
60
- return Boolean(command.optsWithGlobals().json
61
- || (await loadAppConfig()).output === "json");
62
- }
63
- async function output(command, payload, text) {
64
- process.stdout.write(await jsonEnabled(command)
65
- ? `${JSON.stringify(stableIntegrationBootstrapOutput(payload), null, 2)}\n`
66
- : `${String(sanitizePipelineOutput(text))}\n`);
67
- }
68
- function planLine(plan) {
69
- const changes = plan.changes.map((change) => ` ${change.component}\t${change.action}\t${change.target}\t${change.reasonCode}`);
70
- const blockers = plan.blockers.map((blocker) => ` ${blocker.component}\t${blocker.target}\t${blocker.reasonCode}`);
71
- return [
72
- `${plan.state}\t${plan.schemaVersion}\t${plan.profileCode}`,
73
- `plan digest: ${plan.planDigest}`,
74
- `review expires: ${plan.reviewExpiresAt}`,
75
- "changes:",
76
- ...(changes.length ? changes : [" -"]),
77
- "blockers:",
78
- ...(blockers.length ? blockers : [" -"])
79
- ].join("\n");
80
- }
81
- function sessionLine(session) {
82
- if (session.plan != null)
83
- return planLine(session.plan);
84
- const error = session.errorCode == null ? "" : `\n${session.errorCode}: ${session.errorSummary ?? "-"}`;
85
- return `${session.sessionNo}\t${session.status}${error}`;
86
- }
87
- function runLine(run) {
88
- return `${run.runNo}\t${run.state}\tv${run.version}\t${run.step}\t${run.disposition ?? "-"}`;
89
- }
90
- function actionLine(action) {
91
- return `${action.actionNo}\t${action.kind}\t${action.state}\t${action.disposition ?? "-"}`;
92
- }
93
- const PRIVILEGED_STEP_LABELS = {
94
- SUCCESS: "成功",
95
- FAILED: "失败",
96
- UNCHANGED: "无变更",
97
- SKIPPED: "跳过",
98
- WAITING: "等待"
99
- };
100
- function privilegedRunLine(run) {
101
- const lines = run.steps.map((step) => `${PRIVILEGED_STEP_LABELS[step.status]}\t${step.step}\t${step.detail}`);
102
- const error = run.errorCode == null ? [] : [`${run.errorCode}: ${run.errorSummary ?? "-"}`];
103
- return [
104
- `${run.runId}\t${run.mode}\t${run.status}`,
105
- ...lines,
106
- ...error
107
- ].join("\n");
108
- }
109
- function startMayHaveCommitted(error) {
110
- return error instanceof PipelineError
111
- && (error.code === "INTEGRATION_BOOTSTRAP_START_RESULT_UNKNOWN"
112
- || error.code === "INTEGRATION_BOOTSTRAP_CONFLICT");
113
- }
114
- function unknown(message, error) {
115
- return new PipelineError(message, "INTEGRATION_BOOTSTRAP_RESULT_UNKNOWN", error instanceof PipelineError ? error.status : undefined, error instanceof PipelineError ? error.requestId : undefined);
116
- }
117
- function identityMismatch(message) {
118
- return new PipelineError(message, "INTEGRATION_BOOTSTRAP_IDENTITY_MISMATCH", 409);
119
- }
120
- function expectedVersion(value) {
121
- if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) {
122
- throw new PipelineError("expected-run-version must be a canonical non-negative 32-bit integer.", "INTEGRATION_BOOTSTRAP_INPUT_INVALID");
123
- }
124
- const parsed = Number(value);
125
- if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 2_147_483_647) {
126
- throw new PipelineError("expected-run-version must be a non-negative 32-bit integer.", "INTEGRATION_BOOTSTRAP_INPUT_INVALID");
127
- }
128
- return parsed;
129
- }
130
- function requireConfirmation(value, action) {
131
- if (!value) {
132
- throw new PipelineError(`${action} requires --yes.`, "INTEGRATION_BOOTSTRAP_CONFIRMATION_REQUIRED");
133
- }
134
- }
135
- const SHA256 = /^[a-f0-9]{64}$/u;
136
- const INIT_OPERATION_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,159}$/u;
137
- export function integrationBootstrapInitKeys(operationKey) {
138
- if (!INIT_OPERATION_KEY.test(operationKey)) {
139
- throw new PipelineError("operation-key must be 8-160 characters, start with a letter or number, and use only letters, numbers, . _ : or -.", "INTEGRATION_BOOTSTRAP_OPERATION_KEY_INVALID");
140
- }
141
- const prefix = `integration-init:${operationKey}`;
142
- try {
143
- return {
144
- planClientRequestId: integrationBootstrapClientRequestId(`${prefix}:plan`),
145
- planIdempotencyKey: integrationBootstrapIdempotencyKey(`${prefix}:plan-write`),
146
- applyClientRequestId: integrationBootstrapClientRequestId(`${prefix}:apply`),
147
- applyIdempotencyKey: integrationBootstrapIdempotencyKey(`${prefix}:apply-write`)
148
- };
149
- }
150
- catch (error) {
151
- if (error instanceof PipelineError) {
152
- throw new PipelineError("operation-key must be 8-160 characters, start with a letter or number, and use only letters, numbers, . _ : or -.", "INTEGRATION_BOOTSTRAP_OPERATION_KEY_INVALID");
153
- }
154
- throw error;
155
- }
156
- }
157
- function exactExpectedPlanDigest(value) {
158
- if (value == null)
159
- return undefined;
160
- if (!SHA256.test(value)) {
161
- throw new PipelineError("expected-plan-digest must be exactly 64 lowercase hexadecimal characters.", "INTEGRATION_BOOTSTRAP_INPUT_INVALID");
162
- }
163
- return value;
164
- }
165
- async function openAuthorization(url, label, openBrowser, notify) {
166
- notify(`正在打开一次性 Keycloak 管理员授权(${label})…\n`);
167
- try {
168
- await openBrowser(url);
169
- }
170
- catch {
171
- notify(`无法自动打开浏览器,请手动打开:\n${url}\n`);
172
- }
173
- }
174
- function deadline(expiresAt) {
175
- return Date.parse(expiresAt) + 15_000;
176
- }
177
- async function pollPlan(api, sessionNo, expiresAt, sleep) {
178
- let expires = deadline(expiresAt);
179
- for (;;) {
180
- const session = await api.getPlanSession(sessionNo);
181
- if (PLAN_TERMINAL.has(session.status))
182
- return session;
183
- expires = Math.max(expires, deadline(session.expiresAt));
184
- if (!Number.isFinite(expires) || Date.now() >= expires) {
185
- throw unknown("Integration bootstrap plan authorization expired before a durable terminal result was observed.");
186
- }
187
- await sleep(1_000);
188
- }
189
- }
190
- async function pollAction(api, actionNo, expiresAt, sleep) {
191
- const expires = deadline(expiresAt);
192
- for (;;) {
193
- const action = await api.getAction(actionNo);
194
- if (ACTION_TERMINAL.has(action.state))
195
- return action;
196
- if (!Number.isFinite(expires) || Date.now() >= expires) {
197
- throw unknown("Integration bootstrap authorization expired before a durable action result was observed.");
198
- }
199
- await sleep(1_000);
200
- }
201
- }
202
- function exactPlanSession(session, clientRequestId) {
203
- if (session.clientRequestId !== clientRequestId) {
204
- throw identityMismatch("Resolved integration bootstrap plan session does not match the original client-request-id.");
205
- }
206
- }
207
- function exactRun(run, input) {
208
- if (run.clientRequestId !== input.clientRequestId || run.planSessionNo !== input.planSessionNo
209
- || run.planDigest !== input.planDigest) {
210
- throw identityMismatch("Resolved integration bootstrap run does not match the original immutable apply request.");
211
- }
212
- }
213
- function exactAction(action, runNo, kind, input) {
214
- if (action.clientRequestId !== input.clientRequestId || action.runNo !== runNo
215
- || action.kind !== kind || action.expectedRunVersion !== input.expectedRunVersion) {
216
- throw identityMismatch("Resolved integration bootstrap action does not match the original immutable request.");
217
- }
218
- }
219
- function isExactAction(action, runNo, kind, input) {
220
- return action != null && action.clientRequestId === input.clientRequestId
221
- && action.runNo === runNo && action.kind === kind
222
- && action.expectedRunVersion === input.expectedRunVersion;
223
- }
224
- async function resolvePlanAfterStart(api, options, error) {
225
- let session;
226
- try {
227
- session = await api.resolvePlanSession(options.clientRequestId);
228
- }
229
- catch (resolveError) {
230
- throw unknown("Integration bootstrap plan result is unknown. Retry the same plan command with the original client-request-id and idempotency-key; do not choose new values.", resolveError instanceof PipelineError ? resolveError : error);
231
- }
232
- exactPlanSession(session, options.clientRequestId);
233
- if (session.status !== "AWAITING_AUTHORIZATION")
234
- return { session };
235
- try {
236
- return { start: await api.startPlanSession(options.clientRequestId, options.idempotencyKey) };
237
- }
238
- catch (replayError) {
239
- throw unknown("The actor-scoped plan session is awaiting authorization, but its one-time URL could not be recovered. Retry the exact same plan command and keys.", replayError);
240
- }
241
- }
242
- export async function executeIntegrationBootstrapPlan(api, options, openBrowser, sleep, notify = (message) => process.stderr.write(message)) {
243
- const normalized = {
244
- ...options,
245
- clientRequestId: integrationBootstrapClientRequestId(options.clientRequestId),
246
- idempotencyKey: integrationBootstrapIdempotencyKey(options.idempotencyKey)
247
- };
248
- let result;
249
- try {
250
- result = { start: await api.startPlanSession(normalized.clientRequestId, normalized.idempotencyKey) };
251
- }
252
- catch (error) {
253
- if (!startMayHaveCommitted(error))
254
- throw error;
255
- result = await resolvePlanAfterStart(api, normalized, error);
256
- }
257
- if (result.session != null) {
258
- if (options.noOpen || PLAN_TERMINAL.has(result.session.status))
259
- return { session: result.session };
260
- return { session: await pollPlan(api, result.session.sessionNo, result.session.expiresAt, sleep) };
261
- }
262
- const started = result.start;
263
- if (started.clientRequestId !== normalized.clientRequestId) {
264
- throw identityMismatch("Integration bootstrap plan start does not match client-request-id.");
265
- }
266
- if (options.noOpen)
267
- return { started };
268
- await openAuthorization(started.authorizationUrl, `session ${started.sessionNo}`, openBrowser, notify);
269
- return {
270
- started,
271
- session: await pollPlan(api, started.sessionNo, started.authorizationExpiresAt, sleep)
272
- };
273
- }
274
- function initContinuationKeys(operationKey, kind, version) {
275
- const prefix = `integration-init:${operationKey}:${kind.toLowerCase()}:v${version}`;
276
- return {
277
- clientRequestId: integrationBootstrapClientRequestId(prefix),
278
- idempotencyKey: integrationBootstrapIdempotencyKey(`${prefix}:write`)
279
- };
280
- }
281
- async function continueIntegrationBootstrapInit(api, operationKey, initial, noOpen, openBrowser, sleep, notify) {
282
- const results = [];
283
- let run = initial;
284
- // One init invocation may need a read-only reconciliation followed by one write recovery.
285
- // A second unknown write is left durable and the same init command can be run again.
286
- for (let stage = 0; stage < 2 && run.state === "UNKNOWN"; stage += 1) {
287
- const active = run.currentAction;
288
- const resumableKind = active != null
289
- && active.kind !== "APPLY"
290
- && ["AWAITING_AUTHORIZATION", "AUTHORIZING", "RUNNING"].includes(active.state)
291
- ? active.kind : null;
292
- const resumable = resumableKind == null ? null : active;
293
- const kind = resumableKind
294
- ?? (run.recoverAllowed ? "RECOVER"
295
- : run.reapplyAllowed ? "REAPPLY"
296
- : run.reconcileAllowed ? "RECONCILE" : null);
297
- if (kind == null)
298
- break;
299
- const expectedRunVersion = resumable?.expectedRunVersion ?? run.version;
300
- const keys = initContinuationKeys(operationKey, kind, expectedRunVersion);
301
- if (resumable != null && resumable.clientRequestId !== keys.clientRequestId)
302
- break;
303
- const result = await executeIntegrationBootstrapAction(api, run.runNo, kind, {
304
- expectedRunVersion: String(expectedRunVersion),
305
- clientRequestId: keys.clientRequestId,
306
- idempotencyKey: keys.idempotencyKey,
307
- noOpen,
308
- yes: true
309
- }, openBrowser, sleep, notify);
310
- results.push({ kind, ...result });
311
- run = result.run;
312
- if (result.action == null
313
- || !["SUCCEEDED", "UNKNOWN"].includes(result.action.state))
314
- break;
315
- }
316
- return results;
317
- }
318
- export async function executeIntegrationBootstrapInit(api, options, openBrowser, sleep, confirmApply, notify = (message) => process.stderr.write(message)) {
319
- const keys = integrationBootstrapInitKeys(options.operationKey);
320
- const expectedDigest = exactExpectedPlanDigest(options.expectedPlanDigest);
321
- if (options.yes && expectedDigest == null) {
322
- throw new PipelineError("Non-interactive init requires --expected-plan-digest together with --yes.", "INTEGRATION_BOOTSTRAP_CONFIRMATION_REQUIRED");
323
- }
324
- const plan = await executeIntegrationBootstrapPlan(api, {
325
- clientRequestId: keys.planClientRequestId,
326
- idempotencyKey: keys.planIdempotencyKey,
327
- noOpen: options.noOpen
328
- }, openBrowser, sleep, notify);
329
- const session = plan.session;
330
- const reviewed = session?.status === "SUCCEEDED" ? session.plan : null;
331
- if (session == null || reviewed == null || reviewed.state !== "READY") {
332
- return { keys, plan, confirmed: false, continuations: [] };
333
- }
334
- if (expectedDigest != null && expectedDigest !== reviewed.planDigest) {
335
- throw new PipelineError("The READY plan digest does not match --expected-plan-digest; no apply was started.", "INTEGRATION_BOOTSTRAP_PLAN_DIGEST_MISMATCH", 409);
336
- }
337
- notify(`${planLine(reviewed)}\n`);
338
- const confirmed = options.yes || (confirmApply != null && await confirmApply(reviewed));
339
- if (!confirmed)
340
- return { keys, plan, confirmed: false, continuations: [] };
341
- const apply = await executeIntegrationBootstrapApply(api, {
342
- sessionNo: session.sessionNo,
343
- planDigest: reviewed.planDigest,
344
- clientRequestId: keys.applyClientRequestId,
345
- idempotencyKey: keys.applyIdempotencyKey,
346
- noOpen: options.noOpen,
347
- yes: true
348
- }, openBrowser, sleep, notify);
349
- const applyRun = apply.run;
350
- const continuations = applyRun?.state === "UNKNOWN"
351
- ? await continueIntegrationBootstrapInit(api, options.operationKey, applyRun, options.noOpen, openBrowser, sleep, notify)
352
- : [];
353
- return { keys, plan, confirmed: true, apply, continuations };
354
- }
355
- async function resolveApplyAfterStart(api, input, idempotencyKey, error) {
356
- let run;
357
- try {
358
- run = await api.resolveRun(input.clientRequestId);
359
- }
360
- catch (resolveError) {
361
- throw unknown("Integration bootstrap apply result is unknown. Use run resolve with the original client-request-id; never create another apply request.", resolveError instanceof PipelineError ? resolveError : error);
362
- }
363
- exactRun(run, input);
364
- if (run.currentAction?.state !== "AWAITING_AUTHORIZATION")
365
- return { run };
366
- try {
367
- return { start: await api.startApply(input, idempotencyKey) };
368
- }
369
- catch (replayError) {
370
- throw unknown("Apply authorization URL recovery failed. Retry the exact same apply command and keys.", replayError);
371
- }
372
- }
373
- export async function executeIntegrationBootstrapApply(api, options, openBrowser, sleep, notify = (message) => process.stderr.write(message)) {
374
- requireConfirmation(options.yes, "Integration bootstrap apply");
375
- const input = {
376
- planSessionNo: options.sessionNo,
377
- planDigest: options.planDigest,
378
- clientRequestId: integrationBootstrapClientRequestId(options.clientRequestId)
379
- };
380
- const key = integrationBootstrapIdempotencyKey(options.idempotencyKey);
381
- const plan = await api.getPlanSession(input.planSessionNo);
382
- let result;
383
- const planReady = plan.status === "SUCCEEDED" && plan.plan?.state === "READY"
384
- && plan.plan.planDigest === input.planDigest
385
- && Date.parse(plan.plan.reviewExpiresAt) > Date.now();
386
- if (!planReady) {
387
- let existing;
388
- try {
389
- existing = await api.resolveRun(input.clientRequestId);
390
- }
391
- catch (error) {
392
- if (error instanceof PipelineError && error.code === "INTEGRATION_BOOTSTRAP_NOT_FOUND") {
393
- throw new PipelineError("Apply requires the exact unexpired READY v2 plan session and digest.", "INTEGRATION_BOOTSTRAP_PLAN_NOT_READY", 409);
394
- }
395
- throw unknown("The plan is no longer startable and an existing run could not be resolved safely. Use run resolve with the original client-request-id; do not POST another apply.", error);
396
- }
397
- exactRun(existing, input);
398
- if (existing.currentAction?.kind === "APPLY"
399
- && existing.currentAction.state === "AWAITING_AUTHORIZATION") {
400
- try {
401
- result = { start: await api.startApply(input, key) };
402
- }
403
- catch (error) {
404
- if (!startMayHaveCommitted(error))
405
- throw error;
406
- result = await resolveApplyAfterStart(api, input, key, error);
407
- }
408
- }
409
- else {
410
- result = { run: existing };
411
- }
412
- }
413
- else {
414
- try {
415
- result = { start: await api.startApply(input, key) };
416
- }
417
- catch (error) {
418
- if (!startMayHaveCommitted(error))
419
- throw error;
420
- result = await resolveApplyAfterStart(api, input, key, error);
421
- }
422
- }
423
- if (result.run != null) {
424
- const current = result.run.currentAction;
425
- if (options.noOpen || current == null || ACTION_TERMINAL.has(current.state))
426
- return { run: result.run };
427
- const action = await pollAction(api, current.actionNo, current.authorizationExpiresAt, sleep);
428
- return { action, run: await api.getRun(result.run.runNo) };
429
- }
430
- const started = result.start;
431
- if (started.clientRequestId !== input.clientRequestId || started.planSessionNo !== input.planSessionNo
432
- || started.planDigest !== input.planDigest)
433
- throw identityMismatch("Apply start identity is inconsistent.");
434
- if (options.noOpen)
435
- return { started };
436
- await openAuthorization(started.authorizationUrl, `action ${started.actionNo}`, openBrowser, notify);
437
- const action = await pollAction(api, started.actionNo, started.authorizationExpiresAt, sleep);
438
- return { started, action, run: await api.getRun(started.runNo) };
439
- }
440
- async function resolveActionAfterStart(api, runNo, kind, input, idempotencyKey, error) {
441
- let action;
442
- try {
443
- action = await api.resolveAction(input.clientRequestId);
444
- }
445
- catch (resolveError) {
446
- throw unknown(`Integration bootstrap ${kind.toLowerCase()} result is unknown. Run "numa integration bootstrap action resolve ${runNo} --client-request-id ${input.clientRequestId}"; do not replay automatically.`, resolveError instanceof PipelineError ? resolveError : error);
447
- }
448
- exactAction(action, runNo, kind, input);
449
- if (action.state !== "AWAITING_AUTHORIZATION")
450
- return { action };
451
- try {
452
- return { start: await api.startAction(runNo, kind, input, idempotencyKey) };
453
- }
454
- catch (replayError) {
455
- throw unknown(`${kind} authorization URL recovery failed. Retry the exact same action command and keys.`, replayError);
456
- }
457
- }
458
- export async function executeIntegrationBootstrapAction(api, runNo, kind, options, openBrowser, sleep, notify = (message) => process.stderr.write(message)) {
459
- if (kind !== "RECONCILE")
460
- requireConfirmation(options.yes, `Integration bootstrap ${kind.toLowerCase()}`);
461
- const input = {
462
- expectedRunVersion: expectedVersion(options.expectedRunVersion),
463
- clientRequestId: integrationBootstrapClientRequestId(options.clientRequestId)
464
- };
465
- const key = integrationBootstrapIdempotencyKey(options.idempotencyKey);
466
- const before = await api.getRun(runNo);
467
- const existing = isExactAction(before.currentAction, before.runNo, kind, input)
468
- && ["AWAITING_AUTHORIZATION", "AUTHORIZING", "RUNNING"].includes(before.currentAction.state)
469
- ? before.currentAction : null;
470
- if (existing == null && before.version !== input.expectedRunVersion) {
471
- throw new PipelineError("Run version changed before action start.", "INTEGRATION_BOOTSTRAP_RUN_VERSION_CONFLICT", 409);
472
- }
473
- const allowed = kind === "RECONCILE" ? before.reconcileAllowed
474
- : kind === "RECOVER" ? before.recoverAllowed : before.reapplyAllowed;
475
- if (existing == null && !allowed) {
476
- throw new PipelineError(`${kind} is not allowed by the current durable run evidence.`, "INTEGRATION_BOOTSTRAP_ACTION_NOT_ALLOWED", 409);
477
- }
478
- let result;
479
- if (existing != null && existing.state !== "AWAITING_AUTHORIZATION") {
480
- result = { action: existing };
481
- }
482
- else {
483
- try {
484
- result = { start: await api.startAction(before.runNo, kind, input, key) };
485
- }
486
- catch (error) {
487
- if (!startMayHaveCommitted(error))
488
- throw error;
489
- result = await resolveActionAfterStart(api, before.runNo, kind, input, key, error);
490
- }
491
- }
492
- if (result.action != null) {
493
- if (options.noOpen || ACTION_TERMINAL.has(result.action.state)) {
494
- return { action: result.action, run: await api.getRun(before.runNo) };
495
- }
496
- const action = await pollAction(api, result.action.actionNo, result.action.authorizationExpiresAt, sleep);
497
- return { action, run: await api.getRun(before.runNo) };
498
- }
499
- const started = result.start;
500
- if (started.runNo !== before.runNo || started.kind !== kind || started.clientRequestId !== input.clientRequestId) {
501
- throw identityMismatch("Integration bootstrap action start identity is inconsistent.");
502
- }
503
- if (options.noOpen)
504
- return { started, run: await api.getRun(before.runNo) };
505
- await openAuthorization(started.authorizationUrl, `action ${started.actionNo}`, openBrowser, notify);
506
- const action = await pollAction(api, started.actionNo, started.authorizationExpiresAt, sleep);
507
- return { started, action, run: await api.getRun(before.runNo) };
508
- }
509
- function addExamples(command, values, notes) {
510
- return command.addHelpText("after", `\n说明:\n${notes.map((value) => ` - ${value}`).join("\n")}\n\nExamples:\n${values.map((value) => ` ${value}`).join("\n")}\n`);
511
- }
512
- export function registerIntegrationBootstrapCommands(program, dependencies = {}) {
513
- const client = () => dependencies.clientFactory?.()
514
- ?? new IntegrationBootstrapClient(loadConfig("platform"));
515
- const privilegedClient = () => dependencies.privilegedClientFactory?.()
516
- ?? new AuthorizationClient(loadConfig("platform"));
517
- const openBrowser = dependencies.openBrowser ?? (async (url) => { await open(url); });
518
- const sleep = dependencies.sleep ?? ((milliseconds) => delay(milliseconds));
519
- const integration = program.command("integration").description("审阅并执行平台固定的外部系统集成 bootstrap");
520
- const bootstrap = integration.command("bootstrap")
521
- .description("通过一次临时管理员登录幂等初始化固定外部集成")
522
- .addHelpText("after", `
523
- 安全边界:
524
- 所有 realm、client、role、Jenkins instance、folder 与 credential metadata 均由服务端固定。
525
- CLI 不接收、读取或输出管理员 token、client secret、Jenkins password 或 Secret 响应。
526
- 每次命令都创建一次新鲜 PKCE 管理员授权并从实时状态重新检查。
527
- 已匹配步骤输出“无变更”;失败直接报告步骤和原因,不创建恢复点。
528
- bootstrap 不轮换 secret;轮换必须使用独立的 integration secret rotate 命令。
529
- `);
530
- const trustAnchor = bootstrap.command("trust-anchor")
531
- .description("导出一次性导入 master realm 的最小无密钥 PKCE 信任锚");
532
- addExamples(trustAnchor.command("export")
533
- .description("从当前 DevOps 公开端点下载、校验并原子写入 Keycloak client JSON")
534
- .requiredOption("-o, --output <file>", "本地 JSON 输出路径")
535
- .option("--force", "替换已存在的输出文件")
536
- .action(async (options, command) => {
537
- const result = await executeIntegrationBootstrapTrustAnchorExport(client(), options);
538
- await output(command, {
539
- ok: true,
540
- trustAnchor: {
541
- realm: result.realm,
542
- clientId: result.clientId,
543
- configurationDigest: result.configurationDigest,
544
- sourceUrl: result.sourceUrl,
545
- outputPath: result.outputPath
546
- }
547
- }, [
548
- `已导出 master realm 信任锚: ${result.outputPath}`,
549
- `client: ${result.clientId}`,
550
- `sha256: ${result.configurationDigest}`,
551
- "请将该 JSON 导入配置的 Keycloak master realm;导入后再运行 integration bootstrap init。"
552
- ].join("\n"));
553
- }), [
554
- "numa integration bootstrap trust-anchor export --output ./mci-devops-platform-bootstrap-master.json",
555
- "numa integration bootstrap trust-anchor export --output ./mci-devops-platform-bootstrap-master.json --force --json"
556
- ], [
557
- "该 GET 是公开、无会话操作;文件仅包含 public PKCE client,不包含 client secret、用户密码或 token。",
558
- "CLI 强制 master realm header、当前 DevOps callback、S256 PKCE 和禁用 Direct Access Grants。",
559
- "每个 DevOps/Keycloak 信任域只需导入一次;后续新 bootstrap profile 复用同一信任锚。"
560
- ]);
561
- const executePrivileged = async (command, request, options) => {
562
- const browser = normalizedBrowserOptions(options);
563
- const api = privilegedClient();
564
- const started = await api.startPrivilegedOperation(request);
565
- if (browser.noOpen || !started.loginUrl) {
566
- await output(command, { ok: true, privileged_session: started }, started.loginUrl
567
- ? `${started.runId}\t${started.mode}\t${started.status}\n请在浏览器打开: ${started.loginUrl}`
568
- : `${started.runId}\t${started.mode}\t${started.status}`);
569
- return;
570
- }
571
- process.stderr.write(`正在打开一次性 Keycloak 管理员授权(run ${started.runId})…\n`);
572
- try {
573
- await openBrowser(started.loginUrl);
574
- }
575
- catch {
576
- process.stderr.write(`无法自动打开浏览器,请手动打开:\n${started.loginUrl}\n`);
577
- }
578
- const run = await waitForKeycloakBootstrap({
579
- getKeycloakBootstrapRun: (runId) => api.getPrivilegedOperationRun(runId)
580
- }, started, sleep);
581
- const ok = run.status === "SUCCEEDED";
582
- await output(command, { ok, privileged_operation: run }, privilegedRunLine(run));
583
- if (!ok)
584
- process.exitCode = 1;
585
- };
586
- addExamples(bootstrap.command("init")
587
- .description("幂等创建或校正固定 Keycloak/Jenkins writer 配置;不轮换 secret")
588
- .option("--no-open", "不打开浏览器;返回一次性 login URL 和 run ID")
589
- .action(async (options, command) => {
590
- await executePrivileged(command, { mode: "INTEGRATION_INIT" }, options);
591
- }), [
592
- "numa integration bootstrap init",
593
- "numa integration bootstrap init --no-open --json"
594
- ], [
595
- "每次运行都重新检查实时状态;已完成步骤显示“无变更”。",
596
- "失败后直接重新运行本命令;不需要 operation-key、plan、recover 或 reapply。",
597
- "只读取当前 Keycloak secret 并同步到 Jenkins,不会轮换 secret。"
598
- ]);
599
- const secret = integration.command("secret")
600
- .description("显式管理固定集成 secret;不会由 bootstrap 隐式触发");
601
- addExamples(secret.command("rotate")
602
- .description("显式轮换 Keycloak writer secret,并同步覆盖 Jenkins credential")
603
- .option("--no-open", "不打开浏览器;返回一次性 login URL 和 run ID")
604
- .action(async (options, command) => {
605
- await executePrivileged(command, { mode: "SECRET_ROTATE" }, options);
606
- }), [
607
- "numa integration secret rotate",
608
- "numa integration secret rotate --no-open --json"
609
- ], [
610
- "必须完成一次新鲜管理员登录;secret 只在服务端一次性内存和 Jenkins 内部流转。",
611
- "轮换后始终重新读取 Keycloak 当前值、覆盖 Jenkins credential 并验证 token claims。",
612
- "若轮换结果不可确认,不要再次轮换;运行 integration bootstrap init 同步当前 secret。"
613
- ]);
614
- }
615
- //# sourceMappingURL=commands.js.map