@ait-co/console-cli 0.1.37 → 0.1.39
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/README.en.md +38 -29
- package/README.md +38 -29
- package/dist/cli.mjs +138 -371
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
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/
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
}
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
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
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
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
|
-
|
|
5999
|
+
await FILE_BACKEND.set(email, password);
|
|
6168
6000
|
} catch (err) {
|
|
6169
|
-
|
|
6170
|
-
|
|
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
|
-
|
|
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`)
|
|
6306
|
-
*
|
|
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
|
-
|
|
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:
|
|
6094
|
+
kind: "file",
|
|
6330
6095
|
email: state.activeEmail,
|
|
6331
6096
|
password
|
|
6332
6097
|
};
|
|
6333
6098
|
}
|
|
6334
6099
|
/**
|
|
6335
|
-
* Persist credentials to the
|
|
6336
|
-
* pointer. Returns `'unchanged'` (no
|
|
6337
|
-
* + password is already stored — avoids
|
|
6338
|
-
*
|
|
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
|
|
6362
|
-
* Useful for surfaces like `
|
|
6363
|
-
*
|
|
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, `'
|
|
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:
|
|
6143
|
+
kind: "file",
|
|
6381
6144
|
email: state.activeEmail
|
|
6382
6145
|
};
|
|
6383
6146
|
}
|
|
6384
6147
|
/**
|
|
6385
|
-
* Remove the
|
|
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)
|
|
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: "
|
|
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}
|
|
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: "
|
|
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)" : "
|
|
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");
|
|
@@ -7074,8 +6833,11 @@ function readCredentials(path) {
|
|
|
7074
6833
|
}
|
|
7075
6834
|
function writeCredentials(path, map) {
|
|
7076
6835
|
const dir = join(path, "..");
|
|
7077
|
-
if (!existsSync(dir)) mkdirSync(dir, {
|
|
7078
|
-
|
|
6836
|
+
if (!existsSync(dir)) mkdirSync(dir, {
|
|
6837
|
+
recursive: true,
|
|
6838
|
+
mode: 448
|
|
6839
|
+
});
|
|
6840
|
+
writeFileSync(path, `${JSON.stringify(map, null, 2)}\n`, {
|
|
7079
6841
|
encoding: "utf8",
|
|
7080
6842
|
mode: 384
|
|
7081
6843
|
});
|
|
@@ -7193,6 +6955,20 @@ function validateKeyName(raw) {
|
|
|
7193
6955
|
if (!NAME_REGEX.test(raw)) return "bad-chars";
|
|
7194
6956
|
return null;
|
|
7195
6957
|
}
|
|
6958
|
+
/**
|
|
6959
|
+
* Resolve the ait profile name to save the Deploy Key under.
|
|
6960
|
+
*
|
|
6961
|
+
* - `noSaveProfile: true` → undefined (skip saving)
|
|
6962
|
+
* - `saveProfileOverride` present → use that name
|
|
6963
|
+
* - default → use `name` (the --name value)
|
|
6964
|
+
*
|
|
6965
|
+
* Exported for unit testing.
|
|
6966
|
+
*/
|
|
6967
|
+
function resolveProfileName(name, opts) {
|
|
6968
|
+
if (opts.noSaveProfile) return void 0;
|
|
6969
|
+
if (opts.saveProfileOverride) return opts.saveProfileOverride;
|
|
6970
|
+
return name;
|
|
6971
|
+
}
|
|
7196
6972
|
function parseAppsFlag(raw) {
|
|
7197
6973
|
const slugs = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
7198
6974
|
if (slugs.length === 0) return {
|
|
@@ -7286,7 +7062,12 @@ const createCommand = defineCommand({
|
|
|
7286
7062
|
},
|
|
7287
7063
|
"save-profile": {
|
|
7288
7064
|
type: "string",
|
|
7289
|
-
description: "
|
|
7065
|
+
description: "Profile name for the ait token (defaults to --name). The key is written to `~/.ait/credentials` so `ait deploy --profile <name>` works immediately. Use --no-save-profile to skip."
|
|
7066
|
+
},
|
|
7067
|
+
"no-save-profile": {
|
|
7068
|
+
type: "boolean",
|
|
7069
|
+
default: false,
|
|
7070
|
+
description: "Do not save the issued key to an ait token profile — print to stdout only (for CI pipes that store it elsewhere)."
|
|
7290
7071
|
},
|
|
7291
7072
|
json: {
|
|
7292
7073
|
type: "boolean",
|
|
@@ -7337,7 +7118,10 @@ const createCommand = defineCommand({
|
|
|
7337
7118
|
name,
|
|
7338
7119
|
target
|
|
7339
7120
|
}, session.cookies);
|
|
7340
|
-
const saveProfileName =
|
|
7121
|
+
const saveProfileName = resolveProfileName(name, {
|
|
7122
|
+
noSaveProfile: args["no-save-profile"],
|
|
7123
|
+
...args["save-profile"] ? { saveProfileOverride: String(args["save-profile"]) } : {}
|
|
7124
|
+
});
|
|
7341
7125
|
let savedProfile;
|
|
7342
7126
|
let saveProfileWarning;
|
|
7343
7127
|
if (saveProfileName !== void 0) {
|
|
@@ -8163,21 +7947,14 @@ const defaultPromptDeps = {
|
|
|
8163
7947
|
}),
|
|
8164
7948
|
saveTarget: () => select({
|
|
8165
7949
|
message: "Where would you like to save the credentials?",
|
|
8166
|
-
default: "
|
|
8167
|
-
choices: [
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
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
|
-
]
|
|
7950
|
+
default: "file",
|
|
7951
|
+
choices: [{
|
|
7952
|
+
name: "File (~/.config/aitcc/credentials.json, perm 0600) — next login runs headlessly",
|
|
7953
|
+
value: "file"
|
|
7954
|
+
}, {
|
|
7955
|
+
name: "Do not save — one-shot. (Tip: AITCC_EMAIL/AITCC_PASSWORD env for CI.)",
|
|
7956
|
+
value: "none"
|
|
7957
|
+
}]
|
|
8181
7958
|
})
|
|
8182
7959
|
};
|
|
8183
7960
|
const loginCommand = defineCommand({
|
|
@@ -8216,7 +7993,7 @@ const loginCommand = defineCommand({
|
|
|
8216
7993
|
},
|
|
8217
7994
|
save: {
|
|
8218
7995
|
type: "string",
|
|
8219
|
-
description: "Where to persist credentials when --email/--password* are passed: \"
|
|
7996
|
+
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
7997
|
},
|
|
8221
7998
|
"skip-onboarding": {
|
|
8222
7999
|
type: "boolean",
|
|
@@ -8235,7 +8012,7 @@ const loginCommand = defineCommand({
|
|
|
8235
8012
|
save: typeof args.save === "string" ? args.save : void 0
|
|
8236
8013
|
}, {
|
|
8237
8014
|
getCredentials: loadCredentials,
|
|
8238
|
-
saveCredentials: (email, password
|
|
8015
|
+
saveCredentials: (email, password) => saveCredentials(email, password)
|
|
8239
8016
|
});
|
|
8240
8017
|
}
|
|
8241
8018
|
});
|
|
@@ -8285,8 +8062,7 @@ async function runLoginCommand(args, deps) {
|
|
|
8285
8062
|
return exitAfterFlush(resolved.exitCode);
|
|
8286
8063
|
}
|
|
8287
8064
|
let saved = "skipped";
|
|
8288
|
-
if (
|
|
8289
|
-
const useFile = resolved.saveTarget === "file";
|
|
8065
|
+
if (resolved.saveTarget === "file" && resolved.credentials !== null) {
|
|
8290
8066
|
const save = deps.saveCredentials;
|
|
8291
8067
|
if (!save) {
|
|
8292
8068
|
emitError({
|
|
@@ -8296,22 +8072,13 @@ async function runLoginCommand(args, deps) {
|
|
|
8296
8072
|
return exitAfterFlush(ExitCode.Generic);
|
|
8297
8073
|
}
|
|
8298
8074
|
try {
|
|
8299
|
-
const result = await save(resolved.credentials.email, resolved.credentials.password
|
|
8075
|
+
const result = await save(resolved.credentials.email, resolved.credentials.password);
|
|
8300
8076
|
saved = result.status;
|
|
8301
8077
|
if (!args.json) if (result.status === "unchanged") process.stderr.write("Credentials already saved (no change).\n");
|
|
8302
|
-
else
|
|
8303
|
-
else process.stderr.write(`Credentials saved to OS keychain (${resolved.credentials.email}).\n`);
|
|
8078
|
+
else process.stderr.write(`Credentials saved to ~/.config/aitcc/credentials.json (${resolved.credentials.email}).\n`);
|
|
8304
8079
|
} catch (err) {
|
|
8305
8080
|
const message = err.message;
|
|
8306
|
-
|
|
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({
|
|
8081
|
+
emitError({
|
|
8315
8082
|
reason: "file-save-failed",
|
|
8316
8083
|
message
|
|
8317
8084
|
}, `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 +8142,16 @@ async function resolveCredentialsForLogin(args, deps, opts = {}) {
|
|
|
8375
8142
|
exitCode: ExitCode.Usage
|
|
8376
8143
|
};
|
|
8377
8144
|
let saveTarget;
|
|
8378
|
-
if (args.save !== void 0) {
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
}
|
|
8385
|
-
|
|
8386
|
-
}
|
|
8145
|
+
if (args.save !== void 0) if (args.save === "keychain") {
|
|
8146
|
+
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");
|
|
8147
|
+
saveTarget = "file";
|
|
8148
|
+
} else if (args.save !== "file" && args.save !== "none") return {
|
|
8149
|
+
kind: "error",
|
|
8150
|
+
reason: "invalid-save",
|
|
8151
|
+
message: `--save must be "file" or "none" (got "${args.save}").`,
|
|
8152
|
+
exitCode: ExitCode.Usage
|
|
8153
|
+
};
|
|
8154
|
+
else saveTarget = args.save;
|
|
8387
8155
|
if (args.email !== void 0 || args.password !== void 0 || args.passwordStdin) {
|
|
8388
8156
|
if (args.email === void 0 || args.email.trim().length === 0) return {
|
|
8389
8157
|
kind: "error",
|
|
@@ -8452,7 +8220,7 @@ async function resolveCredentialsForLogin(args, deps, opts = {}) {
|
|
|
8452
8220
|
return null;
|
|
8453
8221
|
});
|
|
8454
8222
|
if (fromStore) {
|
|
8455
|
-
if (!args.json) process.stderr.write(`Using credentials
|
|
8223
|
+
if (!args.json) process.stderr.write(`Using saved credentials for ${fromStore.email}. Pass --interactive to type a different account.
|
|
8456
8224
|
`);
|
|
8457
8225
|
return {
|
|
8458
8226
|
kind: "ok",
|
|
@@ -9424,7 +9192,7 @@ function resolveVersion() {
|
|
|
9424
9192
|
if (typeof injected === "string" && injected.length > 0) return injected;
|
|
9425
9193
|
} catch {}
|
|
9426
9194
|
try {
|
|
9427
|
-
return "0.1.
|
|
9195
|
+
return "0.1.39";
|
|
9428
9196
|
} catch {}
|
|
9429
9197
|
return "0.0.0-dev";
|
|
9430
9198
|
}
|
|
@@ -10481,8 +10249,7 @@ async function describeCredentialSource() {
|
|
|
10481
10249
|
function formatCredentials(cred) {
|
|
10482
10250
|
if (cred.source === "none") return "none (run `aitcc login` to save)";
|
|
10483
10251
|
if (cred.source === "env") return `env (AITCC_EMAIL${cred.email ? ` = ${cred.email}` : ""})`;
|
|
10484
|
-
|
|
10485
|
-
return `keychain${cred.email ? ` (${cred.email})` : ""}`;
|
|
10252
|
+
return `file (~/.config/aitcc/credentials.json)${cred.email ? ` (${cred.email})` : ""}`;
|
|
10486
10253
|
}
|
|
10487
10254
|
async function runBackgroundUpdateCheck(json) {
|
|
10488
10255
|
if (json) return;
|