@ait-co/console-cli 0.1.24 → 0.1.26

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/cli.mjs CHANGED
@@ -13,6 +13,10 @@ import { promisify } from "node:util";
13
13
  import { createHash } from "node:crypto";
14
14
  //#region src/api/http.ts
15
15
  var TossApiError = class extends Error {
16
+ status;
17
+ errorCode;
18
+ reason;
19
+ errorType;
16
20
  constructor(status, errorCode, reason, errorType) {
17
21
  super(`Toss API error ${errorCode}: ${reason} (HTTP ${status})`);
18
22
  this.status = status;
@@ -27,6 +31,7 @@ var TossApiError = class extends Error {
27
31
  }
28
32
  };
29
33
  var NetworkError = class extends Error {
34
+ url;
30
35
  constructor(url, cause) {
31
36
  super(`Network request to ${url} failed: ${cause.message}`);
32
37
  this.url = url;
@@ -35,6 +40,9 @@ var NetworkError = class extends Error {
35
40
  }
36
41
  };
37
42
  var MalformedResponseError = class extends Error {
43
+ url;
44
+ status;
45
+ bodyPreview;
38
46
  constructor(url, status, message, bodyPreview) {
39
47
  const suffix = bodyPreview ? ` (body: ${bodyPreview})` : "";
40
48
  super(`Malformed response from ${url} (HTTP ${status}): ${message}${suffix}`);
@@ -2593,7 +2601,7 @@ async function runAppInit(args) {
2593
2601
  workspaceId = await pickWorkspace(session.cookies);
2594
2602
  categoryIds = await pickCategories(session.cookies);
2595
2603
  } catch (err) {
2596
- if (isPromptCancelled$1(err)) {
2604
+ if (isPromptCancelled$2(err)) {
2597
2605
  process.stderr.write("Aborted.\n");
2598
2606
  return exitAfterFlush(ExitCode.Usage);
2599
2607
  }
@@ -2636,7 +2644,7 @@ async function runAppInit(args) {
2636
2644
  categoryIds
2637
2645
  };
2638
2646
  } catch (err) {
2639
- if (isPromptCancelled$1(err)) {
2647
+ if (isPromptCancelled$2(err)) {
2640
2648
  process.stderr.write("Aborted.\n");
2641
2649
  return exitAfterFlush(ExitCode.Usage);
2642
2650
  }
@@ -2695,7 +2703,7 @@ async function fileExists(path) {
2695
2703
  return false;
2696
2704
  }
2697
2705
  }
2698
- function isPromptCancelled$1(err) {
2706
+ function isPromptCancelled$2(err) {
2699
2707
  return err instanceof Error && err.name === "ExitPromptError";
2700
2708
  }
2701
2709
  async function pickWorkspace(cookies) {
@@ -5810,6 +5818,8 @@ const appCommand = defineCommand({
5810
5818
  //#region src/auth/backend.ts
5811
5819
  const CREDENTIAL_SERVICE = "aitcc.credentials";
5812
5820
  var CredentialBackendUnsupportedError = class extends Error {
5821
+ platform;
5822
+ hint;
5813
5823
  constructor(platform, hint) {
5814
5824
  super(`No supported credential backend for platform "${platform}". ${hint}`);
5815
5825
  this.platform = platform;
@@ -5818,6 +5828,9 @@ var CredentialBackendUnsupportedError = class extends Error {
5818
5828
  }
5819
5829
  };
5820
5830
  var CredentialBackendCommandError = class extends Error {
5831
+ command;
5832
+ exitCode;
5833
+ redactedStderr;
5821
5834
  constructor(command, exitCode, redactedStderr) {
5822
5835
  super(`Credential backend command "${command}" failed (exit=${exitCode ?? "null"}): ${redactedStderr}`);
5823
5836
  this.command = command;
@@ -5856,7 +5869,7 @@ async function runCommand(command, opts) {
5856
5869
  function isCommandNotFound(err) {
5857
5870
  return err.code === "ENOENT";
5858
5871
  }
5859
- function stripTrailingNewline(s) {
5872
+ function stripTrailingNewline$1(s) {
5860
5873
  return s.replace(/\r?\n$/, "");
5861
5874
  }
5862
5875
  function redactStderr(stderr) {
@@ -5890,7 +5903,7 @@ const LINUX_BACKEND = {
5890
5903
  throw new CredentialBackendCommandError("secret-tool lookup", result.exitCode, redactStderr(result.stderr));
5891
5904
  }
5892
5905
  if (result.stdout.length === 0) return null;
5893
- const password = stripTrailingNewline(result.stdout);
5906
+ const password = stripTrailingNewline$1(result.stdout);
5894
5907
  return password.length > 0 ? password : null;
5895
5908
  },
5896
5909
  async set(account, password) {
@@ -5954,7 +5967,7 @@ const MACOS_BACKEND = {
5954
5967
  }
5955
5968
  if (result.exitCode === 44) return null;
5956
5969
  if (result.exitCode !== 0) return null;
5957
- const password = stripTrailingNewline(result.stdout);
5970
+ const password = stripTrailingNewline$1(result.stdout);
5958
5971
  return password.length > 0 ? password : null;
5959
5972
  },
5960
5973
  async set(account, password) {
@@ -6489,13 +6502,17 @@ const authImportCommand = defineCommand({
6489
6502
  });
6490
6503
  //#endregion
6491
6504
  //#region src/commands/auth.ts
6505
+ function emitDeprecation(replacement) {
6506
+ process.stderr.write(`warning: this command is deprecated and will be removed in 1.0; ${replacement}\n`);
6507
+ }
6492
6508
  async function runAuthSet(args, deps = {}) {
6509
+ emitDeprecation("use `aitcc login` (interactive prompt offers a save option).");
6493
6510
  const env = deps.env ?? process.env;
6494
6511
  let email = args.email?.trim();
6495
- let password$2 = args.password;
6496
- const argvPasswordUsed = password$2 !== void 0;
6512
+ let password$1 = args.password;
6513
+ const argvPasswordUsed = password$1 !== void 0;
6497
6514
  if (!email && env.AITCC_EMAIL) email = env.AITCC_EMAIL;
6498
- if (password$2 === void 0 && env.AITCC_PASSWORD) password$2 = env.AITCC_PASSWORD;
6515
+ if (password$1 === void 0 && env.AITCC_PASSWORD) password$1 = env.AITCC_PASSWORD;
6499
6516
  if (argvPasswordUsed) process.stderr.write("Warning: --password on argv is visible in `ps`/Task Manager. Prefer the AITCC_PASSWORD environment variable for scripted use.\n");
6500
6517
  const interactive = process.stdout.isTTY && process.stdin.isTTY && !args.json;
6501
6518
  if (!email) {
@@ -6509,7 +6526,7 @@ async function runAuthSet(args, deps = {}) {
6509
6526
  validate: (raw) => raw.trim().length > 0 ? true : "email is required"
6510
6527
  })).trim();
6511
6528
  } catch (err) {
6512
- if (isPromptCancelled(err)) {
6529
+ if (isPromptCancelled$1(err)) {
6513
6530
  process.stderr.write("Aborted.\n");
6514
6531
  return exitAfterFlush(ExitCode.Usage);
6515
6532
  }
@@ -6525,26 +6542,26 @@ async function runAuthSet(args, deps = {}) {
6525
6542
  else process.stderr.write(`Invalid email: ${email}\n`);
6526
6543
  return exitAfterFlush(ExitCode.Usage);
6527
6544
  }
6528
- if (password$2 === void 0) {
6545
+ if (password$1 === void 0) {
6529
6546
  if (!interactive) {
6530
6547
  emitInteractiveRequired(args.json, "password");
6531
6548
  return exitAfterFlush(ExitCode.Usage);
6532
6549
  }
6533
6550
  try {
6534
- password$2 = await password({
6551
+ password$1 = await password({
6535
6552
  message: "Password:",
6536
6553
  mask: true,
6537
6554
  validate: (raw) => raw.length > 0 ? true : "password is required"
6538
6555
  });
6539
6556
  } catch (err) {
6540
- if (isPromptCancelled(err)) {
6557
+ if (isPromptCancelled$1(err)) {
6541
6558
  process.stderr.write("Aborted.\n");
6542
6559
  return exitAfterFlush(ExitCode.Usage);
6543
6560
  }
6544
6561
  throw err;
6545
6562
  }
6546
6563
  }
6547
- if (password$2.length === 0) {
6564
+ if (password$1.length === 0) {
6548
6565
  if (args.json) emitJson({
6549
6566
  ok: false,
6550
6567
  reason: "invalid-password",
@@ -6555,7 +6572,7 @@ async function runAuthSet(args, deps = {}) {
6555
6572
  }
6556
6573
  let result;
6557
6574
  try {
6558
- result = await saveCredentials(email, password$2, deps.backend ? { override: deps.backend } : {});
6575
+ result = await saveCredentials(email, password$1, deps.backend ? { override: deps.backend } : {});
6559
6576
  } catch (err) {
6560
6577
  const message = err.message;
6561
6578
  if (args.json) emitJson({
@@ -6576,6 +6593,7 @@ async function runAuthSet(args, deps = {}) {
6576
6593
  return exitAfterFlush(ExitCode.Ok);
6577
6594
  }
6578
6595
  async function runAuthClear(args, deps = {}) {
6596
+ emitDeprecation("use `aitcc logout --purge` to remove session and saved credentials together.");
6579
6597
  const interactive = process.stdout.isTTY && process.stdin.isTTY && !args.json;
6580
6598
  const active = await getActiveCredentialEmail(deps.env ? { env: deps.env } : {}).catch(() => null);
6581
6599
  if (!args.yes) {
@@ -6596,7 +6614,7 @@ async function runAuthClear(args, deps = {}) {
6596
6614
  default: false
6597
6615
  });
6598
6616
  } catch (err) {
6599
- if (isPromptCancelled(err)) {
6617
+ if (isPromptCancelled$1(err)) {
6600
6618
  process.stderr.write("Aborted.\n");
6601
6619
  return exitAfterFlush(ExitCode.Usage);
6602
6620
  }
@@ -6634,6 +6652,7 @@ async function runAuthClear(args, deps = {}) {
6634
6652
  return exitAfterFlush(ExitCode.Ok);
6635
6653
  }
6636
6654
  async function runAuthStatus(args, deps = {}) {
6655
+ emitDeprecation("use `aitcc whoami` (now reports credential source).");
6637
6656
  const active = await getActiveCredentialEmail(deps.env ? { env: deps.env } : {}).catch(() => null);
6638
6657
  const session = await readSession();
6639
6658
  if (args.json) {
@@ -6672,7 +6691,7 @@ function emitInteractiveRequired(json, missing) {
6672
6691
  });
6673
6692
  else process.stderr.write(`Cannot prompt for ${missing} in non-interactive mode. Use --${missing} or set AITCC_${missing.toUpperCase()}.\n`);
6674
6693
  }
6675
- function isPromptCancelled(err) {
6694
+ function isPromptCancelled$1(err) {
6676
6695
  return err instanceof Error && err.name === "ExitPromptError";
6677
6696
  }
6678
6697
  const authCommand = defineCommand({
@@ -6684,7 +6703,7 @@ const authCommand = defineCommand({
6684
6703
  set: defineCommand({
6685
6704
  meta: {
6686
6705
  name: "set",
6687
- description: "Save email + password to the OS keychain for future headless logins."
6706
+ description: "[deprecated] Use `aitcc login` instead the prompt now offers a save option."
6688
6707
  },
6689
6708
  args: {
6690
6709
  json: {
@@ -6712,7 +6731,7 @@ const authCommand = defineCommand({
6712
6731
  clear: defineCommand({
6713
6732
  meta: {
6714
6733
  name: "clear",
6715
- description: "Delete the saved credentials and the auth-state pointer."
6734
+ description: "[deprecated] Use `aitcc logout --purge` instead."
6716
6735
  },
6717
6736
  args: {
6718
6737
  json: {
@@ -6737,7 +6756,7 @@ const authCommand = defineCommand({
6737
6756
  status: defineCommand({
6738
6757
  meta: {
6739
6758
  name: "status",
6740
- description: "Report whether credentials and a session are configured."
6759
+ description: "[deprecated] Use `aitcc whoami` (now reports credential source)."
6741
6760
  },
6742
6761
  args: { json: {
6743
6762
  type: "boolean",
@@ -7226,6 +7245,8 @@ function isResponse(m) {
7226
7245
  return "id" in m;
7227
7246
  }
7228
7247
  var CdpProtocolError = class extends Error {
7248
+ method;
7249
+ code;
7229
7250
  constructor(method, code, message) {
7230
7251
  super(`CDP error for ${method}: ${message} (code=${code})`);
7231
7252
  this.method = method;
@@ -7470,6 +7491,7 @@ function validateCookie(raw, index) {
7470
7491
  //#endregion
7471
7492
  //#region src/chrome.ts
7472
7493
  var ChromeNotFoundError = class extends Error {
7494
+ candidates;
7473
7495
  constructor(candidates) {
7474
7496
  super(`Could not find Chrome or a Chromium-family browser. Tried: ${candidates.join(", ")}.\nInstall Chrome, or set AITCC_BROWSER to an executable path.`);
7475
7497
  this.candidates = candidates;
@@ -7477,6 +7499,7 @@ var ChromeNotFoundError = class extends Error {
7477
7499
  }
7478
7500
  };
7479
7501
  var ChromeLaunchError = class extends Error {
7502
+ executable;
7480
7503
  constructor(executable, cause) {
7481
7504
  super(`Failed to launch ${executable}: ${cause.message}`);
7482
7505
  this.executable = executable;
@@ -7485,6 +7508,7 @@ var ChromeLaunchError = class extends Error {
7485
7508
  }
7486
7509
  };
7487
7510
  var ChromeEndpointTimeoutError = class extends Error {
7511
+ executable;
7488
7512
  constructor(executable) {
7489
7513
  super(`${executable} did not print a DevTools endpoint within the timeout. It may have been blocked by the OS or launched a GUI-less variant.`);
7490
7514
  this.executable = executable;
@@ -7897,10 +7921,38 @@ function chooseLoginMode(input) {
7897
7921
  if (input.interactiveFlag) return "interactive";
7898
7922
  return input.hasCredentials ? "headless" : "interactive";
7899
7923
  }
7924
+ const defaultPromptDeps = {
7925
+ email: (defaultValue) => input({
7926
+ message: "Email:",
7927
+ ...defaultValue !== void 0 ? { default: defaultValue } : {},
7928
+ validate: (raw) => {
7929
+ const trimmed = raw.trim();
7930
+ if (trimmed.length === 0) return "email is required";
7931
+ if (!trimmed.includes("@")) return "must contain \"@\"";
7932
+ return true;
7933
+ }
7934
+ }).then((s) => s.trim()),
7935
+ password: () => password({
7936
+ message: "Password:",
7937
+ mask: true,
7938
+ validate: (raw) => raw.length > 0 ? true : "password is required"
7939
+ }),
7940
+ saveTarget: () => select({
7941
+ message: "Where would you like to save the credentials?",
7942
+ default: "keychain",
7943
+ choices: [{
7944
+ name: "OS keychain (recommended) — next login runs headlessly",
7945
+ value: "keychain"
7946
+ }, {
7947
+ name: "Do not save — one-shot. (Tip: AITCC_EMAIL/AITCC_PASSWORD env for CI.)",
7948
+ value: "none"
7949
+ }]
7950
+ })
7951
+ };
7900
7952
  const loginCommand = defineCommand({
7901
7953
  meta: {
7902
7954
  name: "login",
7903
- description: "Open a browser to sign in, then capture the console session cookies."
7955
+ description: "Sign in to the Apps in Toss console and capture the session cookies."
7904
7956
  },
7905
7957
  args: {
7906
7958
  json: {
@@ -7918,9 +7970,26 @@ const loginCommand = defineCommand({
7918
7970
  description: "Force the visible-browser flow even if credentials are configured.",
7919
7971
  default: false
7920
7972
  },
7973
+ email: {
7974
+ type: "string",
7975
+ description: "Email (skip prompt; required for non-interactive use)."
7976
+ },
7977
+ password: {
7978
+ type: "string",
7979
+ description: "Password (skip prompt; visible in `ps`/Task Manager — prefer --password-stdin or AITCC_PASSWORD env)."
7980
+ },
7981
+ "password-stdin": {
7982
+ type: "boolean",
7983
+ description: "Read the password from stdin (recommended for non-interactive use).",
7984
+ default: false
7985
+ },
7986
+ save: {
7987
+ type: "string",
7988
+ description: "Where to persist credentials when --email/--password* are passed: \"keychain\" or \"none\" (default)."
7989
+ },
7921
7990
  "skip-onboarding": {
7922
7991
  type: "boolean",
7923
- description: "Skip the post-login prompt to save credentials to the OS keychain.",
7992
+ description: "Deprecated no-op; kept so existing scripts do not break.",
7924
7993
  default: false
7925
7994
  }
7926
7995
  },
@@ -7929,7 +7998,10 @@ const loginCommand = defineCommand({
7929
7998
  json: args.json,
7930
7999
  timeout: args.timeout,
7931
8000
  interactive: args.interactive,
7932
- skipOnboarding: args["skip-onboarding"]
8001
+ email: typeof args.email === "string" ? args.email : void 0,
8002
+ password: typeof args.password === "string" ? args.password : void 0,
8003
+ passwordStdin: args["password-stdin"],
8004
+ save: typeof args.save === "string" ? args.save : void 0
7933
8005
  }, {
7934
8006
  getCredentials: loadCredentials,
7935
8007
  saveCredentials
@@ -7973,17 +8045,41 @@ async function runLoginCommand(args, deps) {
7973
8045
  }
7974
8046
  process.stderr.write(`Using custom authorize URL from AITCC_OAUTH_URL: ${authorizeUrl}\n`);
7975
8047
  }
7976
- let credentials = null;
7977
- if (!args.interactive) {
7978
- const getCredentials = deps.getCredentials;
7979
- if (getCredentials) credentials = await getCredentials().catch((err) => {
7980
- process.stderr.write(`Credential lookup failed (${err.message}); using interactive login.\n`);
7981
- return null;
7982
- });
8048
+ const resolved = await resolveCredentialsForLogin(args, deps);
8049
+ if (resolved.kind === "error") {
8050
+ emitError({
8051
+ reason: resolved.reason,
8052
+ message: resolved.message
8053
+ }, resolved.message);
8054
+ return exitAfterFlush(resolved.exitCode);
8055
+ }
8056
+ let saved = "skipped";
8057
+ if (resolved.saveTarget === "keychain" && resolved.credentials !== null) {
8058
+ const save = deps.saveCredentials;
8059
+ if (!save) {
8060
+ emitError({
8061
+ reason: "save-unavailable",
8062
+ message: "no save backend configured"
8063
+ }, "Cannot save credentials: no backend configured.");
8064
+ return exitAfterFlush(ExitCode.Generic);
8065
+ }
8066
+ try {
8067
+ const result = await save(resolved.credentials.email, resolved.credentials.password);
8068
+ saved = result.status;
8069
+ if (!args.json) if (result.status === "unchanged") process.stderr.write("Credentials already saved (no change).\n");
8070
+ else process.stderr.write(`Credentials saved to OS keychain (${resolved.credentials.email}).\n`);
8071
+ } catch (err) {
8072
+ const message = err.message;
8073
+ emitError({
8074
+ reason: "keychain-save-failed",
8075
+ message
8076
+ }, `Failed to save credentials to the OS keychain: ${message}\nOn Linux, install libsecret (\`secret-tool\`) and retry. Re-run with \`--save none\` to skip persistence.`);
8077
+ return exitAfterFlush(ExitCode.Usage);
8078
+ }
7983
8079
  }
7984
8080
  const initialMode = chooseLoginMode({
7985
8081
  interactiveFlag: args.interactive,
7986
- hasCredentials: credentials !== null
8082
+ hasCredentials: resolved.credentials !== null
7987
8083
  });
7988
8084
  const endpointTimeoutMs = Math.min(6e4, Math.max(3e4, Math.floor(timeoutMs / 2)));
7989
8085
  const firstAttemptStart = Date.now();
@@ -7993,9 +8089,9 @@ async function runLoginCommand(args, deps) {
7993
8089
  endpointTimeoutMs,
7994
8090
  authorizeUrl,
7995
8091
  mode: initialMode,
7996
- credentials,
7997
- emitError,
7998
- deps
8092
+ credentials: resolved.credentials,
8093
+ saved,
8094
+ emitError
7999
8095
  });
8000
8096
  if (result.status === "fallback-to-interactive") {
8001
8097
  process.stderr.write(`${result.message}\n`);
@@ -8006,8 +8102,8 @@ async function runLoginCommand(args, deps) {
8006
8102
  authorizeUrl,
8007
8103
  mode: "interactive",
8008
8104
  credentials: null,
8009
- emitError,
8010
- deps
8105
+ saved,
8106
+ emitError
8011
8107
  });
8012
8108
  if (second.status === "exit") return exitAfterFlush(second.code);
8013
8109
  second.status;
@@ -8015,8 +8111,167 @@ async function runLoginCommand(args, deps) {
8015
8111
  }
8016
8112
  return exitAfterFlush(result.code);
8017
8113
  }
8114
+ /**
8115
+ * Resolve credentials and the requested save target for `aitcc login`.
8116
+ * Pure-ish: only side-effect is reading stdin via `deps.readStdin` (when
8117
+ * `--password-stdin` is set) and prompting via `deps.prompts` (when TTY).
8118
+ */
8119
+ async function resolveCredentialsForLogin(args, deps, opts = {}) {
8120
+ const env = opts.env ?? process.env;
8121
+ const stdoutIsTTY = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
8122
+ const stdinIsTTY = opts.stdinIsTTY ?? Boolean(process.stdin.isTTY);
8123
+ const interactiveTty = stdoutIsTTY && stdinIsTTY && !args.json;
8124
+ if (args.password !== void 0 && args.passwordStdin) return {
8125
+ kind: "error",
8126
+ reason: "conflicting-password-source",
8127
+ message: "--password and --password-stdin cannot be used together.",
8128
+ exitCode: ExitCode.Usage
8129
+ };
8130
+ if (args.interactive && (args.email !== void 0 || args.password !== void 0 || args.passwordStdin || args.save !== void 0)) return {
8131
+ kind: "error",
8132
+ reason: "conflicting-interactive-flags",
8133
+ message: "--interactive cannot be combined with --email/--password/--password-stdin/--save. Drop --interactive to use credentials, or drop the credential flags to type in the browser.",
8134
+ exitCode: ExitCode.Usage
8135
+ };
8136
+ let saveTarget;
8137
+ if (args.save !== void 0) {
8138
+ if (args.save !== "keychain" && args.save !== "none") return {
8139
+ kind: "error",
8140
+ reason: "invalid-save",
8141
+ message: `--save must be "keychain" or "none" (got "${args.save}").`,
8142
+ exitCode: ExitCode.Usage
8143
+ };
8144
+ saveTarget = args.save;
8145
+ }
8146
+ if (args.email !== void 0 || args.password !== void 0 || args.passwordStdin) {
8147
+ if (args.email === void 0 || args.email.trim().length === 0) return {
8148
+ kind: "error",
8149
+ reason: "missing-email",
8150
+ message: "--email is required when --password / --password-stdin is passed.",
8151
+ exitCode: ExitCode.Usage
8152
+ };
8153
+ if (!args.email.includes("@")) return {
8154
+ kind: "error",
8155
+ reason: "invalid-email",
8156
+ message: `Invalid email: ${args.email}`,
8157
+ exitCode: ExitCode.Usage
8158
+ };
8159
+ let password;
8160
+ if (args.passwordStdin) {
8161
+ password = stripTrailingNewline(await (deps.readStdin ?? readStdinAll)());
8162
+ if (password.length === 0) return {
8163
+ kind: "error",
8164
+ reason: "invalid-password",
8165
+ message: "--password-stdin received an empty password on stdin.",
8166
+ exitCode: ExitCode.Usage
8167
+ };
8168
+ } else if (args.password !== void 0) {
8169
+ process.stderr.write("Warning: --password on argv is visible in `ps`/Task Manager. Prefer --password-stdin or the AITCC_PASSWORD environment variable.\n");
8170
+ password = args.password;
8171
+ if (password.length === 0) return {
8172
+ kind: "error",
8173
+ reason: "invalid-password",
8174
+ message: "--password value is empty.",
8175
+ exitCode: ExitCode.Usage
8176
+ };
8177
+ } else return {
8178
+ kind: "error",
8179
+ reason: "missing-password",
8180
+ message: "--email passed without a password. Add --password-stdin (recommended) or --password.",
8181
+ exitCode: ExitCode.Usage
8182
+ };
8183
+ return {
8184
+ kind: "ok",
8185
+ credentials: {
8186
+ source: "argv",
8187
+ email: args.email.trim(),
8188
+ password
8189
+ },
8190
+ saveTarget: saveTarget ?? "none"
8191
+ };
8192
+ }
8193
+ if (args.interactive) return {
8194
+ kind: "ok",
8195
+ credentials: null,
8196
+ saveTarget: saveTarget ?? "none"
8197
+ };
8198
+ if (env.AITCC_EMAIL && env.AITCC_PASSWORD) return {
8199
+ kind: "ok",
8200
+ credentials: {
8201
+ source: "env",
8202
+ email: env.AITCC_EMAIL,
8203
+ password: env.AITCC_PASSWORD
8204
+ },
8205
+ saveTarget: saveTarget ?? "none"
8206
+ };
8207
+ const getCredentials = deps.getCredentials;
8208
+ if (getCredentials) {
8209
+ const fromStore = await getCredentials().catch((err) => {
8210
+ process.stderr.write(`Credential lookup failed (${err.message}); ignoring.\n`);
8211
+ return null;
8212
+ });
8213
+ if (fromStore) {
8214
+ if (!args.json) process.stderr.write(`Using credentials from OS keychain for ${fromStore.email}. Pass --interactive to type a different account.
8215
+ `);
8216
+ return {
8217
+ kind: "ok",
8218
+ credentials: {
8219
+ source: fromStore.kind,
8220
+ email: fromStore.email,
8221
+ password: fromStore.password
8222
+ },
8223
+ saveTarget: saveTarget ?? "none"
8224
+ };
8225
+ }
8226
+ }
8227
+ if (interactiveTty) {
8228
+ const prompts = deps.prompts ?? defaultPromptDeps;
8229
+ let email;
8230
+ let password;
8231
+ try {
8232
+ email = await prompts.email();
8233
+ password = await prompts.password();
8234
+ } catch (err) {
8235
+ if (isPromptCancelled(err)) return {
8236
+ kind: "error",
8237
+ reason: "aborted",
8238
+ message: "Aborted.",
8239
+ exitCode: ExitCode.Usage
8240
+ };
8241
+ throw err;
8242
+ }
8243
+ let promptedSave;
8244
+ if (saveTarget !== void 0) promptedSave = saveTarget;
8245
+ else try {
8246
+ promptedSave = await prompts.saveTarget();
8247
+ } catch (err) {
8248
+ if (isPromptCancelled(err)) return {
8249
+ kind: "error",
8250
+ reason: "aborted",
8251
+ message: "Aborted.",
8252
+ exitCode: ExitCode.Usage
8253
+ };
8254
+ throw err;
8255
+ }
8256
+ return {
8257
+ kind: "ok",
8258
+ credentials: {
8259
+ source: "prompt",
8260
+ email,
8261
+ password
8262
+ },
8263
+ saveTarget: promptedSave
8264
+ };
8265
+ }
8266
+ return {
8267
+ kind: "error",
8268
+ reason: "interactive-required",
8269
+ message: "No credentials configured and stdin is not a TTY. Pass --email + --password-stdin (or set AITCC_EMAIL + AITCC_PASSWORD).",
8270
+ exitCode: ExitCode.Usage
8271
+ };
8272
+ }
8018
8273
  async function attemptLogin(opts) {
8019
- const { args, timeoutMs, endpointTimeoutMs, authorizeUrl, mode, credentials, emitError, deps } = opts;
8274
+ const { args, timeoutMs, endpointTimeoutMs, authorizeUrl, mode, credentials, saved, emitError } = opts;
8020
8275
  const launched = await launchChrome({
8021
8276
  initialUrl: authorizeUrl,
8022
8277
  endpointTimeoutMs,
@@ -8055,8 +8310,8 @@ async function attemptLogin(opts) {
8055
8310
  }
8056
8311
  if (mode === "interactive") process.stderr.write("Opened a browser window — complete the sign-in there. The CLI will capture the session automatically.\n");
8057
8312
  else {
8058
- const source = credentials?.kind === "env" ? "env" : "keychain";
8059
- process.stderr.write(`Signing in headlessly with credentials from ${source}…\n`);
8313
+ const sourceLabel = credentials?.source ?? "configured store";
8314
+ process.stderr.write(`Signing in headlessly with credentials from ${sourceLabel}…\n`);
8060
8315
  }
8061
8316
  let client = null;
8062
8317
  const disposeAll = async () => {
@@ -8223,57 +8478,29 @@ async function attemptLogin(opts) {
8223
8478
  capturedAt: session.capturedAt,
8224
8479
  cookieCount: cookies.length,
8225
8480
  mode,
8481
+ credentialSource: credentials?.source ?? "browser",
8482
+ saved,
8226
8483
  stepUp
8227
8484
  })}\n`);
8228
8485
  else process.stdout.write(`Logged in as ${user.name} <${user.email}>\n`);
8229
8486
  await disposeAll();
8230
- if (mode === "interactive" && credentials === null && !args.json && !args.skipOnboarding && process.stdout.isTTY && process.stdin.isTTY && deps.saveCredentials) await runOnboardingPrompt(user.email, deps.saveCredentials);
8231
8487
  return {
8232
8488
  status: "exit",
8233
8489
  code: ExitCode.Ok
8234
8490
  };
8235
8491
  }
8236
- /**
8237
- * Post-login prompt that offers to persist the user's email + password
8238
- * to the OS keychain so subsequent `aitcc login` runs can take the
8239
- * headless form-fill path. Failures are non-fatal: we already wrote a
8240
- * valid session, so a keychain hiccup just means the next login will
8241
- * fall back to interactive — exactly the same UX as before.
8242
- */
8243
- async function runOnboardingPrompt(email, save) {
8244
- process.stdout.write("\n");
8245
- process.stdout.write("Would you like to save your password to the OS keychain so the next `aitcc login` runs headlessly?\n");
8246
- let agreed;
8247
- try {
8248
- agreed = await confirm({
8249
- message: "Save credentials?",
8250
- default: true
8251
- });
8252
- } catch (err) {
8253
- if (err instanceof Error && err.name === "ExitPromptError") return;
8254
- process.stderr.write(`Onboarding prompt failed: ${err.message}\n`);
8255
- return;
8256
- }
8257
- if (!agreed) return;
8258
- let password$1;
8259
- try {
8260
- password$1 = await password({
8261
- message: "Password:",
8262
- mask: true,
8263
- validate: (raw) => raw.length > 0 ? true : "password is required"
8264
- });
8265
- } catch (err) {
8266
- if (err instanceof Error && err.name === "ExitPromptError") return;
8267
- process.stderr.write(`Could not read password: ${err.message}\n`);
8268
- return;
8269
- }
8270
- try {
8271
- await save(email, password$1);
8272
- process.stdout.write("Saved. Next `aitcc login` will run headlessly.\n");
8273
- } catch (err) {
8274
- process.stderr.write(`Could not save credentials: ${err.message}. You can retry later with \`aitcc auth set\`.
8275
- `);
8276
- }
8492
+ async function readStdinAll() {
8493
+ if (process.stdin.isTTY) throw new Error("--password-stdin requires stdin to be a pipe, not a TTY.");
8494
+ process.stdin.setEncoding("utf8");
8495
+ let buf = "";
8496
+ for await (const chunk of process.stdin) buf += chunk;
8497
+ return buf;
8498
+ }
8499
+ function stripTrailingNewline(s) {
8500
+ return s.replace(/\r?\n$/, "");
8501
+ }
8502
+ function isPromptCancelled(err) {
8503
+ return err instanceof Error && err.name === "ExitPromptError";
8277
8504
  }
8278
8505
  async function waitForLanding(client, sessionId, timeoutMs) {
8279
8506
  return await new Promise((resolve) => {
@@ -8337,18 +8564,25 @@ async function resolveUserWithRetry(cookies, opts = {}) {
8337
8564
  const logoutCommand = defineCommand({
8338
8565
  meta: {
8339
8566
  name: "logout",
8340
- description: "Delete the local session file."
8567
+ description: "Delete the local session file (and optionally the saved credentials)."
8568
+ },
8569
+ args: {
8570
+ json: {
8571
+ type: "boolean",
8572
+ description: "Emit machine-readable JSON to stdout.",
8573
+ default: false
8574
+ },
8575
+ purge: {
8576
+ type: "boolean",
8577
+ description: "Also delete saved keychain credentials and the auth-state pointer.",
8578
+ default: false
8579
+ }
8341
8580
  },
8342
- args: { json: {
8343
- type: "boolean",
8344
- description: "Emit machine-readable JSON to stdout.",
8345
- default: false
8346
- } },
8347
8581
  async run({ args }) {
8348
8582
  const path = sessionPathForDiagnostics();
8349
- let existed;
8583
+ let sessionRemoved;
8350
8584
  try {
8351
- existed = (await clearSession()).existed;
8585
+ sessionRemoved = (await clearSession()).existed;
8352
8586
  } catch (err) {
8353
8587
  const message = err.message;
8354
8588
  if (args.json) process.stdout.write(`${JSON.stringify({
@@ -8360,13 +8594,29 @@ const logoutCommand = defineCommand({
8360
8594
  process.stderr.write(`Failed to remove session file at ${path}: ${message}\n`);
8361
8595
  return exitAfterFlush(ExitCode.Generic);
8362
8596
  }
8363
- if (args.json) process.stdout.write(`${JSON.stringify({
8364
- ok: true,
8365
- status: existed ? "logged-out" : "no-session",
8366
- path
8367
- })}\n`);
8368
- else if (existed) process.stdout.write(`Logged out. Session removed from ${path}\n`);
8369
- else process.stdout.write(`No active session at ${path}.\n`);
8597
+ let credentialsPurged = false;
8598
+ let purgeError = null;
8599
+ if (args.purge) try {
8600
+ credentialsPurged = (await deleteCredentials()).existed;
8601
+ } catch (err) {
8602
+ purgeError = err.message;
8603
+ }
8604
+ if (args.json) {
8605
+ const payload = {
8606
+ ok: true,
8607
+ sessionRemoved,
8608
+ credentialsPurged,
8609
+ path
8610
+ };
8611
+ if (purgeError !== null) payload.purgeError = purgeError;
8612
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
8613
+ } else {
8614
+ if (sessionRemoved) process.stdout.write(`Logged out. Session removed from ${path}\n`);
8615
+ else process.stdout.write(`No active session at ${path}.\n`);
8616
+ if (args.purge) if (purgeError !== null) process.stderr.write(`Could not delete saved credentials: ${purgeError}\n`);
8617
+ else if (credentialsPurged) process.stdout.write("Saved credentials deleted from the OS keychain.\n");
8618
+ else process.stdout.write("No saved credentials to delete.\n");
8619
+ }
8370
8620
  return exitAfterFlush(ExitCode.Ok);
8371
8621
  }
8372
8622
  });
@@ -8901,7 +9151,7 @@ function resolveVersion() {
8901
9151
  if (typeof injected === "string" && injected.length > 0) return injected;
8902
9152
  } catch {}
8903
9153
  try {
8904
- return "0.1.24";
9154
+ return "0.1.26";
8905
9155
  } catch {}
8906
9156
  return "0.0.0-dev";
8907
9157
  }
@@ -9273,6 +9523,22 @@ function maybeEmitNotice(entry, env) {
9273
9523
  }
9274
9524
  //#endregion
9275
9525
  //#region src/commands/whoami.ts
9526
+ async function describeCredentialSource() {
9527
+ const active = await getActiveCredentialEmail().catch(() => null);
9528
+ if (!active) return {
9529
+ source: "none",
9530
+ email: null
9531
+ };
9532
+ return {
9533
+ source: active.kind,
9534
+ email: active.email
9535
+ };
9536
+ }
9537
+ function formatCredentials(cred) {
9538
+ if (cred.source === "none") return "none (run `aitcc login` to save)";
9539
+ if (cred.source === "env") return `env (AITCC_EMAIL${cred.email ? ` = ${cred.email}` : ""})`;
9540
+ return `keychain${cred.email ? ` (${cred.email})` : ""}`;
9541
+ }
9276
9542
  async function runBackgroundUpdateCheck(json) {
9277
9543
  if (json) return;
9278
9544
  const timeoutMs = 500;
@@ -9300,14 +9566,18 @@ const whoamiCommand = defineCommand({
9300
9566
  },
9301
9567
  async run({ args }) {
9302
9568
  const session = await readSession();
9569
+ const cred = await describeCredentialSource();
9303
9570
  if (!session) {
9304
9571
  if (args.json) process.stdout.write(`${JSON.stringify({
9305
9572
  ok: true,
9306
- authenticated: false
9573
+ authenticated: false,
9574
+ credentialSource: cred.source,
9575
+ ...cred.email ? { credentialEmail: cred.email } : {}
9307
9576
  })}\n`);
9308
9577
  else {
9309
9578
  process.stderr.write("Not logged in. Run `aitcc login` to start a session.\n");
9310
9579
  process.stderr.write(`Session file checked: ${sessionPathForDiagnostics()}\n`);
9580
+ process.stderr.write(`Credentials: ${formatCredentials(cred)}\n`);
9311
9581
  }
9312
9582
  return exitAfterFlush(ExitCode.NotAuthenticated);
9313
9583
  }
@@ -9318,12 +9588,15 @@ const whoamiCommand = defineCommand({
9318
9588
  authenticated: true,
9319
9589
  source: "cache",
9320
9590
  user: session.user,
9321
- capturedAt: session.capturedAt
9591
+ capturedAt: session.capturedAt,
9592
+ credentialSource: cred.source,
9593
+ ...cred.email ? { credentialEmail: cred.email } : {}
9322
9594
  })}\n`);
9323
9595
  return exitAfterFlush(ExitCode.Ok);
9324
9596
  }
9325
9597
  const label = session.user.displayName ? `${session.user.displayName} <${session.user.email}>` : session.user.email;
9326
9598
  process.stdout.write(`Logged in as ${label} (cached)\n`);
9599
+ process.stdout.write(`Credentials: ${formatCredentials(cred)}\n`);
9327
9600
  process.stdout.write(`Session captured: ${session.capturedAt}\n`);
9328
9601
  await runBackgroundUpdateCheck(args.json);
9329
9602
  return exitAfterFlush(ExitCode.Ok);
@@ -9347,11 +9620,14 @@ const whoamiCommand = defineCommand({
9347
9620
  workspaceName: w.workspaceName,
9348
9621
  role: w.role
9349
9622
  })),
9350
- capturedAt: session.capturedAt
9623
+ capturedAt: session.capturedAt,
9624
+ credentialSource: cred.source,
9625
+ ...cred.email ? { credentialEmail: cred.email } : {}
9351
9626
  })}\n`);
9352
9627
  return exitAfterFlush(ExitCode.Ok);
9353
9628
  }
9354
9629
  process.stdout.write(`Logged in as ${info.name} <${info.email}> (${info.role})\n`);
9630
+ process.stdout.write(`Credentials: ${formatCredentials(cred)}\n`);
9355
9631
  if (info.workspaces.length > 0) {
9356
9632
  process.stdout.write("Workspaces:\n");
9357
9633
  for (const w of info.workspaces) process.stdout.write(` - ${w.workspaceName} (id ${w.workspaceId}, ${w.role})\n`);
@@ -9364,9 +9640,14 @@ const whoamiCommand = defineCommand({
9364
9640
  ok: true,
9365
9641
  authenticated: false,
9366
9642
  reason: "session-expired",
9367
- errorCode: err.errorCode
9643
+ errorCode: err.errorCode,
9644
+ credentialSource: cred.source,
9645
+ ...cred.email ? { credentialEmail: cred.email } : {}
9368
9646
  })}\n`);
9369
- else process.stderr.write("Session is no longer valid. Run `aitcc login` again.\n");
9647
+ else {
9648
+ process.stderr.write("Session is no longer valid. Run `aitcc login` again.\n");
9649
+ process.stderr.write(`Credentials: ${formatCredentials(cred)}\n`);
9650
+ }
9370
9651
  return exitAfterFlush(ExitCode.NotAuthenticated);
9371
9652
  }
9372
9653
  if (err instanceof NetworkError) {