sayren 0.15.0 → 0.16.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 (35) hide show
  1. package/dist/index.mjs +206 -559
  2. package/package.json +4 -4
  3. package/dist/boilerplate/.claude/skills/sayren/SKILL.md +0 -18
  4. package/dist/boilerplate/.mcp.json +0 -8
  5. package/dist/boilerplate/CLAUDE.md +0 -26
  6. package/dist/boilerplate/README.md +0 -12
  7. package/dist/boilerplate/_gitignore +0 -8
  8. package/dist/boilerplate/biome.json +0 -45
  9. package/dist/boilerplate/messages/en.json +0 -9
  10. package/dist/boilerplate/messages/ko.json +0 -9
  11. package/dist/boilerplate/package.json +0 -46
  12. package/dist/boilerplate/public/_headers +0 -5
  13. package/dist/boilerplate/public/favicon.svg +0 -1
  14. package/dist/boilerplate/src/hooks/use-catalog.ts +0 -28
  15. package/dist/boilerplate/src/i18n/index.ts +0 -99
  16. package/dist/boilerplate/src/i18n/locale.server.ts +0 -10
  17. package/dist/boilerplate/src/lib/analytics.ts +0 -60
  18. package/dist/boilerplate/src/lib/api.server.ts +0 -27
  19. package/dist/boilerplate/src/lib/api.ts +0 -7
  20. package/dist/boilerplate/src/lib/config.server.ts +0 -23
  21. package/dist/boilerplate/src/lib/config.test.ts +0 -16
  22. package/dist/boilerplate/src/lib/config.ts +0 -19
  23. package/dist/boilerplate/src/lib/cookies.server.ts +0 -35
  24. package/dist/boilerplate/src/lib/format.ts +0 -19
  25. package/dist/boilerplate/src/lib/queries.ts +0 -61
  26. package/dist/boilerplate/src/routeTree.gen.ts +0 -69
  27. package/dist/boilerplate/src/router.tsx +0 -44
  28. package/dist/boilerplate/src/routes/__root.tsx +0 -119
  29. package/dist/boilerplate/src/routes/index.tsx +0 -52
  30. package/dist/boilerplate/src/start.ts +0 -20
  31. package/dist/boilerplate/src/styles.css +0 -29
  32. package/dist/boilerplate/src/theme.css +0 -38
  33. package/dist/boilerplate/tsconfig.json +0 -16
  34. package/dist/boilerplate/vite.config.ts +0 -12
  35. package/dist/boilerplate/vitest.config.ts +0 -5
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ import { APP_DEPLOY_KEY_PATTERN, APP_README_MAX_BYTES, APP_SECRET_PATTERNS, APP_
6
6
  import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
7
7
  import { createServer } from "node:http";
8
8
  import { dirname, extname, join, relative, resolve, sep } from "node:path";
9
- import { fileURLToPath, pathToFileURL } from "node:url";
9
+ import { pathToFileURL } from "node:url";
10
10
  import { gzipSync } from "node:zlib";
11
11
  import { createHash, randomBytes } from "node:crypto";
12
12
 
@@ -1082,7 +1082,7 @@ const mcp_report_next_reverify_remote = ((inputs = {}, options = {}) => {
1082
1082
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1083
1083
  /** @typedef {{ files: NonNullable<unknown> }} Mcp_Report_Template_Hint_LocalInputs */
1084
1084
  const ko_mcp_report_template_hint_local = (i) => {
1085
- return `손대지 않아야 하는 파일을 고친 결과일 수 있다 — \`get_template_file\`로 ${i?.files}의 원본을 받아 대조한다.`;
1085
+ return `손대지 않아야 하는 파일을 고친 결과일 수 있다 — ${i?.files}을(를) 규칙의 \`example\`이나 git 기록과 대조해 되돌린다.`;
1086
1086
  };
1087
1087
  /**
1088
1088
 
@@ -1100,7 +1100,7 @@ const mcp_report_template_hint_local = ((inputs, options = {}) => {
1100
1100
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1101
1101
  /** @typedef {{ files: NonNullable<unknown> }} Mcp_Report_Template_Hint_RemoteInputs */
1102
1102
  const ko_mcp_report_template_hint_remote = (i) => {
1103
- return `손대지 않아야 하는 파일을 고친 결과일 수 있다 — ${i?.files}을(를) \`read_file\`로 읽어 \`get_template_file\`의 템플릿 원본(지금 카탈로그 버전)이나 규칙의 \`example\`과 대조하고 되돌린다.`;
1103
+ return `손대지 않아야 하는 파일을 고친 결과일 수 있다 — ${i?.files}을(를) \`read_file\`로 읽어 규칙의 \`example\`과 대조하고 되돌린다.`;
1104
1104
  };
1105
1105
  /**
1106
1106
 
@@ -1985,7 +1985,7 @@ const mcp_rule_local_import_exists_example = ((inputs = {}, options = {}) => {
1985
1985
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
1986
1986
  /** @typedef {{}} Mcp_Rule_Local_Import_Exists_FixInputs */
1987
1987
  const ko_mcp_rule_local_import_exists_fix = () => {
1988
- return `빠진 파일을 템플릿 원본(\`get_template_file\`, 또는 \`npx @sayren/mcp create\`로 받은 사본)에서 가져와 같은 경로에 둔다. 호스팅 사이트는 \`write_files\`(\`expectedHash: null\`)나 \`sayren deploy\`로 올린다. 쓰지 않는 import면 지운다. 템플릿의 화면 문구는 \`messages/ko.json\`이다`;
1988
+ return `빠진 파일을 같은 경로에 다시 만든다(git 기록에 있으면 되살린다). 호스팅 사이트는 \`write_files\`(\`expectedHash: null\`)나 \`sayren deploy\`로 올린다. 쓰지 않는 import면 지운다. 화면 문구 카탈로그는 \`messages/ko.json\`이다`;
1989
1989
  };
1990
1990
  /**
1991
1991
 
@@ -2751,42 +2751,6 @@ const mcp_rule_tenant_header_why = ((inputs = {}, options = {}) => {
2751
2751
  return ko_mcp_rule_tenant_header_why(inputs);
2752
2752
  });
2753
2753
 
2754
- //#endregion
2755
- //#region ../mcp/src/paraglide/messages/mcp_template_catalog_version_missing.js
2756
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
2757
- /** @typedef {{ name: NonNullable<unknown> }} Mcp_Template_Catalog_Version_MissingInputs */
2758
- const ko_mcp_template_catalog_version_missing = (i) => {
2759
- return `catalog에 ${i?.name} 버전이 없습니다`;
2760
- };
2761
- /**
2762
-
2763
- * @param {Mcp_Template_Catalog_Version_MissingInputs} inputs
2764
- * @param {{ locale?: "ko" }} options
2765
- * @returns {LocalizedString}
2766
- */
2767
- const mcp_template_catalog_version_missing = ((inputs, options = {}) => {
2768
- experimentalStaticLocale$1 ?? options.locale ?? getLocale$1();
2769
- return ko_mcp_template_catalog_version_missing(inputs);
2770
- });
2771
-
2772
- //#endregion
2773
- //#region ../mcp/src/paraglide/messages/mcp_template_published_version_unknown.js
2774
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
2775
- /** @typedef {{ name: NonNullable<unknown> }} Mcp_Template_Published_Version_UnknownInputs */
2776
- const ko_mcp_template_published_version_unknown = (i) => {
2777
- return `${i?.name}의 배포 버전을 모르겠습니다`;
2778
- };
2779
- /**
2780
-
2781
- * @param {Mcp_Template_Published_Version_UnknownInputs} inputs
2782
- * @param {{ locale?: "ko" }} options
2783
- * @returns {LocalizedString}
2784
- */
2785
- const mcp_template_published_version_unknown = ((inputs, options = {}) => {
2786
- experimentalStaticLocale$1 ?? options.locale ?? getLocale$1();
2787
- return ko_mcp_template_published_version_unknown(inputs);
2788
- });
2789
-
2790
2754
  //#endregion
2791
2755
  //#region ../mcp/src/paraglide/messages/mcp_verify_analytics_ids.js
2792
2756
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -3519,7 +3483,7 @@ function lazyMessages(table) {
3519
3483
  /**
3520
3484
  * 셀러 프로젝트의 `.mcp.json`(Claude Code 형식) — 원격 MCP(`mcp.sayren.io`)에 붙는 `sayren` 항목을 만들고 읽는다.
3521
3485
  *
3522
- * `npx sayren pull`·`login`(packages/cli)과 `sayren-mcp create`가 같은 함수로 쓴다. 파일 입출력은 부르는 쪽이 하고 여기는 문자열만
3486
+ * `npx sayren pull`·`link`·`login`(packages/cli)이 같은 함수로 쓴다. 파일 입출력은 부르는 쪽이 하고 여기는 문자열만
3523
3487
  * 다룬다 — 게이트웨이(Worker)가 헤더 이름을 이 파일에서 가져가므로 Node 전용 API를 쓰지 않는다.
3524
3488
  *
3525
3489
  * - **토큰을 쓰지 않는다(#71)** — 항목은 주소와 `X-Store-Id`뿐이고 MCP 클라이언트가 게이트웨이의 401 + `resource_metadata`를 보고
@@ -3609,7 +3573,7 @@ function sayrenStoreIdOf(text) {
3609
3573
  //#endregion
3610
3574
  //#region ../mcp/src/mcp-json-file.ts
3611
3575
  /**
3612
- * `.mcp.json` 파일 쓰기(Node) — `sayren` CLI(packages/cli)와 `sayren-mcp create`가 같이 쓴다. 형식은 `mcp-json.ts`(Node 없음)다.
3576
+ * `.mcp.json` 파일 쓰기(Node) — `sayren` CLI(packages/cli)가 쓴다. 형식은 `mcp-json.ts`(Node 없음)다.
3613
3577
  * 항목에 토큰을 쓰지 않는다(#71 R1). 심볼릭 링크는 따라가지 않는다(폴더 밖 파일을 덮지 않게).
3614
3578
  */
3615
3579
  function git(dir, args) {
@@ -5511,24 +5475,6 @@ const cli_checks_lock_drift = ((inputs, options = {}) => {
5511
5475
  return ko_cli_checks_lock_drift(inputs);
5512
5476
  });
5513
5477
 
5514
- //#endregion
5515
- //#region src/paraglide/messages/cli_checks_lock_missing.js
5516
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5517
- /** @typedef {{}} Cli_Checks_Lock_MissingInputs */
5518
- const ko_cli_checks_lock_missing = () => {
5519
- return `pnpm-lock.yaml이 없습니다. 호스팅 빌드의 설치가 실패할 수 있습니다`;
5520
- };
5521
- /**
5522
-
5523
- * @param {Cli_Checks_Lock_MissingInputs} inputs
5524
- * @param {{ locale?: "ko" }} options
5525
- * @returns {LocalizedString}
5526
- */
5527
- const cli_checks_lock_missing = ((inputs = {}, options = {}) => {
5528
- experimentalStaticLocale ?? options.locale ?? getLocale();
5529
- return ko_cli_checks_lock_missing(inputs);
5530
- });
5531
-
5532
5478
  //#endregion
5533
5479
  //#region src/paraglide/messages/cli_checks_summary.js
5534
5480
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -5586,14 +5532,14 @@ const cli_create_dir_not_empty = ((inputs, options = {}) => {
5586
5532
  //#endregion
5587
5533
  //#region src/paraglide/messages/cli_create_done.js
5588
5534
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5589
- /** @typedef {{ dir: NonNullable<unknown>, count: NonNullable<unknown> }} Cli_Create_DoneInputs */
5535
+ /** @typedef {{ dir: NonNullable<unknown>, count: NonNullable<unknown>, install: NonNullable<unknown>, dev: NonNullable<unknown> }} Cli_Create_DoneInputs */
5590
5536
  const ko_cli_create_done = (i) => {
5591
5537
  return `시작 코드를 만들었습니다 — ${i?.dir} (파일 ${i?.count}개)
5592
5538
  다음:
5593
5539
  cd ${i?.dir}
5594
- npm install
5540
+ ${i?.install}
5595
5541
  npx sayren link # 상점에 연결하고 .env에 상점 값을 씁니다
5596
- npm run dev`;
5542
+ ${i?.dev}`;
5597
5543
  };
5598
5544
  /**
5599
5545
 
@@ -5609,13 +5555,13 @@ const cli_create_done = ((inputs, options = {}) => {
5609
5555
  //#endregion
5610
5556
  //#region src/paraglide/messages/cli_create_done_store_code.js
5611
5557
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5612
- /** @typedef {{ dir: NonNullable<unknown>, count: NonNullable<unknown>, storeCode: NonNullable<unknown> }} Cli_Create_Done_Store_CodeInputs */
5558
+ /** @typedef {{ dir: NonNullable<unknown>, count: NonNullable<unknown>, storeCode: NonNullable<unknown>, install: NonNullable<unknown>, dev: NonNullable<unknown> }} Cli_Create_Done_Store_CodeInputs */
5613
5559
  const ko_cli_create_done_store_code = (i) => {
5614
5560
  return `시작 코드를 만들었습니다 — ${i?.dir} (파일 ${i?.count}개, .env에 상점 ${i?.storeCode} 연결 값)
5615
5561
  다음:
5616
5562
  cd ${i?.dir}
5617
- npm install
5618
- npm run dev
5563
+ ${i?.install}
5564
+ ${i?.dev}
5619
5565
  npx sayren deploy # 처음이면 상점 사이트에 연결한 뒤 배포합니다`;
5620
5566
  };
5621
5567
  /**
@@ -5647,6 +5593,42 @@ const cli_create_name_invalid = ((inputs, options = {}) => {
5647
5593
  return ko_cli_create_name_invalid(inputs);
5648
5594
  });
5649
5595
 
5596
+ //#endregion
5597
+ //#region src/paraglide/messages/cli_create_starter_no_release.js
5598
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5599
+ /** @typedef {{ status: NonNullable<unknown> }} Cli_Create_Starter_No_ReleaseInputs */
5600
+ const ko_cli_create_starter_no_release = (i) => {
5601
+ return `시작 코드의 최신 릴리스를 찾지 못했습니다(GitHub avarcorp/sayren-starter, HTTP ${i?.status}). 잠시 뒤 다시 시도하십시오`;
5602
+ };
5603
+ /**
5604
+
5605
+ * @param {Cli_Create_Starter_No_ReleaseInputs} inputs
5606
+ * @param {{ locale?: "ko" }} options
5607
+ * @returns {LocalizedString}
5608
+ */
5609
+ const cli_create_starter_no_release = ((inputs, options = {}) => {
5610
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5611
+ return ko_cli_create_starter_no_release(inputs);
5612
+ });
5613
+
5614
+ //#endregion
5615
+ //#region src/paraglide/messages/cli_create_starter_unreachable.js
5616
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5617
+ /** @typedef {{}} Cli_Create_Starter_UnreachableInputs */
5618
+ const ko_cli_create_starter_unreachable = () => {
5619
+ return `시작 코드의 최신 버전을 확인하지 못했습니다. 시작 코드는 GitHub(avarcorp/sayren-starter)에서 받으므로 네트워크 연결을 확인하십시오`;
5620
+ };
5621
+ /**
5622
+
5623
+ * @param {Cli_Create_Starter_UnreachableInputs} inputs
5624
+ * @param {{ locale?: "ko" }} options
5625
+ * @returns {LocalizedString}
5626
+ */
5627
+ const cli_create_starter_unreachable = ((inputs = {}, options = {}) => {
5628
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5629
+ return ko_cli_create_starter_unreachable(inputs);
5630
+ });
5631
+
5650
5632
  //#endregion
5651
5633
  //#region src/paraglide/messages/cli_create_store_code_invalid.js
5652
5634
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -6029,9 +6011,9 @@ const cli_deploy_build_local_failed = ((inputs, options = {}) => {
6029
6011
  //#endregion
6030
6012
  //#region src/paraglide/messages/cli_deploy_build_local_no_deps.js
6031
6013
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6032
- /** @typedef {{}} Cli_Deploy_Build_Local_No_DepsInputs */
6033
- const ko_cli_deploy_build_local_no_deps = () => {
6034
- return `의존성이 설치되지 않아(node_modules 없음) 로컬 빌드를 건너뜁니다. npm install --ignore-scripts 뒤 배포하면 올리기 전에 빌드를 확인합니다`;
6014
+ /** @typedef {{ install: NonNullable<unknown> }} Cli_Deploy_Build_Local_No_DepsInputs */
6015
+ const ko_cli_deploy_build_local_no_deps = (i) => {
6016
+ return `의존성이 설치되지 않아(node_modules 없음) 로컬 빌드를 건너뜁니다. ${i?.install} 뒤 배포하면 올리기 전에 빌드를 확인합니다`;
6035
6017
  };
6036
6018
  /**
6037
6019
 
@@ -6039,7 +6021,7 @@ const ko_cli_deploy_build_local_no_deps = () => {
6039
6021
  * @param {{ locale?: "ko" }} options
6040
6022
  * @returns {LocalizedString}
6041
6023
  */
6042
- const cli_deploy_build_local_no_deps = ((inputs = {}, options = {}) => {
6024
+ const cli_deploy_build_local_no_deps = ((inputs, options = {}) => {
6043
6025
  experimentalStaticLocale ?? options.locale ?? getLocale();
6044
6026
  return ko_cli_deploy_build_local_no_deps(inputs);
6045
6027
  });
@@ -6874,274 +6856,21 @@ const cli_flag_value_required = ((inputs, options = {}) => {
6874
6856
  });
6875
6857
 
6876
6858
  //#endregion
6877
- //#region src/paraglide/messages/cli_init_confirm.js
6878
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6879
- /** @typedef {{}} Cli_Init_ConfirmInputs */
6880
- const ko_cli_init_confirm = () => {
6881
- return `사이트를 만드시겠습니까?`;
6882
- };
6883
- /**
6884
-
6885
- * @param {Cli_Init_ConfirmInputs} inputs
6886
- * @param {{ locale?: "ko" }} options
6887
- * @returns {LocalizedString}
6888
- */
6889
- const cli_init_confirm = ((inputs = {}, options = {}) => {
6890
- experimentalStaticLocale ?? options.locale ?? getLocale();
6891
- return ko_cli_init_confirm(inputs);
6892
- });
6893
-
6894
- //#endregion
6895
- //#region src/paraglide/messages/cli_init_created.js
6859
+ //#region src/paraglide/messages/cli_init_retired.js
6896
6860
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6897
- /** @typedef {{ url: NonNullable<unknown> }} Cli_Init_CreatedInputs */
6898
- const ko_cli_init_created = (i) => {
6899
- return `사이트를 만들었습니다 — ${i?.url} (오픈 준비 중, 반영까지 1분 안팎)`;
6861
+ /** @typedef {{}} Cli_Init_RetiredInputs */
6862
+ const ko_cli_init_retired = () => {
6863
+ return `npx sayren init은 더는 사이트를 만들지 않습니다. npx sayren create <이름>으로 시작 코드를 만든 뒤 그 폴더에서 npx sayren link로 사이트를 만들고 연결하십시오`;
6900
6864
  };
6901
6865
  /**
6902
6866
 
6903
- * @param {Cli_Init_CreatedInputs} inputs
6867
+ * @param {Cli_Init_RetiredInputs} inputs
6904
6868
  * @param {{ locale?: "ko" }} options
6905
6869
  * @returns {LocalizedString}
6906
6870
  */
6907
- const cli_init_created = ((inputs, options = {}) => {
6871
+ const cli_init_retired = ((inputs = {}, options = {}) => {
6908
6872
  experimentalStaticLocale ?? options.locale ?? getLocale();
6909
- return ko_cli_init_created(inputs);
6910
- });
6911
-
6912
- //#endregion
6913
- //#region src/paraglide/messages/cli_init_dir_not_empty.js
6914
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6915
- /** @typedef {{ dir: NonNullable<unknown> }} Cli_Init_Dir_Not_EmptyInputs */
6916
- const ko_cli_init_dir_not_empty = (i) => {
6917
- return `폴더가 비어 있지 않습니다: ${i?.dir}. 빈 폴더나 새 폴더 이름을 주십시오`;
6918
- };
6919
- /**
6920
-
6921
- * @param {Cli_Init_Dir_Not_EmptyInputs} inputs
6922
- * @param {{ locale?: "ko" }} options
6923
- * @returns {LocalizedString}
6924
- */
6925
- const cli_init_dir_not_empty = ((inputs, options = {}) => {
6926
- experimentalStaticLocale ?? options.locale ?? getLocale();
6927
- return ko_cli_init_dir_not_empty(inputs);
6928
- });
6929
-
6930
- //#endregion
6931
- //#region src/paraglide/messages/cli_init_inside_pulled.js
6932
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6933
- /** @typedef {{}} Cli_Init_Inside_PulledInputs */
6934
- const ko_cli_init_inside_pulled = () => {
6935
- return `지금 폴더는 이미 받은 사이트 폴더입니다. 새 사이트는 다른 폴더에서 npx sayren init <폴더>로 만들고, 이 사이트를 다시 받으려면 npx sayren pull --force를 쓰십시오`;
6936
- };
6937
- /**
6938
-
6939
- * @param {Cli_Init_Inside_PulledInputs} inputs
6940
- * @param {{ locale?: "ko" }} options
6941
- * @returns {LocalizedString}
6942
- */
6943
- const cli_init_inside_pulled = ((inputs = {}, options = {}) => {
6944
- experimentalStaticLocale ?? options.locale ?? getLocale();
6945
- return ko_cli_init_inside_pulled(inputs);
6946
- });
6947
-
6948
- //#endregion
6949
- //#region src/paraglide/messages/cli_init_no_templates.js
6950
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6951
- /** @typedef {{}} Cli_Init_No_TemplatesInputs */
6952
- const ko_cli_init_no_templates = () => {
6953
- return `쓸 수 있는 템플릿이 없습니다`;
6954
- };
6955
- /**
6956
-
6957
- * @param {Cli_Init_No_TemplatesInputs} inputs
6958
- * @param {{ locale?: "ko" }} options
6959
- * @returns {LocalizedString}
6960
- */
6961
- const cli_init_no_templates = ((inputs = {}, options = {}) => {
6962
- experimentalStaticLocale ?? options.locale ?? getLocale();
6963
- return ko_cli_init_no_templates(inputs);
6964
- });
6965
-
6966
- //#endregion
6967
- //#region src/paraglide/messages/cli_init_pull_failed.js
6968
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6969
- /** @typedef {{ storeId: NonNullable<unknown> }} Cli_Init_Pull_FailedInputs */
6970
- const ko_cli_init_pull_failed = (i) => {
6971
- return `사이트는 만들었지만 소스를 받지 못했습니다. 다시 만들지 말고 npx sayren pull --project ${i?.storeId}로 받으십시오`;
6972
- };
6973
- /**
6974
-
6975
- * @param {Cli_Init_Pull_FailedInputs} inputs
6976
- * @param {{ locale?: "ko" }} options
6977
- * @returns {LocalizedString}
6978
- */
6979
- const cli_init_pull_failed = ((inputs, options = {}) => {
6980
- experimentalStaticLocale ?? options.locale ?? getLocale();
6981
- return ko_cli_init_pull_failed(inputs);
6982
- });
6983
-
6984
- //#endregion
6985
- //#region src/paraglide/messages/cli_init_site_exists.js
6986
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6987
- /** @typedef {{ storeId: NonNullable<unknown> }} Cli_Init_Site_ExistsInputs */
6988
- const ko_cli_init_site_exists = (i) => {
6989
- return `이 상점에는 이미 호스팅 사이트가 있어 새로 만들지 않았습니다. npx sayren pull --project ${i?.storeId}로 소스를 받으십시오`;
6990
- };
6991
- /**
6992
-
6993
- * @param {Cli_Init_Site_ExistsInputs} inputs
6994
- * @param {{ locale?: "ko" }} options
6995
- * @returns {LocalizedString}
6996
- */
6997
- const cli_init_site_exists = ((inputs, options = {}) => {
6998
- experimentalStaticLocale ?? options.locale ?? getLocale();
6999
- return ko_cli_init_site_exists(inputs);
7000
- });
7001
-
7002
- //#endregion
7003
- //#region src/paraglide/messages/cli_init_site_url.js
7004
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7005
- /** @typedef {{ url: NonNullable<unknown> }} Cli_Init_Site_UrlInputs */
7006
- const ko_cli_init_site_url = (i) => {
7007
- return `지금 사이트: ${i?.url}`;
7008
- };
7009
- /**
7010
-
7011
- * @param {Cli_Init_Site_UrlInputs} inputs
7012
- * @param {{ locale?: "ko" }} options
7013
- * @returns {LocalizedString}
7014
- */
7015
- const cli_init_site_url = ((inputs, options = {}) => {
7016
- experimentalStaticLocale ?? options.locale ?? getLocale();
7017
- return ko_cli_init_site_url(inputs);
7018
- });
7019
-
7020
- //#endregion
7021
- //#region src/paraglide/messages/cli_init_slug_taken.js
7022
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7023
- /** @typedef {{}} Cli_Init_Slug_TakenInputs */
7024
- const ko_cli_init_slug_taken = () => {
7025
- return `상점 코드와 같은 주소를 다른 사이트가 쓰고 있어 만들지 못했습니다. 상점 플랫폼 › 스토어프론트에서 다른 주소로 만든 뒤 npx sayren pull로 받으십시오`;
7026
- };
7027
- /**
7028
-
7029
- * @param {Cli_Init_Slug_TakenInputs} inputs
7030
- * @param {{ locale?: "ko" }} options
7031
- * @returns {LocalizedString}
7032
- */
7033
- const cli_init_slug_taken = ((inputs = {}, options = {}) => {
7034
- experimentalStaticLocale ?? options.locale ?? getLocale();
7035
- return ko_cli_init_slug_taken(inputs);
7036
- });
7037
-
7038
- //#endregion
7039
- //#region src/paraglide/messages/cli_init_summary.js
7040
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7041
- /** @typedef {{ storeId: NonNullable<unknown>, name: NonNullable<unknown>, templateId: NonNullable<unknown>, version: NonNullable<unknown> }} Cli_Init_SummaryInputs */
7042
- const ko_cli_init_summary = (i) => {
7043
- return `상점 ${i?.storeId}에 템플릿 ${i?.name}(${i?.templateId} v${i?.version})로 호스팅 사이트를 만듭니다. 주소는 상점 코드이고 오픈 준비 중으로 시작합니다. 만들기는 상점당 30일에 3번까지입니다`;
7044
- };
7045
- /**
7046
-
7047
- * @param {Cli_Init_SummaryInputs} inputs
7048
- * @param {{ locale?: "ko" }} options
7049
- * @returns {LocalizedString}
7050
- */
7051
- const cli_init_summary = ((inputs, options = {}) => {
7052
- experimentalStaticLocale ?? options.locale ?? getLocale();
7053
- return ko_cli_init_summary(inputs);
7054
- });
7055
-
7056
- //#endregion
7057
- //#region src/paraglide/messages/cli_init_template_option.js
7058
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7059
- /** @typedef {{ templateId: NonNullable<unknown>, name: NonNullable<unknown>, version: NonNullable<unknown>, description: NonNullable<unknown> }} Cli_Init_Template_OptionInputs */
7060
- const ko_cli_init_template_option = (i) => {
7061
- return `${i?.templateId} — ${i?.name} v${i?.version}: ${i?.description}`;
7062
- };
7063
- /**
7064
-
7065
- * @param {Cli_Init_Template_OptionInputs} inputs
7066
- * @param {{ locale?: "ko" }} options
7067
- * @returns {LocalizedString}
7068
- */
7069
- const cli_init_template_option = ((inputs, options = {}) => {
7070
- experimentalStaticLocale ?? options.locale ?? getLocale();
7071
- return ko_cli_init_template_option(inputs);
7072
- });
7073
-
7074
- //#endregion
7075
- //#region src/paraglide/messages/cli_init_template_prompt.js
7076
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7077
- /** @typedef {{}} Cli_Init_Template_PromptInputs */
7078
- const ko_cli_init_template_prompt = () => {
7079
- return `템플릿을 고르십시오:`;
7080
- };
7081
- /**
7082
-
7083
- * @param {Cli_Init_Template_PromptInputs} inputs
7084
- * @param {{ locale?: "ko" }} options
7085
- * @returns {LocalizedString}
7086
- */
7087
- const cli_init_template_prompt = ((inputs = {}, options = {}) => {
7088
- experimentalStaticLocale ?? options.locale ?? getLocale();
7089
- return ko_cli_init_template_prompt(inputs);
7090
- });
7091
-
7092
- //#endregion
7093
- //#region src/paraglide/messages/cli_init_template_required.js
7094
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7095
- /** @typedef {{ list: NonNullable<unknown> }} Cli_Init_Template_RequiredInputs */
7096
- const ko_cli_init_template_required = (i) => {
7097
- return `템플릿을 고르지 않았습니다. --template <id>로 주십시오(비대화형은 필수입니다):
7098
- ${i?.list}`;
7099
- };
7100
- /**
7101
-
7102
- * @param {Cli_Init_Template_RequiredInputs} inputs
7103
- * @param {{ locale?: "ko" }} options
7104
- * @returns {LocalizedString}
7105
- */
7106
- const cli_init_template_required = ((inputs, options = {}) => {
7107
- experimentalStaticLocale ?? options.locale ?? getLocale();
7108
- return ko_cli_init_template_required(inputs);
7109
- });
7110
-
7111
- //#endregion
7112
- //#region src/paraglide/messages/cli_init_template_unknown.js
7113
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7114
- /** @typedef {{ templateId: NonNullable<unknown>, ids: NonNullable<unknown> }} Cli_Init_Template_UnknownInputs */
7115
- const ko_cli_init_template_unknown = (i) => {
7116
- return `템플릿을 찾지 못했습니다: ${i?.templateId}. 쓸 수 있는 템플릿: ${i?.ids}`;
7117
- };
7118
- /**
7119
-
7120
- * @param {Cli_Init_Template_UnknownInputs} inputs
7121
- * @param {{ locale?: "ko" }} options
7122
- * @returns {LocalizedString}
7123
- */
7124
- const cli_init_template_unknown = ((inputs, options = {}) => {
7125
- experimentalStaticLocale ?? options.locale ?? getLocale();
7126
- return ko_cli_init_template_unknown(inputs);
7127
- });
7128
-
7129
- //#endregion
7130
- //#region src/paraglide/messages/cli_init_yes_required.js
7131
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7132
- /** @typedef {{}} Cli_Init_Yes_RequiredInputs */
7133
- const ko_cli_init_yes_required = () => {
7134
- return `사이트 만들기를 확인할 수 없는 환경입니다. --yes를 함께 주십시오`;
7135
- };
7136
- /**
7137
-
7138
- * @param {Cli_Init_Yes_RequiredInputs} inputs
7139
- * @param {{ locale?: "ko" }} options
7140
- * @returns {LocalizedString}
7141
- */
7142
- const cli_init_yes_required = ((inputs = {}, options = {}) => {
7143
- experimentalStaticLocale ?? options.locale ?? getLocale();
7144
- return ko_cli_init_yes_required(inputs);
6873
+ return ko_cli_init_retired(inputs);
7145
6874
  });
7146
6875
 
7147
6876
  //#endregion
@@ -7327,11 +7056,11 @@ const cli_link_gitignore = ((inputs, options = {}) => {
7327
7056
  //#endregion
7328
7057
  //#region src/paraglide/messages/cli_link_next_steps.js
7329
7058
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7330
- /** @typedef {{}} Cli_Link_Next_StepsInputs */
7331
- const ko_cli_link_next_steps = () => {
7059
+ /** @typedef {{ install: NonNullable<unknown>, dev: NonNullable<unknown> }} Cli_Link_Next_StepsInputs */
7060
+ const ko_cli_link_next_steps = (i) => {
7332
7061
  return `다음:
7333
- npm install
7334
- npm run dev # http://localhost:4010 — 운영 상점 데이터로 동작합니다
7062
+ ${i?.install} # 의존성의 설치 스크립트를 돌리지 않습니다
7063
+ ${i?.dev} # http://localhost:4010 — 운영 상점 데이터로 동작합니다
7335
7064
  npx sayren deploy # 빌드 → 버전 → 발행`;
7336
7065
  };
7337
7066
  /**
@@ -7340,7 +7069,7 @@ const ko_cli_link_next_steps = () => {
7340
7069
  * @param {{ locale?: "ko" }} options
7341
7070
  * @returns {LocalizedString}
7342
7071
  */
7343
- const cli_link_next_steps = ((inputs = {}, options = {}) => {
7072
+ const cli_link_next_steps = ((inputs, options = {}) => {
7344
7073
  experimentalStaticLocale ?? options.locale ?? getLocale();
7345
7074
  return ko_cli_link_next_steps(inputs);
7346
7075
  });
@@ -8266,13 +7995,13 @@ const cli_pull_mcp_written = ((inputs, options = {}) => {
8266
7995
  //#endregion
8267
7996
  //#region src/paraglide/messages/cli_pull_next_steps.js
8268
7997
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
8269
- /** @typedef {{ dir: NonNullable<unknown> }} Cli_Pull_Next_StepsInputs */
7998
+ /** @typedef {{ dir: NonNullable<unknown>, install: NonNullable<unknown>, dev: NonNullable<unknown> }} Cli_Pull_Next_StepsInputs */
8270
7999
  const ko_cli_pull_next_steps = (i) => {
8271
8000
  return `
8272
8001
  다음:
8273
8002
  cd ${i?.dir}
8274
- npm install --ignore-scripts # 의존성의 설치 스크립트를 돌리지 않습니다
8275
- npm run dev # http://localhost:4010 — 운영 상점 데이터로 동작합니다
8003
+ ${i?.install} # 의존성의 설치 스크립트를 돌리지 않습니다
8004
+ ${i?.dev} # http://localhost:4010 — 운영 상점 데이터로 동작합니다
8276
8005
  npx sayren deploy # 빌드 → 버전 → 발행`;
8277
8006
  };
8278
8007
  /**
@@ -8291,7 +8020,7 @@ const cli_pull_next_steps = ((inputs, options = {}) => {
8291
8020
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
8292
8021
  /** @typedef {{}} Cli_Pull_No_SiteInputs */
8293
8022
  const ko_cli_pull_no_site = () => {
8294
- return `이 상점에는 호스팅 사이트가 없습니다. npx sayren init으로 만들거나 상점 플랫폼 › 스토어프론트에서 만든 뒤 다시 받으십시오`;
8023
+ return `이 상점에는 호스팅 사이트가 없습니다. npx sayren link로 만들거나 상점 플랫폼 › 스토어프론트에서 만든 뒤 다시 받으십시오`;
8295
8024
  };
8296
8025
  /**
8297
8026
 
@@ -8397,9 +8126,9 @@ const cli_pull_store_mismatch = ((inputs, options = {}) => {
8397
8126
  //#endregion
8398
8127
  //#region src/paraglide/messages/cli_pull_watched_changed.js
8399
8128
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
8400
- /** @typedef {{ path: NonNullable<unknown> }} Cli_Pull_Watched_ChangedInputs */
8129
+ /** @typedef {{ path: NonNullable<unknown>, install: NonNullable<unknown> }} Cli_Pull_Watched_ChangedInputs */
8401
8130
  const ko_cli_pull_watched_changed = (i) => {
8402
- return `알림: 원격의 ${i?.path}가 이 폴더와 다릅니다. 설치 전에 내용을 확인하십시오(npm install --ignore-scripts 권장)`;
8131
+ return `알림: 원격의 ${i?.path}가 이 폴더와 다릅니다. 설치 전에 내용을 확인하십시오(${i?.install} 권장)`;
8403
8132
  };
8404
8133
  /**
8405
8134
 
@@ -9373,7 +9102,6 @@ const ko_cli_usage = (i) => {
9373
9102
  npx sayren create <이름> [--template <템플릿>] # 시작 코드를 새 폴더에 만듭니다(파일만, 로그인 없음)
9374
9103
  npx sayren link [폴더] [--store <storeId>] # 폴더를 상점 사이트에 연결합니다(없으면 사이트를 만듭니다)
9375
9104
  npx sayren env pull # 연결된 상점 값을 .env에 씁니다
9376
- npx sayren init [폴더] [--template <id>] [--store <storeId>] [--yes]
9377
9105
  npx sayren pull --project <storeId> [폴더] [--force [--yes]]
9378
9106
  npx sayren deploy [--no-publish] [--skip-build] [--message <메모>] [--force [--yes]]
9379
9107
  npx sayren status
@@ -9388,7 +9116,6 @@ const ko_cli_usage = (i) => {
9388
9116
  create 시작 코드(상점 이름·상품 목록 홈, SDK·문구·분석·에이전트 안내)를 <이름> 폴더에 만듭니다. 상점을 묻지 않고 호스팅 사이트도 만들지 않습니다
9389
9117
  link 상점을 고르고(--store → 토큰 범위 → 선택) 사이트가 없으면 만든 뒤 .sayren/project.json·.env·.mcp.json을 씁니다
9390
9118
  env env pull: 연결된 상점의 SAYREN_STORE_CODE·SAYREN_API_URL을 .env에 씁니다
9391
- init 템플릿을 골라 호스팅 사이트를 만들고 pull과 같은 폴더(기본: 상점 코드)로 받습니다. 사이트가 이미 있으면 만들지 않습니다
9392
9119
  pull 호스팅 중인 사이트의 작업 트리를 폴더(기본: 상점 코드)로 받고 .mcp.json·.env·.sayren/project.json을 씁니다
9393
9120
  deploy 로컬 검사 → 로컬 빌드 → 작업 트리 올리기 → 빌드 → 발행. 연결이 없으면 link부터 하고 다음 버전으로 교체합니다. 원격이 마지막 받기·올리기 이후 바뀌었으면 덮어쓰지 않습니다
9394
9121
  status 사이트·발행 버전·최근 빌드·편집 환경
@@ -9404,12 +9131,11 @@ const ko_cli_usage = (i) => {
9404
9131
  옵션:
9405
9132
  --project <storeId> pull: 받을 상점(--store와 같습니다)
9406
9133
  --template <템플릿> create: github:owner/repo[/경로][#ref](공개 GitHub 저장소) 또는 템플릿 id. 없으면 CLI에 든 시작 코드
9407
- init: 호스팅 템플릿 id(비대화형은 필수)
9408
9134
  --store-code <코드> create: 이 상점 코드로 .env를 씁니다(없으면 link·env pull이 씁니다)
9409
- --dir <폴더> 작업 폴더(기본: 지금 폴더, pull·init은 상점 코드)
9135
+ --dir <폴더> 작업 폴더(기본: 지금 폴더, pull은 상점 코드)
9410
9136
  --force pull: 같은 사이트를 받은 폴더에 원격을 다시 받습니다(지울·덮을 파일을 보여 주고 묻습니다)
9411
9137
  deploy: 원격 변경·열린 편집 환경을 무시하고 덮어씁니다(확인을 묻습니다)
9412
- --yes --force·init의 확인을 묻지 않습니다(비대화형)
9138
+ --yes --force의 확인을 묻지 않습니다(비대화형)
9413
9139
  --no-publish deploy: 버전까지만 만들고 발행하지 않습니다
9414
9140
  --skip-build deploy: 올리기 전 로컬 빌드(npm run build)를 건너뜁니다
9415
9141
  --message <메모> deploy: 버전 메모
@@ -10344,7 +10070,7 @@ async function selectStore(auth, client, io, candidates, folderStore = null) {
10344
10070
 
10345
10071
  //#endregion
10346
10072
  //#region package.json
10347
- var version = "0.15.0";
10073
+ var version = "0.16.0";
10348
10074
 
10349
10075
  //#endregion
10350
10076
  //#region src/client.ts
@@ -12222,7 +11948,7 @@ function isNeverUpload(path) {
12222
11948
  return segments.some((segment) => segment.startsWith(".env") || segment === ".sayren" || segment === ".dev.vars" || segment.startsWith(".dev.vars.")) || name === ".mcp.json" || name === ".sayrenrc" || name === ".npmrc";
12223
11949
  }
12224
11950
  /** 걸어 들어가지 않는 폴더 — 설치물·산출물·캐시·VCS(경로 규칙이 어차피 막는다. 크기 때문에 읽지도 않는다) */
12225
- const SKIP_DIRS$1 = new Set([
11951
+ const SKIP_DIRS = new Set([
12226
11952
  "node_modules",
12227
11953
  "dist",
12228
11954
  ".output",
@@ -12243,7 +11969,7 @@ function collectTree(root) {
12243
11969
  const full = join(dir, entry.name);
12244
11970
  const rel = normalizeStorefrontPath(relative(root, full).split(sep).join("/"));
12245
11971
  if (entry.isDirectory()) {
12246
- if (SKIP_DIRS$1.has(entry.name)) continue;
11972
+ if (SKIP_DIRS.has(entry.name)) continue;
12247
11973
  walk(full);
12248
11974
  continue;
12249
11975
  }
@@ -12379,7 +12105,7 @@ function runLocalChecks(tree) {
12379
12105
  if (packageJson && lock) {
12380
12106
  const drift = lockDrift(packageJson, lock);
12381
12107
  if (drift.length) warnings.push(cli_checks_lock_drift({ names: drift.slice(0, 5).join(", ") }));
12382
- } else if (packageJson && !lock) warnings.push(cli_checks_lock_missing());
12108
+ }
12383
12109
  return {
12384
12110
  report: report$1,
12385
12111
  blockingRules,
@@ -12454,7 +12180,8 @@ function lockImporterSpecifiers(lock) {
12454
12180
  //#region src/guide-skill.ts
12455
12181
  /**
12456
12182
  * 받은 폴더의 에이전트 안내 스킬(2026-10-16 사용자 결정) — 셀러의 Claude Code가 기능·화면을 고치기 전에 sayren 중앙 스킬을 `npx sayren skill`로 읽게 한다(스킬은 이 하나만 설치한다).
12457
- * 보일러플레이트(`npx sayren create`, #271)가 싣는 `packages/cli/boilerplate/.claude/skills/sayren/SKILL.md`와 같은 내용이다(`guide-skill.test.ts`가 고정한다).
12183
+ * 이 파일이 원천이다 — `create`는 받은 트리에 늘 이 내용을 쓰고(시작 코드 저장소에는 두지 않는다), `pull`·`init`은 없을 때만 넣는다(`withGuideSkill`).
12184
+ * sayren.io 디스커버리(`apps/website/app/lib/agent-skills.ts`)가 같은 내용을 내준다(`agent-skills.test.ts`가 고정한다).
12458
12185
  */
12459
12186
  const GUIDE_SKILL_PATH = ".claude/skills/sayren/SKILL.md";
12460
12187
  const GUIDE_SKILL = "---\nname: sayren\ndescription: Load before adding, implementing or changing any feature or page of this sayren storefront. Lists the current sayren skills and loads the ones the task needs.\n---\n\n# sayren skills\n\nThe sayren skills are maintained centrally and change without updating this project.\nDo not rely on memory or earlier copies. Load them at the start of each task.\n\n1. List the skills (name and when to use each):\n `npx -y sayren@latest skill list`\n2. Read every skill that matches the task, in full, and follow it:\n `npx -y sayren@latest skill <name>`\n\nIf the sayren MCP server is connected, the tools `list_skills` and `get_skill` return the same content.\n\nFollow the project's `CLAUDE.md` first. The skills add the sayren-specific details.\n";
@@ -12517,6 +12244,60 @@ function tailOf(output, lines = OUTPUT_TAIL_LINES) {
12517
12244
  return output.trimEnd().split(/\r?\n/).slice(-lines).join("\n");
12518
12245
  }
12519
12246
 
12247
+ //#endregion
12248
+ //#region src/package-manager.ts
12249
+ const KNOWN = [
12250
+ "npm",
12251
+ "pnpm",
12252
+ "yarn",
12253
+ "bun"
12254
+ ];
12255
+ const LOCKFILES = [
12256
+ ["pnpm-lock.yaml", "pnpm"],
12257
+ ["yarn.lock", "yarn"],
12258
+ ["bun.lock", "bun"],
12259
+ ["bun.lockb", "bun"],
12260
+ ["package-lock.json", "npm"]
12261
+ ];
12262
+ /** `has(path)`는 그 파일이 있는지, `read(path)`는 텍스트(없으면 null)다 — 받은 트리(Map)와 디스크 폴더가 같은 판정을 쓴다 */
12263
+ function packageManagerOf(source) {
12264
+ const pkg = source.read("package.json");
12265
+ if (pkg) try {
12266
+ const field = JSON.parse(pkg).packageManager;
12267
+ const name = typeof field === "string" ? field.split("@")[0]?.trim() : void 0;
12268
+ if (name && KNOWN.includes(name)) return name;
12269
+ } catch {}
12270
+ for (const [file, manager] of LOCKFILES) if (source.has(file)) return manager;
12271
+ return "npm";
12272
+ }
12273
+ /** 받은 트리(경로 → 내용) */
12274
+ function packageManagerOfFiles(files) {
12275
+ return packageManagerOf({
12276
+ has: (path) => files.has(path),
12277
+ read: (path) => {
12278
+ const bytes = files.get(path);
12279
+ return bytes ? new TextDecoder().decode(bytes) : null;
12280
+ }
12281
+ });
12282
+ }
12283
+ /** 디스크 폴더 */
12284
+ function packageManagerOfDir(dir) {
12285
+ return packageManagerOf({
12286
+ has: (path) => existsSync(join(dir, path)),
12287
+ read: (path) => {
12288
+ try {
12289
+ return readFileSync(join(dir, path), "utf8");
12290
+ } catch {
12291
+ return null;
12292
+ }
12293
+ }
12294
+ });
12295
+ }
12296
+ /** 설치 스크립트를 돌리지 않는 설치 명령 */
12297
+ const installCommand = (manager) => `${manager} install --ignore-scripts`;
12298
+ /** 개발 서버 명령 — npm·bun은 `run`이 있어야 스크립트를 부른다 */
12299
+ const devCommand = (manager) => manager === "npm" || manager === "bun" ? `${manager} run dev` : `${manager} dev`;
12300
+
12520
12301
  //#endregion
12521
12302
  //#region src/commands.ts
12522
12303
  /** 종료 코드 — 0 성공 · 1 실패 · 3 발행이 승인 대기(에이전트 토큰) */
@@ -12638,12 +12419,16 @@ async function receiveSource(client, auth, source, flags, io, migratedDir) {
12638
12419
  if (sha256Hex(tgz) !== source.archive.sha256) throw new CliError(cli_pull_archive_corrupt());
12639
12420
  const files = await unpackArchive(tgz);
12640
12421
  withGuideSkill(files);
12422
+ const manager = packageManagerOfFiles(files);
12641
12423
  const collisions = caseCollisions(files.keys());
12642
12424
  if (collisions.length > 0) throw new CliError(cli_pull_case_collisions({ groups: collisions.map((group) => group.join(" · ")).join(", ") }));
12643
12425
  const diff = diffLocal(target, files);
12644
12426
  if (existing.length > 0) {
12645
12427
  const watched = diff.overwrite.filter((path) => path === "package.json" || path === "vite.config.ts");
12646
- for (const path of watched) io.out(cli_pull_watched_changed({ path }));
12428
+ for (const path of watched) io.out(cli_pull_watched_changed({
12429
+ path,
12430
+ install: installCommand(manager)
12431
+ }));
12647
12432
  if (diff.remove.length + diff.overwrite.length > 0) {
12648
12433
  io.out(cli_pull_replace_summary({
12649
12434
  remove: diff.remove.length,
@@ -12706,81 +12491,19 @@ async function receiveSource(client, auth, source, flags, io, migratedDir) {
12706
12491
  siteId: source.siteId
12707
12492
  }));
12708
12493
  if (source.editSessionOpen) io.out(cli_pull_edit_session_open());
12709
- io.out(cli_pull_next_steps({ dir: pulledDir }));
12494
+ io.out(cli_pull_next_steps({
12495
+ dir: pulledDir,
12496
+ install: installCommand(manager),
12497
+ dev: devCommand(manager)
12498
+ }));
12710
12499
  return 0;
12711
12500
  }
12712
12501
  /**
12713
- * `sayren init [폴더]`(#101) — 템플릿을 골라 호스팅 사이트를 만들고(`POST /v1/storefront/site`) `pull`과 같은 폴더를 만든다.
12714
- * 사이트가 이미 있으면 만들지 않고 `pull`을 안내한다(상점당 열린 사이트는 하나다). 만들기는 상점 한도(30일 3회)를 쓰므로 확인을 묻는다.
12715
- * 토큰은 `storefront:rw`가 있어야 한다(로그인 토큰 `CLI_LOGIN_SCOPE`에 있다). 에이전트 토큰이어도 만들기는 승인 대상이 아니다
12716
- * (쓰기 정책 표에 없다 — 오픈 준비 중으로 시작해 구매자 화면을 바꾸지 않는다).
12502
+ * `sayren init`(#101)은 템플릿을 골라 호스팅 사이트를 만들던 명령이다. 템플릿 사이트를 만들지 않으므로(#315 2차, 공개 API 템플릿 삭제)
12503
+ * 사이트를 만들지 않고 시작 경로(`create` → `link`)를 안내하고 끝낸다
12717
12504
  */
12718
- async function init(flags, io) {
12719
- const target = flags.dir ? resolve(io.cwd, flags.dir) : null;
12720
- if (target && existsSync(target) && readdirSync(target).length > 0) throw new CliError(cli_init_dir_not_empty({ dir: relativeOf(io, target) }));
12721
- if (!target && isPulledDir(io.cwd)) throw new CliError(cli_init_inside_pulled());
12722
- const authDir = target && existsSync(target) ? target : io.cwd;
12723
- const { auth, client } = await apiFor(flags, authDir, null, io);
12724
- const storeId = auth.storeId ?? "<storeId>";
12725
- const hosting = await client.storefront.get();
12726
- if (hosting.site) {
12727
- io.out(cli_init_site_url({ url: hosting.site.url }));
12728
- throw new CliError(cli_init_site_exists({ storeId }));
12729
- }
12730
- if (!hosting.editingEnabled) throw new CliError(cli_pull_editing_disabled());
12731
- const template = await pickTemplate(await client.storefront.listTemplates(), flags.template, io);
12732
- io.out(cli_init_summary({
12733
- name: template.name,
12734
- templateId: template.templateId,
12735
- version: template.version,
12736
- storeId
12737
- }));
12738
- if (!flags.yes) {
12739
- if (!io.confirm) throw new CliError(cli_init_yes_required());
12740
- if (!await io.confirm(cli_init_confirm())) {
12741
- io.out(cli_cancelled());
12742
- return 1;
12743
- }
12744
- }
12745
- let site;
12746
- try {
12747
- site = await client.storefront.create({ templateId: template.templateId });
12748
- } catch (error) {
12749
- const code = codeOf(error);
12750
- if (code === "STOREFRONT_SITE_EXISTS") throw new CliError(cli_init_site_exists({ storeId }));
12751
- if (code === "STOREFRONT_SLUG_TAKEN") throw new CliError(cli_init_slug_taken());
12752
- throw error;
12753
- }
12754
- io.out(cli_init_created({ url: site.url }));
12755
- try {
12756
- return await receiveSource(client, auth, await client.storefront.getSource(), { dir: flags.dir }, io, authDir);
12757
- } catch (error) {
12758
- io.err(cli_init_pull_failed({ storeId }));
12759
- throw error;
12760
- }
12761
- }
12762
- /** `--template`이 있으면 그 템플릿, 없으면 대화형 선택. 비대화형이면 `--template`이 있어야 한다 */
12763
- async function pickTemplate(templates, wanted, io) {
12764
- if (templates.length === 0) throw new CliError(cli_init_no_templates());
12765
- const ids = templates.map((t) => t.templateId).join(", ");
12766
- if (wanted) {
12767
- const found = templates.find((t) => t.templateId === wanted);
12768
- if (!found) throw new CliError(cli_init_template_unknown({
12769
- templateId: wanted,
12770
- ids
12771
- }));
12772
- return found;
12773
- }
12774
- const labels = templates.map((t) => cli_init_template_option({
12775
- templateId: t.templateId,
12776
- name: t.name,
12777
- version: t.version,
12778
- description: t.description
12779
- }));
12780
- const picked = io.select ? await io.select(cli_init_template_prompt(), labels) : null;
12781
- const index = picked ? labels.indexOf(picked) : -1;
12782
- if (index < 0) throw new CliError(cli_init_template_required({ list: labels.map((label) => ` ${label}`).join("\n") }));
12783
- return templates[index];
12505
+ function init() {
12506
+ throw new CliError(cli_init_retired());
12784
12507
  }
12785
12508
  /**
12786
12509
  * `sayren link [폴더]`(#315 C6) — 폴더를 상점의 호스팅 사이트에 연결한다. 상점은 `--store` → 토큰 범위 → 대화형 선택이고, 사이트가 없으면
@@ -12792,7 +12515,11 @@ async function link(flags, io) {
12792
12515
  if (!existsSync(dir)) throw new CliError(cli_link_dir_missing({ dir: relativeOf(io, dir) }));
12793
12516
  migrateLegacy(dir, io.out);
12794
12517
  await linkDir(flags, dir, io);
12795
- io.out(cli_link_next_steps());
12518
+ const manager = packageManagerOfDir(dir);
12519
+ io.out(cli_link_next_steps({
12520
+ install: installCommand(manager),
12521
+ dev: devCommand(manager)
12522
+ }));
12796
12523
  return 0;
12797
12524
  }
12798
12525
  /** link 본체 — `deploy`도 연결이 없으면 이것부터 부른다 */
@@ -13041,7 +12768,7 @@ async function localBuild(dir, skip, io) {
13041
12768
  return;
13042
12769
  }
13043
12770
  if (plan.kind === "no-deps") {
13044
- io.out(cli_deploy_build_local_no_deps());
12771
+ io.out(cli_deploy_build_local_no_deps({ install: installCommand(packageManagerOfDir(dir)) }));
13045
12772
  return;
13046
12773
  }
13047
12774
  io.out(cli_deploy_build_local_running());
@@ -13390,9 +13117,10 @@ var StarterTransformError = class extends Error {
13390
13117
  this.code = code;
13391
13118
  }
13392
13119
  };
13393
- /** 새 프로젝트로 옮기지 않는 루트 경로인가 — 로컬 env·CLI 연결 정보 */
13120
+ /** 새 프로젝트로 옮기지 않는 루트 경로인가 — 로컬 env·CLI 연결 정보·원본 저장소의 CI */
13394
13121
  function isStarterLocalOnly(path) {
13395
13122
  if (path === ".sayren" || path.startsWith(".sayren/")) return true;
13123
+ if (path === ".github" || path.startsWith(".github/")) return true;
13396
13124
  if (path.includes("/")) return false;
13397
13125
  return (path === ".env" || path.startsWith(".env.")) && path !== ".env.example";
13398
13126
  }
@@ -13431,140 +13159,45 @@ function withPackageName(bytes, name, fields = {}) {
13431
13159
  return new TextEncoder().encode(`${JSON.stringify(next, null, 2)}\n`);
13432
13160
  }
13433
13161
 
13434
- //#endregion
13435
- //#region ../mcp/src/template-package.ts
13436
- /** package.json의 기준 템플릿 표식 필드 — 원천은 이 함수다(api `template-marker.ts`가 같은 모양을 읽는다) */
13437
- function withTemplateMarker(pkg, marker) {
13438
- const current = pkg.sayren;
13439
- pkg.sayren = {
13440
- ...current && typeof current === "object" && !Array.isArray(current) ? current : {},
13441
- template: marker.id,
13442
- templateVersion: marker.version
13443
- };
13444
- return pkg;
13445
- }
13446
- const DEP_FIELDS = [
13447
- "dependencies",
13448
- "devDependencies",
13449
- "peerDependencies"
13450
- ];
13451
- function renderTemplatePackageJson(raw, meta, options = {}) {
13452
- const pkg = JSON.parse(raw);
13453
- for (const field of DEP_FIELDS) {
13454
- const deps = pkg[field];
13455
- if (!deps) continue;
13456
- for (const [name, spec] of Object.entries(deps)) deps[name] = resolveSpec(name, spec, meta);
13457
- }
13458
- if (options.name) pkg.name = options.name;
13459
- const marker = options.template ?? meta.template;
13460
- if (marker) withTemplateMarker(pkg, marker);
13461
- return `${JSON.stringify(pkg, null, 2)}\n`;
13462
- }
13463
- function resolveSpec(name, spec, meta) {
13464
- if (spec === "catalog:" || spec === "catalog:default") {
13465
- const version$1 = meta.catalog[name];
13466
- if (!version$1) throw new Error(mcp_template_catalog_version_missing({ name }));
13467
- return version$1;
13468
- }
13469
- if (spec.startsWith("workspace:")) return workspaceRange(name, spec.slice(10), meta);
13470
- return spec;
13471
- }
13472
- /**
13473
- * `workspace:` 뒤 표기 → 셀러 프로젝트의 범위(#232). 템플릿은 SDK를 범위(`workspace:^0.35.0`)로 적고 `workspace:`만 뗀다 —
13474
- * SDK patch가 올라도 풀어 쓴 package.json·첫 작업 트리가 그대로라 템플릿 버전을 올리지 않는다. 범위가 아닌 `*`·`^`·`~`만
13475
- * 지금 워크스페이스 버전으로 푼다(`*`는 `^{버전}`)
13476
- */
13477
- function workspaceRange(name, rest, meta) {
13478
- if (rest !== "*" && rest !== "^" && rest !== "~" && rest !== "") return rest;
13479
- const version$1 = meta.versions[name];
13480
- if (!version$1) throw new Error(mcp_template_published_version_unknown({ name }));
13481
- return `${rest === "~" ? "~" : "^"}${version$1}`;
13482
- }
13483
- /** pnpm-workspace.yaml의 최상위 `catalog:` 블록만 읽는다(` key: value`, 따옴표 허용) */
13484
- function parseCatalog(yaml) {
13485
- const out = {};
13486
- let inCatalog = false;
13487
- for (const line of yaml.split("\n")) {
13488
- if (/^catalog:\s*$/.test(line)) {
13489
- inCatalog = true;
13490
- continue;
13491
- }
13492
- if (!inCatalog) continue;
13493
- if (/^\S/.test(line)) break;
13494
- const match = line.match(/^\s+["']?([^"':]+)["']?\s*:\s*["']?([^"'#\s]+)["']?/);
13495
- if (match?.[1] && match[2]) out[match[1].trim()] = match[2];
13496
- }
13497
- return out;
13498
- }
13499
- function readWorkspaceVersions(packagesDir) {
13500
- const out = {};
13501
- for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
13502
- if (!entry.isDirectory()) continue;
13503
- const file = join(packagesDir, entry.name, "package.json");
13504
- if (!existsSync(file)) continue;
13505
- const pkg = JSON.parse(readFileSync(file, "utf8"));
13506
- if (pkg.name && pkg.version) out[pkg.name] = pkg.version;
13507
- }
13508
- return out;
13509
- }
13510
-
13511
13162
  //#endregion
13512
13163
  //#region src/create-command.ts
13513
13164
  /**
13514
13165
  * `npx sayren create <이름> [--template <템플릿>]`(#271, #315 C7) — 시작 코드를 새 폴더에 쓴다. 파일만 만들고 상점을 묻지 않는다.
13515
13166
  * 상점 연결은 `sayren link`(사이트·`.sayren/project.json`)와 `sayren env pull`(`.env`)이 한다. `--store-code`를 주면 그 값으로 `.env`만 쓴다.
13516
13167
  *
13517
- * 템플릿: 없으면 CLI에 묶은 시작 코드(네트워크 없음), `github:owner/repo[/경로][#ref]`는 codeload tarball, 그 밖의 `<id>`는 sayren 레지스트리
13518
- * 자리다(마켓 전까지 늘 「템플릿을 찾을 수 없습니다」). 받은 소스는 변환 함수 하나(`starter-transform.ts`, 서버와 공유)를 지난다.
13168
+ * 템플릿: 없으면 시작 코드 공개 저장소(`avarcorp/sayren-starter`)의 최신 릴리스 태그, `github:owner/repo[/경로][#ref]`는 그 저장소다.
13169
+ * 둘 다 codeload tarball로 받는다(CLI에 시작 코드를 묶지 않는다 — 네트워크가 필요하다). 그 밖의 `<id>`는 sayren 레지스트리 자리다
13170
+ * (마켓 전까지 늘 「템플릿을 찾을 수 없습니다」).
13171
+ *
13172
+ * 받은 소스는 변환 함수 하나(`starter-transform.ts`, 서버와 공유)를 지난다. 변환(`package.json` `name`·`private`, `_gitignore` → `.gitignore`,
13173
+ * 로컬 전용 파일 빼기)은 **고정 계약이다**: 이미 설치된 옛 CLI도 늘 최신 시작 코드를 받으므로 시작 코드 저장소는 이 변환만 믿고 쓰고,
13174
+ * CLI는 변환을 바꾸거나 새 전제를 더하지 않는다. 안내 스킬(`.claude/skills/sayren/SKILL.md`)은 시작 코드 저장소에 두지 않고 CLI가 써 넣는다.
13519
13175
  */
13520
- /** 묶은 시작 코드 위치 — 게시본은 빌드가 묶은 `dist/boilerplate`, 저장소에서 바로 돌리면 원천 `packages/cli/boilerplate` */
13521
- function boilerplateRoot() {
13522
- const here = dirname(fileURLToPath(import.meta.url));
13523
- const candidates = [join(here, "boilerplate"), join(here, "..", "boilerplate")];
13524
- const found = candidates.find((dir) => existsSync(join(dir, "package.json")));
13525
- if (!found) throw new Error(`boilerplate not found: ${candidates.join(", ")}`);
13526
- return found;
13527
- }
13528
- /** 싣지 않는 것 — 설치물·산출물. 점 파일은 에이전트 안내(`.claude/`·`.mcp.json`)와 `.gitignore`만(scripts/starter-bundle.mjs와 같다) */
13529
- const SKIP_DIRS = new Set([
13530
- "node_modules",
13531
- "dist",
13532
- ".output",
13533
- ".tanstack",
13534
- ".nitro",
13535
- ".wrangler"
13536
- ]);
13537
- const DOT_KEPT = new Set([
13538
- ".claude",
13539
- ".mcp.json",
13540
- ".gitignore"
13541
- ]);
13176
+ /** 기본 시작 코드 — 공개 저장소. 릴리스(태그 v*)는 그 저장소 Actions의 검사를 통과해야 생긴다 */
13177
+ const STARTER_REPO = {
13178
+ owner: "avarcorp",
13179
+ repo: "sayren-starter"
13180
+ };
13542
13181
  /**
13543
- * 묶은 시작 코드 — 상대 경로 → 내용. 게시본은 빌드가 이미 npm 범위로 풀었다. 저장소 원천(워크스페이스 구성원)에서 바로 돌리면
13544
- * `catalog:`·`workspace:`를 여기서 푼다(게시본과 같은 결과).
13182
+ * 기본 시작 코드의 최신 릴리스 태그 — `releases/latest`가 302로 가리키는 `/releases/tag/{태그}`에서 읽는다(GitHub API rate limit을 쓰지 않는다).
13183
+ * 릴리스가 없으면 GitHub은 `/releases`로 보내거나 404다
13545
13184
  */
13546
- function bundledStarterFiles(root) {
13547
- const files = /* @__PURE__ */ new Map();
13548
- const walk = (dir) => {
13549
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
13550
- if (SKIP_DIRS.has(entry.name)) continue;
13551
- if (entry.name.startsWith(".") && !DOT_KEPT.has(entry.name)) continue;
13552
- const full = join(dir, entry.name);
13553
- if (entry.isDirectory()) walk(full);
13554
- else if (entry.isFile()) files.set(relative(root, full).split(sep).join("/"), readFileSync(full));
13555
- }
13556
- };
13557
- walk(root);
13558
- const repo = join(root, "..", "..", "..");
13559
- const pkg = files.get("package.json");
13560
- if (pkg && existsSync(join(repo, "pnpm-workspace.yaml"))) {
13561
- const meta = {
13562
- catalog: parseCatalog(readFileSync(join(repo, "pnpm-workspace.yaml"), "utf8")),
13563
- versions: readWorkspaceVersions(join(repo, "packages"))
13564
- };
13565
- files.set("package.json", Buffer.from(renderTemplatePackageJson(Buffer.from(pkg).toString("utf8"), meta)));
13185
+ async function latestStarterTag(io) {
13186
+ const url = `https://github.com/${STARTER_REPO.owner}/${STARTER_REPO.repo}/releases/latest`;
13187
+ let res;
13188
+ try {
13189
+ res = await io.fetch(url, {
13190
+ method: "HEAD",
13191
+ redirect: "manual"
13192
+ });
13193
+ } catch {
13194
+ throw new CliError(cli_create_starter_unreachable());
13566
13195
  }
13567
- return files;
13196
+ const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
13197
+ const tag = location ? /\/releases\/tag\/([^/?#]+)$/.exec(new URL(location, url).pathname)?.[1] : void 0;
13198
+ const decoded = tag ? decodeURIComponent(tag) : "";
13199
+ if (!GIT_REF.test(decoded) || decoded.includes("..")) throw new CliError(cli_create_starter_no_release({ status: String(res.status) }));
13200
+ return decoded;
13568
13201
  }
13569
13202
  const GITHUB_OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
13570
13203
  const GITHUB_REPO = /^[A-Za-z0-9._-]{1,100}$/;
@@ -13573,7 +13206,7 @@ const GIT_REF = /^[A-Za-z0-9._/+-]{1,255}$/;
13573
13206
  /** `--template` 값 → 받을 곳. 형식이 틀리면 api·네트워크를 부르기 전에 멈춘다 */
13574
13207
  function parseTemplateSpec(value) {
13575
13208
  const raw = value?.trim();
13576
- if (!raw) return { kind: "bundled" };
13209
+ if (!raw) return { kind: "starter" };
13577
13210
  if (!raw.startsWith("github:")) return {
13578
13211
  kind: "registry",
13579
13212
  id: raw
@@ -13645,7 +13278,17 @@ async function create(flags, io) {
13645
13278
  if (storeCode !== void 0 && !STORE_CODE_PATTERN.test(storeCode)) throw new CliError(cli_create_store_code_invalid({ storeCode }));
13646
13279
  const spec = parseTemplateSpec(flags.template);
13647
13280
  if (spec.kind === "registry") throw new CliError(cli_create_template_not_found({ template: spec.id }));
13648
- const source = spec.kind === "github" ? await githubTemplateFiles(spec, flags.template?.trim() ?? "", io) : bundledStarterFiles(boilerplateRoot());
13281
+ let source;
13282
+ if (spec.kind === "github") source = await githubTemplateFiles(spec, flags.template?.trim() ?? "", io);
13283
+ else {
13284
+ const ref = await latestStarterTag(io);
13285
+ source = await githubTemplateFiles({
13286
+ kind: "github",
13287
+ ...STARTER_REPO,
13288
+ path: "",
13289
+ ref
13290
+ }, `github:${STARTER_REPO.owner}/${STARTER_REPO.repo}#${ref}`, io);
13291
+ }
13649
13292
  let files;
13650
13293
  try {
13651
13294
  files = transformStarterFiles(source, { name });
@@ -13653,7 +13296,13 @@ async function create(flags, io) {
13653
13296
  if (error instanceof StarterTransformError) throw new CliError(cli_create_template_no_package_json({ reason: error.message }));
13654
13297
  throw error;
13655
13298
  }
13299
+ files.set(GUIDE_SKILL_PATH, new TextEncoder().encode(GUIDE_SKILL));
13656
13300
  writeTree(target, files);
13301
+ const manager = packageManagerOfFiles(files);
13302
+ const steps = {
13303
+ install: installCommand(manager),
13304
+ dev: devCommand(manager)
13305
+ };
13657
13306
  if (storeCode) {
13658
13307
  const { apiOrigin } = resolveApiOrigin(flags, target, null, io.env);
13659
13308
  updateEnvFile(target, {
@@ -13663,11 +13312,13 @@ async function create(flags, io) {
13663
13312
  io.out(cli_create_done_store_code({
13664
13313
  dir: name,
13665
13314
  count: files.size,
13666
- storeCode
13315
+ storeCode,
13316
+ ...steps
13667
13317
  }));
13668
13318
  } else io.out(cli_create_done({
13669
13319
  dir: name,
13670
- count: files.size
13320
+ count: files.size,
13321
+ ...steps
13671
13322
  }));
13672
13323
  return 0;
13673
13324
  }
@@ -13826,11 +13477,7 @@ async function main(argv, io) {
13826
13477
  ...common,
13827
13478
  dir: str(parsed.flags.dir) ?? parsed.positional[1]
13828
13479
  }, io);
13829
- case "init": return init({
13830
- ...common,
13831
- template: str(parsed.flags.template),
13832
- yes: parsed.flags.yes === true
13833
- }, io);
13480
+ case "init": return init();
13834
13481
  case "pull": return pull({
13835
13482
  ...common,
13836
13483
  project: str(parsed.flags.project),