@ait-co/console-cli 0.1.35 → 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 +228 -40
- 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
|
@@ -7,8 +7,8 @@ import { isMap, parse, parseDocument } from "yaml";
|
|
|
7
7
|
import { unzipSync } from "fflate";
|
|
8
8
|
import { checkbox, confirm, editor, input, password, select } from "@inquirer/prompts";
|
|
9
9
|
import { imageSize } from "image-size";
|
|
10
|
-
import { execFile, spawn } from "node:child_process";
|
|
11
|
-
import { constants, createReadStream, existsSync } from "node:fs";
|
|
10
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
11
|
+
import { constants, createReadStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { createInterface } from "node:readline/promises";
|
|
13
13
|
import { promisify } from "node:util";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
@@ -682,14 +682,15 @@ async function postBundleTestPush(workspaceId, miniAppId, deploymentId, cookies,
|
|
|
682
682
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
683
683
|
return raw;
|
|
684
684
|
}
|
|
685
|
-
async function fetchBundleTestLinks(workspaceId, miniAppId, cookies, opts = {}) {
|
|
685
|
+
async function fetchBundleTestLinks(workspaceId, miniAppId, deploymentId, cookies, opts = {}) {
|
|
686
686
|
const raw = await requestConsoleApi({
|
|
687
|
-
url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/bundles/test-links`,
|
|
687
|
+
url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/bundles/test-links?deploymentId=${encodeURIComponent(deploymentId)}`,
|
|
688
688
|
cookies,
|
|
689
689
|
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
690
690
|
});
|
|
691
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
692
|
-
|
|
691
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`Unexpected test-links shape for app=${miniAppId}`);
|
|
692
|
+
const data = raw;
|
|
693
|
+
return { linkUri: typeof data.linkUri === "string" ? data.linkUri : "" };
|
|
693
694
|
}
|
|
694
695
|
async function createMiniApp(workspaceId, payload, cookies, opts = {}) {
|
|
695
696
|
return normalizeCreateResult(await requestConsoleApi({
|
|
@@ -4527,7 +4528,7 @@ const bundlesCommand = defineCommand({
|
|
|
4527
4528
|
"test-links": defineCommand({
|
|
4528
4529
|
meta: {
|
|
4529
4530
|
name: "test-links",
|
|
4530
|
-
description: "Show
|
|
4531
|
+
description: "Show the test deep-link URL for a specific bundle."
|
|
4531
4532
|
},
|
|
4532
4533
|
args: {
|
|
4533
4534
|
id: {
|
|
@@ -4535,6 +4536,10 @@ const bundlesCommand = defineCommand({
|
|
|
4535
4536
|
description: "Mini-app ID. Optional when `aitcc.yaml` provides `miniAppId`.",
|
|
4536
4537
|
required: false
|
|
4537
4538
|
},
|
|
4539
|
+
"deployment-id": {
|
|
4540
|
+
type: "string",
|
|
4541
|
+
description: "deploymentId of the bundle to fetch the test link for."
|
|
4542
|
+
},
|
|
4538
4543
|
workspace: {
|
|
4539
4544
|
type: "string",
|
|
4540
4545
|
description: "Workspace ID. Defaults to the selected workspace."
|
|
@@ -4546,6 +4551,15 @@ const bundlesCommand = defineCommand({
|
|
|
4546
4551
|
}
|
|
4547
4552
|
},
|
|
4548
4553
|
async run({ args }) {
|
|
4554
|
+
const deploymentId = typeof args["deployment-id"] === "string" ? args["deployment-id"] : "";
|
|
4555
|
+
if (deploymentId === "") {
|
|
4556
|
+
if (args.json) emitJson({
|
|
4557
|
+
ok: false,
|
|
4558
|
+
reason: "missing-deployment-id"
|
|
4559
|
+
});
|
|
4560
|
+
else process.stderr.write("app bundles test-links: --deployment-id <uuid> is required.\n");
|
|
4561
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
4562
|
+
}
|
|
4549
4563
|
const ctx = await resolveAppOrFail({
|
|
4550
4564
|
json: args.json,
|
|
4551
4565
|
appIdRaw: args.id,
|
|
@@ -4557,26 +4571,22 @@ const bundlesCommand = defineCommand({
|
|
|
4557
4571
|
printContextHeader(ctx, { json: args.json });
|
|
4558
4572
|
const { session, workspaceId } = ctx;
|
|
4559
4573
|
try {
|
|
4560
|
-
const
|
|
4574
|
+
const { linkUri } = await fetchBundleTestLinks(workspaceId, appId, deploymentId, session.cookies);
|
|
4561
4575
|
if (args.json) {
|
|
4562
4576
|
emitJson({
|
|
4563
4577
|
ok: true,
|
|
4564
4578
|
workspaceId,
|
|
4565
4579
|
appId,
|
|
4566
|
-
|
|
4580
|
+
deploymentId,
|
|
4581
|
+
linkUri
|
|
4567
4582
|
});
|
|
4568
4583
|
return exitAfterFlush(ExitCode.Ok);
|
|
4569
4584
|
}
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
process.stdout.write(`App ${appId} (ws ${workspaceId}): no test links available\n`);
|
|
4585
|
+
if (linkUri === "") {
|
|
4586
|
+
process.stdout.write(`App ${appId} (ws ${workspaceId}): no test link available for deployment ${deploymentId}\n`);
|
|
4573
4587
|
return exitAfterFlush(ExitCode.Ok);
|
|
4574
4588
|
}
|
|
4575
|
-
process.stdout.write(`App ${appId} (ws ${workspaceId}):\n`);
|
|
4576
|
-
for (const k of keys) {
|
|
4577
|
-
const v = links[k];
|
|
4578
|
-
process.stdout.write(` ${k}\t${typeof v === "string" ? v : JSON.stringify(v)}\n`);
|
|
4579
|
-
}
|
|
4589
|
+
process.stdout.write(`App ${appId} (ws ${workspaceId}):\n ${linkUri}\n`);
|
|
4580
4590
|
return exitAfterFlush(ExitCode.Ok);
|
|
4581
4591
|
} catch (err) {
|
|
4582
4592
|
return emitFailureFromError(args.json, err);
|
|
@@ -5903,6 +5913,74 @@ function redactStderr(stderr) {
|
|
|
5903
5913
|
return trimmed;
|
|
5904
5914
|
}
|
|
5905
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
|
|
5906
5984
|
//#region src/auth/backends/linux.ts
|
|
5907
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.";
|
|
5908
5986
|
const MISSING_HINT_SHORT$1 = "libsecret tools are missing. Install `libsecret-tools` and ensure a Secret Service provider is running.";
|
|
@@ -6172,6 +6250,7 @@ if ($ok) { [Console]::Out.Write('deleted'); } else { [Console]::Out.Write('absen
|
|
|
6172
6250
|
//#region src/auth/credentials.ts
|
|
6173
6251
|
function resolveBackend(opts = {}) {
|
|
6174
6252
|
if (opts.override) return opts.override;
|
|
6253
|
+
if (opts.useFile === true || process.env.AITCC_CREDENTIAL_BACKEND === "file") return FILE_BACKEND;
|
|
6175
6254
|
const platform = opts.platform ?? process.platform;
|
|
6176
6255
|
switch (platform) {
|
|
6177
6256
|
case "darwin": return MACOS_BACKEND;
|
|
@@ -6223,15 +6302,14 @@ async function clearAuthState() {
|
|
|
6223
6302
|
/**
|
|
6224
6303
|
* Resolve credentials from the highest-priority source available:
|
|
6225
6304
|
* 1. `AITCC_EMAIL` + `AITCC_PASSWORD` env vars (CI single-shot use).
|
|
6226
|
-
* 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`.
|
|
6227
6308
|
*
|
|
6228
6309
|
* Returns `null` when no source is configured. The discriminated `kind`
|
|
6229
6310
|
* lets callers (e.g. the login flow) tell why a credential was found
|
|
6230
6311
|
* without having to peek at process env themselves — useful for
|
|
6231
6312
|
* "auto-login from CI" diagnostics.
|
|
6232
|
-
*
|
|
6233
|
-
* A future `'file'` source (~/.config/aitcc/credentials.json) is left as a
|
|
6234
|
-
* follow-up; once added, it slots between (1) and (2).
|
|
6235
6313
|
*/
|
|
6236
6314
|
async function loadCredentials(opts = {}) {
|
|
6237
6315
|
const env = opts.env ?? process.env;
|
|
@@ -6244,10 +6322,11 @@ async function loadCredentials(opts = {}) {
|
|
|
6244
6322
|
};
|
|
6245
6323
|
const state = await readAuthState();
|
|
6246
6324
|
if (!state) return null;
|
|
6247
|
-
const
|
|
6325
|
+
const backend = resolveBackend(opts);
|
|
6326
|
+
const password = await backend.get(state.activeEmail);
|
|
6248
6327
|
if (password === null) return null;
|
|
6249
6328
|
return {
|
|
6250
|
-
kind: "keychain",
|
|
6329
|
+
kind: backend.name === "file" ? "file" : "keychain",
|
|
6251
6330
|
email: state.activeEmail,
|
|
6252
6331
|
password
|
|
6253
6332
|
};
|
|
@@ -6298,7 +6377,7 @@ async function getActiveCredentialEmail(opts = {}) {
|
|
|
6298
6377
|
const state = await readAuthState();
|
|
6299
6378
|
if (!state) return null;
|
|
6300
6379
|
return {
|
|
6301
|
-
kind: "keychain",
|
|
6380
|
+
kind: env.AITCC_CREDENTIAL_BACKEND === "file" ? "file" : "keychain",
|
|
6302
6381
|
email: state.activeEmail
|
|
6303
6382
|
};
|
|
6304
6383
|
}
|
|
@@ -6975,6 +7054,81 @@ const completionCommand = defineCommand({
|
|
|
6975
7054
|
}
|
|
6976
7055
|
});
|
|
6977
7056
|
//#endregion
|
|
7057
|
+
//#region src/ait-token-profile.ts
|
|
7058
|
+
const AIT_CREDENTIALS_PATH = join(homedir(), ".ait", "credentials");
|
|
7059
|
+
function credentialsPath() {
|
|
7060
|
+
const override = process.env._AIT_CREDENTIALS_PATH_OVERRIDE;
|
|
7061
|
+
if (override && override.length > 0) return override;
|
|
7062
|
+
return AIT_CREDENTIALS_PATH;
|
|
7063
|
+
}
|
|
7064
|
+
function readCredentials(path) {
|
|
7065
|
+
if (!existsSync(path)) return {};
|
|
7066
|
+
try {
|
|
7067
|
+
const raw = readFileSync(path, "utf8");
|
|
7068
|
+
const parsed = JSON.parse(raw);
|
|
7069
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
7070
|
+
return {};
|
|
7071
|
+
} catch {
|
|
7072
|
+
return {};
|
|
7073
|
+
}
|
|
7074
|
+
}
|
|
7075
|
+
function writeCredentials(path, map) {
|
|
7076
|
+
const dir = join(path, "..");
|
|
7077
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
7078
|
+
writeFileSync(path, JSON.stringify(map, null, 2) + "\n", {
|
|
7079
|
+
encoding: "utf8",
|
|
7080
|
+
mode: 384
|
|
7081
|
+
});
|
|
7082
|
+
}
|
|
7083
|
+
/**
|
|
7084
|
+
* Save a Deploy Key to the `ait` token profile store.
|
|
7085
|
+
*
|
|
7086
|
+
* Tries direct-write first. If that fails and `ait` is on PATH, falls back
|
|
7087
|
+
* to spawning `ait token add --api-key ... <profile>`.
|
|
7088
|
+
*
|
|
7089
|
+
* SECRET-HANDLING: `apiKey` must not appear in any log or thrown message.
|
|
7090
|
+
* The `detail` field in failure results contains only non-secret context.
|
|
7091
|
+
*/
|
|
7092
|
+
function saveAitTokenProfile(profile, apiKey) {
|
|
7093
|
+
const path = credentialsPath();
|
|
7094
|
+
try {
|
|
7095
|
+
const map = readCredentials(path);
|
|
7096
|
+
map[profile] = apiKey;
|
|
7097
|
+
writeCredentials(path, map);
|
|
7098
|
+
return {
|
|
7099
|
+
ok: true,
|
|
7100
|
+
method: "direct",
|
|
7101
|
+
profile
|
|
7102
|
+
};
|
|
7103
|
+
} catch (err) {
|
|
7104
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
7105
|
+
try {
|
|
7106
|
+
execFileSync("ait", [
|
|
7107
|
+
"token",
|
|
7108
|
+
"add",
|
|
7109
|
+
"--api-key",
|
|
7110
|
+
apiKey,
|
|
7111
|
+
profile
|
|
7112
|
+
], {
|
|
7113
|
+
stdio: "ignore",
|
|
7114
|
+
timeout: 1e4,
|
|
7115
|
+
env: { ...process.env }
|
|
7116
|
+
});
|
|
7117
|
+
return {
|
|
7118
|
+
ok: true,
|
|
7119
|
+
method: "spawn",
|
|
7120
|
+
profile
|
|
7121
|
+
};
|
|
7122
|
+
} catch {
|
|
7123
|
+
return {
|
|
7124
|
+
ok: false,
|
|
7125
|
+
reason: "write-failed",
|
|
7126
|
+
detail
|
|
7127
|
+
};
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
//#endregion
|
|
6978
7132
|
//#region src/api/api-keys.ts
|
|
6979
7133
|
const BASE$2 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
|
|
6980
7134
|
async function fetchApiKeys(workspaceId, cookies, opts = {}) {
|
|
@@ -7130,6 +7284,10 @@ const createCommand = defineCommand({
|
|
|
7130
7284
|
type: "string",
|
|
7131
7285
|
description: "Workspace ID. Defaults to the selected workspace."
|
|
7132
7286
|
},
|
|
7287
|
+
"save-profile": {
|
|
7288
|
+
type: "string",
|
|
7289
|
+
description: "After issuing, save the key as an `ait` token profile (written to `~/.ait/credentials`). The named profile can then be used with `ait deploy --profile <name>` immediately. If omitted, the key is printed to stdout once and not persisted locally."
|
|
7290
|
+
},
|
|
7133
7291
|
json: {
|
|
7134
7292
|
type: "boolean",
|
|
7135
7293
|
description: "Emit machine-readable JSON to stdout.",
|
|
@@ -7179,6 +7337,14 @@ const createCommand = defineCommand({
|
|
|
7179
7337
|
name,
|
|
7180
7338
|
target
|
|
7181
7339
|
}, session.cookies);
|
|
7340
|
+
const saveProfileName = args["save-profile"] ? String(args["save-profile"]) : void 0;
|
|
7341
|
+
let savedProfile;
|
|
7342
|
+
let saveProfileWarning;
|
|
7343
|
+
if (saveProfileName !== void 0) {
|
|
7344
|
+
const saveResult = saveAitTokenProfile(saveProfileName, result.apiKey);
|
|
7345
|
+
if (saveResult.ok) savedProfile = saveResult.profile;
|
|
7346
|
+
else saveProfileWarning = `Could not save to ait profile "${saveProfileName}": ${saveResult.detail}. Save the key manually with \`ait token add --api-key <key> ` + saveProfileName + "`.";
|
|
7347
|
+
}
|
|
7182
7348
|
if (args.json) {
|
|
7183
7349
|
emitJson({
|
|
7184
7350
|
ok: true,
|
|
@@ -7189,12 +7355,16 @@ const createCommand = defineCommand({
|
|
|
7189
7355
|
isAll: target.isAll,
|
|
7190
7356
|
appNames: [...target.appNames]
|
|
7191
7357
|
},
|
|
7358
|
+
...savedProfile !== void 0 ? { savedProfile } : {},
|
|
7359
|
+
...saveProfileWarning !== void 0 ? { saveProfileWarning } : {},
|
|
7192
7360
|
extra: result.extra
|
|
7193
7361
|
});
|
|
7194
7362
|
return exitAfterFlush(ExitCode.Ok);
|
|
7195
7363
|
}
|
|
7196
7364
|
process.stdout.write(`${result.apiKey}\n`);
|
|
7197
|
-
process.stderr.write(
|
|
7365
|
+
if (savedProfile !== void 0) process.stderr.write(`Saved as ait profile "${savedProfile}". Run: ait deploy --profile ${savedProfile}\n`);
|
|
7366
|
+
else process.stderr.write("⚠️ This key is shown only once. Save it to a secret manager now — it cannot be retrieved later.\n");
|
|
7367
|
+
if (saveProfileWarning !== void 0) process.stderr.write(`Warning: ${saveProfileWarning}\n`);
|
|
7198
7368
|
return exitAfterFlush(ExitCode.Ok);
|
|
7199
7369
|
} catch (err) {
|
|
7200
7370
|
return emitFailureFromError(args.json, err);
|
|
@@ -7994,13 +8164,20 @@ const defaultPromptDeps = {
|
|
|
7994
8164
|
saveTarget: () => select({
|
|
7995
8165
|
message: "Where would you like to save the credentials?",
|
|
7996
8166
|
default: "keychain",
|
|
7997
|
-
choices: [
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
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
|
+
]
|
|
8004
8181
|
})
|
|
8005
8182
|
};
|
|
8006
8183
|
const loginCommand = defineCommand({
|
|
@@ -8039,7 +8216,7 @@ const loginCommand = defineCommand({
|
|
|
8039
8216
|
},
|
|
8040
8217
|
save: {
|
|
8041
8218
|
type: "string",
|
|
8042
|
-
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."
|
|
8043
8220
|
},
|
|
8044
8221
|
"skip-onboarding": {
|
|
8045
8222
|
type: "boolean",
|
|
@@ -8058,7 +8235,7 @@ const loginCommand = defineCommand({
|
|
|
8058
8235
|
save: typeof args.save === "string" ? args.save : void 0
|
|
8059
8236
|
}, {
|
|
8060
8237
|
getCredentials: loadCredentials,
|
|
8061
|
-
saveCredentials
|
|
8238
|
+
saveCredentials: (email, password, useFile) => saveCredentials(email, password, { useFile: useFile === true })
|
|
8062
8239
|
});
|
|
8063
8240
|
}
|
|
8064
8241
|
});
|
|
@@ -8108,7 +8285,8 @@ async function runLoginCommand(args, deps) {
|
|
|
8108
8285
|
return exitAfterFlush(resolved.exitCode);
|
|
8109
8286
|
}
|
|
8110
8287
|
let saved = "skipped";
|
|
8111
|
-
if (resolved.saveTarget === "keychain" && resolved.credentials !== null) {
|
|
8288
|
+
if ((resolved.saveTarget === "keychain" || resolved.saveTarget === "file") && resolved.credentials !== null) {
|
|
8289
|
+
const useFile = resolved.saveTarget === "file";
|
|
8112
8290
|
const save = deps.saveCredentials;
|
|
8113
8291
|
if (!save) {
|
|
8114
8292
|
emitError({
|
|
@@ -8118,16 +8296,25 @@ async function runLoginCommand(args, deps) {
|
|
|
8118
8296
|
return exitAfterFlush(ExitCode.Generic);
|
|
8119
8297
|
}
|
|
8120
8298
|
try {
|
|
8121
|
-
const result = await save(resolved.credentials.email, resolved.credentials.password);
|
|
8299
|
+
const result = await save(resolved.credentials.email, resolved.credentials.password, useFile);
|
|
8122
8300
|
saved = result.status;
|
|
8123
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`);
|
|
8124
8303
|
else process.stderr.write(`Credentials saved to OS keychain (${resolved.credentials.email}).\n`);
|
|
8125
8304
|
} catch (err) {
|
|
8126
8305
|
const message = err.message;
|
|
8127
|
-
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({
|
|
8128
8311
|
reason: "keychain-save-failed",
|
|
8129
8312
|
message
|
|
8130
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.`);
|
|
8131
8318
|
return exitAfterFlush(ExitCode.Usage);
|
|
8132
8319
|
}
|
|
8133
8320
|
}
|
|
@@ -8189,10 +8376,10 @@ async function resolveCredentialsForLogin(args, deps, opts = {}) {
|
|
|
8189
8376
|
};
|
|
8190
8377
|
let saveTarget;
|
|
8191
8378
|
if (args.save !== void 0) {
|
|
8192
|
-
if (args.save !== "keychain" && args.save !== "none") return {
|
|
8379
|
+
if (args.save !== "keychain" && args.save !== "file" && args.save !== "none") return {
|
|
8193
8380
|
kind: "error",
|
|
8194
8381
|
reason: "invalid-save",
|
|
8195
|
-
message: `--save must be "keychain" or "none" (got "${args.save}").`,
|
|
8382
|
+
message: `--save must be "keychain", "file", or "none" (got "${args.save}").`,
|
|
8196
8383
|
exitCode: ExitCode.Usage
|
|
8197
8384
|
};
|
|
8198
8385
|
saveTarget = args.save;
|
|
@@ -9237,7 +9424,7 @@ function resolveVersion() {
|
|
|
9237
9424
|
if (typeof injected === "string" && injected.length > 0) return injected;
|
|
9238
9425
|
} catch {}
|
|
9239
9426
|
try {
|
|
9240
|
-
return "0.1.
|
|
9427
|
+
return "0.1.37";
|
|
9241
9428
|
} catch {}
|
|
9242
9429
|
return "0.0.0-dev";
|
|
9243
9430
|
}
|
|
@@ -10294,6 +10481,7 @@ async function describeCredentialSource() {
|
|
|
10294
10481
|
function formatCredentials(cred) {
|
|
10295
10482
|
if (cred.source === "none") return "none (run `aitcc login` to save)";
|
|
10296
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})` : ""}`;
|
|
10297
10485
|
return `keychain${cred.email ? ` (${cred.email})` : ""}`;
|
|
10298
10486
|
}
|
|
10299
10487
|
async function runBackgroundUpdateCheck(json) {
|