@ait-co/console-cli 0.1.34 → 0.1.36

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 CHANGED
@@ -22,7 +22,7 @@ The installer detects OS (`uname -s`) and arch (`uname -m`), downloads the match
22
22
  Pin a specific version:
23
23
 
24
24
  ```sh
25
- curl -fsSL https://raw.githubusercontent.com/apps-in-toss-community/console-cli/main/install.sh | AITCC_VERSION=v0.1.1 sh
25
+ curl -fsSL https://raw.githubusercontent.com/apps-in-toss-community/console-cli/main/install.sh | AITCC_VERSION=v0.1.34 sh
26
26
  ```
27
27
 
28
28
  Override the install directory with `AITCC_INSTALL_DIR=/custom/path` (default `$HOME/.local/bin`).
@@ -128,7 +128,7 @@ For one-shot CI runs (e.g. `aitcc app deploy` from a workflow), seed the runner
128
128
 
129
129
  ```sh
130
130
  # Desktop (already logged in):
131
- aitcc auth export --format env >> $GITHUB_ENV # or: aitcc auth export --format env → store as a secret
131
+ aitcc auth export --format env >> $GITHUB_ENV # or store as a secret
132
132
  # CI (with the secret exposed as $AITCC_SESSION):
133
133
  aitcc app deploy ./aitc-sdk-example.ait --json
134
134
  ```
package/README.md CHANGED
@@ -22,7 +22,7 @@ installer가 OS(`uname -s`)와 아키텍처(`uname -m`)를 자동 감지해 최
22
22
  특정 버전 고정:
23
23
 
24
24
  ```sh
25
- curl -fsSL https://raw.githubusercontent.com/apps-in-toss-community/console-cli/main/install.sh | AITCC_VERSION=v0.1.1 sh
25
+ curl -fsSL https://raw.githubusercontent.com/apps-in-toss-community/console-cli/main/install.sh | AITCC_VERSION=v0.1.34 sh
26
26
  ```
27
27
 
28
28
  설치 경로를 바꾸려면 `AITCC_INSTALL_DIR=/custom/path` (기본 `$HOME/.local/bin`).
@@ -124,7 +124,7 @@ OS 키체인 대신 plain `0600` 파일을 쓰는 이유는 [CLAUDE.md](./CLAUDE
124
124
 
125
125
  ## CI/CD 통합
126
126
 
127
- 워크플로의 번성 실행(`aitcc app deploy` 등)을 위해 desktop에서 캡처한 세션을 runner에 주입합니다:
127
+ 워크플로의 일회성 실행(`aitcc app deploy` 등)을 위해 desktop에서 캡처한 세션을 runner에 주입합니다:
128
128
 
129
129
  ```sh
130
130
  # Desktop (이미 로그인된 상태):
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)) return {};
692
- return raw;
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({
@@ -3382,7 +3383,8 @@ function formatDiffValue(v) {
3382
3383
  return String(v);
3383
3384
  }
3384
3385
  function describeServiceStatus(s) {
3385
- if (s === "PREPARE") return "PREPARE (출시 전 — release 토글 미사용)";
3386
+ if (s === "PREPARE") return "PREPARE (출시 전 — 번들 출시 검수 미통과)";
3387
+ if (s === "OPENED") return "OPENED (출시 중)";
3386
3388
  if (s === "RUNNING") return "RUNNING (출시 중)";
3387
3389
  if (s === "STOPPED") return "STOPPED (운영 중지)";
3388
3390
  return s;
@@ -3583,6 +3585,9 @@ function deriveReviewState(env) {
3583
3585
  lockReason: locked ? "review-pending" : null
3584
3586
  };
3585
3587
  }
3588
+ function isInServiceStatus(serviceStatus) {
3589
+ return serviceStatus === "OPENED" || serviceStatus === "RUNNING";
3590
+ }
3586
3591
  function deriveLsStatus(derived, serviceStatus) {
3587
3592
  if (!derived) return {
3588
3593
  status: "unknown",
@@ -3592,7 +3597,7 @@ function deriveLsStatus(derived, serviceStatus) {
3592
3597
  const locked = derived.approvalType === "REVIEW";
3593
3598
  const lockReason = locked ? "review-pending" : null;
3594
3599
  let status = derived.state;
3595
- if (status === "approved" && serviceStatus === "RUNNING") status = "in-service";
3600
+ if (status === "approved" && isInServiceStatus(serviceStatus)) status = "in-service";
3596
3601
  return {
3597
3602
  status,
3598
3603
  locked,
@@ -4523,7 +4528,7 @@ const bundlesCommand = defineCommand({
4523
4528
  "test-links": defineCommand({
4524
4529
  meta: {
4525
4530
  name: "test-links",
4526
- description: "Show per-device test URLs for the mini-app."
4531
+ description: "Show the test deep-link URL for a specific bundle."
4527
4532
  },
4528
4533
  args: {
4529
4534
  id: {
@@ -4531,6 +4536,10 @@ const bundlesCommand = defineCommand({
4531
4536
  description: "Mini-app ID. Optional when `aitcc.yaml` provides `miniAppId`.",
4532
4537
  required: false
4533
4538
  },
4539
+ "deployment-id": {
4540
+ type: "string",
4541
+ description: "deploymentId of the bundle to fetch the test link for."
4542
+ },
4534
4543
  workspace: {
4535
4544
  type: "string",
4536
4545
  description: "Workspace ID. Defaults to the selected workspace."
@@ -4542,6 +4551,15 @@ const bundlesCommand = defineCommand({
4542
4551
  }
4543
4552
  },
4544
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
+ }
4545
4563
  const ctx = await resolveAppOrFail({
4546
4564
  json: args.json,
4547
4565
  appIdRaw: args.id,
@@ -4553,26 +4571,22 @@ const bundlesCommand = defineCommand({
4553
4571
  printContextHeader(ctx, { json: args.json });
4554
4572
  const { session, workspaceId } = ctx;
4555
4573
  try {
4556
- const links = await fetchBundleTestLinks(workspaceId, appId, session.cookies);
4574
+ const { linkUri } = await fetchBundleTestLinks(workspaceId, appId, deploymentId, session.cookies);
4557
4575
  if (args.json) {
4558
4576
  emitJson({
4559
4577
  ok: true,
4560
4578
  workspaceId,
4561
4579
  appId,
4562
- links
4580
+ deploymentId,
4581
+ linkUri
4563
4582
  });
4564
4583
  return exitAfterFlush(ExitCode.Ok);
4565
4584
  }
4566
- const keys = Object.keys(links);
4567
- if (keys.length === 0) {
4568
- 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`);
4569
4587
  return exitAfterFlush(ExitCode.Ok);
4570
4588
  }
4571
- process.stdout.write(`App ${appId} (ws ${workspaceId}):\n`);
4572
- for (const k of keys) {
4573
- const v = links[k];
4574
- process.stdout.write(` ${k}\t${typeof v === "string" ? v : JSON.stringify(v)}\n`);
4575
- }
4589
+ process.stdout.write(`App ${appId} (ws ${workspaceId}):\n ${linkUri}\n`);
4576
4590
  return exitAfterFlush(ExitCode.Ok);
4577
4591
  } catch (err) {
4578
4592
  return emitFailureFromError(args.json, err);
@@ -6971,6 +6985,81 @@ const completionCommand = defineCommand({
6971
6985
  }
6972
6986
  });
6973
6987
  //#endregion
6988
+ //#region src/ait-token-profile.ts
6989
+ const AIT_CREDENTIALS_PATH = join(homedir(), ".ait", "credentials");
6990
+ function credentialsPath() {
6991
+ const override = process.env._AIT_CREDENTIALS_PATH_OVERRIDE;
6992
+ if (override && override.length > 0) return override;
6993
+ return AIT_CREDENTIALS_PATH;
6994
+ }
6995
+ function readCredentials(path) {
6996
+ if (!existsSync(path)) return {};
6997
+ try {
6998
+ const raw = readFileSync(path, "utf8");
6999
+ const parsed = JSON.parse(raw);
7000
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
7001
+ return {};
7002
+ } catch {
7003
+ return {};
7004
+ }
7005
+ }
7006
+ function writeCredentials(path, map) {
7007
+ const dir = join(path, "..");
7008
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
7009
+ writeFileSync(path, JSON.stringify(map, null, 2) + "\n", {
7010
+ encoding: "utf8",
7011
+ mode: 384
7012
+ });
7013
+ }
7014
+ /**
7015
+ * Save a Deploy Key to the `ait` token profile store.
7016
+ *
7017
+ * Tries direct-write first. If that fails and `ait` is on PATH, falls back
7018
+ * to spawning `ait token add --api-key ... <profile>`.
7019
+ *
7020
+ * SECRET-HANDLING: `apiKey` must not appear in any log or thrown message.
7021
+ * The `detail` field in failure results contains only non-secret context.
7022
+ */
7023
+ function saveAitTokenProfile(profile, apiKey) {
7024
+ const path = credentialsPath();
7025
+ try {
7026
+ const map = readCredentials(path);
7027
+ map[profile] = apiKey;
7028
+ writeCredentials(path, map);
7029
+ return {
7030
+ ok: true,
7031
+ method: "direct",
7032
+ profile
7033
+ };
7034
+ } catch (err) {
7035
+ const detail = err instanceof Error ? err.message : String(err);
7036
+ try {
7037
+ execFileSync("ait", [
7038
+ "token",
7039
+ "add",
7040
+ "--api-key",
7041
+ apiKey,
7042
+ profile
7043
+ ], {
7044
+ stdio: "ignore",
7045
+ timeout: 1e4,
7046
+ env: { ...process.env }
7047
+ });
7048
+ return {
7049
+ ok: true,
7050
+ method: "spawn",
7051
+ profile
7052
+ };
7053
+ } catch {
7054
+ return {
7055
+ ok: false,
7056
+ reason: "write-failed",
7057
+ detail
7058
+ };
7059
+ }
7060
+ }
7061
+ }
7062
+ //#endregion
6974
7063
  //#region src/api/api-keys.ts
6975
7064
  const BASE$2 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
6976
7065
  async function fetchApiKeys(workspaceId, cookies, opts = {}) {
@@ -7126,6 +7215,10 @@ const createCommand = defineCommand({
7126
7215
  type: "string",
7127
7216
  description: "Workspace ID. Defaults to the selected workspace."
7128
7217
  },
7218
+ "save-profile": {
7219
+ type: "string",
7220
+ 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."
7221
+ },
7129
7222
  json: {
7130
7223
  type: "boolean",
7131
7224
  description: "Emit machine-readable JSON to stdout.",
@@ -7175,6 +7268,14 @@ const createCommand = defineCommand({
7175
7268
  name,
7176
7269
  target
7177
7270
  }, session.cookies);
7271
+ const saveProfileName = args["save-profile"] ? String(args["save-profile"]) : void 0;
7272
+ let savedProfile;
7273
+ let saveProfileWarning;
7274
+ if (saveProfileName !== void 0) {
7275
+ const saveResult = saveAitTokenProfile(saveProfileName, result.apiKey);
7276
+ if (saveResult.ok) savedProfile = saveResult.profile;
7277
+ else saveProfileWarning = `Could not save to ait profile "${saveProfileName}": ${saveResult.detail}. Save the key manually with \`ait token add --api-key <key> ` + saveProfileName + "`.";
7278
+ }
7178
7279
  if (args.json) {
7179
7280
  emitJson({
7180
7281
  ok: true,
@@ -7185,12 +7286,16 @@ const createCommand = defineCommand({
7185
7286
  isAll: target.isAll,
7186
7287
  appNames: [...target.appNames]
7187
7288
  },
7289
+ ...savedProfile !== void 0 ? { savedProfile } : {},
7290
+ ...saveProfileWarning !== void 0 ? { saveProfileWarning } : {},
7188
7291
  extra: result.extra
7189
7292
  });
7190
7293
  return exitAfterFlush(ExitCode.Ok);
7191
7294
  }
7192
7295
  process.stdout.write(`${result.apiKey}\n`);
7193
- process.stderr.write("⚠️ This key is shown only once. Save it to a secret manager now — it cannot be retrieved later.\n");
7296
+ if (savedProfile !== void 0) process.stderr.write(`Saved as ait profile "${savedProfile}". Run: ait deploy --profile ${savedProfile}\n`);
7297
+ else process.stderr.write("⚠️ This key is shown only once. Save it to a secret manager now — it cannot be retrieved later.\n");
7298
+ if (saveProfileWarning !== void 0) process.stderr.write(`Warning: ${saveProfileWarning}\n`);
7194
7299
  return exitAfterFlush(ExitCode.Ok);
7195
7300
  } catch (err) {
7196
7301
  return emitFailureFromError(args.json, err);
@@ -9233,7 +9338,7 @@ function resolveVersion() {
9233
9338
  if (typeof injected === "string" && injected.length > 0) return injected;
9234
9339
  } catch {}
9235
9340
  try {
9236
- return "0.1.34";
9341
+ return "0.1.36";
9237
9342
  } catch {}
9238
9343
  return "0.0.0-dev";
9239
9344
  }