@tokensrc/codex 1.14.21 → 1.14.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/commands.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { hostname } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { PACKAGE_VERSION } from "./branding.js";
4
5
  import { getStoragePath, loadAccounts, setStoragePath, setStoragePathDirect } from "codex-multi-auth/storage";
5
6
  import { normalizeCodexServiceUrl } from "./client.js";
6
7
  import { CodexServiceError } from "./errors.js";
@@ -19,7 +20,9 @@ import { clearCodexSelection, loadCodexSelection, saveCodexSelection } from "./s
19
20
  import { installCodexShortcuts, removeCodexShortcuts } from "./shortcuts.js";
20
21
  import { isCodexAccountAvailable, isCodexSubscriptionExpired, renderCodexAccountDashboard, selectCodexAccount } from "./tui.js";
21
22
  import { ensureCodexV6DeviceCredentials, reconcileCodexV6DeviceAccess, reportCodexV6DeviceCredentials, requireAvailableCodexV6DeviceAccess } from "./request-bound-device-v6-flow.js";
22
- import { resetCodexV6LocalState } from "./request-bound-device-v6-state.js";
23
+ import { getOrCreateCodexInstallationId } from "./multi-device-v5-state.js";
24
+ import { readCodexV6BindingState, resetCodexV6LocalState } from "./request-bound-device-v6-state.js";
25
+ import { probeCodexV6OnlineCredentialHealth, reportCodexV6OnlineCredentialHealth } from "./request-bound-device-v6-health.js";
23
26
  export const NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE = "当前用户没有绑定可用的 ChatGPT 账号。请前往 OA 系统提交 IT11 工单,申请独享或共享的 ChatGPT 个人账号。";
24
27
  const SCHEMA_V2_LEGACY_MANAGEMENT_DISABLED = "schema_v2_legacy_management_disabled";
25
28
  function findV3Assignment(input) {
@@ -36,6 +39,30 @@ async function json(command, deps) {
36
39
  function print(output, value) {
37
40
  output.write(`${JSON.stringify(value, null, 2)}\n`);
38
41
  }
42
+ function safeDiagnostic(deps, stage, details = {}) {
43
+ if (!deps.safeStartupDiagnostics)
44
+ return;
45
+ const safeDetails = Object.fromEntries(Object.entries(details).filter(([, value]) => value !== undefined));
46
+ deps.errorOutput.write(`[numa-debug] ${JSON.stringify({ stage, ...safeDetails })}\n`);
47
+ }
48
+ async function withSafeDiagnostic(deps, stage, operation) {
49
+ const startedAt = Date.now();
50
+ safeDiagnostic(deps, `${stage}.started`);
51
+ try {
52
+ const result = await operation();
53
+ safeDiagnostic(deps, `${stage}.completed`, { elapsedMs: Date.now() - startedAt });
54
+ return result;
55
+ }
56
+ catch (error) {
57
+ const diagnostic = error;
58
+ safeDiagnostic(deps, `${stage}.failed`, {
59
+ elapsedMs: Date.now() - startedAt,
60
+ errorCode: typeof diagnostic?.code === "string" ? diagnostic.code : "command_failed",
61
+ httpStatus: typeof diagnostic?.status === "number" ? diagnostic.status : undefined
62
+ });
63
+ throw error;
64
+ }
65
+ }
39
66
  function success(value) {
40
67
  return {
41
68
  schemaVersion: CODEX_DISTRIBUTION_SCHEMA_VERSION,
@@ -216,12 +243,113 @@ function formatV6UserBinding(binding) {
216
243
  + ` (${binding.installationId}) token-set=${binding.credentialLineageId ?? "WAITING"}`;
217
244
  }
218
245
  async function v6DeviceAccess(deps, options = {}, deviceLabel) {
219
- return reconcileCodexV6DeviceAccess({
246
+ return withSafeDiagnostic(deps, "v6.device_access.reconcile", () => reconcileCodexV6DeviceAccess({
220
247
  client: deps.clientFor(serviceConnection(options)),
221
248
  tenantId: deps.config.connection.tenantId,
222
249
  env: deps.env,
223
250
  deviceLabel
251
+ }));
252
+ }
253
+ async function loadV6DeviceCandidates(deps, options = {}, onlineHealth) {
254
+ const client = deps.clientFor(serviceConnection(options));
255
+ const installationId = await getOrCreateCodexInstallationId(deps.env);
256
+ const [requests, directory, localBinding] = await Promise.all([
257
+ client.listV6AccountRequests(),
258
+ client.listAccounts(),
259
+ readCodexV6BindingState(deps.env)
260
+ ]);
261
+ const bindingLists = await Promise.all(requests.items.map(async (request) => ({
262
+ request,
263
+ bindings: (await client.listV6DeviceBindings(request.id)).items
264
+ })));
265
+ const allBindings = bindingLists.flatMap(({ bindings }) => bindings);
266
+ const replaced = new Set(allBindings
267
+ .filter((binding) => binding.predecessorBindingId !== null
268
+ && ["READY", "ACTIVE"].includes(binding.status))
269
+ .map((binding) => binding.predecessorBindingId));
270
+ const now = Date.now();
271
+ const candidates = [];
272
+ for (const { request, bindings } of bindingLists) {
273
+ for (const binding of bindings) {
274
+ if (binding.installationId !== installationId
275
+ || replaced.has(binding.id)
276
+ || !["READY", "ACTIVE", "REPLACEMENT_PENDING", "NEEDS_REAUTH"].includes(binding.status)
277
+ || binding.managedAccountId === null || binding.providerAccountId === null
278
+ || binding.credentialLineageId === null)
279
+ continue;
280
+ const directoryAccount = directory.accounts.find((account) => account.id === binding.managedAccountId || account.accountId === binding.providerAccountId);
281
+ const credentialExpired = binding.credentialExpiresAt === null
282
+ || !Number.isFinite(Date.parse(binding.credentialExpiresAt))
283
+ || Date.parse(binding.credentialExpiresAt) <= now + 60_000;
284
+ const serverRejected = binding.status === "NEEDS_REAUTH"
285
+ || binding.accessTokenHealth === "REJECTED"
286
+ || binding.refreshTokenHealth === "REJECTED";
287
+ const currentRejected = localBinding?.bindingId === binding.id
288
+ && onlineHealth !== undefined
289
+ && ["TOKEN_REVOKED", "AUTH_REJECTED", "CONNECTOR_TOKEN_REVOKED",
290
+ "CONNECTOR_AUTH_REJECTED"].includes(onlineHealth.status);
291
+ const requestUsable = request.status === "APPROVED"
292
+ && ["AVAILABLE", "PARTIALLY_AVAILABLE"].includes(request.availability)
293
+ && request.validUntil !== null && Date.parse(request.validUntil) > now;
294
+ const available = requestUsable
295
+ && !credentialExpired && !serverRejected && !currentRejected;
296
+ candidates.push({
297
+ request,
298
+ binding,
299
+ account: {
300
+ ...(directoryAccount ?? {
301
+ id: binding.managedAccountId,
302
+ accountId: binding.providerAccountId,
303
+ label: binding.providerAccountId,
304
+ isDefault: false
305
+ }),
306
+ // A binding is the selectable identity. The same managed account may
307
+ // legitimately appear under more than one active Request.
308
+ id: binding.id,
309
+ accountId: binding.providerAccountId,
310
+ isDefault: localBinding?.bindingId === binding.id,
311
+ credentialStatus: available ? "AVAILABLE"
312
+ : credentialExpired || serverRejected || currentRejected
313
+ ? "NEEDS_REAUTH" : "TEMPORARILY_UNAVAILABLE",
314
+ lineageStatus: available ? "ACTIVE" : "NEEDS_REAUTH"
315
+ }
316
+ });
317
+ }
318
+ }
319
+ return {
320
+ installationId,
321
+ candidates,
322
+ ...(localBinding ? { currentBindingId: localBinding.bindingId } : {})
323
+ };
324
+ }
325
+ async function selectV6DeviceCandidate(deps, requested, options = {}) {
326
+ const inventory = await loadV6DeviceCandidates(deps, options);
327
+ let resolvedRequest = requested;
328
+ if (requested) {
329
+ const matches = inventory.candidates.filter(({ request, binding, account }) => [
330
+ request.id,
331
+ binding.id,
332
+ binding.managedAccountId,
333
+ binding.providerAccountId,
334
+ binding.credentialLineageId,
335
+ account.label
336
+ ].includes(requested));
337
+ if (matches.length > 1)
338
+ throw new CodexServiceError(`账号选择 ${requested} 不唯一,请使用 Request ID 或 Binding ID。`, "codex_account_reference_ambiguous", 409);
339
+ if (matches.length === 1)
340
+ resolvedRequest = matches[0].binding.id;
341
+ }
342
+ const chosen = await selectCodexAccount(inventory.candidates.map(({ account }) => account), {
343
+ requested: resolvedRequest,
344
+ selectedAccountId: inventory.currentBindingId,
345
+ nonInteractiveCommand: "numa codex use",
346
+ noAvailableMessage: "当前设备没有可领取或可用的独立 Token Set。",
347
+ output: deps.errorOutput
224
348
  });
349
+ const candidate = inventory.candidates.find(({ binding }) => binding.id === chosen.id);
350
+ if (!candidate)
351
+ throw new CodexServiceError("选择的 Request/设备 Binding 已不在候选列表中。", "codex_device_binding_not_found", 404);
352
+ return candidate;
225
353
  }
226
354
  async function loadAccountData(deps, options = {}, allowV4Replacement = false) {
227
355
  const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
@@ -409,32 +537,78 @@ async function installCredentials(deps, selection, service, allowV4Replacement =
409
537
  async function selectedCredentials(deps, requested, options = {}) {
410
538
  if (requestBoundDeviceCredentialsEnabled(deps.config)) {
411
539
  const client = deps.clientFor(serviceConnection(options));
412
- const ensured = await ensureCodexV6DeviceCredentials({
540
+ const candidate = requested
541
+ ? await selectV6DeviceCandidate(deps, requested, options)
542
+ : undefined;
543
+ let ensured = await withSafeDiagnostic(deps, "v6.device_credentials.ensure", () => ensureCodexV6DeviceCredentials({
413
544
  client,
414
545
  tenantId: deps.config.connection.tenantId,
415
- env: deps.env
416
- });
546
+ env: deps.env,
547
+ ...(candidate ? { target: { request: candidate.request, binding: candidate.binding } } : {})
548
+ }));
417
549
  if (ensured.replacementDeferredWarningCode) {
418
550
  deps.errorOutput.write(`新设备 Token Set 暂时无法交付 (${ensured.replacementDeferredWarningCode});`
419
551
  + "本次继续使用尚有效的旧 Token Set,稍后会自动重试。\n");
420
552
  }
421
- const binding = ensured.binding;
422
- if (requested && ![
423
- binding.id,
424
- binding.requestId,
425
- binding.managedAccountId,
426
- binding.providerAccountId,
427
- binding.credentialLineageId
428
- ].includes(requested)) {
429
- throw new CodexServiceError("当前设备只能使用管理员为它绑定的独立 Token Set。", "codex_device_binding_not_found", 404);
553
+ let binding = ensured.binding;
554
+ let health = await withSafeDiagnostic(deps, "v6.online_health.probe", () => probeCodexV6OnlineCredentialHealth({
555
+ env: deps.env,
556
+ fetch: deps.fetch
557
+ }));
558
+ const healthRejected = () => ["TOKEN_REVOKED", "AUTH_REJECTED", "CONNECTOR_TOKEN_REVOKED",
559
+ "CONNECTOR_AUTH_REJECTED"].includes(health.status);
560
+ let report;
561
+ if (healthRejected()) {
562
+ report = await reportCodexV6OnlineCredentialHealth({ client, health, env: deps.env });
563
+ if (report.replacementBinding) {
564
+ const replacementBinding = report.replacementBinding;
565
+ const request = (await client.getV6AccountRequest(replacementBinding.requestId)).request;
566
+ ensured = await withSafeDiagnostic(deps, "v6.device_credentials.replace", () => ensureCodexV6DeviceCredentials({
567
+ client,
568
+ tenantId: deps.config.connection.tenantId,
569
+ env: deps.env,
570
+ target: { request, binding: replacementBinding }
571
+ }));
572
+ binding = ensured.binding;
573
+ health = await withSafeDiagnostic(deps, "v6.online_health.replacement_probe", () => probeCodexV6OnlineCredentialHealth({ env: deps.env, fetch: deps.fetch }));
574
+ if (!healthRejected()) {
575
+ deps.errorOutput.write(health.status === "HEALTHY"
576
+ ? "已自动安装并验证服务端换发的新设备 Token Set。\n"
577
+ : "已自动安装服务端换发的新设备 Token Set;在线探知结果暂不确定。\n");
578
+ }
579
+ else {
580
+ report = await reportCodexV6OnlineCredentialHealth({ client, health, env: deps.env });
581
+ }
582
+ }
583
+ }
584
+ if (healthRejected()) {
585
+ const connectorOnly = health.status.startsWith("CONNECTOR_");
586
+ throw new CodexServiceError(`当前设备 Token Set 的${connectorOnly ? "关键 ChatGPT/connector 授权" : "主 Codex 授权"}`
587
+ + `已被上游拒绝${health.safeErrorCode ? ` (${health.safeErrorCode})` : ""};`
588
+ + (report?.reported
589
+ ? "失效状态已安全上报,系统将进入换发流程。"
590
+ : `状态上报未完成${report?.warningCode ? ` (${report.warningCode}`
591
+ + `${report.warningStatus ? `/HTTP-${report.warningStatus}` : ""})` : ""};`)
592
+ + "服务器上的“已交付”不代表实时可用,请由管理员换发 Token Set。", ["TOKEN_REVOKED", "CONNECTOR_TOKEN_REVOKED"].includes(health.status)
593
+ ? "codex_device_token_revoked" : "codex_device_credential_rejected", (connectorOnly ? health.connectorHttpStatus : health.codexApiHttpStatus) ?? 401);
430
594
  }
595
+ if (["FORBIDDEN", "UNREACHABLE"].includes(health.status)) {
596
+ deps.errorOutput.write(`Codex 在线凭据探知暂不可用 (${health.status.toLowerCase()}`
597
+ + `${health.codexApiHttpStatus ? `, HTTP ${health.codexApiHttpStatus}` : ""});本次仅依据本地与服务端证据继续。\n`);
598
+ }
599
+ const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
600
+ const directoryAccount = candidate?.binding.id === binding.id ? candidate.account
601
+ : (await client.listAccounts()).accounts.find((account) => account.id === binding.managedAccountId || account.accountId === binding.providerAccountId);
602
+ const label = directoryAccount?.label
603
+ ?? (previous?.account.bindingId === binding.id ? previous.account.label : undefined)
604
+ ?? binding.providerAccountId;
431
605
  const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
432
606
  serviceUrl: options.server ?? deps.config.connection.serverUrl,
433
607
  allowInsecurePoc: Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp),
434
608
  account: {
435
609
  id: binding.managedAccountId,
436
610
  accountId: binding.providerAccountId,
437
- label: `${binding.deviceLabel} / ${binding.requestId}`,
611
+ label,
438
612
  distributionMode: "REQUEST_BOUND_DEVICE_V6",
439
613
  managedAccountId: binding.managedAccountId,
440
614
  providerAccountId: binding.providerAccountId,
@@ -598,7 +772,11 @@ async function launchWithSelectedCredentials(deps, selected, launch) {
598
772
  env: deps.env
599
773
  });
600
774
  if (!report.reported && report.warningCode) {
601
- deps.errorOutput.write(`Codex 独立设备 Token 代际上报暂缓 (${report.warningCode});其他设备不会受影响。\n`);
775
+ deps.errorOutput.write(`Codex 独立设备 Token 代际上报失败:stage=${report.warningStage ?? "UNKNOWN"}`
776
+ + `${report.warningStatus ? `, HTTP ${report.warningStatus}` : ""}`
777
+ + `, code=${report.warningCode}。`
778
+ + `${report.pendingCredentialChange ? "已检测到本地代际变化,状态与重试证据均已保留;" : ""}`
779
+ + "其他设备不会受影响。\n");
602
780
  }
603
781
  }
604
782
  }
@@ -653,6 +831,27 @@ export function registerCodexCommands(program, deps, options = {}) {
653
831
  ? program
654
832
  : program.command("codex").description("Select an account and launch the official Codex client");
655
833
  const includeAuthCommands = options.includeAuthCommands ?? options.nested === false;
834
+ codex.hook("preAction", (_command, actionCommand) => {
835
+ let serviceOrigin = deps.config.connection.serverUrl;
836
+ try {
837
+ serviceOrigin = new URL(serviceOrigin).origin;
838
+ }
839
+ catch {
840
+ serviceOrigin = "invalid";
841
+ }
842
+ safeDiagnostic(deps, "startup", {
843
+ cliVersion: PACKAGE_VERSION,
844
+ command: actionCommand.name(),
845
+ credentialModel: requestBoundDeviceCredentialsEnabled(deps.config)
846
+ ? "REQUEST_BOUND_DEVICE_V6"
847
+ : "LEGACY",
848
+ profileId: deps.config.profile?.profileId,
849
+ profileRevision: deps.config.profile?.revision,
850
+ environment: deps.config.profile?.environment,
851
+ profileStale: deps.config.profile?.stale,
852
+ serviceOrigin
853
+ });
854
+ });
656
855
  if (includeAuthCommands)
657
856
  codex.command("login").description("Log in with OIDC").action(async (_options, command) => {
658
857
  if (!deps.auth.login)
@@ -776,31 +975,45 @@ export function registerCodexCommands(program, deps, options = {}) {
776
975
  serviceOptions(codex.command("list").description("List available Codex accounts without downloading credentials"))
777
976
  .action(async (opts, command) => {
778
977
  if (requestBoundDeviceCredentialsEnabled(deps.config)) {
779
- const client = deps.clientFor(serviceConnection(opts));
780
978
  const access = await v6DeviceAccess(deps, opts);
979
+ const localHealth = await probeCodexV6OnlineCredentialHealth({
980
+ env: deps.env,
981
+ fetch: deps.fetch
982
+ });
983
+ const rejected = ["TOKEN_REVOKED", "AUTH_REJECTED", "CONNECTOR_TOKEN_REVOKED",
984
+ "CONNECTOR_AUTH_REJECTED"].includes(localHealth.status);
985
+ const healthReport = rejected
986
+ ? await reportCodexV6OnlineCredentialHealth({
987
+ client: deps.clientFor(serviceConnection(opts)),
988
+ health: localHealth,
989
+ env: deps.env
990
+ })
991
+ : undefined;
992
+ const inventory = await loadV6DeviceCandidates(deps, opts, localHealth);
781
993
  if (await json(command, deps)) {
782
994
  print(deps.output, success({
783
995
  credentialModel: "REQUEST_BOUND_DEVICE_V6",
784
996
  installationId: access.installationId,
785
- ...access.response
997
+ ...access.response,
998
+ candidates: inventory.candidates.map(({ request, binding, account }) => ({
999
+ request,
1000
+ binding,
1001
+ account
1002
+ })),
1003
+ localCredentialHealth: localHealth,
1004
+ ...(healthReport ? { localCredentialHealthReport: healthReport } : {})
786
1005
  }));
787
1006
  return;
788
1007
  }
789
- if (access.response.accessStatus !== "AVAILABLE") {
1008
+ if (inventory.candidates.length === 0 && access.response.accessStatus !== "AVAILABLE") {
790
1009
  requireAvailableCodexV6DeviceAccess(access);
791
1010
  }
792
- const { binding } = requireAvailableCodexV6DeviceAccess(access);
793
- const accountResult = await client.listAccounts();
794
- const currentAccount = accountResult.accounts.find((account) => account.id === binding.managedAccountId || account.accountId === binding.providerAccountId);
795
- if (!currentAccount)
796
- throw new CodexServiceError("The request-bound ChatGPT account is absent from the user account directory.", "invalid_codex_service_response");
797
- const availableAccount = {
798
- ...currentAccount,
799
- credentialStatus: "AVAILABLE",
800
- lineageStatus: "ACTIVE"
801
- };
802
- deps.output.write(renderCodexAccountDashboard([availableAccount], availableAccount.id));
803
- deps.output.write(`设备凭据 ${binding.status === "ACTIVE" || binding.status === "READY" ? "可用" : binding.status}\n`);
1011
+ deps.output.write(renderCodexAccountDashboard(inventory.candidates.map(({ account }) => account), inventory.currentBindingId ?? access.response.binding?.id ?? undefined));
1012
+ deps.output.write(`设备凭据 ${rejected ? "已失效,需换发" : "可用"}\n`);
1013
+ if (healthReport?.reported)
1014
+ deps.output.write(healthReport.replacementPending
1015
+ ? "失效状态已上报;同账号 Token Set 暂无库存,正在等待换发。\n"
1016
+ : "失效状态已上报;新的设备 Token Set 将在使用时自动换发。\n");
804
1017
  if (access.response.migration === "AUTO_MIGRATED") {
805
1018
  deps.output.write("既有本机凭据已自动转换为该申请下的第一个独立设备绑定。\n");
806
1019
  }
@@ -831,7 +1044,8 @@ export function registerCodexCommands(program, deps, options = {}) {
831
1044
  if (requestBoundDeviceCredentialsEnabled(deps.config)) {
832
1045
  if (opts.takeover)
833
1046
  throw new CodexServiceError("v6 每台设备使用独立 Token Set,不支持接管其他设备的凭据。", "codex_device_takeover_not_allowed", 409);
834
- const selected = await selectedCredentials(deps, account, opts);
1047
+ const target = account ?? (await selectV6DeviceCandidate(deps, undefined, opts)).binding.id;
1048
+ const selected = await selectedCredentials(deps, target, opts);
835
1049
  if (await json(command, deps))
836
1050
  print(deps.output, success({
837
1051
  account: selected.selection.account,
@@ -862,7 +1076,10 @@ export function registerCodexCommands(program, deps, options = {}) {
862
1076
  .action(async (args, opts) => {
863
1077
  if (opts.selectAccount)
864
1078
  await clearCodexSelection(deps.config.persistence.configDirectory);
865
- const selected = await selectedCredentials(deps, opts.account, opts);
1079
+ const target = requestBoundDeviceCredentialsEnabled(deps.config) && opts.selectAccount
1080
+ ? (await selectV6DeviceCandidate(deps, opts.account, opts)).binding.id
1081
+ : opts.account;
1082
+ const selected = await selectedCredentials(deps, target, opts);
866
1083
  deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
867
1084
  process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.run(args, { signal }));
868
1085
  });
@@ -874,7 +1091,10 @@ export function registerCodexCommands(program, deps, options = {}) {
874
1091
  throw new Error("官方 Codex 桌面 App 仅支持 macOS 和 Windows。");
875
1092
  if (opts.selectAccount)
876
1093
  await clearCodexSelection(deps.config.persistence.configDirectory);
877
- const selected = await selectedCredentials(deps, opts.account, opts);
1094
+ const target = requestBoundDeviceCredentialsEnabled(deps.config) && opts.selectAccount
1095
+ ? (await selectV6DeviceCandidate(deps, opts.account, opts)).binding.id
1096
+ : opts.account;
1097
+ const selected = await selectedCredentials(deps, target, opts);
878
1098
  deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
879
1099
  process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.app(workspace, { signal }));
880
1100
  });
@@ -976,13 +1196,18 @@ export function registerCodexCommands(program, deps, options = {}) {
976
1196
  ? "device_token_set.generation_reported"
977
1197
  : "device_token_set.current",
978
1198
  at: new Date().toISOString(),
979
- ...(result.warningCode ? { warningCode: result.warningCode } : {})
1199
+ ...(result.warningCode ? { warningCode: result.warningCode } : {}),
1200
+ ...(result.warningStage ? { warningStage: result.warningStage } : {}),
1201
+ ...(result.warningStatus ? { warningStatus: result.warningStatus } : {}),
1202
+ ...(result.pendingCredentialChange ? { pendingCredentialChange: true } : {})
980
1203
  };
981
1204
  if (await json(command, deps))
982
1205
  print(deps.output, success(event));
983
1206
  else
984
1207
  deps.output.write(`${event.at} ${event.level} ${event.event}`
985
- + `${result.warningCode ? ` (${result.warningCode})` : ""}; other devices unaffected\n`);
1208
+ + `${result.warningCode ? ` (${result.warningStage ?? "UNKNOWN"}`
1209
+ + `${result.warningStatus ? `/HTTP-${result.warningStatus}` : ""}`
1210
+ + `/${result.warningCode})` : ""}; other devices unaffected\n`);
986
1211
  if (opts.once)
987
1212
  break;
988
1213
  await new Promise((resolve) => setTimeout(resolve, interval));