sayren 0.14.0 → 0.15.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.
package/dist/index.mjs CHANGED
@@ -3633,13 +3633,13 @@ function isGitTracked(dir, name) {
3633
3633
  }
3634
3634
  /** 심볼릭 링크면 쓰지 않는다(링크를 따라 폴더 밖 파일을 덮지 않게). 권한은 600이다 */
3635
3635
  function writePrivateFile(path, text) {
3636
- let link = false;
3636
+ let link$1 = false;
3637
3637
  try {
3638
- link = lstatSync(path).isSymbolicLink();
3638
+ link$1 = lstatSync(path).isSymbolicLink();
3639
3639
  } catch {
3640
- link = false;
3640
+ link$1 = false;
3641
3641
  }
3642
- if (link) throw new McpJsonError(mcp_json_symlink_refused({ path }));
3642
+ if (link$1) throw new McpJsonError(mcp_json_symlink_refused({ path }));
3643
3643
  writeFileSync(path, text, { mode: 384 });
3644
3644
  try {
3645
3645
  chmodSync(path, 384);
@@ -5586,12 +5586,13 @@ const cli_create_dir_not_empty = ((inputs, options = {}) => {
5586
5586
  //#endregion
5587
5587
  //#region src/paraglide/messages/cli_create_done.js
5588
5588
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5589
- /** @typedef {{ dir: NonNullable<unknown>, count: NonNullable<unknown>, storeCode: NonNullable<unknown> }} Cli_Create_DoneInputs */
5589
+ /** @typedef {{ dir: NonNullable<unknown>, count: NonNullable<unknown> }} Cli_Create_DoneInputs */
5590
5590
  const ko_cli_create_done = (i) => {
5591
- return `보일러플레이트를 만들었습니다 — ${i?.dir} (파일 ${i?.count}개, .env에 상점 ${i?.storeCode} 연결 값)
5591
+ return `시작 코드를 만들었습니다 — ${i?.dir} (파일 ${i?.count}개)
5592
5592
  다음:
5593
5593
  cd ${i?.dir}
5594
5594
  npm install
5595
+ npx sayren link # 상점에 연결하고 .env에 상점 값을 씁니다
5595
5596
  npm run dev`;
5596
5597
  };
5597
5598
  /**
@@ -5606,39 +5607,44 @@ const cli_create_done = ((inputs, options = {}) => {
5606
5607
  });
5607
5608
 
5608
5609
  //#endregion
5609
- //#region src/paraglide/messages/cli_create_name_invalid.js
5610
+ //#region src/paraglide/messages/cli_create_done_store_code.js
5610
5611
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5611
- /** @typedef {{ name: NonNullable<unknown> }} Cli_Create_Name_InvalidInputs */
5612
- const ko_cli_create_name_invalid = (i) => {
5613
- return `폴더 이름은 영문 소문자·숫자·.·_·-로 쓰고 영문 소문자나 숫자로 시작합니다: ${i?.name}`;
5612
+ /** @typedef {{ dir: NonNullable<unknown>, count: NonNullable<unknown>, storeCode: NonNullable<unknown> }} Cli_Create_Done_Store_CodeInputs */
5613
+ const ko_cli_create_done_store_code = (i) => {
5614
+ return `시작 코드를 만들었습니다 — ${i?.dir} (파일 ${i?.count}개, .env에 상점 ${i?.storeCode} 연결 값)
5615
+ 다음:
5616
+ cd ${i?.dir}
5617
+ npm install
5618
+ npm run dev
5619
+ npx sayren deploy # 처음이면 상점 사이트에 연결한 뒤 배포합니다`;
5614
5620
  };
5615
5621
  /**
5616
5622
 
5617
- * @param {Cli_Create_Name_InvalidInputs} inputs
5623
+ * @param {Cli_Create_Done_Store_CodeInputs} inputs
5618
5624
  * @param {{ locale?: "ko" }} options
5619
5625
  * @returns {LocalizedString}
5620
5626
  */
5621
- const cli_create_name_invalid = ((inputs, options = {}) => {
5627
+ const cli_create_done_store_code = ((inputs, options = {}) => {
5622
5628
  experimentalStaticLocale ?? options.locale ?? getLocale();
5623
- return ko_cli_create_name_invalid(inputs);
5629
+ return ko_cli_create_done_store_code(inputs);
5624
5630
  });
5625
5631
 
5626
5632
  //#endregion
5627
- //#region src/paraglide/messages/cli_create_prompt_store_code.js
5633
+ //#region src/paraglide/messages/cli_create_name_invalid.js
5628
5634
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5629
- /** @typedef {{}} Cli_Create_Prompt_Store_CodeInputs */
5630
- const ko_cli_create_prompt_store_code = () => {
5631
- return `상점 코드(상점 플랫폼 › 설정 › 상점 정보): `;
5635
+ /** @typedef {{ name: NonNullable<unknown> }} Cli_Create_Name_InvalidInputs */
5636
+ const ko_cli_create_name_invalid = (i) => {
5637
+ return `폴더 이름은 영문 소문자·숫자·.·_·-로 쓰고 영문 소문자나 숫자로 시작합니다: ${i?.name}`;
5632
5638
  };
5633
5639
  /**
5634
5640
 
5635
- * @param {Cli_Create_Prompt_Store_CodeInputs} inputs
5641
+ * @param {Cli_Create_Name_InvalidInputs} inputs
5636
5642
  * @param {{ locale?: "ko" }} options
5637
5643
  * @returns {LocalizedString}
5638
5644
  */
5639
- const cli_create_prompt_store_code = ((inputs = {}, options = {}) => {
5645
+ const cli_create_name_invalid = ((inputs, options = {}) => {
5640
5646
  experimentalStaticLocale ?? options.locale ?? getLocale();
5641
- return ko_cli_create_prompt_store_code(inputs);
5647
+ return ko_cli_create_name_invalid(inputs);
5642
5648
  });
5643
5649
 
5644
5650
  //#endregion
@@ -5660,21 +5666,165 @@ const cli_create_store_code_invalid = ((inputs, options = {}) => {
5660
5666
  });
5661
5667
 
5662
5668
  //#endregion
5663
- //#region src/paraglide/messages/cli_create_store_code_required.js
5669
+ //#region src/paraglide/messages/cli_create_template_download_failed.js
5664
5670
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5665
- /** @typedef {{}} Cli_Create_Store_Code_RequiredInputs */
5666
- const ko_cli_create_store_code_required = () => {
5667
- return `상점 코드를 받지 못했습니다. --store-code <상점 코드>로 주십시오(비대화형은 필수입니다)`;
5671
+ /** @typedef {{ template: NonNullable<unknown>, status: NonNullable<unknown> }} Cli_Create_Template_Download_FailedInputs */
5672
+ const ko_cli_create_template_download_failed = (i) => {
5673
+ return `템플릿을 받지 못했습니다: ${i?.template} (HTTP ${i?.status})`;
5668
5674
  };
5669
5675
  /**
5670
5676
 
5671
- * @param {Cli_Create_Store_Code_RequiredInputs} inputs
5677
+ * @param {Cli_Create_Template_Download_FailedInputs} inputs
5672
5678
  * @param {{ locale?: "ko" }} options
5673
5679
  * @returns {LocalizedString}
5674
5680
  */
5675
- const cli_create_store_code_required = ((inputs = {}, options = {}) => {
5681
+ const cli_create_template_download_failed = ((inputs, options = {}) => {
5676
5682
  experimentalStaticLocale ?? options.locale ?? getLocale();
5677
- return ko_cli_create_store_code_required(inputs);
5683
+ return ko_cli_create_template_download_failed(inputs);
5684
+ });
5685
+
5686
+ //#endregion
5687
+ //#region src/paraglide/messages/cli_create_template_github_not_found.js
5688
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5689
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_Github_Not_FoundInputs */
5690
+ const ko_cli_create_template_github_not_found = (i) => {
5691
+ return `GitHub에서 템플릿을 찾을 수 없습니다: ${i?.template}. 공개 저장소·ref를 확인하십시오`;
5692
+ };
5693
+ /**
5694
+
5695
+ * @param {Cli_Create_Template_Github_Not_FoundInputs} inputs
5696
+ * @param {{ locale?: "ko" }} options
5697
+ * @returns {LocalizedString}
5698
+ */
5699
+ const cli_create_template_github_not_found = ((inputs, options = {}) => {
5700
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5701
+ return ko_cli_create_template_github_not_found(inputs);
5702
+ });
5703
+
5704
+ //#endregion
5705
+ //#region src/paraglide/messages/cli_create_template_invalid.js
5706
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5707
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_InvalidInputs */
5708
+ const ko_cli_create_template_invalid = (i) => {
5709
+ return `템플릿 형식이 아닙니다: ${i?.template}. github:owner/repo[/경로][#ref] 또는 템플릿 id를 주십시오`;
5710
+ };
5711
+ /**
5712
+
5713
+ * @param {Cli_Create_Template_InvalidInputs} inputs
5714
+ * @param {{ locale?: "ko" }} options
5715
+ * @returns {LocalizedString}
5716
+ */
5717
+ const cli_create_template_invalid = ((inputs, options = {}) => {
5718
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5719
+ return ko_cli_create_template_invalid(inputs);
5720
+ });
5721
+
5722
+ //#endregion
5723
+ //#region src/paraglide/messages/cli_create_template_no_package_json.js
5724
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5725
+ /** @typedef {{ reason: NonNullable<unknown> }} Cli_Create_Template_No_Package_JsonInputs */
5726
+ const ko_cli_create_template_no_package_json = (i) => {
5727
+ return `시작 코드로 쓸 수 없는 템플릿입니다(${i?.reason})`;
5728
+ };
5729
+ /**
5730
+
5731
+ * @param {Cli_Create_Template_No_Package_JsonInputs} inputs
5732
+ * @param {{ locale?: "ko" }} options
5733
+ * @returns {LocalizedString}
5734
+ */
5735
+ const cli_create_template_no_package_json = ((inputs, options = {}) => {
5736
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5737
+ return ko_cli_create_template_no_package_json(inputs);
5738
+ });
5739
+
5740
+ //#endregion
5741
+ //#region src/paraglide/messages/cli_create_template_not_found.js
5742
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5743
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_Not_FoundInputs */
5744
+ const ko_cli_create_template_not_found = (i) => {
5745
+ return `템플릿을 찾을 수 없습니다: ${i?.template}`;
5746
+ };
5747
+ /**
5748
+
5749
+ * @param {Cli_Create_Template_Not_FoundInputs} inputs
5750
+ * @param {{ locale?: "ko" }} options
5751
+ * @returns {LocalizedString}
5752
+ */
5753
+ const cli_create_template_not_found = ((inputs, options = {}) => {
5754
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5755
+ return ko_cli_create_template_not_found(inputs);
5756
+ });
5757
+
5758
+ //#endregion
5759
+ //#region src/paraglide/messages/cli_create_template_path_empty.js
5760
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5761
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_Path_EmptyInputs */
5762
+ const ko_cli_create_template_path_empty = (i) => {
5763
+ return `템플릿 경로에 파일이 없습니다: ${i?.template}`;
5764
+ };
5765
+ /**
5766
+
5767
+ * @param {Cli_Create_Template_Path_EmptyInputs} inputs
5768
+ * @param {{ locale?: "ko" }} options
5769
+ * @returns {LocalizedString}
5770
+ */
5771
+ const cli_create_template_path_empty = ((inputs, options = {}) => {
5772
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5773
+ return ko_cli_create_template_path_empty(inputs);
5774
+ });
5775
+
5776
+ //#endregion
5777
+ //#region src/paraglide/messages/cli_create_template_too_large.js
5778
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5779
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_Too_LargeInputs */
5780
+ const ko_cli_create_template_too_large = (i) => {
5781
+ return `템플릿 저장소가 너무 커서 받지 않았습니다: ${i?.template}`;
5782
+ };
5783
+ /**
5784
+
5785
+ * @param {Cli_Create_Template_Too_LargeInputs} inputs
5786
+ * @param {{ locale?: "ko" }} options
5787
+ * @returns {LocalizedString}
5788
+ */
5789
+ const cli_create_template_too_large = ((inputs, options = {}) => {
5790
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5791
+ return ko_cli_create_template_too_large(inputs);
5792
+ });
5793
+
5794
+ //#endregion
5795
+ //#region src/paraglide/messages/cli_create_template_unreachable.js
5796
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5797
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_UnreachableInputs */
5798
+ const ko_cli_create_template_unreachable = (i) => {
5799
+ return `템플릿을 받지 못했습니다: ${i?.template}. 네트워크 연결을 확인하십시오`;
5800
+ };
5801
+ /**
5802
+
5803
+ * @param {Cli_Create_Template_UnreachableInputs} inputs
5804
+ * @param {{ locale?: "ko" }} options
5805
+ * @returns {LocalizedString}
5806
+ */
5807
+ const cli_create_template_unreachable = ((inputs, options = {}) => {
5808
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5809
+ return ko_cli_create_template_unreachable(inputs);
5810
+ });
5811
+
5812
+ //#endregion
5813
+ //#region src/paraglide/messages/cli_create_template_unreadable.js
5814
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5815
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_UnreadableInputs */
5816
+ const ko_cli_create_template_unreadable = (i) => {
5817
+ return `템플릿 압축 파일을 읽지 못했습니다: ${i?.template}`;
5818
+ };
5819
+ /**
5820
+
5821
+ * @param {Cli_Create_Template_UnreadableInputs} inputs
5822
+ * @param {{ locale?: "ko" }} options
5823
+ * @returns {LocalizedString}
5824
+ */
5825
+ const cli_create_template_unreadable = ((inputs, options = {}) => {
5826
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5827
+ return ko_cli_create_template_unreadable(inputs);
5678
5828
  });
5679
5829
 
5680
5830
  //#endregion
@@ -5682,7 +5832,7 @@ const cli_create_store_code_required = ((inputs = {}, options = {}) => {
5682
5832
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5683
5833
  /** @typedef {{}} Cli_Create_UsageInputs */
5684
5834
  const ko_cli_create_usage = () => {
5685
- return `폴더 이름을 주십시오: npx sayren create <이름> [--store-code <상점 코드>]`;
5835
+ return `폴더 이름을 주십시오: npx sayren create <이름> [--template <템플릿>]`;
5686
5836
  };
5687
5837
  /**
5688
5838
 
@@ -6111,40 +6261,40 @@ const cli_deploy_force_yes_required = ((inputs = {}, options = {}) => {
6111
6261
  });
6112
6262
 
6113
6263
  //#endregion
6114
- //#region src/paraglide/messages/cli_deploy_merged.js
6264
+ //#region src/paraglide/messages/cli_deploy_linking.js
6115
6265
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6116
- /** @typedef {{}} Cli_Deploy_MergedInputs */
6117
- const ko_cli_deploy_merged = () => {
6118
- return `pull 이후 원격 작업 트리가 바뀌어(상점 플랫폼·AI 편집) 서버가 병합했습니다. 원격 변경은 그대로 남습니다.
6119
- · 병합된 트리를 이 폴더에 받으려면: npx sayren pull --force`;
6266
+ /** @typedef {{ dir: NonNullable<unknown> }} Cli_Deploy_LinkingInputs */
6267
+ const ko_cli_deploy_linking = (i) => {
6268
+ return `${i?.dir}는 아직 상점에 연결되지 않았습니다 — 먼저 연결합니다(npx sayren link)`;
6120
6269
  };
6121
6270
  /**
6122
6271
 
6123
- * @param {Cli_Deploy_MergedInputs} inputs
6272
+ * @param {Cli_Deploy_LinkingInputs} inputs
6124
6273
  * @param {{ locale?: "ko" }} options
6125
6274
  * @returns {LocalizedString}
6126
6275
  */
6127
- const cli_deploy_merged = ((inputs = {}, options = {}) => {
6276
+ const cli_deploy_linking = ((inputs, options = {}) => {
6128
6277
  experimentalStaticLocale ?? options.locale ?? getLocale();
6129
- return ko_cli_deploy_merged(inputs);
6278
+ return ko_cli_deploy_linking(inputs);
6130
6279
  });
6131
6280
 
6132
6281
  //#endregion
6133
- //#region src/paraglide/messages/cli_deploy_no_state.js
6282
+ //#region src/paraglide/messages/cli_deploy_merged.js
6134
6283
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6135
- /** @typedef {{ stateFile: NonNullable<unknown>, storeId: NonNullable<unknown>, dir: NonNullable<unknown> }} Cli_Deploy_No_StateInputs */
6136
- const ko_cli_deploy_no_state = (i) => {
6137
- return `${i?.stateFile}가 없습니다. 이 사이트 소스를 clone한 폴더면 npx sayren pull --force --project ${i?.storeId} --dir ${i?.dir}로 이 폴더를 원격 작업 트리에 맞춘 뒤(로컬 변경은 먼저 커밋·백업하십시오) 다시 배포하십시오. 처음이면 npx sayren pull로 받으십시오`;
6284
+ /** @typedef {{}} Cli_Deploy_MergedInputs */
6285
+ const ko_cli_deploy_merged = () => {
6286
+ return `pull 이후 원격 작업 트리가 바뀌어(상점 플랫폼·AI 편집) 서버가 병합했습니다. 원격 변경은 그대로 남습니다.
6287
+ · 병합된 트리를 이 폴더에 받으려면: npx sayren pull --force`;
6138
6288
  };
6139
6289
  /**
6140
6290
 
6141
- * @param {Cli_Deploy_No_StateInputs} inputs
6291
+ * @param {Cli_Deploy_MergedInputs} inputs
6142
6292
  * @param {{ locale?: "ko" }} options
6143
6293
  * @returns {LocalizedString}
6144
6294
  */
6145
- const cli_deploy_no_state = ((inputs, options = {}) => {
6295
+ const cli_deploy_merged = ((inputs = {}, options = {}) => {
6146
6296
  experimentalStaticLocale ?? options.locale ?? getLocale();
6147
- return ko_cli_deploy_no_state(inputs);
6297
+ return ko_cli_deploy_merged(inputs);
6148
6298
  });
6149
6299
 
6150
6300
  //#endregion
@@ -6489,6 +6639,78 @@ const cli_edit_session_open = ((inputs = {}, options = {}) => {
6489
6639
  return ko_cli_edit_session_open(inputs);
6490
6640
  });
6491
6641
 
6642
+ //#endregion
6643
+ //#region src/paraglide/messages/cli_env_not_linked.js
6644
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6645
+ /** @typedef {{ file: NonNullable<unknown> }} Cli_Env_Not_LinkedInputs */
6646
+ const ko_cli_env_not_linked = (i) => {
6647
+ return `이 폴더는 상점에 연결되지 않았습니다(${i?.file} 없음). npx sayren link로 먼저 연결하십시오`;
6648
+ };
6649
+ /**
6650
+
6651
+ * @param {Cli_Env_Not_LinkedInputs} inputs
6652
+ * @param {{ locale?: "ko" }} options
6653
+ * @returns {LocalizedString}
6654
+ */
6655
+ const cli_env_not_linked = ((inputs, options = {}) => {
6656
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6657
+ return ko_cli_env_not_linked(inputs);
6658
+ });
6659
+
6660
+ //#endregion
6661
+ //#region src/paraglide/messages/cli_env_pulled.js
6662
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6663
+ /** @typedef {{ file: NonNullable<unknown>, storeCode: NonNullable<unknown> }} Cli_Env_PulledInputs */
6664
+ const ko_cli_env_pulled = (i) => {
6665
+ return `${i?.file}에 상점 ${i?.storeCode} 연결 값(SAYREN_API_URL·SAYREN_STORE_CODE)을 썼습니다`;
6666
+ };
6667
+ /**
6668
+
6669
+ * @param {Cli_Env_PulledInputs} inputs
6670
+ * @param {{ locale?: "ko" }} options
6671
+ * @returns {LocalizedString}
6672
+ */
6673
+ const cli_env_pulled = ((inputs, options = {}) => {
6674
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6675
+ return ko_cli_env_pulled(inputs);
6676
+ });
6677
+
6678
+ //#endregion
6679
+ //#region src/paraglide/messages/cli_env_store_code_unknown.js
6680
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6681
+ /** @typedef {{}} Cli_Env_Store_Code_UnknownInputs */
6682
+ const ko_cli_env_store_code_unknown = () => {
6683
+ return `상점 코드를 읽지 못했습니다. 로그인(npx sayren login)을 다시 하거나 토큰에 stores:read 권한이 있는지 확인하십시오`;
6684
+ };
6685
+ /**
6686
+
6687
+ * @param {Cli_Env_Store_Code_UnknownInputs} inputs
6688
+ * @param {{ locale?: "ko" }} options
6689
+ * @returns {LocalizedString}
6690
+ */
6691
+ const cli_env_store_code_unknown = ((inputs = {}, options = {}) => {
6692
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6693
+ return ko_cli_env_store_code_unknown(inputs);
6694
+ });
6695
+
6696
+ //#endregion
6697
+ //#region src/paraglide/messages/cli_env_usage.js
6698
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6699
+ /** @typedef {{}} Cli_Env_UsageInputs */
6700
+ const ko_cli_env_usage = () => {
6701
+ return `사용법: npx sayren env pull [--dir <폴더>]`;
6702
+ };
6703
+ /**
6704
+
6705
+ * @param {Cli_Env_UsageInputs} inputs
6706
+ * @param {{ locale?: "ko" }} options
6707
+ * @returns {LocalizedString}
6708
+ */
6709
+ const cli_env_usage = ((inputs = {}, options = {}) => {
6710
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6711
+ return ko_cli_env_usage(inputs);
6712
+ });
6713
+
6492
6714
  //#endregion
6493
6715
  //#region src/paraglide/messages/cli_error_api_unreachable.js
6494
6716
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -7030,6 +7252,135 @@ const cli_legacy_rc_tracked = ((inputs, options = {}) => {
7030
7252
  return ko_cli_legacy_rc_tracked(inputs);
7031
7253
  });
7032
7254
 
7255
+ //#endregion
7256
+ //#region src/paraglide/messages/cli_link_dir_missing.js
7257
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7258
+ /** @typedef {{ dir: NonNullable<unknown> }} Cli_Link_Dir_MissingInputs */
7259
+ const ko_cli_link_dir_missing = (i) => {
7260
+ return `폴더가 없습니다: ${i?.dir}`;
7261
+ };
7262
+ /**
7263
+
7264
+ * @param {Cli_Link_Dir_MissingInputs} inputs
7265
+ * @param {{ locale?: "ko" }} options
7266
+ * @returns {LocalizedString}
7267
+ */
7268
+ const cli_link_dir_missing = ((inputs, options = {}) => {
7269
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7270
+ return ko_cli_link_dir_missing(inputs);
7271
+ });
7272
+
7273
+ //#endregion
7274
+ //#region src/paraglide/messages/cli_link_done.js
7275
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7276
+ /** @typedef {{ storeId: NonNullable<unknown>, url: NonNullable<unknown>, file: NonNullable<unknown>, storeCode: NonNullable<unknown>, envFile: NonNullable<unknown> }} Cli_Link_DoneInputs */
7277
+ const ko_cli_link_done = (i) => {
7278
+ return `연결했습니다 — 상점 ${i?.storeId}, 사이트 ${i?.url}. 연결 정보는 ${i?.file}, 상점 ${i?.storeCode} 연결 값은 ${i?.envFile}에 두었습니다`;
7279
+ };
7280
+ /**
7281
+
7282
+ * @param {Cli_Link_DoneInputs} inputs
7283
+ * @param {{ locale?: "ko" }} options
7284
+ * @returns {LocalizedString}
7285
+ */
7286
+ const cli_link_done = ((inputs, options = {}) => {
7287
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7288
+ return ko_cli_link_done(inputs);
7289
+ });
7290
+
7291
+ //#endregion
7292
+ //#region src/paraglide/messages/cli_link_editing_disabled.js
7293
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7294
+ /** @typedef {{}} Cli_Link_Editing_DisabledInputs */
7295
+ const ko_cli_link_editing_disabled = () => {
7296
+ return `지금은 스토어프론트를 연결할 수 없습니다(편집 기능이 꺼져 있습니다)`;
7297
+ };
7298
+ /**
7299
+
7300
+ * @param {Cli_Link_Editing_DisabledInputs} inputs
7301
+ * @param {{ locale?: "ko" }} options
7302
+ * @returns {LocalizedString}
7303
+ */
7304
+ const cli_link_editing_disabled = ((inputs = {}, options = {}) => {
7305
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7306
+ return ko_cli_link_editing_disabled(inputs);
7307
+ });
7308
+
7309
+ //#endregion
7310
+ //#region src/paraglide/messages/cli_link_gitignore.js
7311
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7312
+ /** @typedef {{ lines: NonNullable<unknown> }} Cli_Link_GitignoreInputs */
7313
+ const ko_cli_link_gitignore = (i) => {
7314
+ return `.gitignore에 ${i?.lines}를 더했습니다(배포에 올리지 않습니다)`;
7315
+ };
7316
+ /**
7317
+
7318
+ * @param {Cli_Link_GitignoreInputs} inputs
7319
+ * @param {{ locale?: "ko" }} options
7320
+ * @returns {LocalizedString}
7321
+ */
7322
+ const cli_link_gitignore = ((inputs, options = {}) => {
7323
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7324
+ return ko_cli_link_gitignore(inputs);
7325
+ });
7326
+
7327
+ //#endregion
7328
+ //#region src/paraglide/messages/cli_link_next_steps.js
7329
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7330
+ /** @typedef {{}} Cli_Link_Next_StepsInputs */
7331
+ const ko_cli_link_next_steps = () => {
7332
+ return `다음:
7333
+ npm install
7334
+ npm run dev # http://localhost:4010 — 운영 상점 데이터로 동작합니다
7335
+ npx sayren deploy # 빌드 → 버전 → 발행`;
7336
+ };
7337
+ /**
7338
+
7339
+ * @param {Cli_Link_Next_StepsInputs} inputs
7340
+ * @param {{ locale?: "ko" }} options
7341
+ * @returns {LocalizedString}
7342
+ */
7343
+ const cli_link_next_steps = ((inputs = {}, options = {}) => {
7344
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7345
+ return ko_cli_link_next_steps(inputs);
7346
+ });
7347
+
7348
+ //#endregion
7349
+ //#region src/paraglide/messages/cli_link_site_created.js
7350
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7351
+ /** @typedef {{ url: NonNullable<unknown> }} Cli_Link_Site_CreatedInputs */
7352
+ const ko_cli_link_site_created = (i) => {
7353
+ return `호스팅 사이트를 만들었습니다 — ${i?.url} (오픈 준비 중, 첫 배포가 성공하면 발행합니다)`;
7354
+ };
7355
+ /**
7356
+
7357
+ * @param {Cli_Link_Site_CreatedInputs} inputs
7358
+ * @param {{ locale?: "ko" }} options
7359
+ * @returns {LocalizedString}
7360
+ */
7361
+ const cli_link_site_created = ((inputs, options = {}) => {
7362
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7363
+ return ko_cli_link_site_created(inputs);
7364
+ });
7365
+
7366
+ //#endregion
7367
+ //#region src/paraglide/messages/cli_link_slug_taken.js
7368
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7369
+ /** @typedef {{}} Cli_Link_Slug_TakenInputs */
7370
+ const ko_cli_link_slug_taken = () => {
7371
+ return `상점 코드와 같은 주소를 다른 사이트가 쓰고 있어 사이트를 만들지 못했습니다. 상점 플랫폼 › 스토어프론트에서 다른 주소로 만든 뒤 npx sayren link로 다시 연결하십시오`;
7372
+ };
7373
+ /**
7374
+
7375
+ * @param {Cli_Link_Slug_TakenInputs} inputs
7376
+ * @param {{ locale?: "ko" }} options
7377
+ * @returns {LocalizedString}
7378
+ */
7379
+ const cli_link_slug_taken = ((inputs = {}, options = {}) => {
7380
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7381
+ return ko_cli_link_slug_taken(inputs);
7382
+ });
7383
+
7033
7384
  //#endregion
7034
7385
  //#region src/paraglide/messages/cli_login_browser_prompt.js
7035
7386
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -8413,7 +8764,7 @@ const cli_state_invalid = ((inputs, options = {}) => {
8413
8764
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
8414
8765
  /** @typedef {{ name: NonNullable<unknown> }} Cli_State_Missing_FieldsInputs */
8415
8766
  const ko_cli_state_missing_fields = (i) => {
8416
- return `${i?.name}에 storeId·siteId·baseRef가 없습니다. npx sayren pull로 다시 받으십시오`;
8767
+ return `${i?.name}에 storeId·siteId·baseRef가 없습니다. npx sayren link로 다시 연결하십시오`;
8417
8768
  };
8418
8769
  /**
8419
8770
 
@@ -8534,6 +8885,24 @@ const cli_status_folder_base = ((inputs, options = {}) => {
8534
8885
  return ko_cli_status_folder_base(inputs);
8535
8886
  });
8536
8887
 
8888
+ //#endregion
8889
+ //#region src/paraglide/messages/cli_status_folder_linked_only.js
8890
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
8891
+ /** @typedef {{}} Cli_Status_Folder_Linked_OnlyInputs */
8892
+ const ko_cli_status_folder_linked_only = () => {
8893
+ return `이 폴더: 연결만 했습니다(아직 올리지 않음 — 다음 deploy가 교체합니다)`;
8894
+ };
8895
+ /**
8896
+
8897
+ * @param {Cli_Status_Folder_Linked_OnlyInputs} inputs
8898
+ * @param {{ locale?: "ko" }} options
8899
+ * @returns {LocalizedString}
8900
+ */
8901
+ const cli_status_folder_linked_only = ((inputs = {}, options = {}) => {
8902
+ experimentalStaticLocale ?? options.locale ?? getLocale();
8903
+ return ko_cli_status_folder_linked_only(inputs);
8904
+ });
8905
+
8537
8906
  //#endregion
8538
8907
  //#region src/paraglide/messages/cli_status_folder_other_site.js
8539
8908
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -9001,7 +9370,9 @@ const ko_cli_usage = (i) => {
9001
9370
  npx sayren login --paste # 액세스 토큰을 입력받아 저장(셸 기록에 남지 않습니다)
9002
9371
  npx sayren logout
9003
9372
  npx sayren whoami
9004
- npx sayren create <이름> [--store-code <상점 코드>] # 스토어프론트 보일러플레이트를 새 폴더에 만듭니다(로그인 없음)
9373
+ npx sayren create <이름> [--template <템플릿>] # 시작 코드를 새 폴더에 만듭니다(파일만, 로그인 없음)
9374
+ npx sayren link [폴더] [--store <storeId>] # 폴더를 상점 사이트에 연결합니다(없으면 사이트를 만듭니다)
9375
+ npx sayren env pull # 연결된 상점 값을 .env에 씁니다
9005
9376
  npx sayren init [폴더] [--template <id>] [--store <storeId>] [--yes]
9006
9377
  npx sayren pull --project <storeId> [폴더] [--force [--yes]]
9007
9378
  npx sayren deploy [--no-publish] [--skip-build] [--message <메모>] [--force [--yes]]
@@ -9014,10 +9385,12 @@ const ko_cli_usage = (i) => {
9014
9385
  login Sayren 계정으로 로그인합니다. 로그인 토큰은 사용자 설정 폴더(~/.config/sayren/credentials.json)에 둡니다
9015
9386
  logout 로그인을 폐기하고 이 컴퓨터의 토큰을 지웁니다
9016
9387
  whoami 쓰는 계정·토큰 종류·상점 범위
9017
- create 스토어프론트 보일러플레이트(상점 이름·상품 목록 홈, SDK·문구·분석·에이전트 안내)를 <이름> 폴더에 만들고 .env에 상점 연결 값을 씁니다. 호스팅 사이트는 만들지 않습니다
9388
+ create 시작 코드(상점 이름·상품 목록 홈, SDK·문구·분석·에이전트 안내)를 <이름> 폴더에 만듭니다. 상점을 묻지 않고 호스팅 사이트도 만들지 않습니다
9389
+ link 상점을 고르고(--store → 토큰 범위 → 선택) 사이트가 없으면 만든 뒤 .sayren/project.json·.env·.mcp.json을 씁니다
9390
+ env env pull: 연결된 상점의 SAYREN_STORE_CODE·SAYREN_API_URL을 .env에 씁니다
9018
9391
  init 템플릿을 골라 호스팅 사이트를 만들고 pull과 같은 폴더(기본: 상점 코드)로 받습니다. 사이트가 이미 있으면 만들지 않습니다
9019
- pull 호스팅 중인 사이트의 작업 트리를 폴더(기본: 상점 코드)로 받고 .mcp.json·.env·.sayren/state.json을 씁니다
9020
- deploy 로컬 검사 → 로컬 빌드 → 작업 트리 올리기 → 빌드 → 발행. 원격이 pull 이후 바뀌었으면 덮어쓰지 않습니다
9392
+ pull 호스팅 중인 사이트의 작업 트리를 폴더(기본: 상점 코드)로 받고 .mcp.json·.env·.sayren/project.json을 씁니다
9393
+ deploy 로컬 검사 → 로컬 빌드 → 작업 트리 올리기 → 빌드 → 발행. 연결이 없으면 link부터 하고 다음 버전으로 교체합니다. 원격이 마지막 받기·올리기 이후 바뀌었으면 덮어쓰지 않습니다
9021
9394
  status 사이트·발행 버전·최근 빌드·편집 환경
9022
9395
  verify 로컬 규칙·경로·크기 검사(서버 빌드와 같은 규칙)
9023
9396
  skill sayren 스킬 목록(list)과 본문(<이름>)을 출력합니다. 스킬은 중앙에서 고치므로 늘 최신입니다
@@ -9030,8 +9403,9 @@ const ko_cli_usage = (i) => {
9030
9403
 
9031
9404
  옵션:
9032
9405
  --project <storeId> pull: 받을 상점(--store와 같습니다)
9033
- --template <id> init: 템플릿 id(비대화형은 필수)
9034
- --store-code <코드> create: 연결할 상점 코드(없으면 묻습니다, 비대화형은 필수)
9406
+ --template <템플릿> create: github:owner/repo[/경로][#ref](공개 GitHub 저장소) 또는 템플릿 id. 없으면 CLI에 든 시작 코드
9407
+ init: 호스팅 템플릿 id(비대화형은 필수)
9408
+ --store-code <코드> create: 이 상점 코드로 .env를 씁니다(없으면 link·env pull이 씁니다)
9035
9409
  --dir <폴더> 작업 폴더(기본: 지금 폴더, pull·init은 상점 코드)
9036
9410
  --force pull: 같은 사이트를 받은 폴더에 원격을 다시 받습니다(지울·덮을 파일을 보여 주고 묻습니다)
9037
9411
  deploy: 원격 변경·열린 편집 환경을 무시하고 덮어씁니다(확인을 묻습니다)
@@ -9234,8 +9608,9 @@ function useProcessLocale(locale) {
9234
9608
  /**
9235
9609
  * 설정 (2026-09-29 사용자 결정: `.sayrenrc`를 없애고 받은 폴더에 원격 MCP `.mcp.json`을 둔다)
9236
9610
  *
9237
- * - `.sayren/state.json`: pull·deploy가 쓰는 비밀 아닌 상태(`storeId`·`siteId`·`baseRef`, 기본이 아닌 api 주소). `.gitignore` 대상이고
9238
- * 올리지 않는다. `baseRef`가 deploy의 `expectedBaseRef`다(원격이 pull 이후 바뀌었으면 `SOURCE_CHANGED`).
9611
+ * - `.sayren/project.json`(#315 C6, 옛 `.sayren/state.json`): link·pull·deploy가 쓰는 연결 정보(`storeId`·`siteId`·`baseRef`, 기본이 아닌 api 주소).
9612
+ * `.gitignore` 대상이고 올리지 않는다. `baseRef`가 deploy의 `expectedBaseRef`다(원격이 pull 이후 바뀌었으면 `SOURCE_CHANGED`). `link`가 막
9613
+ * 연결한 폴더는 `baseRef`가 null이다 — 첫 deploy가 조건 없이 다음 버전으로 교체하고(머리 없는 사이트는 첫 커밋), 그 뒤부터 올린 트리가 기준이다.
9239
9614
  * - `.mcp.json`: Claude Code 형식의 `sayren` 원격 MCP 항목. 주소와 `X-Store-Id`만 있고 토큰은 없다(#71 — MCP 클라이언트가
9240
9615
  * 연결할 때 스스로 로그인한다). 옛 폴더의 폐기된 토큰(`sy_pat_…`) 헤더는 쓸 때 지운다. `.gitignore` 대상이고 올리지 않는다.
9241
9616
  * - `.env`: 스토어프론트 템플릿 개발 서버가 읽는 값(`SAYREN_API_URL`·`SAYREN_STORE_CODE`)만 둔다. 토큰을 두지 않는다.
@@ -9245,10 +9620,12 @@ function useProcessLocale(locale) {
9245
9620
  * 옛 API 토큰(`sy_pat_…`)은 R1에서 폐기되어 api에 보내지 않고 거절한다(`cli_retired_token`).
9246
9621
  * api 주소는 `--api-origin` → `SAYREN_API_ORIGIN` → `.env`의
9247
9622
  * `SAYREN_API_URL` → 상태 파일 → 기본값 순서이고, 파일에서 온 기본이 아닌 주소는 확인한다(`checkAuthSources`).
9248
- * 옛 폴더(`.sayrenrc`, `.env`의 `SAYREN_TOKEN`)는 한 번 정리한다(`migrateLegacy`).
9623
+ * 옛 폴더(`.sayrenrc`, `.sayren/state.json`, `.env`의 `SAYREN_TOKEN`)는 한 번 정리한다(`migrateLegacy`).
9249
9624
  */
9250
9625
  const STATE_DIR = ".sayren";
9251
- const STATE_FILE = `${STATE_DIR}/state.json`;
9626
+ const STATE_FILE = `${STATE_DIR}/project.json`;
9627
+ /** 옛 연결 파일(0.2.0 ~ 0.14.x) — 읽고, 옮긴 뒤 지운다 */
9628
+ const LEGACY_STATE_FILE = `${STATE_DIR}/state.json`;
9252
9629
  /** 옛 상태 파일(0.1.x) — 읽어서 옮기고 지운다 */
9253
9630
  const LEGACY_RC_FILE = ".sayrenrc";
9254
9631
  const ENV_FILE = ".env";
@@ -9263,7 +9640,7 @@ function parseState(text, name) {
9263
9640
  }
9264
9641
  if (!parsed || typeof parsed !== "object") throw new CliError(cli_state_invalid({ name }));
9265
9642
  const raw = parsed;
9266
- if (typeof raw.storeId !== "string" || typeof raw.siteId !== "string" || typeof raw.baseRef !== "string") throw new CliError(cli_state_missing_fields({ name }));
9643
+ if (typeof raw.storeId !== "string" || typeof raw.siteId !== "string" || typeof raw.baseRef !== "string" && raw.baseRef !== null) throw new CliError(cli_state_missing_fields({ name }));
9267
9644
  return {
9268
9645
  storeId: raw.storeId,
9269
9646
  siteId: raw.siteId,
@@ -9271,10 +9648,13 @@ function parseState(text, name) {
9271
9648
  ...typeof raw.apiOrigin === "string" ? { apiOrigin: raw.apiOrigin } : {}
9272
9649
  };
9273
9650
  }
9651
+ /** 연결 정보 — `project.json`, 없으면 옛 `state.json`을 읽는다(옮기기는 `migrateLegacy`) */
9274
9652
  function readState(dir) {
9275
- const path = join(dir, STATE_FILE);
9276
- if (!existsSync(path)) return null;
9277
- return parseState(readFileSync(path, "utf8"), STATE_FILE);
9653
+ for (const name of [STATE_FILE, LEGACY_STATE_FILE]) {
9654
+ const path = join(dir, name);
9655
+ if (existsSync(path)) return parseState(readFileSync(path, "utf8"), name);
9656
+ }
9657
+ return null;
9278
9658
  }
9279
9659
  function writeState(dir, state) {
9280
9660
  const out = {
@@ -9285,10 +9665,15 @@ function writeState(dir, state) {
9285
9665
  if (state.apiOrigin && state.apiOrigin !== DEFAULT_API_ORIGIN) out.apiOrigin = state.apiOrigin;
9286
9666
  mkdirSync(join(dir, STATE_DIR), { recursive: true });
9287
9667
  writeFileSync(join(dir, STATE_FILE), `${JSON.stringify(out, null, 2)}\n`);
9668
+ rmSync(join(dir, LEGACY_STATE_FILE), { force: true });
9288
9669
  }
9289
- /** 받은 폴더인가 — 상태 파일 또는 옛 `.sayrenrc`가 있다 */
9670
+ /** 연결된(받은) 폴더인가 — 연결 파일 또는 옛 `state.json`·`.sayrenrc`가 있다 */
9290
9671
  function isPulledDir(dir) {
9291
- return existsSync(join(dir, STATE_FILE)) || existsSync(join(dir, LEGACY_RC_FILE));
9672
+ return [
9673
+ STATE_FILE,
9674
+ LEGACY_STATE_FILE,
9675
+ LEGACY_RC_FILE
9676
+ ].some((name) => existsSync(join(dir, name)));
9292
9677
  }
9293
9678
  function readMcpText(dir) {
9294
9679
  const path = join(dir, MCP_FILE);
@@ -9327,12 +9712,18 @@ function writePrivate(path, text) {
9327
9712
  }
9328
9713
  }
9329
9714
  /**
9330
- * 0.1.x가 받은 폴더를 한 번 정리한다. `.sayrenrc` → `.sayren/state.json`(옛 파일은 지운다), `.env`의 폐기된 `SAYREN_TOKEN`(`sy_pat_…`)은
9715
+ * 옛 CLI가 받은 폴더를 한 번 정리한다. `.sayren/state.json`(0.2.0 ~ 0.14.x)·`.sayrenrc`(0.1.x) → `.sayren/project.json`(옛 파일은 지운다), `.env`의 폐기된 `SAYREN_TOKEN`(`sy_pat_…`)은
9331
9716
  * 지운다(R1 뒤 쓸 수 없다). 다른 값이 든 `SAYREN_TOKEN` 줄은 두고(CLI는 읽지 않는다) 알린다. 정리했으면 한 줄로 알린다.
9332
9717
  */
9333
9718
  function migrateLegacy(dir, notify) {
9334
9719
  if (!isPulledDir(dir)) return;
9335
9720
  const done = [];
9721
+ const legacyState = join(dir, LEGACY_STATE_FILE);
9722
+ if (existsSync(legacyState)) {
9723
+ if (existsSync(join(dir, STATE_FILE))) rmSync(legacyState, { force: true });
9724
+ else writeState(dir, parseState(readFileSync(legacyState, "utf8"), LEGACY_STATE_FILE));
9725
+ done.push(`${LEGACY_STATE_FILE} → ${STATE_FILE}`);
9726
+ }
9336
9727
  const rcPath = join(dir, LEGACY_RC_FILE);
9337
9728
  if (existsSync(rcPath)) {
9338
9729
  const tracked = isGitTracked(dir, LEGACY_RC_FILE);
@@ -9953,7 +10344,7 @@ async function selectStore(auth, client, io, candidates, folderStore = null) {
9953
10344
 
9954
10345
  //#endregion
9955
10346
  //#region package.json
9956
- var version = "0.14.0";
10347
+ var version = "0.15.0";
9957
10348
 
9958
10349
  //#endregion
9959
10350
  //#region src/client.ts
@@ -11626,42 +12017,49 @@ const FORBIDDEN_SEGMENTS = new Set([
11626
12017
  ".nitro",
11627
12018
  ".vinxi"
11628
12019
  ]);
11629
- /** 쓸 수 있는 루트 파일 */
11630
- const WRITABLE_ROOT_FILES = new Set([
11631
- "README.md",
11632
- "tsconfig.json",
11633
- "vite.config.ts",
11634
- "vitest.config.ts",
11635
- "biome.json",
11636
- ".gitignore"
12020
+ /**
12021
+ * 작업 트리에 두지 않는 이름(어디에 있든) — 형식은 맞지만 추적하지 않는다(목록·스냅샷·업로드·쓰기에서 빠진다, #298 P0 거절 목록).
12022
+ * - `.dev.vars`·`.dev.vars.*`(이름 규칙은 `isDevVars`): wrangler 로컬 개발 비밀 값 파일. `.env*`와 같이 업로드·스냅샷·Git push에 들어가지 않게
12023
+ * - `.sayren`·`.sayrenrc`: CLI 연결·상태 파일(`.sayren/project.json`, 옛 `.sayrenrc`는 토큰이 든다). 받기(pull)가 로컬 연결을 덮지 않게
12024
+ * - `.npmrc`·`.yarnrc`·`.yarnrc.yml`·`bunfig.toml`: 레지스트리 인증 토큰이 들어가는 설치 설정. 레지스트리·스크립트 설정이 의존성 판정을 비켜 가지 않게
12025
+ * - `.pnpmfile.cjs`·`.pnpmfile.mjs`: 설치 중 실행되는 훅(`--ignore-scripts`와 무관하게 돈다). 판정한 설치 트리를 바꾸지 않게
12026
+ * - `coverage`·`.turbo`·`.cache`: 테스트·도구 산출물. `.DS_Store`·`Thumbs.db`: OS 부산물
12027
+ */
12028
+ const UNTRACKED_SEGMENTS = new Set([
12029
+ ".sayren",
12030
+ ".sayrenrc",
12031
+ ".npmrc",
12032
+ ".yarnrc",
12033
+ ".yarnrc.yml",
12034
+ "bunfig.toml",
12035
+ ".pnpmfile.cjs",
12036
+ ".pnpmfile.mjs",
12037
+ "coverage",
12038
+ ".turbo",
12039
+ ".cache",
12040
+ ".DS_Store",
12041
+ "Thumbs.db"
11637
12042
  ]);
12043
+ /** wrangler 로컬 비밀 값 파일(`.dev.vars`, 환경별 `.dev.vars.<env>`) — 어디에 있든 추적하지 않는다 */
12044
+ const isDevVars = (segment) => segment === ".dev.vars" || segment.startsWith(".dev.vars.");
12045
+ /** 루트에 두지 않는 이름 — `pnpm-workspace.yaml`은 사이트를 워크스페이스로 바꾸고 설치 설정(overrides 등)을 싣는다 */
12046
+ const UNTRACKED_ROOT_FILES = new Set(["pnpm-workspace.yaml"]);
11638
12047
  /**
11639
- * 읽기만 되는 루트 파일 — 의존성은 전용 경로로만 바꾼다(레지스트리 프록시와 잠금 파일을 함께 맞춘다).
11640
- * `CLAUDE.md`·`.mcp.json`은 템플릿이 싣는 에이전트 안내·MCP 연결 설정이다(2026-10-14 blank, #271부터 보일러플레이트 `boilerplate`) — 편집 도구는 바꾸지 않는다
12048
+ * 읽기만 되는 루트 파일 — `CLAUDE.md`·`.mcp.json`은 시작 코드가 싣는 에이전트 안내·MCP 연결 설정이다. 편집 도구는 바꾸지 않는다.
12049
+ * `package.json`·잠금 파일은 쓸 수 있다(#298 P0) — 바뀌면 api가 의존성을 다시 검사한다
11641
12050
  */
11642
- const READONLY_ROOT_FILES = new Set([
11643
- "package.json",
11644
- "pnpm-lock.yaml",
11645
- "CLAUDE.md",
11646
- ".mcp.json"
11647
- ]);
11648
- /** 읽기만 되는 폴더 — 템플릿이 싣는 Claude 스킬(`.claude/skills/…`, 2026-10-14). 셀러 로컬 Claude Code와 AI 상점 도우미가 같은 기준을 읽는다 */
12051
+ const READONLY_ROOT_FILES = new Set(["CLAUDE.md", ".mcp.json"]);
12052
+ /** 읽기만 되는 폴더 — 시작 코드가 싣는 Claude 스킬(`.claude/skills/…`, 2026-10-14). 셀러 로컬 Claude Code와 AI가 같은 기준을 읽는다 */
11649
12053
  const READONLY_DIRS = new Set([".claude"]);
11650
12054
  /**
11651
- * 쓸 수 있는 폴더 — 코드(`src`), 정적 파일(`public`), 화면 문구 카탈로그(`messages`, 템플릿 `src/i18n`이 `messages/ko.json`을 import한다, #73).
11652
- * 템플릿이 import하는 파일이 이 밖에 있으면 첫 작업 트리(source.tgz)를 풀 때 빠져 빌드·검사가 깨진다(#78)
12055
+ * 스냅샷 묶기에서 빼는 이름(`container-tree.ts`의 `--exclude`) — 금지 조각과 추적하지 않는 이름. 최종 거름은 `isTracked`다
12056
+ * (여기는 큰 설치물·산출물을 tar에 넣지 않으려는 것이다)
11653
12057
  */
11654
- const WRITABLE_DIRS = new Set([
11655
- "src",
11656
- "public",
11657
- "messages"
11658
- ]);
11659
- /** 작업 트리 루트에 둘 수 있는 이름(폴더·파일) — 컨테이너 스냅샷이 이 이름만 묶는다(`container-tree.ts`) */
11660
- const TRACKED_ROOT_NAMES = [
11661
- ...WRITABLE_DIRS,
11662
- ...READONLY_DIRS,
11663
- ...WRITABLE_ROOT_FILES,
11664
- ...READONLY_ROOT_FILES
12058
+ const SNAPSHOT_EXCLUDED_NAMES = [
12059
+ ...FORBIDDEN_SEGMENTS,
12060
+ ...UNTRACKED_SEGMENTS,
12061
+ ".dev.vars",
12062
+ ".dev.vars.*"
11665
12063
  ];
11666
12064
  /** 형식·금지 세그먼트만 본다(위치 규칙은 보지 않는다). 통과하면 null, 아니면 이유 */
11667
12065
  function formatProblem(path) {
@@ -11678,21 +12076,24 @@ function formatProblem(path) {
11678
12076
  }
11679
12077
  return null;
11680
12078
  }
11681
- /** 위치 규칙: 어떤 모드로 이 경로에 닿을 수 있는가 */
12079
+ /** 추적하지 않는 이름이면 이유, 아니면 null(형식은 이미 통과한 경로) */
12080
+ function untrackedReason(segments) {
12081
+ if (segments.some((s) => UNTRACKED_SEGMENTS.has(s) || isDevVars(s))) return "untracked";
12082
+ if (segments.length === 1 && UNTRACKED_ROOT_FILES.has(segments[0])) return "untracked";
12083
+ return null;
12084
+ }
12085
+ /**
12086
+ * 위치 규칙: 어떤 모드로 이 경로에 닿을 수 있는가. 거절 목록 방식이다(#298 P0) — 형식(금지 조각·`.env*`·문자·길이)과 추적하지 않는 이름만
12087
+ * 막고 나머지는 어디든 쓸 수 있다. 예외는 읽기만 되는 에이전트 안내(`CLAUDE.md`·`.mcp.json`·`.claude/**`)다
12088
+ */
11682
12089
  function accessOf(path) {
11683
12090
  if (formatProblem(path) !== null) return null;
11684
12091
  const segments = path.split("/");
12092
+ if (untrackedReason(segments) !== null) return null;
11685
12093
  const [first] = segments;
11686
- if (!first) return null;
11687
- if (segments.length === 1) {
11688
- if (WRITABLE_ROOT_FILES.has(first)) return "write";
11689
- if (READONLY_ROOT_FILES.has(first)) return "read";
11690
- return null;
11691
- }
11692
- const readonlyDir = READONLY_DIRS.has(first);
11693
- if (!readonlyDir && !WRITABLE_DIRS.has(first)) return null;
11694
- if (segments.slice(1).some((s) => s.startsWith("."))) return null;
11695
- return readonlyDir ? "read" : "write";
12094
+ if (segments.length === 1 && READONLY_ROOT_FILES.has(first)) return "read";
12095
+ if (segments.length > 1 && READONLY_DIRS.has(first)) return "read";
12096
+ return "write";
11696
12097
  }
11697
12098
 
11698
12099
  //#endregion
@@ -11806,18 +12207,19 @@ async function gunzip(bytes, maxBytes) {
11806
12207
  //#endregion
11807
12208
  //#region src/tree.ts
11808
12209
  /**
11809
- * 로컬 작업 트리 — 서버 작업 트리와 **같은 경로 규칙**(편집 Sandbox `paths.ts`의 `accessOf`)으로 고른다. `src/**`·`public/**`·`messages/**`·루트 설정
11810
- * 파일·`package.json`·`pnpm-lock.yaml`만 올리고, `.env*`·`.mcp.json`·`.sayren/`·`node_modules`·`dist`·`package-lock.json` 등은 올리지 않는다.
12210
+ * 로컬 작업 트리 — 서버 작업 트리와 **같은 경로 규칙**(편집 Sandbox `paths.ts`의 `accessOf`)으로 고른다. 거절 목록 방식이다(#298 P0): 폴더 구조와
12211
+ * 무관하게 올리고, `.env*`·`.mcp.json`·`.sayren/`·`.npmrc`·`node_modules`·`dist` 등 설치물·산출물·비밀·로컬 상태만 올리지 않는다.
11811
12212
  * 비밀·로컬 상태 파일(`isNeverUpload`)은 서버 경로 규칙과 별개로 CLI가 먼저 막는다 — 서버 규칙이 넓어져도 토큰을 올리지 않게.
11812
12213
  */
11813
12214
  /**
11814
- * 어떤 경우에도 올리지 않는 파일 — `.env*`(개발 서버 값), `.mcp.json`(토큰), `.sayren/`(CLI 상태), 옛 `.sayrenrc`.
12215
+ * 어떤 경우에도 올리지 않는 파일 — `.env*`(개발 서버 값), `.dev.vars*`(wrangler 로컬 비밀 값), `.mcp.json`(토큰), `.sayren/`(CLI 상태),
12216
+ * 옛 `.sayrenrc`, `.npmrc`(레지스트리 토큰).
11815
12217
  * 폴더 깊이와 무관하게 이름으로 본다.
11816
12218
  */
11817
12219
  function isNeverUpload(path) {
11818
12220
  const segments = path.split("/");
11819
12221
  const name = segments[segments.length - 1] ?? "";
11820
- return segments.some((segment) => segment.startsWith(".env") || segment === ".sayren") || name === ".mcp.json" || name === ".sayrenrc";
12222
+ return segments.some((segment) => segment.startsWith(".env") || segment === ".sayren" || segment === ".dev.vars" || segment.startsWith(".dev.vars.")) || name === ".mcp.json" || name === ".sayrenrc" || name === ".npmrc";
11821
12223
  }
11822
12224
  /** 걸어 들어가지 않는 폴더 — 설치물·산출물·캐시·VCS(경로 규칙이 어차피 막는다. 크기 때문에 읽지도 않는다) */
11823
12225
  const SKIP_DIRS$1 = new Set([
@@ -12380,6 +12782,107 @@ async function pickTemplate(templates, wanted, io) {
12380
12782
  if (index < 0) throw new CliError(cli_init_template_required({ list: labels.map((label) => ` ${label}`).join("\n") }));
12381
12783
  return templates[index];
12382
12784
  }
12785
+ /**
12786
+ * `sayren link [폴더]`(#315 C6) — 폴더를 상점의 호스팅 사이트에 연결한다. 상점은 `--store` → 토큰 범위 → 대화형 선택이고, 사이트가 없으면
12787
+ * 시작 코드 없이 만든다(`initialSource: "NONE"` — 오픈 준비 중·버전 없음, 첫 deploy가 첫 커밋). 연결 정보는 `.sayren/project.json`, 상점 값은
12788
+ * `env pull`과 같이 `.env`에 쓴다. 토큰은 어느 파일에도 쓰지 않는다.
12789
+ */
12790
+ async function link(flags, io) {
12791
+ const dir = resolve(io.cwd, flags.dir ?? ".");
12792
+ if (!existsSync(dir)) throw new CliError(cli_link_dir_missing({ dir: relativeOf(io, dir) }));
12793
+ migrateLegacy(dir, io.out);
12794
+ await linkDir(flags, dir, io);
12795
+ io.out(cli_link_next_steps());
12796
+ return 0;
12797
+ }
12798
+ /** link 본체 — `deploy`도 연결이 없으면 이것부터 부른다 */
12799
+ async function linkDir(flags, dir, io) {
12800
+ let previous = null;
12801
+ try {
12802
+ previous = readState(dir);
12803
+ } catch {
12804
+ previous = null;
12805
+ }
12806
+ const { auth, client } = await apiFor(flags, dir, previous, io);
12807
+ const storeId = auth.storeId;
12808
+ const hosting = await client.storefront.get();
12809
+ if (!hosting.editingEnabled) throw new CliError(cli_link_editing_disabled());
12810
+ let site = hosting.site;
12811
+ if (site) {
12812
+ const git$1 = await client.storefront.getGit().catch(() => null);
12813
+ if (git$1?.owner === "SELLER" && git$1.repository) throw new CliError(cli_deploy_seller_git({
12814
+ repository: git$1.repository,
12815
+ branch: git$1.productionBranch ?? "main"
12816
+ }));
12817
+ } else try {
12818
+ site = await client.storefront.create({ initialSource: "NONE" });
12819
+ io.out(cli_link_site_created({ url: site.url }));
12820
+ } catch (error) {
12821
+ const code = codeOf(error);
12822
+ if (code === "STOREFRONT_SLUG_TAKEN") throw new CliError(cli_link_slug_taken());
12823
+ if (code !== "STOREFRONT_SITE_EXISTS") throw error;
12824
+ site = (await client.storefront.get()).site;
12825
+ if (!site) throw error;
12826
+ }
12827
+ const same = previous?.storeId === storeId && previous.siteId === site.siteId;
12828
+ const state = {
12829
+ storeId,
12830
+ siteId: site.siteId,
12831
+ baseRef: same ? previous?.baseRef ?? null : null,
12832
+ ...auth.apiOrigin !== DEFAULT_API_ORIGIN ? { apiOrigin: auth.apiOrigin } : {}
12833
+ };
12834
+ writeState(dir, state);
12835
+ const storeCode = await storeCodeOf(client, storeId);
12836
+ writeStoreEnv(dir, auth.apiOrigin, storeCode);
12837
+ const addedIgnore = ensureGitignore(dir);
12838
+ if (writeMcpJson(dir, { storeId }) === "retired-token-removed") io.out(cli_notice({ message: retiredMcpTokenNotice() }));
12839
+ io.out(cli_link_done({
12840
+ storeId,
12841
+ url: site.url,
12842
+ file: STATE_FILE,
12843
+ envFile: ENV_FILE,
12844
+ storeCode
12845
+ }));
12846
+ if (addedIgnore.length) io.out(cli_link_gitignore({ lines: addedIgnore.join(" ") }));
12847
+ return {
12848
+ client,
12849
+ state
12850
+ };
12851
+ }
12852
+ /** 상점 코드 — 계정의 상점 목록(`stores:read`), 안 되면 작업 트리 조회(`storefront:r`) */
12853
+ async function storeCodeOf(client, storeId) {
12854
+ const listed = await client.auth.listMyStores().then((stores) => stores.find((store) => store.storeId === storeId)?.storeCode).catch(() => void 0);
12855
+ if (listed) return listed;
12856
+ const fromSource = await client.storefront.getSource().then((source) => source.storeCode).catch(() => void 0);
12857
+ if (fromSource) return fromSource;
12858
+ throw new CliError(cli_env_store_code_unknown());
12859
+ }
12860
+ /** `.env`의 상점 연결 값 — 다른 줄은 둔다. 토큰 줄(`SAYREN_TOKEN`)은 지운다 */
12861
+ function writeStoreEnv(dir, apiOrigin, storeCode) {
12862
+ updateEnvFile(dir, {
12863
+ SAYREN_API_URL: `${apiOrigin}/storefront/v1`,
12864
+ SAYREN_STORE_CODE: storeCode
12865
+ }, ["SAYREN_TOKEN"]);
12866
+ }
12867
+ /** `sayren env pull` — 연결된 상점의 `SAYREN_STORE_CODE`·`SAYREN_API_URL`을 `.env`에 쓴다 */
12868
+ async function envPull(flags, io) {
12869
+ const dir = resolve(io.cwd, flags.dir ?? ".");
12870
+ migrateLegacy(dir, io.out);
12871
+ const state = readState(dir);
12872
+ if (!state) throw new CliError(cli_env_not_linked({ file: STATE_FILE }));
12873
+ const { auth, client } = await apiFor({
12874
+ ...flags,
12875
+ store: state.storeId
12876
+ }, dir, state, io);
12877
+ const storeCode = await storeCodeOf(client, state.storeId);
12878
+ writeStoreEnv(dir, auth.apiOrigin, storeCode);
12879
+ ensureGitignore(dir);
12880
+ io.out(cli_env_pulled({
12881
+ file: ENV_FILE,
12882
+ storeCode
12883
+ }));
12884
+ return 0;
12885
+ }
12383
12886
  async function verify(flags, io) {
12384
12887
  const dir = resolve(io.cwd, flags.dir ?? ".");
12385
12888
  const tree = collectTree(dir);
@@ -12414,38 +12917,30 @@ function printChecks(tree, checks, io) {
12414
12917
  }) : cli_skipped_outside({ paths })}`);
12415
12918
  }
12416
12919
  }
12920
+ /**
12921
+ * `deploy` — 연결 정보(`.sayren/project.json`)가 없으면 `link`부터 한다(#315 C6, Vercel 방식). 막 연결한 폴더는 기준(`baseRef`)이 없어
12922
+ * 확인 없이 다음 버전으로 교체한다(사이트에 소스가 없으면 첫 커밋). 그 뒤 deploy는 올린 트리를 기준으로 조건부 교체한다.
12923
+ */
12417
12924
  async function deploy(flags, io) {
12418
12925
  const dir = resolve(io.cwd, flags.dir ?? ".");
12419
12926
  migrateLegacy(dir, io.out);
12420
- const rc = readState(dir);
12421
- if (!rc) {
12422
- let storeId = null;
12423
- try {
12424
- storeId = readMcpStoreId(dir);
12425
- } catch {
12426
- storeId = null;
12427
- }
12428
- const git$1 = await apiFor(flags, dir, null, {
12429
- ...io,
12430
- select: null,
12431
- confirm: null
12432
- }).then(({ client: client$1 }) => client$1.storefront.getGit()).catch(() => null);
12433
- if (git$1?.owner === "SELLER" && git$1.repository) throw new CliError(cli_deploy_seller_git({
12434
- repository: git$1.repository,
12435
- branch: git$1.productionBranch ?? "main"
12436
- }));
12437
- throw new CliError(cli_deploy_no_state({
12438
- stateFile: STATE_FILE,
12439
- storeId: storeId ?? "<storeId>",
12440
- dir: relativeOf(io, dir)
12441
- }));
12442
- }
12443
- const { client } = await apiFor(flags, dir, rc, io);
12444
- const tree = collectTree(dir);
12927
+ const prior = readState(dir);
12928
+ const linked = prior ? await apiFor(flags, dir, prior, io) : null;
12929
+ let tree = collectTree(dir);
12445
12930
  const checks = runLocalChecks(tree);
12446
12931
  printChecks(tree, checks, io);
12447
12932
  if (checks.blockingRules.length > 0 || checks.problems.length > 0) throw new CliError(cli_deploy_checks_failed());
12448
12933
  await localBuild(dir, flags.skipBuild === true, io);
12934
+ let client;
12935
+ let rc;
12936
+ if (linked && prior) {
12937
+ client = linked.client;
12938
+ rc = prior;
12939
+ } else {
12940
+ io.out(cli_deploy_linking({ dir: relativeOf(io, dir) }));
12941
+ ({client, state: rc} = await linkDir(flags, dir, io));
12942
+ tree = collectTree(dir);
12943
+ }
12449
12944
  const site = (await client.storefront.get()).site;
12450
12945
  if (!site || site.siteId !== rc.siteId) throw new CliError(cli_deploy_site_mismatch({ siteId: rc.siteId }));
12451
12946
  if (flags.force && !flags.yes) {
@@ -12499,8 +12994,21 @@ async function deploy(flags, io) {
12499
12994
  io.out(cli_deploy_not_published({ version: created.version.number }));
12500
12995
  return 0;
12501
12996
  }
12997
+ let current = site.publishedVersion;
12998
+ if (!current) {
12999
+ const after = (await client.storefront.get()).site;
13000
+ if (after?.publishedVersion?.versionId === created.version.versionId) {
13001
+ io.out(cli_deploy_published({
13002
+ version: created.version.number,
13003
+ url: after.url
13004
+ }));
13005
+ io.out(cli_deploy_publish_delay());
13006
+ return 0;
13007
+ }
13008
+ current = after?.publishedVersion ?? null;
13009
+ }
12502
13010
  try {
12503
- const published = await client.storefront.publish(created.version.versionId, { expectedPublishedVersionId: site.publishedVersion?.versionId ?? null });
13011
+ const published = await client.storefront.publish(created.version.versionId, { expectedPublishedVersionId: current?.versionId ?? null });
12504
13012
  io.out(cli_deploy_published({
12505
13013
  version: created.version.number,
12506
13014
  url: published.url
@@ -12567,7 +13075,7 @@ async function replaceSource(client, tree, rc, force, io) {
12567
13075
  const packageJson = textOf("package.json");
12568
13076
  const lockfile = textOf("pnpm-lock.yaml");
12569
13077
  const body = {
12570
- ...force ? { endEditSession: true } : { expectedBaseRef: rc.baseRef },
13078
+ ...force ? { endEditSession: true } : rc.baseRef ? { expectedBaseRef: rc.baseRef } : {},
12571
13079
  files,
12572
13080
  ...packageJson === void 0 ? {} : { packageJson },
12573
13081
  ...lockfile === void 0 ? {} : { lockfile }
@@ -12739,6 +13247,7 @@ async function status(flags, io) {
12739
13247
  io.out(` ${editing.session ? cli_status_editing_open({ status: editing.session.status }) : cli_status_editing_closed()}`);
12740
13248
  }
12741
13249
  if (rc) if (rc.siteId !== site.siteId) io.out(` ${cli_status_folder_other_site({ siteId: rc.siteId })}`);
13250
+ else if (!rc.baseRef) io.out(` ${cli_status_folder_linked_only()}`);
12742
13251
  else io.out(` ${cli_status_folder_base({ ref: rc.baseRef.slice(rc.baseRef.lastIndexOf("/") + 1, -4).slice(0, 12) })}`);
12743
13252
  return 0;
12744
13253
  }
@@ -12871,6 +13380,57 @@ function relativeOf(io, path) {
12871
13380
  return relative(io.cwd, path) || ".";
12872
13381
  }
12873
13382
 
13383
+ //#endregion
13384
+ //#region ../mcp/src/starter-transform.ts
13385
+ var StarterTransformError = class extends Error {
13386
+ code;
13387
+ constructor(code, message) {
13388
+ super(message);
13389
+ this.name = "StarterTransformError";
13390
+ this.code = code;
13391
+ }
13392
+ };
13393
+ /** 새 프로젝트로 옮기지 않는 루트 경로인가 — 로컬 env·CLI 연결 정보 */
13394
+ function isStarterLocalOnly(path) {
13395
+ if (path === ".sayren" || path.startsWith(".sayren/")) return true;
13396
+ if (path.includes("/")) return false;
13397
+ return (path === ".env" || path.startsWith(".env.")) && path !== ".env.example";
13398
+ }
13399
+ function transformStarterFiles(files, options) {
13400
+ const out = /* @__PURE__ */ new Map();
13401
+ for (const [path, bytes] of files) {
13402
+ if (isStarterLocalOnly(path)) continue;
13403
+ const slash = path.lastIndexOf("/");
13404
+ const base = path.slice(slash + 1);
13405
+ out.set(base === "_gitignore" ? `${path.slice(0, slash + 1)}.gitignore` : path, bytes);
13406
+ }
13407
+ const raw = out.get("package.json");
13408
+ if (!raw) throw new StarterTransformError("STARTER_PACKAGE_JSON_MISSING", "package.json이 없습니다");
13409
+ const next = withPackageName(raw, options.name, { private: true });
13410
+ if (!next) throw new StarterTransformError("STARTER_PACKAGE_JSON_INVALID", "package.json을 JSON으로 읽을 수 없습니다");
13411
+ out.set("package.json", next);
13412
+ return out;
13413
+ }
13414
+ /**
13415
+ * `package.json`의 `name`을 바꾼다(`fields`가 있으면 그 필드도). 나머지 필드·순서는 그대로다. JSON 객체가 아니면 null이다.
13416
+ * 편집 Sandbox의 시작 코드 사이트(이미 정규화된 R2 원본)는 이것만 부른다 — 서버가 바꾸는 것은 `name` 하나다.
13417
+ */
13418
+ function withPackageName(bytes, name, fields = {}) {
13419
+ let pkg;
13420
+ try {
13421
+ pkg = JSON.parse(new TextDecoder().decode(bytes));
13422
+ } catch {
13423
+ return null;
13424
+ }
13425
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
13426
+ const next = {
13427
+ ...pkg,
13428
+ name,
13429
+ ...fields
13430
+ };
13431
+ return new TextEncoder().encode(`${JSON.stringify(next, null, 2)}\n`);
13432
+ }
13433
+
12874
13434
  //#endregion
12875
13435
  //#region ../mcp/src/template-package.ts
12876
13436
  /** package.json의 기준 템플릿 표식 필드 — 원천은 이 함수다(api `template-marker.ts`가 같은 모양을 읽는다) */
@@ -12951,11 +13511,13 @@ function readWorkspaceVersions(packagesDir) {
12951
13511
  //#endregion
12952
13512
  //#region src/create-command.ts
12953
13513
  /**
12954
- * `npx sayren create <이름>`(#271) — 스토어프론트 보일러플레이트를 새 폴더에 쓴다. 원천은 `packages/cli/boilerplate` 하나이고
12955
- * 호스팅 「처음부터」 템플릿(카탈로그 `boilerplate`)도 같은 폴더에서 만든다. 로그인·사이트 만들기는 하지 않는다(사이트는 `init`).
12956
- * 상점 연결 값은 `--store-code` → 묻기(대화형) 순서이고 `.env`에 `SAYREN_API_URL`·`SAYREN_STORE_CODE`로 쓴다.
13514
+ * `npx sayren create <이름> [--template <템플릿>]`(#271, #315 C7) — 시작 코드를 새 폴더에 쓴다. 파일만 만들고 상점을 묻지 않는다.
13515
+ * 상점 연결은 `sayren link`(사이트·`.sayren/project.json`)와 `sayren env pull`(`.env`)이 한다. `--store-code`를 주면 그 값으로 `.env`만 쓴다.
13516
+ *
13517
+ * 템플릿: 없으면 CLI에 묶은 시작 코드(네트워크 없음), `github:owner/repo[/경로][#ref]`는 codeload tarball, 그 밖의 `<id>`는 sayren 레지스트리
13518
+ * 자리다(마켓 전까지 늘 「템플릿을 찾을 수 없습니다」). 받은 소스는 변환 함수 하나(`starter-transform.ts`, 서버와 공유)를 지난다.
12957
13519
  */
12958
- /** 보일러플레이트 위치 — 게시본은 빌드가 복사한 `dist/boilerplate`, 저장소에서 바로 돌리면 `packages/cli/boilerplate` */
13520
+ /** 묶은 시작 코드 위치 — 게시본은 빌드가 묶은 `dist/boilerplate`, 저장소에서 바로 돌리면 원천 `packages/cli/boilerplate` */
12959
13521
  function boilerplateRoot() {
12960
13522
  const here = dirname(fileURLToPath(import.meta.url));
12961
13523
  const candidates = [join(here, "boilerplate"), join(here, "..", "boilerplate")];
@@ -12963,18 +13525,7 @@ function boilerplateRoot() {
12963
13525
  if (!found) throw new Error(`boilerplate not found: ${candidates.join(", ")}`);
12964
13526
  return found;
12965
13527
  }
12966
- /** 빌드가 남기는 버전 표(`scripts/copy-boilerplate.mjs`) — 게시본에는 pnpm-workspace.yaml이 없다 */
12967
- const BOILERPLATE_META_FILE = ".boilerplate-meta.json";
12968
- function metaOf(root) {
12969
- const bundled = join(root, BOILERPLATE_META_FILE);
12970
- if (existsSync(bundled)) return JSON.parse(readFileSync(bundled, "utf8"));
12971
- const repo = join(root, "..", "..", "..");
12972
- return {
12973
- catalog: parseCatalog(readFileSync(join(repo, "pnpm-workspace.yaml"), "utf8")),
12974
- versions: readWorkspaceVersions(join(repo, "packages"))
12975
- };
12976
- }
12977
- /** 싣지 않는 것 — 설치물·산출물·로컬 env. 점 파일은 에이전트 안내(`.claude/`·`.mcp.json`)와 `.gitignore`만 */
13528
+ /** 싣지 않는 것 — 설치물·산출물. 점 파일은 에이전트 안내(`.claude/`·`.mcp.json`)와 `.gitignore`만(scripts/starter-bundle.mjs와 같다) */
12978
13529
  const SKIP_DIRS = new Set([
12979
13530
  "node_modules",
12980
13531
  "dist",
@@ -12988,8 +13539,11 @@ const DOT_KEPT = new Set([
12988
13539
  ".mcp.json",
12989
13540
  ".gitignore"
12990
13541
  ]);
12991
- /** 보일러플레이트 파일 — 상대 경로 → 내용. 게시본의 `_gitignore`는 `.gitignore`로 되돌린다(npm이 `.gitignore`를 싣지 않는다) */
12992
- function boilerplateFiles(root, name) {
13542
+ /**
13543
+ * 묶은 시작 코드 — 상대 경로 → 내용. 게시본은 빌드가 이미 npm 범위로 풀었다. 저장소 원천(워크스페이스 구성원)에서 바로 돌리면
13544
+ * `catalog:`·`workspace:`를 여기서 푼다(게시본과 같은 결과).
13545
+ */
13546
+ function bundledStarterFiles(root) {
12993
13547
  const files = /* @__PURE__ */ new Map();
12994
13548
  const walk = (dir) => {
12995
13549
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -12997,16 +13551,86 @@ function boilerplateFiles(root, name) {
12997
13551
  if (entry.name.startsWith(".") && !DOT_KEPT.has(entry.name)) continue;
12998
13552
  const full = join(dir, entry.name);
12999
13553
  if (entry.isDirectory()) walk(full);
13000
- else if (entry.isFile()) {
13001
- const path = relative(root, full).split(sep).join("/");
13002
- files.set(path === "_gitignore" ? ".gitignore" : path, readFileSync(full));
13003
- }
13554
+ else if (entry.isFile()) files.set(relative(root, full).split(sep).join("/"), readFileSync(full));
13004
13555
  }
13005
13556
  };
13006
13557
  walk(root);
13558
+ const repo = join(root, "..", "..", "..");
13007
13559
  const pkg = files.get("package.json");
13008
- if (!pkg) throw new Error("boilerplate package.json missing");
13009
- files.set("package.json", Buffer.from(renderTemplatePackageJson(pkg.toString("utf8"), metaOf(root), { name })));
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)));
13566
+ }
13567
+ return files;
13568
+ }
13569
+ const GITHUB_OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
13570
+ const GITHUB_REPO = /^[A-Za-z0-9._-]{1,100}$/;
13571
+ const PATH_SEGMENT = /^[A-Za-z0-9._@+-]{1,255}$/;
13572
+ const GIT_REF = /^[A-Za-z0-9._/+-]{1,255}$/;
13573
+ /** `--template` 값 → 받을 곳. 형식이 틀리면 api·네트워크를 부르기 전에 멈춘다 */
13574
+ function parseTemplateSpec(value) {
13575
+ const raw = value?.trim();
13576
+ if (!raw) return { kind: "bundled" };
13577
+ if (!raw.startsWith("github:")) return {
13578
+ kind: "registry",
13579
+ id: raw
13580
+ };
13581
+ const body = raw.slice(7);
13582
+ const hash = body.indexOf("#");
13583
+ const location = hash < 0 ? body : body.slice(0, hash);
13584
+ const ref = hash < 0 ? null : body.slice(hash + 1);
13585
+ const [owner = "", repo = "", ...rest] = location.split("/");
13586
+ if (!(GITHUB_OWNER.test(owner) && GITHUB_REPO.test(repo) && repo !== "." && repo !== ".." && rest.every((segment) => PATH_SEGMENT.test(segment) && segment !== "." && segment !== "..") && (ref === null || GIT_REF.test(ref) && !ref.split("/").some((s) => s === "" || s === "." || s === "..")))) throw new CliError(cli_create_template_invalid({ template: raw }));
13587
+ return {
13588
+ kind: "github",
13589
+ owner,
13590
+ repo,
13591
+ path: rest.join("/"),
13592
+ ref
13593
+ };
13594
+ }
13595
+ /** codeload tarball 주소 — ref가 없으면 기본 브랜치(`HEAD`) */
13596
+ function codeloadUrlOf(spec) {
13597
+ const ref = spec.ref ? spec.ref.split("/").map(encodeURIComponent).join("/") : "HEAD";
13598
+ return `https://codeload.github.com/${spec.owner}/${spec.repo}/tar.gz/${ref}`;
13599
+ }
13600
+ /** 받은 저장소 tarball을 풀어 둘 상한(압축 폭탄 방어선). 템플릿 저장소가 모노레포여도 정상 저장소는 닿지 않는다 */
13601
+ const TEMPLATE_ARCHIVE_MAX_BYTES = 256 * 1024 * 1024;
13602
+ /** GitHub 템플릿 — tarball을 받아 맨 위 폴더(`{repo}-{sha}/`)를 벗기고 `경로` 아래만 남긴다 */
13603
+ async function githubTemplateFiles(spec, label, io) {
13604
+ let res;
13605
+ try {
13606
+ res = await io.fetch(codeloadUrlOf(spec));
13607
+ } catch {
13608
+ throw new CliError(cli_create_template_unreachable({ template: label }));
13609
+ }
13610
+ if (res.status === 404) throw new CliError(cli_create_template_github_not_found({ template: label }));
13611
+ if (!res.ok) throw new CliError(cli_create_template_download_failed({
13612
+ template: label,
13613
+ status: String(res.status)
13614
+ }));
13615
+ let entries;
13616
+ try {
13617
+ entries = readTar(await gunzip(new Uint8Array(await res.arrayBuffer()), TEMPLATE_ARCHIVE_MAX_BYTES), TEMPLATE_ARCHIVE_MAX_BYTES);
13618
+ } catch (error) {
13619
+ if (error instanceof TarLimitError) throw new CliError(cli_create_template_too_large({ template: label }));
13620
+ throw new CliError(cli_create_template_unreadable({ template: label }));
13621
+ }
13622
+ const prefix = spec.path ? `${spec.path}/` : "";
13623
+ const files = /* @__PURE__ */ new Map();
13624
+ for (const entry of entries) {
13625
+ const slash = entry.path.indexOf("/");
13626
+ if (slash < 0) continue;
13627
+ const inRepo = entry.path.slice(slash + 1);
13628
+ if (!inRepo.startsWith(prefix)) continue;
13629
+ const path = inRepo.slice(prefix.length);
13630
+ if (!path || path.startsWith("/") || path.split("/").some((s) => s === "" || s === "..")) continue;
13631
+ files.set(path, entry.bytes);
13632
+ }
13633
+ if (files.size === 0) throw new CliError(cli_create_template_path_empty({ template: label }));
13010
13634
  return files;
13011
13635
  }
13012
13636
  /** npm 패키지 이름으로도 쓰는 폴더 이름 — 범위(@scope/) 없는 이름만 */
@@ -13017,24 +13641,33 @@ async function create(flags, io) {
13017
13641
  if (!NAME_PATTERN.test(name)) throw new CliError(cli_create_name_invalid({ name }));
13018
13642
  const target = resolve(io.cwd, name);
13019
13643
  if (existsSync(target) && (!statSync(target).isDirectory() || readdirSync(target).length > 0)) throw new CliError(cli_create_dir_not_empty({ dir: name }));
13020
- const storeCode = (flags.storeCode ?? (io.ask ? await io.ask(cli_create_prompt_store_code()) : null))?.trim();
13021
- if (!storeCode) throw new CliError(cli_create_store_code_required());
13022
- if (!STORE_CODE_PATTERN.test(storeCode)) throw new CliError(cli_create_store_code_invalid({ storeCode }));
13023
- const { apiOrigin } = resolveApiOrigin(flags, target, null, io.env);
13024
- const files = boilerplateFiles(boilerplateRoot(), name);
13025
- for (const [path, bytes] of files) {
13026
- const file = join(target, path);
13027
- mkdirSync(dirname(file), { recursive: true });
13028
- writeFileSync(file, bytes);
13644
+ const storeCode = flags.storeCode?.trim();
13645
+ if (storeCode !== void 0 && !STORE_CODE_PATTERN.test(storeCode)) throw new CliError(cli_create_store_code_invalid({ storeCode }));
13646
+ const spec = parseTemplateSpec(flags.template);
13647
+ 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());
13649
+ let files;
13650
+ try {
13651
+ files = transformStarterFiles(source, { name });
13652
+ } catch (error) {
13653
+ if (error instanceof StarterTransformError) throw new CliError(cli_create_template_no_package_json({ reason: error.message }));
13654
+ throw error;
13029
13655
  }
13030
- updateEnvFile(target, {
13031
- SAYREN_API_URL: `${apiOrigin}/storefront/v1`,
13032
- SAYREN_STORE_CODE: storeCode
13033
- });
13034
- io.out(cli_create_done({
13656
+ writeTree(target, files);
13657
+ if (storeCode) {
13658
+ const { apiOrigin } = resolveApiOrigin(flags, target, null, io.env);
13659
+ updateEnvFile(target, {
13660
+ SAYREN_API_URL: `${apiOrigin}/storefront/v1`,
13661
+ SAYREN_STORE_CODE: storeCode
13662
+ });
13663
+ io.out(cli_create_done_store_code({
13664
+ dir: name,
13665
+ count: files.size,
13666
+ storeCode
13667
+ }));
13668
+ } else io.out(cli_create_done({
13035
13669
  dir: name,
13036
- count: files.size,
13037
- storeCode
13670
+ count: files.size
13038
13671
  }));
13039
13672
  return 0;
13040
13673
  }
@@ -13183,8 +13816,16 @@ async function main(argv, io) {
13183
13816
  case "create": return create({
13184
13817
  name: parsed.positional[0],
13185
13818
  storeCode: str(parsed.flags["store-code"]),
13186
- apiOrigin: str(parsed.flags["api-origin"])
13819
+ apiOrigin: str(parsed.flags["api-origin"]),
13820
+ template: str(parsed.flags.template)
13187
13821
  }, io);
13822
+ case "link": return link(common, io);
13823
+ case "env":
13824
+ if (parsed.positional[0] !== "pull") throw new CliError(cli_env_usage());
13825
+ return envPull({
13826
+ ...common,
13827
+ dir: str(parsed.flags.dir) ?? parsed.positional[1]
13828
+ }, io);
13188
13829
  case "init": return init({
13189
13830
  ...common,
13190
13831
  template: str(parsed.flags.template),