@ait-co/console-cli 0.1.24 → 0.1.25

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
@@ -2593,7 +2593,7 @@ async function runAppInit(args) {
2593
2593
  workspaceId = await pickWorkspace(session.cookies);
2594
2594
  categoryIds = await pickCategories(session.cookies);
2595
2595
  } catch (err) {
2596
- if (isPromptCancelled$1(err)) {
2596
+ if (isPromptCancelled$2(err)) {
2597
2597
  process.stderr.write("Aborted.\n");
2598
2598
  return exitAfterFlush(ExitCode.Usage);
2599
2599
  }
@@ -2636,7 +2636,7 @@ async function runAppInit(args) {
2636
2636
  categoryIds
2637
2637
  };
2638
2638
  } catch (err) {
2639
- if (isPromptCancelled$1(err)) {
2639
+ if (isPromptCancelled$2(err)) {
2640
2640
  process.stderr.write("Aborted.\n");
2641
2641
  return exitAfterFlush(ExitCode.Usage);
2642
2642
  }
@@ -2695,7 +2695,7 @@ async function fileExists(path) {
2695
2695
  return false;
2696
2696
  }
2697
2697
  }
2698
- function isPromptCancelled$1(err) {
2698
+ function isPromptCancelled$2(err) {
2699
2699
  return err instanceof Error && err.name === "ExitPromptError";
2700
2700
  }
2701
2701
  async function pickWorkspace(cookies) {
@@ -5856,7 +5856,7 @@ async function runCommand(command, opts) {
5856
5856
  function isCommandNotFound(err) {
5857
5857
  return err.code === "ENOENT";
5858
5858
  }
5859
- function stripTrailingNewline(s) {
5859
+ function stripTrailingNewline$1(s) {
5860
5860
  return s.replace(/\r?\n$/, "");
5861
5861
  }
5862
5862
  function redactStderr(stderr) {
@@ -5890,7 +5890,7 @@ const LINUX_BACKEND = {
5890
5890
  throw new CredentialBackendCommandError("secret-tool lookup", result.exitCode, redactStderr(result.stderr));
5891
5891
  }
5892
5892
  if (result.stdout.length === 0) return null;
5893
- const password = stripTrailingNewline(result.stdout);
5893
+ const password = stripTrailingNewline$1(result.stdout);
5894
5894
  return password.length > 0 ? password : null;
5895
5895
  },
5896
5896
  async set(account, password) {
@@ -5954,7 +5954,7 @@ const MACOS_BACKEND = {
5954
5954
  }
5955
5955
  if (result.exitCode === 44) return null;
5956
5956
  if (result.exitCode !== 0) return null;
5957
- const password = stripTrailingNewline(result.stdout);
5957
+ const password = stripTrailingNewline$1(result.stdout);
5958
5958
  return password.length > 0 ? password : null;
5959
5959
  },
5960
5960
  async set(account, password) {
@@ -6489,13 +6489,17 @@ const authImportCommand = defineCommand({
6489
6489
  });
6490
6490
  //#endregion
6491
6491
  //#region src/commands/auth.ts
6492
+ function emitDeprecation(replacement) {
6493
+ process.stderr.write(`warning: this command is deprecated and will be removed in 1.0; ${replacement}\n`);
6494
+ }
6492
6495
  async function runAuthSet(args, deps = {}) {
6496
+ emitDeprecation("use `aitcc login` (interactive prompt offers a save option).");
6493
6497
  const env = deps.env ?? process.env;
6494
6498
  let email = args.email?.trim();
6495
- let password$2 = args.password;
6496
- const argvPasswordUsed = password$2 !== void 0;
6499
+ let password$1 = args.password;
6500
+ const argvPasswordUsed = password$1 !== void 0;
6497
6501
  if (!email && env.AITCC_EMAIL) email = env.AITCC_EMAIL;
6498
- if (password$2 === void 0 && env.AITCC_PASSWORD) password$2 = env.AITCC_PASSWORD;
6502
+ if (password$1 === void 0 && env.AITCC_PASSWORD) password$1 = env.AITCC_PASSWORD;
6499
6503
  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
6504
  const interactive = process.stdout.isTTY && process.stdin.isTTY && !args.json;
6501
6505
  if (!email) {
@@ -6509,7 +6513,7 @@ async function runAuthSet(args, deps = {}) {
6509
6513
  validate: (raw) => raw.trim().length > 0 ? true : "email is required"
6510
6514
  })).trim();
6511
6515
  } catch (err) {
6512
- if (isPromptCancelled(err)) {
6516
+ if (isPromptCancelled$1(err)) {
6513
6517
  process.stderr.write("Aborted.\n");
6514
6518
  return exitAfterFlush(ExitCode.Usage);
6515
6519
  }
@@ -6525,26 +6529,26 @@ async function runAuthSet(args, deps = {}) {
6525
6529
  else process.stderr.write(`Invalid email: ${email}\n`);
6526
6530
  return exitAfterFlush(ExitCode.Usage);
6527
6531
  }
6528
- if (password$2 === void 0) {
6532
+ if (password$1 === void 0) {
6529
6533
  if (!interactive) {
6530
6534
  emitInteractiveRequired(args.json, "password");
6531
6535
  return exitAfterFlush(ExitCode.Usage);
6532
6536
  }
6533
6537
  try {
6534
- password$2 = await password({
6538
+ password$1 = await password({
6535
6539
  message: "Password:",
6536
6540
  mask: true,
6537
6541
  validate: (raw) => raw.length > 0 ? true : "password is required"
6538
6542
  });
6539
6543
  } catch (err) {
6540
- if (isPromptCancelled(err)) {
6544
+ if (isPromptCancelled$1(err)) {
6541
6545
  process.stderr.write("Aborted.\n");
6542
6546
  return exitAfterFlush(ExitCode.Usage);
6543
6547
  }
6544
6548
  throw err;
6545
6549
  }
6546
6550
  }
6547
- if (password$2.length === 0) {
6551
+ if (password$1.length === 0) {
6548
6552
  if (args.json) emitJson({
6549
6553
  ok: false,
6550
6554
  reason: "invalid-password",
@@ -6555,7 +6559,7 @@ async function runAuthSet(args, deps = {}) {
6555
6559
  }
6556
6560
  let result;
6557
6561
  try {
6558
- result = await saveCredentials(email, password$2, deps.backend ? { override: deps.backend } : {});
6562
+ result = await saveCredentials(email, password$1, deps.backend ? { override: deps.backend } : {});
6559
6563
  } catch (err) {
6560
6564
  const message = err.message;
6561
6565
  if (args.json) emitJson({
@@ -6576,6 +6580,7 @@ async function runAuthSet(args, deps = {}) {
6576
6580
  return exitAfterFlush(ExitCode.Ok);
6577
6581
  }
6578
6582
  async function runAuthClear(args, deps = {}) {
6583
+ emitDeprecation("use `aitcc logout --purge` to remove session and saved credentials together.");
6579
6584
  const interactive = process.stdout.isTTY && process.stdin.isTTY && !args.json;
6580
6585
  const active = await getActiveCredentialEmail(deps.env ? { env: deps.env } : {}).catch(() => null);
6581
6586
  if (!args.yes) {
@@ -6596,7 +6601,7 @@ async function runAuthClear(args, deps = {}) {
6596
6601
  default: false
6597
6602
  });
6598
6603
  } catch (err) {
6599
- if (isPromptCancelled(err)) {
6604
+ if (isPromptCancelled$1(err)) {
6600
6605
  process.stderr.write("Aborted.\n");
6601
6606
  return exitAfterFlush(ExitCode.Usage);
6602
6607
  }
@@ -6634,6 +6639,7 @@ async function runAuthClear(args, deps = {}) {
6634
6639
  return exitAfterFlush(ExitCode.Ok);
6635
6640
  }
6636
6641
  async function runAuthStatus(args, deps = {}) {
6642
+ emitDeprecation("use `aitcc whoami` (now reports credential source).");
6637
6643
  const active = await getActiveCredentialEmail(deps.env ? { env: deps.env } : {}).catch(() => null);
6638
6644
  const session = await readSession();
6639
6645
  if (args.json) {
@@ -6672,7 +6678,7 @@ function emitInteractiveRequired(json, missing) {
6672
6678
  });
6673
6679
  else process.stderr.write(`Cannot prompt for ${missing} in non-interactive mode. Use --${missing} or set AITCC_${missing.toUpperCase()}.\n`);
6674
6680
  }
6675
- function isPromptCancelled(err) {
6681
+ function isPromptCancelled$1(err) {
6676
6682
  return err instanceof Error && err.name === "ExitPromptError";
6677
6683
  }
6678
6684
  const authCommand = defineCommand({
@@ -6684,7 +6690,7 @@ const authCommand = defineCommand({
6684
6690
  set: defineCommand({
6685
6691
  meta: {
6686
6692
  name: "set",
6687
- description: "Save email + password to the OS keychain for future headless logins."
6693
+ description: "[deprecated] Use `aitcc login` instead the prompt now offers a save option."
6688
6694
  },
6689
6695
  args: {
6690
6696
  json: {
@@ -6712,7 +6718,7 @@ const authCommand = defineCommand({
6712
6718
  clear: defineCommand({
6713
6719
  meta: {
6714
6720
  name: "clear",
6715
- description: "Delete the saved credentials and the auth-state pointer."
6721
+ description: "[deprecated] Use `aitcc logout --purge` instead."
6716
6722
  },
6717
6723
  args: {
6718
6724
  json: {
@@ -6737,7 +6743,7 @@ const authCommand = defineCommand({
6737
6743
  status: defineCommand({
6738
6744
  meta: {
6739
6745
  name: "status",
6740
- description: "Report whether credentials and a session are configured."
6746
+ description: "[deprecated] Use `aitcc whoami` (now reports credential source)."
6741
6747
  },
6742
6748
  args: { json: {
6743
6749
  type: "boolean",
@@ -7897,10 +7903,38 @@ function chooseLoginMode(input) {
7897
7903
  if (input.interactiveFlag) return "interactive";
7898
7904
  return input.hasCredentials ? "headless" : "interactive";
7899
7905
  }
7906
+ const defaultPromptDeps = {
7907
+ email: (defaultValue) => input({
7908
+ message: "Email:",
7909
+ ...defaultValue !== void 0 ? { default: defaultValue } : {},
7910
+ validate: (raw) => {
7911
+ const trimmed = raw.trim();
7912
+ if (trimmed.length === 0) return "email is required";
7913
+ if (!trimmed.includes("@")) return "must contain \"@\"";
7914
+ return true;
7915
+ }
7916
+ }).then((s) => s.trim()),
7917
+ password: () => password({
7918
+ message: "Password:",
7919
+ mask: true,
7920
+ validate: (raw) => raw.length > 0 ? true : "password is required"
7921
+ }),
7922
+ saveTarget: () => select({
7923
+ message: "Where would you like to save the credentials?",
7924
+ default: "keychain",
7925
+ choices: [{
7926
+ name: "OS keychain (recommended) — next login runs headlessly",
7927
+ value: "keychain"
7928
+ }, {
7929
+ name: "Do not save — one-shot. (Tip: AITCC_EMAIL/AITCC_PASSWORD env for CI.)",
7930
+ value: "none"
7931
+ }]
7932
+ })
7933
+ };
7900
7934
  const loginCommand = defineCommand({
7901
7935
  meta: {
7902
7936
  name: "login",
7903
- description: "Open a browser to sign in, then capture the console session cookies."
7937
+ description: "Sign in to the Apps in Toss console and capture the session cookies."
7904
7938
  },
7905
7939
  args: {
7906
7940
  json: {
@@ -7918,9 +7952,26 @@ const loginCommand = defineCommand({
7918
7952
  description: "Force the visible-browser flow even if credentials are configured.",
7919
7953
  default: false
7920
7954
  },
7955
+ email: {
7956
+ type: "string",
7957
+ description: "Email (skip prompt; required for non-interactive use)."
7958
+ },
7959
+ password: {
7960
+ type: "string",
7961
+ description: "Password (skip prompt; visible in `ps`/Task Manager — prefer --password-stdin or AITCC_PASSWORD env)."
7962
+ },
7963
+ "password-stdin": {
7964
+ type: "boolean",
7965
+ description: "Read the password from stdin (recommended for non-interactive use).",
7966
+ default: false
7967
+ },
7968
+ save: {
7969
+ type: "string",
7970
+ description: "Where to persist credentials when --email/--password* are passed: \"keychain\" or \"none\" (default)."
7971
+ },
7921
7972
  "skip-onboarding": {
7922
7973
  type: "boolean",
7923
- description: "Skip the post-login prompt to save credentials to the OS keychain.",
7974
+ description: "Deprecated no-op; kept so existing scripts do not break.",
7924
7975
  default: false
7925
7976
  }
7926
7977
  },
@@ -7929,7 +7980,10 @@ const loginCommand = defineCommand({
7929
7980
  json: args.json,
7930
7981
  timeout: args.timeout,
7931
7982
  interactive: args.interactive,
7932
- skipOnboarding: args["skip-onboarding"]
7983
+ email: typeof args.email === "string" ? args.email : void 0,
7984
+ password: typeof args.password === "string" ? args.password : void 0,
7985
+ passwordStdin: args["password-stdin"],
7986
+ save: typeof args.save === "string" ? args.save : void 0
7933
7987
  }, {
7934
7988
  getCredentials: loadCredentials,
7935
7989
  saveCredentials
@@ -7973,17 +8027,41 @@ async function runLoginCommand(args, deps) {
7973
8027
  }
7974
8028
  process.stderr.write(`Using custom authorize URL from AITCC_OAUTH_URL: ${authorizeUrl}\n`);
7975
8029
  }
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
- });
8030
+ const resolved = await resolveCredentialsForLogin(args, deps);
8031
+ if (resolved.kind === "error") {
8032
+ emitError({
8033
+ reason: resolved.reason,
8034
+ message: resolved.message
8035
+ }, resolved.message);
8036
+ return exitAfterFlush(resolved.exitCode);
8037
+ }
8038
+ let saved = "skipped";
8039
+ if (resolved.saveTarget === "keychain" && resolved.credentials !== null) {
8040
+ const save = deps.saveCredentials;
8041
+ if (!save) {
8042
+ emitError({
8043
+ reason: "save-unavailable",
8044
+ message: "no save backend configured"
8045
+ }, "Cannot save credentials: no backend configured.");
8046
+ return exitAfterFlush(ExitCode.Generic);
8047
+ }
8048
+ try {
8049
+ const result = await save(resolved.credentials.email, resolved.credentials.password);
8050
+ saved = result.status;
8051
+ if (!args.json) if (result.status === "unchanged") process.stderr.write("Credentials already saved (no change).\n");
8052
+ else process.stderr.write(`Credentials saved to OS keychain (${resolved.credentials.email}).\n`);
8053
+ } catch (err) {
8054
+ const message = err.message;
8055
+ emitError({
8056
+ reason: "keychain-save-failed",
8057
+ message
8058
+ }, `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.`);
8059
+ return exitAfterFlush(ExitCode.Usage);
8060
+ }
7983
8061
  }
7984
8062
  const initialMode = chooseLoginMode({
7985
8063
  interactiveFlag: args.interactive,
7986
- hasCredentials: credentials !== null
8064
+ hasCredentials: resolved.credentials !== null
7987
8065
  });
7988
8066
  const endpointTimeoutMs = Math.min(6e4, Math.max(3e4, Math.floor(timeoutMs / 2)));
7989
8067
  const firstAttemptStart = Date.now();
@@ -7993,9 +8071,9 @@ async function runLoginCommand(args, deps) {
7993
8071
  endpointTimeoutMs,
7994
8072
  authorizeUrl,
7995
8073
  mode: initialMode,
7996
- credentials,
7997
- emitError,
7998
- deps
8074
+ credentials: resolved.credentials,
8075
+ saved,
8076
+ emitError
7999
8077
  });
8000
8078
  if (result.status === "fallback-to-interactive") {
8001
8079
  process.stderr.write(`${result.message}\n`);
@@ -8006,8 +8084,8 @@ async function runLoginCommand(args, deps) {
8006
8084
  authorizeUrl,
8007
8085
  mode: "interactive",
8008
8086
  credentials: null,
8009
- emitError,
8010
- deps
8087
+ saved,
8088
+ emitError
8011
8089
  });
8012
8090
  if (second.status === "exit") return exitAfterFlush(second.code);
8013
8091
  second.status;
@@ -8015,8 +8093,167 @@ async function runLoginCommand(args, deps) {
8015
8093
  }
8016
8094
  return exitAfterFlush(result.code);
8017
8095
  }
8096
+ /**
8097
+ * Resolve credentials and the requested save target for `aitcc login`.
8098
+ * Pure-ish: only side-effect is reading stdin via `deps.readStdin` (when
8099
+ * `--password-stdin` is set) and prompting via `deps.prompts` (when TTY).
8100
+ */
8101
+ async function resolveCredentialsForLogin(args, deps, opts = {}) {
8102
+ const env = opts.env ?? process.env;
8103
+ const stdoutIsTTY = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
8104
+ const stdinIsTTY = opts.stdinIsTTY ?? Boolean(process.stdin.isTTY);
8105
+ const interactiveTty = stdoutIsTTY && stdinIsTTY && !args.json;
8106
+ if (args.password !== void 0 && args.passwordStdin) return {
8107
+ kind: "error",
8108
+ reason: "conflicting-password-source",
8109
+ message: "--password and --password-stdin cannot be used together.",
8110
+ exitCode: ExitCode.Usage
8111
+ };
8112
+ if (args.interactive && (args.email !== void 0 || args.password !== void 0 || args.passwordStdin || args.save !== void 0)) return {
8113
+ kind: "error",
8114
+ reason: "conflicting-interactive-flags",
8115
+ 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.",
8116
+ exitCode: ExitCode.Usage
8117
+ };
8118
+ let saveTarget;
8119
+ if (args.save !== void 0) {
8120
+ if (args.save !== "keychain" && args.save !== "none") return {
8121
+ kind: "error",
8122
+ reason: "invalid-save",
8123
+ message: `--save must be "keychain" or "none" (got "${args.save}").`,
8124
+ exitCode: ExitCode.Usage
8125
+ };
8126
+ saveTarget = args.save;
8127
+ }
8128
+ if (args.email !== void 0 || args.password !== void 0 || args.passwordStdin) {
8129
+ if (args.email === void 0 || args.email.trim().length === 0) return {
8130
+ kind: "error",
8131
+ reason: "missing-email",
8132
+ message: "--email is required when --password / --password-stdin is passed.",
8133
+ exitCode: ExitCode.Usage
8134
+ };
8135
+ if (!args.email.includes("@")) return {
8136
+ kind: "error",
8137
+ reason: "invalid-email",
8138
+ message: `Invalid email: ${args.email}`,
8139
+ exitCode: ExitCode.Usage
8140
+ };
8141
+ let password;
8142
+ if (args.passwordStdin) {
8143
+ password = stripTrailingNewline(await (deps.readStdin ?? readStdinAll)());
8144
+ if (password.length === 0) return {
8145
+ kind: "error",
8146
+ reason: "invalid-password",
8147
+ message: "--password-stdin received an empty password on stdin.",
8148
+ exitCode: ExitCode.Usage
8149
+ };
8150
+ } else if (args.password !== void 0) {
8151
+ process.stderr.write("Warning: --password on argv is visible in `ps`/Task Manager. Prefer --password-stdin or the AITCC_PASSWORD environment variable.\n");
8152
+ password = args.password;
8153
+ if (password.length === 0) return {
8154
+ kind: "error",
8155
+ reason: "invalid-password",
8156
+ message: "--password value is empty.",
8157
+ exitCode: ExitCode.Usage
8158
+ };
8159
+ } else return {
8160
+ kind: "error",
8161
+ reason: "missing-password",
8162
+ message: "--email passed without a password. Add --password-stdin (recommended) or --password.",
8163
+ exitCode: ExitCode.Usage
8164
+ };
8165
+ return {
8166
+ kind: "ok",
8167
+ credentials: {
8168
+ source: "argv",
8169
+ email: args.email.trim(),
8170
+ password
8171
+ },
8172
+ saveTarget: saveTarget ?? "none"
8173
+ };
8174
+ }
8175
+ if (args.interactive) return {
8176
+ kind: "ok",
8177
+ credentials: null,
8178
+ saveTarget: saveTarget ?? "none"
8179
+ };
8180
+ if (env.AITCC_EMAIL && env.AITCC_PASSWORD) return {
8181
+ kind: "ok",
8182
+ credentials: {
8183
+ source: "env",
8184
+ email: env.AITCC_EMAIL,
8185
+ password: env.AITCC_PASSWORD
8186
+ },
8187
+ saveTarget: saveTarget ?? "none"
8188
+ };
8189
+ const getCredentials = deps.getCredentials;
8190
+ if (getCredentials) {
8191
+ const fromStore = await getCredentials().catch((err) => {
8192
+ process.stderr.write(`Credential lookup failed (${err.message}); ignoring.\n`);
8193
+ return null;
8194
+ });
8195
+ if (fromStore) {
8196
+ if (!args.json) process.stderr.write(`Using credentials from OS keychain for ${fromStore.email}. Pass --interactive to type a different account.
8197
+ `);
8198
+ return {
8199
+ kind: "ok",
8200
+ credentials: {
8201
+ source: fromStore.kind,
8202
+ email: fromStore.email,
8203
+ password: fromStore.password
8204
+ },
8205
+ saveTarget: saveTarget ?? "none"
8206
+ };
8207
+ }
8208
+ }
8209
+ if (interactiveTty) {
8210
+ const prompts = deps.prompts ?? defaultPromptDeps;
8211
+ let email;
8212
+ let password;
8213
+ try {
8214
+ email = await prompts.email();
8215
+ password = await prompts.password();
8216
+ } catch (err) {
8217
+ if (isPromptCancelled(err)) return {
8218
+ kind: "error",
8219
+ reason: "aborted",
8220
+ message: "Aborted.",
8221
+ exitCode: ExitCode.Usage
8222
+ };
8223
+ throw err;
8224
+ }
8225
+ let promptedSave;
8226
+ if (saveTarget !== void 0) promptedSave = saveTarget;
8227
+ else try {
8228
+ promptedSave = await prompts.saveTarget();
8229
+ } catch (err) {
8230
+ if (isPromptCancelled(err)) return {
8231
+ kind: "error",
8232
+ reason: "aborted",
8233
+ message: "Aborted.",
8234
+ exitCode: ExitCode.Usage
8235
+ };
8236
+ throw err;
8237
+ }
8238
+ return {
8239
+ kind: "ok",
8240
+ credentials: {
8241
+ source: "prompt",
8242
+ email,
8243
+ password
8244
+ },
8245
+ saveTarget: promptedSave
8246
+ };
8247
+ }
8248
+ return {
8249
+ kind: "error",
8250
+ reason: "interactive-required",
8251
+ message: "No credentials configured and stdin is not a TTY. Pass --email + --password-stdin (or set AITCC_EMAIL + AITCC_PASSWORD).",
8252
+ exitCode: ExitCode.Usage
8253
+ };
8254
+ }
8018
8255
  async function attemptLogin(opts) {
8019
- const { args, timeoutMs, endpointTimeoutMs, authorizeUrl, mode, credentials, emitError, deps } = opts;
8256
+ const { args, timeoutMs, endpointTimeoutMs, authorizeUrl, mode, credentials, saved, emitError } = opts;
8020
8257
  const launched = await launchChrome({
8021
8258
  initialUrl: authorizeUrl,
8022
8259
  endpointTimeoutMs,
@@ -8055,8 +8292,8 @@ async function attemptLogin(opts) {
8055
8292
  }
8056
8293
  if (mode === "interactive") process.stderr.write("Opened a browser window — complete the sign-in there. The CLI will capture the session automatically.\n");
8057
8294
  else {
8058
- const source = credentials?.kind === "env" ? "env" : "keychain";
8059
- process.stderr.write(`Signing in headlessly with credentials from ${source}…\n`);
8295
+ const sourceLabel = credentials?.source ?? "configured store";
8296
+ process.stderr.write(`Signing in headlessly with credentials from ${sourceLabel}…\n`);
8060
8297
  }
8061
8298
  let client = null;
8062
8299
  const disposeAll = async () => {
@@ -8223,57 +8460,29 @@ async function attemptLogin(opts) {
8223
8460
  capturedAt: session.capturedAt,
8224
8461
  cookieCount: cookies.length,
8225
8462
  mode,
8463
+ credentialSource: credentials?.source ?? "browser",
8464
+ saved,
8226
8465
  stepUp
8227
8466
  })}\n`);
8228
8467
  else process.stdout.write(`Logged in as ${user.name} <${user.email}>\n`);
8229
8468
  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
8469
  return {
8232
8470
  status: "exit",
8233
8471
  code: ExitCode.Ok
8234
8472
  };
8235
8473
  }
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
- }
8474
+ async function readStdinAll() {
8475
+ if (process.stdin.isTTY) throw new Error("--password-stdin requires stdin to be a pipe, not a TTY.");
8476
+ process.stdin.setEncoding("utf8");
8477
+ let buf = "";
8478
+ for await (const chunk of process.stdin) buf += chunk;
8479
+ return buf;
8480
+ }
8481
+ function stripTrailingNewline(s) {
8482
+ return s.replace(/\r?\n$/, "");
8483
+ }
8484
+ function isPromptCancelled(err) {
8485
+ return err instanceof Error && err.name === "ExitPromptError";
8277
8486
  }
8278
8487
  async function waitForLanding(client, sessionId, timeoutMs) {
8279
8488
  return await new Promise((resolve) => {
@@ -8337,18 +8546,25 @@ async function resolveUserWithRetry(cookies, opts = {}) {
8337
8546
  const logoutCommand = defineCommand({
8338
8547
  meta: {
8339
8548
  name: "logout",
8340
- description: "Delete the local session file."
8549
+ description: "Delete the local session file (and optionally the saved credentials)."
8550
+ },
8551
+ args: {
8552
+ json: {
8553
+ type: "boolean",
8554
+ description: "Emit machine-readable JSON to stdout.",
8555
+ default: false
8556
+ },
8557
+ purge: {
8558
+ type: "boolean",
8559
+ description: "Also delete saved keychain credentials and the auth-state pointer.",
8560
+ default: false
8561
+ }
8341
8562
  },
8342
- args: { json: {
8343
- type: "boolean",
8344
- description: "Emit machine-readable JSON to stdout.",
8345
- default: false
8346
- } },
8347
8563
  async run({ args }) {
8348
8564
  const path = sessionPathForDiagnostics();
8349
- let existed;
8565
+ let sessionRemoved;
8350
8566
  try {
8351
- existed = (await clearSession()).existed;
8567
+ sessionRemoved = (await clearSession()).existed;
8352
8568
  } catch (err) {
8353
8569
  const message = err.message;
8354
8570
  if (args.json) process.stdout.write(`${JSON.stringify({
@@ -8360,13 +8576,29 @@ const logoutCommand = defineCommand({
8360
8576
  process.stderr.write(`Failed to remove session file at ${path}: ${message}\n`);
8361
8577
  return exitAfterFlush(ExitCode.Generic);
8362
8578
  }
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`);
8579
+ let credentialsPurged = false;
8580
+ let purgeError = null;
8581
+ if (args.purge) try {
8582
+ credentialsPurged = (await deleteCredentials()).existed;
8583
+ } catch (err) {
8584
+ purgeError = err.message;
8585
+ }
8586
+ if (args.json) {
8587
+ const payload = {
8588
+ ok: true,
8589
+ sessionRemoved,
8590
+ credentialsPurged,
8591
+ path
8592
+ };
8593
+ if (purgeError !== null) payload.purgeError = purgeError;
8594
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
8595
+ } else {
8596
+ if (sessionRemoved) process.stdout.write(`Logged out. Session removed from ${path}\n`);
8597
+ else process.stdout.write(`No active session at ${path}.\n`);
8598
+ if (args.purge) if (purgeError !== null) process.stderr.write(`Could not delete saved credentials: ${purgeError}\n`);
8599
+ else if (credentialsPurged) process.stdout.write("Saved credentials deleted from the OS keychain.\n");
8600
+ else process.stdout.write("No saved credentials to delete.\n");
8601
+ }
8370
8602
  return exitAfterFlush(ExitCode.Ok);
8371
8603
  }
8372
8604
  });
@@ -8901,7 +9133,7 @@ function resolveVersion() {
8901
9133
  if (typeof injected === "string" && injected.length > 0) return injected;
8902
9134
  } catch {}
8903
9135
  try {
8904
- return "0.1.24";
9136
+ return "0.1.25";
8905
9137
  } catch {}
8906
9138
  return "0.0.0-dev";
8907
9139
  }
@@ -9273,6 +9505,22 @@ function maybeEmitNotice(entry, env) {
9273
9505
  }
9274
9506
  //#endregion
9275
9507
  //#region src/commands/whoami.ts
9508
+ async function describeCredentialSource() {
9509
+ const active = await getActiveCredentialEmail().catch(() => null);
9510
+ if (!active) return {
9511
+ source: "none",
9512
+ email: null
9513
+ };
9514
+ return {
9515
+ source: active.kind,
9516
+ email: active.email
9517
+ };
9518
+ }
9519
+ function formatCredentials(cred) {
9520
+ if (cred.source === "none") return "none (run `aitcc login` to save)";
9521
+ if (cred.source === "env") return `env (AITCC_EMAIL${cred.email ? ` = ${cred.email}` : ""})`;
9522
+ return `keychain${cred.email ? ` (${cred.email})` : ""}`;
9523
+ }
9276
9524
  async function runBackgroundUpdateCheck(json) {
9277
9525
  if (json) return;
9278
9526
  const timeoutMs = 500;
@@ -9300,14 +9548,18 @@ const whoamiCommand = defineCommand({
9300
9548
  },
9301
9549
  async run({ args }) {
9302
9550
  const session = await readSession();
9551
+ const cred = await describeCredentialSource();
9303
9552
  if (!session) {
9304
9553
  if (args.json) process.stdout.write(`${JSON.stringify({
9305
9554
  ok: true,
9306
- authenticated: false
9555
+ authenticated: false,
9556
+ credentialSource: cred.source,
9557
+ ...cred.email ? { credentialEmail: cred.email } : {}
9307
9558
  })}\n`);
9308
9559
  else {
9309
9560
  process.stderr.write("Not logged in. Run `aitcc login` to start a session.\n");
9310
9561
  process.stderr.write(`Session file checked: ${sessionPathForDiagnostics()}\n`);
9562
+ process.stderr.write(`Credentials: ${formatCredentials(cred)}\n`);
9311
9563
  }
9312
9564
  return exitAfterFlush(ExitCode.NotAuthenticated);
9313
9565
  }
@@ -9318,12 +9570,15 @@ const whoamiCommand = defineCommand({
9318
9570
  authenticated: true,
9319
9571
  source: "cache",
9320
9572
  user: session.user,
9321
- capturedAt: session.capturedAt
9573
+ capturedAt: session.capturedAt,
9574
+ credentialSource: cred.source,
9575
+ ...cred.email ? { credentialEmail: cred.email } : {}
9322
9576
  })}\n`);
9323
9577
  return exitAfterFlush(ExitCode.Ok);
9324
9578
  }
9325
9579
  const label = session.user.displayName ? `${session.user.displayName} <${session.user.email}>` : session.user.email;
9326
9580
  process.stdout.write(`Logged in as ${label} (cached)\n`);
9581
+ process.stdout.write(`Credentials: ${formatCredentials(cred)}\n`);
9327
9582
  process.stdout.write(`Session captured: ${session.capturedAt}\n`);
9328
9583
  await runBackgroundUpdateCheck(args.json);
9329
9584
  return exitAfterFlush(ExitCode.Ok);
@@ -9347,11 +9602,14 @@ const whoamiCommand = defineCommand({
9347
9602
  workspaceName: w.workspaceName,
9348
9603
  role: w.role
9349
9604
  })),
9350
- capturedAt: session.capturedAt
9605
+ capturedAt: session.capturedAt,
9606
+ credentialSource: cred.source,
9607
+ ...cred.email ? { credentialEmail: cred.email } : {}
9351
9608
  })}\n`);
9352
9609
  return exitAfterFlush(ExitCode.Ok);
9353
9610
  }
9354
9611
  process.stdout.write(`Logged in as ${info.name} <${info.email}> (${info.role})\n`);
9612
+ process.stdout.write(`Credentials: ${formatCredentials(cred)}\n`);
9355
9613
  if (info.workspaces.length > 0) {
9356
9614
  process.stdout.write("Workspaces:\n");
9357
9615
  for (const w of info.workspaces) process.stdout.write(` - ${w.workspaceName} (id ${w.workspaceId}, ${w.role})\n`);
@@ -9364,9 +9622,14 @@ const whoamiCommand = defineCommand({
9364
9622
  ok: true,
9365
9623
  authenticated: false,
9366
9624
  reason: "session-expired",
9367
- errorCode: err.errorCode
9625
+ errorCode: err.errorCode,
9626
+ credentialSource: cred.source,
9627
+ ...cred.email ? { credentialEmail: cred.email } : {}
9368
9628
  })}\n`);
9369
- else process.stderr.write("Session is no longer valid. Run `aitcc login` again.\n");
9629
+ else {
9630
+ process.stderr.write("Session is no longer valid. Run `aitcc login` again.\n");
9631
+ process.stderr.write(`Credentials: ${formatCredentials(cred)}\n`);
9632
+ }
9370
9633
  return exitAfterFlush(ExitCode.NotAuthenticated);
9371
9634
  }
9372
9635
  if (err instanceof NetworkError) {