@ait-co/console-cli 0.1.36 → 0.1.37
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 +37 -0
- package/README.md +37 -0
- package/dist/cli.mjs +109 -22
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -111,6 +111,43 @@ Pass `--interactive` to force the visible-browser flow even when credentials are
|
|
|
111
111
|
|
|
112
112
|
The CLI looks for Chrome in the standard OS install locations (Google Chrome, Chromium, Microsoft Edge). Override the executable with `AITCC_BROWSER=/path/to/chrome` if your install is elsewhere; override the sign-in URL with `AITCC_OAUTH_URL` if you need to point at a staging environment. `--timeout <seconds>` controls how long the CLI will wait for sign-in to finish (default 300s).
|
|
113
113
|
|
|
114
|
+
## Using aitcc in SSH / headless environments
|
|
115
|
+
|
|
116
|
+
macOS Keychain access is blocked in SSH remote sessions and GUI-less servers, so `--save keychain` will fail there. Three workarounds are available:
|
|
117
|
+
|
|
118
|
+
**Option 1 — Export / import the session (recommended, requires a KR IP)**
|
|
119
|
+
|
|
120
|
+
Export the session on a desktop GUI Mac and inject it into the SSH environment:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
# On a desktop Mac that is already logged in:
|
|
124
|
+
aitcc auth export --format env # outputs: AITCC_SESSION=...
|
|
125
|
+
|
|
126
|
+
# In the SSH environment:
|
|
127
|
+
AITCC_SESSION='...' aitcc app deploy ./bundle.ait --json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Option 2 — Unlock the keychain**
|
|
131
|
+
|
|
132
|
+
Unlock the keychain in the same SSH session, then retry:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
security unlock-keychain ~/Library/Keychains/login.keychain-db
|
|
136
|
+
# (enter login password when prompted)
|
|
137
|
+
aitcc login --save keychain --email you@example.com --password-stdin
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Option 3 — File backend**
|
|
141
|
+
|
|
142
|
+
Store credentials in a plain file instead of the keychain. Assumes a single-user machine; FileVault (full-disk encryption) is strongly recommended:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
aitcc login --save=file --email you@example.com --password-stdin
|
|
146
|
+
# → stored in ~/.config/aitcc/credentials.json (perm 0600)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
After saving, subsequent `aitcc login` runs in the SSH environment will read this file and sign in headlessly. Override the path with the `AITCC_CREDENTIAL_FILE` environment variable. See [issue #176](https://github.com/apps-in-toss-community/console-cli/issues/176) for background.
|
|
150
|
+
|
|
114
151
|
## Session storage
|
|
115
152
|
|
|
116
153
|
The local session lives at an XDG-compliant path with file mode `0600`:
|
package/README.md
CHANGED
|
@@ -111,6 +111,43 @@ aitcc app status # 플래그 없이 동작 — aitcc.yaml에서 컨텍
|
|
|
111
111
|
|
|
112
112
|
CLI는 OS 표준 위치(Google Chrome, Chromium, Microsoft Edge)에서 Chrome을 찾습니다. 다른 곳에 설치돼 있으면 `AITCC_BROWSER=/path/to/chrome`으로 실행 파일을 지정하고, 스테이징 환경을 가리키려면 `AITCC_OAUTH_URL`로 로그인 URL을 override합니다. `--timeout <초>`로 로그인 대기 시간을 조절합니다 (기본 300초).
|
|
113
113
|
|
|
114
|
+
## SSH / headless 환경에서 로그인
|
|
115
|
+
|
|
116
|
+
SSH 원격 세션이나 GUI가 없는 서버에서는 macOS Keychain 접근이 막혀 `--save keychain`이 실패할 수 있습니다. 세 가지 우회 방법이 있습니다:
|
|
117
|
+
|
|
118
|
+
**방법 1 — 세션 export/import (권장, KR IP 필요)**
|
|
119
|
+
|
|
120
|
+
Desktop GUI Mac에서 세션을 export해 SSH 환경에 주입합니다:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
# Desktop (GUI Mac, 이미 로그인된 상태):
|
|
124
|
+
aitcc auth export --format env # → AITCC_SESSION=... 출력
|
|
125
|
+
|
|
126
|
+
# SSH 환경에서:
|
|
127
|
+
AITCC_SESSION='...' aitcc app deploy ./bundle.ait --json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**방법 2 — keychain unlock**
|
|
131
|
+
|
|
132
|
+
같은 SSH 세션에서 keychain을 잠금 해제한 뒤 재시도합니다:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
security unlock-keychain ~/Library/Keychains/login.keychain-db
|
|
136
|
+
# (login 비밀번호 입력)
|
|
137
|
+
aitcc login --save keychain --email you@example.com --password-stdin
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**방법 3 — 파일 backend**
|
|
141
|
+
|
|
142
|
+
Keychain 대신 평문 파일에 자격증명을 저장합니다. 단일 사용자 머신 전제이며 FileVault(디스크 암호화) 사용을 권장합니다:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
aitcc login --save=file --email you@example.com --password-stdin
|
|
146
|
+
# → ~/.config/aitcc/credentials.json (perm 0600)에 저장
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
저장 후 SSH 환경에서 `aitcc login`이 자동으로 이 파일을 읽어 headless 로그인을 진행합니다. 경로를 바꾸려면 `AITCC_CREDENTIAL_FILE` env var를 지정합니다. 자세한 내용은 [issue #176](https://github.com/apps-in-toss-community/console-cli/issues/176) 참조.
|
|
150
|
+
|
|
114
151
|
## 세션 저장
|
|
115
152
|
|
|
116
153
|
로컬 세션은 XDG 규약 경로에 mode `0600`으로 저장됩니다:
|
package/dist/cli.mjs
CHANGED
|
@@ -5913,6 +5913,74 @@ function redactStderr(stderr) {
|
|
|
5913
5913
|
return trimmed;
|
|
5914
5914
|
}
|
|
5915
5915
|
//#endregion
|
|
5916
|
+
//#region src/auth/backends/file.ts
|
|
5917
|
+
function credentialFilePath() {
|
|
5918
|
+
const override = process.env.AITCC_CREDENTIAL_FILE;
|
|
5919
|
+
if (override && override.length > 0) return override;
|
|
5920
|
+
return join(configDir(), "credentials.json");
|
|
5921
|
+
}
|
|
5922
|
+
function makeKey(account) {
|
|
5923
|
+
return `${CREDENTIAL_SERVICE}:${account}`;
|
|
5924
|
+
}
|
|
5925
|
+
async function readStore(filePath) {
|
|
5926
|
+
let raw;
|
|
5927
|
+
try {
|
|
5928
|
+
raw = await readFile(filePath, "utf8");
|
|
5929
|
+
} catch (err) {
|
|
5930
|
+
if (err.code === "ENOENT") return null;
|
|
5931
|
+
throw err;
|
|
5932
|
+
}
|
|
5933
|
+
try {
|
|
5934
|
+
const mode = (await stat(filePath)).mode & 511;
|
|
5935
|
+
if (mode !== 384) process.stderr.write(`Warning: credential file ${filePath} has permissions ${mode.toString(8)} — expected 0600.\n Run: chmod 600 ${filePath}\n`);
|
|
5936
|
+
} catch {}
|
|
5937
|
+
try {
|
|
5938
|
+
return JSON.parse(raw);
|
|
5939
|
+
} catch {
|
|
5940
|
+
return null;
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
async function writeStore(filePath, store) {
|
|
5944
|
+
await mkdir(dirname(filePath), {
|
|
5945
|
+
recursive: true,
|
|
5946
|
+
mode: 448
|
|
5947
|
+
});
|
|
5948
|
+
await writeFile(filePath, JSON.stringify(store, null, 2), { mode: 384 });
|
|
5949
|
+
try {
|
|
5950
|
+
await chmod(filePath, 384);
|
|
5951
|
+
} catch {}
|
|
5952
|
+
}
|
|
5953
|
+
const FILE_BACKEND = {
|
|
5954
|
+
name: "file",
|
|
5955
|
+
async get(account) {
|
|
5956
|
+
const store = await readStore(credentialFilePath());
|
|
5957
|
+
if (store === null) return null;
|
|
5958
|
+
const value = store[makeKey(account)];
|
|
5959
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
5960
|
+
},
|
|
5961
|
+
async set(account, password) {
|
|
5962
|
+
const filePath = credentialFilePath();
|
|
5963
|
+
const store = await readStore(filePath) ?? {};
|
|
5964
|
+
store[makeKey(account)] = password;
|
|
5965
|
+
await writeStore(filePath, store);
|
|
5966
|
+
},
|
|
5967
|
+
async clear(account) {
|
|
5968
|
+
const filePath = credentialFilePath();
|
|
5969
|
+
const store = await readStore(filePath);
|
|
5970
|
+
if (store === null) return { existed: false };
|
|
5971
|
+
const key = makeKey(account);
|
|
5972
|
+
if (!(key in store)) return { existed: false };
|
|
5973
|
+
delete store[key];
|
|
5974
|
+
if (Object.keys(store).length === 0) try {
|
|
5975
|
+
await unlink(filePath);
|
|
5976
|
+
} catch (err) {
|
|
5977
|
+
if (err.code !== "ENOENT") throw err;
|
|
5978
|
+
}
|
|
5979
|
+
else await writeStore(filePath, store);
|
|
5980
|
+
return { existed: true };
|
|
5981
|
+
}
|
|
5982
|
+
};
|
|
5983
|
+
//#endregion
|
|
5916
5984
|
//#region src/auth/backends/linux.ts
|
|
5917
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.";
|
|
5918
5986
|
const MISSING_HINT_SHORT$1 = "libsecret tools are missing. Install `libsecret-tools` and ensure a Secret Service provider is running.";
|
|
@@ -6182,6 +6250,7 @@ if ($ok) { [Console]::Out.Write('deleted'); } else { [Console]::Out.Write('absen
|
|
|
6182
6250
|
//#region src/auth/credentials.ts
|
|
6183
6251
|
function resolveBackend(opts = {}) {
|
|
6184
6252
|
if (opts.override) return opts.override;
|
|
6253
|
+
if (opts.useFile === true || process.env.AITCC_CREDENTIAL_BACKEND === "file") return FILE_BACKEND;
|
|
6185
6254
|
const platform = opts.platform ?? process.platform;
|
|
6186
6255
|
switch (platform) {
|
|
6187
6256
|
case "darwin": return MACOS_BACKEND;
|
|
@@ -6233,15 +6302,14 @@ async function clearAuthState() {
|
|
|
6233
6302
|
/**
|
|
6234
6303
|
* Resolve credentials from the highest-priority source available:
|
|
6235
6304
|
* 1. `AITCC_EMAIL` + `AITCC_PASSWORD` env vars (CI single-shot use).
|
|
6236
|
-
* 2.
|
|
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`.
|
|
6237
6308
|
*
|
|
6238
6309
|
* Returns `null` when no source is configured. The discriminated `kind`
|
|
6239
6310
|
* lets callers (e.g. the login flow) tell why a credential was found
|
|
6240
6311
|
* without having to peek at process env themselves — useful for
|
|
6241
6312
|
* "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
6313
|
*/
|
|
6246
6314
|
async function loadCredentials(opts = {}) {
|
|
6247
6315
|
const env = opts.env ?? process.env;
|
|
@@ -6254,10 +6322,11 @@ async function loadCredentials(opts = {}) {
|
|
|
6254
6322
|
};
|
|
6255
6323
|
const state = await readAuthState();
|
|
6256
6324
|
if (!state) return null;
|
|
6257
|
-
const
|
|
6325
|
+
const backend = resolveBackend(opts);
|
|
6326
|
+
const password = await backend.get(state.activeEmail);
|
|
6258
6327
|
if (password === null) return null;
|
|
6259
6328
|
return {
|
|
6260
|
-
kind: "keychain",
|
|
6329
|
+
kind: backend.name === "file" ? "file" : "keychain",
|
|
6261
6330
|
email: state.activeEmail,
|
|
6262
6331
|
password
|
|
6263
6332
|
};
|
|
@@ -6308,7 +6377,7 @@ async function getActiveCredentialEmail(opts = {}) {
|
|
|
6308
6377
|
const state = await readAuthState();
|
|
6309
6378
|
if (!state) return null;
|
|
6310
6379
|
return {
|
|
6311
|
-
kind: "keychain",
|
|
6380
|
+
kind: env.AITCC_CREDENTIAL_BACKEND === "file" ? "file" : "keychain",
|
|
6312
6381
|
email: state.activeEmail
|
|
6313
6382
|
};
|
|
6314
6383
|
}
|
|
@@ -8095,13 +8164,20 @@ const defaultPromptDeps = {
|
|
|
8095
8164
|
saveTarget: () => select({
|
|
8096
8165
|
message: "Where would you like to save the credentials?",
|
|
8097
8166
|
default: "keychain",
|
|
8098
|
-
choices: [
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
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
|
+
]
|
|
8105
8181
|
})
|
|
8106
8182
|
};
|
|
8107
8183
|
const loginCommand = defineCommand({
|
|
@@ -8140,7 +8216,7 @@ const loginCommand = defineCommand({
|
|
|
8140
8216
|
},
|
|
8141
8217
|
save: {
|
|
8142
8218
|
type: "string",
|
|
8143
|
-
description: "Where to persist credentials when --email/--password* are passed: \"keychain\" or \"none\" (default)."
|
|
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."
|
|
8144
8220
|
},
|
|
8145
8221
|
"skip-onboarding": {
|
|
8146
8222
|
type: "boolean",
|
|
@@ -8159,7 +8235,7 @@ const loginCommand = defineCommand({
|
|
|
8159
8235
|
save: typeof args.save === "string" ? args.save : void 0
|
|
8160
8236
|
}, {
|
|
8161
8237
|
getCredentials: loadCredentials,
|
|
8162
|
-
saveCredentials
|
|
8238
|
+
saveCredentials: (email, password, useFile) => saveCredentials(email, password, { useFile: useFile === true })
|
|
8163
8239
|
});
|
|
8164
8240
|
}
|
|
8165
8241
|
});
|
|
@@ -8209,7 +8285,8 @@ async function runLoginCommand(args, deps) {
|
|
|
8209
8285
|
return exitAfterFlush(resolved.exitCode);
|
|
8210
8286
|
}
|
|
8211
8287
|
let saved = "skipped";
|
|
8212
|
-
if (resolved.saveTarget === "keychain" && resolved.credentials !== null) {
|
|
8288
|
+
if ((resolved.saveTarget === "keychain" || resolved.saveTarget === "file") && resolved.credentials !== null) {
|
|
8289
|
+
const useFile = resolved.saveTarget === "file";
|
|
8213
8290
|
const save = deps.saveCredentials;
|
|
8214
8291
|
if (!save) {
|
|
8215
8292
|
emitError({
|
|
@@ -8219,16 +8296,25 @@ async function runLoginCommand(args, deps) {
|
|
|
8219
8296
|
return exitAfterFlush(ExitCode.Generic);
|
|
8220
8297
|
}
|
|
8221
8298
|
try {
|
|
8222
|
-
const result = await save(resolved.credentials.email, resolved.credentials.password);
|
|
8299
|
+
const result = await save(resolved.credentials.email, resolved.credentials.password, useFile);
|
|
8223
8300
|
saved = result.status;
|
|
8224
8301
|
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`);
|
|
8225
8303
|
else process.stderr.write(`Credentials saved to OS keychain (${resolved.credentials.email}).\n`);
|
|
8226
8304
|
} catch (err) {
|
|
8227
8305
|
const message = err.message;
|
|
8228
|
-
emitError({
|
|
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({
|
|
8229
8311
|
reason: "keychain-save-failed",
|
|
8230
8312
|
message
|
|
8231
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({
|
|
8315
|
+
reason: "file-save-failed",
|
|
8316
|
+
message
|
|
8317
|
+
}, `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
8318
|
return exitAfterFlush(ExitCode.Usage);
|
|
8233
8319
|
}
|
|
8234
8320
|
}
|
|
@@ -8290,10 +8376,10 @@ async function resolveCredentialsForLogin(args, deps, opts = {}) {
|
|
|
8290
8376
|
};
|
|
8291
8377
|
let saveTarget;
|
|
8292
8378
|
if (args.save !== void 0) {
|
|
8293
|
-
if (args.save !== "keychain" && args.save !== "none") return {
|
|
8379
|
+
if (args.save !== "keychain" && args.save !== "file" && args.save !== "none") return {
|
|
8294
8380
|
kind: "error",
|
|
8295
8381
|
reason: "invalid-save",
|
|
8296
|
-
message: `--save must be "keychain" or "none" (got "${args.save}").`,
|
|
8382
|
+
message: `--save must be "keychain", "file", or "none" (got "${args.save}").`,
|
|
8297
8383
|
exitCode: ExitCode.Usage
|
|
8298
8384
|
};
|
|
8299
8385
|
saveTarget = args.save;
|
|
@@ -9338,7 +9424,7 @@ function resolveVersion() {
|
|
|
9338
9424
|
if (typeof injected === "string" && injected.length > 0) return injected;
|
|
9339
9425
|
} catch {}
|
|
9340
9426
|
try {
|
|
9341
|
-
return "0.1.
|
|
9427
|
+
return "0.1.37";
|
|
9342
9428
|
} catch {}
|
|
9343
9429
|
return "0.0.0-dev";
|
|
9344
9430
|
}
|
|
@@ -10395,6 +10481,7 @@ async function describeCredentialSource() {
|
|
|
10395
10481
|
function formatCredentials(cred) {
|
|
10396
10482
|
if (cred.source === "none") return "none (run `aitcc login` to save)";
|
|
10397
10483
|
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})` : ""}`;
|
|
10398
10485
|
return `keychain${cred.email ? ` (${cred.email})` : ""}`;
|
|
10399
10486
|
}
|
|
10400
10487
|
async function runBackgroundUpdateCheck(json) {
|