@numa-tech/numa 1.12.7 → 1.12.10

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 (99) hide show
  1. package/README.md +266 -47
  2. package/dist/ai-registry/bridge.d.ts +8 -0
  3. package/dist/ai-registry/bridge.js +63 -0
  4. package/dist/ai-registry/bridge.js.map +1 -0
  5. package/dist/ai-registry/client.d.ts +29 -3
  6. package/dist/ai-registry/client.js +112 -8
  7. package/dist/ai-registry/client.js.map +1 -1
  8. package/dist/ai-registry/commands.js +367 -112
  9. package/dist/ai-registry/commands.js.map +1 -1
  10. package/dist/ai-registry/errors.d.ts +2 -1
  11. package/dist/ai-registry/errors.js +12 -1
  12. package/dist/ai-registry/errors.js.map +1 -1
  13. package/dist/ai-registry/mcp-installer.d.ts +1 -0
  14. package/dist/ai-registry/mcp-installer.js +4 -0
  15. package/dist/ai-registry/mcp-installer.js.map +1 -1
  16. package/dist/ai-registry/namespace.d.ts +17 -0
  17. package/dist/ai-registry/namespace.js +53 -0
  18. package/dist/ai-registry/namespace.js.map +1 -0
  19. package/dist/ai-registry/schemas.d.ts +127 -2
  20. package/dist/ai-registry/schemas.js +42 -2
  21. package/dist/ai-registry/schemas.js.map +1 -1
  22. package/dist/ai-registry/sync.d.ts +23 -0
  23. package/dist/ai-registry/sync.js +276 -0
  24. package/dist/ai-registry/sync.js.map +1 -0
  25. package/dist/ai-registry/tui.d.ts +2 -0
  26. package/dist/ai-registry/tui.js +140 -0
  27. package/dist/ai-registry/tui.js.map +1 -0
  28. package/dist/ai-registry/workspace.d.ts +191 -0
  29. package/dist/ai-registry/workspace.js +167 -0
  30. package/dist/ai-registry/workspace.js.map +1 -0
  31. package/dist/app-config.d.ts +3 -2
  32. package/dist/app-config.js +5 -3
  33. package/dist/app-config.js.map +1 -1
  34. package/dist/application-candidates/client.d.ts +167 -0
  35. package/dist/application-candidates/client.js +103 -0
  36. package/dist/application-candidates/client.js.map +1 -0
  37. package/dist/application-candidates/commands.d.ts +13 -0
  38. package/dist/application-candidates/commands.js +225 -0
  39. package/dist/application-candidates/commands.js.map +1 -0
  40. package/dist/application-candidates/schemas.d.ts +171 -0
  41. package/dist/application-candidates/schemas.js +80 -0
  42. package/dist/application-candidates/schemas.js.map +1 -0
  43. package/dist/application-onboarding/client.d.ts +256 -0
  44. package/dist/application-onboarding/client.js +213 -0
  45. package/dist/application-onboarding/client.js.map +1 -0
  46. package/dist/application-onboarding/commands.d.ts +64 -0
  47. package/dist/application-onboarding/commands.js +694 -0
  48. package/dist/application-onboarding/commands.js.map +1 -0
  49. package/dist/application-onboarding/errors.d.ts +11 -0
  50. package/dist/application-onboarding/errors.js +51 -0
  51. package/dist/application-onboarding/errors.js.map +1 -0
  52. package/dist/application-onboarding/schemas.d.ts +1167 -0
  53. package/dist/application-onboarding/schemas.js +459 -0
  54. package/dist/application-onboarding/schemas.js.map +1 -0
  55. package/dist/application-onboarding/scm-credentials.d.ts +8 -0
  56. package/dist/application-onboarding/scm-credentials.js +98 -0
  57. package/dist/application-onboarding/scm-credentials.js.map +1 -0
  58. package/dist/application-onboarding/tui-model.d.ts +131 -0
  59. package/dist/application-onboarding/tui-model.js +554 -0
  60. package/dist/application-onboarding/tui-model.js.map +1 -0
  61. package/dist/application-onboarding/tui.d.ts +14 -0
  62. package/dist/application-onboarding/tui.js +554 -0
  63. package/dist/application-onboarding/tui.js.map +1 -0
  64. package/dist/cli.js +43 -7
  65. package/dist/cli.js.map +1 -1
  66. package/dist/command-catalog.d.ts +1 -0
  67. package/dist/command-catalog.js +231 -10
  68. package/dist/command-catalog.js.map +1 -1
  69. package/dist/config-page.js +2 -0
  70. package/dist/config-page.js.map +1 -1
  71. package/dist/config.d.ts +1 -0
  72. package/dist/config.js +1 -0
  73. package/dist/config.js.map +1 -1
  74. package/dist/pipeline/client.d.ts +10 -6
  75. package/dist/pipeline/client.js +7 -4
  76. package/dist/pipeline/client.js.map +1 -1
  77. package/dist/pipeline/commands.d.ts +5 -1
  78. package/dist/pipeline/commands.js +89 -18
  79. package/dist/pipeline/commands.js.map +1 -1
  80. package/dist/pipeline/schemas.d.ts +7 -7
  81. package/dist/pipeline/schemas.js +2 -2
  82. package/dist/pipeline/schemas.js.map +1 -1
  83. package/dist/platform-profile.d.ts +8 -2
  84. package/dist/platform-profile.js +5 -2
  85. package/dist/platform-profile.js.map +1 -1
  86. package/dist/web-console-page.d.ts +3 -3
  87. package/dist/web-console-page.js +75 -20
  88. package/dist/web-console-page.js.map +1 -1
  89. package/dist/web-console.d.ts +1 -1
  90. package/dist/web-console.js +50 -3
  91. package/dist/web-console.js.map +1 -1
  92. package/package.json +8 -5
  93. package/skills/numa-ai-registry/SKILL.md +25 -19
  94. package/skills/numa-ai-registry/evals/evals.json +10 -10
  95. package/skills/numa-ai-registry/references/command-contract.md +41 -6
  96. package/skills/numa-cli/SKILL.md +44 -0
  97. package/skills/numa-cli/agents/openai.yaml +4 -0
  98. package/skills/numa-cli/evals/evals.json +35 -0
  99. package/skills/numa-cli/references/pipeline-release.md +107 -0
@@ -0,0 +1,694 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import process from "node:process";
3
+ import { loadAppConfig } from "../app-config.js";
4
+ import { loadConfig } from "../config.js";
5
+ import { registerApplicationCandidateCommands } from "../application-candidates/commands.js";
6
+ import { OnboardingClient } from "./client.js";
7
+ import { OnboardingError, sanitizeOnboardingOutput } from "./errors.js";
8
+ import { OnboardingModeSchema, OnboardingEventOutputSchema, OnboardingSessionStateSchema, OnboardingSpecSchema, ScmProviderSchema } from "./schemas.js";
9
+ import { readScmAdminConnectionInput, readScmCredentialBundle } from "./scm-credentials.js";
10
+ import { runOnboardingTui } from "./tui.js";
11
+ const TERMINAL_STATES = new Set(["COMPLETED", "FAILED", "CANCELLED"]);
12
+ const ACTIVE_STATES = new Set(["RUNNING", "WAITING_EXTERNAL", "VERIFYING"]);
13
+ const MAX_SPEC_BYTES = 262_144;
14
+ const STATE_SUMMARIES = {
15
+ DRAFT: "服务端草稿尚未完成",
16
+ READY: "草稿完整,等待服务端预检",
17
+ PLANNED: "服务端计划已锁定,等待 apply",
18
+ RUNNING: "服务端正在执行入驻步骤",
19
+ WAITING_EXTERNAL: "服务端正在等待外部系统或人工条件",
20
+ VERIFYING: "副作用步骤已完成,服务端正在验收",
21
+ COMPLETED: "服务端验收已完成",
22
+ FAILED: "服务端执行失败;请依据事件中的 errorCode 和 recoveryAction 恢复",
23
+ CANCELLED: "服务端已取消尚未开始的步骤;既有外部资源未回滚"
24
+ };
25
+ function addExamples(command, examples, notes = []) {
26
+ const noteSection = notes.length
27
+ ? `\n说明:\n${notes.map((item) => ` - ${item}`).join("\n")}\n`
28
+ : "";
29
+ return command.addHelpText("after", `${noteSection}\nExamples:\n${examples.map((item) => ` ${item}`).join("\n")}\n`);
30
+ }
31
+ function snakeCase(key) {
32
+ return key.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toLowerCase();
33
+ }
34
+ function stableOutput(value) {
35
+ const safe = sanitizeOnboardingOutput(value);
36
+ if (Array.isArray(safe))
37
+ return safe.map(stableOutput);
38
+ if (safe && typeof safe === "object") {
39
+ return Object.fromEntries(Object.entries(safe).map(([key, item]) => [snakeCase(key), stableOutput(item)]));
40
+ }
41
+ return safe;
42
+ }
43
+ export function publicOnboardingEvent(event) {
44
+ if (event.output == null)
45
+ return { ...event, output: event.output };
46
+ const parsed = OnboardingEventOutputSchema.safeParse(event.output);
47
+ return { ...event, output: parsed.success ? parsed.data : undefined };
48
+ }
49
+ export function onboardingExecutionView(session, events = []) {
50
+ const safeEvents = events
51
+ .map(publicOnboardingEvent)
52
+ .sort((left, right) => left.sequenceNo - right.sequenceNo);
53
+ const eventOutput = Object.assign({}, ...safeEvents.map((event) => event.output ?? {}));
54
+ const failed = [...safeEvents].reverse().find((event) => event.stepState === "FAILED" || event.errorCode != null);
55
+ return {
56
+ state: session.state,
57
+ currentStep: session.currentStep ?? null,
58
+ summary: STATE_SUMMARIES[session.state],
59
+ active: ACTIVE_STATES.has(session.state),
60
+ terminal: TERMINAL_STATES.has(session.state),
61
+ lastSequence: safeEvents.reduce((maximum, event) => Math.max(maximum, event.sequenceNo), 0),
62
+ retryable: failed?.retryable ?? false,
63
+ errorCode: failed?.errorCode ?? null,
64
+ eventOutput
65
+ };
66
+ }
67
+ function executionText(session, events = []) {
68
+ const view = onboardingExecutionView(session, events);
69
+ const lines = [sessionLine(session), `状态说明: ${view.summary}`];
70
+ if (view.errorCode)
71
+ lines.push(`error-code: ${view.errorCode}${view.retryable ? " (retryable)" : ""}`);
72
+ if (view.eventOutput.recoveryAction)
73
+ lines.push(`recovery-action: ${view.eventOutput.recoveryAction}`);
74
+ if (view.eventOutput.retryAfterSeconds != null)
75
+ lines.push(`retry-after: ${view.eventOutput.retryAfterSeconds}s`);
76
+ if (view.state === "COMPLETED") {
77
+ const resources = [
78
+ view.eventOutput.applicationId == null ? undefined : `application:${view.eventOutput.applicationId}`,
79
+ view.eventOutput.repositoryId == null ? undefined : `repository:${view.eventOutput.repositoryId}`,
80
+ view.eventOutput.repositoryFullName,
81
+ view.eventOutput.changeRequestUrl
82
+ ].filter((value) => value != null && value !== "");
83
+ if (resources.length)
84
+ lines.push(`结果: ${resources.join(" · ")}`);
85
+ }
86
+ return lines.join("\n");
87
+ }
88
+ async function jsonEnabled(command) {
89
+ const configured = await loadAppConfig();
90
+ return Boolean(command.optsWithGlobals().json || configured.output === "json");
91
+ }
92
+ async function output(command, payload, textValue) {
93
+ if (await jsonEnabled(command)) {
94
+ process.stdout.write(`${JSON.stringify(stableOutput(payload), null, 2)}\n`);
95
+ }
96
+ else {
97
+ process.stdout.write(`${String(sanitizeOnboardingOutput(textValue))}\n`);
98
+ }
99
+ }
100
+ export function parseOnboardingMode(value) {
101
+ const parsed = OnboardingModeSchema.safeParse(value.trim().toUpperCase());
102
+ if (!parsed.success) {
103
+ throw new OnboardingError("mode must be new or existing.", "onboarding_input_invalid");
104
+ }
105
+ return parsed.data;
106
+ }
107
+ export function parseApplicationCatalogOptionType(value) {
108
+ const normalized = value.trim().toLowerCase();
109
+ if (!["team", "system", "business-domain", "environment"].includes(normalized)) {
110
+ throw new OnboardingError("type must be team, system, business-domain, or environment.", "onboarding_input_invalid");
111
+ }
112
+ return normalized;
113
+ }
114
+ function catalogOptionLine(item) {
115
+ const id = item.id ?? "-";
116
+ const code = item.code ?? item.key ?? item.slug ?? "-";
117
+ const name = item.name ?? item.displayName ?? item.label ?? "-";
118
+ return `${String(id)}\t${String(code)}\t${String(name)}`;
119
+ }
120
+ function parseSessionState(value) {
121
+ if (value == null)
122
+ return undefined;
123
+ const parsed = OnboardingSessionStateSchema.safeParse(value.trim().toUpperCase());
124
+ if (!parsed.success) {
125
+ throw new OnboardingError("state is not a supported onboarding session state.", "onboarding_input_invalid");
126
+ }
127
+ return parsed.data;
128
+ }
129
+ function nonNegativeInteger(value, label, fallback) {
130
+ if (value == null)
131
+ return fallback;
132
+ const parsed = Number(value);
133
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
134
+ throw new OnboardingError(`${label} must be a non-negative integer.`, "onboarding_input_invalid");
135
+ }
136
+ return parsed;
137
+ }
138
+ function positiveInteger(value, label, fallback, maximum = Number.MAX_SAFE_INTEGER) {
139
+ if (value == null && fallback != null)
140
+ return fallback;
141
+ const parsed = Number(value);
142
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > maximum) {
143
+ throw new OnboardingError(`${label} must be an integer between 1 and ${maximum}.`, "onboarding_input_invalid");
144
+ }
145
+ return parsed;
146
+ }
147
+ function revision(value) {
148
+ if (value == null) {
149
+ throw new OnboardingError("version is required.", "onboarding_scm_input_invalid");
150
+ }
151
+ return nonNegativeInteger(value, "version", 0);
152
+ }
153
+ function parseScmProvider(value) {
154
+ if (!value)
155
+ return undefined;
156
+ const parsed = ScmProviderSchema.safeParse(value.trim().toUpperCase());
157
+ if (!parsed.success)
158
+ throw new OnboardingError("provider must be github or codeup.", "onboarding_scm_input_invalid");
159
+ return parsed.data;
160
+ }
161
+ function safeCursor(value) {
162
+ const cursor = value?.trim();
163
+ if (!cursor)
164
+ return undefined;
165
+ if (cursor.length > 4096 || /[\u0000-\u001f\u007f]/u.test(cursor)) {
166
+ throw new OnboardingError("cursor must be at most 4096 printable characters.", "onboarding_scm_input_invalid");
167
+ }
168
+ return cursor;
169
+ }
170
+ export async function resolveScmConnection(client, code) {
171
+ const normalized = code.trim();
172
+ if (!normalized)
173
+ throw new OnboardingError("SCM connection code is required.", "onboarding_scm_input_invalid");
174
+ const result = await client.listScmConnections({ search: normalized });
175
+ const matches = result.items.filter((connection) => connection.code === normalized);
176
+ if (matches.length !== 1) {
177
+ throw new OnboardingError(`SCM connection not found or ambiguous: ${normalized}`, "onboarding_scm_connection_not_found");
178
+ }
179
+ return matches[0];
180
+ }
181
+ export function scmCredentialUpdate(version, authType, credential) {
182
+ if (authType === "GITHUB_APP")
183
+ return { version, githubApp: credential };
184
+ if (authType === "GITHUB_PAT")
185
+ return { version, githubPat: credential };
186
+ return { version, codeup: credential };
187
+ }
188
+ function scmConnectionLine(connection) {
189
+ const enabled = Object.entries(connection.capabilities).filter(([, value]) => value).map(([key]) => key);
190
+ return `${connection.code}\t${connection.provider}\t${connection.health}\t${connection.organizationKey}\t${enabled.join(",") || "no-capabilities"}`;
191
+ }
192
+ function repositoryLine(repository) {
193
+ return `${repository.externalId}\t${repository.fullName ?? repository.name}\t${repository.defaultBranch}\t${repository.headRevision}${repository.archived ? "\tARCHIVED" : ""}`;
194
+ }
195
+ function requireConfirmation(value, operation) {
196
+ if (!value) {
197
+ throw new OnboardingError(`${operation} requires --yes.`, "onboarding_confirmation_required");
198
+ }
199
+ }
200
+ function idempotencyKey(value) {
201
+ const candidate = value?.trim();
202
+ if (!candidate) {
203
+ throw new OnboardingError("--apply requires --idempotency-key so retries can reuse the same submission identity.", "onboarding_idempotency_key_required");
204
+ }
205
+ if (candidate.length > 160 || /[\u0000-\u001f\u007f]/u.test(candidate)) {
206
+ throw new OnboardingError("idempotency-key must be at most 160 printable characters.", "onboarding_input_invalid");
207
+ }
208
+ return candidate;
209
+ }
210
+ export async function readOnboardingSpec(path) {
211
+ try {
212
+ const info = await stat(path);
213
+ if (!info.isFile() || info.size < 2 || info.size > MAX_SPEC_BYTES) {
214
+ throw new Error("spec must be a JSON file between 2 bytes and 256 KiB");
215
+ }
216
+ const raw = JSON.parse(await readFile(path, "utf8"));
217
+ const parsed = OnboardingSpecSchema.safeParse(raw);
218
+ if (!parsed.success)
219
+ throw new Error(parsed.error.message);
220
+ return parsed.data;
221
+ }
222
+ catch (error) {
223
+ if (error instanceof OnboardingError)
224
+ throw error;
225
+ throw new OnboardingError(`Cannot read onboarding specification ${path}: ${error instanceof Error ? error.message : String(error)}`, "onboarding_input_invalid");
226
+ }
227
+ }
228
+ export async function executeOnboardingSpec(client, spec, options = {}) {
229
+ if (options.plan && options.apply) {
230
+ throw new OnboardingError("Choose --plan or --apply, not both.", "onboarding_input_invalid");
231
+ }
232
+ if (options.mode && options.mode !== spec.mode) {
233
+ throw new OnboardingError("--mode must match the mode in --spec.", "onboarding_input_invalid");
234
+ }
235
+ if (!options.apply && options.idempotencyKey) {
236
+ throw new OnboardingError("--idempotency-key is only valid with --apply.", "onboarding_input_invalid");
237
+ }
238
+ const key = options.apply ? idempotencyKey(options.idempotencyKey) : undefined;
239
+ const draftSession = options.sessionNo
240
+ ? await client.getSession(options.sessionNo)
241
+ : await client.createSession(spec.mode);
242
+ if (draftSession.mode !== spec.mode) {
243
+ throw new OnboardingError("Existing session mode must match the mode in --spec.", "onboarding_input_invalid");
244
+ }
245
+ let session = await client.updateSession(draftSession.sessionNo, {
246
+ revision: draftSession.revision,
247
+ currentStep: "REVIEW",
248
+ draft: spec
249
+ });
250
+ if (!options.plan && !options.apply)
251
+ return { session };
252
+ const preflight = await client.preflight(session.sessionNo);
253
+ session = await client.getSession(session.sessionNo);
254
+ if (!options.apply)
255
+ return { session, preflight };
256
+ if (preflight.blockingErrors.length > 0) {
257
+ throw new OnboardingError(`Preflight has ${preflight.blockingErrors.length} blocking error(s); apply was not submitted.`, "onboarding_preflight_blocked", undefined, undefined, false, { sessionNo: session.sessionNo, blockingErrors: preflight.blockingErrors });
258
+ }
259
+ const planHash = preflight.planHash ?? session.planHash;
260
+ if (!planHash) {
261
+ throw new OnboardingError("The server did not return a plan hash; apply was not submitted.", "onboarding_plan_missing");
262
+ }
263
+ const task = await client.apply(session.sessionNo, { planHash, idempotencyKey: key });
264
+ session = await client.getSession(session.sessionNo);
265
+ return { session, preflight, task, idempotencyKey: key };
266
+ }
267
+ function defaultSleep(milliseconds, signal) {
268
+ return new Promise((resolve, reject) => {
269
+ if (signal?.aborted) {
270
+ reject(new OnboardingError("Onboarding watch was cancelled.", "onboarding_watch_cancelled"));
271
+ return;
272
+ }
273
+ const timeout = setTimeout(resolve, milliseconds);
274
+ signal?.addEventListener("abort", () => {
275
+ clearTimeout(timeout);
276
+ reject(new OnboardingError("Onboarding watch was cancelled.", "onboarding_watch_cancelled"));
277
+ }, { once: true });
278
+ });
279
+ }
280
+ export async function watchOnboardingEvents(client, sessionNo, options = {}) {
281
+ const sleep = options.sleep ?? defaultSleep;
282
+ let lastSequence = options.afterSequence ?? 0;
283
+ while (true) {
284
+ try {
285
+ const events = await client.getEvents(sessionNo, lastSequence);
286
+ for (const event of [...events.items].sort((left, right) => left.sequenceNo - right.sequenceNo)) {
287
+ if (event.sequenceNo <= lastSequence)
288
+ continue;
289
+ lastSequence = event.sequenceNo;
290
+ options.onEvent?.(event);
291
+ }
292
+ const session = await client.getSession(sessionNo);
293
+ options.onSession?.(session);
294
+ if (TERMINAL_STATES.has(session.state))
295
+ return { session, lastSequence };
296
+ }
297
+ catch (error) {
298
+ if (!(error instanceof OnboardingError) || (!error.retryable && (error.status == null || error.status < 500))) {
299
+ throw error;
300
+ }
301
+ options.onReconnect?.(error);
302
+ }
303
+ await sleep(2_000, options.signal);
304
+ }
305
+ }
306
+ export async function inspectOnboardingSession(client, sessionNo, afterSequence = 0) {
307
+ const [session, eventPage] = await Promise.all([
308
+ client.getSession(sessionNo),
309
+ client.getEvents(sessionNo, afterSequence)
310
+ ]);
311
+ const events = eventPage.items
312
+ .filter((event) => event.sequenceNo > afterSequence)
313
+ .map(publicOnboardingEvent)
314
+ .sort((left, right) => left.sequenceNo - right.sequenceNo);
315
+ const lastSequence = events.reduce((maximum, event) => Math.max(maximum, event.sequenceNo), afterSequence);
316
+ return {
317
+ session,
318
+ events,
319
+ lastSequence,
320
+ execution: { ...onboardingExecutionView(session, events), lastSequence }
321
+ };
322
+ }
323
+ export async function executeOnboardingAction(client, sessionNo, action) {
324
+ await client[action](sessionNo);
325
+ return inspectOnboardingSession(client, sessionNo);
326
+ }
327
+ async function withInterrupt(run) {
328
+ const controller = new AbortController();
329
+ const stop = () => controller.abort();
330
+ process.once("SIGINT", stop);
331
+ try {
332
+ return await run(controller.signal);
333
+ }
334
+ finally {
335
+ process.removeListener("SIGINT", stop);
336
+ }
337
+ }
338
+ function sessionLine(session) {
339
+ return `${session.sessionNo}\t${session.mode}\t${session.state}\t${session.currentStep ?? "-"}\trev:${session.revision}`;
340
+ }
341
+ function eventLine(event) {
342
+ const safe = publicOnboardingEvent(event);
343
+ const details = safe.output && Object.keys(safe.output).length
344
+ ? `\t${JSON.stringify(safe.output)}`
345
+ : "";
346
+ return `#${safe.sequenceNo}\t${safe.stepCode ?? safe.eventType}\t${safe.stepState ?? "-"}\t${safe.message}${details}`;
347
+ }
348
+ export function registerApplicationOnboardingCommands(program, dependencies = {}) {
349
+ const client = () => dependencies.clientFactory?.() ?? new OnboardingClient(loadConfig("platform"));
350
+ const isInteractive = dependencies.interactive ?? (() => Boolean(process.stdin.isTTY && process.stdout.isTTY));
351
+ const tui = dependencies.runTui ?? runOnboardingTui;
352
+ const sleep = dependencies.sleep ?? defaultSleep;
353
+ const app = program.command("app")
354
+ .description("创建新应用或把存量代码库接入 DevOps 平台")
355
+ .addHelpText("after", `
356
+ 核心约束:
357
+ 所有客户端共享服务端 sessionNo、revision、planHash 和事件 sequence。
358
+ --plan 只预检;--apply 才提交副作用,且服务端决定真实状态。
359
+ 结果不明时用相同 idempotency-key 重试或 inspect/resume,禁止新建重复会话。
360
+ `);
361
+ registerApplicationCandidateCommands(app);
362
+ addExamples(app.command("options <type>")
363
+ .description("按名称查询应用所需的团队、系统、业务域或环境 ID")
364
+ .option("--search <text>", "按 code 或名称搜索")
365
+ .option("--limit <number>", "返回条数 1-100", "50")
366
+ .action(async (type, options, command) => {
367
+ const result = await client().listCatalogOptions(parseApplicationCatalogOptionType(type), {
368
+ search: options.search?.trim() || undefined,
369
+ limit: positiveInteger(options.limit, "limit", 50, 100)
370
+ });
371
+ await output(command, { ok: true, options: result }, result.items.map(catalogOptionLine).join("\n") || "没有找到目录选项");
372
+ }), [
373
+ "numa app options team --search platform --json",
374
+ "numa app options system --json",
375
+ "numa app options environment --json"
376
+ ], [
377
+ "当前用户身份和 Owner 信息使用 numa profile 查询;应用归属以服务端 teamId 权限校验为准。"
378
+ ]);
379
+ const scm = app.command("scm")
380
+ .description("查询 SCM 连接和真实仓库;平台管理员可维护连接与轮换凭据")
381
+ .addHelpText("after", `
382
+ 安全边界:
383
+ 普通诊断只显示 code、provider、organization、health 和 capability。
384
+ live probe 与连接变更由服务端校验平台管理员权限和乐观锁 version。
385
+ provider credential 只能来自权限受限文件或重定向 stdin;禁止通过 argv、环境变量或普通 JSON 输出传递。
386
+ Codeup 云 AK/Role 只能引用已有 aliyunAccountId/aliyunAssumeRoleId,Numa 不读取或保存云 AK。
387
+ 未提供 Codeup accessToken 时使用服务端 Aliyun identity,不提交空 credential bundle。
388
+ `);
389
+ addExamples(scm.command("list")
390
+ .description("列出当前用户可见的安全 SCM 连接摘要")
391
+ .option("--provider <provider>", "github|codeup")
392
+ .option("--search <text>", "按 code、name 或 organization 搜索")
393
+ .action(async (options, command) => {
394
+ const connections = await client().listScmConnections({
395
+ provider: parseScmProvider(options.provider),
396
+ search: options.search?.trim() || undefined
397
+ });
398
+ await output(command, { ok: true, connections }, connections.items.map(scmConnectionLine).join("\n") || "没有找到可见的 SCM 连接");
399
+ }), ["numa app scm list --provider github --json"]);
400
+ addExamples(scm.command("inspect <code>")
401
+ .description("查看安全连接信息,可选查询一页真实仓库")
402
+ .option("--repositories", "同时查询仓库页;结果固定 externalId/defaultBranch/headRevision")
403
+ .option("--search <text>", "仓库搜索词;必须与 --repositories 一起使用")
404
+ .option("--cursor <cursor>", "仓库分页游标;必须与 --repositories 一起使用")
405
+ .option("--limit <number>", "每页仓库数量 1-100", "50")
406
+ .action(async (code, options, command) => {
407
+ if (!options.repositories && (options.search || options.cursor)) {
408
+ throw new OnboardingError("--search and --cursor require --repositories.", "onboarding_scm_input_invalid");
409
+ }
410
+ const onboarding = client();
411
+ const connection = await resolveScmConnection(onboarding, code);
412
+ const repositories = options.repositories
413
+ ? await onboarding.listRepositories(connection.code, {
414
+ search: options.search?.trim() || undefined,
415
+ cursor: safeCursor(options.cursor),
416
+ limit: positiveInteger(options.limit, "limit", 50, 100)
417
+ })
418
+ : undefined;
419
+ const text = repositories
420
+ ? `${scmConnectionLine(connection)}\n${repositories.items.map(repositoryLine).join("\n") || "没有匹配仓库"}${repositories.hasMore ? `\nnext-cursor: ${repositories.nextCursor}` : ""}`
421
+ : scmConnectionLine(connection);
422
+ await output(command, { ok: true, connection, ...(repositories ? { repositories } : {}) }, text);
423
+ }), [
424
+ "numa app scm inspect codeup-main --json",
425
+ "numa app scm inspect github-main --repositories --search order --limit 50 --json"
426
+ ], [
427
+ "仓库选择必须保存 externalId、defaultBranch 和 headRevision;向导会把 headRevision 写入 expectedHead。",
428
+ "HTTP 429 会保留 Retry-After 诊断,不会由客户端无限重试。"
429
+ ]);
430
+ addExamples(scm.command("probe <code>")
431
+ .description("显示缓存健康诊断;管理员可用 --live 发起真实 provider probe")
432
+ .option("--live", "调用管理员 live probe;服务端会重新探测 capability/health")
433
+ .option("--revision <number>", "live probe 的乐观锁 version")
434
+ .action(async (code, options, command) => {
435
+ const onboarding = client();
436
+ if (!options.live) {
437
+ if (options.revision != null)
438
+ throw new OnboardingError("--revision is only valid with --live.", "onboarding_scm_input_invalid");
439
+ const connection = await resolveScmConnection(onboarding, code);
440
+ await output(command, {
441
+ ok: true,
442
+ probe: {
443
+ mode: "CACHED",
444
+ code: connection.code,
445
+ provider: connection.provider,
446
+ health: connection.health,
447
+ capabilities: connection.capabilities,
448
+ lastCheckedAt: connection.lastCheckedAt ?? null
449
+ }
450
+ }, `${scmConnectionLine(connection)}\tchecked:${connection.lastCheckedAt ?? "never"}`);
451
+ return;
452
+ }
453
+ const version = revision(options.revision);
454
+ const before = await onboarding.getAdminScmConnection(code);
455
+ if (before.version !== version) {
456
+ throw new OnboardingError("SCM connection version changed; inspect and retry with the latest version.", "onboarding_scm_revision_conflict", 409);
457
+ }
458
+ const connection = await onboarding.probeAdminScmConnection(before.code, { version });
459
+ await output(command, { ok: true, probe: { mode: "LIVE", connection } }, `${connection.code}\t${connection.status}\tversion:${connection.version}`);
460
+ }), [
461
+ "numa app scm probe github-main --json",
462
+ "numa app scm probe github-main --live --revision 4 --json"
463
+ ]);
464
+ addExamples(scm.command("create")
465
+ .description("平台管理员创建 SCM 连接;GitHub 或可选 Codeup token 可在创建后安全写入")
466
+ .requiredOption("--input <file>", "无 secret 的连接 metadata JSON")
467
+ .option("--credential-file <file>", "仅 owner 可读(chmod 600)的 provider credential JSON")
468
+ .option("--credential-stdin", "从重定向 stdin 读取 provider credential JSON")
469
+ .action(async (options, command) => {
470
+ const onboarding = client();
471
+ const input = await readScmAdminConnectionInput(options.input);
472
+ const existing = await onboarding.listAdminScmConnections();
473
+ if (existing.items.some((item) => item.code === input.code)) {
474
+ throw new OnboardingError(`SCM connection already exists: ${input.code}`, "onboarding_scm_connection_exists", 409);
475
+ }
476
+ const credential = await readScmCredentialBundle(input.authType, options);
477
+ let connection = await onboarding.createAdminScmConnection(input);
478
+ if (credential) {
479
+ connection = await onboarding.rotateAdminScmCredential(connection.code, scmCredentialUpdate(connection.version, connection.authType, credential));
480
+ }
481
+ await output(command, { ok: true, connection }, `${connection.code}\t${connection.status}\tversion:${connection.version}\tcredential:${connection.credentialConfigured ? "configured" : "missing"}`);
482
+ }), [
483
+ "numa app scm create --input github.json --credential-file github-app.private.json --json",
484
+ "numa app scm create --input codeup.json --json"
485
+ ]);
486
+ addExamples(scm.command("update <code>")
487
+ .description("平台管理员按 version 更新连接 metadata;身份字段变更可能使已有 plan 失效")
488
+ .requiredOption("--input <file>", "无 secret 的完整连接 metadata JSON")
489
+ .requiredOption("--revision <number>", "当前乐观锁 version")
490
+ .option("--yes", "确认连接更新")
491
+ .action(async (code, options, command) => {
492
+ requireConfirmation(options.yes, "SCM connection update");
493
+ const onboarding = client();
494
+ const input = await readScmAdminConnectionInput(options.input);
495
+ if (input.code !== code)
496
+ throw new OnboardingError("Input code must match the target connection code.", "onboarding_scm_input_invalid");
497
+ const version = revision(options.revision);
498
+ const before = await onboarding.getAdminScmConnection(code);
499
+ if (before.version !== version)
500
+ throw new OnboardingError("SCM connection version changed; inspect and retry.", "onboarding_scm_revision_conflict", 409);
501
+ const connection = await onboarding.updateAdminScmConnection(code, { ...input, version });
502
+ await output(command, { ok: true, connection }, `${connection.code}\t${connection.status}\tversion:${connection.version}`);
503
+ }), ["numa app scm update github-main --input github.json --revision 4 --yes --json"]);
504
+ addExamples(scm.command("rotate <code>")
505
+ .description("平台管理员写入新 GitHub 或 Codeup access token;不会回显 credential")
506
+ .requiredOption("--revision <number>", "当前乐观锁 version")
507
+ .option("--credential-file <file>", "仅 owner 可读(chmod 600)的 provider credential JSON")
508
+ .option("--credential-stdin", "从重定向 stdin 读取 provider credential JSON")
509
+ .option("--yes", "确认凭据轮换并使旧 plan 失效")
510
+ .action(async (code, options, command) => {
511
+ requireConfirmation(options.yes, "SCM credential rotation");
512
+ if (!options.credentialFile && !options.credentialStdin) {
513
+ throw new OnboardingError("SCM credential rotation requires --credential-file or --credential-stdin.", "onboarding_scm_credential_source_invalid");
514
+ }
515
+ const onboarding = client();
516
+ const version = revision(options.revision);
517
+ const before = await onboarding.getAdminScmConnection(code);
518
+ if (before.version !== version)
519
+ throw new OnboardingError("SCM connection version changed; inspect and retry.", "onboarding_scm_revision_conflict", 409);
520
+ const credential = await readScmCredentialBundle(before.authType, options);
521
+ if (!credential)
522
+ throw new OnboardingError("SCM credential input is required.", "onboarding_scm_credential_source_invalid");
523
+ const connection = await onboarding.rotateAdminScmCredential(code, scmCredentialUpdate(version, before.authType, credential));
524
+ await output(command, { ok: true, connection }, `${connection.code}\tcredential:configured\tversion:${connection.version}`);
525
+ }), [
526
+ "numa app scm rotate github-main --revision 4 --credential-file github-app.private.json --yes --json",
527
+ "numa app scm rotate codeup-main --revision 2 --credential-stdin --yes --json < codeup-token.private.json"
528
+ ]);
529
+ addExamples(scm.command("disable <code>")
530
+ .description("平台管理员软禁用连接;服务端会阻止仍被非终态会话使用的连接")
531
+ .requiredOption("--revision <number>", "当前乐观锁 version")
532
+ .option("--yes", "确认软禁用")
533
+ .action(async (code, options, command) => {
534
+ requireConfirmation(options.yes, "SCM connection disable");
535
+ const onboarding = client();
536
+ const version = revision(options.revision);
537
+ const before = await onboarding.getAdminScmConnection(code);
538
+ if (before.version !== version)
539
+ throw new OnboardingError("SCM connection version changed; inspect and retry.", "onboarding_scm_revision_conflict", 409);
540
+ await onboarding.disableAdminScmConnection(code, version);
541
+ await output(command, { ok: true, code, disabled: true, previousVersion: version }, `${code}\tDISABLED`);
542
+ }), ["numa app scm disable github-main --revision 5 --yes --json"]);
543
+ addExamples(app.command("onboard")
544
+ .description("启动入驻向导,或按 JSON spec 创建、预检和执行")
545
+ .option("--mode <mode>", "new|existing;如有 --spec 必须与文件一致")
546
+ .option("--session <session-no>", "更新已有草稿并跨 Web/CLI 恢复;不创建新会话")
547
+ .option("--spec <file>", "schemaVersion=1 的 onboarding JSON 文件")
548
+ .option("--plan", "保存草稿并执行服务端预检,不产生外部副作用")
549
+ .option("--apply", "保存草稿、预检并提交服务端异步执行")
550
+ .option("--idempotency-key <key>", "apply 幂等键;结果不明时必须复用")
551
+ .action(async (options, command) => {
552
+ const mode = options.mode ? parseOnboardingMode(options.mode) : undefined;
553
+ if (!options.spec) {
554
+ if (!isInteractive()) {
555
+ throw new OnboardingError("Non-interactive onboarding requires --spec <file>.", "onboarding_input_required");
556
+ }
557
+ if (options.plan || options.apply || options.idempotencyKey) {
558
+ throw new OnboardingError("--plan, --apply, and --idempotency-key require --spec.", "onboarding_input_invalid");
559
+ }
560
+ await tui(client(), { mode, sessionNo: options.session });
561
+ return;
562
+ }
563
+ const onboarding = client();
564
+ const result = await executeOnboardingSpec(onboarding, await readOnboardingSpec(options.spec), {
565
+ mode,
566
+ sessionNo: options.session,
567
+ plan: options.plan,
568
+ apply: options.apply,
569
+ idempotencyKey: options.idempotencyKey
570
+ });
571
+ const execution = onboardingExecutionView(result.session);
572
+ const text = result.task
573
+ ? `入驻执行已提交:\n${executionText(result.session)}\nidempotency-key: ${result.idempotencyKey}`
574
+ : result.preflight
575
+ ? `预检完成: ${sessionLine(result.session)}\nplan: ${result.preflight.planHash ?? "-"}\nblocking: ${result.preflight.blockingErrors.length}`
576
+ : `草稿已保存: ${sessionLine(result.session)}`;
577
+ await output(command, { ok: true, ...result, execution }, text);
578
+ }), [
579
+ "numa app onboard",
580
+ "numa app onboard --session <session-no> --spec onboarding.json --plan --json",
581
+ "numa app onboard --spec onboarding.json --plan --json",
582
+ "numa app onboard --spec onboarding.json --apply --idempotency-key app-order-20260819 --json"
583
+ ], [
584
+ "非 TTY 必须提供 --spec;缺失时稳定返回 onboarding_input_required。",
585
+ "未指定 --plan/--apply 时只保存服务端草稿。",
586
+ "完整 spec 保存的 currentStep 为稳定字符串 REVIEW。"
587
+ ]);
588
+ addExamples(app.command("resume <session-no>")
589
+ .description("让服务端重新检查外部等待条件,不在客户端推进状态")
590
+ .action(async (sessionNo, _options, command) => {
591
+ const inspection = await executeOnboardingAction(client(), sessionNo, "resume");
592
+ await output(command, { ok: true, ...inspection }, executionText(inspection.session, inspection.events));
593
+ }), ["numa app resume 8d15ee89-43f0-4378-a7aa-51f09c5b42fc --json"]);
594
+ addExamples(app.command("apply <session-no>")
595
+ .description("按服务端已锁定 planHash 幂等提交已有 PLANNED 会话")
596
+ .requiredOption("--idempotency-key <key>", "结果不明时复用相同幂等键")
597
+ .action(async (sessionNo, options, command) => {
598
+ const onboarding = client();
599
+ const before = await onboarding.getSession(sessionNo);
600
+ if (!before.planHash) {
601
+ throw new OnboardingError("Session has no server plan hash; run onboarding preflight first.", "onboarding_plan_missing");
602
+ }
603
+ const key = idempotencyKey(options.idempotencyKey);
604
+ const task = await onboarding.apply(sessionNo, { planHash: before.planHash, idempotencyKey: key });
605
+ const session = await onboarding.getSession(sessionNo);
606
+ await output(command, { ok: true, session, task, idempotencyKey: key, execution: onboardingExecutionView(session) }, `入驻执行已提交:\n${executionText(session)}\nidempotency-key: ${key}`);
607
+ }), ["numa app apply 8d15ee89-43f0-4378-a7aa-51f09c5b42fc --idempotency-key app-order-20260819 --json"], [
608
+ "用于先 --plan 后 apply 的安全自动化,也用于响应不明时以同一 key 重放。"
609
+ ]);
610
+ addExamples(app.command("inspect <session-no>")
611
+ .description("查询会话;watch 从最后 event sequence 持续恢复")
612
+ .option("--watch", "持续补拉事件直到服务端终态;Ctrl+C 只停止本地观察")
613
+ .option("--after-sequence <number>", "从已持久化的最后 sequence 之后恢复", "0")
614
+ .action(async (sessionNo, options, command) => {
615
+ const onboarding = client();
616
+ const useJson = await jsonEnabled(command);
617
+ const afterSequence = nonNegativeInteger(options.afterSequence, "after-sequence", 0);
618
+ let session;
619
+ const observedEvents = [];
620
+ let lastSequence = afterSequence;
621
+ if (options.watch) {
622
+ const watched = await withInterrupt((signal) => watchOnboardingEvents(onboarding, sessionNo, {
623
+ afterSequence,
624
+ sleep,
625
+ signal,
626
+ onEvent: (event) => {
627
+ const safeEvent = publicOnboardingEvent(event);
628
+ lastSequence = Math.max(lastSequence, event.sequenceNo);
629
+ observedEvents.push(safeEvent);
630
+ if (!useJson)
631
+ process.stderr.write(`${String(sanitizeOnboardingOutput(eventLine(safeEvent)))}\n`);
632
+ },
633
+ onReconnect: (error) => { if (!useJson)
634
+ process.stderr.write(`连接中断,将从 #${lastSequence} 恢复: ${error.message}\n`); },
635
+ onSession: () => undefined
636
+ }));
637
+ session = watched.session;
638
+ lastSequence = watched.lastSequence;
639
+ }
640
+ else {
641
+ const inspection = await inspectOnboardingSession(onboarding, sessionNo, afterSequence);
642
+ session = inspection.session;
643
+ observedEvents.push(...inspection.events);
644
+ lastSequence = inspection.lastSequence;
645
+ }
646
+ const execution = { ...onboardingExecutionView(session, observedEvents), lastSequence };
647
+ await output(command, { ok: true, session, events: observedEvents, lastSequence, execution }, executionText(session, observedEvents));
648
+ }), [
649
+ "numa app inspect 8d15ee89-43f0-4378-a7aa-51f09c5b42fc --json",
650
+ "numa app inspect 8d15ee89-43f0-4378-a7aa-51f09c5b42fc --watch --after-sequence 42"
651
+ ]);
652
+ addExamples(app.command("list")
653
+ .description("列出当前用户可见的入驻会话")
654
+ .option("--state <state>", "按服务端会话状态过滤")
655
+ .option("--cursor <cursor>", "分页游标")
656
+ .option("--all", "请求当前用户有权查看的全部会话;默认仅本人")
657
+ .action(async (options, command) => {
658
+ const sessions = await client().listSessions({
659
+ state: parseSessionState(options.state),
660
+ cursor: options.cursor,
661
+ mine: !options.all
662
+ });
663
+ await output(command, { ok: true, sessions }, sessions.items.map(sessionLine).join("\n") || "没有找到入驻会话");
664
+ }), ["numa app list --state failed --json"]);
665
+ addExamples(app.command("tui")
666
+ .description("打开可恢复的 Application Onboarding TUI")
667
+ .option("--session <session-no>", "打开已有服务端会话")
668
+ .action(async (options) => tui(client(), { sessionNo: options.session })), [
669
+ "numa app tui",
670
+ "numa app tui --session 8d15ee89-43f0-4378-a7aa-51f09c5b42fc"
671
+ ]);
672
+ addExamples(app.command("retry <session-no>")
673
+ .description("让服务端重试可重入的失败步骤")
674
+ .action(async (sessionNo, _options, command) => {
675
+ const inspection = await executeOnboardingAction(client(), sessionNo, "retry");
676
+ await output(command, { ok: true, ...inspection }, executionText(inspection.session, inspection.events));
677
+ }), ["numa app retry 8d15ee89-43f0-4378-a7aa-51f09c5b42fc --json"]);
678
+ addExamples(app.command("reassess <session-no>")
679
+ .description("对存量仓库的新 head 请求服务端重新评估")
680
+ .action(async (sessionNo, _options, command) => {
681
+ const inspection = await executeOnboardingAction(client(), sessionNo, "reassess");
682
+ await output(command, { ok: true, ...inspection }, executionText(inspection.session, inspection.events));
683
+ }), ["numa app reassess 8d15ee89-43f0-4378-a7aa-51f09c5b42fc --json"]);
684
+ addExamples(app.command("cancel <session-no>")
685
+ .description("取消尚未开始的步骤;不会回滚已完成的外部副作用")
686
+ .option("--yes", "确认取消")
687
+ .action(async (sessionNo, options, command) => {
688
+ if (!options.yes)
689
+ throw new OnboardingError("Cancelling an onboarding session requires --yes.", "onboarding_confirmation_required");
690
+ const inspection = await executeOnboardingAction(client(), sessionNo, "cancel");
691
+ await output(command, { ok: true, ...inspection }, executionText(inspection.session, inspection.events));
692
+ }), ["numa app cancel 8d15ee89-43f0-4378-a7aa-51f09c5b42fc --yes --json"]);
693
+ }
694
+ //# sourceMappingURL=commands.js.map