@ait-co/console-cli 0.1.5 → 0.1.6

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,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from "citty";
3
- import { chmod, mkdir, mkdtemp, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
4
- import { basename, dirname, join, win32 } from "node:path";
3
+ import { access, chmod, mkdir, mkdtemp, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
5
5
  import { homedir, tmpdir } from "node:os";
6
+ import { parse } from "yaml";
7
+ import { imageSize } from "image-size";
6
8
  import { spawn } from "node:child_process";
7
9
  import { constants } from "node:fs";
8
10
  //#region src/api/http.ts
@@ -119,10 +121,25 @@ async function requestConsoleApi(options) {
119
121
  headers["Content-Type"] = "application/json";
120
122
  init.body = JSON.stringify(options.body);
121
123
  }
122
- const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
124
+ return executeAndUnwrap(url, init, options.fetchImpl);
125
+ }
126
+ /**
127
+ * Send a pre-built `RequestInit` against the console API and unwrap the
128
+ * Toss envelope. Use this when the caller needs to build the body itself
129
+ * (multipart uploads, binary requests, anything that can't live under
130
+ * `requestConsoleApi`'s JSON-body assumption). Cookie header composition
131
+ * and any additional headers remain the caller's responsibility.
132
+ *
133
+ * Exists so `uploadMiniAppResource` doesn't have to re-implement the
134
+ * text→JSON→envelope→error branch in `requestConsoleApi`; drift between
135
+ * the two paths has bitten us once (cf. the refactor in the
136
+ * `app register` review).
137
+ */
138
+ async function executeAndUnwrap(url, init, fetchImpl) {
139
+ const impl = fetchImpl ?? ((input, i) => fetch(input, i));
123
140
  let res;
124
141
  try {
125
- res = await fetchImpl(url, init);
142
+ res = await impl(url, init);
126
143
  } catch (err) {
127
144
  throw new NetworkError(url.toString(), err);
128
145
  }
@@ -187,6 +204,60 @@ async function fetchReviewStatus(workspaceId, cookies, opts = {}) {
187
204
  })
188
205
  };
189
206
  }
207
+ async function createMiniApp(workspaceId, payload, cookies, opts = {}) {
208
+ return normalizeCreateResult(await requestConsoleApi({
209
+ url: `${BASE$2}/workspaces/${workspaceId}/mini-app/review`,
210
+ method: "POST",
211
+ cookies,
212
+ body: payload,
213
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
214
+ }));
215
+ }
216
+ function normalizeCreateResult(raw) {
217
+ if (raw === null || typeof raw !== "object") return {
218
+ miniAppId: void 0,
219
+ reviewState: void 0,
220
+ extra: {}
221
+ };
222
+ const rec = raw;
223
+ const rawId = rec.miniAppId ?? rec.id ?? rec.appId;
224
+ const miniAppId = typeof rawId === "string" || typeof rawId === "number" ? rawId : void 0;
225
+ const rawState = rec.reviewState ?? rec.status;
226
+ return {
227
+ miniAppId,
228
+ reviewState: typeof rawState === "string" ? rawState : void 0,
229
+ extra: rec
230
+ };
231
+ }
232
+ /**
233
+ * Upload an image to `/resource/:wid/upload?validWidth=W&validHeight=H`
234
+ * and return the CDN URL the server hands back. The endpoint is a
235
+ * multipart/form-data POST; we build a FormData with a single `resource`
236
+ * field because that matches the bundle analysis for the console's
237
+ * uploader, which pairs a `fileName` string field with a `resource`
238
+ * Blob (see VALIDATION-RULES.md → iconUri). Dog-food #23 may reveal that
239
+ * the field name is actually `file` — if so, swap it in one place here.
240
+ */
241
+ async function uploadMiniAppResource(params, opts = {}) {
242
+ const url = new URL(`${BASE$2}/resource/${params.workspaceId}/upload`);
243
+ url.searchParams.set("validWidth", String(params.validWidth));
244
+ url.searchParams.set("validHeight", String(params.validHeight));
245
+ const form = new FormData();
246
+ const view = new Uint8Array(params.file.buffer.buffer, params.file.buffer.byteOffset, params.file.buffer.byteLength);
247
+ const blob = new Blob([view], { type: params.file.contentType });
248
+ form.append("resource", blob, params.file.fileName);
249
+ form.append("fileName", params.file.fileName);
250
+ const cookieHeader = cookieHeaderFor(url, params.cookies);
251
+ const headers = { Accept: "application/json, text/plain, */*" };
252
+ if (cookieHeader) headers.Cookie = cookieHeader;
253
+ const imageUrl = await executeAndUnwrap(url, {
254
+ method: "POST",
255
+ headers,
256
+ body: form
257
+ }, opts.fetchImpl);
258
+ if (typeof imageUrl !== "string") throw new MalformedResponseError(url.toString(), 200, `expected string imageUrl, got ${typeof imageUrl}`);
259
+ return imageUrl;
260
+ }
190
261
  //#endregion
191
262
  //#region src/exit.ts
192
263
  const ExitCode = {
@@ -376,14 +447,48 @@ function emitNetworkError(json, message) {
376
447
  });
377
448
  else process.stderr.write(`Network error reaching the console API: ${message}.\n`);
378
449
  }
379
- function emitApiError(json, message) {
450
+ function emitApiError(json, message, details) {
380
451
  if (json) emitJson({
381
452
  ok: false,
382
453
  reason: "api-error",
454
+ ...details?.status !== void 0 ? { status: details.status } : {},
455
+ ...details?.errorCode !== void 0 ? { errorCode: details.errorCode } : {},
383
456
  message
384
457
  });
385
458
  else process.stderr.write(`Unexpected error: ${message}\n`);
386
459
  }
460
+ /**
461
+ * Shared auth/network/api dispatch. Every session-scoped command's
462
+ * `catch (err)` block boils down to the same sequence: TossApiError
463
+ * (auth → exit 10, otherwise → exit 17 with status + errorCode),
464
+ * NetworkError (exit 11), fallback (exit 17 with just a message).
465
+ * Exists so we get a single source of truth for the api-error JSON
466
+ * shape — previously each command duplicated the if/else ladder and
467
+ * `register` diverged (it exposed `status`/`errorCode` that the others
468
+ * didn't) until this extraction lined them up.
469
+ *
470
+ * Returns `Promise<void>` but never returns at runtime: every branch
471
+ * awaits `exitAfterFlush` which calls `process.exit`.
472
+ */
473
+ async function emitFailureFromError(json, err) {
474
+ if (err instanceof TossApiError && err.isAuthError) {
475
+ emitNotAuthenticated(json, "session-expired");
476
+ return exitAfterFlush(ExitCode.NotAuthenticated);
477
+ }
478
+ if (err instanceof TossApiError) {
479
+ emitApiError(json, err.message, {
480
+ status: err.status,
481
+ errorCode: err.errorCode
482
+ });
483
+ return exitAfterFlush(ExitCode.ApiError);
484
+ }
485
+ if (err instanceof NetworkError) {
486
+ emitNetworkError(json, err.message);
487
+ return exitAfterFlush(ExitCode.NetworkError);
488
+ }
489
+ emitApiError(json, err.message);
490
+ return exitAfterFlush(ExitCode.ApiError);
491
+ }
387
492
  function parsePositiveInt(raw) {
388
493
  if (!/^[1-9]\d*$/.test(raw)) return null;
389
494
  const n = Number.parseInt(raw, 10);
@@ -441,6 +546,469 @@ async function resolveWorkspaceContext(args) {
441
546
  };
442
547
  }
443
548
  //#endregion
549
+ //#region src/config/app-manifest.ts
550
+ var ManifestError = class extends Error {
551
+ kind;
552
+ field;
553
+ constructor(kind, message, field) {
554
+ super(message);
555
+ this.name = "ManifestError";
556
+ this.kind = kind;
557
+ this.field = field;
558
+ }
559
+ };
560
+ const DEFAULT_NAMES = ["aitcc.app.yaml", "aitcc.app.json"];
561
+ async function fileExists(path) {
562
+ try {
563
+ await access(path);
564
+ return true;
565
+ } catch {
566
+ return false;
567
+ }
568
+ }
569
+ /**
570
+ * Resolve the manifest file path. When `explicit` is provided, we use it
571
+ * verbatim (resolved against `cwd`) and require it to exist. Otherwise we
572
+ * auto-detect `aitcc.app.yaml` then `aitcc.app.json` under `cwd`.
573
+ */
574
+ async function resolveManifestPath(explicit, cwd) {
575
+ if (explicit) {
576
+ const abs = isAbsolute(explicit) ? explicit : resolve(cwd, explicit);
577
+ if (!await fileExists(abs)) throw new ManifestError("invalid-config", `manifest file not found at ${abs}`);
578
+ return abs;
579
+ }
580
+ for (const name of DEFAULT_NAMES) {
581
+ const abs = resolve(cwd, name);
582
+ if (await fileExists(abs)) return abs;
583
+ }
584
+ throw new ManifestError("invalid-config", `no app manifest found (looked for ${DEFAULT_NAMES.join(", ")} in ${cwd})`);
585
+ }
586
+ async function loadAppManifest(path) {
587
+ return validateManifest(parseManifestFile(path, await readFile(path, "utf8")), dirname(path));
588
+ }
589
+ function parseManifestFile(path, raw) {
590
+ const isJson = path.toLowerCase().endsWith(".json");
591
+ try {
592
+ const out = isJson ? JSON.parse(raw) : parse(raw);
593
+ if (out === null || typeof out !== "object" || Array.isArray(out)) throw new ManifestError("invalid-config", `manifest at ${path} is not a mapping`);
594
+ return out;
595
+ } catch (err) {
596
+ if (err instanceof ManifestError) throw err;
597
+ const msg = err.message;
598
+ throw new ManifestError("invalid-config", `failed to parse manifest at ${path}: ${msg}`);
599
+ }
600
+ }
601
+ function requireString(input, key) {
602
+ const v = input[key];
603
+ if (v === void 0 || v === null) throw new ManifestError("missing-required-field", `${key} is required`, key);
604
+ if (typeof v !== "string") throw new ManifestError("invalid-config", `${key} must be a string`, key);
605
+ if (v.trim().length === 0) throw new ManifestError("missing-required-field", `${key} is required`, key);
606
+ return v;
607
+ }
608
+ function optionalString(input, key) {
609
+ const v = input[key];
610
+ if (v === void 0 || v === null) return void 0;
611
+ if (typeof v !== "string") throw new ManifestError("invalid-config", `${key} must be a string when provided`, key);
612
+ return v;
613
+ }
614
+ function requirePath(input, key, configDir) {
615
+ const rel = requireString(input, key);
616
+ return isAbsolute(rel) ? rel : resolve(configDir, rel);
617
+ }
618
+ function optionalPath(input, key, configDir) {
619
+ const rel = optionalString(input, key);
620
+ if (rel === void 0) return void 0;
621
+ return isAbsolute(rel) ? rel : resolve(configDir, rel);
622
+ }
623
+ function requireNumberArray(input, key, { min }) {
624
+ const v = input[key];
625
+ if (!Array.isArray(v)) throw new ManifestError("invalid-config", `${key} must be an array of numbers`, key);
626
+ if (v.length < min) throw new ManifestError("invalid-config", `${key} must contain at least ${min} item(s)`, key);
627
+ return v.map((item, idx) => {
628
+ if (typeof item !== "number" || !Number.isInteger(item)) throw new ManifestError("invalid-config", `${key}[${idx}] must be an integer`, key);
629
+ return item;
630
+ });
631
+ }
632
+ function optionalStringArray(input, key, { max } = {}) {
633
+ const v = input[key];
634
+ if (v === void 0 || v === null) return [];
635
+ if (!Array.isArray(v)) throw new ManifestError("invalid-config", `${key} must be an array of strings`, key);
636
+ if (max !== void 0 && v.length > max) throw new ManifestError("invalid-config", `${key} accepts at most ${max} entries (got ${v.length})`, key);
637
+ return v.map((item, idx) => {
638
+ if (typeof item !== "string") throw new ManifestError("invalid-config", `${key}[${idx}] must be a string`, key);
639
+ return item;
640
+ });
641
+ }
642
+ function requirePathArray(input, key, configDir, { min }) {
643
+ const v = input[key];
644
+ if (!Array.isArray(v)) throw new ManifestError("invalid-config", `${key} must be an array of paths`, key);
645
+ if (v.length < min) throw new ManifestError("invalid-config", `${key} must contain at least ${min} item(s)`, key);
646
+ return v.map((item, idx) => {
647
+ if (typeof item !== "string" || item.trim().length === 0) throw new ManifestError("invalid-config", `${key}[${idx}] must be a non-empty string`, key);
648
+ return isAbsolute(item) ? item : resolve(configDir, item);
649
+ });
650
+ }
651
+ function optionalPathArray(input, key, configDir) {
652
+ const v = input[key];
653
+ if (v === void 0 || v === null) return [];
654
+ if (!Array.isArray(v)) throw new ManifestError("invalid-config", `${key} must be an array of paths`, key);
655
+ return v.map((item, idx) => {
656
+ if (typeof item !== "string" || item.trim().length === 0) throw new ManifestError("invalid-config", `${key}[${idx}] must be a non-empty string`, key);
657
+ return isAbsolute(item) ? item : resolve(configDir, item);
658
+ });
659
+ }
660
+ const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
661
+ function isValidEmail(v) {
662
+ return EMAIL_REGEX.test(v.toLowerCase());
663
+ }
664
+ function isValidHttpUrl(v) {
665
+ try {
666
+ const parsed = new URL(v);
667
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
668
+ } catch {
669
+ return false;
670
+ }
671
+ }
672
+ function validateManifest(raw, configDir) {
673
+ const titleKo = requireString(raw, "titleKo");
674
+ const titleEn = requireString(raw, "titleEn");
675
+ const appName = requireString(raw, "appName");
676
+ const csEmail = requireString(raw, "csEmail");
677
+ if (!isValidEmail(csEmail)) throw new ManifestError("invalid-config", `csEmail is not a valid email address (got ${csEmail})`, "csEmail");
678
+ const subtitle = requireString(raw, "subtitle");
679
+ if (subtitle.length > 20) throw new ManifestError("invalid-config", `subtitle must be 20 characters or fewer (got ${subtitle.length})`, "subtitle");
680
+ const description = requireString(raw, "description");
681
+ const homePageUri = optionalString(raw, "homePageUri");
682
+ if (homePageUri !== void 0 && !isValidHttpUrl(homePageUri)) throw new ManifestError("invalid-config", `homePageUri must be a http(s) URL (got ${homePageUri})`, "homePageUri");
683
+ return {
684
+ titleKo,
685
+ titleEn,
686
+ appName,
687
+ homePageUri,
688
+ csEmail,
689
+ logo: requirePath(raw, "logo", configDir),
690
+ logoDarkMode: optionalPath(raw, "logoDarkMode", configDir),
691
+ horizontalThumbnail: requirePath(raw, "horizontalThumbnail", configDir),
692
+ categoryIds: requireNumberArray(raw, "categoryIds", { min: 1 }),
693
+ subtitle,
694
+ description,
695
+ keywords: optionalStringArray(raw, "keywords", { max: 10 }),
696
+ verticalScreenshots: requirePathArray(raw, "verticalScreenshots", configDir, { min: 3 }),
697
+ horizontalScreenshots: optionalPathArray(raw, "horizontalScreenshots", configDir)
698
+ };
699
+ }
700
+ //#endregion
701
+ //#region src/config/image-validator.ts
702
+ var ImageDimensionError = class extends Error {
703
+ path;
704
+ expected;
705
+ actual;
706
+ reason;
707
+ constructor(args) {
708
+ super(args.message);
709
+ this.name = "ImageDimensionError";
710
+ this.path = args.path;
711
+ this.expected = args.expected;
712
+ this.actual = args.actual;
713
+ this.reason = args.reason;
714
+ }
715
+ };
716
+ function format(dim) {
717
+ return `${dim.width}x${dim.height}`;
718
+ }
719
+ async function validateImageDimensions(path, expected) {
720
+ let buffer;
721
+ try {
722
+ buffer = await readFile(path);
723
+ } catch (err) {
724
+ throw new ImageDimensionError({
725
+ path,
726
+ expected: format(expected),
727
+ actual: void 0,
728
+ reason: "unreadable",
729
+ message: `could not read image at ${path}: ${err.message}`
730
+ });
731
+ }
732
+ let dims;
733
+ try {
734
+ dims = imageSize(buffer);
735
+ } catch (err) {
736
+ throw new ImageDimensionError({
737
+ path,
738
+ expected: format(expected),
739
+ actual: void 0,
740
+ reason: "unreadable",
741
+ message: `could not decode image header at ${path}: ${err.message}`
742
+ });
743
+ }
744
+ if (typeof dims.width !== "number" || typeof dims.height !== "number") throw new ImageDimensionError({
745
+ path,
746
+ expected: format(expected),
747
+ actual: void 0,
748
+ reason: "unreadable",
749
+ message: `image header at ${path} did not expose width/height`
750
+ });
751
+ if (dims.width !== expected.width || dims.height !== expected.height) {
752
+ const actual = `${dims.width}x${dims.height}`;
753
+ throw new ImageDimensionError({
754
+ path,
755
+ expected: format(expected),
756
+ actual,
757
+ reason: "mismatch",
758
+ message: `image ${path} has dimensions ${actual}; expected ${format(expected)}`
759
+ });
760
+ }
761
+ }
762
+ const DIMENSIONS = {
763
+ logo: {
764
+ width: 600,
765
+ height: 600
766
+ },
767
+ horizontalThumbnail: {
768
+ width: 1932,
769
+ height: 828
770
+ },
771
+ verticalScreenshot: {
772
+ width: 636,
773
+ height: 1048
774
+ },
775
+ horizontalScreenshot: {
776
+ width: 1504,
777
+ height: 741
778
+ }
779
+ };
780
+ //#endregion
781
+ //#region src/commands/register-payload.ts
782
+ function buildSubmitPayload(manifest, urls) {
783
+ const images = [
784
+ {
785
+ imageUrl: urls.horizontalThumbnail,
786
+ imageType: "THUMBNAIL",
787
+ orientation: "HORIZONTAL"
788
+ },
789
+ ...urls.verticalScreenshots.map((u) => ({
790
+ imageUrl: u,
791
+ imageType: "PREVIEW",
792
+ orientation: "VERTICAL"
793
+ })),
794
+ ...urls.horizontalScreenshots.map((u) => ({
795
+ imageUrl: u,
796
+ imageType: "PREVIEW",
797
+ orientation: "HORIZONTAL"
798
+ }))
799
+ ];
800
+ return {
801
+ miniApp: {
802
+ title: manifest.titleKo,
803
+ titleEn: manifest.titleEn,
804
+ appName: manifest.appName,
805
+ iconUri: urls.logo,
806
+ status: "PREPARE",
807
+ csEmail: manifest.csEmail,
808
+ description: manifest.subtitle,
809
+ detailDescription: manifest.description,
810
+ images,
811
+ ...urls.logoDarkMode !== void 0 ? { darkModeIconUri: urls.logoDarkMode } : {},
812
+ ...manifest.homePageUri !== void 0 ? { homePageUri: manifest.homePageUri } : {}
813
+ },
814
+ impression: {
815
+ keywordList: manifest.keywords,
816
+ categoryIds: manifest.categoryIds
817
+ }
818
+ };
819
+ }
820
+ //#endregion
821
+ //#region src/commands/register.ts
822
+ async function runRegister(args, deps = {}) {
823
+ const ctx = await resolveWorkspaceContext({
824
+ json: args.json,
825
+ ...args.workspace !== void 0 ? { workspace: args.workspace } : {}
826
+ });
827
+ if (!ctx) return;
828
+ const { session, workspaceId } = ctx;
829
+ const manifest = await loadAndValidateManifest(args, deps);
830
+ if (!manifest) return;
831
+ if (!args.dryRun && !args.acceptTerms) {
832
+ emitTermsNotAccepted(args.json);
833
+ await exitAfterFlush(ExitCode.Usage);
834
+ return;
835
+ }
836
+ try {
837
+ if (args.dryRun) {
838
+ const payload = buildSubmitPayload(manifest, {
839
+ logo: "<dry-run:logo>",
840
+ logoDarkMode: manifest.logoDarkMode !== void 0 ? "<dry-run:logoDarkMode>" : void 0,
841
+ horizontalThumbnail: "<dry-run:horizontalThumbnail>",
842
+ verticalScreenshots: manifest.verticalScreenshots.map((_, i) => `<dry-run:verticalScreenshots[${i}]>`),
843
+ horizontalScreenshots: manifest.horizontalScreenshots.map((_, i) => `<dry-run:horizontalScreenshots[${i}]>`)
844
+ });
845
+ emitDryRun(args.json, workspaceId, payload);
846
+ return exitAfterFlush(ExitCode.Ok);
847
+ }
848
+ const payload = buildSubmitPayload(manifest, await uploadAllImages(workspaceId, manifest, session.cookies, deps));
849
+ const result = await (deps.submitImpl ?? ((wid, p, c) => createMiniApp(wid, p, c)))(workspaceId, payload, session.cookies);
850
+ emitSuccess(args.json, workspaceId, result);
851
+ return exitAfterFlush(ExitCode.Ok);
852
+ } catch (err) {
853
+ return emitFailureAndExit(args.json, err);
854
+ }
855
+ }
856
+ async function loadAndValidateManifest(args, deps) {
857
+ const cwd = deps.cwd ?? process.cwd();
858
+ let manifest;
859
+ try {
860
+ manifest = await loadAppManifest(await resolveManifestPath(args.config, cwd));
861
+ } catch (err) {
862
+ if (err instanceof ManifestError) {
863
+ emitManifestError(args.json, err);
864
+ await exitAfterFlush(ExitCode.Usage);
865
+ return null;
866
+ }
867
+ throw err;
868
+ }
869
+ try {
870
+ await validateImageDimensions(manifest.logo, DIMENSIONS.logo);
871
+ if (manifest.logoDarkMode !== void 0) await validateImageDimensions(manifest.logoDarkMode, DIMENSIONS.logo);
872
+ await validateImageDimensions(manifest.horizontalThumbnail, DIMENSIONS.horizontalThumbnail);
873
+ for (const p of manifest.verticalScreenshots) await validateImageDimensions(p, DIMENSIONS.verticalScreenshot);
874
+ for (const p of manifest.horizontalScreenshots) await validateImageDimensions(p, DIMENSIONS.horizontalScreenshot);
875
+ } catch (err) {
876
+ if (err instanceof ImageDimensionError) {
877
+ emitImageDimensionError(args.json, err);
878
+ await exitAfterFlush(ExitCode.Usage);
879
+ return null;
880
+ }
881
+ throw err;
882
+ }
883
+ return manifest;
884
+ }
885
+ async function uploadAllImages(workspaceId, manifest, cookies, deps) {
886
+ const uploadImpl = deps.uploadImpl ?? ((p) => uploadMiniAppResource(p));
887
+ const logo = await uploadOne(uploadImpl, {
888
+ workspaceId,
889
+ validWidth: DIMENSIONS.logo.width,
890
+ validHeight: DIMENSIONS.logo.height,
891
+ cookies,
892
+ path: manifest.logo
893
+ });
894
+ const logoDarkMode = manifest.logoDarkMode !== void 0 ? await uploadOne(uploadImpl, {
895
+ workspaceId,
896
+ validWidth: DIMENSIONS.logo.width,
897
+ validHeight: DIMENSIONS.logo.height,
898
+ cookies,
899
+ path: manifest.logoDarkMode
900
+ }) : void 0;
901
+ const horizontalThumbnail = await uploadOne(uploadImpl, {
902
+ workspaceId,
903
+ validWidth: DIMENSIONS.horizontalThumbnail.width,
904
+ validHeight: DIMENSIONS.horizontalThumbnail.height,
905
+ cookies,
906
+ path: manifest.horizontalThumbnail
907
+ });
908
+ const verticalScreenshots = [];
909
+ for (const p of manifest.verticalScreenshots) verticalScreenshots.push(await uploadOne(uploadImpl, {
910
+ workspaceId,
911
+ validWidth: DIMENSIONS.verticalScreenshot.width,
912
+ validHeight: DIMENSIONS.verticalScreenshot.height,
913
+ cookies,
914
+ path: p
915
+ }));
916
+ const horizontalScreenshots = [];
917
+ for (const p of manifest.horizontalScreenshots) horizontalScreenshots.push(await uploadOne(uploadImpl, {
918
+ workspaceId,
919
+ validWidth: DIMENSIONS.horizontalScreenshot.width,
920
+ validHeight: DIMENSIONS.horizontalScreenshot.height,
921
+ cookies,
922
+ path: p
923
+ }));
924
+ return {
925
+ logo,
926
+ logoDarkMode,
927
+ horizontalThumbnail,
928
+ verticalScreenshots,
929
+ horizontalScreenshots
930
+ };
931
+ }
932
+ async function uploadOne(uploadImpl, input) {
933
+ const buffer = await readFile(input.path);
934
+ return uploadImpl({
935
+ workspaceId: input.workspaceId,
936
+ validWidth: input.validWidth,
937
+ validHeight: input.validHeight,
938
+ cookies: input.cookies,
939
+ file: {
940
+ buffer,
941
+ fileName: basename(input.path),
942
+ contentType: "image/png"
943
+ }
944
+ });
945
+ }
946
+ function emitManifestError(json, err) {
947
+ if (json) if (err.kind === "missing-required-field") emitJson({
948
+ ok: false,
949
+ reason: "missing-required-field",
950
+ field: err.field ?? null,
951
+ message: err.message
952
+ });
953
+ else emitJson({
954
+ ok: false,
955
+ reason: "invalid-config",
956
+ message: err.message
957
+ });
958
+ else process.stderr.write(`${err.message}\n`);
959
+ }
960
+ function emitImageDimensionError(json, err) {
961
+ if (json) if (err.reason === "mismatch") emitJson({
962
+ ok: false,
963
+ reason: "image-dimension-mismatch",
964
+ path: err.path,
965
+ expected: err.expected,
966
+ actual: err.actual ?? null,
967
+ message: err.message
968
+ });
969
+ else emitJson({
970
+ ok: false,
971
+ reason: "image-unreadable",
972
+ path: err.path,
973
+ message: err.message
974
+ });
975
+ else process.stderr.write(`${err.message}\n`);
976
+ }
977
+ function emitTermsNotAccepted(json) {
978
+ const message = "The console requires several legal-agreement checkboxes before submitting a mini-app for review. Re-run with --accept-terms to attest that you have read and agree to each of them (see VALIDATION-RULES.md or the console UI), or use --dry-run to preview the payload without submitting.";
979
+ if (json) emitJson({
980
+ ok: false,
981
+ reason: "terms-not-accepted",
982
+ message
983
+ });
984
+ else process.stderr.write(`${message}\n`);
985
+ }
986
+ function emitDryRun(json, workspaceId, payload) {
987
+ if (json) emitJson({
988
+ ok: true,
989
+ dryRun: true,
990
+ workspaceId,
991
+ payload
992
+ });
993
+ else {
994
+ process.stdout.write("[dry-run] Would POST to ");
995
+ process.stdout.write(`https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole/workspaces/${workspaceId}/mini-app/review\n`);
996
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
997
+ }
998
+ }
999
+ function emitSuccess(json, workspaceId, result) {
1000
+ if (json) emitJson({
1001
+ ok: true,
1002
+ workspaceId,
1003
+ appId: result.miniAppId ?? null,
1004
+ reviewState: result.reviewState ?? null
1005
+ });
1006
+ else process.stdout.write(`Registered mini-app ${result.miniAppId ?? "(id unknown)"} in workspace ${workspaceId} (reviewState=${result.reviewState ?? "unknown"}).\n`);
1007
+ }
1008
+ async function emitFailureAndExit(json, err) {
1009
+ return emitFailureFromError(json, err);
1010
+ }
1011
+ //#endregion
444
1012
  //#region src/commands/app.ts
445
1013
  function findReviewEntry(reviewEntries, appId) {
446
1014
  const target = String(appId);
@@ -460,72 +1028,105 @@ const appCommand = defineCommand({
460
1028
  name: "app",
461
1029
  description: "Inspect mini-apps in a workspace."
462
1030
  },
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`)."
1031
+ subCommands: {
1032
+ ls: defineCommand({
1033
+ meta: {
1034
+ name: "ls",
1035
+ description: "List mini-apps in the selected workspace."
472
1036
  },
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);
1037
+ args: {
1038
+ workspace: {
1039
+ type: "string",
1040
+ description: "Workspace ID. Defaults to the selected workspace (`aitcc workspace use`)."
1041
+ },
1042
+ json: {
1043
+ type: "boolean",
1044
+ description: "Emit machine-readable JSON to stdout.",
1045
+ default: false
502
1046
  }
503
- if (apps.length === 0) {
504
- process.stdout.write(`No apps in workspace ${workspaceId}.\n`);
1047
+ },
1048
+ async run({ args }) {
1049
+ const ctx = await resolveWorkspaceContext(args);
1050
+ if (!ctx) return;
1051
+ const { session, workspaceId } = ctx;
1052
+ try {
1053
+ const [apps, review] = await Promise.all([fetchMiniApps(workspaceId, session.cookies), fetchReviewStatus(workspaceId, session.cookies)]);
1054
+ if (args.json) {
1055
+ const joined = apps.map((app) => {
1056
+ const reviewState = reviewStateFor(findReviewEntry(review.miniApps, app.id));
1057
+ return {
1058
+ id: app.id,
1059
+ name: app.name ?? null,
1060
+ ...reviewState !== void 0 ? { reviewState } : {},
1061
+ extra: app.extra
1062
+ };
1063
+ });
1064
+ emitJson({
1065
+ ok: true,
1066
+ workspaceId,
1067
+ hasPolicyViolation: review.hasPolicyViolation,
1068
+ apps: joined
1069
+ });
1070
+ return exitAfterFlush(ExitCode.Ok);
1071
+ }
1072
+ if (apps.length === 0) {
1073
+ process.stdout.write(`No apps in workspace ${workspaceId}.\n`);
1074
+ if (review.hasPolicyViolation) process.stderr.write("Note: workspace-wide policy violation flag is set.\n");
1075
+ return exitAfterFlush(ExitCode.Ok);
1076
+ }
1077
+ for (const app of apps) {
1078
+ const reviewState = reviewStateFor(findReviewEntry(review.miniApps, app.id)) ?? "-";
1079
+ const name = app.name ?? "(unnamed)";
1080
+ process.stdout.write(`${app.id}\t${name}\t${reviewState}\n`);
1081
+ }
505
1082
  if (review.hasPolicyViolation) process.stderr.write("Note: workspace-wide policy violation flag is set.\n");
506
1083
  return exitAfterFlush(ExitCode.Ok);
1084
+ } catch (err) {
1085
+ return emitFailureFromError(args.json, err);
507
1086
  }
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);
1087
+ }
1088
+ }),
1089
+ register: defineCommand({
1090
+ meta: {
1091
+ name: "register",
1092
+ description: "Register a mini-app in the selected workspace from a YAML/JSON manifest. Uploads logo/thumbnail/screenshots, then submits the create payload."
1093
+ },
1094
+ args: {
1095
+ workspace: {
1096
+ type: "string",
1097
+ description: "Workspace ID. Defaults to the selected workspace (`aitcc workspace use`)."
1098
+ },
1099
+ config: {
1100
+ type: "string",
1101
+ description: "Path to the app manifest. Defaults to `./aitcc.app.yaml`, then `./aitcc.app.json`."
1102
+ },
1103
+ "dry-run": {
1104
+ type: "boolean",
1105
+ description: "Validate manifest + images and print the inferred submit payload; no uploads.",
1106
+ default: false
1107
+ },
1108
+ "accept-terms": {
1109
+ type: "boolean",
1110
+ description: "Attest to the required console legal-agreement checkboxes (see VALIDATION-RULES.md). Required for real submits.",
1111
+ default: false
1112
+ },
1113
+ json: {
1114
+ type: "boolean",
1115
+ description: "Emit machine-readable JSON to stdout.",
1116
+ default: false
523
1117
  }
524
- emitApiError(args.json, err.message);
525
- return exitAfterFlush(ExitCode.ApiError);
1118
+ },
1119
+ async run({ args }) {
1120
+ await runRegister({
1121
+ json: args.json,
1122
+ dryRun: args["dry-run"],
1123
+ acceptTerms: args["accept-terms"],
1124
+ ...args.workspace !== void 0 ? { workspace: args.workspace } : {},
1125
+ ...args.config !== void 0 ? { config: args.config } : {}
1126
+ });
526
1127
  }
527
- }
528
- }) }
1128
+ })
1129
+ }
529
1130
  });
530
1131
  //#endregion
531
1132
  //#region src/api/api-keys.ts
@@ -605,16 +1206,7 @@ const keysCommand = defineCommand({
605
1206
  }
606
1207
  return exitAfterFlush(ExitCode.Ok);
607
1208
  } 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);
1209
+ return emitFailureFromError(args.json, err);
618
1210
  }
619
1211
  }
620
1212
  }) }
@@ -1367,16 +1959,7 @@ const membersCommand = defineCommand({
1367
1959
  for (const m of members) process.stdout.write(`${m.bizUserNo}\t${m.name}\t${m.email}\t${m.role}\t${m.status}\n`);
1368
1960
  return exitAfterFlush(ExitCode.Ok);
1369
1961
  } 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);
1962
+ return emitFailureFromError(args.json, err);
1380
1963
  }
1381
1964
  }
1382
1965
  }) }
@@ -1494,7 +2077,7 @@ function resolveVersion() {
1494
2077
  if (typeof injected === "string" && injected.length > 0) return injected;
1495
2078
  } catch {}
1496
2079
  try {
1497
- return "0.1.5";
2080
+ return "0.1.6";
1498
2081
  } catch {}
1499
2082
  return "0.0.0-dev";
1500
2083
  }
@@ -1928,16 +2511,7 @@ const workspaceCommand = defineCommand({
1928
2511
  if (current === void 0) process.stderr.write("No workspace selected. Run `aitcc workspace use <id>`.\n");
1929
2512
  return exitAfterFlush(ExitCode.Ok);
1930
2513
  } 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);
2514
+ return emitFailureFromError(args.json, err);
1941
2515
  }
1942
2516
  }
1943
2517
  }),
@@ -1999,16 +2573,7 @@ const workspaceCommand = defineCommand({
1999
2573
  else process.stdout.write(`Using workspace ${match.workspaceId} (${match.workspaceName}).\n`);
2000
2574
  return exitAfterFlush(ExitCode.Ok);
2001
2575
  } 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);
2576
+ return emitFailureFromError(args.json, err);
2012
2577
  }
2013
2578
  }
2014
2579
  }),
@@ -2073,16 +2638,7 @@ const workspaceCommand = defineCommand({
2073
2638
  if (detail.extra) for (const [k, v] of Object.entries(detail.extra)) process.stdout.write(` ${k}: ${formatScalar(v)}\n`);
2074
2639
  return exitAfterFlush(ExitCode.Ok);
2075
2640
  } 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);
2641
+ return emitFailureFromError(args.json, err);
2086
2642
  }
2087
2643
  }
2088
2644
  })