@ait-co/console-cli 0.1.37 → 0.1.38

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
@@ -5851,28 +5851,6 @@ const appCommand = defineCommand({
5851
5851
  //#endregion
5852
5852
  //#region src/auth/backend.ts
5853
5853
  const CREDENTIAL_SERVICE = "aitcc.credentials";
5854
- var CredentialBackendUnsupportedError = class extends Error {
5855
- platform;
5856
- hint;
5857
- constructor(platform, hint) {
5858
- super(`No supported credential backend for platform "${platform}". ${hint}`);
5859
- this.platform = platform;
5860
- this.hint = hint;
5861
- this.name = "CredentialBackendUnsupportedError";
5862
- }
5863
- };
5864
- var CredentialBackendCommandError = class extends Error {
5865
- command;
5866
- exitCode;
5867
- redactedStderr;
5868
- constructor(command, exitCode, redactedStderr) {
5869
- super(`Credential backend command "${command}" failed (exit=${exitCode ?? "null"}): ${redactedStderr}`);
5870
- this.command = command;
5871
- this.exitCode = exitCode;
5872
- this.redactedStderr = redactedStderr;
5873
- this.name = "CredentialBackendCommandError";
5874
- }
5875
- };
5876
5854
  async function runCommand(command, opts) {
5877
5855
  return new Promise((resolve, reject) => {
5878
5856
  const child = spawn(command, [...opts.args], { stdio: [
@@ -5906,12 +5884,6 @@ function isCommandNotFound(err) {
5906
5884
  function stripTrailingNewline$1(s) {
5907
5885
  return s.replace(/\r?\n$/, "");
5908
5886
  }
5909
- function redactStderr(stderr) {
5910
- const trimmed = stderr.trim();
5911
- if (trimmed.length === 0) return "<no stderr>";
5912
- if (trimmed.length > 200) return `${trimmed.slice(0, 200)}… <truncated>`;
5913
- return trimmed;
5914
- }
5915
5887
  //#endregion
5916
5888
  //#region src/auth/backends/file.ts
5917
5889
  function credentialFilePath() {
@@ -5981,283 +5953,73 @@ const FILE_BACKEND = {
5981
5953
  }
5982
5954
  };
5983
5955
  //#endregion
5984
- //#region src/auth/backends/linux.ts
5985
- const MISSING_HINT_FULL$1 = "libsecret tools are missing. Install `libsecret-tools` (Debian/Ubuntu) or the equivalent and ensure a Secret Service provider (gnome-keyring / KWallet) is running.";
5986
- const MISSING_HINT_SHORT$1 = "libsecret tools are missing. Install `libsecret-tools` and ensure a Secret Service provider is running.";
5987
- const LINUX_BACKEND = {
5988
- name: "libsecret",
5989
- async get(account) {
5990
- let result;
5991
- try {
5992
- result = await runCommand("secret-tool", { args: [
5993
- "lookup",
5994
- "service",
5995
- CREDENTIAL_SERVICE,
5996
- "account",
5997
- account
5998
- ] });
5999
- } catch (err) {
6000
- if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("linux", MISSING_HINT_FULL$1);
6001
- throw err;
6002
- }
6003
- if (result.exitCode !== 0) {
6004
- if (result.stdout.length === 0) return null;
6005
- throw new CredentialBackendCommandError("secret-tool lookup", result.exitCode, redactStderr(result.stderr));
6006
- }
6007
- if (result.stdout.length === 0) return null;
6008
- const password = stripTrailingNewline$1(result.stdout);
6009
- return password.length > 0 ? password : null;
6010
- },
6011
- async set(account, password) {
6012
- let result;
6013
- try {
6014
- result = await runCommand("secret-tool", {
6015
- args: [
6016
- "store",
6017
- "--label",
6018
- "aitcc Toss Business credentials",
6019
- "service",
6020
- CREDENTIAL_SERVICE,
6021
- "account",
6022
- account
6023
- ],
6024
- stdin: password
6025
- });
6026
- } catch (err) {
6027
- if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("linux", MISSING_HINT_SHORT$1);
6028
- throw err;
6029
- }
6030
- if (result.exitCode !== 0) throw new CredentialBackendCommandError("secret-tool store", result.exitCode, redactStderr(result.stderr));
6031
- },
6032
- async clear(account) {
6033
- let result;
6034
- try {
6035
- result = await runCommand("secret-tool", { args: [
6036
- "clear",
6037
- "service",
6038
- CREDENTIAL_SERVICE,
6039
- "account",
6040
- account
6041
- ] });
6042
- } catch (err) {
6043
- if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("linux", "libsecret tools are missing.");
6044
- throw err;
6045
- }
6046
- if (result.exitCode !== 0) throw new CredentialBackendCommandError("secret-tool clear", result.exitCode, redactStderr(result.stderr));
6047
- return { existed: true };
6048
- }
6049
- };
6050
- //#endregion
6051
- //#region src/auth/backends/macos.ts
6052
- const MISSING_HINT = "macOS `security` is missing from PATH.";
6053
- const MACOS_BACKEND = {
6054
- name: "macos-keychain",
6055
- async get(account) {
6056
- let result;
6057
- try {
6058
- result = await runCommand("security", { args: [
6059
- "find-generic-password",
6060
- "-s",
6061
- CREDENTIAL_SERVICE,
6062
- "-a",
6063
- account,
6064
- "-w"
6065
- ] });
6066
- } catch (err) {
6067
- if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("darwin", MISSING_HINT);
6068
- throw err;
6069
- }
6070
- if (result.exitCode === 44) return null;
6071
- if (result.exitCode !== 0) return null;
6072
- const password = stripTrailingNewline$1(result.stdout);
6073
- return password.length > 0 ? password : null;
6074
- },
6075
- async set(account, password) {
6076
- let result;
6077
- try {
6078
- result = await runCommand("security", { args: [
6079
- "add-generic-password",
6080
- "-U",
6081
- "-A",
6082
- "-s",
6083
- CREDENTIAL_SERVICE,
6084
- "-a",
6085
- account,
6086
- "-w",
6087
- password
6088
- ] });
6089
- } catch (err) {
6090
- if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("darwin", MISSING_HINT);
6091
- throw err;
6092
- }
6093
- if (result.exitCode !== 0) throw new CredentialBackendCommandError("security add-generic-password", result.exitCode, redactStderr(result.stderr));
6094
- },
6095
- async clear(account) {
6096
- let result;
6097
- try {
6098
- result = await runCommand("security", { args: [
6099
- "delete-generic-password",
6100
- "-s",
6101
- CREDENTIAL_SERVICE,
6102
- "-a",
6103
- account
6104
- ] });
6105
- } catch (err) {
6106
- if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("darwin", MISSING_HINT);
6107
- throw err;
6108
- }
6109
- if (result.exitCode === 44) return { existed: false };
6110
- if (result.exitCode === 0) return { existed: true };
6111
- throw new CredentialBackendCommandError("security delete-generic-password", result.exitCode, redactStderr(result.stderr));
5956
+ //#region src/auth/keychain-migration.ts
5957
+ /**
5958
+ * Attempt a one-time migration of a macOS Keychain credential entry to the
5959
+ * file backend. Silent on failure — the caller is expected to fall through
5960
+ * to "no credentials found" if migration is not possible.
5961
+ *
5962
+ * @param email The email (= keychain account) to look up.
5963
+ */
5964
+ async function migrateKeychainToFileIfNeeded(email) {
5965
+ if (process.platform !== "darwin") return {
5966
+ migrated: false,
5967
+ reason: "non-darwin platform — skipping"
5968
+ };
5969
+ let result;
5970
+ try {
5971
+ result = await runCommand("security", { args: [
5972
+ "find-generic-password",
5973
+ "-s",
5974
+ CREDENTIAL_SERVICE,
5975
+ "-a",
5976
+ email,
5977
+ "-w"
5978
+ ] });
5979
+ } catch (err) {
5980
+ if (isCommandNotFound(err)) return {
5981
+ migrated: false,
5982
+ reason: "`security` not found"
5983
+ };
5984
+ return {
5985
+ migrated: false,
5986
+ reason: err.message
5987
+ };
6112
5988
  }
6113
- };
6114
- //#endregion
6115
- //#region src/auth/backends/windows.ts
6116
- const PS_HEADER = `
6117
- $ErrorActionPreference = 'Stop';
6118
- Add-Type @"
6119
- using System;
6120
- using System.Runtime.InteropServices;
6121
-
6122
- public class AitccCredApi {
6123
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
6124
- public struct CREDENTIAL {
6125
- public uint Flags;
6126
- public uint Type;
6127
- public IntPtr TargetName;
6128
- public IntPtr Comment;
6129
- public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
6130
- public uint CredentialBlobSize;
6131
- public IntPtr CredentialBlob;
6132
- public uint Persist;
6133
- public uint AttributeCount;
6134
- public IntPtr Attributes;
6135
- public IntPtr TargetAlias;
6136
- public IntPtr UserName;
6137
- }
6138
- [DllImport("Advapi32.dll", SetLastError = true, EntryPoint = "CredWriteW", CharSet = CharSet.Unicode)]
6139
- public static extern bool CredWrite([In] ref CREDENTIAL Credential, [In] uint Flags);
6140
- [DllImport("Advapi32.dll", SetLastError = true, EntryPoint = "CredReadW", CharSet = CharSet.Unicode)]
6141
- public static extern bool CredRead(string target, uint type, uint reservedFlag, out IntPtr CredentialPtr);
6142
- [DllImport("Advapi32.dll", SetLastError = true, EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode)]
6143
- public static extern bool CredDelete(string target, uint type, uint flags);
6144
- [DllImport("Advapi32.dll", SetLastError = true, EntryPoint = "CredFree")]
6145
- public static extern void CredFree([In] IntPtr cred);
6146
- }
6147
- "@
6148
- `;
6149
- const MISSING_HINT_FULL = "`powershell.exe` is missing from PATH. Windows credential storage requires PowerShell.";
6150
- const MISSING_HINT_SHORT = "`powershell.exe` is missing from PATH.";
6151
- function targetName(account) {
6152
- return `${CREDENTIAL_SERVICE}/${account}`;
6153
- }
6154
- function powerShellArgs(script) {
6155
- return [
6156
- "-NoProfile",
6157
- "-NonInteractive",
6158
- "-Command",
6159
- script
6160
- ];
6161
- }
6162
- function escapeSingleQuotes(s) {
6163
- return s.replace(/'/g, "''");
6164
- }
6165
- async function runPowerShell(script) {
5989
+ if (result.exitCode !== 0) return {
5990
+ migrated: false,
5991
+ reason: `security exited ${result.exitCode ?? "null"}`
5992
+ };
5993
+ const password = stripTrailingNewline$1(result.stdout);
5994
+ if (password.length === 0) return {
5995
+ migrated: false,
5996
+ reason: "empty password in keychain entry"
5997
+ };
6166
5998
  try {
6167
- return await runCommand("powershell.exe", { args: powerShellArgs(script) });
5999
+ await FILE_BACKEND.set(email, password);
6168
6000
  } catch (err) {
6169
- if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("win32", MISSING_HINT_FULL);
6170
- throw err;
6001
+ return {
6002
+ migrated: false,
6003
+ reason: `file write failed: ${err.message}`
6004
+ };
6171
6005
  }
6006
+ try {
6007
+ await runCommand("security", { args: [
6008
+ "delete-generic-password",
6009
+ "-s",
6010
+ CREDENTIAL_SERVICE,
6011
+ "-a",
6012
+ email
6013
+ ] });
6014
+ } catch {}
6015
+ process.stderr.write(`기존 keychain 자격증명을 ~/.config/aitcc/credentials.json으로 이전했습니다.\n`);
6016
+ return { migrated: true };
6172
6017
  }
6173
- const WINDOWS_BACKEND = {
6174
- name: "windows-credential-manager",
6175
- async get(account) {
6176
- const result = await runPowerShell(`
6177
- ${PS_HEADER}
6178
- $target = '${escapeSingleQuotes(targetName(account))}';
6179
- $ptr = [IntPtr]::Zero;
6180
- $ok = [AitccCredApi]::CredRead($target, 1, 0, [ref]$ptr);
6181
- if (-not $ok) { exit 0; }
6182
- $cred = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [Type][AitccCredApi+CREDENTIAL]);
6183
- $blob = New-Object byte[] $cred.CredentialBlobSize;
6184
- [Runtime.InteropServices.Marshal]::Copy($cred.CredentialBlob, $blob, 0, $cred.CredentialBlobSize);
6185
- $pw = [System.Text.Encoding]::Unicode.GetString($blob);
6186
- [AitccCredApi]::CredFree($ptr);
6187
- [Console]::Out.Write($pw);
6188
- `);
6189
- if (result.exitCode !== 0) throw new CredentialBackendCommandError("powershell CredRead", result.exitCode, redactStderr(result.stderr));
6190
- return result.stdout.length > 0 ? result.stdout : null;
6191
- },
6192
- async set(account, password) {
6193
- const target = targetName(account);
6194
- const passwordHex = Buffer.from(password, "utf8").toString("hex");
6195
- const script = `
6196
- ${PS_HEADER}
6197
- $target = '${escapeSingleQuotes(target)}';
6198
- $user = '${escapeSingleQuotes(account)}';
6199
- $pwHex = '${passwordHex}';
6200
- $pwBytes = New-Object byte[] ($pwHex.Length / 2);
6201
- for ($i = 0; $i -lt $pwBytes.Length; $i++) {
6202
- $pwBytes[$i] = [Convert]::ToByte($pwHex.Substring($i * 2, 2), 16);
6203
- }
6204
- $pwUtf16 = [System.Text.Encoding]::Unicode.GetBytes([System.Text.Encoding]::UTF8.GetString($pwBytes));
6205
- $cred = New-Object AitccCredApi+CREDENTIAL;
6206
- $cred.Type = 1;
6207
- $cred.TargetName = [Runtime.InteropServices.Marshal]::StringToHGlobalUni($target);
6208
- $cred.CredentialBlobSize = [uint32]$pwUtf16.Length;
6209
- $cred.CredentialBlob = [Runtime.InteropServices.Marshal]::AllocHGlobal($pwUtf16.Length);
6210
- [Runtime.InteropServices.Marshal]::Copy($pwUtf16, 0, $cred.CredentialBlob, $pwUtf16.Length);
6211
- $cred.Persist = 2;
6212
- $cred.UserName = [Runtime.InteropServices.Marshal]::StringToHGlobalUni($user);
6213
- try {
6214
- $ok = [AitccCredApi]::CredWrite([ref]$cred, 0);
6215
- if (-not $ok) { Write-Error 'CredWrite failed'; exit 1; }
6216
- } finally {
6217
- [Runtime.InteropServices.Marshal]::FreeHGlobal($cred.TargetName);
6218
- [Runtime.InteropServices.Marshal]::FreeHGlobal($cred.UserName);
6219
- [Runtime.InteropServices.Marshal]::FreeHGlobal($cred.CredentialBlob);
6220
- }
6221
- `;
6222
- let result;
6223
- try {
6224
- result = await runPowerShell(script);
6225
- } catch (err) {
6226
- if (err instanceof CredentialBackendUnsupportedError) throw new CredentialBackendUnsupportedError("win32", MISSING_HINT_SHORT);
6227
- throw err;
6228
- }
6229
- if (result.exitCode !== 0) throw new CredentialBackendCommandError("powershell CredWrite", result.exitCode, redactStderr(result.stderr));
6230
- },
6231
- async clear(account) {
6232
- const script = `
6233
- ${PS_HEADER}
6234
- $target = '${escapeSingleQuotes(targetName(account))}';
6235
- $ok = [AitccCredApi]::CredDelete($target, 1, 0);
6236
- if ($ok) { [Console]::Out.Write('deleted'); } else { [Console]::Out.Write('absent'); }
6237
- `;
6238
- let result;
6239
- try {
6240
- result = await runPowerShell(script);
6241
- } catch (err) {
6242
- if (err instanceof CredentialBackendUnsupportedError) throw new CredentialBackendUnsupportedError("win32", MISSING_HINT_SHORT);
6243
- throw err;
6244
- }
6245
- if (result.exitCode !== 0) throw new CredentialBackendCommandError("powershell CredDelete", result.exitCode, redactStderr(result.stderr));
6246
- return { existed: result.stdout.includes("deleted") };
6247
- }
6248
- };
6249
6018
  //#endregion
6250
6019
  //#region src/auth/credentials.ts
6251
6020
  function resolveBackend(opts = {}) {
6252
6021
  if (opts.override) return opts.override;
6253
- if (opts.useFile === true || process.env.AITCC_CREDENTIAL_BACKEND === "file") return FILE_BACKEND;
6254
- const platform = opts.platform ?? process.platform;
6255
- switch (platform) {
6256
- case "darwin": return MACOS_BACKEND;
6257
- case "linux": return LINUX_BACKEND;
6258
- case "win32": return WINDOWS_BACKEND;
6259
- default: throw new CredentialBackendUnsupportedError(platform, "Only macOS, Linux (libsecret), and Windows are supported.");
6260
- }
6022
+ return FILE_BACKEND;
6261
6023
  }
6262
6024
  async function readAuthState() {
6263
6025
  let raw;
@@ -6302,9 +6064,8 @@ async function clearAuthState() {
6302
6064
  /**
6303
6065
  * Resolve credentials from the highest-priority source available:
6304
6066
  * 1. `AITCC_EMAIL` + `AITCC_PASSWORD` env vars (CI single-shot use).
6305
- * 2. File backend (`~/.config/aitcc/credentials.json`) when
6306
- * `AITCC_CREDENTIAL_BACKEND=file` is set or `opts.useFile` is true.
6307
- * 3. OS keychain entry whose email is recorded in `auth-state.json`.
6067
+ * 2. File backend (`~/.config/aitcc/credentials.json`) — the only
6068
+ * persistent store; email pointer lives in `auth-state.json`.
6308
6069
  *
6309
6070
  * Returns `null` when no source is configured. The discriminated `kind`
6310
6071
  * lets callers (e.g. the login flow) tell why a credential was found
@@ -6323,19 +6084,23 @@ async function loadCredentials(opts = {}) {
6323
6084
  const state = await readAuthState();
6324
6085
  if (!state) return null;
6325
6086
  const backend = resolveBackend(opts);
6326
- const password = await backend.get(state.activeEmail);
6087
+ let password = await backend.get(state.activeEmail);
6088
+ if (password === null) {
6089
+ await migrateKeychainToFileIfNeeded(state.activeEmail).catch(() => null);
6090
+ password = await backend.get(state.activeEmail);
6091
+ }
6327
6092
  if (password === null) return null;
6328
6093
  return {
6329
- kind: backend.name === "file" ? "file" : "keychain",
6094
+ kind: "file",
6330
6095
  email: state.activeEmail,
6331
6096
  password
6332
6097
  };
6333
6098
  }
6334
6099
  /**
6335
- * Persist credentials to the OS keychain and update the active-email
6336
- * pointer. Returns `'unchanged'` (no keychain write) when the same email
6337
- * + password is already stored — avoids triggering OS keychain prompts on
6338
- * every call when the user re-runs `auth set` with the same input.
6100
+ * Persist credentials to the file backend and update the active-email
6101
+ * pointer. Returns `'unchanged'` (no file write) when the same email
6102
+ * + password is already stored — avoids unnecessary disk writes on every
6103
+ * call when the user re-runs `login` with the same input.
6339
6104
  */
6340
6105
  async function saveCredentials(email, password, opts = {}) {
6341
6106
  if (!email) throw new Error("email is required");
@@ -6358,15 +6123,13 @@ async function saveCredentials(email, password, opts = {}) {
6358
6123
  return { status };
6359
6124
  }
6360
6125
  /**
6361
- * Read just the active-email pointer without touching the OS keychain.
6362
- * Useful for surfaces like `auth status --json` that want to report
6363
- * whether credentials are configured without triggering a Touch ID /
6364
- * libsecret prompt for the password.
6126
+ * Read just the active-email pointer without loading the password from disk.
6127
+ * Useful for surfaces like `whoami` that want to report whether credentials
6128
+ * are configured without performing a full credential read.
6365
6129
  *
6366
6130
  * Returns the email and where it was found (`'env'` when
6367
- * `AITCC_EMAIL` + `AITCC_PASSWORD` are present, `'keychain'` when the
6368
- * `auth-state.json` pointer exists), or `null` when nothing is
6369
- * configured.
6131
+ * `AITCC_EMAIL` + `AITCC_PASSWORD` are present, `'file'` when the
6132
+ * `auth-state.json` pointer exists), or `null` when nothing is configured.
6370
6133
  */
6371
6134
  async function getActiveCredentialEmail(opts = {}) {
6372
6135
  const env = opts.env ?? process.env;
@@ -6377,22 +6140,18 @@ async function getActiveCredentialEmail(opts = {}) {
6377
6140
  const state = await readAuthState();
6378
6141
  if (!state) return null;
6379
6142
  return {
6380
- kind: env.AITCC_CREDENTIAL_BACKEND === "file" ? "file" : "keychain",
6143
+ kind: "file",
6381
6144
  email: state.activeEmail
6382
6145
  };
6383
6146
  }
6384
6147
  /**
6385
- * Remove the keychain entry and the auth-state pointer. Returns
6148
+ * Remove the file credential entry and the auth-state pointer. Returns
6386
6149
  * `existed: true` if either side previously held data.
6387
6150
  */
6388
6151
  async function deleteCredentials(opts = {}) {
6389
6152
  const state = await readAuthState();
6390
6153
  let backendExisted = false;
6391
- if (state) try {
6392
- backendExisted = (await resolveBackend(opts).clear(state.activeEmail)).existed;
6393
- } catch (err) {
6394
- if (err instanceof CredentialBackendUnsupportedError) {} else throw err;
6395
- }
6154
+ if (state) backendExisted = (await resolveBackend(opts).clear(state.activeEmail)).existed;
6396
6155
  const stateResult = await clearAuthState();
6397
6156
  return { existed: backendExisted || stateResult.existed };
6398
6157
  }
@@ -6680,7 +6439,7 @@ async function runAuthSet(args, deps = {}) {
6680
6439
  const message = err.message;
6681
6440
  if (args.json) emitJson({
6682
6441
  ok: false,
6683
- reason: "keychain-error",
6442
+ reason: "save-error",
6684
6443
  message
6685
6444
  });
6686
6445
  else process.stderr.write(`Failed to save credentials: ${message}\n`);
@@ -6692,7 +6451,7 @@ async function runAuthSet(args, deps = {}) {
6692
6451
  email
6693
6452
  });
6694
6453
  else if (result.status === "unchanged") process.stdout.write("Credentials already saved (no change).\n");
6695
- else process.stdout.write(`Credentials saved for ${email} (keychain).\n`);
6454
+ else process.stdout.write(`Credentials saved for ${email}.\n`);
6696
6455
  return exitAfterFlush(ExitCode.Ok);
6697
6456
  }
6698
6457
  async function runAuthClear(args, deps = {}) {
@@ -6739,7 +6498,7 @@ async function runAuthClear(args, deps = {}) {
6739
6498
  const message = err.message;
6740
6499
  if (args.json) emitJson({
6741
6500
  ok: false,
6742
- reason: "keychain-error",
6501
+ reason: "clear-error",
6743
6502
  message
6744
6503
  });
6745
6504
  else process.stderr.write(`Failed to clear credentials: ${message}\n`);
@@ -6775,7 +6534,7 @@ async function runAuthStatus(args, deps = {}) {
6775
6534
  return exitAfterFlush(ExitCode.Ok);
6776
6535
  }
6777
6536
  if (active) {
6778
- const sourceLabel = active.kind === "env" ? "environment (AITCC_EMAIL/PASSWORD)" : "keychain";
6537
+ const sourceLabel = active.kind === "env" ? "environment (AITCC_EMAIL/PASSWORD)" : "file";
6779
6538
  process.stdout.write(`Email: ${active.email}\n`);
6780
6539
  process.stdout.write(`Source: ${sourceLabel}\n`);
6781
6540
  } else process.stdout.write("Email: (not configured)\n");
@@ -8163,21 +7922,14 @@ const defaultPromptDeps = {
8163
7922
  }),
8164
7923
  saveTarget: () => select({
8165
7924
  message: "Where would you like to save the credentials?",
8166
- default: "keychain",
8167
- choices: [
8168
- {
8169
- name: "OS keychain (recommended) — next login runs headlessly",
8170
- value: "keychain"
8171
- },
8172
- {
8173
- name: "File (~/.config/aitcc/credentials.json, perm 0600) — use for SSH/headless sessions",
8174
- value: "file"
8175
- },
8176
- {
8177
- name: "Do not save — one-shot. (Tip: AITCC_EMAIL/AITCC_PASSWORD env for CI.)",
8178
- value: "none"
8179
- }
8180
- ]
7925
+ default: "file",
7926
+ choices: [{
7927
+ name: "File (~/.config/aitcc/credentials.json, perm 0600) — next login runs headlessly",
7928
+ value: "file"
7929
+ }, {
7930
+ name: "Do not save — one-shot. (Tip: AITCC_EMAIL/AITCC_PASSWORD env for CI.)",
7931
+ value: "none"
7932
+ }]
8181
7933
  })
8182
7934
  };
8183
7935
  const loginCommand = defineCommand({
@@ -8216,7 +7968,7 @@ const loginCommand = defineCommand({
8216
7968
  },
8217
7969
  save: {
8218
7970
  type: "string",
8219
- description: "Where to persist credentials when --email/--password* are passed: \"keychain\", \"file\", or \"none\" (default). Use \"file\" for SSH/headless sessions where the OS keychain is unavailable."
7971
+ description: "Where to persist credentials when --email/--password* are passed: \"file\" or \"none\" (default). Credentials are stored in ~/.config/aitcc/credentials.json (perm 0600)."
8220
7972
  },
8221
7973
  "skip-onboarding": {
8222
7974
  type: "boolean",
@@ -8235,7 +7987,7 @@ const loginCommand = defineCommand({
8235
7987
  save: typeof args.save === "string" ? args.save : void 0
8236
7988
  }, {
8237
7989
  getCredentials: loadCredentials,
8238
- saveCredentials: (email, password, useFile) => saveCredentials(email, password, { useFile: useFile === true })
7990
+ saveCredentials: (email, password) => saveCredentials(email, password)
8239
7991
  });
8240
7992
  }
8241
7993
  });
@@ -8285,8 +8037,7 @@ async function runLoginCommand(args, deps) {
8285
8037
  return exitAfterFlush(resolved.exitCode);
8286
8038
  }
8287
8039
  let saved = "skipped";
8288
- if ((resolved.saveTarget === "keychain" || resolved.saveTarget === "file") && resolved.credentials !== null) {
8289
- const useFile = resolved.saveTarget === "file";
8040
+ if (resolved.saveTarget === "file" && resolved.credentials !== null) {
8290
8041
  const save = deps.saveCredentials;
8291
8042
  if (!save) {
8292
8043
  emitError({
@@ -8296,22 +8047,13 @@ async function runLoginCommand(args, deps) {
8296
8047
  return exitAfterFlush(ExitCode.Generic);
8297
8048
  }
8298
8049
  try {
8299
- const result = await save(resolved.credentials.email, resolved.credentials.password, useFile);
8050
+ const result = await save(resolved.credentials.email, resolved.credentials.password);
8300
8051
  saved = result.status;
8301
8052
  if (!args.json) if (result.status === "unchanged") process.stderr.write("Credentials already saved (no change).\n");
8302
- else if (useFile) process.stderr.write(`Credentials saved to file backend (${resolved.credentials.email}).\n`);
8303
- else process.stderr.write(`Credentials saved to OS keychain (${resolved.credentials.email}).\n`);
8053
+ else process.stderr.write(`Credentials saved to ~/.config/aitcc/credentials.json (${resolved.credentials.email}).\n`);
8304
8054
  } catch (err) {
8305
8055
  const message = err.message;
8306
- if (!useFile && process.platform === "darwin") emitError({
8307
- reason: "keychain-save-failed",
8308
- message
8309
- }, "keychain 접근에 실패했습니다.\nSSH/headless 세션이면 다음 중 하나를 시도하세요:\n 1) 데스크톱 GUI Mac에서: aitcc auth export --format=env (KR IP 필요)\n SSH에서: AITCC_SESSION='...' aitcc auth import --from-env\n 2) 같은 SSH 세션에서 keychain unlock:\n security unlock-keychain ~/Library/Keychains/login.keychain-db\n (login 비밀번호 입력 후 재시도)\n 3) keychain 대신 파일 저장:\n aitcc login --save=file (~/.config/aitcc/credentials.json, perm 0600)\n참고: https://github.com/apps-in-toss-community/console-cli/issues/176");
8310
- else if (!useFile) emitError({
8311
- reason: "keychain-save-failed",
8312
- message
8313
- }, `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.`);
8314
- else emitError({
8056
+ emitError({
8315
8057
  reason: "file-save-failed",
8316
8058
  message
8317
8059
  }, `Failed to save credentials to file backend: ${message}\nCheck that ~/.config/aitcc/ is writable, or use AITCC_CREDENTIAL_FILE to specify a custom path.`);
@@ -8375,15 +8117,16 @@ async function resolveCredentialsForLogin(args, deps, opts = {}) {
8375
8117
  exitCode: ExitCode.Usage
8376
8118
  };
8377
8119
  let saveTarget;
8378
- if (args.save !== void 0) {
8379
- if (args.save !== "keychain" && args.save !== "file" && args.save !== "none") return {
8380
- kind: "error",
8381
- reason: "invalid-save",
8382
- message: `--save must be "keychain", "file", or "none" (got "${args.save}").`,
8383
- exitCode: ExitCode.Usage
8384
- };
8385
- saveTarget = args.save;
8386
- }
8120
+ if (args.save !== void 0) if (args.save === "keychain") {
8121
+ process.stderr.write("Warning: --save=keychain is deprecated. OS keychain support has been removed. Using --save=file instead (~/.config/aitcc/credentials.json, perm 0600).\n");
8122
+ saveTarget = "file";
8123
+ } else if (args.save !== "file" && args.save !== "none") return {
8124
+ kind: "error",
8125
+ reason: "invalid-save",
8126
+ message: `--save must be "file" or "none" (got "${args.save}").`,
8127
+ exitCode: ExitCode.Usage
8128
+ };
8129
+ else saveTarget = args.save;
8387
8130
  if (args.email !== void 0 || args.password !== void 0 || args.passwordStdin) {
8388
8131
  if (args.email === void 0 || args.email.trim().length === 0) return {
8389
8132
  kind: "error",
@@ -8452,7 +8195,7 @@ async function resolveCredentialsForLogin(args, deps, opts = {}) {
8452
8195
  return null;
8453
8196
  });
8454
8197
  if (fromStore) {
8455
- if (!args.json) process.stderr.write(`Using credentials from OS keychain for ${fromStore.email}. Pass --interactive to type a different account.
8198
+ if (!args.json) process.stderr.write(`Using saved credentials for ${fromStore.email}. Pass --interactive to type a different account.
8456
8199
  `);
8457
8200
  return {
8458
8201
  kind: "ok",
@@ -9424,7 +9167,7 @@ function resolveVersion() {
9424
9167
  if (typeof injected === "string" && injected.length > 0) return injected;
9425
9168
  } catch {}
9426
9169
  try {
9427
- return "0.1.37";
9170
+ return "0.1.38";
9428
9171
  } catch {}
9429
9172
  return "0.0.0-dev";
9430
9173
  }
@@ -10481,8 +10224,7 @@ async function describeCredentialSource() {
10481
10224
  function formatCredentials(cred) {
10482
10225
  if (cred.source === "none") return "none (run `aitcc login` to save)";
10483
10226
  if (cred.source === "env") return `env (AITCC_EMAIL${cred.email ? ` = ${cred.email}` : ""})`;
10484
- if (cred.source === "file") return `file (~/.config/aitcc/credentials.json)${cred.email ? ` (${cred.email})` : ""}`;
10485
- return `keychain${cred.email ? ` (${cred.email})` : ""}`;
10227
+ return `file (~/.config/aitcc/credentials.json)${cred.email ? ` (${cred.email})` : ""}`;
10486
10228
  }
10487
10229
  async function runBackgroundUpdateCheck(json) {
10488
10230
  if (json) return;