@ait-co/console-cli 0.1.4 → 0.1.5

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
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from "citty";
3
- import { spawn } from "node:child_process";
4
- import { constants } from "node:fs";
5
3
  import { chmod, mkdir, mkdtemp, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
6
- import { homedir, tmpdir } from "node:os";
7
4
  import { basename, dirname, join, win32 } from "node:path";
5
+ import { homedir, tmpdir } from "node:os";
6
+ import { spawn } from "node:child_process";
7
+ import { constants } from "node:fs";
8
8
  //#region src/api/http.ts
9
9
  var TossApiError = class extends Error {
10
10
  constructor(status, errorCode, reason, errorType) {
@@ -143,6 +143,483 @@ async function requestConsoleApi(options) {
143
143
  throw new TossApiError(res.status, parsed.error.errorCode, parsed.error.reason, parsed.error.errorType);
144
144
  }
145
145
  //#endregion
146
+ //#region src/api/mini-apps.ts
147
+ const BASE$2 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
148
+ async function fetchMiniApps(workspaceId, cookies, opts = {}) {
149
+ const raw = await requestConsoleApi({
150
+ url: `${BASE$2}/workspaces/${workspaceId}/mini-app`,
151
+ cookies,
152
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
153
+ });
154
+ if (!Array.isArray(raw)) throw new Error(`Unexpected mini-app list shape for workspace=${workspaceId}`);
155
+ return raw.map((item, index) => normalizeMiniApp(item, workspaceId, index));
156
+ }
157
+ function normalizeMiniApp(item, workspaceId, index) {
158
+ if (item === null || typeof item !== "object") throw new Error(`Unexpected mini-app entry at index ${index} for workspace=${workspaceId}: not an object`);
159
+ const rec = item;
160
+ const rawId = rec.id ?? rec.miniAppId ?? rec.appId;
161
+ if (typeof rawId !== "string" && typeof rawId !== "number") throw new Error(`Unexpected mini-app entry at index ${index} for workspace=${workspaceId}: missing id`);
162
+ const rawName = rec.name ?? rec.miniAppName ?? rec.appName;
163
+ const name = typeof rawName === "string" ? rawName : void 0;
164
+ const { id: _id, miniAppId: _mid, appId: _aid, name: _n, miniAppName: _mn, appName: _an, ...extra } = rec;
165
+ return {
166
+ id: rawId,
167
+ name,
168
+ extra
169
+ };
170
+ }
171
+ async function fetchReviewStatus(workspaceId, cookies, opts = {}) {
172
+ const raw = await requestConsoleApi({
173
+ url: `${BASE$2}/workspaces/${workspaceId}/mini-apps/review-status`,
174
+ cookies,
175
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
176
+ });
177
+ if (raw === null || typeof raw !== "object") throw new Error(`Unexpected review-status shape for workspace=${workspaceId}`);
178
+ const rec = raw;
179
+ const hasPolicyViolation = Boolean(rec.hasPolicyViolation);
180
+ const miniAppsRaw = rec.miniApps;
181
+ if (!Array.isArray(miniAppsRaw)) throw new Error(`Unexpected review-status shape for workspace=${workspaceId}: miniApps is not an array`);
182
+ return {
183
+ hasPolicyViolation,
184
+ miniApps: miniAppsRaw.map((m) => {
185
+ if (m === null || typeof m !== "object") return {};
186
+ return m;
187
+ })
188
+ };
189
+ }
190
+ //#endregion
191
+ //#region src/exit.ts
192
+ const ExitCode = {
193
+ Ok: 0,
194
+ Generic: 1,
195
+ Usage: 2,
196
+ NotAuthenticated: 10,
197
+ NetworkError: 11,
198
+ LoginTimeout: 12,
199
+ LoginStateMismatch: 13,
200
+ LoginBrowserNotFound: 14,
201
+ LoginBrowserFailed: 15,
202
+ LoginCookieCaptureFailed: 16,
203
+ ApiError: 17,
204
+ UpgradeUnavailable: 20,
205
+ UpgradeAlreadyLatest: 21
206
+ };
207
+ //#endregion
208
+ //#region src/flush.ts
209
+ async function exitAfterFlush(code) {
210
+ await new Promise((resolve) => process.stdout.write("", () => resolve()));
211
+ process.exit(code);
212
+ }
213
+ //#endregion
214
+ //#region src/paths.ts
215
+ const APP_NAME = "aitcc";
216
+ function configDir() {
217
+ if (process.platform === "win32") {
218
+ const appData = process.env.APPDATA;
219
+ if (appData && appData.length > 0) return join(appData, APP_NAME);
220
+ return join(homedir() || ".", "AppData", "Roaming", APP_NAME);
221
+ }
222
+ const xdg = process.env.XDG_CONFIG_HOME;
223
+ if (xdg && xdg.length > 0) return join(xdg, APP_NAME);
224
+ return join(homedir() || ".", ".config", APP_NAME);
225
+ }
226
+ function sessionFilePath() {
227
+ return join(configDir(), "session.json");
228
+ }
229
+ function cacheDir() {
230
+ if (process.platform === "win32") {
231
+ const localAppData = process.env.LOCALAPPDATA;
232
+ if (localAppData && localAppData.length > 0) return join(localAppData, APP_NAME, "Cache");
233
+ return join(homedir() || ".", "AppData", "Local", APP_NAME, "Cache");
234
+ }
235
+ const xdg = process.env.XDG_CACHE_HOME;
236
+ if (xdg && xdg.length > 0) return join(xdg, APP_NAME);
237
+ return join(homedir() || ".", ".cache", APP_NAME);
238
+ }
239
+ function upgradeCheckPath() {
240
+ return join(cacheDir(), "upgrade-check.json");
241
+ }
242
+ //#endregion
243
+ //#region src/session.ts
244
+ /**
245
+ * Read the persisted session. Returns `null` when no session exists, when
246
+ * the file is corrupt, or when the shape fails validation — each of those
247
+ * emits a one-line warning on stderr for diagnostics.
248
+ *
249
+ * **Side effect**: a v1 session file is transparently rewritten to v2 on
250
+ * the first successful read of this process. This keeps read-only callers
251
+ * (`whoami`, `workspace ls`) from stranding users on an old schema. If the
252
+ * rewrite fails, we warn once per process and continue with the in-memory
253
+ * v2 value so the calling command still succeeds.
254
+ */
255
+ async function readSession() {
256
+ const path = sessionFilePath();
257
+ let raw;
258
+ try {
259
+ raw = await readFile(path, "utf8");
260
+ } catch (err) {
261
+ const code = err.code;
262
+ if (code === "ENOENT") return null;
263
+ process.stderr.write(`warning: could not read session file at ${path}: ${code ?? "unknown"}\n`);
264
+ return null;
265
+ }
266
+ let rawParsed;
267
+ try {
268
+ rawParsed = JSON.parse(raw);
269
+ } catch {
270
+ process.stderr.write(`warning: session file at ${path} is corrupt and will be ignored\n`);
271
+ return null;
272
+ }
273
+ const schemaReason = validateSessionShape(rawParsed);
274
+ if (schemaReason) {
275
+ process.stderr.write(`warning: session file at ${path} ignored (${schemaReason}); re-run \`aitcc login\`\n`);
276
+ return null;
277
+ }
278
+ const validated = rawParsed;
279
+ if (validated.schemaVersion === 1) {
280
+ const upgraded = {
281
+ ...validated,
282
+ schemaVersion: 2
283
+ };
284
+ try {
285
+ await writeSession(upgraded);
286
+ } catch (err) {
287
+ warnMigrationOnce(path, err.code);
288
+ }
289
+ return upgraded;
290
+ }
291
+ return validated;
292
+ }
293
+ let migrationWarned = false;
294
+ function warnMigrationOnce(path, code) {
295
+ if (migrationWarned) return;
296
+ migrationWarned = true;
297
+ process.stderr.write(`warning: could not migrate session file at ${path} to schemaVersion 2: ${code ?? "unknown"}\n`);
298
+ }
299
+ function validateSessionShape(input) {
300
+ if (input === null || typeof input !== "object") return "root is not an object";
301
+ const parsed = input;
302
+ if (parsed.schemaVersion !== 1 && parsed.schemaVersion !== 2) return `unknown schemaVersion ${String(parsed.schemaVersion)}`;
303
+ if (!parsed.user || typeof parsed.user.id !== "string") return "missing user.id";
304
+ if (typeof parsed.user.email !== "string") return "missing user.email";
305
+ if (parsed.user.displayName !== void 0 && typeof parsed.user.displayName !== "string") return "user.displayName has wrong type";
306
+ if (!Array.isArray(parsed.cookies)) return "cookies is not an array";
307
+ if (parsed.origins !== void 0 && !Array.isArray(parsed.origins)) return "origins is not an array";
308
+ if (parsed.capturedAt !== void 0 && typeof parsed.capturedAt !== "string") return "capturedAt has wrong type";
309
+ if (parsed.currentWorkspaceId !== void 0) {
310
+ const wid = parsed.currentWorkspaceId;
311
+ if (typeof wid !== "number" || !Number.isInteger(wid) || wid <= 0) return "currentWorkspaceId has wrong type";
312
+ }
313
+ return null;
314
+ }
315
+ async function writeSession(session) {
316
+ await mkdir(dirname(sessionFilePath()), {
317
+ recursive: true,
318
+ mode: 448
319
+ });
320
+ await writeFile(sessionFilePath(), JSON.stringify(session, null, 2), { mode: 384 });
321
+ try {
322
+ await chmod(sessionFilePath(), 384);
323
+ } catch {}
324
+ }
325
+ /**
326
+ * Persist a new `currentWorkspaceId` on an existing session. Returns the
327
+ * updated session, or `null` if there is no session to update (callers
328
+ * should surface "not logged in" in that case).
329
+ */
330
+ async function setCurrentWorkspaceId(workspaceId) {
331
+ const session = await readSession();
332
+ if (!session) return null;
333
+ const updated = {
334
+ ...session,
335
+ currentWorkspaceId: workspaceId
336
+ };
337
+ await writeSession(updated);
338
+ return updated;
339
+ }
340
+ async function clearSession() {
341
+ try {
342
+ await unlink(sessionFilePath());
343
+ return { existed: true };
344
+ } catch (err) {
345
+ if (err.code === "ENOENT") return { existed: false };
346
+ throw err;
347
+ }
348
+ }
349
+ function sessionPathForDiagnostics() {
350
+ return sessionFilePath();
351
+ }
352
+ //#endregion
353
+ //#region src/commands/_shared.ts
354
+ function emitJson(payload) {
355
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
356
+ }
357
+ function emitNotAuthenticated(json, reason) {
358
+ if (json) emitJson(reason ? {
359
+ ok: true,
360
+ authenticated: false,
361
+ reason
362
+ } : {
363
+ ok: true,
364
+ authenticated: false
365
+ });
366
+ else {
367
+ process.stderr.write(reason === "session-expired" ? "Session is no longer valid. Run `aitcc login` again.\n" : "Not logged in. Run `aitcc login` to start a session.\n");
368
+ process.stderr.write(`Session file checked: ${sessionPathForDiagnostics()}\n`);
369
+ }
370
+ }
371
+ function emitNetworkError(json, message) {
372
+ if (json) emitJson({
373
+ ok: false,
374
+ reason: "network-error",
375
+ message
376
+ });
377
+ else process.stderr.write(`Network error reaching the console API: ${message}.\n`);
378
+ }
379
+ function emitApiError(json, message) {
380
+ if (json) emitJson({
381
+ ok: false,
382
+ reason: "api-error",
383
+ message
384
+ });
385
+ else process.stderr.write(`Unexpected error: ${message}\n`);
386
+ }
387
+ function parsePositiveInt(raw) {
388
+ if (!/^[1-9]\d*$/.test(raw)) return null;
389
+ const n = Number.parseInt(raw, 10);
390
+ return Number.isSafeInteger(n) ? n : null;
391
+ }
392
+ /**
393
+ * Boilerplate wrapper for any workspace-scoped command (`app ls`,
394
+ * `members ls`, `keys ls`, ...). Loads the session, resolves the workspace
395
+ * id from `--workspace <id>` or the persisted selection, and handles the
396
+ * three common failure branches (`no session`, `invalid id`, `no workspace
397
+ * selected`). On success, the caller gets the session + resolved id back.
398
+ *
399
+ * The return type is `Promise<... | null>` but the `null` branch is never
400
+ * observed at runtime: every failure path `await`s `exitAfterFlush` which
401
+ * calls `process.exit(...)` and doesn't return. The `| null` is a type-
402
+ * level handshake that forces callers to add `if (!ctx) return;`, keeping
403
+ * the bail-out readable.
404
+ */
405
+ async function resolveWorkspaceContext(args) {
406
+ const session = await readSession();
407
+ if (!session) {
408
+ emitNotAuthenticated(args.json);
409
+ await exitAfterFlush(ExitCode.NotAuthenticated);
410
+ return null;
411
+ }
412
+ let workspaceId;
413
+ if (args.workspace) {
414
+ const raw = String(args.workspace);
415
+ const parsed = parsePositiveInt(raw);
416
+ if (parsed === null) {
417
+ const message = `--workspace must be a positive integer (got ${raw})`;
418
+ if (args.json) emitJson({
419
+ ok: false,
420
+ reason: "invalid-id",
421
+ message
422
+ });
423
+ else process.stderr.write(`${message}\n`);
424
+ await exitAfterFlush(ExitCode.Usage);
425
+ return null;
426
+ }
427
+ workspaceId = parsed;
428
+ } else workspaceId = session.currentWorkspaceId;
429
+ if (workspaceId === void 0) {
430
+ if (args.json) emitJson({
431
+ ok: false,
432
+ reason: "no-workspace-selected"
433
+ });
434
+ else process.stderr.write("No workspace selected. Pass `--workspace <id>` or run `aitcc workspace use <id>`.\n");
435
+ await exitAfterFlush(ExitCode.Usage);
436
+ return null;
437
+ }
438
+ return {
439
+ session,
440
+ workspaceId
441
+ };
442
+ }
443
+ //#endregion
444
+ //#region src/commands/app.ts
445
+ function findReviewEntry(reviewEntries, appId) {
446
+ const target = String(appId);
447
+ for (const entry of reviewEntries) {
448
+ const candidate = entry.id ?? entry.miniAppId ?? entry.appId;
449
+ if (candidate !== void 0 && String(candidate) === target) return entry;
450
+ }
451
+ return null;
452
+ }
453
+ function reviewStateFor(entry) {
454
+ if (!entry) return void 0;
455
+ const raw = entry.reviewState ?? entry.status;
456
+ return typeof raw === "string" ? raw : void 0;
457
+ }
458
+ const appCommand = defineCommand({
459
+ meta: {
460
+ name: "app",
461
+ description: "Inspect mini-apps in a workspace."
462
+ },
463
+ subCommands: { ls: defineCommand({
464
+ meta: {
465
+ name: "ls",
466
+ description: "List mini-apps in the selected workspace."
467
+ },
468
+ args: {
469
+ workspace: {
470
+ type: "string",
471
+ description: "Workspace ID. Defaults to the selected workspace (`aitcc workspace use`)."
472
+ },
473
+ json: {
474
+ type: "boolean",
475
+ description: "Emit machine-readable JSON to stdout.",
476
+ default: false
477
+ }
478
+ },
479
+ async run({ args }) {
480
+ const ctx = await resolveWorkspaceContext(args);
481
+ if (!ctx) return;
482
+ const { session, workspaceId } = ctx;
483
+ try {
484
+ const [apps, review] = await Promise.all([fetchMiniApps(workspaceId, session.cookies), fetchReviewStatus(workspaceId, session.cookies)]);
485
+ if (args.json) {
486
+ const joined = apps.map((app) => {
487
+ const reviewState = reviewStateFor(findReviewEntry(review.miniApps, app.id));
488
+ return {
489
+ id: app.id,
490
+ name: app.name ?? null,
491
+ ...reviewState !== void 0 ? { reviewState } : {},
492
+ extra: app.extra
493
+ };
494
+ });
495
+ emitJson({
496
+ ok: true,
497
+ workspaceId,
498
+ hasPolicyViolation: review.hasPolicyViolation,
499
+ apps: joined
500
+ });
501
+ return exitAfterFlush(ExitCode.Ok);
502
+ }
503
+ if (apps.length === 0) {
504
+ process.stdout.write(`No apps in workspace ${workspaceId}.\n`);
505
+ if (review.hasPolicyViolation) process.stderr.write("Note: workspace-wide policy violation flag is set.\n");
506
+ return exitAfterFlush(ExitCode.Ok);
507
+ }
508
+ for (const app of apps) {
509
+ const reviewState = reviewStateFor(findReviewEntry(review.miniApps, app.id)) ?? "-";
510
+ const name = app.name ?? "(unnamed)";
511
+ process.stdout.write(`${app.id}\t${name}\t${reviewState}\n`);
512
+ }
513
+ if (review.hasPolicyViolation) process.stderr.write("Note: workspace-wide policy violation flag is set.\n");
514
+ return exitAfterFlush(ExitCode.Ok);
515
+ } catch (err) {
516
+ if (err instanceof TossApiError && err.isAuthError) {
517
+ emitNotAuthenticated(args.json, "session-expired");
518
+ return exitAfterFlush(ExitCode.NotAuthenticated);
519
+ }
520
+ if (err instanceof NetworkError) {
521
+ emitNetworkError(args.json, err.message);
522
+ return exitAfterFlush(ExitCode.NetworkError);
523
+ }
524
+ emitApiError(args.json, err.message);
525
+ return exitAfterFlush(ExitCode.ApiError);
526
+ }
527
+ }
528
+ }) }
529
+ });
530
+ //#endregion
531
+ //#region src/api/api-keys.ts
532
+ const BASE$1 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
533
+ async function fetchApiKeys(workspaceId, cookies, opts = {}) {
534
+ const raw = await requestConsoleApi({
535
+ url: `${BASE$1}/workspaces/${workspaceId}/api-keys`,
536
+ cookies,
537
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
538
+ });
539
+ if (!Array.isArray(raw)) throw new Error(`Unexpected api-keys shape for workspace=${workspaceId}: not an array`);
540
+ return raw.map((entry, index) => normalizeKey(entry, workspaceId, index));
541
+ }
542
+ function normalizeKey(raw, workspaceId, index) {
543
+ if (raw === null || typeof raw !== "object") throw new Error(`Unexpected api-key entry at index ${index} for workspace=${workspaceId}: not an object`);
544
+ const rec = raw;
545
+ const rawId = rec.id ?? rec.apiKeyId ?? rec.keyId;
546
+ if (typeof rawId !== "string" && typeof rawId !== "number") throw new Error(`Unexpected api-key entry at index ${index} for workspace=${workspaceId}: missing id`);
547
+ const rawName = rec.name ?? rec.apiKeyName ?? rec.keyName ?? rec.description;
548
+ const name = typeof rawName === "string" ? rawName : void 0;
549
+ const { id: _id, apiKeyId: _aid, keyId: _kid, name: _n, apiKeyName: _an, keyName: _kn, description: _d, ...extra } = rec;
550
+ return {
551
+ id: rawId,
552
+ name,
553
+ extra
554
+ };
555
+ }
556
+ const keysCommand = defineCommand({
557
+ meta: {
558
+ name: "keys",
559
+ description: "Inspect console API keys used for deploy automation."
560
+ },
561
+ subCommands: { ls: defineCommand({
562
+ meta: {
563
+ name: "ls",
564
+ description: "List console API keys in the selected workspace."
565
+ },
566
+ args: {
567
+ workspace: {
568
+ type: "string",
569
+ description: "Workspace ID. Defaults to the selected workspace (`aitcc workspace use`)."
570
+ },
571
+ json: {
572
+ type: "boolean",
573
+ description: "Emit machine-readable JSON to stdout.",
574
+ default: false
575
+ }
576
+ },
577
+ async run({ args }) {
578
+ const ctx = await resolveWorkspaceContext(args);
579
+ if (!ctx) return;
580
+ const { session, workspaceId } = ctx;
581
+ try {
582
+ const keys = await fetchApiKeys(workspaceId, session.cookies);
583
+ if (args.json) {
584
+ emitJson({
585
+ ok: true,
586
+ workspaceId,
587
+ keys: keys.map((k) => ({
588
+ id: k.id,
589
+ name: k.name ?? null,
590
+ extra: k.extra
591
+ })),
592
+ ...keys.length === 0 ? { needsKey: true } : {}
593
+ });
594
+ return exitAfterFlush(ExitCode.Ok);
595
+ }
596
+ if (keys.length === 0) {
597
+ process.stdout.write(`No API keys in workspace ${workspaceId}.\n`);
598
+ process.stderr.write("Hint: issue a key from the console UI (API 키 → 발급받기) to enable deploy automation.\n");
599
+ return exitAfterFlush(ExitCode.Ok);
600
+ }
601
+ process.stdout.write(`${keys.length} API key(s) in workspace ${workspaceId}:\n`);
602
+ for (const k of keys) {
603
+ const name = k.name ?? "(unnamed)";
604
+ process.stdout.write(`${k.id}\t${name}\n`);
605
+ }
606
+ return exitAfterFlush(ExitCode.Ok);
607
+ } catch (err) {
608
+ if (err instanceof TossApiError && err.isAuthError) {
609
+ emitNotAuthenticated(args.json, "session-expired");
610
+ return exitAfterFlush(ExitCode.NotAuthenticated);
611
+ }
612
+ if (err instanceof NetworkError) {
613
+ emitNetworkError(args.json, err.message);
614
+ return exitAfterFlush(ExitCode.NetworkError);
615
+ }
616
+ emitApiError(args.json, err.message);
617
+ return exitAfterFlush(ExitCode.ApiError);
618
+ }
619
+ }
620
+ }) }
621
+ });
622
+ //#endregion
146
623
  //#region src/api/me.ts
147
624
  const MEMBER_USER_INFO_URL = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole/members/me/user-info";
148
625
  async function fetchConsoleMemberUserInfo(cookies, opts = {}) {
@@ -519,102 +996,6 @@ async function launchChrome(options) {
519
996
  };
520
997
  }
521
998
  //#endregion
522
- //#region src/exit.ts
523
- const ExitCode = {
524
- Ok: 0,
525
- Generic: 1,
526
- Usage: 2,
527
- NotAuthenticated: 10,
528
- NetworkError: 11,
529
- LoginTimeout: 12,
530
- LoginStateMismatch: 13,
531
- LoginBrowserNotFound: 14,
532
- LoginBrowserFailed: 15,
533
- LoginCookieCaptureFailed: 16,
534
- ApiError: 17,
535
- UpgradeUnavailable: 20,
536
- UpgradeAlreadyLatest: 21
537
- };
538
- //#endregion
539
- //#region src/flush.ts
540
- async function exitAfterFlush(code) {
541
- await new Promise((resolve) => process.stdout.write("", () => resolve()));
542
- process.exit(code);
543
- }
544
- //#endregion
545
- //#region src/paths.ts
546
- const APP_NAME = "aitcc";
547
- function configDir() {
548
- if (process.platform === "win32") {
549
- const appData = process.env.APPDATA;
550
- if (appData && appData.length > 0) return join(appData, APP_NAME);
551
- return join(homedir() || ".", "AppData", "Roaming", APP_NAME);
552
- }
553
- const xdg = process.env.XDG_CONFIG_HOME;
554
- if (xdg && xdg.length > 0) return join(xdg, APP_NAME);
555
- return join(homedir() || ".", ".config", APP_NAME);
556
- }
557
- function sessionFilePath() {
558
- return join(configDir(), "session.json");
559
- }
560
- //#endregion
561
- //#region src/session.ts
562
- async function readSession() {
563
- const path = sessionFilePath();
564
- let raw;
565
- try {
566
- raw = await readFile(path, "utf8");
567
- } catch (err) {
568
- const code = err.code;
569
- if (code === "ENOENT") return null;
570
- process.stderr.write(`warning: could not read session file at ${path}: ${code ?? "unknown"}\n`);
571
- return null;
572
- }
573
- let parsed;
574
- try {
575
- parsed = JSON.parse(raw);
576
- } catch {
577
- process.stderr.write(`warning: session file at ${path} is corrupt and will be ignored\n`);
578
- return null;
579
- }
580
- const schemaReason = validateSessionShape(parsed);
581
- if (schemaReason) {
582
- process.stderr.write(`warning: session file at ${path} ignored (${schemaReason}); re-run \`aitcc login\`\n`);
583
- return null;
584
- }
585
- return parsed;
586
- }
587
- function validateSessionShape(parsed) {
588
- if (parsed.schemaVersion !== 1) return `unknown schemaVersion ${String(parsed.schemaVersion)}`;
589
- if (!parsed.user || typeof parsed.user.id !== "string") return "missing user.id";
590
- if (typeof parsed.user.email !== "string") return "missing user.email";
591
- if (parsed.user.displayName !== void 0 && typeof parsed.user.displayName !== "string") return "user.displayName has wrong type";
592
- if (!Array.isArray(parsed.cookies)) return "cookies is not an array";
593
- return null;
594
- }
595
- async function writeSession(session) {
596
- await mkdir(dirname(sessionFilePath()), {
597
- recursive: true,
598
- mode: 448
599
- });
600
- await writeFile(sessionFilePath(), JSON.stringify(session, null, 2), { mode: 384 });
601
- try {
602
- await chmod(sessionFilePath(), 384);
603
- } catch {}
604
- }
605
- async function clearSession() {
606
- try {
607
- await unlink(sessionFilePath());
608
- return { existed: true };
609
- } catch (err) {
610
- if (err.code === "ENOENT") return { existed: false };
611
- throw err;
612
- }
613
- }
614
- function sessionPathForDiagnostics() {
615
- return sessionFilePath();
616
- }
617
- //#endregion
618
999
  //#region src/commands/login.ts
619
1000
  const DEFAULT_AUTHORIZE_URL = "https://business.toss.im/account/sign-in?client_id=4uktpjgqd0cp9txybqzuxc2y6w0cuupb&redirect_uri=https%3A%2F%2Fapps-in-toss.toss.im%2Fsign-up&state=%2Fworkspace";
620
1001
  const LOGIN_LANDING_HOST = "apps-in-toss.toss.im";
@@ -776,7 +1157,7 @@ const loginCommand = defineCommand({
776
1157
  return exitWith(authFailed ? ExitCode.LoginCookieCaptureFailed : ExitCode.ApiError);
777
1158
  }
778
1159
  const session = {
779
- schemaVersion: 1,
1160
+ schemaVersion: 2,
780
1161
  user: {
781
1162
  id: String(user.id),
782
1163
  email: user.email,
@@ -902,6 +1283,105 @@ const logoutCommand = defineCommand({
902
1283
  }
903
1284
  });
904
1285
  //#endregion
1286
+ //#region src/api/members.ts
1287
+ const BASE = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
1288
+ async function fetchWorkspaceMembers(workspaceId, cookies, opts = {}) {
1289
+ const raw = await requestConsoleApi({
1290
+ url: `${BASE}/workspaces/${workspaceId}/members`,
1291
+ cookies,
1292
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1293
+ });
1294
+ if (!Array.isArray(raw)) throw new Error(`Unexpected members shape for workspace=${workspaceId}: not an array`);
1295
+ return raw.map((entry, index) => normalizeMember(entry, workspaceId, index));
1296
+ }
1297
+ function normalizeMember(raw, workspaceId, index) {
1298
+ if (raw === null || typeof raw !== "object") throw new Error(`Unexpected member entry at index ${index} for workspace=${workspaceId}: not an object`);
1299
+ const rec = raw;
1300
+ const stringField = (k) => {
1301
+ const v = rec[k];
1302
+ if (typeof v !== "string") throw new Error(`Unexpected member entry at index ${index} for workspace=${workspaceId}: missing ${k}`);
1303
+ return v;
1304
+ };
1305
+ const numField = (k) => {
1306
+ const v = rec[k];
1307
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new Error(`Unexpected member entry at index ${index} for workspace=${workspaceId}: missing ${k}`);
1308
+ return v;
1309
+ };
1310
+ return {
1311
+ workspaceId: numField("workspaceId"),
1312
+ bizUserNo: numField("bizUserNo"),
1313
+ name: stringField("name"),
1314
+ email: stringField("email"),
1315
+ status: stringField("status"),
1316
+ role: stringField("role"),
1317
+ isOwnerDelegationRequested: Boolean(rec.isOwnerDelegationRequested),
1318
+ isAdult: Boolean(rec.isAdult)
1319
+ };
1320
+ }
1321
+ const membersCommand = defineCommand({
1322
+ meta: {
1323
+ name: "members",
1324
+ description: "Inspect workspace members."
1325
+ },
1326
+ subCommands: { ls: defineCommand({
1327
+ meta: {
1328
+ name: "ls",
1329
+ description: "List members of the selected workspace."
1330
+ },
1331
+ args: {
1332
+ workspace: {
1333
+ type: "string",
1334
+ description: "Workspace ID. Defaults to the selected workspace (`aitcc workspace use`)."
1335
+ },
1336
+ json: {
1337
+ type: "boolean",
1338
+ description: "Emit machine-readable JSON to stdout.",
1339
+ default: false
1340
+ }
1341
+ },
1342
+ async run({ args }) {
1343
+ const ctx = await resolveWorkspaceContext(args);
1344
+ if (!ctx) return;
1345
+ const { session, workspaceId } = ctx;
1346
+ try {
1347
+ const members = await fetchWorkspaceMembers(workspaceId, session.cookies);
1348
+ if (args.json) {
1349
+ emitJson({
1350
+ ok: true,
1351
+ workspaceId,
1352
+ members: members.map((m) => ({
1353
+ bizUserNo: m.bizUserNo,
1354
+ name: m.name,
1355
+ email: m.email,
1356
+ status: m.status,
1357
+ role: m.role,
1358
+ isOwnerDelegationRequested: m.isOwnerDelegationRequested
1359
+ }))
1360
+ });
1361
+ return exitAfterFlush(ExitCode.Ok);
1362
+ }
1363
+ if (members.length === 0) {
1364
+ process.stdout.write(`No members in workspace ${workspaceId}.\n`);
1365
+ return exitAfterFlush(ExitCode.Ok);
1366
+ }
1367
+ for (const m of members) process.stdout.write(`${m.bizUserNo}\t${m.name}\t${m.email}\t${m.role}\t${m.status}\n`);
1368
+ return exitAfterFlush(ExitCode.Ok);
1369
+ } catch (err) {
1370
+ if (err instanceof TossApiError && err.isAuthError) {
1371
+ emitNotAuthenticated(args.json, "session-expired");
1372
+ return exitAfterFlush(ExitCode.NotAuthenticated);
1373
+ }
1374
+ if (err instanceof NetworkError) {
1375
+ emitNetworkError(args.json, err.message);
1376
+ return exitAfterFlush(ExitCode.NetworkError);
1377
+ }
1378
+ emitApiError(args.json, err.message);
1379
+ return exitAfterFlush(ExitCode.ApiError);
1380
+ }
1381
+ }
1382
+ }) }
1383
+ });
1384
+ //#endregion
905
1385
  //#region src/github.ts
906
1386
  const REPO_OWNER = "apps-in-toss-community";
907
1387
  const REPO_NAME = "console-cli";
@@ -921,6 +1401,29 @@ async function fetchLatestRelease() {
921
1401
  if (!res.ok) throw new Error(`GitHub releases/latest returned ${res.status} ${res.statusText}`);
922
1402
  return await res.json();
923
1403
  }
1404
+ /**
1405
+ * Conditional GET against `releases/latest`. If the server returns 304 we
1406
+ * learn "no change" without consuming a core rate-limit slot. Intended for
1407
+ * the background update check, which re-runs often; `fetchLatestRelease()`
1408
+ * remains the right call when the upgrade command actually needs the body.
1409
+ */
1410
+ async function fetchLatestReleaseConditional(previousEtag) {
1411
+ const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`;
1412
+ const headers = defaultHeaders();
1413
+ if (previousEtag && previousEtag.length > 0) headers["If-None-Match"] = previousEtag;
1414
+ const res = await fetch(url, { headers });
1415
+ const etag = res.headers.get("etag") ?? void 0;
1416
+ if (res.status === 304) return {
1417
+ status: "not-modified",
1418
+ etag
1419
+ };
1420
+ if (!res.ok) throw new Error(`GitHub releases/latest returned ${res.status} ${res.statusText}`);
1421
+ return {
1422
+ status: "updated",
1423
+ release: await res.json(),
1424
+ etag
1425
+ };
1426
+ }
924
1427
  function versionFromTag(tag) {
925
1428
  const at = tag.lastIndexOf("@");
926
1429
  const candidate = at >= 0 ? tag.slice(at + 1) : tag;
@@ -991,7 +1494,7 @@ function resolveVersion() {
991
1494
  if (typeof injected === "string" && injected.length > 0) return injected;
992
1495
  } catch {}
993
1496
  try {
994
- return "0.1.4";
1497
+ return "0.1.5";
995
1498
  } catch {}
996
1499
  return "0.0.0-dev";
997
1500
  }
@@ -1137,7 +1640,113 @@ const upgradeCommand = defineCommand({
1137
1640
  }
1138
1641
  });
1139
1642
  //#endregion
1643
+ //#region src/update-check.ts
1644
+ const UPDATE_CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
1645
+ async function readCache() {
1646
+ let raw;
1647
+ try {
1648
+ raw = await readFile(upgradeCheckPath(), "utf8");
1649
+ } catch {
1650
+ return null;
1651
+ }
1652
+ let parsed;
1653
+ try {
1654
+ parsed = JSON.parse(raw);
1655
+ } catch {
1656
+ return null;
1657
+ }
1658
+ if (!parsed || typeof parsed !== "object") return null;
1659
+ const obj = parsed;
1660
+ if (typeof obj.lastCheckedAt !== "string") return null;
1661
+ if (obj.latestTag !== void 0 && typeof obj.latestTag !== "string") return null;
1662
+ if (obj.etag !== void 0 && typeof obj.etag !== "string") return null;
1663
+ return {
1664
+ lastCheckedAt: obj.lastCheckedAt,
1665
+ ...obj.latestTag !== void 0 ? { latestTag: obj.latestTag } : {},
1666
+ ...obj.etag !== void 0 ? { etag: obj.etag } : {}
1667
+ };
1668
+ }
1669
+ async function writeCache(entry) {
1670
+ const path = upgradeCheckPath();
1671
+ await mkdir(dirname(path), { recursive: true });
1672
+ const tmp = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 10)}.tmp`;
1673
+ try {
1674
+ await writeFile(tmp, JSON.stringify(entry, null, 2), { mode: 384 });
1675
+ await rename(tmp, path);
1676
+ } catch (err) {
1677
+ await unlink(tmp).catch(() => {});
1678
+ throw err;
1679
+ }
1680
+ }
1681
+ /** Has the throttle window elapsed since the last recorded check? */
1682
+ function isDueForCheck(cache, now = Date.now(), intervalMs = UPDATE_CHECK_INTERVAL_MS) {
1683
+ if (!cache) return true;
1684
+ const last = Date.parse(cache.lastCheckedAt);
1685
+ if (!Number.isFinite(last)) return true;
1686
+ if (now < last) return true;
1687
+ return now - last >= intervalMs;
1688
+ }
1689
+ /**
1690
+ * Perform the throttled update check. Returns the final cache entry (for
1691
+ * testing) or null when skipped. Never throws — network errors are
1692
+ * intentionally swallowed so they never interrupt the foreground command.
1693
+ */
1694
+ async function maybeCheckForUpdate(opts = {}) {
1695
+ const env = opts.env ?? process.env;
1696
+ const isTTY = opts.isTTY ?? Boolean(process.stderr.isTTY);
1697
+ const now = opts.now ?? Date.now();
1698
+ const intervalMs = opts.intervalMs ?? 864e5;
1699
+ const optOut = env.AITCC_NO_UPDATE_CHECK;
1700
+ if (optOut && optOut !== "0" && optOut.toLowerCase() !== "false") return null;
1701
+ if (!isTTY) return null;
1702
+ const cache = await readCache();
1703
+ if (!isDueForCheck(cache, now, intervalMs)) return null;
1704
+ const nowIso = new Date(now).toISOString();
1705
+ const placeholder = {
1706
+ lastCheckedAt: nowIso,
1707
+ ...cache?.latestTag !== void 0 ? { latestTag: cache.latestTag } : {},
1708
+ ...cache?.etag !== void 0 ? { etag: cache.etag } : {}
1709
+ };
1710
+ await writeCache(placeholder).catch(() => {});
1711
+ const previousEtag = cache?.etag;
1712
+ let entry = placeholder;
1713
+ try {
1714
+ const result = await fetchLatestReleaseConditional(previousEtag);
1715
+ if (result.status === "not-modified") entry = {
1716
+ lastCheckedAt: nowIso,
1717
+ ...cache?.latestTag !== void 0 ? { latestTag: cache.latestTag } : {},
1718
+ ...result.etag !== void 0 ? { etag: result.etag } : cache?.etag !== void 0 ? { etag: cache.etag } : {}
1719
+ };
1720
+ else entry = {
1721
+ lastCheckedAt: nowIso,
1722
+ latestTag: result.release.tag_name,
1723
+ ...result.etag !== void 0 ? { etag: result.etag } : {}
1724
+ };
1725
+ await writeCache(entry).catch(() => {});
1726
+ } catch {}
1727
+ maybeEmitNotice(entry, env);
1728
+ return entry;
1729
+ }
1730
+ function maybeEmitNotice(entry, env) {
1731
+ if (!entry.latestTag) return;
1732
+ if (VERSION.startsWith("0.0.0-dev")) return;
1733
+ const latest = versionFromTag(entry.latestTag);
1734
+ if (!latest) return;
1735
+ if (compareSemver(latest, VERSION) <= 0) return;
1736
+ const dim = env.NO_COLOR ? "" : "\x1B[2m";
1737
+ const reset = env.NO_COLOR ? "" : "\x1B[0m";
1738
+ process.stderr.write(`\n${dim}(aitcc ${latest} is available — run \`aitcc upgrade\` to install)${reset}\n`);
1739
+ }
1740
+ //#endregion
1140
1741
  //#region src/commands/whoami.ts
1742
+ async function runBackgroundUpdateCheck(json) {
1743
+ if (json) return;
1744
+ const timeoutMs = 500;
1745
+ await Promise.race([maybeCheckForUpdate().catch(() => null), new Promise((resolve) => {
1746
+ const t = setTimeout(() => resolve(null), timeoutMs);
1747
+ if (typeof t.unref === "function") t.unref();
1748
+ })]);
1749
+ }
1141
1750
  const whoamiCommand = defineCommand({
1142
1751
  meta: {
1143
1752
  name: "whoami",
@@ -1182,6 +1791,7 @@ const whoamiCommand = defineCommand({
1182
1791
  const label = session.user.displayName ? `${session.user.displayName} <${session.user.email}>` : session.user.email;
1183
1792
  process.stdout.write(`Logged in as ${label} (cached)\n`);
1184
1793
  process.stdout.write(`Session captured: ${session.capturedAt}\n`);
1794
+ await runBackgroundUpdateCheck(args.json);
1185
1795
  return exitAfterFlush(ExitCode.Ok);
1186
1796
  }
1187
1797
  try {
@@ -1212,6 +1822,7 @@ const whoamiCommand = defineCommand({
1212
1822
  process.stdout.write("Workspaces:\n");
1213
1823
  for (const w of info.workspaces) process.stdout.write(` - ${w.workspaceName} (id ${w.workspaceId}, ${w.role})\n`);
1214
1824
  }
1825
+ await runBackgroundUpdateCheck(args.json);
1215
1826
  return exitAfterFlush(ExitCode.Ok);
1216
1827
  } catch (err) {
1217
1828
  if (err instanceof TossApiError && err.isAuthError) {
@@ -1244,6 +1855,240 @@ const whoamiCommand = defineCommand({
1244
1855
  }
1245
1856
  });
1246
1857
  //#endregion
1858
+ //#region src/api/workspaces.ts
1859
+ const WORKSPACES_BASE = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
1860
+ async function fetchWorkspaceDetail(workspaceId, cookies, opts = {}) {
1861
+ const raw = await requestConsoleApi({
1862
+ url: `${WORKSPACES_BASE}/workspaces/${workspaceId}`,
1863
+ cookies,
1864
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1865
+ });
1866
+ const id = raw.id;
1867
+ const name = raw.name;
1868
+ if (typeof id !== "number" || !Number.isInteger(id) || id <= 0 || typeof name !== "string") throw new Error(`Unexpected workspace detail shape for id=${workspaceId}`);
1869
+ const { id: _id, name: _name, ...extra } = raw;
1870
+ return {
1871
+ workspaceId: id,
1872
+ workspaceName: name,
1873
+ extra
1874
+ };
1875
+ }
1876
+ //#endregion
1877
+ //#region src/commands/workspace.ts
1878
+ function formatScalar(v) {
1879
+ if (v === null) return "null";
1880
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return String(v);
1881
+ return JSON.stringify(v);
1882
+ }
1883
+ const workspaceCommand = defineCommand({
1884
+ meta: {
1885
+ name: "workspace",
1886
+ description: "Inspect and switch between the workspaces this account can access."
1887
+ },
1888
+ subCommands: {
1889
+ ls: defineCommand({
1890
+ meta: {
1891
+ name: "ls",
1892
+ description: "List workspaces the current user has access to."
1893
+ },
1894
+ args: { json: {
1895
+ type: "boolean",
1896
+ description: "Emit machine-readable JSON to stdout.",
1897
+ default: false
1898
+ } },
1899
+ async run({ args }) {
1900
+ const session = await readSession();
1901
+ if (!session) {
1902
+ emitNotAuthenticated(args.json);
1903
+ return exitAfterFlush(ExitCode.NotAuthenticated);
1904
+ }
1905
+ try {
1906
+ const info = await fetchConsoleMemberUserInfo(session.cookies);
1907
+ const current = session.currentWorkspaceId;
1908
+ if (args.json) {
1909
+ emitJson({
1910
+ ok: true,
1911
+ workspaces: info.workspaces.map((w) => ({
1912
+ workspaceId: w.workspaceId,
1913
+ workspaceName: w.workspaceName,
1914
+ role: w.role,
1915
+ current: w.workspaceId === current
1916
+ }))
1917
+ });
1918
+ return exitAfterFlush(ExitCode.Ok);
1919
+ }
1920
+ if (info.workspaces.length === 0) {
1921
+ process.stdout.write("No workspaces.\n");
1922
+ return exitAfterFlush(ExitCode.Ok);
1923
+ }
1924
+ for (const w of info.workspaces) {
1925
+ const marker = w.workspaceId === current ? "* " : " ";
1926
+ process.stdout.write(`${marker}${w.workspaceId} ${w.workspaceName} (${w.role})\n`);
1927
+ }
1928
+ if (current === void 0) process.stderr.write("No workspace selected. Run `aitcc workspace use <id>`.\n");
1929
+ return exitAfterFlush(ExitCode.Ok);
1930
+ } catch (err) {
1931
+ if (err instanceof TossApiError && err.isAuthError) {
1932
+ emitNotAuthenticated(args.json, "session-expired");
1933
+ return exitAfterFlush(ExitCode.NotAuthenticated);
1934
+ }
1935
+ if (err instanceof NetworkError) {
1936
+ emitNetworkError(args.json, err.message);
1937
+ return exitAfterFlush(ExitCode.NetworkError);
1938
+ }
1939
+ emitApiError(args.json, err.message);
1940
+ return exitAfterFlush(ExitCode.ApiError);
1941
+ }
1942
+ }
1943
+ }),
1944
+ use: defineCommand({
1945
+ meta: {
1946
+ name: "use",
1947
+ description: "Select the current workspace by ID. Subsequent commands use this."
1948
+ },
1949
+ args: {
1950
+ id: {
1951
+ type: "positional",
1952
+ description: "Workspace ID",
1953
+ required: true
1954
+ },
1955
+ json: {
1956
+ type: "boolean",
1957
+ description: "Emit machine-readable JSON to stdout.",
1958
+ default: false
1959
+ }
1960
+ },
1961
+ async run({ args }) {
1962
+ const raw = String(args.id);
1963
+ const parsed = parsePositiveInt(raw);
1964
+ if (parsed === null) {
1965
+ const message = `workspace id must be a positive integer (got ${raw})`;
1966
+ if (args.json) emitJson({
1967
+ ok: false,
1968
+ reason: "invalid-id",
1969
+ message
1970
+ });
1971
+ else process.stderr.write(`${message}\n`);
1972
+ return exitAfterFlush(ExitCode.Usage);
1973
+ }
1974
+ const session = await readSession();
1975
+ if (!session) {
1976
+ emitNotAuthenticated(args.json);
1977
+ return exitAfterFlush(ExitCode.NotAuthenticated);
1978
+ }
1979
+ try {
1980
+ const match = (await fetchConsoleMemberUserInfo(session.cookies)).workspaces.find((w) => w.workspaceId === parsed);
1981
+ if (!match) {
1982
+ if (args.json) emitJson({
1983
+ ok: false,
1984
+ reason: "not-found",
1985
+ workspaceId: parsed
1986
+ });
1987
+ else process.stderr.write(`Workspace ${parsed} is not accessible from this account. Run \`aitcc workspace ls\` to see available workspaces.\n`);
1988
+ return exitAfterFlush(ExitCode.Usage);
1989
+ }
1990
+ if (await setCurrentWorkspaceId(parsed) === null) {
1991
+ emitNotAuthenticated(args.json);
1992
+ return exitAfterFlush(ExitCode.NotAuthenticated);
1993
+ }
1994
+ if (args.json) emitJson({
1995
+ ok: true,
1996
+ workspaceId: match.workspaceId,
1997
+ workspaceName: match.workspaceName
1998
+ });
1999
+ else process.stdout.write(`Using workspace ${match.workspaceId} (${match.workspaceName}).\n`);
2000
+ return exitAfterFlush(ExitCode.Ok);
2001
+ } catch (err) {
2002
+ if (err instanceof TossApiError && err.isAuthError) {
2003
+ emitNotAuthenticated(args.json, "session-expired");
2004
+ return exitAfterFlush(ExitCode.NotAuthenticated);
2005
+ }
2006
+ if (err instanceof NetworkError) {
2007
+ emitNetworkError(args.json, err.message);
2008
+ return exitAfterFlush(ExitCode.NetworkError);
2009
+ }
2010
+ emitApiError(args.json, err.message);
2011
+ return exitAfterFlush(ExitCode.ApiError);
2012
+ }
2013
+ }
2014
+ }),
2015
+ show: defineCommand({
2016
+ meta: {
2017
+ name: "show",
2018
+ description: "Show details of the selected workspace (or the one passed with --workspace)."
2019
+ },
2020
+ args: {
2021
+ workspace: {
2022
+ type: "string",
2023
+ description: "Workspace ID to inspect. Defaults to the selected workspace."
2024
+ },
2025
+ json: {
2026
+ type: "boolean",
2027
+ description: "Emit machine-readable JSON to stdout.",
2028
+ default: false
2029
+ }
2030
+ },
2031
+ async run({ args }) {
2032
+ const session = await readSession();
2033
+ if (!session) {
2034
+ emitNotAuthenticated(args.json);
2035
+ return exitAfterFlush(ExitCode.NotAuthenticated);
2036
+ }
2037
+ let workspaceId;
2038
+ if (args.workspace) {
2039
+ const raw = String(args.workspace);
2040
+ const parsed = parsePositiveInt(raw);
2041
+ if (parsed === null) {
2042
+ const message = `--workspace must be a positive integer (got ${raw})`;
2043
+ if (args.json) emitJson({
2044
+ ok: false,
2045
+ reason: "invalid-id",
2046
+ message
2047
+ });
2048
+ else process.stderr.write(`${message}\n`);
2049
+ return exitAfterFlush(ExitCode.Usage);
2050
+ }
2051
+ workspaceId = parsed;
2052
+ } else workspaceId = session.currentWorkspaceId;
2053
+ if (workspaceId === void 0) {
2054
+ if (args.json) emitJson({
2055
+ ok: false,
2056
+ reason: "no-workspace-selected"
2057
+ });
2058
+ else process.stderr.write("No workspace selected. Pass `--workspace <id>` or run `aitcc workspace use <id>`.\n");
2059
+ return exitAfterFlush(ExitCode.Usage);
2060
+ }
2061
+ try {
2062
+ const detail = await fetchWorkspaceDetail(workspaceId, session.cookies);
2063
+ if (args.json) {
2064
+ emitJson({
2065
+ ok: true,
2066
+ workspaceId: detail.workspaceId,
2067
+ workspaceName: detail.workspaceName,
2068
+ extra: detail.extra ?? {}
2069
+ });
2070
+ return exitAfterFlush(ExitCode.Ok);
2071
+ }
2072
+ process.stdout.write(`Workspace ${detail.workspaceId}: ${detail.workspaceName}\n`);
2073
+ if (detail.extra) for (const [k, v] of Object.entries(detail.extra)) process.stdout.write(` ${k}: ${formatScalar(v)}\n`);
2074
+ return exitAfterFlush(ExitCode.Ok);
2075
+ } catch (err) {
2076
+ if (err instanceof TossApiError && err.isAuthError) {
2077
+ emitNotAuthenticated(args.json, "session-expired");
2078
+ return exitAfterFlush(ExitCode.NotAuthenticated);
2079
+ }
2080
+ if (err instanceof NetworkError) {
2081
+ emitNetworkError(args.json, err.message);
2082
+ return exitAfterFlush(ExitCode.NetworkError);
2083
+ }
2084
+ emitApiError(args.json, err.message);
2085
+ return exitAfterFlush(ExitCode.ApiError);
2086
+ }
2087
+ }
2088
+ })
2089
+ }
2090
+ });
2091
+ //#endregion
1247
2092
  //#region src/cli.ts
1248
2093
  runMain(defineCommand({
1249
2094
  meta: {
@@ -1255,7 +2100,11 @@ runMain(defineCommand({
1255
2100
  whoami: whoamiCommand,
1256
2101
  login: loginCommand,
1257
2102
  logout: logoutCommand,
1258
- upgrade: upgradeCommand
2103
+ upgrade: upgradeCommand,
2104
+ workspace: workspaceCommand,
2105
+ app: appCommand,
2106
+ members: membersCommand,
2107
+ keys: keysCommand
1259
2108
  }
1260
2109
  }));
1261
2110
  //#endregion