@ait-co/console-cli 0.1.36 → 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/README.en.md +28 -4
- package/README.md +28 -4
- package/dist/cli.mjs +162 -333
- 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,289 +5884,142 @@ 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
|
-
//#endregion
|
|
5916
|
-
//#region src/auth/backends/linux.ts
|
|
5917
|
-
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.";
|
|
5918
|
-
const MISSING_HINT_SHORT$1 = "libsecret tools are missing. Install `libsecret-tools` and ensure a Secret Service provider is running.";
|
|
5919
|
-
const LINUX_BACKEND = {
|
|
5920
|
-
name: "libsecret",
|
|
5921
|
-
async get(account) {
|
|
5922
|
-
let result;
|
|
5923
|
-
try {
|
|
5924
|
-
result = await runCommand("secret-tool", { args: [
|
|
5925
|
-
"lookup",
|
|
5926
|
-
"service",
|
|
5927
|
-
CREDENTIAL_SERVICE,
|
|
5928
|
-
"account",
|
|
5929
|
-
account
|
|
5930
|
-
] });
|
|
5931
|
-
} catch (err) {
|
|
5932
|
-
if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("linux", MISSING_HINT_FULL$1);
|
|
5933
|
-
throw err;
|
|
5934
|
-
}
|
|
5935
|
-
if (result.exitCode !== 0) {
|
|
5936
|
-
if (result.stdout.length === 0) return null;
|
|
5937
|
-
throw new CredentialBackendCommandError("secret-tool lookup", result.exitCode, redactStderr(result.stderr));
|
|
5938
|
-
}
|
|
5939
|
-
if (result.stdout.length === 0) return null;
|
|
5940
|
-
const password = stripTrailingNewline$1(result.stdout);
|
|
5941
|
-
return password.length > 0 ? password : null;
|
|
5942
|
-
},
|
|
5943
|
-
async set(account, password) {
|
|
5944
|
-
let result;
|
|
5945
|
-
try {
|
|
5946
|
-
result = await runCommand("secret-tool", {
|
|
5947
|
-
args: [
|
|
5948
|
-
"store",
|
|
5949
|
-
"--label",
|
|
5950
|
-
"aitcc Toss Business credentials",
|
|
5951
|
-
"service",
|
|
5952
|
-
CREDENTIAL_SERVICE,
|
|
5953
|
-
"account",
|
|
5954
|
-
account
|
|
5955
|
-
],
|
|
5956
|
-
stdin: password
|
|
5957
|
-
});
|
|
5958
|
-
} catch (err) {
|
|
5959
|
-
if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("linux", MISSING_HINT_SHORT$1);
|
|
5960
|
-
throw err;
|
|
5961
|
-
}
|
|
5962
|
-
if (result.exitCode !== 0) throw new CredentialBackendCommandError("secret-tool store", result.exitCode, redactStderr(result.stderr));
|
|
5963
|
-
},
|
|
5964
|
-
async clear(account) {
|
|
5965
|
-
let result;
|
|
5966
|
-
try {
|
|
5967
|
-
result = await runCommand("secret-tool", { args: [
|
|
5968
|
-
"clear",
|
|
5969
|
-
"service",
|
|
5970
|
-
CREDENTIAL_SERVICE,
|
|
5971
|
-
"account",
|
|
5972
|
-
account
|
|
5973
|
-
] });
|
|
5974
|
-
} catch (err) {
|
|
5975
|
-
if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("linux", "libsecret tools are missing.");
|
|
5976
|
-
throw err;
|
|
5977
|
-
}
|
|
5978
|
-
if (result.exitCode !== 0) throw new CredentialBackendCommandError("secret-tool clear", result.exitCode, redactStderr(result.stderr));
|
|
5979
|
-
return { existed: true };
|
|
5980
|
-
}
|
|
5981
|
-
};
|
|
5982
|
-
//#endregion
|
|
5983
|
-
//#region src/auth/backends/macos.ts
|
|
5984
|
-
const MISSING_HINT = "macOS `security` is missing from PATH.";
|
|
5985
|
-
const MACOS_BACKEND = {
|
|
5986
|
-
name: "macos-keychain",
|
|
5987
|
-
async get(account) {
|
|
5988
|
-
let result;
|
|
5989
|
-
try {
|
|
5990
|
-
result = await runCommand("security", { args: [
|
|
5991
|
-
"find-generic-password",
|
|
5992
|
-
"-s",
|
|
5993
|
-
CREDENTIAL_SERVICE,
|
|
5994
|
-
"-a",
|
|
5995
|
-
account,
|
|
5996
|
-
"-w"
|
|
5997
|
-
] });
|
|
5998
|
-
} catch (err) {
|
|
5999
|
-
if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("darwin", MISSING_HINT);
|
|
6000
|
-
throw err;
|
|
6001
|
-
}
|
|
6002
|
-
if (result.exitCode === 44) return null;
|
|
6003
|
-
if (result.exitCode !== 0) return null;
|
|
6004
|
-
const password = stripTrailingNewline$1(result.stdout);
|
|
6005
|
-
return password.length > 0 ? password : null;
|
|
6006
|
-
},
|
|
6007
|
-
async set(account, password) {
|
|
6008
|
-
let result;
|
|
6009
|
-
try {
|
|
6010
|
-
result = await runCommand("security", { args: [
|
|
6011
|
-
"add-generic-password",
|
|
6012
|
-
"-U",
|
|
6013
|
-
"-A",
|
|
6014
|
-
"-s",
|
|
6015
|
-
CREDENTIAL_SERVICE,
|
|
6016
|
-
"-a",
|
|
6017
|
-
account,
|
|
6018
|
-
"-w",
|
|
6019
|
-
password
|
|
6020
|
-
] });
|
|
6021
|
-
} catch (err) {
|
|
6022
|
-
if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("darwin", MISSING_HINT);
|
|
6023
|
-
throw err;
|
|
6024
|
-
}
|
|
6025
|
-
if (result.exitCode !== 0) throw new CredentialBackendCommandError("security add-generic-password", result.exitCode, redactStderr(result.stderr));
|
|
6026
|
-
},
|
|
6027
|
-
async clear(account) {
|
|
6028
|
-
let result;
|
|
6029
|
-
try {
|
|
6030
|
-
result = await runCommand("security", { args: [
|
|
6031
|
-
"delete-generic-password",
|
|
6032
|
-
"-s",
|
|
6033
|
-
CREDENTIAL_SERVICE,
|
|
6034
|
-
"-a",
|
|
6035
|
-
account
|
|
6036
|
-
] });
|
|
6037
|
-
} catch (err) {
|
|
6038
|
-
if (isCommandNotFound(err)) throw new CredentialBackendUnsupportedError("darwin", MISSING_HINT);
|
|
6039
|
-
throw err;
|
|
6040
|
-
}
|
|
6041
|
-
if (result.exitCode === 44) return { existed: false };
|
|
6042
|
-
if (result.exitCode === 0) return { existed: true };
|
|
6043
|
-
throw new CredentialBackendCommandError("security delete-generic-password", result.exitCode, redactStderr(result.stderr));
|
|
6044
|
-
}
|
|
6045
|
-
};
|
|
6046
5887
|
//#endregion
|
|
6047
|
-
//#region src/auth/backends/
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
using System.Runtime.InteropServices;
|
|
6053
|
-
|
|
6054
|
-
public class AitccCredApi {
|
|
6055
|
-
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
6056
|
-
public struct CREDENTIAL {
|
|
6057
|
-
public uint Flags;
|
|
6058
|
-
public uint Type;
|
|
6059
|
-
public IntPtr TargetName;
|
|
6060
|
-
public IntPtr Comment;
|
|
6061
|
-
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
6062
|
-
public uint CredentialBlobSize;
|
|
6063
|
-
public IntPtr CredentialBlob;
|
|
6064
|
-
public uint Persist;
|
|
6065
|
-
public uint AttributeCount;
|
|
6066
|
-
public IntPtr Attributes;
|
|
6067
|
-
public IntPtr TargetAlias;
|
|
6068
|
-
public IntPtr UserName;
|
|
6069
|
-
}
|
|
6070
|
-
[DllImport("Advapi32.dll", SetLastError = true, EntryPoint = "CredWriteW", CharSet = CharSet.Unicode)]
|
|
6071
|
-
public static extern bool CredWrite([In] ref CREDENTIAL Credential, [In] uint Flags);
|
|
6072
|
-
[DllImport("Advapi32.dll", SetLastError = true, EntryPoint = "CredReadW", CharSet = CharSet.Unicode)]
|
|
6073
|
-
public static extern bool CredRead(string target, uint type, uint reservedFlag, out IntPtr CredentialPtr);
|
|
6074
|
-
[DllImport("Advapi32.dll", SetLastError = true, EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode)]
|
|
6075
|
-
public static extern bool CredDelete(string target, uint type, uint flags);
|
|
6076
|
-
[DllImport("Advapi32.dll", SetLastError = true, EntryPoint = "CredFree")]
|
|
6077
|
-
public static extern void CredFree([In] IntPtr cred);
|
|
6078
|
-
}
|
|
6079
|
-
"@
|
|
6080
|
-
`;
|
|
6081
|
-
const MISSING_HINT_FULL = "`powershell.exe` is missing from PATH. Windows credential storage requires PowerShell.";
|
|
6082
|
-
const MISSING_HINT_SHORT = "`powershell.exe` is missing from PATH.";
|
|
6083
|
-
function targetName(account) {
|
|
6084
|
-
return `${CREDENTIAL_SERVICE}/${account}`;
|
|
6085
|
-
}
|
|
6086
|
-
function powerShellArgs(script) {
|
|
6087
|
-
return [
|
|
6088
|
-
"-NoProfile",
|
|
6089
|
-
"-NonInteractive",
|
|
6090
|
-
"-Command",
|
|
6091
|
-
script
|
|
6092
|
-
];
|
|
5888
|
+
//#region src/auth/backends/file.ts
|
|
5889
|
+
function credentialFilePath() {
|
|
5890
|
+
const override = process.env.AITCC_CREDENTIAL_FILE;
|
|
5891
|
+
if (override && override.length > 0) return override;
|
|
5892
|
+
return join(configDir(), "credentials.json");
|
|
6093
5893
|
}
|
|
6094
|
-
function
|
|
6095
|
-
return
|
|
5894
|
+
function makeKey(account) {
|
|
5895
|
+
return `${CREDENTIAL_SERVICE}:${account}`;
|
|
6096
5896
|
}
|
|
6097
|
-
async function
|
|
5897
|
+
async function readStore(filePath) {
|
|
5898
|
+
let raw;
|
|
6098
5899
|
try {
|
|
6099
|
-
|
|
5900
|
+
raw = await readFile(filePath, "utf8");
|
|
6100
5901
|
} catch (err) {
|
|
6101
|
-
if (
|
|
5902
|
+
if (err.code === "ENOENT") return null;
|
|
6102
5903
|
throw err;
|
|
6103
5904
|
}
|
|
5905
|
+
try {
|
|
5906
|
+
const mode = (await stat(filePath)).mode & 511;
|
|
5907
|
+
if (mode !== 384) process.stderr.write(`Warning: credential file ${filePath} has permissions ${mode.toString(8)} — expected 0600.\n Run: chmod 600 ${filePath}\n`);
|
|
5908
|
+
} catch {}
|
|
5909
|
+
try {
|
|
5910
|
+
return JSON.parse(raw);
|
|
5911
|
+
} catch {
|
|
5912
|
+
return null;
|
|
5913
|
+
}
|
|
6104
5914
|
}
|
|
6105
|
-
|
|
6106
|
-
|
|
5915
|
+
async function writeStore(filePath, store) {
|
|
5916
|
+
await mkdir(dirname(filePath), {
|
|
5917
|
+
recursive: true,
|
|
5918
|
+
mode: 448
|
|
5919
|
+
});
|
|
5920
|
+
await writeFile(filePath, JSON.stringify(store, null, 2), { mode: 384 });
|
|
5921
|
+
try {
|
|
5922
|
+
await chmod(filePath, 384);
|
|
5923
|
+
} catch {}
|
|
5924
|
+
}
|
|
5925
|
+
const FILE_BACKEND = {
|
|
5926
|
+
name: "file",
|
|
6107
5927
|
async get(account) {
|
|
6108
|
-
const
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
$ok = [AitccCredApi]::CredRead($target, 1, 0, [ref]$ptr);
|
|
6113
|
-
if (-not $ok) { exit 0; }
|
|
6114
|
-
$cred = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [Type][AitccCredApi+CREDENTIAL]);
|
|
6115
|
-
$blob = New-Object byte[] $cred.CredentialBlobSize;
|
|
6116
|
-
[Runtime.InteropServices.Marshal]::Copy($cred.CredentialBlob, $blob, 0, $cred.CredentialBlobSize);
|
|
6117
|
-
$pw = [System.Text.Encoding]::Unicode.GetString($blob);
|
|
6118
|
-
[AitccCredApi]::CredFree($ptr);
|
|
6119
|
-
[Console]::Out.Write($pw);
|
|
6120
|
-
`);
|
|
6121
|
-
if (result.exitCode !== 0) throw new CredentialBackendCommandError("powershell CredRead", result.exitCode, redactStderr(result.stderr));
|
|
6122
|
-
return result.stdout.length > 0 ? result.stdout : null;
|
|
5928
|
+
const store = await readStore(credentialFilePath());
|
|
5929
|
+
if (store === null) return null;
|
|
5930
|
+
const value = store[makeKey(account)];
|
|
5931
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
6123
5932
|
},
|
|
6124
5933
|
async set(account, password) {
|
|
6125
|
-
const
|
|
6126
|
-
const
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
$target = '${escapeSingleQuotes(target)}';
|
|
6130
|
-
$user = '${escapeSingleQuotes(account)}';
|
|
6131
|
-
$pwHex = '${passwordHex}';
|
|
6132
|
-
$pwBytes = New-Object byte[] ($pwHex.Length / 2);
|
|
6133
|
-
for ($i = 0; $i -lt $pwBytes.Length; $i++) {
|
|
6134
|
-
$pwBytes[$i] = [Convert]::ToByte($pwHex.Substring($i * 2, 2), 16);
|
|
6135
|
-
}
|
|
6136
|
-
$pwUtf16 = [System.Text.Encoding]::Unicode.GetBytes([System.Text.Encoding]::UTF8.GetString($pwBytes));
|
|
6137
|
-
$cred = New-Object AitccCredApi+CREDENTIAL;
|
|
6138
|
-
$cred.Type = 1;
|
|
6139
|
-
$cred.TargetName = [Runtime.InteropServices.Marshal]::StringToHGlobalUni($target);
|
|
6140
|
-
$cred.CredentialBlobSize = [uint32]$pwUtf16.Length;
|
|
6141
|
-
$cred.CredentialBlob = [Runtime.InteropServices.Marshal]::AllocHGlobal($pwUtf16.Length);
|
|
6142
|
-
[Runtime.InteropServices.Marshal]::Copy($pwUtf16, 0, $cred.CredentialBlob, $pwUtf16.Length);
|
|
6143
|
-
$cred.Persist = 2;
|
|
6144
|
-
$cred.UserName = [Runtime.InteropServices.Marshal]::StringToHGlobalUni($user);
|
|
6145
|
-
try {
|
|
6146
|
-
$ok = [AitccCredApi]::CredWrite([ref]$cred, 0);
|
|
6147
|
-
if (-not $ok) { Write-Error 'CredWrite failed'; exit 1; }
|
|
6148
|
-
} finally {
|
|
6149
|
-
[Runtime.InteropServices.Marshal]::FreeHGlobal($cred.TargetName);
|
|
6150
|
-
[Runtime.InteropServices.Marshal]::FreeHGlobal($cred.UserName);
|
|
6151
|
-
[Runtime.InteropServices.Marshal]::FreeHGlobal($cred.CredentialBlob);
|
|
6152
|
-
}
|
|
6153
|
-
`;
|
|
6154
|
-
let result;
|
|
6155
|
-
try {
|
|
6156
|
-
result = await runPowerShell(script);
|
|
6157
|
-
} catch (err) {
|
|
6158
|
-
if (err instanceof CredentialBackendUnsupportedError) throw new CredentialBackendUnsupportedError("win32", MISSING_HINT_SHORT);
|
|
6159
|
-
throw err;
|
|
6160
|
-
}
|
|
6161
|
-
if (result.exitCode !== 0) throw new CredentialBackendCommandError("powershell CredWrite", result.exitCode, redactStderr(result.stderr));
|
|
5934
|
+
const filePath = credentialFilePath();
|
|
5935
|
+
const store = await readStore(filePath) ?? {};
|
|
5936
|
+
store[makeKey(account)] = password;
|
|
5937
|
+
await writeStore(filePath, store);
|
|
6162
5938
|
},
|
|
6163
5939
|
async clear(account) {
|
|
6164
|
-
const
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
if (
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
result = await runPowerShell(script);
|
|
5940
|
+
const filePath = credentialFilePath();
|
|
5941
|
+
const store = await readStore(filePath);
|
|
5942
|
+
if (store === null) return { existed: false };
|
|
5943
|
+
const key = makeKey(account);
|
|
5944
|
+
if (!(key in store)) return { existed: false };
|
|
5945
|
+
delete store[key];
|
|
5946
|
+
if (Object.keys(store).length === 0) try {
|
|
5947
|
+
await unlink(filePath);
|
|
6173
5948
|
} catch (err) {
|
|
6174
|
-
if (err
|
|
6175
|
-
throw err;
|
|
5949
|
+
if (err.code !== "ENOENT") throw err;
|
|
6176
5950
|
}
|
|
6177
|
-
|
|
6178
|
-
return { existed:
|
|
5951
|
+
else await writeStore(filePath, store);
|
|
5952
|
+
return { existed: true };
|
|
6179
5953
|
}
|
|
6180
5954
|
};
|
|
6181
5955
|
//#endregion
|
|
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
|
+
};
|
|
5988
|
+
}
|
|
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
|
+
};
|
|
5998
|
+
try {
|
|
5999
|
+
await FILE_BACKEND.set(email, password);
|
|
6000
|
+
} catch (err) {
|
|
6001
|
+
return {
|
|
6002
|
+
migrated: false,
|
|
6003
|
+
reason: `file write failed: ${err.message}`
|
|
6004
|
+
};
|
|
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 };
|
|
6017
|
+
}
|
|
6018
|
+
//#endregion
|
|
6182
6019
|
//#region src/auth/credentials.ts
|
|
6183
6020
|
function resolveBackend(opts = {}) {
|
|
6184
6021
|
if (opts.override) return opts.override;
|
|
6185
|
-
|
|
6186
|
-
switch (platform) {
|
|
6187
|
-
case "darwin": return MACOS_BACKEND;
|
|
6188
|
-
case "linux": return LINUX_BACKEND;
|
|
6189
|
-
case "win32": return WINDOWS_BACKEND;
|
|
6190
|
-
default: throw new CredentialBackendUnsupportedError(platform, "Only macOS, Linux (libsecret), and Windows are supported.");
|
|
6191
|
-
}
|
|
6022
|
+
return FILE_BACKEND;
|
|
6192
6023
|
}
|
|
6193
6024
|
async function readAuthState() {
|
|
6194
6025
|
let raw;
|
|
@@ -6233,15 +6064,13 @@ async function clearAuthState() {
|
|
|
6233
6064
|
/**
|
|
6234
6065
|
* Resolve credentials from the highest-priority source available:
|
|
6235
6066
|
* 1. `AITCC_EMAIL` + `AITCC_PASSWORD` env vars (CI single-shot use).
|
|
6236
|
-
* 2.
|
|
6067
|
+
* 2. File backend (`~/.config/aitcc/credentials.json`) — the only
|
|
6068
|
+
* persistent store; email pointer lives in `auth-state.json`.
|
|
6237
6069
|
*
|
|
6238
6070
|
* Returns `null` when no source is configured. The discriminated `kind`
|
|
6239
6071
|
* lets callers (e.g. the login flow) tell why a credential was found
|
|
6240
6072
|
* without having to peek at process env themselves — useful for
|
|
6241
6073
|
* "auto-login from CI" diagnostics.
|
|
6242
|
-
*
|
|
6243
|
-
* A future `'file'` source (~/.config/aitcc/credentials.json) is left as a
|
|
6244
|
-
* follow-up; once added, it slots between (1) and (2).
|
|
6245
6074
|
*/
|
|
6246
6075
|
async function loadCredentials(opts = {}) {
|
|
6247
6076
|
const env = opts.env ?? process.env;
|
|
@@ -6254,19 +6083,24 @@ async function loadCredentials(opts = {}) {
|
|
|
6254
6083
|
};
|
|
6255
6084
|
const state = await readAuthState();
|
|
6256
6085
|
if (!state) return null;
|
|
6257
|
-
const
|
|
6086
|
+
const backend = resolveBackend(opts);
|
|
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
|
+
}
|
|
6258
6092
|
if (password === null) return null;
|
|
6259
6093
|
return {
|
|
6260
|
-
kind: "
|
|
6094
|
+
kind: "file",
|
|
6261
6095
|
email: state.activeEmail,
|
|
6262
6096
|
password
|
|
6263
6097
|
};
|
|
6264
6098
|
}
|
|
6265
6099
|
/**
|
|
6266
|
-
* Persist credentials to the
|
|
6267
|
-
* pointer. Returns `'unchanged'` (no
|
|
6268
|
-
* + password is already stored — avoids
|
|
6269
|
-
*
|
|
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.
|
|
6270
6104
|
*/
|
|
6271
6105
|
async function saveCredentials(email, password, opts = {}) {
|
|
6272
6106
|
if (!email) throw new Error("email is required");
|
|
@@ -6289,15 +6123,13 @@ async function saveCredentials(email, password, opts = {}) {
|
|
|
6289
6123
|
return { status };
|
|
6290
6124
|
}
|
|
6291
6125
|
/**
|
|
6292
|
-
* Read just the active-email pointer without
|
|
6293
|
-
* Useful for surfaces like `
|
|
6294
|
-
*
|
|
6295
|
-
* 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.
|
|
6296
6129
|
*
|
|
6297
6130
|
* Returns the email and where it was found (`'env'` when
|
|
6298
|
-
* `AITCC_EMAIL` + `AITCC_PASSWORD` are present, `'
|
|
6299
|
-
* `auth-state.json` pointer exists), or `null` when nothing is
|
|
6300
|
-
* configured.
|
|
6131
|
+
* `AITCC_EMAIL` + `AITCC_PASSWORD` are present, `'file'` when the
|
|
6132
|
+
* `auth-state.json` pointer exists), or `null` when nothing is configured.
|
|
6301
6133
|
*/
|
|
6302
6134
|
async function getActiveCredentialEmail(opts = {}) {
|
|
6303
6135
|
const env = opts.env ?? process.env;
|
|
@@ -6308,22 +6140,18 @@ async function getActiveCredentialEmail(opts = {}) {
|
|
|
6308
6140
|
const state = await readAuthState();
|
|
6309
6141
|
if (!state) return null;
|
|
6310
6142
|
return {
|
|
6311
|
-
kind: "
|
|
6143
|
+
kind: "file",
|
|
6312
6144
|
email: state.activeEmail
|
|
6313
6145
|
};
|
|
6314
6146
|
}
|
|
6315
6147
|
/**
|
|
6316
|
-
* Remove the
|
|
6148
|
+
* Remove the file credential entry and the auth-state pointer. Returns
|
|
6317
6149
|
* `existed: true` if either side previously held data.
|
|
6318
6150
|
*/
|
|
6319
6151
|
async function deleteCredentials(opts = {}) {
|
|
6320
6152
|
const state = await readAuthState();
|
|
6321
6153
|
let backendExisted = false;
|
|
6322
|
-
if (state)
|
|
6323
|
-
backendExisted = (await resolveBackend(opts).clear(state.activeEmail)).existed;
|
|
6324
|
-
} catch (err) {
|
|
6325
|
-
if (err instanceof CredentialBackendUnsupportedError) {} else throw err;
|
|
6326
|
-
}
|
|
6154
|
+
if (state) backendExisted = (await resolveBackend(opts).clear(state.activeEmail)).existed;
|
|
6327
6155
|
const stateResult = await clearAuthState();
|
|
6328
6156
|
return { existed: backendExisted || stateResult.existed };
|
|
6329
6157
|
}
|
|
@@ -6611,7 +6439,7 @@ async function runAuthSet(args, deps = {}) {
|
|
|
6611
6439
|
const message = err.message;
|
|
6612
6440
|
if (args.json) emitJson({
|
|
6613
6441
|
ok: false,
|
|
6614
|
-
reason: "
|
|
6442
|
+
reason: "save-error",
|
|
6615
6443
|
message
|
|
6616
6444
|
});
|
|
6617
6445
|
else process.stderr.write(`Failed to save credentials: ${message}\n`);
|
|
@@ -6623,7 +6451,7 @@ async function runAuthSet(args, deps = {}) {
|
|
|
6623
6451
|
email
|
|
6624
6452
|
});
|
|
6625
6453
|
else if (result.status === "unchanged") process.stdout.write("Credentials already saved (no change).\n");
|
|
6626
|
-
else process.stdout.write(`Credentials saved for ${email}
|
|
6454
|
+
else process.stdout.write(`Credentials saved for ${email}.\n`);
|
|
6627
6455
|
return exitAfterFlush(ExitCode.Ok);
|
|
6628
6456
|
}
|
|
6629
6457
|
async function runAuthClear(args, deps = {}) {
|
|
@@ -6670,7 +6498,7 @@ async function runAuthClear(args, deps = {}) {
|
|
|
6670
6498
|
const message = err.message;
|
|
6671
6499
|
if (args.json) emitJson({
|
|
6672
6500
|
ok: false,
|
|
6673
|
-
reason: "
|
|
6501
|
+
reason: "clear-error",
|
|
6674
6502
|
message
|
|
6675
6503
|
});
|
|
6676
6504
|
else process.stderr.write(`Failed to clear credentials: ${message}\n`);
|
|
@@ -6706,7 +6534,7 @@ async function runAuthStatus(args, deps = {}) {
|
|
|
6706
6534
|
return exitAfterFlush(ExitCode.Ok);
|
|
6707
6535
|
}
|
|
6708
6536
|
if (active) {
|
|
6709
|
-
const sourceLabel = active.kind === "env" ? "environment (AITCC_EMAIL/PASSWORD)" : "
|
|
6537
|
+
const sourceLabel = active.kind === "env" ? "environment (AITCC_EMAIL/PASSWORD)" : "file";
|
|
6710
6538
|
process.stdout.write(`Email: ${active.email}\n`);
|
|
6711
6539
|
process.stdout.write(`Source: ${sourceLabel}\n`);
|
|
6712
6540
|
} else process.stdout.write("Email: (not configured)\n");
|
|
@@ -8094,10 +7922,10 @@ const defaultPromptDeps = {
|
|
|
8094
7922
|
}),
|
|
8095
7923
|
saveTarget: () => select({
|
|
8096
7924
|
message: "Where would you like to save the credentials?",
|
|
8097
|
-
default: "
|
|
7925
|
+
default: "file",
|
|
8098
7926
|
choices: [{
|
|
8099
|
-
name: "
|
|
8100
|
-
value: "
|
|
7927
|
+
name: "File (~/.config/aitcc/credentials.json, perm 0600) — next login runs headlessly",
|
|
7928
|
+
value: "file"
|
|
8101
7929
|
}, {
|
|
8102
7930
|
name: "Do not save — one-shot. (Tip: AITCC_EMAIL/AITCC_PASSWORD env for CI.)",
|
|
8103
7931
|
value: "none"
|
|
@@ -8140,7 +7968,7 @@ const loginCommand = defineCommand({
|
|
|
8140
7968
|
},
|
|
8141
7969
|
save: {
|
|
8142
7970
|
type: "string",
|
|
8143
|
-
description: "Where to persist credentials when --email/--password* are passed: \"
|
|
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)."
|
|
8144
7972
|
},
|
|
8145
7973
|
"skip-onboarding": {
|
|
8146
7974
|
type: "boolean",
|
|
@@ -8159,7 +7987,7 @@ const loginCommand = defineCommand({
|
|
|
8159
7987
|
save: typeof args.save === "string" ? args.save : void 0
|
|
8160
7988
|
}, {
|
|
8161
7989
|
getCredentials: loadCredentials,
|
|
8162
|
-
saveCredentials
|
|
7990
|
+
saveCredentials: (email, password) => saveCredentials(email, password)
|
|
8163
7991
|
});
|
|
8164
7992
|
}
|
|
8165
7993
|
});
|
|
@@ -8209,7 +8037,7 @@ async function runLoginCommand(args, deps) {
|
|
|
8209
8037
|
return exitAfterFlush(resolved.exitCode);
|
|
8210
8038
|
}
|
|
8211
8039
|
let saved = "skipped";
|
|
8212
|
-
if (resolved.saveTarget === "
|
|
8040
|
+
if (resolved.saveTarget === "file" && resolved.credentials !== null) {
|
|
8213
8041
|
const save = deps.saveCredentials;
|
|
8214
8042
|
if (!save) {
|
|
8215
8043
|
emitError({
|
|
@@ -8222,13 +8050,13 @@ async function runLoginCommand(args, deps) {
|
|
|
8222
8050
|
const result = await save(resolved.credentials.email, resolved.credentials.password);
|
|
8223
8051
|
saved = result.status;
|
|
8224
8052
|
if (!args.json) if (result.status === "unchanged") process.stderr.write("Credentials already saved (no change).\n");
|
|
8225
|
-
else process.stderr.write(`Credentials saved to
|
|
8053
|
+
else process.stderr.write(`Credentials saved to ~/.config/aitcc/credentials.json (${resolved.credentials.email}).\n`);
|
|
8226
8054
|
} catch (err) {
|
|
8227
8055
|
const message = err.message;
|
|
8228
8056
|
emitError({
|
|
8229
|
-
reason: "
|
|
8057
|
+
reason: "file-save-failed",
|
|
8230
8058
|
message
|
|
8231
|
-
}, `Failed to save credentials to
|
|
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.`);
|
|
8232
8060
|
return exitAfterFlush(ExitCode.Usage);
|
|
8233
8061
|
}
|
|
8234
8062
|
}
|
|
@@ -8289,15 +8117,16 @@ async function resolveCredentialsForLogin(args, deps, opts = {}) {
|
|
|
8289
8117
|
exitCode: ExitCode.Usage
|
|
8290
8118
|
};
|
|
8291
8119
|
let saveTarget;
|
|
8292
|
-
if (args.save !== void 0) {
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
}
|
|
8299
|
-
|
|
8300
|
-
}
|
|
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;
|
|
8301
8130
|
if (args.email !== void 0 || args.password !== void 0 || args.passwordStdin) {
|
|
8302
8131
|
if (args.email === void 0 || args.email.trim().length === 0) return {
|
|
8303
8132
|
kind: "error",
|
|
@@ -8366,7 +8195,7 @@ async function resolveCredentialsForLogin(args, deps, opts = {}) {
|
|
|
8366
8195
|
return null;
|
|
8367
8196
|
});
|
|
8368
8197
|
if (fromStore) {
|
|
8369
|
-
if (!args.json) process.stderr.write(`Using credentials
|
|
8198
|
+
if (!args.json) process.stderr.write(`Using saved credentials for ${fromStore.email}. Pass --interactive to type a different account.
|
|
8370
8199
|
`);
|
|
8371
8200
|
return {
|
|
8372
8201
|
kind: "ok",
|
|
@@ -9338,7 +9167,7 @@ function resolveVersion() {
|
|
|
9338
9167
|
if (typeof injected === "string" && injected.length > 0) return injected;
|
|
9339
9168
|
} catch {}
|
|
9340
9169
|
try {
|
|
9341
|
-
return "0.1.
|
|
9170
|
+
return "0.1.38";
|
|
9342
9171
|
} catch {}
|
|
9343
9172
|
return "0.0.0-dev";
|
|
9344
9173
|
}
|
|
@@ -10395,7 +10224,7 @@ async function describeCredentialSource() {
|
|
|
10395
10224
|
function formatCredentials(cred) {
|
|
10396
10225
|
if (cred.source === "none") return "none (run `aitcc login` to save)";
|
|
10397
10226
|
if (cred.source === "env") return `env (AITCC_EMAIL${cred.email ? ` = ${cred.email}` : ""})`;
|
|
10398
|
-
return `
|
|
10227
|
+
return `file (~/.config/aitcc/credentials.json)${cred.email ? ` (${cred.email})` : ""}`;
|
|
10399
10228
|
}
|
|
10400
10229
|
async function runBackgroundUpdateCheck(json) {
|
|
10401
10230
|
if (json) return;
|