sayren 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/boilerplate/.boilerplate-meta.json +44 -0
  2. package/dist/boilerplate/.claude/skills/sayren/SKILL.md +18 -0
  3. package/dist/boilerplate/.mcp.json +8 -0
  4. package/dist/boilerplate/CLAUDE.md +26 -0
  5. package/dist/boilerplate/README.md +12 -0
  6. package/dist/boilerplate/_gitignore +8 -0
  7. package/dist/boilerplate/biome.json +45 -0
  8. package/dist/boilerplate/messages/en.json +9 -0
  9. package/dist/boilerplate/messages/ko.json +9 -0
  10. package/dist/boilerplate/package.json +46 -0
  11. package/dist/boilerplate/public/_headers +5 -0
  12. package/dist/boilerplate/public/favicon.svg +1 -0
  13. package/dist/boilerplate/src/hooks/use-catalog.ts +28 -0
  14. package/dist/boilerplate/src/i18n/index.ts +99 -0
  15. package/dist/boilerplate/src/i18n/locale.server.ts +10 -0
  16. package/dist/boilerplate/src/lib/analytics.ts +60 -0
  17. package/dist/boilerplate/src/lib/api.server.ts +27 -0
  18. package/dist/boilerplate/src/lib/api.ts +7 -0
  19. package/dist/boilerplate/src/lib/config.server.ts +23 -0
  20. package/dist/boilerplate/src/lib/config.test.ts +16 -0
  21. package/dist/boilerplate/src/lib/config.ts +19 -0
  22. package/dist/boilerplate/src/lib/cookies.server.ts +35 -0
  23. package/dist/boilerplate/src/lib/format.ts +19 -0
  24. package/dist/boilerplate/src/lib/queries.ts +61 -0
  25. package/dist/boilerplate/src/routeTree.gen.ts +69 -0
  26. package/dist/boilerplate/src/router.tsx +44 -0
  27. package/dist/boilerplate/src/routes/__root.tsx +119 -0
  28. package/dist/boilerplate/src/routes/index.tsx +52 -0
  29. package/dist/boilerplate/src/start.ts +20 -0
  30. package/dist/boilerplate/src/styles.css +29 -0
  31. package/dist/boilerplate/src/theme.css +38 -0
  32. package/dist/boilerplate/tsconfig.json +16 -0
  33. package/dist/boilerplate/vite.config.ts +12 -0
  34. package/dist/boilerplate/vitest.config.ts +5 -0
  35. package/dist/index.mjs +353 -10
  36. package/package.json +6 -5
package/dist/index.mjs CHANGED
@@ -2,11 +2,11 @@
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import { stdin, stdout } from "node:process";
4
4
  import { createInterface } from "node:readline/promises";
5
- import { APP_DEPLOY_KEY_PATTERN, APP_README_MAX_BYTES, APP_SECRET_PATTERNS, APP_SERVER_BUNDLE_MAX_GZIP_BYTES, APP_UI_MAX_BYTES, APP_UI_MAX_FILES, ApiError, ApprovalPendingError, STOREFRONT_BUILD_TERMINAL_STATUSES, STOREFRONT_SOURCE_BLOB_BATCH_MAX, appIconProblem, appManifestProblems, appManifestSchema, createStoreClient, normalizeStorefrontPath } from "@sayren/store-sdk";
5
+ import { APP_DEPLOY_KEY_PATTERN, APP_README_MAX_BYTES, APP_SECRET_PATTERNS, APP_SERVER_BUNDLE_MAX_GZIP_BYTES, APP_UI_MAX_BYTES, APP_UI_MAX_FILES, ApiError, ApprovalPendingError, STOREFRONT_BUILD_TERMINAL_STATUSES, STOREFRONT_SOURCE_BLOB_BATCH_MAX, STORE_CODE_PATTERN, appIconProblem, appManifestProblems, appManifestSchema, createStoreClient, normalizeStorefrontPath } from "@sayren/store-sdk";
6
6
  import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
7
7
  import { createServer } from "node:http";
8
8
  import { dirname, extname, join, relative, resolve, sep } from "node:path";
9
- import { pathToFileURL } from "node:url";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
10
  import { gzipSync } from "node:zlib";
11
11
  import { createHash, randomBytes } from "node:crypto";
12
12
 
@@ -86,7 +86,7 @@ function isAccountToken(token) {
86
86
  return accountTokenClaimsOf(token) !== null;
87
87
  }
88
88
  /**
89
- * 폐기된 옛 상점 API 토큰(`sy_pat_…`) — #71 R1 뒤 api가 받지 않는다(401 `TOKEN_TYPE_RETIRED`). 받는 쪽은 api에 보내지 않고
89
+ * 폐기된 옛 상점 API 토큰(`sy_pat_…`) — #71 R1 뒤 api가 받지 않는다(일반 401, #157). 받는 쪽은 api에 보내지 않고
90
90
  * 계정 액세스 토큰을 만들라고 바로 안내한다.
91
91
  */
92
92
  function isRetiredStorePat(token) {
@@ -2751,6 +2751,42 @@ const mcp_rule_tenant_header_why = ((inputs = {}, options = {}) => {
2751
2751
  return ko_mcp_rule_tenant_header_why(inputs);
2752
2752
  });
2753
2753
 
2754
+ //#endregion
2755
+ //#region ../mcp/src/paraglide/messages/mcp_template_catalog_version_missing.js
2756
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
2757
+ /** @typedef {{ name: NonNullable<unknown> }} Mcp_Template_Catalog_Version_MissingInputs */
2758
+ const ko_mcp_template_catalog_version_missing = (i) => {
2759
+ return `catalog에 ${i?.name} 버전이 없습니다`;
2760
+ };
2761
+ /**
2762
+
2763
+ * @param {Mcp_Template_Catalog_Version_MissingInputs} inputs
2764
+ * @param {{ locale?: "ko" }} options
2765
+ * @returns {LocalizedString}
2766
+ */
2767
+ const mcp_template_catalog_version_missing = ((inputs, options = {}) => {
2768
+ experimentalStaticLocale$1 ?? options.locale ?? getLocale$1();
2769
+ return ko_mcp_template_catalog_version_missing(inputs);
2770
+ });
2771
+
2772
+ //#endregion
2773
+ //#region ../mcp/src/paraglide/messages/mcp_template_published_version_unknown.js
2774
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
2775
+ /** @typedef {{ name: NonNullable<unknown> }} Mcp_Template_Published_Version_UnknownInputs */
2776
+ const ko_mcp_template_published_version_unknown = (i) => {
2777
+ return `${i?.name}의 배포 버전을 모르겠습니다`;
2778
+ };
2779
+ /**
2780
+
2781
+ * @param {Mcp_Template_Published_Version_UnknownInputs} inputs
2782
+ * @param {{ locale?: "ko" }} options
2783
+ * @returns {LocalizedString}
2784
+ */
2785
+ const mcp_template_published_version_unknown = ((inputs, options = {}) => {
2786
+ experimentalStaticLocale$1 ?? options.locale ?? getLocale$1();
2787
+ return ko_mcp_template_published_version_unknown(inputs);
2788
+ });
2789
+
2754
2790
  //#endregion
2755
2791
  //#region ../mcp/src/paraglide/messages/mcp_verify_analytics_ids.js
2756
2792
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -5529,6 +5565,136 @@ const cli_continue_confirm = ((inputs = {}, options = {}) => {
5529
5565
  return ko_cli_continue_confirm(inputs);
5530
5566
  });
5531
5567
 
5568
+ //#endregion
5569
+ //#region src/paraglide/messages/cli_create_dir_not_empty.js
5570
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5571
+ /** @typedef {{ dir: NonNullable<unknown> }} Cli_Create_Dir_Not_EmptyInputs */
5572
+ const ko_cli_create_dir_not_empty = (i) => {
5573
+ return `폴더가 비어 있지 않습니다: ${i?.dir}. 새 폴더 이름을 주십시오`;
5574
+ };
5575
+ /**
5576
+
5577
+ * @param {Cli_Create_Dir_Not_EmptyInputs} inputs
5578
+ * @param {{ locale?: "ko" }} options
5579
+ * @returns {LocalizedString}
5580
+ */
5581
+ const cli_create_dir_not_empty = ((inputs, options = {}) => {
5582
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5583
+ return ko_cli_create_dir_not_empty(inputs);
5584
+ });
5585
+
5586
+ //#endregion
5587
+ //#region src/paraglide/messages/cli_create_done.js
5588
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5589
+ /** @typedef {{ dir: NonNullable<unknown>, count: NonNullable<unknown>, storeCode: NonNullable<unknown> }} Cli_Create_DoneInputs */
5590
+ const ko_cli_create_done = (i) => {
5591
+ return `보일러플레이트를 만들었습니다 — ${i?.dir} (파일 ${i?.count}개, .env에 상점 ${i?.storeCode} 연결 값)
5592
+ 다음:
5593
+ cd ${i?.dir}
5594
+ npm install
5595
+ npm run dev`;
5596
+ };
5597
+ /**
5598
+
5599
+ * @param {Cli_Create_DoneInputs} inputs
5600
+ * @param {{ locale?: "ko" }} options
5601
+ * @returns {LocalizedString}
5602
+ */
5603
+ const cli_create_done = ((inputs, options = {}) => {
5604
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5605
+ return ko_cli_create_done(inputs);
5606
+ });
5607
+
5608
+ //#endregion
5609
+ //#region src/paraglide/messages/cli_create_name_invalid.js
5610
+ /** @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}`;
5614
+ };
5615
+ /**
5616
+
5617
+ * @param {Cli_Create_Name_InvalidInputs} inputs
5618
+ * @param {{ locale?: "ko" }} options
5619
+ * @returns {LocalizedString}
5620
+ */
5621
+ const cli_create_name_invalid = ((inputs, options = {}) => {
5622
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5623
+ return ko_cli_create_name_invalid(inputs);
5624
+ });
5625
+
5626
+ //#endregion
5627
+ //#region src/paraglide/messages/cli_create_prompt_store_code.js
5628
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5629
+ /** @typedef {{}} Cli_Create_Prompt_Store_CodeInputs */
5630
+ const ko_cli_create_prompt_store_code = () => {
5631
+ return `상점 코드(상점 플랫폼 › 설정 › 상점 정보): `;
5632
+ };
5633
+ /**
5634
+
5635
+ * @param {Cli_Create_Prompt_Store_CodeInputs} inputs
5636
+ * @param {{ locale?: "ko" }} options
5637
+ * @returns {LocalizedString}
5638
+ */
5639
+ const cli_create_prompt_store_code = ((inputs = {}, options = {}) => {
5640
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5641
+ return ko_cli_create_prompt_store_code(inputs);
5642
+ });
5643
+
5644
+ //#endregion
5645
+ //#region src/paraglide/messages/cli_create_store_code_invalid.js
5646
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5647
+ /** @typedef {{ storeCode: NonNullable<unknown> }} Cli_Create_Store_Code_InvalidInputs */
5648
+ const ko_cli_create_store_code_invalid = (i) => {
5649
+ return `상점 코드 형식이 아닙니다: ${i?.storeCode}. 영문 소문자·숫자·-로 3~30자입니다`;
5650
+ };
5651
+ /**
5652
+
5653
+ * @param {Cli_Create_Store_Code_InvalidInputs} inputs
5654
+ * @param {{ locale?: "ko" }} options
5655
+ * @returns {LocalizedString}
5656
+ */
5657
+ const cli_create_store_code_invalid = ((inputs, options = {}) => {
5658
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5659
+ return ko_cli_create_store_code_invalid(inputs);
5660
+ });
5661
+
5662
+ //#endregion
5663
+ //#region src/paraglide/messages/cli_create_store_code_required.js
5664
+ /** @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 <상점 코드>로 주십시오(비대화형은 필수입니다)`;
5668
+ };
5669
+ /**
5670
+
5671
+ * @param {Cli_Create_Store_Code_RequiredInputs} inputs
5672
+ * @param {{ locale?: "ko" }} options
5673
+ * @returns {LocalizedString}
5674
+ */
5675
+ const cli_create_store_code_required = ((inputs = {}, options = {}) => {
5676
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5677
+ return ko_cli_create_store_code_required(inputs);
5678
+ });
5679
+
5680
+ //#endregion
5681
+ //#region src/paraglide/messages/cli_create_usage.js
5682
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
5683
+ /** @typedef {{}} Cli_Create_UsageInputs */
5684
+ const ko_cli_create_usage = () => {
5685
+ return `폴더 이름을 주십시오: npx sayren create <이름> [--store-code <상점 코드>]`;
5686
+ };
5687
+ /**
5688
+
5689
+ * @param {Cli_Create_UsageInputs} inputs
5690
+ * @param {{ locale?: "ko" }} options
5691
+ * @returns {LocalizedString}
5692
+ */
5693
+ const cli_create_usage = ((inputs = {}, options = {}) => {
5694
+ experimentalStaticLocale ?? options.locale ?? getLocale();
5695
+ return ko_cli_create_usage(inputs);
5696
+ });
5697
+
5532
5698
  //#endregion
5533
5699
  //#region src/paraglide/messages/cli_credentials_git_tracked.js
5534
5700
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -8835,6 +9001,7 @@ const ko_cli_usage = (i) => {
8835
9001
  npx sayren login --paste # 액세스 토큰을 입력받아 저장(셸 기록에 남지 않습니다)
8836
9002
  npx sayren logout
8837
9003
  npx sayren whoami
9004
+ npx sayren create <이름> [--store-code <상점 코드>] # 스토어프론트 보일러플레이트를 새 폴더에 만듭니다(로그인 없음)
8838
9005
  npx sayren init [폴더] [--template <id>] [--store <storeId>] [--yes]
8839
9006
  npx sayren pull --project <storeId> [폴더] [--force [--yes]]
8840
9007
  npx sayren deploy [--no-publish] [--skip-build] [--message <메모>] [--force [--yes]]
@@ -8847,6 +9014,7 @@ const ko_cli_usage = (i) => {
8847
9014
  login Sayren 계정으로 로그인합니다. 로그인 토큰은 사용자 설정 폴더(~/.config/sayren/credentials.json)에 둡니다
8848
9015
  logout 로그인을 폐기하고 이 컴퓨터의 토큰을 지웁니다
8849
9016
  whoami 쓰는 계정·토큰 종류·상점 범위
9017
+ create 스토어프론트 보일러플레이트(상점 이름·상품 목록 홈, SDK·문구·분석·에이전트 안내)를 <이름> 폴더에 만들고 .env에 상점 연결 값을 씁니다. 호스팅 사이트는 만들지 않습니다
8850
9018
  init 템플릿을 골라 호스팅 사이트를 만들고 pull과 같은 폴더(기본: 상점 코드)로 받습니다. 사이트가 이미 있으면 만들지 않습니다
8851
9019
  pull 호스팅 중인 사이트의 작업 트리를 폴더(기본: 상점 코드)로 받고 .mcp.json·.env·.sayren/state.json을 씁니다
8852
9020
  deploy 로컬 검사 → 로컬 빌드 → 작업 트리 올리기 → 빌드 → 발행. 원격이 pull 이후 바뀌었으면 덮어쓰지 않습니다
@@ -8863,6 +9031,7 @@ const ko_cli_usage = (i) => {
8863
9031
  옵션:
8864
9032
  --project <storeId> pull: 받을 상점(--store와 같습니다)
8865
9033
  --template <id> init: 템플릿 id(비대화형은 필수)
9034
+ --store-code <코드> create: 연결할 상점 코드(없으면 묻습니다, 비대화형은 필수)
8866
9035
  --dir <폴더> 작업 폴더(기본: 지금 폴더, pull·init은 상점 코드)
8867
9036
  --force pull: 같은 사이트를 받은 폴더에 원격을 다시 받습니다(지울·덮을 파일을 보여 주고 묻습니다)
8868
9037
  deploy: 원격 변경·열린 편집 환경을 무시하고 덮어씁니다(확인을 묻습니다)
@@ -9784,7 +9953,7 @@ async function selectStore(auth, client, io, candidates, folderStore = null) {
9784
9953
 
9785
9954
  //#endregion
9786
9955
  //#region package.json
9787
- var version = "0.13.0";
9956
+ var version = "0.14.0";
9788
9957
 
9789
9958
  //#endregion
9790
9959
  //#region src/client.ts
@@ -11131,7 +11300,7 @@ function verifySources(files, options = {}) {
11131
11300
  if (cookieFiles.length > 0 && !cookieFiles.some((file) => /httpOnly\s*:\s*true/.test(file.content))) inProject("buyer-token-cookie", mcp_verify_buyer_token_cookie_missing());
11132
11301
  if (!has(".refresh(")) inProject("buyer-session-refresh", mcp_verify_buyer_session_refresh());
11133
11302
  if (has(".session(") && !has(".signUp(")) inProject("buyer-signup", mcp_verify_buyer_signup());
11134
- if (!matches(/\b(auth|authFor\(\))\.signOut\(|\.signOut\(\s*accessToken\b/)) inProject("buyer-signout", mcp_verify_buyer_signout());
11303
+ if (!matches(/\b(auth|authFor\(\))\s*\.signOut\(|\.signOut\(\s*accessToken\b/)) inProject("buyer-signout", mcp_verify_buyer_signout());
11135
11304
  if (!has("reconsentRequired")) inProject("buyer-reconsent", mcp_verify_buyer_reconsent());
11136
11305
  }
11137
11306
  if (has("startPayment(") && !has("checkoutAvailable")) inProject("checkout-availability", mcp_verify_checkout_availability());
@@ -11468,7 +11637,7 @@ const WRITABLE_ROOT_FILES = new Set([
11468
11637
  ]);
11469
11638
  /**
11470
11639
  * 읽기만 되는 루트 파일 — 의존성은 전용 경로로만 바꾼다(레지스트리 프록시와 잠금 파일을 함께 맞춘다).
11471
- * `CLAUDE.md`·`.mcp.json`은 템플릿이 싣는 에이전트 안내·MCP 연결 설정이다(2026-10-14, blank 템플릿) — 편집 도구는 바꾸지 않는다
11640
+ * `CLAUDE.md`·`.mcp.json`은 템플릿이 싣는 에이전트 안내·MCP 연결 설정이다(2026-10-14 blank, #271부터 보일러플레이트 `boilerplate`) — 편집 도구는 바꾸지 않는다
11472
11641
  */
11473
11642
  const READONLY_ROOT_FILES = new Set([
11474
11643
  "package.json",
@@ -11651,7 +11820,7 @@ function isNeverUpload(path) {
11651
11820
  return segments.some((segment) => segment.startsWith(".env") || segment === ".sayren") || name === ".mcp.json" || name === ".sayrenrc";
11652
11821
  }
11653
11822
  /** 걸어 들어가지 않는 폴더 — 설치물·산출물·캐시·VCS(경로 규칙이 어차피 막는다. 크기 때문에 읽지도 않는다) */
11654
- const SKIP_DIRS = new Set([
11823
+ const SKIP_DIRS$1 = new Set([
11655
11824
  "node_modules",
11656
11825
  "dist",
11657
11826
  ".output",
@@ -11672,7 +11841,7 @@ function collectTree(root) {
11672
11841
  const full = join(dir, entry.name);
11673
11842
  const rel = normalizeStorefrontPath(relative(root, full).split(sep).join("/"));
11674
11843
  if (entry.isDirectory()) {
11675
- if (SKIP_DIRS.has(entry.name)) continue;
11844
+ if (SKIP_DIRS$1.has(entry.name)) continue;
11676
11845
  walk(full);
11677
11846
  continue;
11678
11847
  }
@@ -11883,7 +12052,7 @@ function lockImporterSpecifiers(lock) {
11883
12052
  //#region src/guide-skill.ts
11884
12053
  /**
11885
12054
  * 받은 폴더의 에이전트 안내 스킬(2026-10-16 사용자 결정) — 셀러의 Claude Code가 기능·화면을 고치기 전에 sayren 중앙 스킬을 `npx sayren skill`로 읽게 한다(스킬은 이 하나만 설치한다).
11886
- * 빈 템플릿이 싣는 `templates/storefront-blank/.claude/skills/sayren/SKILL.md`와 같은 내용이다(`guide-skill.test.ts`가 고정한다).
12055
+ * 보일러플레이트(`npx sayren create`, #271)가 싣는 `packages/cli/boilerplate/.claude/skills/sayren/SKILL.md`와 같은 내용이다(`guide-skill.test.ts`가 고정한다).
11887
12056
  */
11888
12057
  const GUIDE_SKILL_PATH = ".claude/skills/sayren/SKILL.md";
11889
12058
  const GUIDE_SKILL = "---\nname: sayren\ndescription: Load before adding, implementing or changing any feature or page of this sayren storefront. Lists the current sayren skills and loads the ones the task needs.\n---\n\n# sayren skills\n\nThe sayren skills are maintained centrally and change without updating this project.\nDo not rely on memory or earlier copies. Load them at the start of each task.\n\n1. List the skills (name and when to use each):\n `npx -y sayren@latest skill list`\n2. Read every skill that matches the task, in full, and follow it:\n `npx -y sayren@latest skill <name>`\n\nIf the sayren MCP server is connected, the tools `list_skills` and `get_skill` return the same content.\n\nFollow the project's `CLAUDE.md` first. The skills add the sayren-specific details.\n";
@@ -12702,6 +12871,174 @@ function relativeOf(io, path) {
12702
12871
  return relative(io.cwd, path) || ".";
12703
12872
  }
12704
12873
 
12874
+ //#endregion
12875
+ //#region ../mcp/src/template-package.ts
12876
+ /** package.json의 기준 템플릿 표식 필드 — 원천은 이 함수다(api `template-marker.ts`가 같은 모양을 읽는다) */
12877
+ function withTemplateMarker(pkg, marker) {
12878
+ const current = pkg.sayren;
12879
+ pkg.sayren = {
12880
+ ...current && typeof current === "object" && !Array.isArray(current) ? current : {},
12881
+ template: marker.id,
12882
+ templateVersion: marker.version
12883
+ };
12884
+ return pkg;
12885
+ }
12886
+ const DEP_FIELDS = [
12887
+ "dependencies",
12888
+ "devDependencies",
12889
+ "peerDependencies"
12890
+ ];
12891
+ function renderTemplatePackageJson(raw, meta, options = {}) {
12892
+ const pkg = JSON.parse(raw);
12893
+ for (const field of DEP_FIELDS) {
12894
+ const deps = pkg[field];
12895
+ if (!deps) continue;
12896
+ for (const [name, spec] of Object.entries(deps)) deps[name] = resolveSpec(name, spec, meta);
12897
+ }
12898
+ if (options.name) pkg.name = options.name;
12899
+ const marker = options.template ?? meta.template;
12900
+ if (marker) withTemplateMarker(pkg, marker);
12901
+ return `${JSON.stringify(pkg, null, 2)}\n`;
12902
+ }
12903
+ function resolveSpec(name, spec, meta) {
12904
+ if (spec === "catalog:" || spec === "catalog:default") {
12905
+ const version$1 = meta.catalog[name];
12906
+ if (!version$1) throw new Error(mcp_template_catalog_version_missing({ name }));
12907
+ return version$1;
12908
+ }
12909
+ if (spec.startsWith("workspace:")) return workspaceRange(name, spec.slice(10), meta);
12910
+ return spec;
12911
+ }
12912
+ /**
12913
+ * `workspace:` 뒤 표기 → 셀러 프로젝트의 범위(#232). 템플릿은 SDK를 범위(`workspace:^0.35.0`)로 적고 `workspace:`만 뗀다 —
12914
+ * SDK patch가 올라도 풀어 쓴 package.json·첫 작업 트리가 그대로라 템플릿 버전을 올리지 않는다. 범위가 아닌 `*`·`^`·`~`만
12915
+ * 지금 워크스페이스 버전으로 푼다(`*`는 `^{버전}`)
12916
+ */
12917
+ function workspaceRange(name, rest, meta) {
12918
+ if (rest !== "*" && rest !== "^" && rest !== "~" && rest !== "") return rest;
12919
+ const version$1 = meta.versions[name];
12920
+ if (!version$1) throw new Error(mcp_template_published_version_unknown({ name }));
12921
+ return `${rest === "~" ? "~" : "^"}${version$1}`;
12922
+ }
12923
+ /** pnpm-workspace.yaml의 최상위 `catalog:` 블록만 읽는다(` key: value`, 따옴표 허용) */
12924
+ function parseCatalog(yaml) {
12925
+ const out = {};
12926
+ let inCatalog = false;
12927
+ for (const line of yaml.split("\n")) {
12928
+ if (/^catalog:\s*$/.test(line)) {
12929
+ inCatalog = true;
12930
+ continue;
12931
+ }
12932
+ if (!inCatalog) continue;
12933
+ if (/^\S/.test(line)) break;
12934
+ const match = line.match(/^\s+["']?([^"':]+)["']?\s*:\s*["']?([^"'#\s]+)["']?/);
12935
+ if (match?.[1] && match[2]) out[match[1].trim()] = match[2];
12936
+ }
12937
+ return out;
12938
+ }
12939
+ function readWorkspaceVersions(packagesDir) {
12940
+ const out = {};
12941
+ for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
12942
+ if (!entry.isDirectory()) continue;
12943
+ const file = join(packagesDir, entry.name, "package.json");
12944
+ if (!existsSync(file)) continue;
12945
+ const pkg = JSON.parse(readFileSync(file, "utf8"));
12946
+ if (pkg.name && pkg.version) out[pkg.name] = pkg.version;
12947
+ }
12948
+ return out;
12949
+ }
12950
+
12951
+ //#endregion
12952
+ //#region src/create-command.ts
12953
+ /**
12954
+ * `npx sayren create <이름>`(#271) — 스토어프론트 보일러플레이트를 새 폴더에 쓴다. 원천은 `packages/cli/boilerplate` 하나이고
12955
+ * 호스팅 「처음부터」 템플릿(카탈로그 `boilerplate`)도 같은 폴더에서 만든다. 로그인·사이트 만들기는 하지 않는다(사이트는 `init`).
12956
+ * 상점 연결 값은 `--store-code` → 묻기(대화형) 순서이고 `.env`에 `SAYREN_API_URL`·`SAYREN_STORE_CODE`로 쓴다.
12957
+ */
12958
+ /** 보일러플레이트 위치 — 게시본은 빌드가 복사한 `dist/boilerplate`, 저장소에서 바로 돌리면 `packages/cli/boilerplate` */
12959
+ function boilerplateRoot() {
12960
+ const here = dirname(fileURLToPath(import.meta.url));
12961
+ const candidates = [join(here, "boilerplate"), join(here, "..", "boilerplate")];
12962
+ const found = candidates.find((dir) => existsSync(join(dir, "package.json")));
12963
+ if (!found) throw new Error(`boilerplate not found: ${candidates.join(", ")}`);
12964
+ return found;
12965
+ }
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`만 */
12978
+ const SKIP_DIRS = new Set([
12979
+ "node_modules",
12980
+ "dist",
12981
+ ".output",
12982
+ ".tanstack",
12983
+ ".nitro",
12984
+ ".wrangler"
12985
+ ]);
12986
+ const DOT_KEPT = new Set([
12987
+ ".claude",
12988
+ ".mcp.json",
12989
+ ".gitignore"
12990
+ ]);
12991
+ /** 보일러플레이트 파일 — 상대 경로 → 내용. 게시본의 `_gitignore`는 `.gitignore`로 되돌린다(npm이 `.gitignore`를 싣지 않는다) */
12992
+ function boilerplateFiles(root, name) {
12993
+ const files = /* @__PURE__ */ new Map();
12994
+ const walk = (dir) => {
12995
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
12996
+ if (SKIP_DIRS.has(entry.name)) continue;
12997
+ if (entry.name.startsWith(".") && !DOT_KEPT.has(entry.name)) continue;
12998
+ const full = join(dir, entry.name);
12999
+ 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
+ }
13004
+ }
13005
+ };
13006
+ walk(root);
13007
+ 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 })));
13010
+ return files;
13011
+ }
13012
+ /** npm 패키지 이름으로도 쓰는 폴더 이름 — 범위(@scope/) 없는 이름만 */
13013
+ const NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,213}$/;
13014
+ async function create(flags, io) {
13015
+ const name = flags.name?.trim();
13016
+ if (!name) throw new CliError(cli_create_usage());
13017
+ if (!NAME_PATTERN.test(name)) throw new CliError(cli_create_name_invalid({ name }));
13018
+ const target = resolve(io.cwd, name);
13019
+ 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);
13029
+ }
13030
+ updateEnvFile(target, {
13031
+ SAYREN_API_URL: `${apiOrigin}/storefront/v1`,
13032
+ SAYREN_STORE_CODE: storeCode
13033
+ });
13034
+ io.out(cli_create_done({
13035
+ dir: name,
13036
+ count: files.size,
13037
+ storeCode
13038
+ }));
13039
+ return 0;
13040
+ }
13041
+
12705
13042
  //#endregion
12706
13043
  //#region src/skill-commands.ts
12707
13044
  /**
@@ -12742,6 +13079,7 @@ const VALUE_FLAGS = new Set([
12742
13079
  "store",
12743
13080
  "accounts-origin",
12744
13081
  "template",
13082
+ "store-code",
12745
13083
  "key",
12746
13084
  "name",
12747
13085
  "port",
@@ -12842,6 +13180,11 @@ async function main(argv, io) {
12842
13180
  tool: str(parsed.flags.tool),
12843
13181
  yes: parsed.flags.yes === true
12844
13182
  }, io);
13183
+ case "create": return create({
13184
+ name: parsed.positional[0],
13185
+ storeCode: str(parsed.flags["store-code"]),
13186
+ apiOrigin: str(parsed.flags["api-origin"])
13187
+ }, io);
12845
13188
  case "init": return init({
12846
13189
  ...common,
12847
13190
  template: str(parsed.flags.template),
@@ -12893,6 +13236,7 @@ function defaultIo() {
12893
13236
  out: (line) => console.log(line),
12894
13237
  err: (line) => console.error(line),
12895
13238
  confirm: stdin.isTTY ? async (question) => /^(y|yes|예|네)$/i.test((await ask(`${question} (y/N) `) ?? "").trim()) : null,
13239
+ ask: stdin.isTTY ? ask : null,
12896
13240
  sleep: (ms) => new Promise((resolve$1) => setTimeout(resolve$1, ms)),
12897
13241
  buildTimeoutMs: 20 * 6e4,
12898
13242
  pollMs: 3e3,
@@ -12935,7 +13279,6 @@ function openBrowser(url) {
12935
13279
  function describeError(error) {
12936
13280
  if (error instanceof CliError) return error.message;
12937
13281
  if (error instanceof ApiError) {
12938
- if (error.status === 401 && error.code === "TOKEN_TYPE_RETIRED") return cli_retired_token();
12939
13282
  if (error.status === 401) return cli_error_unauthorized();
12940
13283
  if (error.status === 403 && error.code === "STORE_NOT_GRANTED") return cli_error_store_not_granted();
12941
13284
  if (error.status === 403 && error.code === "INSUFFICIENT_SCOPE") return cli_error_insufficient_scope();
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "sayren",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "imports": {
6
6
  "#paraglide/*": "./src/paraglide/*"
7
7
  },
8
- "description": "sayren CLI — 호스팅 스토어프론트(init·pull·deploy)와 서드파티 앱(app init·dev·deploy·release)",
8
+ "description": "sayren CLI — 스토어프론트 보일러플레이트(create)·호스팅 스토어프론트(init·pull·deploy)와 서드파티 앱(app init·dev·deploy·release)",
9
9
  "bin": {
10
10
  "sayren": "./dist/index.mjs"
11
11
  },
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "esbuild": "^0.28.2",
20
- "@sayren/store-sdk": "^0.37.0"
20
+ "@sayren/store-sdk": "^0.53.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@biomejs/biome": "^2.5.14",
@@ -28,7 +28,8 @@
28
28
  "tsx": "^4.20.3",
29
29
  "typescript": "^6.0.3",
30
30
  "vitest": "^5.0.1",
31
- "@sayren/mcp": "0.27.0"
31
+ "@sayren/app": "0.9.0",
32
+ "@sayren/mcp": "0.29.3"
32
33
  },
33
34
  "publishConfig": {
34
35
  "access": "public"
@@ -41,7 +42,7 @@
41
42
  "scripts": {
42
43
  "dev": "tsx src/index.ts",
43
44
  "i18n": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide --strategy baseLocale --emit-ts-declarations --no-emit-readme --no-emit-prettier-ignore --silent",
44
- "build": "pnpm i18n && tsdown",
45
+ "build": "pnpm i18n && tsdown && node scripts/copy-boilerplate.mjs",
45
46
  "lint": "biome check --write . && tsc --noEmit",
46
47
  "typecheck": "tsc --noEmit",
47
48
  "test": "vitest run"