@ait-co/console-cli 0.1.5 → 0.1.7

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,472 @@ 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
+ const TITLE_EN_REGEX = /^[A-Za-z0-9 :]+$/;
665
+ const DETAIL_DESCRIPTION_MAX_CODEPOINTS = 500;
666
+ function isValidHttpUrl(v) {
667
+ try {
668
+ const parsed = new URL(v);
669
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
670
+ } catch {
671
+ return false;
672
+ }
673
+ }
674
+ function validateManifest(raw, configDir) {
675
+ const titleKo = requireString(raw, "titleKo");
676
+ const titleEn = requireString(raw, "titleEn");
677
+ if (!TITLE_EN_REGEX.test(titleEn)) throw new ManifestError("invalid-config", `titleEn may only contain English letters, digits, spaces, and colons (got "${titleEn}")`, "titleEn");
678
+ const appName = requireString(raw, "appName");
679
+ const csEmail = requireString(raw, "csEmail");
680
+ if (!isValidEmail(csEmail)) throw new ManifestError("invalid-config", `csEmail is not a valid email address (got ${csEmail})`, "csEmail");
681
+ const subtitle = requireString(raw, "subtitle");
682
+ if (subtitle.length > 20) throw new ManifestError("invalid-config", `subtitle must be 20 characters or fewer (got ${subtitle.length})`, "subtitle");
683
+ const description = requireString(raw, "description");
684
+ const descriptionCodepoints = [...description].length;
685
+ if (descriptionCodepoints > DETAIL_DESCRIPTION_MAX_CODEPOINTS) throw new ManifestError("invalid-config", `description must be ${DETAIL_DESCRIPTION_MAX_CODEPOINTS} characters or fewer (got ${descriptionCodepoints})`, "description");
686
+ const homePageUri = optionalString(raw, "homePageUri");
687
+ if (homePageUri !== void 0 && !isValidHttpUrl(homePageUri)) throw new ManifestError("invalid-config", `homePageUri must be a http(s) URL (got ${homePageUri})`, "homePageUri");
688
+ return {
689
+ titleKo,
690
+ titleEn,
691
+ appName,
692
+ homePageUri,
693
+ csEmail,
694
+ logo: requirePath(raw, "logo", configDir),
695
+ logoDarkMode: optionalPath(raw, "logoDarkMode", configDir),
696
+ horizontalThumbnail: requirePath(raw, "horizontalThumbnail", configDir),
697
+ categoryIds: requireNumberArray(raw, "categoryIds", { min: 1 }),
698
+ subtitle,
699
+ description,
700
+ keywords: optionalStringArray(raw, "keywords", { max: 10 }),
701
+ verticalScreenshots: requirePathArray(raw, "verticalScreenshots", configDir, { min: 3 }),
702
+ horizontalScreenshots: optionalPathArray(raw, "horizontalScreenshots", configDir)
703
+ };
704
+ }
705
+ //#endregion
706
+ //#region src/config/image-validator.ts
707
+ var ImageDimensionError = class extends Error {
708
+ path;
709
+ expected;
710
+ actual;
711
+ reason;
712
+ constructor(args) {
713
+ super(args.message);
714
+ this.name = "ImageDimensionError";
715
+ this.path = args.path;
716
+ this.expected = args.expected;
717
+ this.actual = args.actual;
718
+ this.reason = args.reason;
719
+ }
720
+ };
721
+ function format(dim) {
722
+ return `${dim.width}x${dim.height}`;
723
+ }
724
+ async function validateImageDimensions(path, expected) {
725
+ let buffer;
726
+ try {
727
+ buffer = await readFile(path);
728
+ } catch (err) {
729
+ throw new ImageDimensionError({
730
+ path,
731
+ expected: format(expected),
732
+ actual: void 0,
733
+ reason: "unreadable",
734
+ message: `could not read image at ${path}: ${err.message}`
735
+ });
736
+ }
737
+ let dims;
738
+ try {
739
+ dims = imageSize(buffer);
740
+ } catch (err) {
741
+ throw new ImageDimensionError({
742
+ path,
743
+ expected: format(expected),
744
+ actual: void 0,
745
+ reason: "unreadable",
746
+ message: `could not decode image header at ${path}: ${err.message}`
747
+ });
748
+ }
749
+ if (typeof dims.width !== "number" || typeof dims.height !== "number") throw new ImageDimensionError({
750
+ path,
751
+ expected: format(expected),
752
+ actual: void 0,
753
+ reason: "unreadable",
754
+ message: `image header at ${path} did not expose width/height`
755
+ });
756
+ if (dims.width !== expected.width || dims.height !== expected.height) {
757
+ const actual = `${dims.width}x${dims.height}`;
758
+ throw new ImageDimensionError({
759
+ path,
760
+ expected: format(expected),
761
+ actual,
762
+ reason: "mismatch",
763
+ message: `image ${path} has dimensions ${actual}; expected ${format(expected)}`
764
+ });
765
+ }
766
+ }
767
+ const DIMENSIONS = {
768
+ logo: {
769
+ width: 600,
770
+ height: 600
771
+ },
772
+ horizontalThumbnail: {
773
+ width: 1932,
774
+ height: 828
775
+ },
776
+ verticalScreenshot: {
777
+ width: 636,
778
+ height: 1048
779
+ },
780
+ horizontalScreenshot: {
781
+ width: 1504,
782
+ height: 741
783
+ }
784
+ };
785
+ //#endregion
786
+ //#region src/commands/register-payload.ts
787
+ function buildSubmitPayload(manifest, urls) {
788
+ const images = [
789
+ {
790
+ imageUrl: urls.horizontalThumbnail,
791
+ imageType: "THUMBNAIL",
792
+ orientation: "HORIZONTAL"
793
+ },
794
+ ...urls.verticalScreenshots.map((u) => ({
795
+ imageUrl: u,
796
+ imageType: "PREVIEW",
797
+ orientation: "VERTICAL"
798
+ })),
799
+ ...urls.horizontalScreenshots.map((u) => ({
800
+ imageUrl: u,
801
+ imageType: "PREVIEW",
802
+ orientation: "HORIZONTAL"
803
+ }))
804
+ ];
805
+ return {
806
+ title: manifest.titleKo,
807
+ titleEn: manifest.titleEn,
808
+ appName: manifest.appName,
809
+ iconUri: urls.logo,
810
+ status: "PREPARE",
811
+ csEmail: manifest.csEmail,
812
+ description: manifest.subtitle,
813
+ detailDescription: manifest.description,
814
+ images,
815
+ impression: {
816
+ keywordList: manifest.keywords,
817
+ categoryList: manifest.categoryIds.map((id) => ({ id }))
818
+ },
819
+ ...urls.logoDarkMode !== void 0 ? { darkModeIconUri: urls.logoDarkMode } : {},
820
+ ...manifest.homePageUri !== void 0 ? { homePageUri: manifest.homePageUri } : {}
821
+ };
822
+ }
823
+ //#endregion
824
+ //#region src/commands/register.ts
825
+ async function runRegister(args, deps = {}) {
826
+ const ctx = await resolveWorkspaceContext({
827
+ json: args.json,
828
+ ...args.workspace !== void 0 ? { workspace: args.workspace } : {}
829
+ });
830
+ if (!ctx) return;
831
+ const { session, workspaceId } = ctx;
832
+ const manifest = await loadAndValidateManifest(args, deps);
833
+ if (!manifest) return;
834
+ if (!args.dryRun && !args.acceptTerms) {
835
+ emitTermsNotAccepted(args.json);
836
+ await exitAfterFlush(ExitCode.Usage);
837
+ return;
838
+ }
839
+ try {
840
+ if (args.dryRun) {
841
+ const payload = buildSubmitPayload(manifest, {
842
+ logo: "<dry-run:logo>",
843
+ logoDarkMode: manifest.logoDarkMode !== void 0 ? "<dry-run:logoDarkMode>" : void 0,
844
+ horizontalThumbnail: "<dry-run:horizontalThumbnail>",
845
+ verticalScreenshots: manifest.verticalScreenshots.map((_, i) => `<dry-run:verticalScreenshots[${i}]>`),
846
+ horizontalScreenshots: manifest.horizontalScreenshots.map((_, i) => `<dry-run:horizontalScreenshots[${i}]>`)
847
+ });
848
+ emitDryRun(args.json, workspaceId, payload);
849
+ return exitAfterFlush(ExitCode.Ok);
850
+ }
851
+ const payload = buildSubmitPayload(manifest, await uploadAllImages(workspaceId, manifest, session.cookies, deps));
852
+ const result = await (deps.submitImpl ?? ((wid, p, c) => createMiniApp(wid, p, c)))(workspaceId, payload, session.cookies);
853
+ emitSuccess(args.json, workspaceId, result);
854
+ return exitAfterFlush(ExitCode.Ok);
855
+ } catch (err) {
856
+ return emitFailureAndExit(args.json, err);
857
+ }
858
+ }
859
+ async function loadAndValidateManifest(args, deps) {
860
+ const cwd = deps.cwd ?? process.cwd();
861
+ let manifest;
862
+ try {
863
+ manifest = await loadAppManifest(await resolveManifestPath(args.config, cwd));
864
+ } catch (err) {
865
+ if (err instanceof ManifestError) {
866
+ emitManifestError(args.json, err);
867
+ await exitAfterFlush(ExitCode.Usage);
868
+ return null;
869
+ }
870
+ throw err;
871
+ }
872
+ try {
873
+ await validateImageDimensions(manifest.logo, DIMENSIONS.logo);
874
+ if (manifest.logoDarkMode !== void 0) await validateImageDimensions(manifest.logoDarkMode, DIMENSIONS.logo);
875
+ await validateImageDimensions(manifest.horizontalThumbnail, DIMENSIONS.horizontalThumbnail);
876
+ for (const p of manifest.verticalScreenshots) await validateImageDimensions(p, DIMENSIONS.verticalScreenshot);
877
+ for (const p of manifest.horizontalScreenshots) await validateImageDimensions(p, DIMENSIONS.horizontalScreenshot);
878
+ } catch (err) {
879
+ if (err instanceof ImageDimensionError) {
880
+ emitImageDimensionError(args.json, err);
881
+ await exitAfterFlush(ExitCode.Usage);
882
+ return null;
883
+ }
884
+ throw err;
885
+ }
886
+ return manifest;
887
+ }
888
+ async function uploadAllImages(workspaceId, manifest, cookies, deps) {
889
+ const uploadImpl = deps.uploadImpl ?? ((p) => uploadMiniAppResource(p));
890
+ const logo = await uploadOne(uploadImpl, {
891
+ workspaceId,
892
+ validWidth: DIMENSIONS.logo.width,
893
+ validHeight: DIMENSIONS.logo.height,
894
+ cookies,
895
+ path: manifest.logo
896
+ });
897
+ const logoDarkMode = manifest.logoDarkMode !== void 0 ? await uploadOne(uploadImpl, {
898
+ workspaceId,
899
+ validWidth: DIMENSIONS.logo.width,
900
+ validHeight: DIMENSIONS.logo.height,
901
+ cookies,
902
+ path: manifest.logoDarkMode
903
+ }) : void 0;
904
+ const horizontalThumbnail = await uploadOne(uploadImpl, {
905
+ workspaceId,
906
+ validWidth: DIMENSIONS.horizontalThumbnail.width,
907
+ validHeight: DIMENSIONS.horizontalThumbnail.height,
908
+ cookies,
909
+ path: manifest.horizontalThumbnail
910
+ });
911
+ const verticalScreenshots = [];
912
+ for (const p of manifest.verticalScreenshots) verticalScreenshots.push(await uploadOne(uploadImpl, {
913
+ workspaceId,
914
+ validWidth: DIMENSIONS.verticalScreenshot.width,
915
+ validHeight: DIMENSIONS.verticalScreenshot.height,
916
+ cookies,
917
+ path: p
918
+ }));
919
+ const horizontalScreenshots = [];
920
+ for (const p of manifest.horizontalScreenshots) horizontalScreenshots.push(await uploadOne(uploadImpl, {
921
+ workspaceId,
922
+ validWidth: DIMENSIONS.horizontalScreenshot.width,
923
+ validHeight: DIMENSIONS.horizontalScreenshot.height,
924
+ cookies,
925
+ path: p
926
+ }));
927
+ return {
928
+ logo,
929
+ logoDarkMode,
930
+ horizontalThumbnail,
931
+ verticalScreenshots,
932
+ horizontalScreenshots
933
+ };
934
+ }
935
+ async function uploadOne(uploadImpl, input) {
936
+ const buffer = await readFile(input.path);
937
+ return uploadImpl({
938
+ workspaceId: input.workspaceId,
939
+ validWidth: input.validWidth,
940
+ validHeight: input.validHeight,
941
+ cookies: input.cookies,
942
+ file: {
943
+ buffer,
944
+ fileName: basename(input.path),
945
+ contentType: "image/png"
946
+ }
947
+ });
948
+ }
949
+ function emitManifestError(json, err) {
950
+ if (json) if (err.kind === "missing-required-field") emitJson({
951
+ ok: false,
952
+ reason: "missing-required-field",
953
+ field: err.field ?? null,
954
+ message: err.message
955
+ });
956
+ else emitJson({
957
+ ok: false,
958
+ reason: "invalid-config",
959
+ message: err.message
960
+ });
961
+ else process.stderr.write(`${err.message}\n`);
962
+ }
963
+ function emitImageDimensionError(json, err) {
964
+ if (json) if (err.reason === "mismatch") emitJson({
965
+ ok: false,
966
+ reason: "image-dimension-mismatch",
967
+ path: err.path,
968
+ expected: err.expected,
969
+ actual: err.actual ?? null,
970
+ message: err.message
971
+ });
972
+ else emitJson({
973
+ ok: false,
974
+ reason: "image-unreadable",
975
+ path: err.path,
976
+ message: err.message
977
+ });
978
+ else process.stderr.write(`${err.message}\n`);
979
+ }
980
+ function emitTermsNotAccepted(json) {
981
+ 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.";
982
+ if (json) emitJson({
983
+ ok: false,
984
+ reason: "terms-not-accepted",
985
+ message
986
+ });
987
+ else process.stderr.write(`${message}\n`);
988
+ }
989
+ function emitDryRun(json, workspaceId, payload) {
990
+ if (json) emitJson({
991
+ ok: true,
992
+ dryRun: true,
993
+ workspaceId,
994
+ payload
995
+ });
996
+ else {
997
+ process.stdout.write("[dry-run] Would POST to ");
998
+ process.stdout.write(`https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole/workspaces/${workspaceId}/mini-app/review\n`);
999
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
1000
+ }
1001
+ }
1002
+ function emitSuccess(json, workspaceId, result) {
1003
+ if (json) emitJson({
1004
+ ok: true,
1005
+ workspaceId,
1006
+ appId: result.miniAppId ?? null,
1007
+ reviewState: result.reviewState ?? null
1008
+ });
1009
+ else process.stdout.write(`Registered mini-app ${result.miniAppId ?? "(id unknown)"} in workspace ${workspaceId} (reviewState=${result.reviewState ?? "unknown"}).\n`);
1010
+ }
1011
+ async function emitFailureAndExit(json, err) {
1012
+ return emitFailureFromError(json, err);
1013
+ }
1014
+ //#endregion
444
1015
  //#region src/commands/app.ts
445
1016
  function findReviewEntry(reviewEntries, appId) {
446
1017
  const target = String(appId);
@@ -460,72 +1031,105 @@ const appCommand = defineCommand({
460
1031
  name: "app",
461
1032
  description: "Inspect mini-apps in a workspace."
462
1033
  },
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`)."
1034
+ subCommands: {
1035
+ ls: defineCommand({
1036
+ meta: {
1037
+ name: "ls",
1038
+ description: "List mini-apps in the selected workspace."
472
1039
  },
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);
1040
+ args: {
1041
+ workspace: {
1042
+ type: "string",
1043
+ description: "Workspace ID. Defaults to the selected workspace (`aitcc workspace use`)."
1044
+ },
1045
+ json: {
1046
+ type: "boolean",
1047
+ description: "Emit machine-readable JSON to stdout.",
1048
+ default: false
502
1049
  }
503
- if (apps.length === 0) {
504
- process.stdout.write(`No apps in workspace ${workspaceId}.\n`);
1050
+ },
1051
+ async run({ args }) {
1052
+ const ctx = await resolveWorkspaceContext(args);
1053
+ if (!ctx) return;
1054
+ const { session, workspaceId } = ctx;
1055
+ try {
1056
+ const [apps, review] = await Promise.all([fetchMiniApps(workspaceId, session.cookies), fetchReviewStatus(workspaceId, session.cookies)]);
1057
+ if (args.json) {
1058
+ const joined = apps.map((app) => {
1059
+ const reviewState = reviewStateFor(findReviewEntry(review.miniApps, app.id));
1060
+ return {
1061
+ id: app.id,
1062
+ name: app.name ?? null,
1063
+ ...reviewState !== void 0 ? { reviewState } : {},
1064
+ extra: app.extra
1065
+ };
1066
+ });
1067
+ emitJson({
1068
+ ok: true,
1069
+ workspaceId,
1070
+ hasPolicyViolation: review.hasPolicyViolation,
1071
+ apps: joined
1072
+ });
1073
+ return exitAfterFlush(ExitCode.Ok);
1074
+ }
1075
+ if (apps.length === 0) {
1076
+ process.stdout.write(`No apps in workspace ${workspaceId}.\n`);
1077
+ if (review.hasPolicyViolation) process.stderr.write("Note: workspace-wide policy violation flag is set.\n");
1078
+ return exitAfterFlush(ExitCode.Ok);
1079
+ }
1080
+ for (const app of apps) {
1081
+ const reviewState = reviewStateFor(findReviewEntry(review.miniApps, app.id)) ?? "-";
1082
+ const name = app.name ?? "(unnamed)";
1083
+ process.stdout.write(`${app.id}\t${name}\t${reviewState}\n`);
1084
+ }
505
1085
  if (review.hasPolicyViolation) process.stderr.write("Note: workspace-wide policy violation flag is set.\n");
506
1086
  return exitAfterFlush(ExitCode.Ok);
1087
+ } catch (err) {
1088
+ return emitFailureFromError(args.json, err);
507
1089
  }
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);
1090
+ }
1091
+ }),
1092
+ register: defineCommand({
1093
+ meta: {
1094
+ name: "register",
1095
+ description: "Register a mini-app in the selected workspace from a YAML/JSON manifest. Uploads logo/thumbnail/screenshots, then submits the create payload."
1096
+ },
1097
+ args: {
1098
+ workspace: {
1099
+ type: "string",
1100
+ description: "Workspace ID. Defaults to the selected workspace (`aitcc workspace use`)."
1101
+ },
1102
+ config: {
1103
+ type: "string",
1104
+ description: "Path to the app manifest. Defaults to `./aitcc.app.yaml`, then `./aitcc.app.json`."
1105
+ },
1106
+ "dry-run": {
1107
+ type: "boolean",
1108
+ description: "Validate manifest + images and print the inferred submit payload; no uploads.",
1109
+ default: false
1110
+ },
1111
+ "accept-terms": {
1112
+ type: "boolean",
1113
+ description: "Attest to the required console legal-agreement checkboxes (see VALIDATION-RULES.md). Required for real submits.",
1114
+ default: false
1115
+ },
1116
+ json: {
1117
+ type: "boolean",
1118
+ description: "Emit machine-readable JSON to stdout.",
1119
+ default: false
523
1120
  }
524
- emitApiError(args.json, err.message);
525
- return exitAfterFlush(ExitCode.ApiError);
1121
+ },
1122
+ async run({ args }) {
1123
+ await runRegister({
1124
+ json: args.json,
1125
+ dryRun: args["dry-run"],
1126
+ acceptTerms: args["accept-terms"],
1127
+ ...args.workspace !== void 0 ? { workspace: args.workspace } : {},
1128
+ ...args.config !== void 0 ? { config: args.config } : {}
1129
+ });
526
1130
  }
527
- }
528
- }) }
1131
+ })
1132
+ }
529
1133
  });
530
1134
  //#endregion
531
1135
  //#region src/api/api-keys.ts
@@ -605,16 +1209,7 @@ const keysCommand = defineCommand({
605
1209
  }
606
1210
  return exitAfterFlush(ExitCode.Ok);
607
1211
  } 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);
1212
+ return emitFailureFromError(args.json, err);
618
1213
  }
619
1214
  }
620
1215
  }) }
@@ -1367,16 +1962,7 @@ const membersCommand = defineCommand({
1367
1962
  for (const m of members) process.stdout.write(`${m.bizUserNo}\t${m.name}\t${m.email}\t${m.role}\t${m.status}\n`);
1368
1963
  return exitAfterFlush(ExitCode.Ok);
1369
1964
  } 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);
1965
+ return emitFailureFromError(args.json, err);
1380
1966
  }
1381
1967
  }
1382
1968
  }) }
@@ -1494,7 +2080,7 @@ function resolveVersion() {
1494
2080
  if (typeof injected === "string" && injected.length > 0) return injected;
1495
2081
  } catch {}
1496
2082
  try {
1497
- return "0.1.5";
2083
+ return "0.1.7";
1498
2084
  } catch {}
1499
2085
  return "0.0.0-dev";
1500
2086
  }
@@ -1928,16 +2514,7 @@ const workspaceCommand = defineCommand({
1928
2514
  if (current === void 0) process.stderr.write("No workspace selected. Run `aitcc workspace use <id>`.\n");
1929
2515
  return exitAfterFlush(ExitCode.Ok);
1930
2516
  } 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);
2517
+ return emitFailureFromError(args.json, err);
1941
2518
  }
1942
2519
  }
1943
2520
  }),
@@ -1999,16 +2576,7 @@ const workspaceCommand = defineCommand({
1999
2576
  else process.stdout.write(`Using workspace ${match.workspaceId} (${match.workspaceName}).\n`);
2000
2577
  return exitAfterFlush(ExitCode.Ok);
2001
2578
  } 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);
2579
+ return emitFailureFromError(args.json, err);
2012
2580
  }
2013
2581
  }
2014
2582
  }),
@@ -2073,16 +2641,7 @@ const workspaceCommand = defineCommand({
2073
2641
  if (detail.extra) for (const [k, v] of Object.entries(detail.extra)) process.stdout.write(` ${k}: ${formatScalar(v)}\n`);
2074
2642
  return exitAfterFlush(ExitCode.Ok);
2075
2643
  } 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);
2644
+ return emitFailureFromError(args.json, err);
2086
2645
  }
2087
2646
  }
2088
2647
  })