@ait-co/console-cli 0.1.23 → 0.1.25

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/dist/cli.mjs CHANGED
@@ -164,6 +164,48 @@ async function executeAndUnwrap(url, init, fetchImpl) {
164
164
  throw new TossApiError(res.status, parsed.error.errorCode, parsed.error.reason, parsed.error.errorType);
165
165
  }
166
166
  //#endregion
167
+ //#region src/api/certs.ts
168
+ const BASE$5 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
169
+ async function fetchCerts(workspaceId, miniAppId, cookies, opts = {}) {
170
+ const raw = await requestConsoleApi({
171
+ url: `${BASE$5}/workspaces/${workspaceId}/mini-app/${miniAppId}/certs`,
172
+ cookies,
173
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
174
+ });
175
+ if (!Array.isArray(raw)) throw new Error(`Unexpected certs shape for app=${miniAppId}: not an array`);
176
+ return raw.map((c) => {
177
+ if (c === null || typeof c !== "object") return {};
178
+ return c;
179
+ });
180
+ }
181
+ async function issueCert(workspaceId, miniAppId, name, cookies, opts = {}) {
182
+ const raw = await requestConsoleApi({
183
+ url: `${BASE$5}/workspaces/${workspaceId}/mini-app/${miniAppId}/cert/issue`,
184
+ method: "POST",
185
+ body: { name },
186
+ cookies,
187
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
188
+ });
189
+ if (raw === null || typeof raw !== "object") throw new Error(`Unexpected issue-cert shape for app=${miniAppId}: not an object`);
190
+ const rec = raw;
191
+ const privateKey = typeof rec.privateKey === "string" ? rec.privateKey : null;
192
+ const publicKey = typeof rec.publicKey === "string" ? rec.publicKey : null;
193
+ if (privateKey === null || publicKey === null) throw new Error(`Unexpected issue-cert shape for app=${miniAppId}: missing privateKey/publicKey`);
194
+ return {
195
+ privateKey,
196
+ publicKey
197
+ };
198
+ }
199
+ async function revokeCert(workspaceId, miniAppId, certId, cookies, opts = {}) {
200
+ await requestConsoleApi({
201
+ url: `${BASE$5}/workspaces/${workspaceId}/mini-app/${miniAppId}/certs/${encodeURIComponent(certId)}/disable`,
202
+ method: "POST",
203
+ body: {},
204
+ cookies,
205
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
206
+ });
207
+ }
208
+ //#endregion
167
209
  //#region src/api/mini-apps.ts
168
210
  const BASE$4 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
169
211
  async function fetchMiniApps(workspaceId, cookies, opts = {}) {
@@ -329,45 +371,6 @@ async function fetchConversionMetrics(params, cookies, opts = {}) {
329
371
  cacheTime: typeof rec.cacheTime === "string" ? rec.cacheTime : void 0
330
372
  };
331
373
  }
332
- async function fetchCerts(workspaceId, miniAppId, cookies, opts = {}) {
333
- const raw = await requestConsoleApi({
334
- url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/certs`,
335
- cookies,
336
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
337
- });
338
- if (!Array.isArray(raw)) throw new Error(`Unexpected certs shape for app=${miniAppId}: not an array`);
339
- return raw.map((c) => {
340
- if (c === null || typeof c !== "object") return {};
341
- return c;
342
- });
343
- }
344
- async function issueCert(workspaceId, miniAppId, name, cookies, opts = {}) {
345
- const raw = await requestConsoleApi({
346
- url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/cert/issue`,
347
- method: "POST",
348
- body: { name },
349
- cookies,
350
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
351
- });
352
- if (raw === null || typeof raw !== "object") throw new Error(`Unexpected issue-cert shape for app=${miniAppId}: not an object`);
353
- const rec = raw;
354
- const privateKey = typeof rec.privateKey === "string" ? rec.privateKey : null;
355
- const publicKey = typeof rec.publicKey === "string" ? rec.publicKey : null;
356
- if (privateKey === null || publicKey === null) throw new Error(`Unexpected issue-cert shape for app=${miniAppId}: missing privateKey/publicKey`);
357
- return {
358
- privateKey,
359
- publicKey
360
- };
361
- }
362
- async function revokeCert(workspaceId, miniAppId, certId, cookies, opts = {}) {
363
- await requestConsoleApi({
364
- url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/certs/${encodeURIComponent(certId)}/disable`,
365
- method: "POST",
366
- body: {},
367
- cookies,
368
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
369
- });
370
- }
371
374
  async function fetchShareRewards(params, cookies, opts = {}) {
372
375
  const qs = new URLSearchParams();
373
376
  qs.set("search", params.search ?? "");
@@ -1587,6 +1590,153 @@ function printContextHeader(ctx, opts) {
1587
1590
  process.stderr.write(line);
1588
1591
  }
1589
1592
  //#endregion
1593
+ //#region src/api/me.ts
1594
+ const BASE$3 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
1595
+ const MEMBER_USER_INFO_URL = `${BASE$3}/members/me/user-info`;
1596
+ async function fetchConsoleMemberUserInfo(cookies, opts = {}) {
1597
+ return requestConsoleApi({
1598
+ url: MEMBER_USER_INFO_URL,
1599
+ cookies,
1600
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1601
+ });
1602
+ }
1603
+ async function fetchUserTerms(cookies, opts = {}) {
1604
+ const raw = await requestConsoleApi({
1605
+ url: `${BASE$3}/console-user-terms/me`,
1606
+ cookies,
1607
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1608
+ });
1609
+ if (!Array.isArray(raw)) throw new Error("Unexpected user-terms shape: not an array");
1610
+ return raw.map((entry, i) => {
1611
+ if (!entry || typeof entry !== "object") throw new Error(`Unexpected user-terms entry at index ${i}`);
1612
+ const e = entry;
1613
+ return {
1614
+ required: Boolean(e.required),
1615
+ termsId: typeof e.termsId === "number" ? e.termsId : 0,
1616
+ revisionId: typeof e.revisionId === "number" ? e.revisionId : 0,
1617
+ title: typeof e.title === "string" ? e.title : "",
1618
+ contentsUrl: typeof e.contentsUrl === "string" ? e.contentsUrl : "",
1619
+ actionType: typeof e.actionType === "string" ? e.actionType : "",
1620
+ isAgreed: Boolean(e.isAgreed),
1621
+ isOneTimeConsent: Boolean(e.isOneTimeConsent)
1622
+ };
1623
+ });
1624
+ }
1625
+ //#endregion
1626
+ //#region src/api/workspaces.ts
1627
+ const WORKSPACES_BASE = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
1628
+ async function fetchWorkspaceDetail(workspaceId, cookies, opts = {}) {
1629
+ const raw = await requestConsoleApi({
1630
+ url: `${WORKSPACES_BASE}/workspaces/${workspaceId}`,
1631
+ cookies,
1632
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1633
+ });
1634
+ const id = raw.id;
1635
+ const name = raw.name;
1636
+ if (typeof id !== "number" || !Number.isInteger(id) || id <= 0 || typeof name !== "string") throw new Error(`Unexpected workspace detail shape for id=${workspaceId}`);
1637
+ const { id: _id, name: _name, ...extra } = raw;
1638
+ return {
1639
+ workspaceId: id,
1640
+ workspaceName: name,
1641
+ extra
1642
+ };
1643
+ }
1644
+ async function fetchWorkspacePartner(workspaceId, cookies, opts = {}) {
1645
+ const raw = await requestConsoleApi({
1646
+ url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/partner`,
1647
+ cookies,
1648
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1649
+ });
1650
+ const registered = raw.registered;
1651
+ if (typeof registered !== "boolean") throw new Error(`Unexpected workspace partner shape for id=${workspaceId}`);
1652
+ return {
1653
+ registered,
1654
+ approvalType: typeof raw.approvalType === "string" ? raw.approvalType : null,
1655
+ rejectMessage: typeof raw.rejectMessage === "string" ? raw.rejectMessage : null,
1656
+ partner: raw.partner && typeof raw.partner === "object" ? raw.partner : null
1657
+ };
1658
+ }
1659
+ const WORKSPACE_TERM_TYPES = [
1660
+ "TOSS_LOGIN",
1661
+ "BIZ_WORKSPACE",
1662
+ "TOSS_PROMOTION_MONEY",
1663
+ "IAA",
1664
+ "IAP"
1665
+ ];
1666
+ const DEFAULT_SEGMENT_CATEGORY = "생성된 세그먼트";
1667
+ async function fetchWorkspaceSegments(params, cookies, opts = {}) {
1668
+ const page = params.page ?? 0;
1669
+ const qs = new URLSearchParams();
1670
+ qs.set("category", params.category ?? DEFAULT_SEGMENT_CATEGORY);
1671
+ qs.set("search", params.search ?? "");
1672
+ qs.set("page", String(page));
1673
+ const raw = await requestConsoleApi({
1674
+ url: `${WORKSPACES_BASE}/workspaces/${params.workspaceId}/segments/list?${qs.toString()}`,
1675
+ cookies,
1676
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1677
+ });
1678
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`Unexpected segments shape for workspace=${params.workspaceId}`);
1679
+ const data = raw;
1680
+ return {
1681
+ contents: (Array.isArray(data.contents) ? data.contents : []).map((c) => c && typeof c === "object" ? c : {}),
1682
+ totalPage: typeof data.totalPage === "number" ? data.totalPage : 0,
1683
+ currentPage: typeof data.currentPage === "number" ? data.currentPage : page
1684
+ };
1685
+ }
1686
+ async function fetchWorkspaceTerms(workspaceId, type, cookies, opts = {}) {
1687
+ const raw = await requestConsoleApi({
1688
+ url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/console-workspace-terms/${type}/skip-permission`,
1689
+ cookies,
1690
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1691
+ });
1692
+ if (!Array.isArray(raw)) throw new Error(`Unexpected workspace terms shape for type=${type}`);
1693
+ return raw.map((entry, i) => {
1694
+ if (!entry || typeof entry !== "object") throw new Error(`Unexpected workspace terms entry at index ${i} for type=${type}`);
1695
+ const e = entry;
1696
+ return {
1697
+ required: Boolean(e.required),
1698
+ termsId: typeof e.termsId === "number" ? e.termsId : 0,
1699
+ revisionId: typeof e.revisionId === "number" ? e.revisionId : 0,
1700
+ title: typeof e.title === "string" ? e.title : "",
1701
+ contentsUrl: typeof e.contentsUrl === "string" ? e.contentsUrl : "",
1702
+ actionType: typeof e.actionType === "string" ? e.actionType : "",
1703
+ isAgreed: Boolean(e.isAgreed),
1704
+ isOneTimeConsent: Boolean(e.isOneTimeConsent)
1705
+ };
1706
+ });
1707
+ }
1708
+ /**
1709
+ * Persist agreement for one-or-more workspace terms. The endpoint takes a
1710
+ * single `agreedList` regardless of which bucket the terms came from — the
1711
+ * type tag is implicit in the (termsId, revisionId) pairs.
1712
+ *
1713
+ * Captured behaviour (2026-05-08, ws=36577):
1714
+ * - `POST /workspaces/<wid>/console-workspace-terms` with body
1715
+ * `{"agreedList":[{"termsId": <int>, "revisionId": <int>}, ...]}`
1716
+ * - Response on success: `{"resultType":"SUCCESS","success":{}}` — no
1717
+ * useful payload. We resolve `void`.
1718
+ * - Re-submitting an already-agreed term returns `errorCode: 500`
1719
+ * (Internal Server Error). The server is NOT idempotent, so callers
1720
+ * must filter to `isAgreed === false` before invoking.
1721
+ * - Empty `agreedList` returns SUCCESS (no-op), but we throw client-side
1722
+ * before sending — round-tripping a no-op request is wasted.
1723
+ *
1724
+ * Failure surfaces through `TossApiError` like every other write helper.
1725
+ */
1726
+ async function agreeWorkspaceTerms(workspaceId, terms, cookies, opts = {}) {
1727
+ if (terms.length === 0) throw new Error("agreeWorkspaceTerms requires at least one term");
1728
+ await requestConsoleApi({
1729
+ method: "POST",
1730
+ url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/console-workspace-terms`,
1731
+ cookies,
1732
+ body: { agreedList: terms.map(({ termsId, revisionId }) => ({
1733
+ termsId,
1734
+ revisionId
1735
+ })) },
1736
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1737
+ });
1738
+ }
1739
+ //#endregion
1590
1740
  //#region src/config/ait-bundle.ts
1591
1741
  var AitBundleError = class extends Error {
1592
1742
  path;
@@ -1878,30 +2028,21 @@ async function runDeploy(args, deps = {}) {
1878
2028
  const steps = ["upload"];
1879
2029
  if (requestReview) steps.push("review");
1880
2030
  if (release) steps.push("release");
1881
- if (args.dryRun) {
1882
- if (args.json) emitJson({
1883
- ok: true,
1884
- dryRun: true,
1885
- workspaceId,
1886
- appId,
1887
- deploymentId,
1888
- bundleFormat: bundleInfo.format,
1889
- bytes: bundleInfo.bytes.byteLength,
1890
- steps,
1891
- memo: memo ?? null,
1892
- releaseNotes: releaseNotes ?? null,
1893
- confirmed: confirm
1894
- });
1895
- else {
1896
- const stepsLine = steps.map((s) => {
1897
- if (s === "review") return `review (releaseNotes: ${JSON.stringify(releaseNotes ?? "")})`;
1898
- if (s === "release") return `release (${confirm ? "confirmed" : "NOT confirmed"})`;
1899
- return s;
1900
- }).join(" → ");
1901
- process.stdout.write(`DRY RUN\n app ${appId}\n workspace ${workspaceId}\n bundle ${args.path} (${bundleInfo.bytes.byteLength} bytes)\n deploymentId ${deploymentId}\n memo ${memo ?? "(none)"}\n steps ${stepsLine}\n`);
1902
- }
1903
- return exitAfterFlush(ExitCode.Ok);
1904
- }
2031
+ if (args.dryRun) return runDryRun({
2032
+ json: args.json,
2033
+ path: args.path,
2034
+ bundleInfo,
2035
+ deploymentId,
2036
+ explicitDeploymentId: typeof args.deploymentId === "string" && args.deploymentId !== "",
2037
+ workspaceId,
2038
+ appId,
2039
+ session,
2040
+ steps,
2041
+ memo,
2042
+ releaseNotes,
2043
+ confirm,
2044
+ fetchImpl: deps.fetchImpl
2045
+ });
1905
2046
  const apiOpts = deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {};
1906
2047
  let uploaded = false;
1907
2048
  let bundleRecord = null;
@@ -2037,38 +2178,154 @@ async function emitPartialFailure(json, err, progress) {
2037
2178
  else process.stderr.write(`Unexpected error: ${err.message}\n`);
2038
2179
  return exitAfterFlush(ExitCode.ApiError);
2039
2180
  }
2040
- //#endregion
2041
- //#region src/api/me.ts
2042
- const BASE$3 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
2043
- const MEMBER_USER_INFO_URL = `${BASE$3}/members/me/user-info`;
2044
- async function fetchConsoleMemberUserInfo(cookies, opts = {}) {
2045
- return requestConsoleApi({
2046
- url: MEMBER_USER_INFO_URL,
2047
- cookies,
2048
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
2049
- });
2181
+ const WORKSPACE_TERM_ERROR_CODES = {
2182
+ TOSS_LOGIN: 4037,
2183
+ BIZ_WORKSPACE: 4040,
2184
+ TOSS_PROMOTION_MONEY: 4039,
2185
+ IAA: 4099,
2186
+ IAP: 5001
2187
+ };
2188
+ async function runDryRun(input) {
2189
+ const apiOpts = input.fetchImpl ? { fetchImpl: input.fetchImpl } : {};
2190
+ const embedded = input.bundleInfo.deploymentId;
2191
+ const flagMatch = input.explicitDeploymentId ? input.deploymentId === embedded : null;
2192
+ const [permissions, terms] = await Promise.all([fetchPermissions(input.workspaceId, input.session, apiOpts), fetchTermsBlockers(input.workspaceId, input.session, apiOpts)]);
2193
+ const wouldSucceed = (flagMatch === null || flagMatch === true) && terms.blockers.length === 0;
2194
+ if (input.json) {
2195
+ emitJson({
2196
+ ok: true,
2197
+ dryRun: true,
2198
+ wouldSucceed,
2199
+ workspaceId: input.workspaceId,
2200
+ appId: input.appId,
2201
+ deploymentId: input.deploymentId,
2202
+ bundleFormat: input.bundleInfo.format,
2203
+ bytes: input.bundleInfo.bytes.byteLength,
2204
+ steps: input.steps,
2205
+ memo: input.memo ?? null,
2206
+ releaseNotes: input.releaseNotes ?? null,
2207
+ confirmed: input.confirm,
2208
+ bundle: {
2209
+ path: input.path,
2210
+ format: input.bundleInfo.format,
2211
+ deploymentId: input.deploymentId,
2212
+ embeddedDeploymentId: embedded,
2213
+ deploymentIdSource: input.explicitDeploymentId ? "flag" : "bundle",
2214
+ flagMatch,
2215
+ size: input.bundleInfo.bytes.byteLength
2216
+ },
2217
+ context: {
2218
+ workspaceId: input.workspaceId,
2219
+ appId: input.appId,
2220
+ sessionValid: true,
2221
+ permissions
2222
+ },
2223
+ terms
2224
+ });
2225
+ return exitAfterFlush(ExitCode.Ok);
2226
+ }
2227
+ process.stdout.write(renderDryRunText(input, {
2228
+ embedded,
2229
+ flagMatch,
2230
+ permissions,
2231
+ terms,
2232
+ wouldSucceed
2233
+ }));
2234
+ return exitAfterFlush(ExitCode.Ok);
2050
2235
  }
2051
- async function fetchUserTerms(cookies, opts = {}) {
2052
- const raw = await requestConsoleApi({
2053
- url: `${BASE$3}/console-user-terms/me`,
2054
- cookies,
2055
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
2056
- });
2057
- if (!Array.isArray(raw)) throw new Error("Unexpected user-terms shape: not an array");
2058
- return raw.map((entry, i) => {
2059
- if (!entry || typeof entry !== "object") throw new Error(`Unexpected user-terms entry at index ${i}`);
2060
- const e = entry;
2236
+ async function fetchPermissions(workspaceId, session, apiOpts) {
2237
+ try {
2238
+ const ws = (await fetchConsoleMemberUserInfo(session.cookies, apiOpts)).workspaces.find((w) => w.workspaceId === workspaceId);
2239
+ if (!ws) return {
2240
+ role: null,
2241
+ source: "unknown",
2242
+ error: `current user has no membership in workspace ${workspaceId}`
2243
+ };
2061
2244
  return {
2062
- required: Boolean(e.required),
2063
- termsId: typeof e.termsId === "number" ? e.termsId : 0,
2064
- revisionId: typeof e.revisionId === "number" ? e.revisionId : 0,
2065
- title: typeof e.title === "string" ? e.title : "",
2066
- contentsUrl: typeof e.contentsUrl === "string" ? e.contentsUrl : "",
2067
- actionType: typeof e.actionType === "string" ? e.actionType : "",
2068
- isAgreed: Boolean(e.isAgreed),
2069
- isOneTimeConsent: Boolean(e.isOneTimeConsent)
2245
+ role: ws.role,
2246
+ source: "members/me"
2070
2247
  };
2071
- });
2248
+ } catch (err) {
2249
+ return {
2250
+ role: null,
2251
+ source: "unknown",
2252
+ error: err.message
2253
+ };
2254
+ }
2255
+ }
2256
+ async function fetchTermsBlockers(workspaceId, session, apiOpts) {
2257
+ try {
2258
+ const [userTerms, workspaceResults] = await Promise.all([fetchUserTerms(session.cookies, apiOpts), Promise.all(WORKSPACE_TERM_TYPES.map(async (t) => [t, await fetchWorkspaceTerms(workspaceId, t, session.cookies, apiOpts)]))]);
2259
+ const blockers = [];
2260
+ for (const t of userTerms) if (t.required && !t.isAgreed) blockers.push({
2261
+ scope: "user",
2262
+ type: "USER_TERMS",
2263
+ errorCode: 4036,
2264
+ title: t.title,
2265
+ action: "aitcc me terms"
2266
+ });
2267
+ for (const [type, terms] of workspaceResults) for (const t of terms) {
2268
+ if (!t.required || t.isAgreed) continue;
2269
+ blockers.push({
2270
+ scope: "workspace",
2271
+ type,
2272
+ errorCode: WORKSPACE_TERM_ERROR_CODES[type],
2273
+ title: t.title,
2274
+ action: `aitcc workspace terms --type ${type}`
2275
+ });
2276
+ }
2277
+ return {
2278
+ blockers,
2279
+ checked: true
2280
+ };
2281
+ } catch (err) {
2282
+ return {
2283
+ blockers: [],
2284
+ checked: false,
2285
+ error: err.message
2286
+ };
2287
+ }
2288
+ }
2289
+ function renderDryRunText(input, derived) {
2290
+ const lines = [];
2291
+ lines.push(`DRY RUN — app deploy ${input.appId}\n`);
2292
+ lines.push("\nBundle\n");
2293
+ lines.push(` path ${input.path}\n`);
2294
+ lines.push(` format ${input.bundleInfo.format.toUpperCase()}\n`);
2295
+ lines.push(` deploymentId ${input.deploymentId}\n`);
2296
+ if (derived.flagMatch === false) lines.push(` flag match MISMATCH (bundle embeds ${derived.embedded})\n`);
2297
+ else if (derived.flagMatch === true) lines.push(` flag match ok (matches embedded)\n`);
2298
+ lines.push(` size ${formatBytes(input.bundleInfo.bytes.byteLength)}\n`);
2299
+ lines.push("\nContext\n");
2300
+ lines.push(` workspace ${input.workspaceId}\n`);
2301
+ lines.push(` app ${input.appId}\n`);
2302
+ lines.push(` session valid\n`);
2303
+ if (derived.permissions.role !== null) lines.push(` permissions ${derived.permissions.role}\n`);
2304
+ else lines.push(` permissions unknown${derived.permissions.error ? ` (${derived.permissions.error})` : ""}\n`);
2305
+ lines.push("\nTerms\n");
2306
+ if (!derived.terms.checked) {
2307
+ lines.push(` warning: could not check terms status (${derived.terms.error ?? "unknown error"}).\n`);
2308
+ lines.push(" live deploy may still fail with a 4032/4036/4037/4039/4040/4099/5001 errorCode.\n");
2309
+ } else if (derived.terms.blockers.length === 0) lines.push(" all deploy-related terms are agreed\n");
2310
+ else for (const b of derived.terms.blockers) lines.push(` blocked: ${b.scope}/${b.type} — ${b.title} (errorCode ${b.errorCode})\n action: ${b.action}\n`);
2311
+ lines.push("\nPlan\n");
2312
+ const stepsLine = input.steps.map((s) => {
2313
+ if (s === "review") return `review (releaseNotes: ${JSON.stringify(input.releaseNotes ?? "")})`;
2314
+ if (s === "release") return `release (${input.confirm ? "confirmed" : "NOT confirmed"})`;
2315
+ return s;
2316
+ }).join(" → ");
2317
+ lines.push(` steps ${stepsLine}\n`);
2318
+ lines.push(` memo ${input.memo ?? "(none)"}\n`);
2319
+ lines.push("\nResult\n");
2320
+ if (!derived.wouldSucceed) lines.push(" Live deploy would fail. Resolve the blocked items above, then re-run.\n");
2321
+ else if (derived.permissions.role === null) lines.push(" Live deploy would clear bundle + terms checks. Workspace membership could not be confirmed; live deploy may still fail with a permissions error.\n");
2322
+ else lines.push(" Live deploy would clear every pre-flight check.\n");
2323
+ return lines.join("");
2324
+ }
2325
+ function formatBytes(n) {
2326
+ if (n < 1024) return `${n} B`;
2327
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
2328
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
2072
2329
  }
2073
2330
  //#endregion
2074
2331
  //#region src/config/app-manifest.ts
@@ -2336,7 +2593,7 @@ async function runAppInit(args) {
2336
2593
  workspaceId = await pickWorkspace(session.cookies);
2337
2594
  categoryIds = await pickCategories(session.cookies);
2338
2595
  } catch (err) {
2339
- if (isPromptCancelled$1(err)) {
2596
+ if (isPromptCancelled$2(err)) {
2340
2597
  process.stderr.write("Aborted.\n");
2341
2598
  return exitAfterFlush(ExitCode.Usage);
2342
2599
  }
@@ -2379,7 +2636,7 @@ async function runAppInit(args) {
2379
2636
  categoryIds
2380
2637
  };
2381
2638
  } catch (err) {
2382
- if (isPromptCancelled$1(err)) {
2639
+ if (isPromptCancelled$2(err)) {
2383
2640
  process.stderr.write("Aborted.\n");
2384
2641
  return exitAfterFlush(ExitCode.Usage);
2385
2642
  }
@@ -2438,7 +2695,7 @@ async function fileExists(path) {
2438
2695
  return false;
2439
2696
  }
2440
2697
  }
2441
- function isPromptCancelled$1(err) {
2698
+ function isPromptCancelled$2(err) {
2442
2699
  return err instanceof Error && err.name === "ExitPromptError";
2443
2700
  }
2444
2701
  async function pickWorkspace(cookies) {
@@ -4296,6 +4553,25 @@ const bundlesCommand = defineCommand({
4296
4553
  })
4297
4554
  }
4298
4555
  });
4556
+ const CERT_EXPIRY_WARN_DAYS = 30;
4557
+ const MS_PER_DAY = 864e5;
4558
+ function deriveDaysUntilExpiry(cert, now) {
4559
+ const raw = cert.expireTs;
4560
+ let ts = null;
4561
+ if (typeof raw === "number" && Number.isFinite(raw)) ts = raw;
4562
+ else if (typeof raw === "string") {
4563
+ const parsed = Date.parse(raw);
4564
+ if (Number.isFinite(parsed)) ts = parsed;
4565
+ }
4566
+ if (ts === null) return null;
4567
+ return Math.floor((ts - now) / MS_PER_DAY);
4568
+ }
4569
+ function expiryMarker(days) {
4570
+ if (days === null) return "";
4571
+ if (days < 0) return `\t⚠ 만료됨`;
4572
+ if (days <= CERT_EXPIRY_WARN_DAYS) return `\t⚠ 만료 임박 (${days}일)`;
4573
+ return "";
4574
+ }
4299
4575
  const certsLsCommand = defineCommand({
4300
4576
  meta: {
4301
4577
  name: "ls",
@@ -4330,27 +4606,153 @@ const certsLsCommand = defineCommand({
4330
4606
  const { session, workspaceId } = ctx;
4331
4607
  try {
4332
4608
  const certs = await fetchCerts(workspaceId, appId, session.cookies);
4609
+ const now = Date.now();
4610
+ const augmented = certs.map((c) => ({
4611
+ ...c,
4612
+ daysUntilExpiry: deriveDaysUntilExpiry(c, now)
4613
+ }));
4614
+ if (args.json) {
4615
+ emitJson({
4616
+ ok: true,
4617
+ workspaceId,
4618
+ appId,
4619
+ certs: augmented
4620
+ });
4621
+ return exitAfterFlush(ExitCode.Ok);
4622
+ }
4623
+ if (augmented.length === 0) {
4624
+ process.stdout.write(`App ${appId} (ws ${workspaceId}): no mTLS certs\n`);
4625
+ return exitAfterFlush(ExitCode.Ok);
4626
+ }
4627
+ process.stdout.write(`App ${appId} (ws ${workspaceId}): ${augmented.length} cert(s)\n`);
4628
+ for (const c of augmented) {
4629
+ const id = typeof c.id === "string" || typeof c.id === "number" ? c.id : typeof c.certId === "string" || typeof c.certId === "number" ? c.certId : "-";
4630
+ const cn = typeof c.commonName === "string" ? c.commonName : "-";
4631
+ const createdAt = typeof c.createdAt === "string" ? c.createdAt : "";
4632
+ const expiresAt = typeof c.expiresAt === "string" ? c.expiresAt : typeof c.validUntil === "string" ? c.validUntil : typeof c.expireTs === "number" && Number.isFinite(c.expireTs) ? new Date(c.expireTs).toISOString() : "";
4633
+ process.stdout.write(`${id}\t${cn}\t${createdAt}\t${expiresAt}${expiryMarker(c.daysUntilExpiry)}\n`);
4634
+ }
4635
+ return exitAfterFlush(ExitCode.Ok);
4636
+ } catch (err) {
4637
+ return emitFailureFromError(args.json, err);
4638
+ }
4639
+ }
4640
+ });
4641
+ function pickCertById(certs, certId) {
4642
+ const target = certId.trim();
4643
+ if (target.length === 0) return null;
4644
+ for (const c of certs) {
4645
+ const candidate = typeof c.id === "string" || typeof c.id === "number" ? c.id : typeof c.certId === "string" || typeof c.certId === "number" ? c.certId : null;
4646
+ if (candidate !== null && String(candidate) === target) return c;
4647
+ }
4648
+ return null;
4649
+ }
4650
+ function augmentCertExpiry(cert, now = Date.now()) {
4651
+ let expiresAtMs;
4652
+ if (typeof cert.expireTs === "number" && Number.isFinite(cert.expireTs)) expiresAtMs = cert.expireTs;
4653
+ else if (typeof cert.expiresAt === "string") {
4654
+ const t = Date.parse(cert.expiresAt);
4655
+ if (Number.isFinite(t)) expiresAtMs = t;
4656
+ } else if (typeof cert.validUntil === "string") {
4657
+ const t = Date.parse(cert.validUntil);
4658
+ if (Number.isFinite(t)) expiresAtMs = t;
4659
+ }
4660
+ if (expiresAtMs === void 0) return {};
4661
+ const daysUntilExpiry = Math.floor((expiresAtMs - now) / 864e5);
4662
+ return {
4663
+ expiresAtMs,
4664
+ daysUntilExpiry
4665
+ };
4666
+ }
4667
+ const certsShowCommand = defineCommand({
4668
+ meta: {
4669
+ name: "show",
4670
+ description: "Show a single mTLS certificate by id (metadata only — no PEM)."
4671
+ },
4672
+ args: {
4673
+ certId: {
4674
+ type: "positional",
4675
+ description: "Cert ID (from `app certs ls`).",
4676
+ required: true
4677
+ },
4678
+ app: {
4679
+ type: "string",
4680
+ description: "Mini-app ID the cert belongs to."
4681
+ },
4682
+ workspace: {
4683
+ type: "string",
4684
+ description: "Workspace ID. Defaults to the selected workspace."
4685
+ },
4686
+ json: {
4687
+ type: "boolean",
4688
+ description: "Emit machine-readable JSON.",
4689
+ default: false
4690
+ }
4691
+ },
4692
+ async run({ args }) {
4693
+ const certId = typeof args.certId === "string" ? args.certId.trim() : "";
4694
+ if (certId.length === 0) {
4695
+ if (args.json) emitJson({
4696
+ ok: false,
4697
+ reason: "missing-cert-id",
4698
+ message: "certId positional is required"
4699
+ });
4700
+ else process.stderr.write("app certs show: certId is required\n");
4701
+ return exitAfterFlush(ExitCode.Usage);
4702
+ }
4703
+ const ctx = await resolveAppOrFail({
4704
+ json: args.json,
4705
+ appIdRaw: args.app,
4706
+ appIdField: "app",
4707
+ ...args.workspace !== void 0 ? { workspace: args.workspace } : {}
4708
+ });
4709
+ if (!ctx) return;
4710
+ const appId = await requireMiniAppId(ctx, args.json);
4711
+ if (appId === null) return;
4712
+ printContextHeader(ctx, { json: args.json });
4713
+ const { session, workspaceId } = ctx;
4714
+ try {
4715
+ const match = pickCertById(await fetchCerts(workspaceId, appId, session.cookies), certId);
4716
+ if (match === null) {
4717
+ if (args.json) emitJson({
4718
+ ok: false,
4719
+ reason: "not-found",
4720
+ workspaceId,
4721
+ appId,
4722
+ certId,
4723
+ message: `cert ${certId} not found on app ${appId}`
4724
+ });
4725
+ else process.stderr.write(`app certs show: cert ${certId} not found on app ${appId} (ws ${workspaceId})\n`);
4726
+ return exitAfterFlush(ExitCode.Usage);
4727
+ }
4728
+ const expiry = augmentCertExpiry(match);
4333
4729
  if (args.json) {
4334
4730
  emitJson({
4335
4731
  ok: true,
4336
4732
  workspaceId,
4337
4733
  appId,
4338
- certs
4734
+ cert: match,
4735
+ ...expiry
4339
4736
  });
4340
4737
  return exitAfterFlush(ExitCode.Ok);
4341
4738
  }
4342
- if (certs.length === 0) {
4343
- process.stdout.write(`App ${appId} (ws ${workspaceId}): no mTLS certs\n`);
4344
- return exitAfterFlush(ExitCode.Ok);
4345
- }
4346
- process.stdout.write(`App ${appId} (ws ${workspaceId}): ${certs.length} cert(s)\n`);
4347
- for (const c of certs) {
4348
- const id = typeof c.id === "string" || typeof c.id === "number" ? c.id : typeof c.certId === "string" || typeof c.certId === "number" ? c.certId : "-";
4349
- const cn = typeof c.commonName === "string" ? c.commonName : "-";
4350
- const createdAt = typeof c.createdAt === "string" ? c.createdAt : "";
4351
- const expiresAt = typeof c.expiresAt === "string" ? c.expiresAt : typeof c.validUntil === "string" ? c.validUntil : "";
4352
- process.stdout.write(`${id}\t${cn}\t${createdAt}\t${expiresAt}\n`);
4739
+ const id = typeof match.id === "string" || typeof match.id === "number" ? match.id : typeof match.certId === "string" || typeof match.certId === "number" ? match.certId : "-";
4740
+ const name = typeof match.name === "string" ? match.name : "-";
4741
+ const cn = typeof match.commonName === "string" ? match.commonName : "";
4742
+ const createdAt = typeof match.createdAt === "string" ? match.createdAt : "";
4743
+ const expiresAtIso = expiry.expiresAtMs !== void 0 ? new Date(expiry.expiresAtMs).toISOString() : "";
4744
+ const status = typeof match.status === "string" ? match.status : "";
4745
+ const lines = [`App ${appId} (ws ${workspaceId}): cert ${id}`, ` name: ${name}`];
4746
+ if (cn) lines.push(` commonName: ${cn}`);
4747
+ if (createdAt) lines.push(` createdAt: ${createdAt}`);
4748
+ if (expiresAtIso) lines.push(` expiresAt: ${expiresAtIso}`);
4749
+ if (expiry.daysUntilExpiry !== void 0) {
4750
+ const d = expiry.daysUntilExpiry;
4751
+ const suffix = d > 0 ? ` (D-${d})` : d === 0 ? " (expires today)" : ` (expired ${-d} day(s) ago)`;
4752
+ lines.push(` daysUntilExpiry: ${d}${suffix}`);
4353
4753
  }
4754
+ if (status) lines.push(` status: ${status}`);
4755
+ process.stdout.write(`${lines.join("\n")}\n`);
4354
4756
  return exitAfterFlush(ExitCode.Ok);
4355
4757
  } catch (err) {
4356
4758
  return emitFailureFromError(args.json, err);
@@ -4370,6 +4772,7 @@ const certsCommand = defineCommand({
4370
4772
  },
4371
4773
  subCommands: {
4372
4774
  ls: certsLsCommand,
4775
+ show: certsShowCommand,
4373
4776
  issue: defineCommand({
4374
4777
  meta: {
4375
4778
  name: "issue",
@@ -5453,7 +5856,7 @@ async function runCommand(command, opts) {
5453
5856
  function isCommandNotFound(err) {
5454
5857
  return err.code === "ENOENT";
5455
5858
  }
5456
- function stripTrailingNewline(s) {
5859
+ function stripTrailingNewline$1(s) {
5457
5860
  return s.replace(/\r?\n$/, "");
5458
5861
  }
5459
5862
  function redactStderr(stderr) {
@@ -5487,7 +5890,7 @@ const LINUX_BACKEND = {
5487
5890
  throw new CredentialBackendCommandError("secret-tool lookup", result.exitCode, redactStderr(result.stderr));
5488
5891
  }
5489
5892
  if (result.stdout.length === 0) return null;
5490
- const password = stripTrailingNewline(result.stdout);
5893
+ const password = stripTrailingNewline$1(result.stdout);
5491
5894
  return password.length > 0 ? password : null;
5492
5895
  },
5493
5896
  async set(account, password) {
@@ -5551,7 +5954,7 @@ const MACOS_BACKEND = {
5551
5954
  }
5552
5955
  if (result.exitCode === 44) return null;
5553
5956
  if (result.exitCode !== 0) return null;
5554
- const password = stripTrailingNewline(result.stdout);
5957
+ const password = stripTrailingNewline$1(result.stdout);
5555
5958
  return password.length > 0 ? password : null;
5556
5959
  },
5557
5960
  async set(account, password) {
@@ -6086,13 +6489,17 @@ const authImportCommand = defineCommand({
6086
6489
  });
6087
6490
  //#endregion
6088
6491
  //#region src/commands/auth.ts
6492
+ function emitDeprecation(replacement) {
6493
+ process.stderr.write(`warning: this command is deprecated and will be removed in 1.0; ${replacement}\n`);
6494
+ }
6089
6495
  async function runAuthSet(args, deps = {}) {
6496
+ emitDeprecation("use `aitcc login` (interactive prompt offers a save option).");
6090
6497
  const env = deps.env ?? process.env;
6091
6498
  let email = args.email?.trim();
6092
- let password$2 = args.password;
6093
- const argvPasswordUsed = password$2 !== void 0;
6499
+ let password$1 = args.password;
6500
+ const argvPasswordUsed = password$1 !== void 0;
6094
6501
  if (!email && env.AITCC_EMAIL) email = env.AITCC_EMAIL;
6095
- if (password$2 === void 0 && env.AITCC_PASSWORD) password$2 = env.AITCC_PASSWORD;
6502
+ if (password$1 === void 0 && env.AITCC_PASSWORD) password$1 = env.AITCC_PASSWORD;
6096
6503
  if (argvPasswordUsed) process.stderr.write("Warning: --password on argv is visible in `ps`/Task Manager. Prefer the AITCC_PASSWORD environment variable for scripted use.\n");
6097
6504
  const interactive = process.stdout.isTTY && process.stdin.isTTY && !args.json;
6098
6505
  if (!email) {
@@ -6106,7 +6513,7 @@ async function runAuthSet(args, deps = {}) {
6106
6513
  validate: (raw) => raw.trim().length > 0 ? true : "email is required"
6107
6514
  })).trim();
6108
6515
  } catch (err) {
6109
- if (isPromptCancelled(err)) {
6516
+ if (isPromptCancelled$1(err)) {
6110
6517
  process.stderr.write("Aborted.\n");
6111
6518
  return exitAfterFlush(ExitCode.Usage);
6112
6519
  }
@@ -6122,26 +6529,26 @@ async function runAuthSet(args, deps = {}) {
6122
6529
  else process.stderr.write(`Invalid email: ${email}\n`);
6123
6530
  return exitAfterFlush(ExitCode.Usage);
6124
6531
  }
6125
- if (password$2 === void 0) {
6532
+ if (password$1 === void 0) {
6126
6533
  if (!interactive) {
6127
6534
  emitInteractiveRequired(args.json, "password");
6128
6535
  return exitAfterFlush(ExitCode.Usage);
6129
6536
  }
6130
6537
  try {
6131
- password$2 = await password({
6538
+ password$1 = await password({
6132
6539
  message: "Password:",
6133
6540
  mask: true,
6134
6541
  validate: (raw) => raw.length > 0 ? true : "password is required"
6135
6542
  });
6136
6543
  } catch (err) {
6137
- if (isPromptCancelled(err)) {
6544
+ if (isPromptCancelled$1(err)) {
6138
6545
  process.stderr.write("Aborted.\n");
6139
6546
  return exitAfterFlush(ExitCode.Usage);
6140
6547
  }
6141
6548
  throw err;
6142
6549
  }
6143
6550
  }
6144
- if (password$2.length === 0) {
6551
+ if (password$1.length === 0) {
6145
6552
  if (args.json) emitJson({
6146
6553
  ok: false,
6147
6554
  reason: "invalid-password",
@@ -6152,7 +6559,7 @@ async function runAuthSet(args, deps = {}) {
6152
6559
  }
6153
6560
  let result;
6154
6561
  try {
6155
- result = await saveCredentials(email, password$2, deps.backend ? { override: deps.backend } : {});
6562
+ result = await saveCredentials(email, password$1, deps.backend ? { override: deps.backend } : {});
6156
6563
  } catch (err) {
6157
6564
  const message = err.message;
6158
6565
  if (args.json) emitJson({
@@ -6173,6 +6580,7 @@ async function runAuthSet(args, deps = {}) {
6173
6580
  return exitAfterFlush(ExitCode.Ok);
6174
6581
  }
6175
6582
  async function runAuthClear(args, deps = {}) {
6583
+ emitDeprecation("use `aitcc logout --purge` to remove session and saved credentials together.");
6176
6584
  const interactive = process.stdout.isTTY && process.stdin.isTTY && !args.json;
6177
6585
  const active = await getActiveCredentialEmail(deps.env ? { env: deps.env } : {}).catch(() => null);
6178
6586
  if (!args.yes) {
@@ -6193,7 +6601,7 @@ async function runAuthClear(args, deps = {}) {
6193
6601
  default: false
6194
6602
  });
6195
6603
  } catch (err) {
6196
- if (isPromptCancelled(err)) {
6604
+ if (isPromptCancelled$1(err)) {
6197
6605
  process.stderr.write("Aborted.\n");
6198
6606
  return exitAfterFlush(ExitCode.Usage);
6199
6607
  }
@@ -6231,6 +6639,7 @@ async function runAuthClear(args, deps = {}) {
6231
6639
  return exitAfterFlush(ExitCode.Ok);
6232
6640
  }
6233
6641
  async function runAuthStatus(args, deps = {}) {
6642
+ emitDeprecation("use `aitcc whoami` (now reports credential source).");
6234
6643
  const active = await getActiveCredentialEmail(deps.env ? { env: deps.env } : {}).catch(() => null);
6235
6644
  const session = await readSession();
6236
6645
  if (args.json) {
@@ -6269,7 +6678,7 @@ function emitInteractiveRequired(json, missing) {
6269
6678
  });
6270
6679
  else process.stderr.write(`Cannot prompt for ${missing} in non-interactive mode. Use --${missing} or set AITCC_${missing.toUpperCase()}.\n`);
6271
6680
  }
6272
- function isPromptCancelled(err) {
6681
+ function isPromptCancelled$1(err) {
6273
6682
  return err instanceof Error && err.name === "ExitPromptError";
6274
6683
  }
6275
6684
  const authCommand = defineCommand({
@@ -6281,7 +6690,7 @@ const authCommand = defineCommand({
6281
6690
  set: defineCommand({
6282
6691
  meta: {
6283
6692
  name: "set",
6284
- description: "Save email + password to the OS keychain for future headless logins."
6693
+ description: "[deprecated] Use `aitcc login` instead the prompt now offers a save option."
6285
6694
  },
6286
6695
  args: {
6287
6696
  json: {
@@ -6309,7 +6718,7 @@ const authCommand = defineCommand({
6309
6718
  clear: defineCommand({
6310
6719
  meta: {
6311
6720
  name: "clear",
6312
- description: "Delete the saved credentials and the auth-state pointer."
6721
+ description: "[deprecated] Use `aitcc logout --purge` instead."
6313
6722
  },
6314
6723
  args: {
6315
6724
  json: {
@@ -6334,7 +6743,7 @@ const authCommand = defineCommand({
6334
6743
  status: defineCommand({
6335
6744
  meta: {
6336
6745
  name: "status",
6337
- description: "Report whether credentials and a session are configured."
6746
+ description: "[deprecated] Use `aitcc whoami` (now reports credential source)."
6338
6747
  },
6339
6748
  args: { json: {
6340
6749
  type: "boolean",
@@ -7494,10 +7903,38 @@ function chooseLoginMode(input) {
7494
7903
  if (input.interactiveFlag) return "interactive";
7495
7904
  return input.hasCredentials ? "headless" : "interactive";
7496
7905
  }
7906
+ const defaultPromptDeps = {
7907
+ email: (defaultValue) => input({
7908
+ message: "Email:",
7909
+ ...defaultValue !== void 0 ? { default: defaultValue } : {},
7910
+ validate: (raw) => {
7911
+ const trimmed = raw.trim();
7912
+ if (trimmed.length === 0) return "email is required";
7913
+ if (!trimmed.includes("@")) return "must contain \"@\"";
7914
+ return true;
7915
+ }
7916
+ }).then((s) => s.trim()),
7917
+ password: () => password({
7918
+ message: "Password:",
7919
+ mask: true,
7920
+ validate: (raw) => raw.length > 0 ? true : "password is required"
7921
+ }),
7922
+ saveTarget: () => select({
7923
+ message: "Where would you like to save the credentials?",
7924
+ default: "keychain",
7925
+ choices: [{
7926
+ name: "OS keychain (recommended) — next login runs headlessly",
7927
+ value: "keychain"
7928
+ }, {
7929
+ name: "Do not save — one-shot. (Tip: AITCC_EMAIL/AITCC_PASSWORD env for CI.)",
7930
+ value: "none"
7931
+ }]
7932
+ })
7933
+ };
7497
7934
  const loginCommand = defineCommand({
7498
7935
  meta: {
7499
7936
  name: "login",
7500
- description: "Open a browser to sign in, then capture the console session cookies."
7937
+ description: "Sign in to the Apps in Toss console and capture the session cookies."
7501
7938
  },
7502
7939
  args: {
7503
7940
  json: {
@@ -7515,9 +7952,26 @@ const loginCommand = defineCommand({
7515
7952
  description: "Force the visible-browser flow even if credentials are configured.",
7516
7953
  default: false
7517
7954
  },
7955
+ email: {
7956
+ type: "string",
7957
+ description: "Email (skip prompt; required for non-interactive use)."
7958
+ },
7959
+ password: {
7960
+ type: "string",
7961
+ description: "Password (skip prompt; visible in `ps`/Task Manager — prefer --password-stdin or AITCC_PASSWORD env)."
7962
+ },
7963
+ "password-stdin": {
7964
+ type: "boolean",
7965
+ description: "Read the password from stdin (recommended for non-interactive use).",
7966
+ default: false
7967
+ },
7968
+ save: {
7969
+ type: "string",
7970
+ description: "Where to persist credentials when --email/--password* are passed: \"keychain\" or \"none\" (default)."
7971
+ },
7518
7972
  "skip-onboarding": {
7519
7973
  type: "boolean",
7520
- description: "Skip the post-login prompt to save credentials to the OS keychain.",
7974
+ description: "Deprecated no-op; kept so existing scripts do not break.",
7521
7975
  default: false
7522
7976
  }
7523
7977
  },
@@ -7526,7 +7980,10 @@ const loginCommand = defineCommand({
7526
7980
  json: args.json,
7527
7981
  timeout: args.timeout,
7528
7982
  interactive: args.interactive,
7529
- skipOnboarding: args["skip-onboarding"]
7983
+ email: typeof args.email === "string" ? args.email : void 0,
7984
+ password: typeof args.password === "string" ? args.password : void 0,
7985
+ passwordStdin: args["password-stdin"],
7986
+ save: typeof args.save === "string" ? args.save : void 0
7530
7987
  }, {
7531
7988
  getCredentials: loadCredentials,
7532
7989
  saveCredentials
@@ -7570,17 +8027,41 @@ async function runLoginCommand(args, deps) {
7570
8027
  }
7571
8028
  process.stderr.write(`Using custom authorize URL from AITCC_OAUTH_URL: ${authorizeUrl}\n`);
7572
8029
  }
7573
- let credentials = null;
7574
- if (!args.interactive) {
7575
- const getCredentials = deps.getCredentials;
7576
- if (getCredentials) credentials = await getCredentials().catch((err) => {
7577
- process.stderr.write(`Credential lookup failed (${err.message}); using interactive login.\n`);
7578
- return null;
7579
- });
8030
+ const resolved = await resolveCredentialsForLogin(args, deps);
8031
+ if (resolved.kind === "error") {
8032
+ emitError({
8033
+ reason: resolved.reason,
8034
+ message: resolved.message
8035
+ }, resolved.message);
8036
+ return exitAfterFlush(resolved.exitCode);
8037
+ }
8038
+ let saved = "skipped";
8039
+ if (resolved.saveTarget === "keychain" && resolved.credentials !== null) {
8040
+ const save = deps.saveCredentials;
8041
+ if (!save) {
8042
+ emitError({
8043
+ reason: "save-unavailable",
8044
+ message: "no save backend configured"
8045
+ }, "Cannot save credentials: no backend configured.");
8046
+ return exitAfterFlush(ExitCode.Generic);
8047
+ }
8048
+ try {
8049
+ const result = await save(resolved.credentials.email, resolved.credentials.password);
8050
+ saved = result.status;
8051
+ if (!args.json) if (result.status === "unchanged") process.stderr.write("Credentials already saved (no change).\n");
8052
+ else process.stderr.write(`Credentials saved to OS keychain (${resolved.credentials.email}).\n`);
8053
+ } catch (err) {
8054
+ const message = err.message;
8055
+ emitError({
8056
+ reason: "keychain-save-failed",
8057
+ message
8058
+ }, `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.`);
8059
+ return exitAfterFlush(ExitCode.Usage);
8060
+ }
7580
8061
  }
7581
8062
  const initialMode = chooseLoginMode({
7582
8063
  interactiveFlag: args.interactive,
7583
- hasCredentials: credentials !== null
8064
+ hasCredentials: resolved.credentials !== null
7584
8065
  });
7585
8066
  const endpointTimeoutMs = Math.min(6e4, Math.max(3e4, Math.floor(timeoutMs / 2)));
7586
8067
  const firstAttemptStart = Date.now();
@@ -7590,9 +8071,9 @@ async function runLoginCommand(args, deps) {
7590
8071
  endpointTimeoutMs,
7591
8072
  authorizeUrl,
7592
8073
  mode: initialMode,
7593
- credentials,
7594
- emitError,
7595
- deps
8074
+ credentials: resolved.credentials,
8075
+ saved,
8076
+ emitError
7596
8077
  });
7597
8078
  if (result.status === "fallback-to-interactive") {
7598
8079
  process.stderr.write(`${result.message}\n`);
@@ -7603,8 +8084,8 @@ async function runLoginCommand(args, deps) {
7603
8084
  authorizeUrl,
7604
8085
  mode: "interactive",
7605
8086
  credentials: null,
7606
- emitError,
7607
- deps
8087
+ saved,
8088
+ emitError
7608
8089
  });
7609
8090
  if (second.status === "exit") return exitAfterFlush(second.code);
7610
8091
  second.status;
@@ -7612,8 +8093,167 @@ async function runLoginCommand(args, deps) {
7612
8093
  }
7613
8094
  return exitAfterFlush(result.code);
7614
8095
  }
8096
+ /**
8097
+ * Resolve credentials and the requested save target for `aitcc login`.
8098
+ * Pure-ish: only side-effect is reading stdin via `deps.readStdin` (when
8099
+ * `--password-stdin` is set) and prompting via `deps.prompts` (when TTY).
8100
+ */
8101
+ async function resolveCredentialsForLogin(args, deps, opts = {}) {
8102
+ const env = opts.env ?? process.env;
8103
+ const stdoutIsTTY = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
8104
+ const stdinIsTTY = opts.stdinIsTTY ?? Boolean(process.stdin.isTTY);
8105
+ const interactiveTty = stdoutIsTTY && stdinIsTTY && !args.json;
8106
+ if (args.password !== void 0 && args.passwordStdin) return {
8107
+ kind: "error",
8108
+ reason: "conflicting-password-source",
8109
+ message: "--password and --password-stdin cannot be used together.",
8110
+ exitCode: ExitCode.Usage
8111
+ };
8112
+ if (args.interactive && (args.email !== void 0 || args.password !== void 0 || args.passwordStdin || args.save !== void 0)) return {
8113
+ kind: "error",
8114
+ reason: "conflicting-interactive-flags",
8115
+ message: "--interactive cannot be combined with --email/--password/--password-stdin/--save. Drop --interactive to use credentials, or drop the credential flags to type in the browser.",
8116
+ exitCode: ExitCode.Usage
8117
+ };
8118
+ let saveTarget;
8119
+ if (args.save !== void 0) {
8120
+ if (args.save !== "keychain" && args.save !== "none") return {
8121
+ kind: "error",
8122
+ reason: "invalid-save",
8123
+ message: `--save must be "keychain" or "none" (got "${args.save}").`,
8124
+ exitCode: ExitCode.Usage
8125
+ };
8126
+ saveTarget = args.save;
8127
+ }
8128
+ if (args.email !== void 0 || args.password !== void 0 || args.passwordStdin) {
8129
+ if (args.email === void 0 || args.email.trim().length === 0) return {
8130
+ kind: "error",
8131
+ reason: "missing-email",
8132
+ message: "--email is required when --password / --password-stdin is passed.",
8133
+ exitCode: ExitCode.Usage
8134
+ };
8135
+ if (!args.email.includes("@")) return {
8136
+ kind: "error",
8137
+ reason: "invalid-email",
8138
+ message: `Invalid email: ${args.email}`,
8139
+ exitCode: ExitCode.Usage
8140
+ };
8141
+ let password;
8142
+ if (args.passwordStdin) {
8143
+ password = stripTrailingNewline(await (deps.readStdin ?? readStdinAll)());
8144
+ if (password.length === 0) return {
8145
+ kind: "error",
8146
+ reason: "invalid-password",
8147
+ message: "--password-stdin received an empty password on stdin.",
8148
+ exitCode: ExitCode.Usage
8149
+ };
8150
+ } else if (args.password !== void 0) {
8151
+ process.stderr.write("Warning: --password on argv is visible in `ps`/Task Manager. Prefer --password-stdin or the AITCC_PASSWORD environment variable.\n");
8152
+ password = args.password;
8153
+ if (password.length === 0) return {
8154
+ kind: "error",
8155
+ reason: "invalid-password",
8156
+ message: "--password value is empty.",
8157
+ exitCode: ExitCode.Usage
8158
+ };
8159
+ } else return {
8160
+ kind: "error",
8161
+ reason: "missing-password",
8162
+ message: "--email passed without a password. Add --password-stdin (recommended) or --password.",
8163
+ exitCode: ExitCode.Usage
8164
+ };
8165
+ return {
8166
+ kind: "ok",
8167
+ credentials: {
8168
+ source: "argv",
8169
+ email: args.email.trim(),
8170
+ password
8171
+ },
8172
+ saveTarget: saveTarget ?? "none"
8173
+ };
8174
+ }
8175
+ if (args.interactive) return {
8176
+ kind: "ok",
8177
+ credentials: null,
8178
+ saveTarget: saveTarget ?? "none"
8179
+ };
8180
+ if (env.AITCC_EMAIL && env.AITCC_PASSWORD) return {
8181
+ kind: "ok",
8182
+ credentials: {
8183
+ source: "env",
8184
+ email: env.AITCC_EMAIL,
8185
+ password: env.AITCC_PASSWORD
8186
+ },
8187
+ saveTarget: saveTarget ?? "none"
8188
+ };
8189
+ const getCredentials = deps.getCredentials;
8190
+ if (getCredentials) {
8191
+ const fromStore = await getCredentials().catch((err) => {
8192
+ process.stderr.write(`Credential lookup failed (${err.message}); ignoring.\n`);
8193
+ return null;
8194
+ });
8195
+ if (fromStore) {
8196
+ if (!args.json) process.stderr.write(`Using credentials from OS keychain for ${fromStore.email}. Pass --interactive to type a different account.
8197
+ `);
8198
+ return {
8199
+ kind: "ok",
8200
+ credentials: {
8201
+ source: fromStore.kind,
8202
+ email: fromStore.email,
8203
+ password: fromStore.password
8204
+ },
8205
+ saveTarget: saveTarget ?? "none"
8206
+ };
8207
+ }
8208
+ }
8209
+ if (interactiveTty) {
8210
+ const prompts = deps.prompts ?? defaultPromptDeps;
8211
+ let email;
8212
+ let password;
8213
+ try {
8214
+ email = await prompts.email();
8215
+ password = await prompts.password();
8216
+ } catch (err) {
8217
+ if (isPromptCancelled(err)) return {
8218
+ kind: "error",
8219
+ reason: "aborted",
8220
+ message: "Aborted.",
8221
+ exitCode: ExitCode.Usage
8222
+ };
8223
+ throw err;
8224
+ }
8225
+ let promptedSave;
8226
+ if (saveTarget !== void 0) promptedSave = saveTarget;
8227
+ else try {
8228
+ promptedSave = await prompts.saveTarget();
8229
+ } catch (err) {
8230
+ if (isPromptCancelled(err)) return {
8231
+ kind: "error",
8232
+ reason: "aborted",
8233
+ message: "Aborted.",
8234
+ exitCode: ExitCode.Usage
8235
+ };
8236
+ throw err;
8237
+ }
8238
+ return {
8239
+ kind: "ok",
8240
+ credentials: {
8241
+ source: "prompt",
8242
+ email,
8243
+ password
8244
+ },
8245
+ saveTarget: promptedSave
8246
+ };
8247
+ }
8248
+ return {
8249
+ kind: "error",
8250
+ reason: "interactive-required",
8251
+ message: "No credentials configured and stdin is not a TTY. Pass --email + --password-stdin (or set AITCC_EMAIL + AITCC_PASSWORD).",
8252
+ exitCode: ExitCode.Usage
8253
+ };
8254
+ }
7615
8255
  async function attemptLogin(opts) {
7616
- const { args, timeoutMs, endpointTimeoutMs, authorizeUrl, mode, credentials, emitError, deps } = opts;
8256
+ const { args, timeoutMs, endpointTimeoutMs, authorizeUrl, mode, credentials, saved, emitError } = opts;
7617
8257
  const launched = await launchChrome({
7618
8258
  initialUrl: authorizeUrl,
7619
8259
  endpointTimeoutMs,
@@ -7652,8 +8292,8 @@ async function attemptLogin(opts) {
7652
8292
  }
7653
8293
  if (mode === "interactive") process.stderr.write("Opened a browser window — complete the sign-in there. The CLI will capture the session automatically.\n");
7654
8294
  else {
7655
- const source = credentials?.kind === "env" ? "env" : "keychain";
7656
- process.stderr.write(`Signing in headlessly with credentials from ${source}…\n`);
8295
+ const sourceLabel = credentials?.source ?? "configured store";
8296
+ process.stderr.write(`Signing in headlessly with credentials from ${sourceLabel}…\n`);
7657
8297
  }
7658
8298
  let client = null;
7659
8299
  const disposeAll = async () => {
@@ -7820,57 +8460,29 @@ async function attemptLogin(opts) {
7820
8460
  capturedAt: session.capturedAt,
7821
8461
  cookieCount: cookies.length,
7822
8462
  mode,
8463
+ credentialSource: credentials?.source ?? "browser",
8464
+ saved,
7823
8465
  stepUp
7824
8466
  })}\n`);
7825
8467
  else process.stdout.write(`Logged in as ${user.name} <${user.email}>\n`);
7826
8468
  await disposeAll();
7827
- if (mode === "interactive" && credentials === null && !args.json && !args.skipOnboarding && process.stdout.isTTY && process.stdin.isTTY && deps.saveCredentials) await runOnboardingPrompt(user.email, deps.saveCredentials);
7828
8469
  return {
7829
8470
  status: "exit",
7830
8471
  code: ExitCode.Ok
7831
8472
  };
7832
8473
  }
7833
- /**
7834
- * Post-login prompt that offers to persist the user's email + password
7835
- * to the OS keychain so subsequent `aitcc login` runs can take the
7836
- * headless form-fill path. Failures are non-fatal: we already wrote a
7837
- * valid session, so a keychain hiccup just means the next login will
7838
- * fall back to interactive — exactly the same UX as before.
7839
- */
7840
- async function runOnboardingPrompt(email, save) {
7841
- process.stdout.write("\n");
7842
- process.stdout.write("Would you like to save your password to the OS keychain so the next `aitcc login` runs headlessly?\n");
7843
- let agreed;
7844
- try {
7845
- agreed = await confirm({
7846
- message: "Save credentials?",
7847
- default: true
7848
- });
7849
- } catch (err) {
7850
- if (err instanceof Error && err.name === "ExitPromptError") return;
7851
- process.stderr.write(`Onboarding prompt failed: ${err.message}\n`);
7852
- return;
7853
- }
7854
- if (!agreed) return;
7855
- let password$1;
7856
- try {
7857
- password$1 = await password({
7858
- message: "Password:",
7859
- mask: true,
7860
- validate: (raw) => raw.length > 0 ? true : "password is required"
7861
- });
7862
- } catch (err) {
7863
- if (err instanceof Error && err.name === "ExitPromptError") return;
7864
- process.stderr.write(`Could not read password: ${err.message}\n`);
7865
- return;
7866
- }
7867
- try {
7868
- await save(email, password$1);
7869
- process.stdout.write("Saved. Next `aitcc login` will run headlessly.\n");
7870
- } catch (err) {
7871
- process.stderr.write(`Could not save credentials: ${err.message}. You can retry later with \`aitcc auth set\`.
7872
- `);
7873
- }
8474
+ async function readStdinAll() {
8475
+ if (process.stdin.isTTY) throw new Error("--password-stdin requires stdin to be a pipe, not a TTY.");
8476
+ process.stdin.setEncoding("utf8");
8477
+ let buf = "";
8478
+ for await (const chunk of process.stdin) buf += chunk;
8479
+ return buf;
8480
+ }
8481
+ function stripTrailingNewline(s) {
8482
+ return s.replace(/\r?\n$/, "");
8483
+ }
8484
+ function isPromptCancelled(err) {
8485
+ return err instanceof Error && err.name === "ExitPromptError";
7874
8486
  }
7875
8487
  async function waitForLanding(client, sessionId, timeoutMs) {
7876
8488
  return await new Promise((resolve) => {
@@ -7934,18 +8546,25 @@ async function resolveUserWithRetry(cookies, opts = {}) {
7934
8546
  const logoutCommand = defineCommand({
7935
8547
  meta: {
7936
8548
  name: "logout",
7937
- description: "Delete the local session file."
8549
+ description: "Delete the local session file (and optionally the saved credentials)."
8550
+ },
8551
+ args: {
8552
+ json: {
8553
+ type: "boolean",
8554
+ description: "Emit machine-readable JSON to stdout.",
8555
+ default: false
8556
+ },
8557
+ purge: {
8558
+ type: "boolean",
8559
+ description: "Also delete saved keychain credentials and the auth-state pointer.",
8560
+ default: false
8561
+ }
7938
8562
  },
7939
- args: { json: {
7940
- type: "boolean",
7941
- description: "Emit machine-readable JSON to stdout.",
7942
- default: false
7943
- } },
7944
8563
  async run({ args }) {
7945
8564
  const path = sessionPathForDiagnostics();
7946
- let existed;
8565
+ let sessionRemoved;
7947
8566
  try {
7948
- existed = (await clearSession()).existed;
8567
+ sessionRemoved = (await clearSession()).existed;
7949
8568
  } catch (err) {
7950
8569
  const message = err.message;
7951
8570
  if (args.json) process.stdout.write(`${JSON.stringify({
@@ -7957,13 +8576,29 @@ const logoutCommand = defineCommand({
7957
8576
  process.stderr.write(`Failed to remove session file at ${path}: ${message}\n`);
7958
8577
  return exitAfterFlush(ExitCode.Generic);
7959
8578
  }
7960
- if (args.json) process.stdout.write(`${JSON.stringify({
7961
- ok: true,
7962
- status: existed ? "logged-out" : "no-session",
7963
- path
7964
- })}\n`);
7965
- else if (existed) process.stdout.write(`Logged out. Session removed from ${path}\n`);
7966
- else process.stdout.write(`No active session at ${path}.\n`);
8579
+ let credentialsPurged = false;
8580
+ let purgeError = null;
8581
+ if (args.purge) try {
8582
+ credentialsPurged = (await deleteCredentials()).existed;
8583
+ } catch (err) {
8584
+ purgeError = err.message;
8585
+ }
8586
+ if (args.json) {
8587
+ const payload = {
8588
+ ok: true,
8589
+ sessionRemoved,
8590
+ credentialsPurged,
8591
+ path
8592
+ };
8593
+ if (purgeError !== null) payload.purgeError = purgeError;
8594
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
8595
+ } else {
8596
+ if (sessionRemoved) process.stdout.write(`Logged out. Session removed from ${path}\n`);
8597
+ else process.stdout.write(`No active session at ${path}.\n`);
8598
+ if (args.purge) if (purgeError !== null) process.stderr.write(`Could not delete saved credentials: ${purgeError}\n`);
8599
+ else if (credentialsPurged) process.stdout.write("Saved credentials deleted from the OS keychain.\n");
8600
+ else process.stdout.write("No saved credentials to delete.\n");
8601
+ }
7967
8602
  return exitAfterFlush(ExitCode.Ok);
7968
8603
  }
7969
8604
  });
@@ -8498,7 +9133,7 @@ function resolveVersion() {
8498
9133
  if (typeof injected === "string" && injected.length > 0) return injected;
8499
9134
  } catch {}
8500
9135
  try {
8501
- return "0.1.23";
9136
+ return "0.1.25";
8502
9137
  } catch {}
8503
9138
  return "0.0.0-dev";
8504
9139
  }
@@ -8870,6 +9505,22 @@ function maybeEmitNotice(entry, env) {
8870
9505
  }
8871
9506
  //#endregion
8872
9507
  //#region src/commands/whoami.ts
9508
+ async function describeCredentialSource() {
9509
+ const active = await getActiveCredentialEmail().catch(() => null);
9510
+ if (!active) return {
9511
+ source: "none",
9512
+ email: null
9513
+ };
9514
+ return {
9515
+ source: active.kind,
9516
+ email: active.email
9517
+ };
9518
+ }
9519
+ function formatCredentials(cred) {
9520
+ if (cred.source === "none") return "none (run `aitcc login` to save)";
9521
+ if (cred.source === "env") return `env (AITCC_EMAIL${cred.email ? ` = ${cred.email}` : ""})`;
9522
+ return `keychain${cred.email ? ` (${cred.email})` : ""}`;
9523
+ }
8873
9524
  async function runBackgroundUpdateCheck(json) {
8874
9525
  if (json) return;
8875
9526
  const timeoutMs = 500;
@@ -8897,14 +9548,18 @@ const whoamiCommand = defineCommand({
8897
9548
  },
8898
9549
  async run({ args }) {
8899
9550
  const session = await readSession();
9551
+ const cred = await describeCredentialSource();
8900
9552
  if (!session) {
8901
9553
  if (args.json) process.stdout.write(`${JSON.stringify({
8902
9554
  ok: true,
8903
- authenticated: false
9555
+ authenticated: false,
9556
+ credentialSource: cred.source,
9557
+ ...cred.email ? { credentialEmail: cred.email } : {}
8904
9558
  })}\n`);
8905
9559
  else {
8906
9560
  process.stderr.write("Not logged in. Run `aitcc login` to start a session.\n");
8907
9561
  process.stderr.write(`Session file checked: ${sessionPathForDiagnostics()}\n`);
9562
+ process.stderr.write(`Credentials: ${formatCredentials(cred)}\n`);
8908
9563
  }
8909
9564
  return exitAfterFlush(ExitCode.NotAuthenticated);
8910
9565
  }
@@ -8915,12 +9570,15 @@ const whoamiCommand = defineCommand({
8915
9570
  authenticated: true,
8916
9571
  source: "cache",
8917
9572
  user: session.user,
8918
- capturedAt: session.capturedAt
9573
+ capturedAt: session.capturedAt,
9574
+ credentialSource: cred.source,
9575
+ ...cred.email ? { credentialEmail: cred.email } : {}
8919
9576
  })}\n`);
8920
9577
  return exitAfterFlush(ExitCode.Ok);
8921
9578
  }
8922
9579
  const label = session.user.displayName ? `${session.user.displayName} <${session.user.email}>` : session.user.email;
8923
9580
  process.stdout.write(`Logged in as ${label} (cached)\n`);
9581
+ process.stdout.write(`Credentials: ${formatCredentials(cred)}\n`);
8924
9582
  process.stdout.write(`Session captured: ${session.capturedAt}\n`);
8925
9583
  await runBackgroundUpdateCheck(args.json);
8926
9584
  return exitAfterFlush(ExitCode.Ok);
@@ -8944,11 +9602,14 @@ const whoamiCommand = defineCommand({
8944
9602
  workspaceName: w.workspaceName,
8945
9603
  role: w.role
8946
9604
  })),
8947
- capturedAt: session.capturedAt
9605
+ capturedAt: session.capturedAt,
9606
+ credentialSource: cred.source,
9607
+ ...cred.email ? { credentialEmail: cred.email } : {}
8948
9608
  })}\n`);
8949
9609
  return exitAfterFlush(ExitCode.Ok);
8950
9610
  }
8951
9611
  process.stdout.write(`Logged in as ${info.name} <${info.email}> (${info.role})\n`);
9612
+ process.stdout.write(`Credentials: ${formatCredentials(cred)}\n`);
8952
9613
  if (info.workspaces.length > 0) {
8953
9614
  process.stdout.write("Workspaces:\n");
8954
9615
  for (const w of info.workspaces) process.stdout.write(` - ${w.workspaceName} (id ${w.workspaceId}, ${w.role})\n`);
@@ -8961,9 +9622,14 @@ const whoamiCommand = defineCommand({
8961
9622
  ok: true,
8962
9623
  authenticated: false,
8963
9624
  reason: "session-expired",
8964
- errorCode: err.errorCode
9625
+ errorCode: err.errorCode,
9626
+ credentialSource: cred.source,
9627
+ ...cred.email ? { credentialEmail: cred.email } : {}
8965
9628
  })}\n`);
8966
- else process.stderr.write("Session is no longer valid. Run `aitcc login` again.\n");
9629
+ else {
9630
+ process.stderr.write("Session is no longer valid. Run `aitcc login` again.\n");
9631
+ process.stderr.write(`Credentials: ${formatCredentials(cred)}\n`);
9632
+ }
8967
9633
  return exitAfterFlush(ExitCode.NotAuthenticated);
8968
9634
  }
8969
9635
  if (err instanceof NetworkError) {
@@ -8986,120 +9652,6 @@ const whoamiCommand = defineCommand({
8986
9652
  }
8987
9653
  });
8988
9654
  //#endregion
8989
- //#region src/api/workspaces.ts
8990
- const WORKSPACES_BASE = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
8991
- async function fetchWorkspaceDetail(workspaceId, cookies, opts = {}) {
8992
- const raw = await requestConsoleApi({
8993
- url: `${WORKSPACES_BASE}/workspaces/${workspaceId}`,
8994
- cookies,
8995
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
8996
- });
8997
- const id = raw.id;
8998
- const name = raw.name;
8999
- if (typeof id !== "number" || !Number.isInteger(id) || id <= 0 || typeof name !== "string") throw new Error(`Unexpected workspace detail shape for id=${workspaceId}`);
9000
- const { id: _id, name: _name, ...extra } = raw;
9001
- return {
9002
- workspaceId: id,
9003
- workspaceName: name,
9004
- extra
9005
- };
9006
- }
9007
- async function fetchWorkspacePartner(workspaceId, cookies, opts = {}) {
9008
- const raw = await requestConsoleApi({
9009
- url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/partner`,
9010
- cookies,
9011
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
9012
- });
9013
- const registered = raw.registered;
9014
- if (typeof registered !== "boolean") throw new Error(`Unexpected workspace partner shape for id=${workspaceId}`);
9015
- return {
9016
- registered,
9017
- approvalType: typeof raw.approvalType === "string" ? raw.approvalType : null,
9018
- rejectMessage: typeof raw.rejectMessage === "string" ? raw.rejectMessage : null,
9019
- partner: raw.partner && typeof raw.partner === "object" ? raw.partner : null
9020
- };
9021
- }
9022
- const WORKSPACE_TERM_TYPES = [
9023
- "TOSS_LOGIN",
9024
- "BIZ_WORKSPACE",
9025
- "TOSS_PROMOTION_MONEY",
9026
- "IAA",
9027
- "IAP"
9028
- ];
9029
- const DEFAULT_SEGMENT_CATEGORY = "생성된 세그먼트";
9030
- async function fetchWorkspaceSegments(params, cookies, opts = {}) {
9031
- const page = params.page ?? 0;
9032
- const qs = new URLSearchParams();
9033
- qs.set("category", params.category ?? DEFAULT_SEGMENT_CATEGORY);
9034
- qs.set("search", params.search ?? "");
9035
- qs.set("page", String(page));
9036
- const raw = await requestConsoleApi({
9037
- url: `${WORKSPACES_BASE}/workspaces/${params.workspaceId}/segments/list?${qs.toString()}`,
9038
- cookies,
9039
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
9040
- });
9041
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`Unexpected segments shape for workspace=${params.workspaceId}`);
9042
- const data = raw;
9043
- return {
9044
- contents: (Array.isArray(data.contents) ? data.contents : []).map((c) => c && typeof c === "object" ? c : {}),
9045
- totalPage: typeof data.totalPage === "number" ? data.totalPage : 0,
9046
- currentPage: typeof data.currentPage === "number" ? data.currentPage : page
9047
- };
9048
- }
9049
- async function fetchWorkspaceTerms(workspaceId, type, cookies, opts = {}) {
9050
- const raw = await requestConsoleApi({
9051
- url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/console-workspace-terms/${type}/skip-permission`,
9052
- cookies,
9053
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
9054
- });
9055
- if (!Array.isArray(raw)) throw new Error(`Unexpected workspace terms shape for type=${type}`);
9056
- return raw.map((entry, i) => {
9057
- if (!entry || typeof entry !== "object") throw new Error(`Unexpected workspace terms entry at index ${i} for type=${type}`);
9058
- const e = entry;
9059
- return {
9060
- required: Boolean(e.required),
9061
- termsId: typeof e.termsId === "number" ? e.termsId : 0,
9062
- revisionId: typeof e.revisionId === "number" ? e.revisionId : 0,
9063
- title: typeof e.title === "string" ? e.title : "",
9064
- contentsUrl: typeof e.contentsUrl === "string" ? e.contentsUrl : "",
9065
- actionType: typeof e.actionType === "string" ? e.actionType : "",
9066
- isAgreed: Boolean(e.isAgreed),
9067
- isOneTimeConsent: Boolean(e.isOneTimeConsent)
9068
- };
9069
- });
9070
- }
9071
- /**
9072
- * Persist agreement for one-or-more workspace terms. The endpoint takes a
9073
- * single `agreedList` regardless of which bucket the terms came from — the
9074
- * type tag is implicit in the (termsId, revisionId) pairs.
9075
- *
9076
- * Captured behaviour (2026-05-08, ws=36577):
9077
- * - `POST /workspaces/<wid>/console-workspace-terms` with body
9078
- * `{"agreedList":[{"termsId": <int>, "revisionId": <int>}, ...]}`
9079
- * - Response on success: `{"resultType":"SUCCESS","success":{}}` — no
9080
- * useful payload. We resolve `void`.
9081
- * - Re-submitting an already-agreed term returns `errorCode: 500`
9082
- * (Internal Server Error). The server is NOT idempotent, so callers
9083
- * must filter to `isAgreed === false` before invoking.
9084
- * - Empty `agreedList` returns SUCCESS (no-op), but we throw client-side
9085
- * before sending — round-tripping a no-op request is wasted.
9086
- *
9087
- * Failure surfaces through `TossApiError` like every other write helper.
9088
- */
9089
- async function agreeWorkspaceTerms(workspaceId, terms, cookies, opts = {}) {
9090
- if (terms.length === 0) throw new Error("agreeWorkspaceTerms requires at least one term");
9091
- await requestConsoleApi({
9092
- method: "POST",
9093
- url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/console-workspace-terms`,
9094
- cookies,
9095
- body: { agreedList: terms.map(({ termsId, revisionId }) => ({
9096
- termsId,
9097
- revisionId
9098
- })) },
9099
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
9100
- });
9101
- }
9102
- //#endregion
9103
9655
  //#region src/commands/workspace.ts
9104
9656
  function formatScalar(v) {
9105
9657
  if (v === null) return "null";