sayren 0.4.0 → 0.5.0

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.
Files changed (2) hide show
  1. package/dist/index.mjs +1882 -170
  2. package/package.json +4 -3
package/dist/index.mjs CHANGED
@@ -2,11 +2,13 @@
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import { stdin, stdout } from "node:process";
4
4
  import { createInterface } from "node:readline/promises";
5
- import { ApiError, ApprovalPendingError, STOREFRONT_BUILD_TERMINAL_STATUSES, STOREFRONT_SOURCE_BLOB_BATCH_MAX, createStoreClient, normalizeStorefrontPath } from "@sayren/store-sdk";
5
+ import { APP_DEPLOY_KEY_PATTERN, APP_SECRET_PATTERNS, APP_SERVER_BUNDLE_MAX_GZIP_BYTES, APP_UI_MAX_BYTES, APP_UI_MAX_FILES, ApiError, ApprovalPendingError, STOREFRONT_BUILD_TERMINAL_STATUSES, STOREFRONT_SOURCE_BLOB_BATCH_MAX, appManifestProblems, appManifestSchema, createStoreClient, normalizeStorefrontPath } from "@sayren/store-sdk";
6
6
  import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
7
- import { dirname, join, relative, resolve, sep } from "node:path";
8
- import { createHash, randomBytes } from "node:crypto";
9
7
  import { createServer } from "node:http";
8
+ import { dirname, extname, join, relative, resolve, sep } from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+ import { gzipSync } from "node:zlib";
11
+ import { createHash, randomBytes } from "node:crypto";
10
12
 
11
13
  //#region ../i18n/src/paraglide/runtime.js
12
14
  /** @type {any} */
@@ -592,185 +594,973 @@ function defineCustomClientStrategy(strategy$1, handler) {
592
594
  * }} MessageMarkupTag
593
595
  */
594
596
  /**
595
- * Type-level schema for all markup tags in a message.
596
- *
597
- * @typedef {Record<string, MessageMarkupTag>} MessageMarkupSchema
597
+ * Type-level schema for all markup tags in a message.
598
+ *
599
+ * @typedef {Record<string, MessageMarkupTag>} MessageMarkupSchema
600
+ */
601
+ /**
602
+ * Type-only metadata attached to compiled message functions.
603
+ *
604
+ * @template Inputs
605
+ * @template Options
606
+ * @template {MessageMarkupSchema} [Markup = MessageMarkupSchema]
607
+ * @typedef {{
608
+ * readonly __paraglide?: {
609
+ * inputs: Inputs;
610
+ * options: Options;
611
+ * markup: Markup;
612
+ * };
613
+ * }} MessageMetadata
614
+ */
615
+ /**
616
+ * A compiled, framework-neutral message part.
617
+ *
618
+ * @typedef {{
619
+ * type: "text";
620
+ * value: string;
621
+ * } | {
622
+ * type: "markup-start";
623
+ * name: string;
624
+ * options: MessageMarkupOptions;
625
+ * attributes: MessageMarkupAttributes;
626
+ * } | {
627
+ * type: "markup-end";
628
+ * name: string;
629
+ * options: MessageMarkupOptions;
630
+ * attributes: MessageMarkupAttributes;
631
+ * } | {
632
+ * type: "markup-standalone";
633
+ * name: string;
634
+ * options: MessageMarkupOptions;
635
+ * attributes: MessageMarkupAttributes;
636
+ * }} MessagePart
637
+ */
638
+ /**
639
+ * A message function is a message for a specific locale.
640
+ *
641
+ * @example
642
+ * m.hello({ name: 'world' })
643
+ *
644
+ * @typedef {(inputs?: Record<string, never>) => LocalizedString} MessageFunction
645
+ */
646
+ /**
647
+ * A message bundle function that selects the message to be returned.
648
+ *
649
+ * Uses `getLocale()` under the hood to determine the locale with an option.
650
+ *
651
+ * @template {string} T
652
+ *
653
+ * @example
654
+ * * m.hello({ name: 'world' }, { locale: "en" })
655
+ *
656
+ * @typedef {(params: Record<string, never>, options: { locale: T }) => LocalizedString} MessageBundleFunction
657
+ */
658
+
659
+ //#endregion
660
+ //#region ../i18n/src/locale.ts
661
+ /**
662
+ * 로케일 판정(이슈 #73) — 1순위 쿼리 `?lang=`, 2순위 `Accept-Language`(브라우저는 `navigator.languages`), 마지막 `ko`.
663
+ * 순서는 컴파일 옵션(`scripts/compile.mjs`의 strategy)이 정하고, 여기서는 Paraglide에 없는 쿼리 판정만 등록한다.
664
+ * api는 같은 순서를 `@nestjs/i18n` 리졸버(Query → AcceptLanguage)로 맞춘다.
665
+ */
666
+ const LOCALE_QUERY_PARAM = "lang";
667
+ /** `?lang=ko-KR` → `ko`. 지원하지 않는 값·없으면 undefined(다음 판정으로 넘어간다) */
668
+ function localeFromSearch(search) {
669
+ const value = new URLSearchParams(search).get(LOCALE_QUERY_PARAM);
670
+ if (!value) return void 0;
671
+ return toLocale(value) ?? toLocale(value.split("-")[0]);
672
+ }
673
+ defineCustomServerStrategy("custom-query", { getLocale: (request) => request ? localeFromSearch(new URL(request.url).search) : void 0 });
674
+ /** 브라우저의 현재 주소 쿼리 — DOM 타입에 기대지 않는다(DOM lib 없는 소비자, 예: mcp-gateway도 이 소스를 타입 검사한다) */
675
+ function currentSearch() {
676
+ return globalThis.location?.search;
677
+ }
678
+ defineCustomClientStrategy("custom-query", {
679
+ getLocale: () => {
680
+ const search = currentSearch();
681
+ return search === void 0 ? void 0 : localeFromSearch(search);
682
+ },
683
+ setLocale: () => {}
684
+ });
685
+
686
+ //#endregion
687
+ //#region ../i18n/src/paraglide/messages/cli_access_token_expired.js
688
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
689
+ /** @typedef {{}} Cli_Access_Token_ExpiredInputs */
690
+ const ko_cli_access_token_expired = () => {
691
+ return `액세스 토큰이 만료됐습니다. 새 토큰을 만들거나 npx sayren login으로 다시 로그인하십시오`;
692
+ };
693
+ /**
694
+
695
+ * @param {Cli_Access_Token_ExpiredInputs} inputs
696
+ * @param {{ locale?: "ko" }} options
697
+ * @returns {LocalizedString}
698
+ */
699
+ const cli_access_token_expired = ((inputs = {}, options = {}) => {
700
+ experimentalStaticLocale ?? options.locale ?? getLocale();
701
+ return ko_cli_access_token_expired(inputs);
702
+ });
703
+
704
+ //#endregion
705
+ //#region ../i18n/src/paraglide/messages/cli_account_mcp_written.js
706
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
707
+ /** @typedef {{ file: NonNullable<unknown>, mcpFile: NonNullable<unknown> }} Cli_Account_Mcp_WrittenInputs */
708
+ const ko_cli_account_mcp_written = (i) => {
709
+ return `${i?.file} 작성 — 원격 MCP 주소와 상점. ${i?.mcpFile}에는 토큰을 쓰지 않았습니다 — MCP 클라이언트가 연결할 때 Sayren 계정으로 로그인합니다`;
710
+ };
711
+ /**
712
+
713
+ * @param {Cli_Account_Mcp_WrittenInputs} inputs
714
+ * @param {{ locale?: "ko" }} options
715
+ * @returns {LocalizedString}
716
+ */
717
+ const cli_account_mcp_written = ((inputs, options = {}) => {
718
+ experimentalStaticLocale ?? options.locale ?? getLocale();
719
+ return ko_cli_account_mcp_written(inputs);
720
+ });
721
+
722
+ //#endregion
723
+ //#region ../i18n/src/paraglide/messages/cli_accounts_origin_unknown.js
724
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
725
+ /** @typedef {{ origin: NonNullable<unknown> }} Cli_Accounts_Origin_UnknownInputs */
726
+ const ko_cli_accounts_origin_unknown = (i) => {
727
+ return `이 api 주소(${i?.origin})의 계정 서버를 모릅니다. --accounts-origin 또는 SAYREN_ACCOUNTS_ORIGIN으로 주십시오`;
728
+ };
729
+ /**
730
+
731
+ * @param {Cli_Accounts_Origin_UnknownInputs} inputs
732
+ * @param {{ locale?: "ko" }} options
733
+ * @returns {LocalizedString}
734
+ */
735
+ const cli_accounts_origin_unknown = ((inputs, options = {}) => {
736
+ experimentalStaticLocale ?? options.locale ?? getLocale();
737
+ return ko_cli_accounts_origin_unknown(inputs);
738
+ });
739
+
740
+ //#endregion
741
+ //#region ../i18n/src/paraglide/messages/cli_accounts_unreachable.js
742
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
743
+ /** @typedef {{}} Cli_Accounts_UnreachableInputs */
744
+ const ko_cli_accounts_unreachable = () => {
745
+ return `계정 서버에 연결하지 못했습니다. 네트워크를 확인하십시오`;
746
+ };
747
+ /**
748
+
749
+ * @param {Cli_Accounts_UnreachableInputs} inputs
750
+ * @param {{ locale?: "ko" }} options
751
+ * @returns {LocalizedString}
752
+ */
753
+ const cli_accounts_unreachable = ((inputs = {}, options = {}) => {
754
+ experimentalStaticLocale ?? options.locale ?? getLocale();
755
+ return ko_cli_accounts_unreachable(inputs);
756
+ });
757
+
758
+ //#endregion
759
+ //#region ../i18n/src/paraglide/messages/cli_app_bridge_missing.js
760
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
761
+ /** @typedef {{}} Cli_App_Bridge_MissingInputs */
762
+ const ko_cli_app_bridge_missing = () => {
763
+ return `앱 폴더에 @sayren/app이 설치돼 있지 않습니다(ui/_sayren/ui.js를 만들 수 없습니다). npm install을 먼저 하십시오`;
764
+ };
765
+ /**
766
+
767
+ * @param {Cli_App_Bridge_MissingInputs} inputs
768
+ * @param {{ locale?: "ko" }} options
769
+ * @returns {LocalizedString}
770
+ */
771
+ const cli_app_bridge_missing = ((inputs = {}, options = {}) => {
772
+ experimentalStaticLocale ?? options.locale ?? getLocale();
773
+ return ko_cli_app_bridge_missing(inputs);
774
+ });
775
+
776
+ //#endregion
777
+ //#region ../i18n/src/paraglide/messages/cli_app_build_failed.js
778
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
779
+ /** @typedef {{ stage: NonNullable<unknown>, code: NonNullable<unknown>, tail: NonNullable<unknown> }} Cli_App_Build_FailedInputs */
780
+ const ko_cli_app_build_failed = (i) => {
781
+ return `빌드 실패: ${i?.stage} (${i?.code})
782
+ ${i?.tail}`;
783
+ };
784
+ /**
785
+
786
+ * @param {Cli_App_Build_FailedInputs} inputs
787
+ * @param {{ locale?: "ko" }} options
788
+ * @returns {LocalizedString}
789
+ */
790
+ const cli_app_build_failed = ((inputs, options = {}) => {
791
+ experimentalStaticLocale ?? options.locale ?? getLocale();
792
+ return ko_cli_app_build_failed(inputs);
793
+ });
794
+
795
+ //#endregion
796
+ //#region ../i18n/src/paraglide/messages/cli_app_bundle_failed.js
797
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
798
+ /** @typedef {{ tail: NonNullable<unknown> }} Cli_App_Bundle_FailedInputs */
799
+ const ko_cli_app_bundle_failed = (i) => {
800
+ return `서버 번들을 만들지 못했습니다:
801
+ ${i?.tail}`;
802
+ };
803
+ /**
804
+
805
+ * @param {Cli_App_Bundle_FailedInputs} inputs
806
+ * @param {{ locale?: "ko" }} options
807
+ * @returns {LocalizedString}
808
+ */
809
+ const cli_app_bundle_failed = ((inputs, options = {}) => {
810
+ experimentalStaticLocale ?? options.locale ?? getLocale();
811
+ return ko_cli_app_bundle_failed(inputs);
812
+ });
813
+
814
+ //#endregion
815
+ //#region ../i18n/src/paraglide/messages/cli_app_bundle_too_large.js
816
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
817
+ /** @typedef {{ size: NonNullable<unknown> }} Cli_App_Bundle_Too_LargeInputs */
818
+ const ko_cli_app_bundle_too_large = (i) => {
819
+ return `서버 번들이 ${i?.size}(gzip)입니다. 상한은 3MB입니다 (ARTIFACT_TOO_LARGE)`;
820
+ };
821
+ /**
822
+
823
+ * @param {Cli_App_Bundle_Too_LargeInputs} inputs
824
+ * @param {{ locale?: "ko" }} options
825
+ * @returns {LocalizedString}
826
+ */
827
+ const cli_app_bundle_too_large = ((inputs, options = {}) => {
828
+ experimentalStaticLocale ?? options.locale ?? getLocale();
829
+ return ko_cli_app_bundle_too_large(inputs);
830
+ });
831
+
832
+ //#endregion
833
+ //#region ../i18n/src/paraglide/messages/cli_app_default_export_missing.js
834
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
835
+ /** @typedef {{}} Cli_App_Default_Export_MissingInputs */
836
+ const ko_cli_app_default_export_missing = () => {
837
+ return `src/server.ts가 defineApp(...)을 기본 내보내기(export default)해야 합니다`;
838
+ };
839
+ /**
840
+
841
+ * @param {Cli_App_Default_Export_MissingInputs} inputs
842
+ * @param {{ locale?: "ko" }} options
843
+ * @returns {LocalizedString}
844
+ */
845
+ const cli_app_default_export_missing = ((inputs = {}, options = {}) => {
846
+ experimentalStaticLocale ?? options.locale ?? getLocale();
847
+ return ko_cli_app_default_export_missing(inputs);
848
+ });
849
+
850
+ //#endregion
851
+ //#region ../i18n/src/paraglide/messages/cli_app_deploy_done.js
852
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
853
+ /** @typedef {{ number: NonNullable<unknown> }} Cli_App_Deploy_DoneInputs */
854
+ const ko_cli_app_deploy_done = (i) => {
855
+ return `버전 ${i?.number}을 만들었습니다. 적용하려면: npx sayren app release ${i?.number}`;
856
+ };
857
+ /**
858
+
859
+ * @param {Cli_App_Deploy_DoneInputs} inputs
860
+ * @param {{ locale?: "ko" }} options
861
+ * @returns {LocalizedString}
862
+ */
863
+ const cli_app_deploy_done = ((inputs, options = {}) => {
864
+ experimentalStaticLocale ?? options.locale ?? getLocale();
865
+ return ko_cli_app_deploy_done(inputs);
866
+ });
867
+
868
+ //#endregion
869
+ //#region ../i18n/src/paraglide/messages/cli_app_dev_error.js
870
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
871
+ /** @typedef {{ message: NonNullable<unknown> }} Cli_App_Dev_ErrorInputs */
872
+ const ko_cli_app_dev_error = (i) => {
873
+ return `로컬 실행 오류: ${i?.message}`;
874
+ };
875
+ /**
876
+
877
+ * @param {Cli_App_Dev_ErrorInputs} inputs
878
+ * @param {{ locale?: "ko" }} options
879
+ * @returns {LocalizedString}
880
+ */
881
+ const cli_app_dev_error = ((inputs, options = {}) => {
882
+ experimentalStaticLocale ?? options.locale ?? getLocale();
883
+ return ko_cli_app_dev_error(inputs);
884
+ });
885
+
886
+ //#endregion
887
+ //#region ../i18n/src/paraglide/messages/cli_app_dev_listening.js
888
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
889
+ /** @typedef {{ port: NonNullable<unknown>, store: NonNullable<unknown>, events: NonNullable<unknown> }} Cli_App_Dev_ListeningInputs */
890
+ const ko_cli_app_dev_listening = (i) => {
891
+ return `앱 로컬 실행 중 http://localhost:${i?.port} — 상점 ${i?.store} · 이벤트 ${i?.events}
892
+ 이벤트: npx sayren app trigger ORDER.PAID --order <orderId>
893
+ HTTP: http://localhost:${i?.port}/_http/<경로>
894
+ 설정·비밀 값: .env의 SAYREN_APP_SETTING_*·SAYREN_APP_SECRET_*
895
+ 종료는 Ctrl-C`;
896
+ };
897
+ /**
898
+
899
+ * @param {Cli_App_Dev_ListeningInputs} inputs
900
+ * @param {{ locale?: "ko" }} options
901
+ * @returns {LocalizedString}
902
+ */
903
+ const cli_app_dev_listening = ((inputs, options = {}) => {
904
+ experimentalStaticLocale ?? options.locale ?? getLocale();
905
+ return ko_cli_app_dev_listening(inputs);
906
+ });
907
+
908
+ //#endregion
909
+ //#region ../i18n/src/paraglide/messages/cli_app_dev_store_created.js
910
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
911
+ /** @typedef {{ name: NonNullable<unknown>, storeId: NonNullable<unknown> }} Cli_App_Dev_Store_CreatedInputs */
912
+ const ko_cli_app_dev_store_created = (i) => {
913
+ return `개발 상점을 만들었습니다: ${i?.name} (${i?.storeId})`;
914
+ };
915
+ /**
916
+
917
+ * @param {Cli_App_Dev_Store_CreatedInputs} inputs
918
+ * @param {{ locale?: "ko" }} options
919
+ * @returns {LocalizedString}
920
+ */
921
+ const cli_app_dev_store_created = ((inputs, options = {}) => {
922
+ experimentalStaticLocale ?? options.locale ?? getLocale();
923
+ return ko_cli_app_dev_store_created(inputs);
924
+ });
925
+
926
+ //#endregion
927
+ //#region ../i18n/src/paraglide/messages/cli_app_dev_store_empty.js
928
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
929
+ /** @typedef {{}} Cli_App_Dev_Store_EmptyInputs */
930
+ const ko_cli_app_dev_store_empty = () => {
931
+ return `개발 상점이 없습니다. npx sayren app dev-store create <이름>`;
932
+ };
933
+ /**
934
+
935
+ * @param {Cli_App_Dev_Store_EmptyInputs} inputs
936
+ * @param {{ locale?: "ko" }} options
937
+ * @returns {LocalizedString}
938
+ */
939
+ const cli_app_dev_store_empty = ((inputs = {}, options = {}) => {
940
+ experimentalStaticLocale ?? options.locale ?? getLocale();
941
+ return ko_cli_app_dev_store_empty(inputs);
942
+ });
943
+
944
+ //#endregion
945
+ //#region ../i18n/src/paraglide/messages/cli_app_dev_store_name_required.js
946
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
947
+ /** @typedef {{}} Cli_App_Dev_Store_Name_RequiredInputs */
948
+ const ko_cli_app_dev_store_name_required = () => {
949
+ return `개발 상점 이름이 필요합니다(2~50자)`;
950
+ };
951
+ /**
952
+
953
+ * @param {Cli_App_Dev_Store_Name_RequiredInputs} inputs
954
+ * @param {{ locale?: "ko" }} options
955
+ * @returns {LocalizedString}
956
+ */
957
+ const cli_app_dev_store_name_required = ((inputs = {}, options = {}) => {
958
+ experimentalStaticLocale ?? options.locale ?? getLocale();
959
+ return ko_cli_app_dev_store_name_required(inputs);
960
+ });
961
+
962
+ //#endregion
963
+ //#region ../i18n/src/paraglide/messages/cli_app_dir_exists.js
964
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
965
+ /** @typedef {{ dir: NonNullable<unknown> }} Cli_App_Dir_ExistsInputs */
966
+ const ko_cli_app_dir_exists = (i) => {
967
+ return `${i?.dir}에 이미 sayren.app.json이 있습니다`;
968
+ };
969
+ /**
970
+
971
+ * @param {Cli_App_Dir_ExistsInputs} inputs
972
+ * @param {{ locale?: "ko" }} options
973
+ * @returns {LocalizedString}
974
+ */
975
+ const cli_app_dir_exists = ((inputs, options = {}) => {
976
+ experimentalStaticLocale ?? options.locale ?? getLocale();
977
+ return ko_cli_app_dir_exists(inputs);
978
+ });
979
+
980
+ //#endregion
981
+ //#region ../i18n/src/paraglide/messages/cli_app_entry_missing.js
982
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
983
+ /** @typedef {{ entry: NonNullable<unknown> }} Cli_App_Entry_MissingInputs */
984
+ const ko_cli_app_entry_missing = (i) => {
985
+ return `서버 진입 파일(${i?.entry})이 없습니다`;
986
+ };
987
+ /**
988
+
989
+ * @param {Cli_App_Entry_MissingInputs} inputs
990
+ * @param {{ locale?: "ko" }} options
991
+ * @returns {LocalizedString}
992
+ */
993
+ const cli_app_entry_missing = ((inputs, options = {}) => {
994
+ experimentalStaticLocale ?? options.locale ?? getLocale();
995
+ return ko_cli_app_entry_missing(inputs);
996
+ });
997
+
998
+ //#endregion
999
+ //#region ../i18n/src/paraglide/messages/cli_app_handle_invalid.js
1000
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1001
+ /** @typedef {{}} Cli_App_Handle_InvalidInputs */
1002
+ const ko_cli_app_handle_invalid = () => {
1003
+ return `handle은 소문자·숫자·하이픈 3~40자입니다. 예: npx sayren app init order-mail`;
1004
+ };
1005
+ /**
1006
+
1007
+ * @param {Cli_App_Handle_InvalidInputs} inputs
1008
+ * @param {{ locale?: "ko" }} options
1009
+ * @returns {LocalizedString}
1010
+ */
1011
+ const cli_app_handle_invalid = ((inputs = {}, options = {}) => {
1012
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1013
+ return ko_cli_app_handle_invalid(inputs);
1014
+ });
1015
+
1016
+ //#endregion
1017
+ //#region ../i18n/src/paraglide/messages/cli_app_handle_mismatch.js
1018
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1019
+ /** @typedef {{ manifest: NonNullable<unknown>, registered: NonNullable<unknown> }} Cli_App_Handle_MismatchInputs */
1020
+ const ko_cli_app_handle_mismatch = (i) => {
1021
+ return `매니페스트 handle(${i?.manifest})이 등록된 앱(${i?.registered})과 다릅니다`;
1022
+ };
1023
+ /**
1024
+
1025
+ * @param {Cli_App_Handle_MismatchInputs} inputs
1026
+ * @param {{ locale?: "ko" }} options
1027
+ * @returns {LocalizedString}
1028
+ */
1029
+ const cli_app_handle_mismatch = ((inputs, options = {}) => {
1030
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1031
+ return ko_cli_app_handle_mismatch(inputs);
1032
+ });
1033
+
1034
+ //#endregion
1035
+ //#region ../i18n/src/paraglide/messages/cli_app_handler_missing.js
1036
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1037
+ /** @typedef {{ events: NonNullable<unknown> }} Cli_App_Handler_MissingInputs */
1038
+ const ko_cli_app_handler_missing = (i) => {
1039
+ return `매니페스트 events에 있지만 이벤트 함수가 없습니다: ${i?.events} (HANDLER_MISSING)`;
1040
+ };
1041
+ /**
1042
+
1043
+ * @param {Cli_App_Handler_MissingInputs} inputs
1044
+ * @param {{ locale?: "ko" }} options
1045
+ * @returns {LocalizedString}
1046
+ */
1047
+ const cli_app_handler_missing = ((inputs, options = {}) => {
1048
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1049
+ return ko_cli_app_handler_missing(inputs);
1050
+ });
1051
+
1052
+ //#endregion
1053
+ //#region ../i18n/src/paraglide/messages/cli_app_handler_unlisted.js
1054
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1055
+ /** @typedef {{ events: NonNullable<unknown> }} Cli_App_Handler_UnlistedInputs */
1056
+ const ko_cli_app_handler_unlisted = (i) => {
1057
+ return `이벤트 함수는 있지만 매니페스트 events에 없어 호출되지 않습니다: ${i?.events}`;
1058
+ };
1059
+ /**
1060
+
1061
+ * @param {Cli_App_Handler_UnlistedInputs} inputs
1062
+ * @param {{ locale?: "ko" }} options
1063
+ * @returns {LocalizedString}
1064
+ */
1065
+ const cli_app_handler_unlisted = ((inputs, options = {}) => {
1066
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1067
+ return ko_cli_app_handler_unlisted(inputs);
1068
+ });
1069
+
1070
+ //#endregion
1071
+ //#region ../i18n/src/paraglide/messages/cli_app_init_done.js
1072
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1073
+ /** @typedef {{ dir: NonNullable<unknown>, handle: NonNullable<unknown>, appId: NonNullable<unknown> }} Cli_App_Init_DoneInputs */
1074
+ const ko_cli_app_init_done = (i) => {
1075
+ return `앱을 만들었습니다: ${i?.dir} (${i?.handle}, ${i?.appId})
1076
+
1077
+ cd ${i?.dir}
1078
+ npm install
1079
+ npx sayren app dev --store <storeId>
1080
+ npx sayren app deploy -m "첫 버전"`;
1081
+ };
1082
+ /**
1083
+
1084
+ * @param {Cli_App_Init_DoneInputs} inputs
1085
+ * @param {{ locale?: "ko" }} options
1086
+ * @returns {LocalizedString}
1087
+ */
1088
+ const cli_app_init_done = ((inputs, options = {}) => {
1089
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1090
+ return ko_cli_app_init_done(inputs);
1091
+ });
1092
+
1093
+ //#endregion
1094
+ //#region ../i18n/src/paraglide/messages/cli_app_key_invalid.js
1095
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1096
+ /** @typedef {{}} Cli_App_Key_InvalidInputs */
1097
+ const ko_cli_app_key_invalid = () => {
1098
+ return `앱 배포 키 형식이 아닙니다(sak_…). developer.sayren.app에서 발행한 키를 쓰십시오`;
1099
+ };
1100
+ /**
1101
+
1102
+ * @param {Cli_App_Key_InvalidInputs} inputs
1103
+ * @param {{ locale?: "ko" }} options
1104
+ * @returns {LocalizedString}
1105
+ */
1106
+ const cli_app_key_invalid = ((inputs = {}, options = {}) => {
1107
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1108
+ return ko_cli_app_key_invalid(inputs);
1109
+ });
1110
+
1111
+ //#endregion
1112
+ //#region ../i18n/src/paraglide/messages/cli_app_logs_empty.js
1113
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1114
+ /** @typedef {{}} Cli_App_Logs_EmptyInputs */
1115
+ const ko_cli_app_logs_empty = () => {
1116
+ return `실행 기록이 없습니다`;
1117
+ };
1118
+ /**
1119
+
1120
+ * @param {Cli_App_Logs_EmptyInputs} inputs
1121
+ * @param {{ locale?: "ko" }} options
1122
+ * @returns {LocalizedString}
1123
+ */
1124
+ const cli_app_logs_empty = ((inputs = {}, options = {}) => {
1125
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1126
+ return ko_cli_app_logs_empty(inputs);
1127
+ });
1128
+
1129
+ //#endregion
1130
+ //#region ../i18n/src/paraglide/messages/cli_app_manifest_invalid.js
1131
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1132
+ /** @typedef {{ problems: NonNullable<unknown> }} Cli_App_Manifest_InvalidInputs */
1133
+ const ko_cli_app_manifest_invalid = (i) => {
1134
+ return `매니페스트가 올바르지 않습니다:
1135
+ ${i?.problems}`;
1136
+ };
1137
+ /**
1138
+
1139
+ * @param {Cli_App_Manifest_InvalidInputs} inputs
1140
+ * @param {{ locale?: "ko" }} options
1141
+ * @returns {LocalizedString}
1142
+ */
1143
+ const cli_app_manifest_invalid = ((inputs, options = {}) => {
1144
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1145
+ return ko_cli_app_manifest_invalid(inputs);
1146
+ });
1147
+
1148
+ //#endregion
1149
+ //#region ../i18n/src/paraglide/messages/cli_app_manifest_missing.js
1150
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1151
+ /** @typedef {{ file: NonNullable<unknown> }} Cli_App_Manifest_MissingInputs */
1152
+ const ko_cli_app_manifest_missing = (i) => {
1153
+ return `${i?.file}이 없습니다. 앱 폴더에서 실행하거나 --dir을 주십시오`;
1154
+ };
1155
+ /**
1156
+
1157
+ * @param {Cli_App_Manifest_MissingInputs} inputs
1158
+ * @param {{ locale?: "ko" }} options
1159
+ * @returns {LocalizedString}
1160
+ */
1161
+ const cli_app_manifest_missing = ((inputs, options = {}) => {
1162
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1163
+ return ko_cli_app_manifest_missing(inputs);
1164
+ });
1165
+
1166
+ //#endregion
1167
+ //#region ../i18n/src/paraglide/messages/cli_app_not_registered.js
1168
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1169
+ /** @typedef {{ file: NonNullable<unknown> }} Cli_App_Not_RegisteredInputs */
1170
+ const ko_cli_app_not_registered = (i) => {
1171
+ return `등록된 앱이 아닙니다(${i?.file}은 있지만 .sayren/app.json이 없습니다). npx sayren app init <handle>로 만든 폴더에서 실행하십시오`;
1172
+ };
1173
+ /**
1174
+
1175
+ * @param {Cli_App_Not_RegisteredInputs} inputs
1176
+ * @param {{ locale?: "ko" }} options
1177
+ * @returns {LocalizedString}
1178
+ */
1179
+ const cli_app_not_registered = ((inputs, options = {}) => {
1180
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1181
+ return ko_cli_app_not_registered(inputs);
1182
+ });
1183
+
1184
+ //#endregion
1185
+ //#region ../i18n/src/paraglide/messages/cli_app_port_invalid.js
1186
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1187
+ /** @typedef {{ value: NonNullable<unknown> }} Cli_App_Port_InvalidInputs */
1188
+ const ko_cli_app_port_invalid = (i) => {
1189
+ return `--port가 올바르지 않습니다: ${i?.value}`;
1190
+ };
1191
+ /**
1192
+
1193
+ * @param {Cli_App_Port_InvalidInputs} inputs
1194
+ * @param {{ locale?: "ko" }} options
1195
+ * @returns {LocalizedString}
1196
+ */
1197
+ const cli_app_port_invalid = ((inputs, options = {}) => {
1198
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1199
+ return ko_cli_app_port_invalid(inputs);
1200
+ });
1201
+
1202
+ //#endregion
1203
+ //#region ../i18n/src/paraglide/messages/cli_app_release_done.js
1204
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1205
+ /** @typedef {{ number: NonNullable<unknown>, applied: NonNullable<unknown>, pending: NonNullable<unknown> }} Cli_App_Release_DoneInputs */
1206
+ const ko_cli_app_release_done = (i) => {
1207
+ return `버전 ${i?.number}이 현재 버전입니다 — 곧바로 적용 ${i?.applied}, 재동의 대기 ${i?.pending}`;
1208
+ };
1209
+ /**
1210
+
1211
+ * @param {Cli_App_Release_DoneInputs} inputs
1212
+ * @param {{ locale?: "ko" }} options
1213
+ * @returns {LocalizedString}
1214
+ */
1215
+ const cli_app_release_done = ((inputs, options = {}) => {
1216
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1217
+ return ko_cli_app_release_done(inputs);
1218
+ });
1219
+
1220
+ //#endregion
1221
+ //#region ../i18n/src/paraglide/messages/cli_app_secret_deleted.js
1222
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1223
+ /** @typedef {{ name: NonNullable<unknown> }} Cli_App_Secret_DeletedInputs */
1224
+ const ko_cli_app_secret_deleted = (i) => {
1225
+ return `비밀 값 ${i?.name}을 지웠습니다`;
1226
+ };
1227
+ /**
1228
+
1229
+ * @param {Cli_App_Secret_DeletedInputs} inputs
1230
+ * @param {{ locale?: "ko" }} options
1231
+ * @returns {LocalizedString}
1232
+ */
1233
+ const cli_app_secret_deleted = ((inputs, options = {}) => {
1234
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1235
+ return ko_cli_app_secret_deleted(inputs);
1236
+ });
1237
+
1238
+ //#endregion
1239
+ //#region ../i18n/src/paraglide/messages/cli_app_secret_empty.js
1240
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1241
+ /** @typedef {{}} Cli_App_Secret_EmptyInputs */
1242
+ const ko_cli_app_secret_empty = () => {
1243
+ return `비밀 값이 없습니다`;
1244
+ };
1245
+ /**
1246
+
1247
+ * @param {Cli_App_Secret_EmptyInputs} inputs
1248
+ * @param {{ locale?: "ko" }} options
1249
+ * @returns {LocalizedString}
1250
+ */
1251
+ const cli_app_secret_empty = ((inputs = {}, options = {}) => {
1252
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1253
+ return ko_cli_app_secret_empty(inputs);
1254
+ });
1255
+
1256
+ //#endregion
1257
+ //#region ../i18n/src/paraglide/messages/cli_app_secret_in_source.js
1258
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1259
+ /** @typedef {{}} Cli_App_Secret_In_SourceInputs */
1260
+ const ko_cli_app_secret_in_source = () => {
1261
+ return `번들에 액세스 토큰·PG 키·개인 키·앱 배포 키 모양의 값이 들어 있습니다. 비밀 값은 npx sayren app secret put <이름>으로 넣으십시오 (SECRET_IN_SOURCE)`;
1262
+ };
1263
+ /**
1264
+
1265
+ * @param {Cli_App_Secret_In_SourceInputs} inputs
1266
+ * @param {{ locale?: "ko" }} options
1267
+ * @returns {LocalizedString}
1268
+ */
1269
+ const cli_app_secret_in_source = ((inputs = {}, options = {}) => {
1270
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1271
+ return ko_cli_app_secret_in_source(inputs);
1272
+ });
1273
+
1274
+ //#endregion
1275
+ //#region ../i18n/src/paraglide/messages/cli_app_secret_name_required.js
1276
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1277
+ /** @typedef {{}} Cli_App_Secret_Name_RequiredInputs */
1278
+ const ko_cli_app_secret_name_required = () => {
1279
+ return `비밀 값 이름이 필요합니다. 예: npx sayren app secret put RESEND_API_KEY`;
1280
+ };
1281
+ /**
1282
+
1283
+ * @param {Cli_App_Secret_Name_RequiredInputs} inputs
1284
+ * @param {{ locale?: "ko" }} options
1285
+ * @returns {LocalizedString}
1286
+ */
1287
+ const cli_app_secret_name_required = ((inputs = {}, options = {}) => {
1288
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1289
+ return ko_cli_app_secret_name_required(inputs);
1290
+ });
1291
+
1292
+ //#endregion
1293
+ //#region ../i18n/src/paraglide/messages/cli_app_secret_saved.js
1294
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1295
+ /** @typedef {{ name: NonNullable<unknown> }} Cli_App_Secret_SavedInputs */
1296
+ const ko_cli_app_secret_saved = (i) => {
1297
+ return `비밀 값 ${i?.name}을 저장했습니다`;
1298
+ };
1299
+ /**
1300
+
1301
+ * @param {Cli_App_Secret_SavedInputs} inputs
1302
+ * @param {{ locale?: "ko" }} options
1303
+ * @returns {LocalizedString}
1304
+ */
1305
+ const cli_app_secret_saved = ((inputs, options = {}) => {
1306
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1307
+ return ko_cli_app_secret_saved(inputs);
1308
+ });
1309
+
1310
+ //#endregion
1311
+ //#region ../i18n/src/paraglide/messages/cli_app_secret_value_required.js
1312
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1313
+ /** @typedef {{ name: NonNullable<unknown> }} Cli_App_Secret_Value_RequiredInputs */
1314
+ const ko_cli_app_secret_value_required = (i) => {
1315
+ return `값을 표준 입력으로 주십시오. 예: printf '%s' "$VALUE" | npx sayren app secret put ${i?.name}`;
1316
+ };
1317
+ /**
1318
+
1319
+ * @param {Cli_App_Secret_Value_RequiredInputs} inputs
1320
+ * @param {{ locale?: "ko" }} options
1321
+ * @returns {LocalizedString}
1322
+ */
1323
+ const cli_app_secret_value_required = ((inputs, options = {}) => {
1324
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1325
+ return ko_cli_app_secret_value_required(inputs);
1326
+ });
1327
+
1328
+ //#endregion
1329
+ //#region ../i18n/src/paraglide/messages/cli_app_since_invalid.js
1330
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1331
+ /** @typedef {{ value: NonNullable<unknown> }} Cli_App_Since_InvalidInputs */
1332
+ const ko_cli_app_since_invalid = (i) => {
1333
+ return `--since 형식이 아닙니다: ${i?.value} (예: 30m, 1h, 2d)`;
1334
+ };
1335
+ /**
1336
+
1337
+ * @param {Cli_App_Since_InvalidInputs} inputs
1338
+ * @param {{ locale?: "ko" }} options
1339
+ * @returns {LocalizedString}
1340
+ */
1341
+ const cli_app_since_invalid = ((inputs, options = {}) => {
1342
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1343
+ return ko_cli_app_since_invalid(inputs);
1344
+ });
1345
+
1346
+ //#endregion
1347
+ //#region ../i18n/src/paraglide/messages/cli_app_state_invalid.js
1348
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1349
+ /** @typedef {{ file: NonNullable<unknown> }} Cli_App_State_InvalidInputs */
1350
+ const ko_cli_app_state_invalid = (i) => {
1351
+ return `${i?.file}을 읽을 수 없습니다. 파일을 지우고 npx sayren app init을 다시 실행하십시오`;
1352
+ };
1353
+ /**
1354
+
1355
+ * @param {Cli_App_State_InvalidInputs} inputs
1356
+ * @param {{ locale?: "ko" }} options
1357
+ * @returns {LocalizedString}
1358
+ */
1359
+ const cli_app_state_invalid = ((inputs, options = {}) => {
1360
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1361
+ return ko_cli_app_state_invalid(inputs);
1362
+ });
1363
+
1364
+ //#endregion
1365
+ //#region ../i18n/src/paraglide/messages/cli_app_status.js
1366
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1367
+ /** @typedef {{ name: NonNullable<unknown>, handle: NonNullable<unknown>, appId: NonNullable<unknown>, status: NonNullable<unknown>, current: NonNullable<unknown>, active: NonNullable<unknown>, pending: NonNullable<unknown> }} Cli_App_StatusInputs */
1368
+ const ko_cli_app_status = (i) => {
1369
+ return `${i?.name} (${i?.handle}, ${i?.appId})
1370
+ 상태 ${i?.status} · 현재 버전 ${i?.current} · 설치 ${i?.active} · 재동의 대기 ${i?.pending}
1371
+ 최근 버전:`;
1372
+ };
1373
+ /**
1374
+
1375
+ * @param {Cli_App_StatusInputs} inputs
1376
+ * @param {{ locale?: "ko" }} options
1377
+ * @returns {LocalizedString}
598
1378
  */
1379
+ const cli_app_status = ((inputs, options = {}) => {
1380
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1381
+ return ko_cli_app_status(inputs);
1382
+ });
1383
+
1384
+ //#endregion
1385
+ //#region ../i18n/src/paraglide/messages/cli_app_subcommand_unknown.js
1386
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1387
+ /** @typedef {{ command: NonNullable<unknown> }} Cli_App_Subcommand_UnknownInputs */
1388
+ const ko_cli_app_subcommand_unknown = (i) => {
1389
+ return `알 수 없는 앱 명령: ${i?.command}
1390
+
1391
+ 사용법: npx sayren app <init|dev|trigger|verify|deploy|release|status|logs|secret|dev-store>`;
1392
+ };
599
1393
  /**
600
- * Type-only metadata attached to compiled message functions.
601
- *
602
- * @template Inputs
603
- * @template Options
604
- * @template {MessageMarkupSchema} [Markup = MessageMarkupSchema]
605
- * @typedef {{
606
- * readonly __paraglide?: {
607
- * inputs: Inputs;
608
- * options: Options;
609
- * markup: Markup;
610
- * };
611
- * }} MessageMetadata
1394
+
1395
+ * @param {Cli_App_Subcommand_UnknownInputs} inputs
1396
+ * @param {{ locale?: "ko" }} options
1397
+ * @returns {LocalizedString}
612
1398
  */
1399
+ const cli_app_subcommand_unknown = ((inputs, options = {}) => {
1400
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1401
+ return ko_cli_app_subcommand_unknown(inputs);
1402
+ });
1403
+
1404
+ //#endregion
1405
+ //#region ../i18n/src/paraglide/messages/cli_app_trigger_data_invalid.js
1406
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1407
+ /** @typedef {{}} Cli_App_Trigger_Data_InvalidInputs */
1408
+ const ko_cli_app_trigger_data_invalid = () => {
1409
+ return `--data가 JSON이 아닙니다`;
1410
+ };
613
1411
  /**
614
- * A compiled, framework-neutral message part.
615
- *
616
- * @typedef {{
617
- * type: "text";
618
- * value: string;
619
- * } | {
620
- * type: "markup-start";
621
- * name: string;
622
- * options: MessageMarkupOptions;
623
- * attributes: MessageMarkupAttributes;
624
- * } | {
625
- * type: "markup-end";
626
- * name: string;
627
- * options: MessageMarkupOptions;
628
- * attributes: MessageMarkupAttributes;
629
- * } | {
630
- * type: "markup-standalone";
631
- * name: string;
632
- * options: MessageMarkupOptions;
633
- * attributes: MessageMarkupAttributes;
634
- * }} MessagePart
1412
+
1413
+ * @param {Cli_App_Trigger_Data_InvalidInputs} inputs
1414
+ * @param {{ locale?: "ko" }} options
1415
+ * @returns {LocalizedString}
635
1416
  */
1417
+ const cli_app_trigger_data_invalid = ((inputs = {}, options = {}) => {
1418
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1419
+ return ko_cli_app_trigger_data_invalid(inputs);
1420
+ });
1421
+
1422
+ //#endregion
1423
+ //#region ../i18n/src/paraglide/messages/cli_app_trigger_failed.js
1424
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1425
+ /** @typedef {{ status: NonNullable<unknown> }} Cli_App_Trigger_FailedInputs */
1426
+ const ko_cli_app_trigger_failed = (i) => {
1427
+ return `로컬 실행에 보내지 못했습니다(HTTP ${i?.status}). npx sayren app dev가 떠 있는지 확인하십시오`;
1428
+ };
636
1429
  /**
637
- * A message function is a message for a specific locale.
638
- *
639
- * @example
640
- * m.hello({ name: 'world' })
641
- *
642
- * @typedef {(inputs?: Record<string, never>) => LocalizedString} MessageFunction
1430
+
1431
+ * @param {Cli_App_Trigger_FailedInputs} inputs
1432
+ * @param {{ locale?: "ko" }} options
1433
+ * @returns {LocalizedString}
643
1434
  */
1435
+ const cli_app_trigger_failed = ((inputs, options = {}) => {
1436
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1437
+ return ko_cli_app_trigger_failed(inputs);
1438
+ });
1439
+
1440
+ //#endregion
1441
+ //#region ../i18n/src/paraglide/messages/cli_app_trigger_type_required.js
1442
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1443
+ /** @typedef {{}} Cli_App_Trigger_Type_RequiredInputs */
1444
+ const ko_cli_app_trigger_type_required = () => {
1445
+ return `이벤트 종류가 필요합니다. 예: npx sayren app trigger ORDER.PAID --order ord_…`;
1446
+ };
644
1447
  /**
645
- * A message bundle function that selects the message to be returned.
646
- *
647
- * Uses `getLocale()` under the hood to determine the locale with an option.
648
- *
649
- * @template {string} T
650
- *
651
- * @example
652
- * * m.hello({ name: 'world' }, { locale: "en" })
653
- *
654
- * @typedef {(params: Record<string, never>, options: { locale: T }) => LocalizedString} MessageBundleFunction
1448
+
1449
+ * @param {Cli_App_Trigger_Type_RequiredInputs} inputs
1450
+ * @param {{ locale?: "ko" }} options
1451
+ * @returns {LocalizedString}
655
1452
  */
1453
+ const cli_app_trigger_type_required = ((inputs = {}, options = {}) => {
1454
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1455
+ return ko_cli_app_trigger_type_required(inputs);
1456
+ });
656
1457
 
657
1458
  //#endregion
658
- //#region ../i18n/src/locale.ts
1459
+ //#region ../i18n/src/paraglide/messages/cli_app_ui_index_missing.js
1460
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1461
+ /** @typedef {{ dir: NonNullable<unknown> }} Cli_App_Ui_Index_MissingInputs */
1462
+ const ko_cli_app_ui_index_missing = (i) => {
1463
+ return `매니페스트에 ui.adminPage가 있지만 ${i?.dir}/index.html이 없습니다`;
1464
+ };
659
1465
  /**
660
- * 로케일 판정(이슈 #73) — 1순위 쿼리 `?lang=`, 2순위 `Accept-Language`(브라우저는 `navigator.languages`), 마지막 `ko`.
661
- * 순서는 컴파일 옵션(`scripts/compile.mjs`의 strategy)이 정하고, 여기서는 Paraglide에 없는 쿼리 판정만 등록한다.
662
- * api는 같은 순서를 `@nestjs/i18n` 리졸버(Query → AcceptLanguage)로 맞춘다.
1466
+
1467
+ * @param {Cli_App_Ui_Index_MissingInputs} inputs
1468
+ * @param {{ locale?: "ko" }} options
1469
+ * @returns {LocalizedString}
663
1470
  */
664
- const LOCALE_QUERY_PARAM = "lang";
665
- /** `?lang=ko-KR` → `ko`. 지원하지 않는 값·없으면 undefined(다음 판정으로 넘어간다) */
666
- function localeFromSearch(search) {
667
- const value = new URLSearchParams(search).get(LOCALE_QUERY_PARAM);
668
- if (!value) return void 0;
669
- return toLocale(value) ?? toLocale(value.split("-")[0]);
670
- }
671
- defineCustomServerStrategy("custom-query", { getLocale: (request) => request ? localeFromSearch(new URL(request.url).search) : void 0 });
672
- /** 브라우저의 현재 주소 쿼리 — DOM 타입에 기대지 않는다(DOM lib 없는 소비자, 예: mcp-gateway도 이 소스를 타입 검사한다) */
673
- function currentSearch() {
674
- return globalThis.location?.search;
675
- }
676
- defineCustomClientStrategy("custom-query", {
677
- getLocale: () => {
678
- const search = currentSearch();
679
- return search === void 0 ? void 0 : localeFromSearch(search);
680
- },
681
- setLocale: () => {}
1471
+ const cli_app_ui_index_missing = ((inputs, options = {}) => {
1472
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1473
+ return ko_cli_app_ui_index_missing(inputs);
682
1474
  });
683
1475
 
684
1476
  //#endregion
685
- //#region ../i18n/src/lazy-messages.ts
1477
+ //#region ../i18n/src/paraglide/messages/cli_app_ui_too_large.js
1478
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1479
+ /** @typedef {{ limit: NonNullable<unknown> }} Cli_App_Ui_Too_LargeInputs */
1480
+ const ko_cli_app_ui_too_large = (i) => {
1481
+ return `앱 화면 파일이 상한(${i?.limit})을 넘습니다 (ARTIFACT_TOO_LARGE)`;
1482
+ };
686
1483
  /**
687
- * 라벨 표 — 값을 읽을 때마다 메시지 함수를 불러 그 순간의 로케일 문구를 돌려준다(이슈 #73).
688
- * 모듈을 읽을 때 문구를 만들면 서버 렌더링에서 요청 로케일이 아니라 기동 시점 로케일로 굳는다.
689
- * 타입은 `Record<K, string>`이라 호출부(`LABEL[key]`, `Object.entries(LABEL)`)를 바꾸지 않는다.
690
- *
691
- * ```ts
692
- * export const ROLE_LABEL = lazyMessages<StoreRole>({ OWNER: m.store_shell_role_owner, ... });
693
- * ```
1484
+
1485
+ * @param {Cli_App_Ui_Too_LargeInputs} inputs
1486
+ * @param {{ locale?: "ko" }} options
1487
+ * @returns {LocalizedString}
694
1488
  */
695
- function lazyMessages(table) {
696
- const labels = {};
697
- for (const key of Object.keys(table)) Object.defineProperty(labels, key, {
698
- enumerable: true,
699
- get: () => table[key]()
700
- });
701
- return Object.freeze(labels);
702
- }
1489
+ const cli_app_ui_too_large = ((inputs, options = {}) => {
1490
+ experimentalStaticLocale ?? options.locale ?? getLocale();
1491
+ return ko_cli_app_ui_too_large(inputs);
1492
+ });
703
1493
 
704
1494
  //#endregion
705
- //#region ../i18n/src/paraglide/messages/cli_access_token_expired.js
1495
+ //#region ../i18n/src/paraglide/messages/cli_app_uploading.js
706
1496
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
707
- /** @typedef {{}} Cli_Access_Token_ExpiredInputs */
708
- const ko_cli_access_token_expired = () => {
709
- return `액세스 토큰이 만료됐습니다. 새 토큰을 만들거나 npx sayren login으로 다시 로그인하십시오`;
1497
+ /** @typedef {{ size: NonNullable<unknown>, ui: NonNullable<unknown> }} Cli_App_UploadingInputs */
1498
+ const ko_cli_app_uploading = (i) => {
1499
+ return `올리는 중 — 서버 번들 ${i?.size}(gzip), 화면 파일 ${i?.ui}개`;
710
1500
  };
711
1501
  /**
712
1502
 
713
- * @param {Cli_Access_Token_ExpiredInputs} inputs
1503
+ * @param {Cli_App_UploadingInputs} inputs
714
1504
  * @param {{ locale?: "ko" }} options
715
1505
  * @returns {LocalizedString}
716
1506
  */
717
- const cli_access_token_expired = ((inputs = {}, options = {}) => {
1507
+ const cli_app_uploading = ((inputs, options = {}) => {
718
1508
  experimentalStaticLocale ?? options.locale ?? getLocale();
719
- return ko_cli_access_token_expired(inputs);
1509
+ return ko_cli_app_uploading(inputs);
720
1510
  });
721
1511
 
722
1512
  //#endregion
723
- //#region ../i18n/src/paraglide/messages/cli_account_mcp_written.js
1513
+ //#region ../i18n/src/paraglide/messages/cli_app_verify_ok.js
724
1514
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
725
- /** @typedef {{ file: NonNullable<unknown>, mcpFile: NonNullable<unknown> }} Cli_Account_Mcp_WrittenInputs */
726
- const ko_cli_account_mcp_written = (i) => {
727
- return `${i?.file} 작성 — 원격 MCP 주소와 상점. ${i?.mcpFile}에는 토큰을 쓰지 않았습니다 — MCP 클라이언트가 연결할 때 Sayren 계정으로 로그인합니다`;
1515
+ /** @typedef {{ events: NonNullable<unknown>, http: NonNullable<unknown>, size: NonNullable<unknown>, ui: NonNullable<unknown> }} Cli_App_Verify_OkInputs */
1516
+ const ko_cli_app_verify_ok = (i) => {
1517
+ return `검사 통과 — 이벤트 ${i?.events} · HTTP ${i?.http} · 서버 번들 ${i?.size}(gzip) · 화면 파일 ${i?.ui}개`;
728
1518
  };
729
1519
  /**
730
1520
 
731
- * @param {Cli_Account_Mcp_WrittenInputs} inputs
1521
+ * @param {Cli_App_Verify_OkInputs} inputs
732
1522
  * @param {{ locale?: "ko" }} options
733
1523
  * @returns {LocalizedString}
734
1524
  */
735
- const cli_account_mcp_written = ((inputs, options = {}) => {
1525
+ const cli_app_verify_ok = ((inputs, options = {}) => {
736
1526
  experimentalStaticLocale ?? options.locale ?? getLocale();
737
- return ko_cli_account_mcp_written(inputs);
1527
+ return ko_cli_app_verify_ok(inputs);
738
1528
  });
739
1529
 
740
1530
  //#endregion
741
- //#region ../i18n/src/paraglide/messages/cli_accounts_origin_unknown.js
1531
+ //#region ../i18n/src/paraglide/messages/cli_app_version_number_required.js
742
1532
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
743
- /** @typedef {{ origin: NonNullable<unknown> }} Cli_Accounts_Origin_UnknownInputs */
744
- const ko_cli_accounts_origin_unknown = (i) => {
745
- return `이 api 주소(${i?.origin})의 계정 서버를 모릅니다. --accounts-origin 또는 SAYREN_ACCOUNTS_ORIGIN으로 주십시오`;
1533
+ /** @typedef {{}} Cli_App_Version_Number_RequiredInputs */
1534
+ const ko_cli_app_version_number_required = () => {
1535
+ return `버전 번호가 필요합니다. 예: npx sayren app release 1`;
746
1536
  };
747
1537
  /**
748
1538
 
749
- * @param {Cli_Accounts_Origin_UnknownInputs} inputs
1539
+ * @param {Cli_App_Version_Number_RequiredInputs} inputs
750
1540
  * @param {{ locale?: "ko" }} options
751
1541
  * @returns {LocalizedString}
752
1542
  */
753
- const cli_accounts_origin_unknown = ((inputs, options = {}) => {
1543
+ const cli_app_version_number_required = ((inputs = {}, options = {}) => {
754
1544
  experimentalStaticLocale ?? options.locale ?? getLocale();
755
- return ko_cli_accounts_origin_unknown(inputs);
1545
+ return ko_cli_app_version_number_required(inputs);
756
1546
  });
757
1547
 
758
1548
  //#endregion
759
- //#region ../i18n/src/paraglide/messages/cli_accounts_unreachable.js
1549
+ //#region ../i18n/src/paraglide/messages/cli_app_warning.js
760
1550
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
761
- /** @typedef {{}} Cli_Accounts_UnreachableInputs */
762
- const ko_cli_accounts_unreachable = () => {
763
- return `계정 서버에 연결하지 못했습니다. 네트워크를 확인하십시오`;
1551
+ /** @typedef {{ message: NonNullable<unknown> }} Cli_App_WarningInputs */
1552
+ const ko_cli_app_warning = (i) => {
1553
+ return `경고: ${i?.message}`;
764
1554
  };
765
1555
  /**
766
1556
 
767
- * @param {Cli_Accounts_UnreachableInputs} inputs
1557
+ * @param {Cli_App_WarningInputs} inputs
768
1558
  * @param {{ locale?: "ko" }} options
769
1559
  * @returns {LocalizedString}
770
1560
  */
771
- const cli_accounts_unreachable = ((inputs = {}, options = {}) => {
1561
+ const cli_app_warning = ((inputs, options = {}) => {
772
1562
  experimentalStaticLocale ?? options.locale ?? getLocale();
773
- return ko_cli_accounts_unreachable(inputs);
1563
+ return ko_cli_app_warning(inputs);
774
1564
  });
775
1565
 
776
1566
  //#endregion
@@ -4332,6 +5122,7 @@ const ko_cli_usage = (i) => {
4332
5122
  npx sayren deploy [--no-publish] [--skip-build] [--message <메모>] [--force [--yes]]
4333
5123
  npx sayren status
4334
5124
  npx sayren verify
5125
+ npx sayren app <명령> # 서드파티 앱(init·dev·trigger·verify·deploy·release·status·logs·secret·dev-store)
4335
5126
 
4336
5127
  명령:
4337
5128
  login Sayren 계정으로 로그인합니다. 로그인 토큰은 사용자 설정 폴더(~/.config/sayren/credentials.json)에 둡니다
@@ -4342,6 +5133,7 @@ const ko_cli_usage = (i) => {
4342
5133
  deploy 로컬 검사 → 로컬 빌드 → 작업 트리 올리기 → 빌드 → 발행. 원격이 pull 이후 바뀌었으면 덮어쓰지 않습니다
4343
5134
  status 사이트·발행 버전·최근 빌드·편집 환경
4344
5135
  verify 로컬 규칙·경로·크기 검사(서버 빌드와 같은 규칙)
5136
+ app 서드파티 앱 — init <handle>로 만들고 dev로 로컬 실행, deploy로 버전을 올리고 release로 적용합니다. 자세히: https://docs.sayren.app/guides/apps-getting-started
4345
5137
 
4346
5138
  토큰은 Sayren 계정 토큰만 받고 --token → SAYREN_TOKEN 환경 변수 → 로그인(credentials.json) 순서로 읽습니다.
4347
5139
  계정 토큰은 계정에 발급됩니다 — 상점은 --store → SAYREN_STORE_ID → 폴더 설정 순서로 고릅니다(하나면 자동).
@@ -4364,6 +5156,14 @@ const ko_cli_usage = (i) => {
4364
5156
  --api-origin <주소> api 주소(기본 https://api.sayren.app, SAYREN_API_ORIGIN). https만, localhost만 http
4365
5157
  --accounts-origin <주소> 계정 서버 주소(기본: api 주소에 맞춘 값, SAYREN_ACCOUNTS_ORIGIN)
4366
5158
  --token <토큰> 토큰을 직접 줍니다. 셸 기록·프로세스 목록에 남으니 login·SAYREN_TOKEN을 권합니다
5159
+ --key <키> app: 앱 배포 키(developer.sayren.app에서 발행, SAYREN_APP_KEY). CI에서 deploy·release·status·logs·secret에 씁니다
5160
+ -m, --message <메모> app deploy: 버전 메모
5161
+ --name <이름> app init: 앱 이름(기본: handle)
5162
+ --port <포트> app dev·trigger: 로컬 포트(기본 4020)
5163
+ --since <기간> app logs: 최근 기간만(30m·1h·2d)
5164
+ --tail app logs: 새 기록을 계속 봅니다
5165
+ --data <json> app trigger: 이벤트 데이터
5166
+ --order <orderId> app trigger: 주문 id를 데이터에 넣습니다
4367
5167
  --lang <로케일> 메시지 언어(기본: SAYREN_LANG → 시스템 로케일 → ko)
4368
5168
 
4369
5169
  종료 코드: 0 성공 · 1 실패 · 3 승인 대기(에이전트 토큰)`;
@@ -5437,7 +6237,7 @@ const mcp_rule_checkout_availability_title = ((inputs = {}, options = {}) => {
5437
6237
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5438
6238
  /** @typedef {{}} Mcp_Rule_Checkout_Availability_WhyInputs */
5439
6239
  const ko_mcp_rule_checkout_availability_why = () => {
5440
- return `결제 수단이 없거나 공개한 쇼핑몰의 실결제가 아직 열리지 않은 상점은 결제 요청이 409 \`PAYMENT_NOT_CONFIGURED\`다. 버튼을 그대로 두면 구매자가 담고 주문서까지 가서야 결제가 안 된다는 것을 안다`;
6240
+ return `결제수단이 없거나 공개한 쇼핑몰의 실결제가 아직 열리지 않은 상점은 결제 요청이 409 \`PAYMENT_NOT_CONFIGURED\`다. 버튼을 그대로 두면 구매자가 담고 주문서까지 가서야 결제가 안 된다는 것을 안다`;
5441
6241
  };
5442
6242
  /**
5443
6243
 
@@ -6314,6 +7114,82 @@ const mcp_rule_payment_return_route_why = ((inputs = {}, options = {}) => {
6314
7114
  return ko_mcp_rule_payment_return_route_why(inputs);
6315
7115
  });
6316
7116
 
7117
+ //#endregion
7118
+ //#region ../i18n/src/paraglide/messages/mcp_rule_product_link_example.js
7119
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7120
+ /** @typedef {{}} Mcp_Rule_Product_Link_ExampleInputs */
7121
+ const ko_mcp_rule_product_link_example = () => {
7122
+ return `import { productPathParam } from "../lib/product-path";
7123
+
7124
+ <Link to="/products/$productId" params={{ productId: productPathParam(product) }}>
7125
+ {product.name}
7126
+ </Link>`;
7127
+ };
7128
+ /**
7129
+
7130
+ * @param {Mcp_Rule_Product_Link_ExampleInputs} inputs
7131
+ * @param {{ locale?: "ko" }} options
7132
+ * @returns {LocalizedString}
7133
+ */
7134
+ const mcp_rule_product_link_example = ((inputs = {}, options = {}) => {
7135
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7136
+ return ko_mcp_rule_product_link_example(inputs);
7137
+ });
7138
+
7139
+ //#endregion
7140
+ //#region ../i18n/src/paraglide/messages/mcp_rule_product_link_fix.js
7141
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7142
+ /** @typedef {{}} Mcp_Rule_Product_Link_FixInputs */
7143
+ const ko_mcp_rule_product_link_fix = () => {
7144
+ return `링크의 경로 값을 \`productPathParam(product)\`(\`src/lib/product-path.ts\`)로 만든다. 상품번호가 있으면 번호, 없는 옛 응답이면 상품 id다. 장바구니 담기·주문서 같은 서버 요청에는 계속 \`productId\`를 보낸다`;
7145
+ };
7146
+ /**
7147
+
7148
+ * @param {Mcp_Rule_Product_Link_FixInputs} inputs
7149
+ * @param {{ locale?: "ko" }} options
7150
+ * @returns {LocalizedString}
7151
+ */
7152
+ const mcp_rule_product_link_fix = ((inputs = {}, options = {}) => {
7153
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7154
+ return ko_mcp_rule_product_link_fix(inputs);
7155
+ });
7156
+
7157
+ //#endregion
7158
+ //#region ../i18n/src/paraglide/messages/mcp_rule_product_link_title.js
7159
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7160
+ /** @typedef {{}} Mcp_Rule_Product_Link_TitleInputs */
7161
+ const ko_mcp_rule_product_link_title = () => {
7162
+ return `상품 링크는 상품번호 주소로 그린다(\`/products/{productNo}\`)`;
7163
+ };
7164
+ /**
7165
+
7166
+ * @param {Mcp_Rule_Product_Link_TitleInputs} inputs
7167
+ * @param {{ locale?: "ko" }} options
7168
+ * @returns {LocalizedString}
7169
+ */
7170
+ const mcp_rule_product_link_title = ((inputs = {}, options = {}) => {
7171
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7172
+ return ko_mcp_rule_product_link_title(inputs);
7173
+ });
7174
+
7175
+ //#endregion
7176
+ //#region ../i18n/src/paraglide/messages/mcp_rule_product_link_why.js
7177
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7178
+ /** @typedef {{}} Mcp_Rule_Product_Link_WhyInputs */
7179
+ const ko_mcp_rule_product_link_why = () => {
7180
+ return `상품 주소는 상점 안에서 유일한 숫자 상품번호다. 상품 id(\`prod_…\`) 주소도 열리지만 상세 화면이 상품번호 주소로 301 이동시키므로 링크마다 왕복이 한 번 더 생기고 공유·검색 주소가 둘로 갈린다`;
7181
+ };
7182
+ /**
7183
+
7184
+ * @param {Mcp_Rule_Product_Link_WhyInputs} inputs
7185
+ * @param {{ locale?: "ko" }} options
7186
+ * @returns {LocalizedString}
7187
+ */
7188
+ const mcp_rule_product_link_why = ((inputs = {}, options = {}) => {
7189
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7190
+ return ko_mcp_rule_product_link_why(inputs);
7191
+ });
7192
+
6317
7193
  //#endregion
6318
7194
  //#region ../i18n/src/paraglide/messages/mcp_rule_server_only_env_example.js
6319
7195
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -7063,6 +7939,24 @@ const mcp_verify_payment_return_route = ((inputs = {}, options = {}) => {
7063
7939
  return ko_mcp_verify_payment_return_route(inputs);
7064
7940
  });
7065
7941
 
7942
+ //#endregion
7943
+ //#region ../i18n/src/paraglide/messages/mcp_verify_product_link.js
7944
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7945
+ /** @typedef {{}} Mcp_Verify_Product_LinkInputs */
7946
+ const ko_mcp_verify_product_link = () => {
7947
+ return `상품 링크의 경로 값이 상품 id다 — \`productPathParam(product)\`로 상품번호 주소를 쓴다`;
7948
+ };
7949
+ /**
7950
+
7951
+ * @param {Mcp_Verify_Product_LinkInputs} inputs
7952
+ * @param {{ locale?: "ko" }} options
7953
+ * @returns {LocalizedString}
7954
+ */
7955
+ const mcp_verify_product_link = ((inputs = {}, options = {}) => {
7956
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7957
+ return ko_mcp_verify_product_link(inputs);
7958
+ });
7959
+
7066
7960
  //#endregion
7067
7961
  //#region ../i18n/src/paraglide/messages/mcp_verify_server_only_env.js
7068
7962
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -7148,10 +8042,30 @@ const ko_mcp_verify_whole_project = () => {
7148
8042
  * @param {{ locale?: "ko" }} options
7149
8043
  * @returns {LocalizedString}
7150
8044
  */
7151
- const mcp_verify_whole_project = ((inputs = {}, options = {}) => {
7152
- experimentalStaticLocale ?? options.locale ?? getLocale();
7153
- return ko_mcp_verify_whole_project(inputs);
7154
- });
8045
+ const mcp_verify_whole_project = ((inputs = {}, options = {}) => {
8046
+ experimentalStaticLocale ?? options.locale ?? getLocale();
8047
+ return ko_mcp_verify_whole_project(inputs);
8048
+ });
8049
+
8050
+ //#endregion
8051
+ //#region ../i18n/src/lazy-messages.ts
8052
+ /**
8053
+ * 라벨 표 — 값을 읽을 때마다 메시지 함수를 불러 그 순간의 로케일 문구를 돌려준다(이슈 #73).
8054
+ * 모듈을 읽을 때 문구를 만들면 서버 렌더링에서 요청 로케일이 아니라 기동 시점 로케일로 굳는다.
8055
+ * 타입은 `Record<K, string>`이라 호출부(`LABEL[key]`, `Object.entries(LABEL)`)를 바꾸지 않는다.
8056
+ *
8057
+ * ```ts
8058
+ * export const ROLE_LABEL = lazyMessages<StoreRole>({ OWNER: m.store_shell_role_owner, ... });
8059
+ * ```
8060
+ */
8061
+ function lazyMessages(table) {
8062
+ const labels = {};
8063
+ for (const key of Object.keys(table)) Object.defineProperty(labels, key, {
8064
+ enumerable: true,
8065
+ get: () => table[key]()
8066
+ });
8067
+ return Object.freeze(labels);
8068
+ }
7155
8069
 
7156
8070
  //#endregion
7157
8071
  //#region ../i18n/src/process-locale.ts
@@ -7202,47 +8116,6 @@ function takeLangFlag(argv) {
7202
8116
  };
7203
8117
  }
7204
8118
 
7205
- //#endregion
7206
- //#region package.json
7207
- var version = "0.4.0";
7208
-
7209
- //#endregion
7210
- //#region src/client.ts
7211
- /**
7212
- * 관리 API 클라이언트 — `@sayren/store-sdk`(계약의 원천)를 쓴다. User-Agent `sayren-cli/{버전}`은 api가 버전의 만든 경로
7213
- * (`createdVia: CLI`)를 적는 표시다(권한과 무관하다. 에이전트용 액세스 토큰이면 행위자는 여전히 에이전트이고 발행은 승인 대기다).
7214
- * 토큰은 헤더에만 싣고 화면·로그에 찍지 않는다.
7215
- */
7216
- const CLI_VERSION = version;
7217
- const CLI_USER_AGENT = `sayren-cli/${CLI_VERSION}`;
7218
- /**
7219
- * 계정 토큰(#71)을 Bearer로, 고른 상점(`auth.storeId`)을 `X-Store-Id`로 싣는다. 401이면 `refresh`(자격 증명 파일의 refresh 회전)를
7220
- * 한 번 부른 뒤 다시 보낸다. 토큰·상점은 함수로 읽어 갱신·선택된 값을 따라간다.
7221
- */
7222
- function clientOf(auth, fetchImpl = fetch, refresh) {
7223
- return createStoreClient({
7224
- baseUrl: `${auth.apiOrigin}/v1`,
7225
- accessToken: () => auth.token,
7226
- storeId: () => auth.storeId ?? null,
7227
- ...refresh ? { refresh } : {},
7228
- fetch: (input, init$1) => {
7229
- const headers = new Headers(init$1?.headers);
7230
- headers.set("user-agent", CLI_USER_AGENT);
7231
- return fetchImpl(input, {
7232
- ...init$1,
7233
- headers
7234
- });
7235
- }
7236
- });
7237
- }
7238
- /** 오류 코드 — `ApiError`가 아니면 null */
7239
- function codeOf(error) {
7240
- return error instanceof ApiError ? error.code : null;
7241
- }
7242
- function detailsOf(error) {
7243
- return error instanceof ApiError ? error.details ?? {} : {};
7244
- }
7245
-
7246
8119
  //#endregion
7247
8120
  //#region ../mcp/src/account-token.ts
7248
8121
  /**
@@ -8212,6 +9085,807 @@ async function selectStore(auth, client, io, candidates, folderStore = null) {
8212
9085
  return auth.storeId;
8213
9086
  }
8214
9087
 
9088
+ //#endregion
9089
+ //#region package.json
9090
+ var version = "0.5.0";
9091
+
9092
+ //#endregion
9093
+ //#region src/client.ts
9094
+ /**
9095
+ * 관리 API 클라이언트 — `@sayren/store-sdk`(계약의 원천)를 쓴다. User-Agent `sayren-cli/{버전}`은 api가 버전의 만든 경로
9096
+ * (`createdVia: CLI`)를 적는 표시다(권한과 무관하다. 에이전트용 액세스 토큰이면 행위자는 여전히 에이전트이고 발행은 승인 대기다).
9097
+ * 토큰은 헤더에만 싣고 화면·로그에 찍지 않는다.
9098
+ */
9099
+ const CLI_VERSION = version;
9100
+ const CLI_USER_AGENT = `sayren-cli/${CLI_VERSION}`;
9101
+ /**
9102
+ * 계정 토큰(#71)을 Bearer로, 고른 상점(`auth.storeId`)을 `X-Store-Id`로 싣는다. 401이면 `refresh`(자격 증명 파일의 refresh 회전)를
9103
+ * 한 번 부른 뒤 다시 보낸다. 토큰·상점은 함수로 읽어 갱신·선택된 값을 따라간다.
9104
+ */
9105
+ function clientOf(auth, fetchImpl = fetch, refresh) {
9106
+ return createStoreClient({
9107
+ baseUrl: `${auth.apiOrigin}/v1`,
9108
+ accessToken: () => auth.token,
9109
+ storeId: () => auth.storeId ?? null,
9110
+ ...refresh ? { refresh } : {},
9111
+ fetch: (input, init$1) => {
9112
+ const headers = new Headers(init$1?.headers);
9113
+ headers.set("user-agent", CLI_USER_AGENT);
9114
+ return fetchImpl(input, {
9115
+ ...init$1,
9116
+ headers
9117
+ });
9118
+ }
9119
+ });
9120
+ }
9121
+ /** 오류 코드 — `ApiError`가 아니면 null */
9122
+ function codeOf(error) {
9123
+ return error instanceof ApiError ? error.code : null;
9124
+ }
9125
+ function detailsOf(error) {
9126
+ return error instanceof ApiError ? error.details ?? {} : {};
9127
+ }
9128
+
9129
+ //#endregion
9130
+ //#region src/app-commands.ts
9131
+ /**
9132
+ * `sayren app …`(이슈 #22, 매뉴얼 「앱 만들기」) — 앱 폴더(`sayren.app.json`)에서 돈다. 서버는 코드를 빌드하지 않는다(A1):
9133
+ * CLI가 esbuild로 서버 번들 한 파일을 만들어 올리고 api가 `DEPENDENCIES`·`VERIFY`·`BUNDLE`·`UPLOAD`를 돈다. 앱 화면(`ui/`)은
9134
+ * 정적 파일 그대로 올린다.
9135
+ *
9136
+ * 인증은 둘이다. 앱 배포 키(`--key`·`SAYREN_APP_KEY`, developer.sayren.app에서 발행, `sak_…`)는 그 앱의 경로에만 쓰이고(CI 용도),
9137
+ * 그 밖에는 `sayren login`의 계정 토큰이다. 앱 등록(`init`)·개발 상점은 계정 토큰만 된다.
9138
+ */
9139
+ const APP_MANIFEST_FILE = "sayren.app.json";
9140
+ const APP_STATE_FILE = `${STATE_DIR}/app.json`;
9141
+ const APP_DEV_PORT = 4020;
9142
+ const SERVER_ENTRY_CANDIDATES = [
9143
+ "src/server.ts",
9144
+ "src/server.js",
9145
+ "src/server.mjs"
9146
+ ];
9147
+ const UI_DIR = "ui";
9148
+ const SETTING_ENV_PREFIX = "SAYREN_APP_SETTING_";
9149
+ const SECRET_ENV_PREFIX = "SAYREN_APP_SECRET_";
9150
+ /** `dev`의 개발 설치 토큰을 만료 몇 초 전에 다시 받는가 */
9151
+ const DEV_TOKEN_REFRESH_MARGIN_MS = 6e4;
9152
+ function readAppState(dir) {
9153
+ const path = join(dir, APP_STATE_FILE);
9154
+ if (!existsSync(path)) return null;
9155
+ try {
9156
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
9157
+ if (typeof parsed.appId === "string" && typeof parsed.handle === "string" && typeof parsed.apiOrigin === "string") return {
9158
+ appId: parsed.appId,
9159
+ handle: parsed.handle,
9160
+ apiOrigin: parsed.apiOrigin
9161
+ };
9162
+ } catch {}
9163
+ throw new CliError(cli_app_state_invalid({ file: APP_STATE_FILE }));
9164
+ }
9165
+ function writeAppState(dir, state) {
9166
+ mkdirSync(join(dir, STATE_DIR), { recursive: true });
9167
+ writeFileSync(join(dir, APP_STATE_FILE), `${JSON.stringify(state, null, 2)}\n`, { mode: 384 });
9168
+ }
9169
+ /** 매니페스트 원문(JSON)과 검증 결과 — 문제가 있으면 CliError */
9170
+ function loadManifest(dir) {
9171
+ const path = join(dir, APP_MANIFEST_FILE);
9172
+ if (!existsSync(path)) throw new CliError(cli_app_manifest_missing({ file: APP_MANIFEST_FILE }));
9173
+ let raw;
9174
+ try {
9175
+ raw = JSON.parse(readFileSync(path, "utf8"));
9176
+ } catch (error) {
9177
+ throw new CliError(cli_app_manifest_invalid({ problems: `- ${error instanceof Error ? error.message : String(error)}` }));
9178
+ }
9179
+ const parsed = appManifestSchema.safeParse(raw);
9180
+ if (!parsed.success) throw new CliError(cli_app_manifest_invalid({ problems: parsed.error.issues.map((issue) => `- ${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n") }));
9181
+ const problems = appManifestProblems(parsed.data);
9182
+ if (problems.length) throw new CliError(cli_app_manifest_invalid({ problems: problems.map((p) => `- ${p.path}: ${p.message} (${p.code})`).join("\n") }));
9183
+ return {
9184
+ raw,
9185
+ manifest: parsed.data
9186
+ };
9187
+ }
9188
+ function appDir(flags, io) {
9189
+ return resolve(io.cwd, flags.dir ?? ".");
9190
+ }
9191
+ function requireState(dir) {
9192
+ const state = readAppState(dir);
9193
+ if (!state) throw new CliError(cli_app_not_registered({ file: APP_MANIFEST_FILE }));
9194
+ return state;
9195
+ }
9196
+ /** 배포 키(`--key` → `SAYREN_APP_KEY`)가 있으면 그것, 없으면 계정 토큰(`resolveAuth`) */
9197
+ async function developerAuth(flags, dir, io, options = {}) {
9198
+ const state = readAppState(dir);
9199
+ const key = flags.key?.trim() || io.env.SAYREN_APP_KEY?.trim();
9200
+ if (key && !options.accountOnly) {
9201
+ if (!APP_DEPLOY_KEY_PATTERN.test(key)) throw new CliError(cli_app_key_invalid());
9202
+ const { apiOrigin } = resolveApiOrigin({ apiOrigin: flags.apiOrigin ?? state?.apiOrigin }, dir, null, io.env);
9203
+ return {
9204
+ apiOrigin,
9205
+ bearer: key,
9206
+ kind: "key"
9207
+ };
9208
+ }
9209
+ const auth = resolveAuth({
9210
+ ...flags,
9211
+ apiOrigin: flags.apiOrigin ?? state?.apiOrigin
9212
+ }, dir, null, io.env, credentialTokenReader(io.env));
9213
+ const refresh = await readyAuth(auth, io);
9214
+ return {
9215
+ apiOrigin: auth.apiOrigin,
9216
+ get bearer() {
9217
+ return auth.token;
9218
+ },
9219
+ kind: "account",
9220
+ ...refresh ? { refresh } : {}
9221
+ };
9222
+ }
9223
+ async function devApi(auth, io, method, path, body, retried = false) {
9224
+ const res = await io.fetch(`${auth.apiOrigin}/platform/v1/developer${path}`, {
9225
+ method,
9226
+ headers: {
9227
+ authorization: `Bearer ${auth.bearer}`,
9228
+ "user-agent": CLI_USER_AGENT,
9229
+ ...body === void 0 ? {} : { "content-type": "application/json" }
9230
+ },
9231
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
9232
+ });
9233
+ if (res.status === 204) return void 0;
9234
+ const text = await res.text();
9235
+ let json = {};
9236
+ try {
9237
+ json = text ? JSON.parse(text) : {};
9238
+ } catch {
9239
+ throw new ApiError(res.status, "INVALID_RESPONSE", text.slice(0, 200));
9240
+ }
9241
+ if (res.ok && json.meta?.isSuccess !== false) return json.data;
9242
+ if (res.status === 401 && auth.refresh && !retried && await auth.refresh()) return devApi(auth, io, method, path, body, true);
9243
+ throw new ApiError(res.status, json.error?.code ?? json.meta?.code ?? "ERROR", json.error?.message ?? json.meta?.message ?? `HTTP ${res.status}`, void 0, json, json.error?.details);
9244
+ }
9245
+ function serverEntryOf(dir) {
9246
+ const found = SERVER_ENTRY_CANDIDATES.find((candidate) => existsSync(join(dir, candidate)));
9247
+ if (!found) throw new CliError(cli_app_entry_missing({ entry: SERVER_ENTRY_CANDIDATES[0] }));
9248
+ return join(dir, found);
9249
+ }
9250
+ /** esbuild로 서버 번들을 만든다. 앱 의존성은 앱 폴더의 `node_modules`에서 푼다(설치돼 있어야 한다) */
9251
+ async function bundleServer(dir) {
9252
+ const entry = serverEntryOf(dir);
9253
+ const esbuild = await import("esbuild");
9254
+ try {
9255
+ const file = (await esbuild.build({
9256
+ entryPoints: [entry],
9257
+ bundle: true,
9258
+ write: false,
9259
+ format: "esm",
9260
+ platform: "browser",
9261
+ target: "es2022",
9262
+ conditions: [
9263
+ "workerd",
9264
+ "worker",
9265
+ "browser"
9266
+ ],
9267
+ mainFields: [
9268
+ "browser",
9269
+ "module",
9270
+ "main"
9271
+ ],
9272
+ absWorkingDir: dir,
9273
+ logLevel: "silent",
9274
+ legalComments: "none",
9275
+ define: { "process.env.NODE_ENV": "\"production\"" }
9276
+ })).outputFiles?.[0];
9277
+ if (!file) throw new CliError(cli_app_bundle_failed({ tail: "(빈 산출물)" }));
9278
+ return file.text;
9279
+ } catch (error) {
9280
+ if (error instanceof CliError) throw error;
9281
+ const tail = error && typeof error === "object" && "errors" in error ? (error.errors ?? []).map((e) => e.location ? `${e.location.file}:${e.location.line}: ${e.text}` : e.text).join("\n") : error instanceof Error ? error.message : String(error);
9282
+ throw new CliError(cli_app_bundle_failed({ tail }));
9283
+ }
9284
+ }
9285
+ /** 번들을 불러 `defineApp` 메타데이터를 읽는다 — `HANDLER_MISSING`·기본 내보내기 검사. `dev`도 이 모듈을 실행한다 */
9286
+ async function loadBundle(dir, server) {
9287
+ const out = join(dir, STATE_DIR, "build");
9288
+ mkdirSync(out, { recursive: true });
9289
+ const file = join(out, `server.${Date.now().toString(36)}.mjs`);
9290
+ writeFileSync(file, server);
9291
+ let mod;
9292
+ try {
9293
+ mod = await import(pathToFileURL(file).href);
9294
+ } catch (error) {
9295
+ throw new CliError(cli_app_bundle_failed({ tail: error instanceof Error ? error.message : String(error) }));
9296
+ }
9297
+ const app = mod.default;
9298
+ const meta = app?.__sayrenApp;
9299
+ if (!app || typeof app.fetch !== "function" || !meta) throw new CliError(cli_app_default_export_missing());
9300
+ return {
9301
+ app,
9302
+ meta: {
9303
+ events: [...meta.events],
9304
+ http: [...meta.http]
9305
+ }
9306
+ };
9307
+ }
9308
+ /** 로컬 검사 — 매니페스트, 번들, 이벤트 함수 존재, 비밀 값 형식, 크기. 서버 `VERIFY`·`BUNDLE`과 같은 규칙 */
9309
+ async function verifyApp(dir) {
9310
+ const { raw, manifest } = loadManifest(dir);
9311
+ const server = await bundleServer(dir);
9312
+ if (APP_SECRET_PATTERNS.find((pattern) => pattern.test(server))) throw new CliError(cli_app_secret_in_source());
9313
+ const gzipBytes = gzipSync(server).byteLength;
9314
+ if (gzipBytes > APP_SERVER_BUNDLE_MAX_GZIP_BYTES) throw new CliError(cli_app_bundle_too_large({ size: formatMb(gzipBytes) }));
9315
+ const { app, meta } = await loadBundle(dir, server);
9316
+ const missing = manifest.events.filter((event) => !meta.events.includes(event));
9317
+ if (missing.length) throw new CliError(cli_app_handler_missing({ events: missing.join(", ") }));
9318
+ const warnings = [];
9319
+ const listed = manifest.events;
9320
+ const extra = meta.events.filter((event) => !listed.includes(event));
9321
+ if (extra.length) warnings.push(cli_app_handler_unlisted({ events: extra.join(", ") }));
9322
+ const ui = manifest.ui?.adminPage ? collectUi(dir) : [];
9323
+ if (manifest.ui?.adminPage && !ui.some((file) => file.path === "index.html")) throw new CliError(cli_app_ui_index_missing({ dir: UI_DIR }));
9324
+ return {
9325
+ manifest,
9326
+ raw,
9327
+ bundle: {
9328
+ server,
9329
+ gzipBytes,
9330
+ meta
9331
+ },
9332
+ app,
9333
+ ui,
9334
+ warnings
9335
+ };
9336
+ }
9337
+ function collectUi(dir) {
9338
+ const root = join(dir, UI_DIR);
9339
+ if (!existsSync(root)) throw new CliError(cli_app_ui_index_missing({ dir: UI_DIR }));
9340
+ const files = [];
9341
+ const walk = (current) => {
9342
+ for (const name of readdirSync(current)) {
9343
+ if (name.startsWith(".") || name === "node_modules" || name === "_sayren") continue;
9344
+ const full = join(current, name);
9345
+ if (statSync(full).isDirectory()) walk(full);
9346
+ else files.push({
9347
+ path: relative(root, full).split(sep).join("/"),
9348
+ bytes: readFileSync(full)
9349
+ });
9350
+ }
9351
+ };
9352
+ walk(root);
9353
+ const bridge = join(dir, "node_modules/@sayren/app/dist/ui.mjs");
9354
+ if (!existsSync(bridge)) throw new CliError(cli_app_bridge_missing());
9355
+ files.push({
9356
+ path: "_sayren/ui.js",
9357
+ bytes: readFileSync(bridge)
9358
+ });
9359
+ if (files.length > APP_UI_MAX_FILES) throw new CliError(cli_app_ui_too_large({ limit: `${APP_UI_MAX_FILES} files` }));
9360
+ if (files.reduce((sum, file) => sum + file.bytes.byteLength, 0) > APP_UI_MAX_BYTES) throw new CliError(cli_app_ui_too_large({ limit: formatMb(APP_UI_MAX_BYTES) }));
9361
+ return files;
9362
+ }
9363
+ function formatMb(bytes) {
9364
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
9365
+ }
9366
+ const HANDLE_RE = /^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$/;
9367
+ function initFiles(handle, name) {
9368
+ const manifest = {
9369
+ handle,
9370
+ name,
9371
+ description: "",
9372
+ scopes: ["order:r"],
9373
+ network: { hosts: [] },
9374
+ events: ["ORDER.PAID"],
9375
+ settings: {}
9376
+ };
9377
+ return {
9378
+ [APP_MANIFEST_FILE]: `${JSON.stringify(manifest, null, 2)}\n`,
9379
+ "package.json": `${JSON.stringify({
9380
+ name: handle,
9381
+ private: true,
9382
+ type: "module",
9383
+ scripts: {
9384
+ dev: "sayren app dev",
9385
+ verify: "sayren app verify",
9386
+ deploy: "sayren app deploy"
9387
+ },
9388
+ dependencies: { "@sayren/app": `^${CLI_VERSION}` },
9389
+ devDependencies: {
9390
+ sayren: `^${CLI_VERSION}`,
9391
+ typescript: "^5.9.0"
9392
+ }
9393
+ }, null, 2)}\n`,
9394
+ "tsconfig.json": `${JSON.stringify({
9395
+ compilerOptions: {
9396
+ target: "ES2022",
9397
+ module: "ESNext",
9398
+ moduleResolution: "bundler",
9399
+ strict: true,
9400
+ noEmit: true,
9401
+ lib: ["ES2022", "DOM"],
9402
+ skipLibCheck: true
9403
+ },
9404
+ include: ["src"]
9405
+ }, null, 2)}\n`,
9406
+ "src/server.ts": `import { defineApp } from "@sayren/app";
9407
+
9408
+ export default defineApp({
9409
+ events: {
9410
+ "ORDER.PAID": async (event, ctx) => {
9411
+ const { orderId } = event.data as { orderId: string };
9412
+ const order = await ctx.api.orders.get(orderId);
9413
+ ctx.log.info("결제된 주문", order.orderNo, order.totalAmount);
9414
+ },
9415
+ },
9416
+ });
9417
+ `,
9418
+ ".gitignore": "node_modules/\ndist/\n.env\n.sayren/\n",
9419
+ ".env.example": `# 로컬 실행(sayren app dev)의 설정·비밀 값\n# ${SETTING_ENV_PREFIX}<설정 키>=값\n# ${SECRET_ENV_PREFIX}<비밀 값 이름>=값\n`
9420
+ };
9421
+ }
9422
+ async function appInit(handle, flags, io) {
9423
+ if (!handle || !HANDLE_RE.test(handle)) throw new CliError(cli_app_handle_invalid());
9424
+ const dir = resolve(io.cwd, flags.dir ?? handle);
9425
+ if (existsSync(join(dir, APP_MANIFEST_FILE))) throw new CliError(cli_app_dir_exists({ dir: relative(io.cwd, dir) || "." }));
9426
+ const name = flags.name?.trim() || handle;
9427
+ const auth = await developerAuth(flags, dir, io, { accountOnly: true });
9428
+ const app = await devApi(auth, io, "POST", "/apps", {
9429
+ handle,
9430
+ name
9431
+ });
9432
+ mkdirSync(join(dir, "src"), { recursive: true });
9433
+ for (const [path, content] of Object.entries(initFiles(handle, name))) {
9434
+ const full = join(dir, path);
9435
+ mkdirSync(join(full, ".."), { recursive: true });
9436
+ if (!existsSync(full)) writeFileSync(full, content);
9437
+ }
9438
+ writeAppState(dir, {
9439
+ appId: app.appId,
9440
+ handle: app.handle,
9441
+ apiOrigin: auth.apiOrigin
9442
+ });
9443
+ io.out(cli_app_init_done({
9444
+ dir: relative(io.cwd, dir) || ".",
9445
+ appId: app.appId,
9446
+ handle
9447
+ }));
9448
+ return 0;
9449
+ }
9450
+ async function appVerify(flags, io) {
9451
+ const outcome = await verifyApp(appDir(flags, io));
9452
+ for (const warning of outcome.warnings) io.err(cli_app_warning({ message: warning }));
9453
+ io.out(cli_app_verify_ok({
9454
+ events: outcome.bundle.meta.events.join(", ") || "-",
9455
+ http: outcome.bundle.meta.http.join(", ") || "-",
9456
+ size: formatMb(outcome.bundle.gzipBytes),
9457
+ ui: String(outcome.ui.length)
9458
+ }));
9459
+ return 0;
9460
+ }
9461
+ function printBuild(version$1, io) {
9462
+ for (const stage of version$1.build?.stages ?? []) io.out(` ${stage.ok ? "✓" : "✗"} ${stage.stage} ${stage.durationMs}ms`);
9463
+ }
9464
+ async function appDeploy(flags, io) {
9465
+ const dir = appDir(flags, io);
9466
+ const state = requireState(dir);
9467
+ const outcome = await verifyApp(dir);
9468
+ if (outcome.manifest.handle !== state.handle) throw new CliError(cli_app_handle_mismatch({
9469
+ manifest: outcome.manifest.handle,
9470
+ registered: state.handle
9471
+ }));
9472
+ for (const warning of outcome.warnings) io.err(cli_app_warning({ message: warning }));
9473
+ const auth = await developerAuth(flags, dir, io);
9474
+ const packageJson = existsSync(join(dir, "package.json")) ? readFileSync(join(dir, "package.json"), "utf8") : "{}";
9475
+ const lockfile = existsSync(join(dir, "pnpm-lock.yaml")) ? readFileSync(join(dir, "pnpm-lock.yaml"), "utf8") : null;
9476
+ io.out(cli_app_uploading({
9477
+ size: formatMb(outcome.bundle.gzipBytes),
9478
+ ui: String(outcome.ui.length)
9479
+ }));
9480
+ const version$1 = await devApi(auth, io, "PUT", `/apps/${state.appId}/versions`, {
9481
+ manifest: outcome.raw,
9482
+ packageJson,
9483
+ lockfile,
9484
+ server: Buffer.from(outcome.bundle.server, "utf8").toString("base64"),
9485
+ ui: outcome.ui.map((file) => ({
9486
+ path: file.path,
9487
+ content: file.bytes.toString("base64")
9488
+ })),
9489
+ ...flags.message ? { message: flags.message } : {}
9490
+ });
9491
+ printBuild(version$1, io);
9492
+ if (version$1.status !== "READY") {
9493
+ io.err(cli_app_build_failed({
9494
+ stage: version$1.build?.failureStage ?? "-",
9495
+ code: version$1.build?.errorCode ?? "-",
9496
+ tail: version$1.build?.logTail ?? ""
9497
+ }));
9498
+ return 1;
9499
+ }
9500
+ io.out(cli_app_deploy_done({ number: String(version$1.number) }));
9501
+ return 0;
9502
+ }
9503
+ async function appRelease(numberRaw, flags, io) {
9504
+ const number = Number(numberRaw);
9505
+ if (!Number.isInteger(number) || number < 1) throw new CliError(cli_app_version_number_required());
9506
+ const dir = appDir(flags, io);
9507
+ const state = requireState(dir);
9508
+ const result = await devApi(await developerAuth(flags, dir, io), io, "POST", `/apps/${state.appId}/versions/${number}/release`);
9509
+ io.out(cli_app_release_done({
9510
+ number: String(result.version.number),
9511
+ applied: String(result.applied),
9512
+ pending: String(result.pendingReconsent)
9513
+ }));
9514
+ return 0;
9515
+ }
9516
+ async function appStatus(flags, io) {
9517
+ const dir = appDir(flags, io);
9518
+ const state = requireState(dir);
9519
+ const auth = await developerAuth(flags, dir, io);
9520
+ const [app, versions] = await Promise.all([devApi(auth, io, "GET", `/apps/${state.appId}`), devApi(auth, io, "GET", `/apps/${state.appId}/versions`)]);
9521
+ io.out(cli_app_status({
9522
+ handle: app.handle,
9523
+ name: app.name,
9524
+ appId: app.appId,
9525
+ status: app.status,
9526
+ current: app.currentVersionNumber === null ? "-" : String(app.currentVersionNumber),
9527
+ active: String(app.installations.active),
9528
+ pending: String(app.installations.pendingReconsent)
9529
+ }));
9530
+ for (const version$1 of versions.slice(0, 5)) io.out(` v${version$1.number} ${version$1.status}${version$1.current ? " *" : ""} ${version$1.createdAt}${version$1.message ? ` — ${version$1.message}` : ""}${version$1.build?.errorCode ? ` (${version$1.build.errorCode})` : ""}`);
9531
+ return 0;
9532
+ }
9533
+ const SINCE_RE = /^(\d+)([smhd])$/;
9534
+ function sinceMs(value) {
9535
+ if (!value) return null;
9536
+ const match = SINCE_RE.exec(value.trim());
9537
+ if (!match) throw new CliError(cli_app_since_invalid({ value }));
9538
+ const unit = {
9539
+ s: 1e3,
9540
+ m: 6e4,
9541
+ h: 36e5,
9542
+ d: 864e5
9543
+ }[match[2]];
9544
+ return Number(match[1]) * unit;
9545
+ }
9546
+ function invocationLine(row) {
9547
+ const head = `${row.createdAt} ${row.outcome.padEnd(14)} ${row.kind}${row.eventType ? ` ${row.eventType}` : ""} ${row.storeName ?? row.storeId} v${row.versionNumber ?? "-"} ${row.durationMs}ms`;
9548
+ const lines = row.logs.map((line) => ` [${line.level}] ${line.message}`);
9549
+ if (row.errorMessage) lines.push(` ! ${row.errorMessage.split("\n")[0]}`);
9550
+ return [head, ...lines].join("\n");
9551
+ }
9552
+ async function appLogs(flags, io) {
9553
+ const dir = appDir(flags, io);
9554
+ const state = requireState(dir);
9555
+ const auth = await developerAuth(flags, dir, io);
9556
+ const since = sinceMs(flags.since);
9557
+ const seen = /* @__PURE__ */ new Set();
9558
+ const query = new URLSearchParams({ limit: "50" });
9559
+ if (flags.store) query.set("storeId", flags.store);
9560
+ const fetchOnce = async () => {
9561
+ const rows = await devApi(auth, io, "GET", `/apps/${state.appId}/invocations?${query}`);
9562
+ const now = io.now?.() ?? Date.now();
9563
+ const fresh = rows.filter((row) => !seen.has(row.invocationId)).filter((row) => since === null || now - Date.parse(row.createdAt) <= since).reverse();
9564
+ for (const row of fresh) {
9565
+ seen.add(row.invocationId);
9566
+ io.out(invocationLine(row));
9567
+ }
9568
+ return fresh.length;
9569
+ };
9570
+ const count = await fetchOnce();
9571
+ if (!flags.tail) {
9572
+ if (count === 0) io.out(cli_app_logs_empty());
9573
+ return 0;
9574
+ }
9575
+ while (!io.signal?.aborted) {
9576
+ await io.sleep(io.pollMs);
9577
+ await fetchOnce();
9578
+ }
9579
+ return 0;
9580
+ }
9581
+ async function appSecret(action, name, flags, io) {
9582
+ const dir = appDir(flags, io);
9583
+ const state = requireState(dir);
9584
+ const auth = await developerAuth(flags, dir, io);
9585
+ const base = `/apps/${state.appId}/secrets`;
9586
+ if (action === "list") {
9587
+ const list = await devApi(auth, io, "GET", base);
9588
+ if (list.length === 0) io.out(cli_app_secret_empty());
9589
+ for (const item of list) io.out(` ${item.name} ${item.updatedAt}`);
9590
+ return 0;
9591
+ }
9592
+ if (!name) throw new CliError(cli_app_secret_name_required());
9593
+ if (action === "put") {
9594
+ const value = (await (io.readStdin ?? readStdin)()).replace(/\r?\n$/, "");
9595
+ if (!value) throw new CliError(cli_app_secret_value_required({ name }));
9596
+ await devApi(auth, io, "PUT", `${base}/${encodeURIComponent(name)}`, { value });
9597
+ io.out(cli_app_secret_saved({ name }));
9598
+ return 0;
9599
+ }
9600
+ if (action === "delete") {
9601
+ await devApi(auth, io, "DELETE", `${base}/${encodeURIComponent(name)}`);
9602
+ io.out(cli_app_secret_deleted({ name }));
9603
+ return 0;
9604
+ }
9605
+ throw new CliError(cli_app_subcommand_unknown({ command: `secret ${action ?? ""}`.trim() }));
9606
+ }
9607
+ function readStdin() {
9608
+ return new Promise((resolve$1, reject) => {
9609
+ let data = "";
9610
+ process.stdin.setEncoding("utf8");
9611
+ process.stdin.on("data", (chunk) => {
9612
+ data += chunk;
9613
+ });
9614
+ process.stdin.once("end", () => resolve$1(data));
9615
+ process.stdin.once("error", reject);
9616
+ });
9617
+ }
9618
+ async function appDevStore(action, name, flags, io) {
9619
+ const auth = await developerAuth(flags, appDir(flags, io), io, { accountOnly: true });
9620
+ if (action === "list") {
9621
+ const list = await devApi(auth, io, "GET", "/dev-stores");
9622
+ if (list.length === 0) io.out(cli_app_dev_store_empty());
9623
+ for (const store of list) io.out(` ${store.storeId} ${store.storeCode} ${store.name} ${store.status}`);
9624
+ return 0;
9625
+ }
9626
+ if (action === "create") {
9627
+ if (!name?.trim()) throw new CliError(cli_app_dev_store_name_required());
9628
+ const store = await devApi(auth, io, "POST", "/dev-stores", { name: name.trim() });
9629
+ io.out(cli_app_dev_store_created({
9630
+ storeId: store.storeId,
9631
+ name: store.name
9632
+ }));
9633
+ return 0;
9634
+ }
9635
+ throw new CliError(cli_app_subcommand_unknown({ command: `dev-store ${action ?? ""}`.trim() }));
9636
+ }
9637
+ /** `.env`의 `SAYREN_APP_SETTING_*`·`SAYREN_APP_SECRET_*` — 설정은 매니페스트 타입으로 바꾼다 */
9638
+ function localSettingsOf(manifest, env) {
9639
+ const settings = {};
9640
+ const secrets = {};
9641
+ for (const [key, field] of Object.entries(manifest.settings ?? {})) {
9642
+ const raw = env.get(`${SETTING_ENV_PREFIX}${key}`);
9643
+ if (raw === void 0) {
9644
+ if (field.default !== void 0) settings[key] = field.default;
9645
+ continue;
9646
+ }
9647
+ settings[key] = field.type === "number" ? Number(raw) : field.type === "boolean" ? raw === "true" || raw === "1" : raw;
9648
+ }
9649
+ for (const [key, value] of env) if (key.startsWith(SECRET_ENV_PREFIX)) secrets[key.slice(18)] = value;
9650
+ return {
9651
+ settings,
9652
+ secrets
9653
+ };
9654
+ }
9655
+ let devSeq = 0;
9656
+ function devEnvelope(input) {
9657
+ devSeq += 1;
9658
+ return {
9659
+ v: 1,
9660
+ invocationId: `inv_dev_${Date.now().toString(36)}_${devSeq}`,
9661
+ kind: input.kind,
9662
+ installation: {
9663
+ installationId: input.token?.installationId ?? "inst_dev",
9664
+ appId: input.appId,
9665
+ versionNumber: 0,
9666
+ storeId: input.token?.storeId ?? "store_dev",
9667
+ storeName: "(dev)",
9668
+ apiBaseUrl: input.apiBaseUrl
9669
+ },
9670
+ token: input.token?.token ?? null,
9671
+ settings: input.settings,
9672
+ secrets: input.secrets,
9673
+ ...input.kind === "http" ? { user: {
9674
+ userId: "user_dev",
9675
+ role: "OWNER"
9676
+ } } : {},
9677
+ ...input.event ? { event: {
9678
+ id: `evt_dev_${devSeq}`,
9679
+ type: input.event.type,
9680
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
9681
+ storeId: input.token?.storeId ?? "store_dev",
9682
+ sequence: devSeq,
9683
+ data: input.event.data
9684
+ } } : {},
9685
+ ...input.request ? { request: input.request } : {}
9686
+ };
9687
+ }
9688
+ async function invokeLocal(app, envelope) {
9689
+ const res = await app.fetch(new Request("https://app.local/_invoke", {
9690
+ method: "POST",
9691
+ headers: { "content-type": "application/json" },
9692
+ body: JSON.stringify(envelope)
9693
+ }));
9694
+ if (!res.ok) throw new CliError(cli_app_bundle_failed({ tail: await res.text() }));
9695
+ return await res.json();
9696
+ }
9697
+ function printResult(label, result, io) {
9698
+ io.out(`${label} ${result.outcome} ${result.durationMs}ms`);
9699
+ for (const line of result.logs) io.out(` [${line.level}] ${line.message}`);
9700
+ if (result.errorMessage) io.err(` ! ${result.errorMessage}`);
9701
+ }
9702
+ const MIME = {
9703
+ ".html": "text/html; charset=utf-8",
9704
+ ".js": "text/javascript; charset=utf-8",
9705
+ ".mjs": "text/javascript; charset=utf-8",
9706
+ ".css": "text/css; charset=utf-8",
9707
+ ".json": "application/json",
9708
+ ".svg": "image/svg+xml",
9709
+ ".png": "image/png",
9710
+ ".jpg": "image/jpeg",
9711
+ ".ico": "image/x-icon"
9712
+ };
9713
+ function readBody(req) {
9714
+ return new Promise((resolve$1, reject) => {
9715
+ const chunks = [];
9716
+ req.on("data", (chunk) => chunks.push(chunk));
9717
+ req.once("end", () => resolve$1(Buffer.concat(chunks).toString("utf8")));
9718
+ req.once("error", reject);
9719
+ });
9720
+ }
9721
+ /**
9722
+ * 로컬 실행(매뉴얼 「2. 로컬에서 실행하기」) — 번들을 이 프로세스에서 불러 `/_trigger`(이벤트)·`/_http/*`(HTTP 핸들러)·`/`(앱 화면)를 받는다.
9723
+ * 설치 토큰은 `POST …/dev-token`(1시간)으로 받고 만료 전에 다시 받는다. `--store`가 없으면 토큰 없이 돈다(`ctx.api`는 401).
9724
+ */
9725
+ async function appDev(flags, io) {
9726
+ const dir = appDir(flags, io);
9727
+ const state = requireState(dir);
9728
+ const { manifest } = loadManifest(dir);
9729
+ const { app } = await loadBundle(dir, await bundleServer(dir));
9730
+ const port = Number(flags.port ?? io.env.SAYREN_APP_PORT ?? APP_DEV_PORT);
9731
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new CliError(cli_app_port_invalid({ value: String(flags.port) }));
9732
+ const storeId = flags.store?.trim() || io.env.SAYREN_STORE_ID?.trim() || null;
9733
+ const auth = storeId ? await developerAuth(flags, dir, io) : null;
9734
+ let token = null;
9735
+ const ensureToken = async () => {
9736
+ if (!auth || !storeId) return null;
9737
+ const now = io.now?.() ?? Date.now();
9738
+ if (token && Date.parse(token.expiresAt) - now > DEV_TOKEN_REFRESH_MARGIN_MS) return token;
9739
+ token = await devApi(auth, io, "POST", `/apps/${state.appId}/dev-token`, { storeId });
9740
+ return token;
9741
+ };
9742
+ await ensureToken();
9743
+ const local = () => localSettingsOf(manifest, readEnvFile(dir));
9744
+ const apiBaseUrl = `${state.apiOrigin}/v1`;
9745
+ const uiRoot = join(dir, UI_DIR);
9746
+ const handle = async (req, res) => {
9747
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
9748
+ try {
9749
+ if (req.method === "POST" && url.pathname === "/_trigger") {
9750
+ const body = JSON.parse(await readBody(req) || "{}");
9751
+ if (!body.type) {
9752
+ res.writeHead(400, { "content-type": "application/json" });
9753
+ res.end(JSON.stringify({ error: "TYPE_REQUIRED" }));
9754
+ return;
9755
+ }
9756
+ const { settings, secrets } = local();
9757
+ const result = await invokeLocal(app, devEnvelope({
9758
+ kind: "event",
9759
+ token: await ensureToken(),
9760
+ apiBaseUrl,
9761
+ appId: state.appId,
9762
+ settings,
9763
+ secrets,
9764
+ event: {
9765
+ type: body.type,
9766
+ data: body.data ?? {}
9767
+ }
9768
+ }));
9769
+ printResult(`[event] ${body.type}`, result, io);
9770
+ res.writeHead(200, { "content-type": "application/json" });
9771
+ res.end(JSON.stringify(result));
9772
+ return;
9773
+ }
9774
+ if (url.pathname.startsWith("/_http/")) {
9775
+ const headers = {};
9776
+ for (const [key, value] of Object.entries(req.headers)) if (typeof value === "string" && key !== "host") headers[key] = value;
9777
+ const method = (req.method ?? "GET").toUpperCase();
9778
+ const { settings, secrets } = local();
9779
+ const result = await invokeLocal(app, devEnvelope({
9780
+ kind: "http",
9781
+ token: await ensureToken(),
9782
+ apiBaseUrl,
9783
+ appId: state.appId,
9784
+ settings,
9785
+ secrets,
9786
+ request: {
9787
+ method,
9788
+ path: `${url.pathname.slice(6)}${url.search}`,
9789
+ headers,
9790
+ body: method === "GET" ? null : await readBody(req)
9791
+ }
9792
+ }));
9793
+ printResult(`[http] ${method} ${url.pathname}`, result, io);
9794
+ if (!result.response) {
9795
+ res.writeHead(500, { "content-type": "application/json" });
9796
+ res.end(JSON.stringify({
9797
+ error: result.outcome,
9798
+ message: result.errorMessage
9799
+ }));
9800
+ return;
9801
+ }
9802
+ res.writeHead(result.response.status, result.response.headers);
9803
+ res.end(result.response.body ?? "");
9804
+ return;
9805
+ }
9806
+ const file = resolve(uiRoot, url.pathname === "/" ? "index.html" : url.pathname.slice(1));
9807
+ if (file.startsWith(uiRoot) && existsSync(file) && statSync(file).isFile()) {
9808
+ res.writeHead(200, { "content-type": MIME[extname(file)] ?? "application/octet-stream" });
9809
+ res.end(readFileSync(file));
9810
+ return;
9811
+ }
9812
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
9813
+ res.end("not found");
9814
+ } catch (error) {
9815
+ io.err(cli_app_dev_error({ message: error instanceof Error ? error.message : String(error) }));
9816
+ if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" });
9817
+ res.end(JSON.stringify({ error: "DEV_ERROR" }));
9818
+ }
9819
+ };
9820
+ const serverHandle = createServer((req, res) => {
9821
+ handle(req, res);
9822
+ });
9823
+ await new Promise((resolveListen, reject) => {
9824
+ serverHandle.once("error", reject);
9825
+ serverHandle.listen(port, "127.0.0.1", () => resolveListen());
9826
+ });
9827
+ io.out(cli_app_dev_listening({
9828
+ port: String(port),
9829
+ store: storeId ?? "-",
9830
+ events: manifest.events.join(", ") || "-"
9831
+ }));
9832
+ await new Promise((resolveClose) => {
9833
+ const stop = () => {
9834
+ serverHandle.close();
9835
+ resolveClose();
9836
+ };
9837
+ if (io.signal) if (io.signal.aborted) stop();
9838
+ else io.signal.addEventListener("abort", stop, { once: true });
9839
+ else {
9840
+ process.once("SIGINT", stop);
9841
+ process.once("SIGTERM", stop);
9842
+ }
9843
+ });
9844
+ return 0;
9845
+ }
9846
+ async function appTrigger(type, flags, io) {
9847
+ if (!type) throw new CliError(cli_app_trigger_type_required());
9848
+ let data = {};
9849
+ if (flags.data) try {
9850
+ data = JSON.parse(flags.data);
9851
+ } catch {
9852
+ throw new CliError(cli_app_trigger_data_invalid());
9853
+ }
9854
+ if (flags.order) data = {
9855
+ ...data,
9856
+ orderId: flags.order
9857
+ };
9858
+ const port = Number(flags.port ?? io.env.SAYREN_APP_PORT ?? APP_DEV_PORT);
9859
+ const res = await io.fetch(`http://127.0.0.1:${port}/_trigger`, {
9860
+ method: "POST",
9861
+ headers: { "content-type": "application/json" },
9862
+ body: JSON.stringify({
9863
+ type,
9864
+ data
9865
+ })
9866
+ });
9867
+ if (!res.ok) throw new CliError(cli_app_trigger_failed({ status: String(res.status) }));
9868
+ const result = await res.json();
9869
+ printResult(`[event] ${type}`, result, io);
9870
+ return result.outcome === "OK" ? 0 : 1;
9871
+ }
9872
+ async function appMain(positional, flags, io) {
9873
+ const [sub, ...rest] = positional;
9874
+ switch (sub) {
9875
+ case "init": return appInit(rest[0], flags, io);
9876
+ case "verify": return appVerify(flags, io);
9877
+ case "deploy": return appDeploy(flags, io);
9878
+ case "release": return appRelease(rest[0], flags, io);
9879
+ case "status": return appStatus(flags, io);
9880
+ case "logs": return appLogs(flags, io);
9881
+ case "secret": return appSecret(rest[0], rest[1], flags, io);
9882
+ case "dev-store": return appDevStore(rest[0], rest.slice(1).join(" ") || void 0, flags, io);
9883
+ case "dev": return appDev(flags, io);
9884
+ case "trigger": return appTrigger(rest[0], flags, io);
9885
+ default: throw new CliError(cli_app_subcommand_unknown({ command: sub ?? "" }));
9886
+ }
9887
+ }
9888
+
8215
9889
  //#endregion
8216
9890
  //#region ../mcp/src/rules.ts
8217
9891
  /** 규칙의 심각도 — 없으면 `error` */
@@ -8427,6 +10101,16 @@ const RULES = [
8427
10101
  templateFile: "src/lib/ordering.ts",
8428
10102
  severity: "warning"
8429
10103
  }),
10104
+ defineRule({
10105
+ id: "product-link",
10106
+ title: mcp_rule_product_link_title,
10107
+ why: mcp_rule_product_link_why,
10108
+ fix: mcp_rule_product_link_fix,
10109
+ example: mcp_rule_product_link_example,
10110
+ doc: "/guides/order-numbers",
10111
+ templateFile: "src/lib/product-path.ts",
10112
+ severity: "warning"
10113
+ }),
8430
10114
  defineRule({
8431
10115
  id: "server-only-import",
8432
10116
  title: mcp_rule_server_only_import_title,
@@ -8522,6 +10206,8 @@ function verifySources(files, options = {}) {
8522
10206
  inFile("custom-input-private", file, mcp_verify_custom_input_private({ where: where() }), pattern);
8523
10207
  break;
8524
10208
  }
10209
+ const productIdLink = /params=\{\{\s*productId:\s*[\w.?]+\.productId\s*\}\}/;
10210
+ if (/\/products\/\$productId/.test(file.content) && productIdLink.test(file.content)) inFile("product-link", file, mcp_verify_product_link(), productIdLink);
8525
10211
  const analyticsFactory = /createAnalytics\(/;
8526
10212
  if (isServer$1 && analyticsFactory.test(file.content)) inFile("analytics-start", file, mcp_verify_analytics_start_server_file(), analyticsFactory);
8527
10213
  if (isServer$1 && /createStorefrontClient\(/.test(file.content) && !/analyticsIdsFromCookie|visitorId/.test(file.content)) inFile("analytics-ids", file, mcp_verify_analytics_ids(), /createStorefrontClient\(/);
@@ -10075,7 +11761,13 @@ const VALUE_FLAGS = new Set([
10075
11761
  "api-origin",
10076
11762
  "store",
10077
11763
  "accounts-origin",
10078
- "template"
11764
+ "template",
11765
+ "key",
11766
+ "name",
11767
+ "port",
11768
+ "since",
11769
+ "data",
11770
+ "order"
10079
11771
  ]);
10080
11772
  const BOOL_FLAGS = new Set([
10081
11773
  "force",
@@ -10085,7 +11777,8 @@ const BOOL_FLAGS = new Set([
10085
11777
  "help",
10086
11778
  "version",
10087
11779
  "device",
10088
- "paste"
11780
+ "paste",
11781
+ "tail"
10089
11782
  ]);
10090
11783
  function parseArgs(argv) {
10091
11784
  const out = {
@@ -10097,7 +11790,11 @@ function parseArgs(argv) {
10097
11790
  const arg = argv[i];
10098
11791
  if (arg === "-h") out.flags.help = true;
10099
11792
  else if (arg === "-v") out.flags.version = true;
10100
- else if (arg.startsWith("--")) {
11793
+ else if (arg === "-m") {
11794
+ const value = argv[++i];
11795
+ if (value === void 0 || value.startsWith("--")) throw new CliError(cli_flag_value_required({ name: "message" }));
11796
+ out.flags.message = value;
11797
+ } else if (arg.startsWith("--")) {
10101
11798
  const body = arg.slice(2);
10102
11799
  const eq = body.indexOf("=");
10103
11800
  const name = eq < 0 ? body : body.slice(0, eq);
@@ -10142,6 +11839,21 @@ async function main(argv, io) {
10142
11839
  accountsOrigin: str(parsed.flags["accounts-origin"])
10143
11840
  };
10144
11841
  switch (parsed.command) {
11842
+ case "app": return appMain(parsed.positional, {
11843
+ token: str(parsed.flags.token),
11844
+ key: str(parsed.flags.key),
11845
+ apiOrigin: str(parsed.flags["api-origin"]),
11846
+ dir: str(parsed.flags.dir),
11847
+ store: str(parsed.flags.store),
11848
+ message: str(parsed.flags.message),
11849
+ name: str(parsed.flags.name),
11850
+ port: str(parsed.flags.port),
11851
+ since: str(parsed.flags.since),
11852
+ tail: parsed.flags.tail === true,
11853
+ data: str(parsed.flags.data),
11854
+ order: str(parsed.flags.order),
11855
+ yes: parsed.flags.yes === true
11856
+ }, io);
10145
11857
  case "init": return init({
10146
11858
  ...common,
10147
11859
  template: str(parsed.flags.template),