sayren 0.14.0 → 0.15.1

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);
@@ -5511,24 +5511,6 @@ const cli_checks_lock_drift = ((inputs, options = {}) => {
5511
5511
  return ko_cli_checks_lock_drift(inputs);
5512
5512
  });
5513
5513
 
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
5514
  //#endregion
5533
5515
  //#region src/paraglide/messages/cli_checks_summary.js
5534
5516
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -5586,12 +5568,13 @@ const cli_create_dir_not_empty = ((inputs, options = {}) => {
5586
5568
  //#endregion
5587
5569
  //#region src/paraglide/messages/cli_create_done.js
5588
5570
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5589
- /** @typedef {{ dir: NonNullable<unknown>, count: NonNullable<unknown>, storeCode: NonNullable<unknown> }} Cli_Create_DoneInputs */
5571
+ /** @typedef {{ dir: NonNullable<unknown>, count: NonNullable<unknown> }} Cli_Create_DoneInputs */
5590
5572
  const ko_cli_create_done = (i) => {
5591
- return `보일러플레이트를 만들었습니다 — ${i?.dir} (파일 ${i?.count}개, .env에 상점 ${i?.storeCode} 연결 값)
5573
+ return `시작 코드를 만들었습니다 — ${i?.dir} (파일 ${i?.count}개)
5592
5574
  다음:
5593
5575
  cd ${i?.dir}
5594
5576
  npm install
5577
+ npx sayren link # 상점에 연결하고 .env에 상점 값을 씁니다
5595
5578
  npm run dev`;
5596
5579
  };
5597
5580
  /**
@@ -5606,39 +5589,44 @@ const cli_create_done = ((inputs, options = {}) => {
5606
5589
  });
5607
5590
 
5608
5591
  //#endregion
5609
- //#region src/paraglide/messages/cli_create_name_invalid.js
5592
+ //#region src/paraglide/messages/cli_create_done_store_code.js
5610
5593
  /** @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}`;
5594
+ /** @typedef {{ dir: NonNullable<unknown>, count: NonNullable<unknown>, storeCode: NonNullable<unknown> }} Cli_Create_Done_Store_CodeInputs */
5595
+ const ko_cli_create_done_store_code = (i) => {
5596
+ return `시작 코드를 만들었습니다 — ${i?.dir} (파일 ${i?.count}개, .env에 상점 ${i?.storeCode} 연결 값)
5597
+ 다음:
5598
+ cd ${i?.dir}
5599
+ npm install
5600
+ npm run dev
5601
+ npx sayren deploy # 처음이면 상점 사이트에 연결한 뒤 배포합니다`;
5614
5602
  };
5615
5603
  /**
5616
5604
 
5617
- * @param {Cli_Create_Name_InvalidInputs} inputs
5605
+ * @param {Cli_Create_Done_Store_CodeInputs} inputs
5618
5606
  * @param {{ locale?: "ko" }} options
5619
5607
  * @returns {LocalizedString}
5620
5608
  */
5621
- const cli_create_name_invalid = ((inputs, options = {}) => {
5609
+ const cli_create_done_store_code = ((inputs, options = {}) => {
5622
5610
  experimentalStaticLocale ?? options.locale ?? getLocale();
5623
- return ko_cli_create_name_invalid(inputs);
5611
+ return ko_cli_create_done_store_code(inputs);
5624
5612
  });
5625
5613
 
5626
5614
  //#endregion
5627
- //#region src/paraglide/messages/cli_create_prompt_store_code.js
5615
+ //#region src/paraglide/messages/cli_create_name_invalid.js
5628
5616
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5629
- /** @typedef {{}} Cli_Create_Prompt_Store_CodeInputs */
5630
- const ko_cli_create_prompt_store_code = () => {
5631
- return `상점 코드(상점 플랫폼 › 설정 › 상점 정보): `;
5617
+ /** @typedef {{ name: NonNullable<unknown> }} Cli_Create_Name_InvalidInputs */
5618
+ const ko_cli_create_name_invalid = (i) => {
5619
+ return `폴더 이름은 영문 소문자·숫자·.·_·-로 쓰고 영문 소문자나 숫자로 시작합니다: ${i?.name}`;
5632
5620
  };
5633
5621
  /**
5634
5622
 
5635
- * @param {Cli_Create_Prompt_Store_CodeInputs} inputs
5623
+ * @param {Cli_Create_Name_InvalidInputs} inputs
5636
5624
  * @param {{ locale?: "ko" }} options
5637
5625
  * @returns {LocalizedString}
5638
5626
  */
5639
- const cli_create_prompt_store_code = ((inputs = {}, options = {}) => {
5627
+ const cli_create_name_invalid = ((inputs, options = {}) => {
5640
5628
  experimentalStaticLocale ?? options.locale ?? getLocale();
5641
- return ko_cli_create_prompt_store_code(inputs);
5629
+ return ko_cli_create_name_invalid(inputs);
5642
5630
  });
5643
5631
 
5644
5632
  //#endregion
@@ -5660,21 +5648,165 @@ const cli_create_store_code_invalid = ((inputs, options = {}) => {
5660
5648
  });
5661
5649
 
5662
5650
  //#endregion
5663
- //#region src/paraglide/messages/cli_create_store_code_required.js
5651
+ //#region src/paraglide/messages/cli_create_template_download_failed.js
5652
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5653
+ /** @typedef {{ template: NonNullable<unknown>, status: NonNullable<unknown> }} Cli_Create_Template_Download_FailedInputs */
5654
+ const ko_cli_create_template_download_failed = (i) => {
5655
+ return `템플릿을 받지 못했습니다: ${i?.template} (HTTP ${i?.status})`;
5656
+ };
5657
+ /**
5658
+
5659
+ * @param {Cli_Create_Template_Download_FailedInputs} inputs
5660
+ * @param {{ locale?: "ko" }} options
5661
+ * @returns {LocalizedString}
5662
+ */
5663
+ const cli_create_template_download_failed = ((inputs, options = {}) => {
5664
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5665
+ return ko_cli_create_template_download_failed(inputs);
5666
+ });
5667
+
5668
+ //#endregion
5669
+ //#region src/paraglide/messages/cli_create_template_github_not_found.js
5670
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5671
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_Github_Not_FoundInputs */
5672
+ const ko_cli_create_template_github_not_found = (i) => {
5673
+ return `GitHub에서 템플릿을 찾을 수 없습니다: ${i?.template}. 공개 저장소·ref를 확인하십시오`;
5674
+ };
5675
+ /**
5676
+
5677
+ * @param {Cli_Create_Template_Github_Not_FoundInputs} inputs
5678
+ * @param {{ locale?: "ko" }} options
5679
+ * @returns {LocalizedString}
5680
+ */
5681
+ const cli_create_template_github_not_found = ((inputs, options = {}) => {
5682
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5683
+ return ko_cli_create_template_github_not_found(inputs);
5684
+ });
5685
+
5686
+ //#endregion
5687
+ //#region src/paraglide/messages/cli_create_template_invalid.js
5664
5688
  /** @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 <상점 코드>로 주십시오(비대화형은 필수입니다)`;
5689
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_InvalidInputs */
5690
+ const ko_cli_create_template_invalid = (i) => {
5691
+ return `템플릿 형식이 아닙니다: ${i?.template}. github:owner/repo[/경로][#ref] 또는 템플릿 id를 주십시오`;
5668
5692
  };
5669
5693
  /**
5670
5694
 
5671
- * @param {Cli_Create_Store_Code_RequiredInputs} inputs
5695
+ * @param {Cli_Create_Template_InvalidInputs} inputs
5672
5696
  * @param {{ locale?: "ko" }} options
5673
5697
  * @returns {LocalizedString}
5674
5698
  */
5675
- const cli_create_store_code_required = ((inputs = {}, options = {}) => {
5699
+ const cli_create_template_invalid = ((inputs, options = {}) => {
5676
5700
  experimentalStaticLocale ?? options.locale ?? getLocale();
5677
- return ko_cli_create_store_code_required(inputs);
5701
+ return ko_cli_create_template_invalid(inputs);
5702
+ });
5703
+
5704
+ //#endregion
5705
+ //#region src/paraglide/messages/cli_create_template_no_package_json.js
5706
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5707
+ /** @typedef {{ reason: NonNullable<unknown> }} Cli_Create_Template_No_Package_JsonInputs */
5708
+ const ko_cli_create_template_no_package_json = (i) => {
5709
+ return `시작 코드로 쓸 수 없는 템플릿입니다(${i?.reason})`;
5710
+ };
5711
+ /**
5712
+
5713
+ * @param {Cli_Create_Template_No_Package_JsonInputs} inputs
5714
+ * @param {{ locale?: "ko" }} options
5715
+ * @returns {LocalizedString}
5716
+ */
5717
+ const cli_create_template_no_package_json = ((inputs, options = {}) => {
5718
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5719
+ return ko_cli_create_template_no_package_json(inputs);
5720
+ });
5721
+
5722
+ //#endregion
5723
+ //#region src/paraglide/messages/cli_create_template_not_found.js
5724
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5725
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_Not_FoundInputs */
5726
+ const ko_cli_create_template_not_found = (i) => {
5727
+ return `템플릿을 찾을 수 없습니다: ${i?.template}`;
5728
+ };
5729
+ /**
5730
+
5731
+ * @param {Cli_Create_Template_Not_FoundInputs} inputs
5732
+ * @param {{ locale?: "ko" }} options
5733
+ * @returns {LocalizedString}
5734
+ */
5735
+ const cli_create_template_not_found = ((inputs, options = {}) => {
5736
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5737
+ return ko_cli_create_template_not_found(inputs);
5738
+ });
5739
+
5740
+ //#endregion
5741
+ //#region src/paraglide/messages/cli_create_template_path_empty.js
5742
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5743
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_Path_EmptyInputs */
5744
+ const ko_cli_create_template_path_empty = (i) => {
5745
+ return `템플릿 경로에 파일이 없습니다: ${i?.template}`;
5746
+ };
5747
+ /**
5748
+
5749
+ * @param {Cli_Create_Template_Path_EmptyInputs} inputs
5750
+ * @param {{ locale?: "ko" }} options
5751
+ * @returns {LocalizedString}
5752
+ */
5753
+ const cli_create_template_path_empty = ((inputs, options = {}) => {
5754
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5755
+ return ko_cli_create_template_path_empty(inputs);
5756
+ });
5757
+
5758
+ //#endregion
5759
+ //#region src/paraglide/messages/cli_create_template_too_large.js
5760
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5761
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_Too_LargeInputs */
5762
+ const ko_cli_create_template_too_large = (i) => {
5763
+ return `템플릿 저장소가 너무 커서 받지 않았습니다: ${i?.template}`;
5764
+ };
5765
+ /**
5766
+
5767
+ * @param {Cli_Create_Template_Too_LargeInputs} inputs
5768
+ * @param {{ locale?: "ko" }} options
5769
+ * @returns {LocalizedString}
5770
+ */
5771
+ const cli_create_template_too_large = ((inputs, options = {}) => {
5772
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5773
+ return ko_cli_create_template_too_large(inputs);
5774
+ });
5775
+
5776
+ //#endregion
5777
+ //#region src/paraglide/messages/cli_create_template_unreachable.js
5778
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5779
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_UnreachableInputs */
5780
+ const ko_cli_create_template_unreachable = (i) => {
5781
+ return `템플릿을 받지 못했습니다: ${i?.template}. 네트워크 연결을 확인하십시오`;
5782
+ };
5783
+ /**
5784
+
5785
+ * @param {Cli_Create_Template_UnreachableInputs} inputs
5786
+ * @param {{ locale?: "ko" }} options
5787
+ * @returns {LocalizedString}
5788
+ */
5789
+ const cli_create_template_unreachable = ((inputs, options = {}) => {
5790
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5791
+ return ko_cli_create_template_unreachable(inputs);
5792
+ });
5793
+
5794
+ //#endregion
5795
+ //#region src/paraglide/messages/cli_create_template_unreadable.js
5796
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5797
+ /** @typedef {{ template: NonNullable<unknown> }} Cli_Create_Template_UnreadableInputs */
5798
+ const ko_cli_create_template_unreadable = (i) => {
5799
+ return `템플릿 압축 파일을 읽지 못했습니다: ${i?.template}`;
5800
+ };
5801
+ /**
5802
+
5803
+ * @param {Cli_Create_Template_UnreadableInputs} inputs
5804
+ * @param {{ locale?: "ko" }} options
5805
+ * @returns {LocalizedString}
5806
+ */
5807
+ const cli_create_template_unreadable = ((inputs, options = {}) => {
5808
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5809
+ return ko_cli_create_template_unreadable(inputs);
5678
5810
  });
5679
5811
 
5680
5812
  //#endregion
@@ -5682,7 +5814,7 @@ const cli_create_store_code_required = ((inputs = {}, options = {}) => {
5682
5814
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5683
5815
  /** @typedef {{}} Cli_Create_UsageInputs */
5684
5816
  const ko_cli_create_usage = () => {
5685
- return `폴더 이름을 주십시오: npx sayren create <이름> [--store-code <상점 코드>]`;
5817
+ return `폴더 이름을 주십시오: npx sayren create <이름> [--template <템플릿>]`;
5686
5818
  };
5687
5819
  /**
5688
5820
 
@@ -6111,40 +6243,40 @@ const cli_deploy_force_yes_required = ((inputs = {}, options = {}) => {
6111
6243
  });
6112
6244
 
6113
6245
  //#endregion
6114
- //#region src/paraglide/messages/cli_deploy_merged.js
6246
+ //#region src/paraglide/messages/cli_deploy_linking.js
6115
6247
  /** @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`;
6248
+ /** @typedef {{ dir: NonNullable<unknown> }} Cli_Deploy_LinkingInputs */
6249
+ const ko_cli_deploy_linking = (i) => {
6250
+ return `${i?.dir}는 아직 상점에 연결되지 않았습니다 — 먼저 연결합니다(npx sayren link)`;
6120
6251
  };
6121
6252
  /**
6122
6253
 
6123
- * @param {Cli_Deploy_MergedInputs} inputs
6254
+ * @param {Cli_Deploy_LinkingInputs} inputs
6124
6255
  * @param {{ locale?: "ko" }} options
6125
6256
  * @returns {LocalizedString}
6126
6257
  */
6127
- const cli_deploy_merged = ((inputs = {}, options = {}) => {
6258
+ const cli_deploy_linking = ((inputs, options = {}) => {
6128
6259
  experimentalStaticLocale ?? options.locale ?? getLocale();
6129
- return ko_cli_deploy_merged(inputs);
6260
+ return ko_cli_deploy_linking(inputs);
6130
6261
  });
6131
6262
 
6132
6263
  //#endregion
6133
- //#region src/paraglide/messages/cli_deploy_no_state.js
6264
+ //#region src/paraglide/messages/cli_deploy_merged.js
6134
6265
  /** @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로 받으십시오`;
6266
+ /** @typedef {{}} Cli_Deploy_MergedInputs */
6267
+ const ko_cli_deploy_merged = () => {
6268
+ return `pull 이후 원격 작업 트리가 바뀌어(상점 플랫폼·AI 편집) 서버가 병합했습니다. 원격 변경은 그대로 남습니다.
6269
+ · 병합된 트리를 이 폴더에 받으려면: npx sayren pull --force`;
6138
6270
  };
6139
6271
  /**
6140
6272
 
6141
- * @param {Cli_Deploy_No_StateInputs} inputs
6273
+ * @param {Cli_Deploy_MergedInputs} inputs
6142
6274
  * @param {{ locale?: "ko" }} options
6143
6275
  * @returns {LocalizedString}
6144
6276
  */
6145
- const cli_deploy_no_state = ((inputs, options = {}) => {
6277
+ const cli_deploy_merged = ((inputs = {}, options = {}) => {
6146
6278
  experimentalStaticLocale ?? options.locale ?? getLocale();
6147
- return ko_cli_deploy_no_state(inputs);
6279
+ return ko_cli_deploy_merged(inputs);
6148
6280
  });
6149
6281
 
6150
6282
  //#endregion
@@ -6489,6 +6621,78 @@ const cli_edit_session_open = ((inputs = {}, options = {}) => {
6489
6621
  return ko_cli_edit_session_open(inputs);
6490
6622
  });
6491
6623
 
6624
+ //#endregion
6625
+ //#region src/paraglide/messages/cli_env_not_linked.js
6626
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6627
+ /** @typedef {{ file: NonNullable<unknown> }} Cli_Env_Not_LinkedInputs */
6628
+ const ko_cli_env_not_linked = (i) => {
6629
+ return `이 폴더는 상점에 연결되지 않았습니다(${i?.file} 없음). npx sayren link로 먼저 연결하십시오`;
6630
+ };
6631
+ /**
6632
+
6633
+ * @param {Cli_Env_Not_LinkedInputs} inputs
6634
+ * @param {{ locale?: "ko" }} options
6635
+ * @returns {LocalizedString}
6636
+ */
6637
+ const cli_env_not_linked = ((inputs, options = {}) => {
6638
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6639
+ return ko_cli_env_not_linked(inputs);
6640
+ });
6641
+
6642
+ //#endregion
6643
+ //#region src/paraglide/messages/cli_env_pulled.js
6644
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6645
+ /** @typedef {{ file: NonNullable<unknown>, storeCode: NonNullable<unknown> }} Cli_Env_PulledInputs */
6646
+ const ko_cli_env_pulled = (i) => {
6647
+ return `${i?.file}에 상점 ${i?.storeCode} 연결 값(SAYREN_API_URL·SAYREN_STORE_CODE)을 썼습니다`;
6648
+ };
6649
+ /**
6650
+
6651
+ * @param {Cli_Env_PulledInputs} inputs
6652
+ * @param {{ locale?: "ko" }} options
6653
+ * @returns {LocalizedString}
6654
+ */
6655
+ const cli_env_pulled = ((inputs, options = {}) => {
6656
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6657
+ return ko_cli_env_pulled(inputs);
6658
+ });
6659
+
6660
+ //#endregion
6661
+ //#region src/paraglide/messages/cli_env_store_code_unknown.js
6662
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6663
+ /** @typedef {{}} Cli_Env_Store_Code_UnknownInputs */
6664
+ const ko_cli_env_store_code_unknown = () => {
6665
+ return `상점 코드를 읽지 못했습니다. 로그인(npx sayren login)을 다시 하거나 토큰에 stores:read 권한이 있는지 확인하십시오`;
6666
+ };
6667
+ /**
6668
+
6669
+ * @param {Cli_Env_Store_Code_UnknownInputs} inputs
6670
+ * @param {{ locale?: "ko" }} options
6671
+ * @returns {LocalizedString}
6672
+ */
6673
+ const cli_env_store_code_unknown = ((inputs = {}, options = {}) => {
6674
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6675
+ return ko_cli_env_store_code_unknown(inputs);
6676
+ });
6677
+
6678
+ //#endregion
6679
+ //#region src/paraglide/messages/cli_env_usage.js
6680
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6681
+ /** @typedef {{}} Cli_Env_UsageInputs */
6682
+ const ko_cli_env_usage = () => {
6683
+ return `사용법: npx sayren env pull [--dir <폴더>]`;
6684
+ };
6685
+ /**
6686
+
6687
+ * @param {Cli_Env_UsageInputs} inputs
6688
+ * @param {{ locale?: "ko" }} options
6689
+ * @returns {LocalizedString}
6690
+ */
6691
+ const cli_env_usage = ((inputs = {}, options = {}) => {
6692
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6693
+ return ko_cli_env_usage(inputs);
6694
+ });
6695
+
6492
6696
  //#endregion
6493
6697
  //#region src/paraglide/messages/cli_error_api_unreachable.js
6494
6698
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -7030,6 +7234,135 @@ const cli_legacy_rc_tracked = ((inputs, options = {}) => {
7030
7234
  return ko_cli_legacy_rc_tracked(inputs);
7031
7235
  });
7032
7236
 
7237
+ //#endregion
7238
+ //#region src/paraglide/messages/cli_link_dir_missing.js
7239
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7240
+ /** @typedef {{ dir: NonNullable<unknown> }} Cli_Link_Dir_MissingInputs */
7241
+ const ko_cli_link_dir_missing = (i) => {
7242
+ return `폴더가 없습니다: ${i?.dir}`;
7243
+ };
7244
+ /**
7245
+
7246
+ * @param {Cli_Link_Dir_MissingInputs} inputs
7247
+ * @param {{ locale?: "ko" }} options
7248
+ * @returns {LocalizedString}
7249
+ */
7250
+ const cli_link_dir_missing = ((inputs, options = {}) => {
7251
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7252
+ return ko_cli_link_dir_missing(inputs);
7253
+ });
7254
+
7255
+ //#endregion
7256
+ //#region src/paraglide/messages/cli_link_done.js
7257
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7258
+ /** @typedef {{ storeId: NonNullable<unknown>, url: NonNullable<unknown>, file: NonNullable<unknown>, storeCode: NonNullable<unknown>, envFile: NonNullable<unknown> }} Cli_Link_DoneInputs */
7259
+ const ko_cli_link_done = (i) => {
7260
+ return `연결했습니다 — 상점 ${i?.storeId}, 사이트 ${i?.url}. 연결 정보는 ${i?.file}, 상점 ${i?.storeCode} 연결 값은 ${i?.envFile}에 두었습니다`;
7261
+ };
7262
+ /**
7263
+
7264
+ * @param {Cli_Link_DoneInputs} inputs
7265
+ * @param {{ locale?: "ko" }} options
7266
+ * @returns {LocalizedString}
7267
+ */
7268
+ const cli_link_done = ((inputs, options = {}) => {
7269
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7270
+ return ko_cli_link_done(inputs);
7271
+ });
7272
+
7273
+ //#endregion
7274
+ //#region src/paraglide/messages/cli_link_editing_disabled.js
7275
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7276
+ /** @typedef {{}} Cli_Link_Editing_DisabledInputs */
7277
+ const ko_cli_link_editing_disabled = () => {
7278
+ return `지금은 스토어프론트를 연결할 수 없습니다(편집 기능이 꺼져 있습니다)`;
7279
+ };
7280
+ /**
7281
+
7282
+ * @param {Cli_Link_Editing_DisabledInputs} inputs
7283
+ * @param {{ locale?: "ko" }} options
7284
+ * @returns {LocalizedString}
7285
+ */
7286
+ const cli_link_editing_disabled = ((inputs = {}, options = {}) => {
7287
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7288
+ return ko_cli_link_editing_disabled(inputs);
7289
+ });
7290
+
7291
+ //#endregion
7292
+ //#region src/paraglide/messages/cli_link_gitignore.js
7293
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7294
+ /** @typedef {{ lines: NonNullable<unknown> }} Cli_Link_GitignoreInputs */
7295
+ const ko_cli_link_gitignore = (i) => {
7296
+ return `.gitignore에 ${i?.lines}를 더했습니다(배포에 올리지 않습니다)`;
7297
+ };
7298
+ /**
7299
+
7300
+ * @param {Cli_Link_GitignoreInputs} inputs
7301
+ * @param {{ locale?: "ko" }} options
7302
+ * @returns {LocalizedString}
7303
+ */
7304
+ const cli_link_gitignore = ((inputs, options = {}) => {
7305
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7306
+ return ko_cli_link_gitignore(inputs);
7307
+ });
7308
+
7309
+ //#endregion
7310
+ //#region src/paraglide/messages/cli_link_next_steps.js
7311
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7312
+ /** @typedef {{}} Cli_Link_Next_StepsInputs */
7313
+ const ko_cli_link_next_steps = () => {
7314
+ return `다음:
7315
+ npm install
7316
+ npm run dev # http://localhost:4010 — 운영 상점 데이터로 동작합니다
7317
+ npx sayren deploy # 빌드 → 버전 → 발행`;
7318
+ };
7319
+ /**
7320
+
7321
+ * @param {Cli_Link_Next_StepsInputs} inputs
7322
+ * @param {{ locale?: "ko" }} options
7323
+ * @returns {LocalizedString}
7324
+ */
7325
+ const cli_link_next_steps = ((inputs = {}, options = {}) => {
7326
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7327
+ return ko_cli_link_next_steps(inputs);
7328
+ });
7329
+
7330
+ //#endregion
7331
+ //#region src/paraglide/messages/cli_link_site_created.js
7332
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7333
+ /** @typedef {{ url: NonNullable<unknown> }} Cli_Link_Site_CreatedInputs */
7334
+ const ko_cli_link_site_created = (i) => {
7335
+ return `호스팅 사이트를 만들었습니다 — ${i?.url} (오픈 준비 중, 첫 배포가 성공하면 발행합니다)`;
7336
+ };
7337
+ /**
7338
+
7339
+ * @param {Cli_Link_Site_CreatedInputs} inputs
7340
+ * @param {{ locale?: "ko" }} options
7341
+ * @returns {LocalizedString}
7342
+ */
7343
+ const cli_link_site_created = ((inputs, options = {}) => {
7344
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7345
+ return ko_cli_link_site_created(inputs);
7346
+ });
7347
+
7348
+ //#endregion
7349
+ //#region src/paraglide/messages/cli_link_slug_taken.js
7350
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
7351
+ /** @typedef {{}} Cli_Link_Slug_TakenInputs */
7352
+ const ko_cli_link_slug_taken = () => {
7353
+ return `상점 코드와 같은 주소를 다른 사이트가 쓰고 있어 사이트를 만들지 못했습니다. 상점 플랫폼 › 스토어프론트에서 다른 주소로 만든 뒤 npx sayren link로 다시 연결하십시오`;
7354
+ };
7355
+ /**
7356
+
7357
+ * @param {Cli_Link_Slug_TakenInputs} inputs
7358
+ * @param {{ locale?: "ko" }} options
7359
+ * @returns {LocalizedString}
7360
+ */
7361
+ const cli_link_slug_taken = ((inputs = {}, options = {}) => {
7362
+ experimentalStaticLocale ?? options.locale ?? getLocale();
7363
+ return ko_cli_link_slug_taken(inputs);
7364
+ });
7365
+
7033
7366
  //#endregion
7034
7367
  //#region src/paraglide/messages/cli_login_browser_prompt.js
7035
7368
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -8413,7 +8746,7 @@ const cli_state_invalid = ((inputs, options = {}) => {
8413
8746
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
8414
8747
  /** @typedef {{ name: NonNullable<unknown> }} Cli_State_Missing_FieldsInputs */
8415
8748
  const ko_cli_state_missing_fields = (i) => {
8416
- return `${i?.name}에 storeId·siteId·baseRef가 없습니다. npx sayren pull로 다시 받으십시오`;
8749
+ return `${i?.name}에 storeId·siteId·baseRef가 없습니다. npx sayren link로 다시 연결하십시오`;
8417
8750
  };
8418
8751
  /**
8419
8752
 
@@ -8534,6 +8867,24 @@ const cli_status_folder_base = ((inputs, options = {}) => {
8534
8867
  return ko_cli_status_folder_base(inputs);
8535
8868
  });
8536
8869
 
8870
+ //#endregion
8871
+ //#region src/paraglide/messages/cli_status_folder_linked_only.js
8872
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
8873
+ /** @typedef {{}} Cli_Status_Folder_Linked_OnlyInputs */
8874
+ const ko_cli_status_folder_linked_only = () => {
8875
+ return `이 폴더: 연결만 했습니다(아직 올리지 않음 — 다음 deploy가 교체합니다)`;
8876
+ };
8877
+ /**
8878
+
8879
+ * @param {Cli_Status_Folder_Linked_OnlyInputs} inputs
8880
+ * @param {{ locale?: "ko" }} options
8881
+ * @returns {LocalizedString}
8882
+ */
8883
+ const cli_status_folder_linked_only = ((inputs = {}, options = {}) => {
8884
+ experimentalStaticLocale ?? options.locale ?? getLocale();
8885
+ return ko_cli_status_folder_linked_only(inputs);
8886
+ });
8887
+
8537
8888
  //#endregion
8538
8889
  //#region src/paraglide/messages/cli_status_folder_other_site.js
8539
8890
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -9001,7 +9352,9 @@ const ko_cli_usage = (i) => {
9001
9352
  npx sayren login --paste # 액세스 토큰을 입력받아 저장(셸 기록에 남지 않습니다)
9002
9353
  npx sayren logout
9003
9354
  npx sayren whoami
9004
- npx sayren create <이름> [--store-code <상점 코드>] # 스토어프론트 보일러플레이트를 새 폴더에 만듭니다(로그인 없음)
9355
+ npx sayren create <이름> [--template <템플릿>] # 시작 코드를 새 폴더에 만듭니다(파일만, 로그인 없음)
9356
+ npx sayren link [폴더] [--store <storeId>] # 폴더를 상점 사이트에 연결합니다(없으면 사이트를 만듭니다)
9357
+ npx sayren env pull # 연결된 상점 값을 .env에 씁니다
9005
9358
  npx sayren init [폴더] [--template <id>] [--store <storeId>] [--yes]
9006
9359
  npx sayren pull --project <storeId> [폴더] [--force [--yes]]
9007
9360
  npx sayren deploy [--no-publish] [--skip-build] [--message <메모>] [--force [--yes]]
@@ -9014,10 +9367,12 @@ const ko_cli_usage = (i) => {
9014
9367
  login Sayren 계정으로 로그인합니다. 로그인 토큰은 사용자 설정 폴더(~/.config/sayren/credentials.json)에 둡니다
9015
9368
  logout 로그인을 폐기하고 이 컴퓨터의 토큰을 지웁니다
9016
9369
  whoami 쓰는 계정·토큰 종류·상점 범위
9017
- create 스토어프론트 보일러플레이트(상점 이름·상품 목록 홈, SDK·문구·분석·에이전트 안내)를 <이름> 폴더에 만들고 .env에 상점 연결 값을 씁니다. 호스팅 사이트는 만들지 않습니다
9370
+ create 시작 코드(상점 이름·상품 목록 홈, SDK·문구·분석·에이전트 안내)를 <이름> 폴더에 만듭니다. 상점을 묻지 않고 호스팅 사이트도 만들지 않습니다
9371
+ link 상점을 고르고(--store → 토큰 범위 → 선택) 사이트가 없으면 만든 뒤 .sayren/project.json·.env·.mcp.json을 씁니다
9372
+ env env pull: 연결된 상점의 SAYREN_STORE_CODE·SAYREN_API_URL을 .env에 씁니다
9018
9373
  init 템플릿을 골라 호스팅 사이트를 만들고 pull과 같은 폴더(기본: 상점 코드)로 받습니다. 사이트가 이미 있으면 만들지 않습니다
9019
- pull 호스팅 중인 사이트의 작업 트리를 폴더(기본: 상점 코드)로 받고 .mcp.json·.env·.sayren/state.json을 씁니다
9020
- deploy 로컬 검사 → 로컬 빌드 → 작업 트리 올리기 → 빌드 → 발행. 원격이 pull 이후 바뀌었으면 덮어쓰지 않습니다
9374
+ pull 호스팅 중인 사이트의 작업 트리를 폴더(기본: 상점 코드)로 받고 .mcp.json·.env·.sayren/project.json을 씁니다
9375
+ deploy 로컬 검사 → 로컬 빌드 → 작업 트리 올리기 → 빌드 → 발행. 연결이 없으면 link부터 하고 다음 버전으로 교체합니다. 원격이 마지막 받기·올리기 이후 바뀌었으면 덮어쓰지 않습니다
9021
9376
  status 사이트·발행 버전·최근 빌드·편집 환경
9022
9377
  verify 로컬 규칙·경로·크기 검사(서버 빌드와 같은 규칙)
9023
9378
  skill sayren 스킬 목록(list)과 본문(<이름>)을 출력합니다. 스킬은 중앙에서 고치므로 늘 최신입니다
@@ -9030,8 +9385,9 @@ const ko_cli_usage = (i) => {
9030
9385
 
9031
9386
  옵션:
9032
9387
  --project <storeId> pull: 받을 상점(--store와 같습니다)
9033
- --template <id> init: 템플릿 id(비대화형은 필수)
9034
- --store-code <코드> create: 연결할 상점 코드(없으면 묻습니다, 비대화형은 필수)
9388
+ --template <템플릿> create: github:owner/repo[/경로][#ref](공개 GitHub 저장소) 또는 템플릿 id. 없으면 CLI에 든 시작 코드
9389
+ init: 호스팅 템플릿 id(비대화형은 필수)
9390
+ --store-code <코드> create: 이 상점 코드로 .env를 씁니다(없으면 link·env pull이 씁니다)
9035
9391
  --dir <폴더> 작업 폴더(기본: 지금 폴더, pull·init은 상점 코드)
9036
9392
  --force pull: 같은 사이트를 받은 폴더에 원격을 다시 받습니다(지울·덮을 파일을 보여 주고 묻습니다)
9037
9393
  deploy: 원격 변경·열린 편집 환경을 무시하고 덮어씁니다(확인을 묻습니다)
@@ -9234,8 +9590,9 @@ function useProcessLocale(locale) {
9234
9590
  /**
9235
9591
  * 설정 (2026-09-29 사용자 결정: `.sayrenrc`를 없애고 받은 폴더에 원격 MCP `.mcp.json`을 둔다)
9236
9592
  *
9237
- * - `.sayren/state.json`: pull·deploy가 쓰는 비밀 아닌 상태(`storeId`·`siteId`·`baseRef`, 기본이 아닌 api 주소). `.gitignore` 대상이고
9238
- * 올리지 않는다. `baseRef`가 deploy의 `expectedBaseRef`다(원격이 pull 이후 바뀌었으면 `SOURCE_CHANGED`).
9593
+ * - `.sayren/project.json`(#315 C6, 옛 `.sayren/state.json`): link·pull·deploy가 쓰는 연결 정보(`storeId`·`siteId`·`baseRef`, 기본이 아닌 api 주소).
9594
+ * `.gitignore` 대상이고 올리지 않는다. `baseRef`가 deploy의 `expectedBaseRef`다(원격이 pull 이후 바뀌었으면 `SOURCE_CHANGED`). `link`가 막
9595
+ * 연결한 폴더는 `baseRef`가 null이다 — 첫 deploy가 조건 없이 다음 버전으로 교체하고(머리 없는 사이트는 첫 커밋), 그 뒤부터 올린 트리가 기준이다.
9239
9596
  * - `.mcp.json`: Claude Code 형식의 `sayren` 원격 MCP 항목. 주소와 `X-Store-Id`만 있고 토큰은 없다(#71 — MCP 클라이언트가
9240
9597
  * 연결할 때 스스로 로그인한다). 옛 폴더의 폐기된 토큰(`sy_pat_…`) 헤더는 쓸 때 지운다. `.gitignore` 대상이고 올리지 않는다.
9241
9598
  * - `.env`: 스토어프론트 템플릿 개발 서버가 읽는 값(`SAYREN_API_URL`·`SAYREN_STORE_CODE`)만 둔다. 토큰을 두지 않는다.
@@ -9245,10 +9602,12 @@ function useProcessLocale(locale) {
9245
9602
  * 옛 API 토큰(`sy_pat_…`)은 R1에서 폐기되어 api에 보내지 않고 거절한다(`cli_retired_token`).
9246
9603
  * api 주소는 `--api-origin` → `SAYREN_API_ORIGIN` → `.env`의
9247
9604
  * `SAYREN_API_URL` → 상태 파일 → 기본값 순서이고, 파일에서 온 기본이 아닌 주소는 확인한다(`checkAuthSources`).
9248
- * 옛 폴더(`.sayrenrc`, `.env`의 `SAYREN_TOKEN`)는 한 번 정리한다(`migrateLegacy`).
9605
+ * 옛 폴더(`.sayrenrc`, `.sayren/state.json`, `.env`의 `SAYREN_TOKEN`)는 한 번 정리한다(`migrateLegacy`).
9249
9606
  */
9250
9607
  const STATE_DIR = ".sayren";
9251
- const STATE_FILE = `${STATE_DIR}/state.json`;
9608
+ const STATE_FILE = `${STATE_DIR}/project.json`;
9609
+ /** 옛 연결 파일(0.2.0 ~ 0.14.x) — 읽고, 옮긴 뒤 지운다 */
9610
+ const LEGACY_STATE_FILE = `${STATE_DIR}/state.json`;
9252
9611
  /** 옛 상태 파일(0.1.x) — 읽어서 옮기고 지운다 */
9253
9612
  const LEGACY_RC_FILE = ".sayrenrc";
9254
9613
  const ENV_FILE = ".env";
@@ -9263,7 +9622,7 @@ function parseState(text, name) {
9263
9622
  }
9264
9623
  if (!parsed || typeof parsed !== "object") throw new CliError(cli_state_invalid({ name }));
9265
9624
  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 }));
9625
+ 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
9626
  return {
9268
9627
  storeId: raw.storeId,
9269
9628
  siteId: raw.siteId,
@@ -9271,10 +9630,13 @@ function parseState(text, name) {
9271
9630
  ...typeof raw.apiOrigin === "string" ? { apiOrigin: raw.apiOrigin } : {}
9272
9631
  };
9273
9632
  }
9633
+ /** 연결 정보 — `project.json`, 없으면 옛 `state.json`을 읽는다(옮기기는 `migrateLegacy`) */
9274
9634
  function readState(dir) {
9275
- const path = join(dir, STATE_FILE);
9276
- if (!existsSync(path)) return null;
9277
- return parseState(readFileSync(path, "utf8"), STATE_FILE);
9635
+ for (const name of [STATE_FILE, LEGACY_STATE_FILE]) {
9636
+ const path = join(dir, name);
9637
+ if (existsSync(path)) return parseState(readFileSync(path, "utf8"), name);
9638
+ }
9639
+ return null;
9278
9640
  }
9279
9641
  function writeState(dir, state) {
9280
9642
  const out = {
@@ -9285,10 +9647,15 @@ function writeState(dir, state) {
9285
9647
  if (state.apiOrigin && state.apiOrigin !== DEFAULT_API_ORIGIN) out.apiOrigin = state.apiOrigin;
9286
9648
  mkdirSync(join(dir, STATE_DIR), { recursive: true });
9287
9649
  writeFileSync(join(dir, STATE_FILE), `${JSON.stringify(out, null, 2)}\n`);
9650
+ rmSync(join(dir, LEGACY_STATE_FILE), { force: true });
9288
9651
  }
9289
- /** 받은 폴더인가 — 상태 파일 또는 옛 `.sayrenrc`가 있다 */
9652
+ /** 연결된(받은) 폴더인가 — 연결 파일 또는 옛 `state.json`·`.sayrenrc`가 있다 */
9290
9653
  function isPulledDir(dir) {
9291
- return existsSync(join(dir, STATE_FILE)) || existsSync(join(dir, LEGACY_RC_FILE));
9654
+ return [
9655
+ STATE_FILE,
9656
+ LEGACY_STATE_FILE,
9657
+ LEGACY_RC_FILE
9658
+ ].some((name) => existsSync(join(dir, name)));
9292
9659
  }
9293
9660
  function readMcpText(dir) {
9294
9661
  const path = join(dir, MCP_FILE);
@@ -9327,12 +9694,18 @@ function writePrivate(path, text) {
9327
9694
  }
9328
9695
  }
9329
9696
  /**
9330
- * 0.1.x가 받은 폴더를 한 번 정리한다. `.sayrenrc` → `.sayren/state.json`(옛 파일은 지운다), `.env`의 폐기된 `SAYREN_TOKEN`(`sy_pat_…`)은
9697
+ * 옛 CLI가 받은 폴더를 한 번 정리한다. `.sayren/state.json`(0.2.0 ~ 0.14.x)·`.sayrenrc`(0.1.x) → `.sayren/project.json`(옛 파일은 지운다), `.env`의 폐기된 `SAYREN_TOKEN`(`sy_pat_…`)은
9331
9698
  * 지운다(R1 뒤 쓸 수 없다). 다른 값이 든 `SAYREN_TOKEN` 줄은 두고(CLI는 읽지 않는다) 알린다. 정리했으면 한 줄로 알린다.
9332
9699
  */
9333
9700
  function migrateLegacy(dir, notify) {
9334
9701
  if (!isPulledDir(dir)) return;
9335
9702
  const done = [];
9703
+ const legacyState = join(dir, LEGACY_STATE_FILE);
9704
+ if (existsSync(legacyState)) {
9705
+ if (existsSync(join(dir, STATE_FILE))) rmSync(legacyState, { force: true });
9706
+ else writeState(dir, parseState(readFileSync(legacyState, "utf8"), LEGACY_STATE_FILE));
9707
+ done.push(`${LEGACY_STATE_FILE} → ${STATE_FILE}`);
9708
+ }
9336
9709
  const rcPath = join(dir, LEGACY_RC_FILE);
9337
9710
  if (existsSync(rcPath)) {
9338
9711
  const tracked = isGitTracked(dir, LEGACY_RC_FILE);
@@ -9953,7 +10326,7 @@ async function selectStore(auth, client, io, candidates, folderStore = null) {
9953
10326
 
9954
10327
  //#endregion
9955
10328
  //#region package.json
9956
- var version = "0.14.0";
10329
+ var version = "0.15.1";
9957
10330
 
9958
10331
  //#endregion
9959
10332
  //#region src/client.ts
@@ -11626,42 +11999,49 @@ const FORBIDDEN_SEGMENTS = new Set([
11626
11999
  ".nitro",
11627
12000
  ".vinxi"
11628
12001
  ]);
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"
12002
+ /**
12003
+ * 작업 트리에 두지 않는 이름(어디에 있든) — 형식은 맞지만 추적하지 않는다(목록·스냅샷·업로드·쓰기에서 빠진다, #298 P0 거절 목록).
12004
+ * - `.dev.vars`·`.dev.vars.*`(이름 규칙은 `isDevVars`): wrangler 로컬 개발 비밀 값 파일. `.env*`와 같이 업로드·스냅샷·Git push에 들어가지 않게
12005
+ * - `.sayren`·`.sayrenrc`: CLI 연결·상태 파일(`.sayren/project.json`, 옛 `.sayrenrc`는 토큰이 든다). 받기(pull)가 로컬 연결을 덮지 않게
12006
+ * - `.npmrc`·`.yarnrc`·`.yarnrc.yml`·`bunfig.toml`: 레지스트리 인증 토큰이 들어가는 설치 설정. 레지스트리·스크립트 설정이 의존성 판정을 비켜 가지 않게
12007
+ * - `.pnpmfile.cjs`·`.pnpmfile.mjs`: 설치 중 실행되는 훅(`--ignore-scripts`와 무관하게 돈다). 판정한 설치 트리를 바꾸지 않게
12008
+ * - `coverage`·`.turbo`·`.cache`: 테스트·도구 산출물. `.DS_Store`·`Thumbs.db`: OS 부산물
12009
+ */
12010
+ const UNTRACKED_SEGMENTS = new Set([
12011
+ ".sayren",
12012
+ ".sayrenrc",
12013
+ ".npmrc",
12014
+ ".yarnrc",
12015
+ ".yarnrc.yml",
12016
+ "bunfig.toml",
12017
+ ".pnpmfile.cjs",
12018
+ ".pnpmfile.mjs",
12019
+ "coverage",
12020
+ ".turbo",
12021
+ ".cache",
12022
+ ".DS_Store",
12023
+ "Thumbs.db"
11637
12024
  ]);
12025
+ /** wrangler 로컬 비밀 값 파일(`.dev.vars`, 환경별 `.dev.vars.<env>`) — 어디에 있든 추적하지 않는다 */
12026
+ const isDevVars = (segment) => segment === ".dev.vars" || segment.startsWith(".dev.vars.");
12027
+ /** 루트에 두지 않는 이름 — `pnpm-workspace.yaml`은 사이트를 워크스페이스로 바꾸고 설치 설정(overrides 등)을 싣는다 */
12028
+ const UNTRACKED_ROOT_FILES = new Set(["pnpm-workspace.yaml"]);
11638
12029
  /**
11639
- * 읽기만 되는 루트 파일 — 의존성은 전용 경로로만 바꾼다(레지스트리 프록시와 잠금 파일을 함께 맞춘다).
11640
- * `CLAUDE.md`·`.mcp.json`은 템플릿이 싣는 에이전트 안내·MCP 연결 설정이다(2026-10-14 blank, #271부터 보일러플레이트 `boilerplate`) — 편집 도구는 바꾸지 않는다
12030
+ * 읽기만 되는 루트 파일 — `CLAUDE.md`·`.mcp.json`은 시작 코드가 싣는 에이전트 안내·MCP 연결 설정이다. 편집 도구는 바꾸지 않는다.
12031
+ * `package.json`·잠금 파일은 쓸 수 있다(#298 P0) — 바뀌면 api가 의존성을 다시 검사한다
11641
12032
  */
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 상점 도우미가 같은 기준을 읽는다 */
12033
+ const READONLY_ROOT_FILES = new Set(["CLAUDE.md", ".mcp.json"]);
12034
+ /** 읽기만 되는 폴더 — 시작 코드가 싣는 Claude 스킬(`.claude/skills/…`, 2026-10-14). 셀러 로컬 Claude Code와 AI가 같은 기준을 읽는다 */
11649
12035
  const READONLY_DIRS = new Set([".claude"]);
11650
12036
  /**
11651
- * 쓸 수 있는 폴더 — 코드(`src`), 정적 파일(`public`), 화면 문구 카탈로그(`messages`, 템플릿 `src/i18n`이 `messages/ko.json`을 import한다, #73).
11652
- * 템플릿이 import하는 파일이 이 밖에 있으면 첫 작업 트리(source.tgz)를 풀 때 빠져 빌드·검사가 깨진다(#78)
12037
+ * 스냅샷 묶기에서 빼는 이름(`container-tree.ts`의 `--exclude`) — 금지 조각과 추적하지 않는 이름. 최종 거름은 `isTracked`다
12038
+ * (여기는 큰 설치물·산출물을 tar에 넣지 않으려는 것이다)
11653
12039
  */
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
12040
+ const SNAPSHOT_EXCLUDED_NAMES = [
12041
+ ...FORBIDDEN_SEGMENTS,
12042
+ ...UNTRACKED_SEGMENTS,
12043
+ ".dev.vars",
12044
+ ".dev.vars.*"
11665
12045
  ];
11666
12046
  /** 형식·금지 세그먼트만 본다(위치 규칙은 보지 않는다). 통과하면 null, 아니면 이유 */
11667
12047
  function formatProblem(path) {
@@ -11678,21 +12058,24 @@ function formatProblem(path) {
11678
12058
  }
11679
12059
  return null;
11680
12060
  }
11681
- /** 위치 규칙: 어떤 모드로 이 경로에 닿을 수 있는가 */
12061
+ /** 추적하지 않는 이름이면 이유, 아니면 null(형식은 이미 통과한 경로) */
12062
+ function untrackedReason(segments) {
12063
+ if (segments.some((s) => UNTRACKED_SEGMENTS.has(s) || isDevVars(s))) return "untracked";
12064
+ if (segments.length === 1 && UNTRACKED_ROOT_FILES.has(segments[0])) return "untracked";
12065
+ return null;
12066
+ }
12067
+ /**
12068
+ * 위치 규칙: 어떤 모드로 이 경로에 닿을 수 있는가. 거절 목록 방식이다(#298 P0) — 형식(금지 조각·`.env*`·문자·길이)과 추적하지 않는 이름만
12069
+ * 막고 나머지는 어디든 쓸 수 있다. 예외는 읽기만 되는 에이전트 안내(`CLAUDE.md`·`.mcp.json`·`.claude/**`)다
12070
+ */
11682
12071
  function accessOf(path) {
11683
12072
  if (formatProblem(path) !== null) return null;
11684
12073
  const segments = path.split("/");
12074
+ if (untrackedReason(segments) !== null) return null;
11685
12075
  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";
12076
+ if (segments.length === 1 && READONLY_ROOT_FILES.has(first)) return "read";
12077
+ if (segments.length > 1 && READONLY_DIRS.has(first)) return "read";
12078
+ return "write";
11696
12079
  }
11697
12080
 
11698
12081
  //#endregion
@@ -11806,18 +12189,19 @@ async function gunzip(bytes, maxBytes) {
11806
12189
  //#endregion
11807
12190
  //#region src/tree.ts
11808
12191
  /**
11809
- * 로컬 작업 트리 — 서버 작업 트리와 **같은 경로 규칙**(편집 Sandbox `paths.ts`의 `accessOf`)으로 고른다. `src/**`·`public/**`·`messages/**`·루트 설정
11810
- * 파일·`package.json`·`pnpm-lock.yaml`만 올리고, `.env*`·`.mcp.json`·`.sayren/`·`node_modules`·`dist`·`package-lock.json` 등은 올리지 않는다.
12192
+ * 로컬 작업 트리 — 서버 작업 트리와 **같은 경로 규칙**(편집 Sandbox `paths.ts`의 `accessOf`)으로 고른다. 거절 목록 방식이다(#298 P0): 폴더 구조와
12193
+ * 무관하게 올리고, `.env*`·`.mcp.json`·`.sayren/`·`.npmrc`·`node_modules`·`dist` 등 설치물·산출물·비밀·로컬 상태만 올리지 않는다.
11811
12194
  * 비밀·로컬 상태 파일(`isNeverUpload`)은 서버 경로 규칙과 별개로 CLI가 먼저 막는다 — 서버 규칙이 넓어져도 토큰을 올리지 않게.
11812
12195
  */
11813
12196
  /**
11814
- * 어떤 경우에도 올리지 않는 파일 — `.env*`(개발 서버 값), `.mcp.json`(토큰), `.sayren/`(CLI 상태), 옛 `.sayrenrc`.
12197
+ * 어떤 경우에도 올리지 않는 파일 — `.env*`(개발 서버 값), `.dev.vars*`(wrangler 로컬 비밀 값), `.mcp.json`(토큰), `.sayren/`(CLI 상태),
12198
+ * 옛 `.sayrenrc`, `.npmrc`(레지스트리 토큰).
11815
12199
  * 폴더 깊이와 무관하게 이름으로 본다.
11816
12200
  */
11817
12201
  function isNeverUpload(path) {
11818
12202
  const segments = path.split("/");
11819
12203
  const name = segments[segments.length - 1] ?? "";
11820
- return segments.some((segment) => segment.startsWith(".env") || segment === ".sayren") || name === ".mcp.json" || name === ".sayrenrc";
12204
+ return segments.some((segment) => segment.startsWith(".env") || segment === ".sayren" || segment === ".dev.vars" || segment.startsWith(".dev.vars.")) || name === ".mcp.json" || name === ".sayrenrc" || name === ".npmrc";
11821
12205
  }
11822
12206
  /** 걸어 들어가지 않는 폴더 — 설치물·산출물·캐시·VCS(경로 규칙이 어차피 막는다. 크기 때문에 읽지도 않는다) */
11823
12207
  const SKIP_DIRS$1 = new Set([
@@ -11977,7 +12361,7 @@ function runLocalChecks(tree) {
11977
12361
  if (packageJson && lock) {
11978
12362
  const drift = lockDrift(packageJson, lock);
11979
12363
  if (drift.length) warnings.push(cli_checks_lock_drift({ names: drift.slice(0, 5).join(", ") }));
11980
- } else if (packageJson && !lock) warnings.push(cli_checks_lock_missing());
12364
+ }
11981
12365
  return {
11982
12366
  report: report$1,
11983
12367
  blockingRules,
@@ -12380,6 +12764,107 @@ async function pickTemplate(templates, wanted, io) {
12380
12764
  if (index < 0) throw new CliError(cli_init_template_required({ list: labels.map((label) => ` ${label}`).join("\n") }));
12381
12765
  return templates[index];
12382
12766
  }
12767
+ /**
12768
+ * `sayren link [폴더]`(#315 C6) — 폴더를 상점의 호스팅 사이트에 연결한다. 상점은 `--store` → 토큰 범위 → 대화형 선택이고, 사이트가 없으면
12769
+ * 시작 코드 없이 만든다(`initialSource: "NONE"` — 오픈 준비 중·버전 없음, 첫 deploy가 첫 커밋). 연결 정보는 `.sayren/project.json`, 상점 값은
12770
+ * `env pull`과 같이 `.env`에 쓴다. 토큰은 어느 파일에도 쓰지 않는다.
12771
+ */
12772
+ async function link(flags, io) {
12773
+ const dir = resolve(io.cwd, flags.dir ?? ".");
12774
+ if (!existsSync(dir)) throw new CliError(cli_link_dir_missing({ dir: relativeOf(io, dir) }));
12775
+ migrateLegacy(dir, io.out);
12776
+ await linkDir(flags, dir, io);
12777
+ io.out(cli_link_next_steps());
12778
+ return 0;
12779
+ }
12780
+ /** link 본체 — `deploy`도 연결이 없으면 이것부터 부른다 */
12781
+ async function linkDir(flags, dir, io) {
12782
+ let previous = null;
12783
+ try {
12784
+ previous = readState(dir);
12785
+ } catch {
12786
+ previous = null;
12787
+ }
12788
+ const { auth, client } = await apiFor(flags, dir, previous, io);
12789
+ const storeId = auth.storeId;
12790
+ const hosting = await client.storefront.get();
12791
+ if (!hosting.editingEnabled) throw new CliError(cli_link_editing_disabled());
12792
+ let site = hosting.site;
12793
+ if (site) {
12794
+ const git$1 = await client.storefront.getGit().catch(() => null);
12795
+ if (git$1?.owner === "SELLER" && git$1.repository) throw new CliError(cli_deploy_seller_git({
12796
+ repository: git$1.repository,
12797
+ branch: git$1.productionBranch ?? "main"
12798
+ }));
12799
+ } else try {
12800
+ site = await client.storefront.create({ initialSource: "NONE" });
12801
+ io.out(cli_link_site_created({ url: site.url }));
12802
+ } catch (error) {
12803
+ const code = codeOf(error);
12804
+ if (code === "STOREFRONT_SLUG_TAKEN") throw new CliError(cli_link_slug_taken());
12805
+ if (code !== "STOREFRONT_SITE_EXISTS") throw error;
12806
+ site = (await client.storefront.get()).site;
12807
+ if (!site) throw error;
12808
+ }
12809
+ const same = previous?.storeId === storeId && previous.siteId === site.siteId;
12810
+ const state = {
12811
+ storeId,
12812
+ siteId: site.siteId,
12813
+ baseRef: same ? previous?.baseRef ?? null : null,
12814
+ ...auth.apiOrigin !== DEFAULT_API_ORIGIN ? { apiOrigin: auth.apiOrigin } : {}
12815
+ };
12816
+ writeState(dir, state);
12817
+ const storeCode = await storeCodeOf(client, storeId);
12818
+ writeStoreEnv(dir, auth.apiOrigin, storeCode);
12819
+ const addedIgnore = ensureGitignore(dir);
12820
+ if (writeMcpJson(dir, { storeId }) === "retired-token-removed") io.out(cli_notice({ message: retiredMcpTokenNotice() }));
12821
+ io.out(cli_link_done({
12822
+ storeId,
12823
+ url: site.url,
12824
+ file: STATE_FILE,
12825
+ envFile: ENV_FILE,
12826
+ storeCode
12827
+ }));
12828
+ if (addedIgnore.length) io.out(cli_link_gitignore({ lines: addedIgnore.join(" ") }));
12829
+ return {
12830
+ client,
12831
+ state
12832
+ };
12833
+ }
12834
+ /** 상점 코드 — 계정의 상점 목록(`stores:read`), 안 되면 작업 트리 조회(`storefront:r`) */
12835
+ async function storeCodeOf(client, storeId) {
12836
+ const listed = await client.auth.listMyStores().then((stores) => stores.find((store) => store.storeId === storeId)?.storeCode).catch(() => void 0);
12837
+ if (listed) return listed;
12838
+ const fromSource = await client.storefront.getSource().then((source) => source.storeCode).catch(() => void 0);
12839
+ if (fromSource) return fromSource;
12840
+ throw new CliError(cli_env_store_code_unknown());
12841
+ }
12842
+ /** `.env`의 상점 연결 값 — 다른 줄은 둔다. 토큰 줄(`SAYREN_TOKEN`)은 지운다 */
12843
+ function writeStoreEnv(dir, apiOrigin, storeCode) {
12844
+ updateEnvFile(dir, {
12845
+ SAYREN_API_URL: `${apiOrigin}/storefront/v1`,
12846
+ SAYREN_STORE_CODE: storeCode
12847
+ }, ["SAYREN_TOKEN"]);
12848
+ }
12849
+ /** `sayren env pull` — 연결된 상점의 `SAYREN_STORE_CODE`·`SAYREN_API_URL`을 `.env`에 쓴다 */
12850
+ async function envPull(flags, io) {
12851
+ const dir = resolve(io.cwd, flags.dir ?? ".");
12852
+ migrateLegacy(dir, io.out);
12853
+ const state = readState(dir);
12854
+ if (!state) throw new CliError(cli_env_not_linked({ file: STATE_FILE }));
12855
+ const { auth, client } = await apiFor({
12856
+ ...flags,
12857
+ store: state.storeId
12858
+ }, dir, state, io);
12859
+ const storeCode = await storeCodeOf(client, state.storeId);
12860
+ writeStoreEnv(dir, auth.apiOrigin, storeCode);
12861
+ ensureGitignore(dir);
12862
+ io.out(cli_env_pulled({
12863
+ file: ENV_FILE,
12864
+ storeCode
12865
+ }));
12866
+ return 0;
12867
+ }
12383
12868
  async function verify(flags, io) {
12384
12869
  const dir = resolve(io.cwd, flags.dir ?? ".");
12385
12870
  const tree = collectTree(dir);
@@ -12414,38 +12899,30 @@ function printChecks(tree, checks, io) {
12414
12899
  }) : cli_skipped_outside({ paths })}`);
12415
12900
  }
12416
12901
  }
12902
+ /**
12903
+ * `deploy` — 연결 정보(`.sayren/project.json`)가 없으면 `link`부터 한다(#315 C6, Vercel 방식). 막 연결한 폴더는 기준(`baseRef`)이 없어
12904
+ * 확인 없이 다음 버전으로 교체한다(사이트에 소스가 없으면 첫 커밋). 그 뒤 deploy는 올린 트리를 기준으로 조건부 교체한다.
12905
+ */
12417
12906
  async function deploy(flags, io) {
12418
12907
  const dir = resolve(io.cwd, flags.dir ?? ".");
12419
12908
  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);
12909
+ const prior = readState(dir);
12910
+ const linked = prior ? await apiFor(flags, dir, prior, io) : null;
12911
+ let tree = collectTree(dir);
12445
12912
  const checks = runLocalChecks(tree);
12446
12913
  printChecks(tree, checks, io);
12447
12914
  if (checks.blockingRules.length > 0 || checks.problems.length > 0) throw new CliError(cli_deploy_checks_failed());
12448
12915
  await localBuild(dir, flags.skipBuild === true, io);
12916
+ let client;
12917
+ let rc;
12918
+ if (linked && prior) {
12919
+ client = linked.client;
12920
+ rc = prior;
12921
+ } else {
12922
+ io.out(cli_deploy_linking({ dir: relativeOf(io, dir) }));
12923
+ ({client, state: rc} = await linkDir(flags, dir, io));
12924
+ tree = collectTree(dir);
12925
+ }
12449
12926
  const site = (await client.storefront.get()).site;
12450
12927
  if (!site || site.siteId !== rc.siteId) throw new CliError(cli_deploy_site_mismatch({ siteId: rc.siteId }));
12451
12928
  if (flags.force && !flags.yes) {
@@ -12499,8 +12976,21 @@ async function deploy(flags, io) {
12499
12976
  io.out(cli_deploy_not_published({ version: created.version.number }));
12500
12977
  return 0;
12501
12978
  }
12979
+ let current = site.publishedVersion;
12980
+ if (!current) {
12981
+ const after = (await client.storefront.get()).site;
12982
+ if (after?.publishedVersion?.versionId === created.version.versionId) {
12983
+ io.out(cli_deploy_published({
12984
+ version: created.version.number,
12985
+ url: after.url
12986
+ }));
12987
+ io.out(cli_deploy_publish_delay());
12988
+ return 0;
12989
+ }
12990
+ current = after?.publishedVersion ?? null;
12991
+ }
12502
12992
  try {
12503
- const published = await client.storefront.publish(created.version.versionId, { expectedPublishedVersionId: site.publishedVersion?.versionId ?? null });
12993
+ const published = await client.storefront.publish(created.version.versionId, { expectedPublishedVersionId: current?.versionId ?? null });
12504
12994
  io.out(cli_deploy_published({
12505
12995
  version: created.version.number,
12506
12996
  url: published.url
@@ -12567,7 +13057,7 @@ async function replaceSource(client, tree, rc, force, io) {
12567
13057
  const packageJson = textOf("package.json");
12568
13058
  const lockfile = textOf("pnpm-lock.yaml");
12569
13059
  const body = {
12570
- ...force ? { endEditSession: true } : { expectedBaseRef: rc.baseRef },
13060
+ ...force ? { endEditSession: true } : rc.baseRef ? { expectedBaseRef: rc.baseRef } : {},
12571
13061
  files,
12572
13062
  ...packageJson === void 0 ? {} : { packageJson },
12573
13063
  ...lockfile === void 0 ? {} : { lockfile }
@@ -12739,6 +13229,7 @@ async function status(flags, io) {
12739
13229
  io.out(` ${editing.session ? cli_status_editing_open({ status: editing.session.status }) : cli_status_editing_closed()}`);
12740
13230
  }
12741
13231
  if (rc) if (rc.siteId !== site.siteId) io.out(` ${cli_status_folder_other_site({ siteId: rc.siteId })}`);
13232
+ else if (!rc.baseRef) io.out(` ${cli_status_folder_linked_only()}`);
12742
13233
  else io.out(` ${cli_status_folder_base({ ref: rc.baseRef.slice(rc.baseRef.lastIndexOf("/") + 1, -4).slice(0, 12) })}`);
12743
13234
  return 0;
12744
13235
  }
@@ -12871,6 +13362,57 @@ function relativeOf(io, path) {
12871
13362
  return relative(io.cwd, path) || ".";
12872
13363
  }
12873
13364
 
13365
+ //#endregion
13366
+ //#region ../mcp/src/starter-transform.ts
13367
+ var StarterTransformError = class extends Error {
13368
+ code;
13369
+ constructor(code, message) {
13370
+ super(message);
13371
+ this.name = "StarterTransformError";
13372
+ this.code = code;
13373
+ }
13374
+ };
13375
+ /** 새 프로젝트로 옮기지 않는 루트 경로인가 — 로컬 env·CLI 연결 정보 */
13376
+ function isStarterLocalOnly(path) {
13377
+ if (path === ".sayren" || path.startsWith(".sayren/")) return true;
13378
+ if (path.includes("/")) return false;
13379
+ return (path === ".env" || path.startsWith(".env.")) && path !== ".env.example";
13380
+ }
13381
+ function transformStarterFiles(files, options) {
13382
+ const out = /* @__PURE__ */ new Map();
13383
+ for (const [path, bytes] of files) {
13384
+ if (isStarterLocalOnly(path)) continue;
13385
+ const slash = path.lastIndexOf("/");
13386
+ const base = path.slice(slash + 1);
13387
+ out.set(base === "_gitignore" ? `${path.slice(0, slash + 1)}.gitignore` : path, bytes);
13388
+ }
13389
+ const raw = out.get("package.json");
13390
+ if (!raw) throw new StarterTransformError("STARTER_PACKAGE_JSON_MISSING", "package.json이 없습니다");
13391
+ const next = withPackageName(raw, options.name, { private: true });
13392
+ if (!next) throw new StarterTransformError("STARTER_PACKAGE_JSON_INVALID", "package.json을 JSON으로 읽을 수 없습니다");
13393
+ out.set("package.json", next);
13394
+ return out;
13395
+ }
13396
+ /**
13397
+ * `package.json`의 `name`을 바꾼다(`fields`가 있으면 그 필드도). 나머지 필드·순서는 그대로다. JSON 객체가 아니면 null이다.
13398
+ * 편집 Sandbox의 시작 코드 사이트(이미 정규화된 R2 원본)는 이것만 부른다 — 서버가 바꾸는 것은 `name` 하나다.
13399
+ */
13400
+ function withPackageName(bytes, name, fields = {}) {
13401
+ let pkg;
13402
+ try {
13403
+ pkg = JSON.parse(new TextDecoder().decode(bytes));
13404
+ } catch {
13405
+ return null;
13406
+ }
13407
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
13408
+ const next = {
13409
+ ...pkg,
13410
+ name,
13411
+ ...fields
13412
+ };
13413
+ return new TextEncoder().encode(`${JSON.stringify(next, null, 2)}\n`);
13414
+ }
13415
+
12874
13416
  //#endregion
12875
13417
  //#region ../mcp/src/template-package.ts
12876
13418
  /** package.json의 기준 템플릿 표식 필드 — 원천은 이 함수다(api `template-marker.ts`가 같은 모양을 읽는다) */
@@ -12951,11 +13493,13 @@ function readWorkspaceVersions(packagesDir) {
12951
13493
  //#endregion
12952
13494
  //#region src/create-command.ts
12953
13495
  /**
12954
- * `npx sayren create <이름>`(#271) — 스토어프론트 보일러플레이트를 새 폴더에 쓴다. 원천은 `packages/cli/boilerplate` 하나이고
12955
- * 호스팅 「처음부터」 템플릿(카탈로그 `boilerplate`)도 같은 폴더에서 만든다. 로그인·사이트 만들기는 하지 않는다(사이트는 `init`).
12956
- * 상점 연결 값은 `--store-code` → 묻기(대화형) 순서이고 `.env`에 `SAYREN_API_URL`·`SAYREN_STORE_CODE`로 쓴다.
13496
+ * `npx sayren create <이름> [--template <템플릿>]`(#271, #315 C7) — 시작 코드를 새 폴더에 쓴다. 파일만 만들고 상점을 묻지 않는다.
13497
+ * 상점 연결은 `sayren link`(사이트·`.sayren/project.json`)와 `sayren env pull`(`.env`)이 한다. `--store-code`를 주면 그 값으로 `.env`만 쓴다.
13498
+ *
13499
+ * 템플릿: 없으면 CLI에 묶은 시작 코드(네트워크 없음), `github:owner/repo[/경로][#ref]`는 codeload tarball, 그 밖의 `<id>`는 sayren 레지스트리
13500
+ * 자리다(마켓 전까지 늘 「템플릿을 찾을 수 없습니다」). 받은 소스는 변환 함수 하나(`starter-transform.ts`, 서버와 공유)를 지난다.
12957
13501
  */
12958
- /** 보일러플레이트 위치 — 게시본은 빌드가 복사한 `dist/boilerplate`, 저장소에서 바로 돌리면 `packages/cli/boilerplate` */
13502
+ /** 묶은 시작 코드 위치 — 게시본은 빌드가 묶은 `dist/boilerplate`, 저장소에서 바로 돌리면 원천 `packages/cli/boilerplate` */
12959
13503
  function boilerplateRoot() {
12960
13504
  const here = dirname(fileURLToPath(import.meta.url));
12961
13505
  const candidates = [join(here, "boilerplate"), join(here, "..", "boilerplate")];
@@ -12963,18 +13507,7 @@ function boilerplateRoot() {
12963
13507
  if (!found) throw new Error(`boilerplate not found: ${candidates.join(", ")}`);
12964
13508
  return found;
12965
13509
  }
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`만 */
13510
+ /** 싣지 않는 것 — 설치물·산출물. 점 파일은 에이전트 안내(`.claude/`·`.mcp.json`)와 `.gitignore`만(scripts/starter-bundle.mjs와 같다) */
12978
13511
  const SKIP_DIRS = new Set([
12979
13512
  "node_modules",
12980
13513
  "dist",
@@ -12988,8 +13521,11 @@ const DOT_KEPT = new Set([
12988
13521
  ".mcp.json",
12989
13522
  ".gitignore"
12990
13523
  ]);
12991
- /** 보일러플레이트 파일 — 상대 경로 → 내용. 게시본의 `_gitignore`는 `.gitignore`로 되돌린다(npm이 `.gitignore`를 싣지 않는다) */
12992
- function boilerplateFiles(root, name) {
13524
+ /**
13525
+ * 묶은 시작 코드 — 상대 경로 → 내용. 게시본은 빌드가 이미 npm 범위로 풀었다. 저장소 원천(워크스페이스 구성원)에서 바로 돌리면
13526
+ * `catalog:`·`workspace:`를 여기서 푼다(게시본과 같은 결과).
13527
+ */
13528
+ function bundledStarterFiles(root) {
12993
13529
  const files = /* @__PURE__ */ new Map();
12994
13530
  const walk = (dir) => {
12995
13531
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -12997,16 +13533,86 @@ function boilerplateFiles(root, name) {
12997
13533
  if (entry.name.startsWith(".") && !DOT_KEPT.has(entry.name)) continue;
12998
13534
  const full = join(dir, entry.name);
12999
13535
  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
- }
13536
+ else if (entry.isFile()) files.set(relative(root, full).split(sep).join("/"), readFileSync(full));
13004
13537
  }
13005
13538
  };
13006
13539
  walk(root);
13540
+ const repo = join(root, "..", "..", "..");
13007
13541
  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 })));
13542
+ if (pkg && existsSync(join(repo, "pnpm-workspace.yaml"))) {
13543
+ const meta = {
13544
+ catalog: parseCatalog(readFileSync(join(repo, "pnpm-workspace.yaml"), "utf8")),
13545
+ versions: readWorkspaceVersions(join(repo, "packages"))
13546
+ };
13547
+ files.set("package.json", Buffer.from(renderTemplatePackageJson(Buffer.from(pkg).toString("utf8"), meta)));
13548
+ }
13549
+ return files;
13550
+ }
13551
+ const GITHUB_OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
13552
+ const GITHUB_REPO = /^[A-Za-z0-9._-]{1,100}$/;
13553
+ const PATH_SEGMENT = /^[A-Za-z0-9._@+-]{1,255}$/;
13554
+ const GIT_REF = /^[A-Za-z0-9._/+-]{1,255}$/;
13555
+ /** `--template` 값 → 받을 곳. 형식이 틀리면 api·네트워크를 부르기 전에 멈춘다 */
13556
+ function parseTemplateSpec(value) {
13557
+ const raw = value?.trim();
13558
+ if (!raw) return { kind: "bundled" };
13559
+ if (!raw.startsWith("github:")) return {
13560
+ kind: "registry",
13561
+ id: raw
13562
+ };
13563
+ const body = raw.slice(7);
13564
+ const hash = body.indexOf("#");
13565
+ const location = hash < 0 ? body : body.slice(0, hash);
13566
+ const ref = hash < 0 ? null : body.slice(hash + 1);
13567
+ const [owner = "", repo = "", ...rest] = location.split("/");
13568
+ 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 }));
13569
+ return {
13570
+ kind: "github",
13571
+ owner,
13572
+ repo,
13573
+ path: rest.join("/"),
13574
+ ref
13575
+ };
13576
+ }
13577
+ /** codeload tarball 주소 — ref가 없으면 기본 브랜치(`HEAD`) */
13578
+ function codeloadUrlOf(spec) {
13579
+ const ref = spec.ref ? spec.ref.split("/").map(encodeURIComponent).join("/") : "HEAD";
13580
+ return `https://codeload.github.com/${spec.owner}/${spec.repo}/tar.gz/${ref}`;
13581
+ }
13582
+ /** 받은 저장소 tarball을 풀어 둘 상한(압축 폭탄 방어선). 템플릿 저장소가 모노레포여도 정상 저장소는 닿지 않는다 */
13583
+ const TEMPLATE_ARCHIVE_MAX_BYTES = 256 * 1024 * 1024;
13584
+ /** GitHub 템플릿 — tarball을 받아 맨 위 폴더(`{repo}-{sha}/`)를 벗기고 `경로` 아래만 남긴다 */
13585
+ async function githubTemplateFiles(spec, label, io) {
13586
+ let res;
13587
+ try {
13588
+ res = await io.fetch(codeloadUrlOf(spec));
13589
+ } catch {
13590
+ throw new CliError(cli_create_template_unreachable({ template: label }));
13591
+ }
13592
+ if (res.status === 404) throw new CliError(cli_create_template_github_not_found({ template: label }));
13593
+ if (!res.ok) throw new CliError(cli_create_template_download_failed({
13594
+ template: label,
13595
+ status: String(res.status)
13596
+ }));
13597
+ let entries;
13598
+ try {
13599
+ entries = readTar(await gunzip(new Uint8Array(await res.arrayBuffer()), TEMPLATE_ARCHIVE_MAX_BYTES), TEMPLATE_ARCHIVE_MAX_BYTES);
13600
+ } catch (error) {
13601
+ if (error instanceof TarLimitError) throw new CliError(cli_create_template_too_large({ template: label }));
13602
+ throw new CliError(cli_create_template_unreadable({ template: label }));
13603
+ }
13604
+ const prefix = spec.path ? `${spec.path}/` : "";
13605
+ const files = /* @__PURE__ */ new Map();
13606
+ for (const entry of entries) {
13607
+ const slash = entry.path.indexOf("/");
13608
+ if (slash < 0) continue;
13609
+ const inRepo = entry.path.slice(slash + 1);
13610
+ if (!inRepo.startsWith(prefix)) continue;
13611
+ const path = inRepo.slice(prefix.length);
13612
+ if (!path || path.startsWith("/") || path.split("/").some((s) => s === "" || s === "..")) continue;
13613
+ files.set(path, entry.bytes);
13614
+ }
13615
+ if (files.size === 0) throw new CliError(cli_create_template_path_empty({ template: label }));
13010
13616
  return files;
13011
13617
  }
13012
13618
  /** npm 패키지 이름으로도 쓰는 폴더 이름 — 범위(@scope/) 없는 이름만 */
@@ -13017,24 +13623,33 @@ async function create(flags, io) {
13017
13623
  if (!NAME_PATTERN.test(name)) throw new CliError(cli_create_name_invalid({ name }));
13018
13624
  const target = resolve(io.cwd, name);
13019
13625
  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);
13626
+ const storeCode = flags.storeCode?.trim();
13627
+ if (storeCode !== void 0 && !STORE_CODE_PATTERN.test(storeCode)) throw new CliError(cli_create_store_code_invalid({ storeCode }));
13628
+ const spec = parseTemplateSpec(flags.template);
13629
+ if (spec.kind === "registry") throw new CliError(cli_create_template_not_found({ template: spec.id }));
13630
+ const source = spec.kind === "github" ? await githubTemplateFiles(spec, flags.template?.trim() ?? "", io) : bundledStarterFiles(boilerplateRoot());
13631
+ let files;
13632
+ try {
13633
+ files = transformStarterFiles(source, { name });
13634
+ } catch (error) {
13635
+ if (error instanceof StarterTransformError) throw new CliError(cli_create_template_no_package_json({ reason: error.message }));
13636
+ throw error;
13029
13637
  }
13030
- updateEnvFile(target, {
13031
- SAYREN_API_URL: `${apiOrigin}/storefront/v1`,
13032
- SAYREN_STORE_CODE: storeCode
13033
- });
13034
- io.out(cli_create_done({
13638
+ writeTree(target, files);
13639
+ if (storeCode) {
13640
+ const { apiOrigin } = resolveApiOrigin(flags, target, null, io.env);
13641
+ updateEnvFile(target, {
13642
+ SAYREN_API_URL: `${apiOrigin}/storefront/v1`,
13643
+ SAYREN_STORE_CODE: storeCode
13644
+ });
13645
+ io.out(cli_create_done_store_code({
13646
+ dir: name,
13647
+ count: files.size,
13648
+ storeCode
13649
+ }));
13650
+ } else io.out(cli_create_done({
13035
13651
  dir: name,
13036
- count: files.size,
13037
- storeCode
13652
+ count: files.size
13038
13653
  }));
13039
13654
  return 0;
13040
13655
  }
@@ -13183,8 +13798,16 @@ async function main(argv, io) {
13183
13798
  case "create": return create({
13184
13799
  name: parsed.positional[0],
13185
13800
  storeCode: str(parsed.flags["store-code"]),
13186
- apiOrigin: str(parsed.flags["api-origin"])
13801
+ apiOrigin: str(parsed.flags["api-origin"]),
13802
+ template: str(parsed.flags.template)
13187
13803
  }, io);
13804
+ case "link": return link(common, io);
13805
+ case "env":
13806
+ if (parsed.positional[0] !== "pull") throw new CliError(cli_env_usage());
13807
+ return envPull({
13808
+ ...common,
13809
+ dir: str(parsed.flags.dir) ?? parsed.positional[1]
13810
+ }, io);
13188
13811
  case "init": return init({
13189
13812
  ...common,
13190
13813
  template: str(parsed.flags.template),