sayren 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +230 -161
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -2261,84 +2261,6 @@ const mcp_rule_payment_browser_launch_why = ((inputs = {}, options = {}) => {
2261
2261
  return ko_mcp_rule_payment_browser_launch_why(inputs);
2262
2262
  });
2263
2263
 
2264
- //#endregion
2265
- //#region ../mcp/src/paraglide/messages/mcp_rule_payment_popup_prepare_example.js
2266
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
2267
- /** @typedef {{}} Mcp_Rule_Payment_Popup_Prepare_ExampleInputs */
2268
- const ko_mcp_rule_payment_popup_prepare_example = () => {
2269
- return `const paymentWindow = payments.prepareWindow(); // 클릭 시점에 먼저 연다
2270
- const { start, error } = await startPayment({ data });
2271
- if (!start) {
2272
- paymentWindow.close();
2273
- return;
2274
- }
2275
- await payments.open(parsed, { window: paymentWindow });`;
2276
- };
2277
- /**
2278
-
2279
- * @param {Mcp_Rule_Payment_Popup_Prepare_ExampleInputs} inputs
2280
- * @param {{ locale?: "ko" }} options
2281
- * @returns {LocalizedString}
2282
- */
2283
- const mcp_rule_payment_popup_prepare_example = ((inputs = {}, options = {}) => {
2284
- experimentalStaticLocale$1 ?? options.locale ?? getLocale$1();
2285
- return ko_mcp_rule_payment_popup_prepare_example(inputs);
2286
- });
2287
-
2288
- //#endregion
2289
- //#region ../mcp/src/paraglide/messages/mcp_rule_payment_popup_prepare_fix.js
2290
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
2291
- /** @typedef {{}} Mcp_Rule_Payment_Popup_Prepare_FixInputs */
2292
- const ko_mcp_rule_payment_popup_prepare_fix = () => {
2293
- return `클릭 핸들러 첫 줄에서 \`payments.prepareWindow()\`로 빈 창을 열고, 서버 함수가 끝나면 그 핸들을 \`payments.open(start, { window })\`에 넘긴다. 결제 시작이 실패하면 \`window.close()\`로 닫는다`;
2294
- };
2295
- /**
2296
-
2297
- * @param {Mcp_Rule_Payment_Popup_Prepare_FixInputs} inputs
2298
- * @param {{ locale?: "ko" }} options
2299
- * @returns {LocalizedString}
2300
- */
2301
- const mcp_rule_payment_popup_prepare_fix = ((inputs = {}, options = {}) => {
2302
- experimentalStaticLocale$1 ?? options.locale ?? getLocale$1();
2303
- return ko_mcp_rule_payment_popup_prepare_fix(inputs);
2304
- });
2305
-
2306
- //#endregion
2307
- //#region ../mcp/src/paraglide/messages/mcp_rule_payment_popup_prepare_title.js
2308
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
2309
- /** @typedef {{}} Mcp_Rule_Payment_Popup_Prepare_TitleInputs */
2310
- const ko_mcp_rule_payment_popup_prepare_title = () => {
2311
- return `결제 시작이 서버 함수를 거치면 클릭 시점에 \`payments.prepareWindow()\`로 창을 먼저 연다`;
2312
- };
2313
- /**
2314
-
2315
- * @param {Mcp_Rule_Payment_Popup_Prepare_TitleInputs} inputs
2316
- * @param {{ locale?: "ko" }} options
2317
- * @returns {LocalizedString}
2318
- */
2319
- const mcp_rule_payment_popup_prepare_title = ((inputs = {}, options = {}) => {
2320
- experimentalStaticLocale$1 ?? options.locale ?? getLocale$1();
2321
- return ko_mcp_rule_payment_popup_prepare_title(inputs);
2322
- });
2323
-
2324
- //#endregion
2325
- //#region ../mcp/src/paraglide/messages/mcp_rule_payment_popup_prepare_why.js
2326
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
2327
- /** @typedef {{}} Mcp_Rule_Payment_Popup_Prepare_WhyInputs */
2328
- const ko_mcp_rule_payment_popup_prepare_why = () => {
2329
- return `팝업은 사용자 클릭에서 동기로 열어야 차단되지 않는다. 서버 응답을 기다린 뒤 열면 브라우저가 막아 결제가 시작되지 않는다`;
2330
- };
2331
- /**
2332
-
2333
- * @param {Mcp_Rule_Payment_Popup_Prepare_WhyInputs} inputs
2334
- * @param {{ locale?: "ko" }} options
2335
- * @returns {LocalizedString}
2336
- */
2337
- const mcp_rule_payment_popup_prepare_why = ((inputs = {}, options = {}) => {
2338
- experimentalStaticLocale$1 ?? options.locale ?? getLocale$1();
2339
- return ko_mcp_rule_payment_popup_prepare_why(inputs);
2340
- });
2341
-
2342
2264
  //#endregion
2343
2265
  //#region ../mcp/src/paraglide/messages/mcp_rule_payment_processing_status_example.js
2344
2266
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -3297,24 +3219,6 @@ const mcp_verify_payment_browser_launch_server_file = ((inputs = {}, options = {
3297
3219
  return ko_mcp_verify_payment_browser_launch_server_file(inputs);
3298
3220
  });
3299
3221
 
3300
- //#endregion
3301
- //#region ../mcp/src/paraglide/messages/mcp_verify_payment_popup_prepare.js
3302
- /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
3303
- /** @typedef {{}} Mcp_Verify_Payment_Popup_PrepareInputs */
3304
- const ko_mcp_verify_payment_popup_prepare = () => {
3305
- return `받아 둔 결제 시작 값으로 결제창을 열지만 클릭 시점에 \`payments.prepareWindow()\`로 창을 먼저 여는 코드가 없다`;
3306
- };
3307
- /**
3308
-
3309
- * @param {Mcp_Verify_Payment_Popup_PrepareInputs} inputs
3310
- * @param {{ locale?: "ko" }} options
3311
- * @returns {LocalizedString}
3312
- */
3313
- const mcp_verify_payment_popup_prepare = ((inputs = {}, options = {}) => {
3314
- experimentalStaticLocale$1 ?? options.locale ?? getLocale$1();
3315
- return ko_mcp_verify_payment_popup_prepare(inputs);
3316
- });
3317
-
3318
3222
  //#endregion
3319
3223
  //#region ../mcp/src/paraglide/messages/mcp_verify_payment_processing_status.js
3320
3224
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -8134,6 +8038,61 @@ const cli_retired_token = ((inputs = {}, options = {}) => {
8134
8038
  return ko_cli_retired_token(inputs);
8135
8039
  });
8136
8040
 
8041
+ //#endregion
8042
+ //#region src/paraglide/messages/cli_skills_install_failed.js
8043
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
8044
+ /** @typedef {{ command: NonNullable<unknown>, detail: NonNullable<unknown> }} Cli_Skills_Install_FailedInputs */
8045
+ const ko_cli_skills_install_failed = (i) => {
8046
+ return `에이전트 스킬을 설치하지 못했습니다. 받은 폴더에서 직접 실행하십시오: ${i?.command}
8047
+ ${i?.detail}`;
8048
+ };
8049
+ /**
8050
+
8051
+ * @param {Cli_Skills_Install_FailedInputs} inputs
8052
+ * @param {{ locale?: "ko" }} options
8053
+ * @returns {LocalizedString}
8054
+ */
8055
+ const cli_skills_install_failed = ((inputs, options = {}) => {
8056
+ experimentalStaticLocale ?? options.locale ?? getLocale();
8057
+ return ko_cli_skills_install_failed(inputs);
8058
+ });
8059
+
8060
+ //#endregion
8061
+ //#region src/paraglide/messages/cli_skills_installed.js
8062
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
8063
+ /** @typedef {{}} Cli_Skills_InstalledInputs */
8064
+ const ko_cli_skills_installed = () => {
8065
+ return `에이전트 스킬 설치 완료 — .claude/skills/ (배포 때 올리지 않습니다)`;
8066
+ };
8067
+ /**
8068
+
8069
+ * @param {Cli_Skills_InstalledInputs} inputs
8070
+ * @param {{ locale?: "ko" }} options
8071
+ * @returns {LocalizedString}
8072
+ */
8073
+ const cli_skills_installed = ((inputs = {}, options = {}) => {
8074
+ experimentalStaticLocale ?? options.locale ?? getLocale();
8075
+ return ko_cli_skills_installed(inputs);
8076
+ });
8077
+
8078
+ //#endregion
8079
+ //#region src/paraglide/messages/cli_skills_installing.js
8080
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
8081
+ /** @typedef {{ source: NonNullable<unknown> }} Cli_Skills_InstallingInputs */
8082
+ const ko_cli_skills_installing = (i) => {
8083
+ return `에이전트 스킬 설치 중 — ${i?.source}`;
8084
+ };
8085
+ /**
8086
+
8087
+ * @param {Cli_Skills_InstallingInputs} inputs
8088
+ * @param {{ locale?: "ko" }} options
8089
+ * @returns {LocalizedString}
8090
+ */
8091
+ const cli_skills_installing = ((inputs, options = {}) => {
8092
+ experimentalStaticLocale ?? options.locale ?? getLocale();
8093
+ return ko_cli_skills_installing(inputs);
8094
+ });
8095
+
8137
8096
  //#endregion
8138
8097
  //#region src/paraglide/messages/cli_skipped_outside.js
8139
8098
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -9784,7 +9743,7 @@ async function selectStore(auth, client, io, candidates, folderStore = null) {
9784
9743
 
9785
9744
  //#endregion
9786
9745
  //#region package.json
9787
- var version = "0.9.0";
9746
+ var version = "0.11.0";
9788
9747
 
9789
9748
  //#endregion
9790
9749
  //#region src/client.ts
@@ -10870,21 +10829,12 @@ const RULES = [
10870
10829
  doc: "/guides/checkout-flow",
10871
10830
  templateFile: "src/lib/payments.ts"
10872
10831
  }),
10873
- defineRule({
10874
- id: "payment-popup-prepare",
10875
- title: mcp_rule_payment_popup_prepare_title,
10876
- why: mcp_rule_payment_popup_prepare_why,
10877
- fix: mcp_rule_payment_popup_prepare_fix,
10878
- example: mcp_rule_payment_popup_prepare_example,
10879
- doc: "/guides/checkout-flow",
10880
- templateFile: "src/routes/checkout.index.tsx"
10881
- }),
10882
10832
  defineRule({
10883
10833
  id: "payment-return-route",
10884
10834
  title: mcp_rule_payment_return_route_title,
10885
10835
  why: mcp_rule_payment_return_route_why,
10886
10836
  fix: mcp_rule_payment_return_route_fix,
10887
- example: "useEffect(() => {\n void payments.result(window.location.href).then((result) => {\n if (result.status === \"COMPLETED\") navigate({ to: \"/checkout/complete\", search: { orderId: result.orderId } });\n });\n}, []);",
10837
+ example: "useEffect(() => {\n void payments.result().then((result) => {\n if (result.status === \"COMPLETED\") navigate({ to: \"/checkout/complete\", search: { orderId: result.orderId } });\n });\n}, []);",
10888
10838
  doc: "/guides/checkout-flow",
10889
10839
  templateFile: "src/routes/checkout.return.tsx"
10890
10840
  }),
@@ -11128,7 +11078,6 @@ function verifySources(files, options = {}) {
11128
11078
  }
11129
11079
  if (has("startPayment(") && !has("@sayren/storefront-sdk/payments")) inProject("payment-browser-launch", mcp_verify_payment_browser_launch_missing());
11130
11080
  const opensPayment = matches(/\bpayments\.open\(|\.open\(\s*(start|paymentStart)\b/);
11131
- if (opensPayment && !has("prepareWindow(")) inFile("payment-popup-prepare", opensPayment, mcp_verify_payment_popup_prepare(), /\bpayments\.open\(|\.open\(\s*(start|paymentStart)\b/);
11132
11081
  if (opensPayment && !/amounts/.test(opensPayment.content)) inFile("payment-amounts-final", opensPayment, mcp_verify_payment_amounts_final(), /\bpayments\.open\(/);
11133
11082
  const fixedReturnUrl = matches(/returnUrl\s*[:=]\s*["'`]https?:\/\//);
11134
11083
  if (fixedReturnUrl) inFile("checkout-origin", fixedReturnUrl, mcp_verify_checkout_origin(), /returnUrl\s*[:=]\s*["'`]https?:\/\//);
@@ -11476,8 +11425,18 @@ const WRITABLE_ROOT_FILES = new Set([
11476
11425
  "biome.json",
11477
11426
  ".gitignore"
11478
11427
  ]);
11479
- /** 읽기만 되는 루트 파일 — 의존성은 전용 경로로만 바꾼다(레지스트리 프록시와 잠금 파일을 함께 맞춘다) */
11480
- const READONLY_ROOT_FILES = new Set(["package.json", "pnpm-lock.yaml"]);
11428
+ /**
11429
+ * 읽기만 되는 루트 파일 — 의존성은 전용 경로로만 바꾼다(레지스트리 프록시와 잠금 파일을 함께 맞춘다).
11430
+ * `CLAUDE.md`·`.mcp.json`은 템플릿이 싣는 에이전트 안내·MCP 연결 설정이다(2026-10-14, blank 템플릿) — 편집 도구는 바꾸지 않는다
11431
+ */
11432
+ const READONLY_ROOT_FILES = new Set([
11433
+ "package.json",
11434
+ "pnpm-lock.yaml",
11435
+ "CLAUDE.md",
11436
+ ".mcp.json"
11437
+ ]);
11438
+ /** 읽기만 되는 폴더 — 템플릿이 싣는 Claude 스킬(`.claude/skills/…`, 2026-10-14). 셀러 로컬 Claude Code와 AI 상점 도우미가 같은 기준을 읽는다 */
11439
+ const READONLY_DIRS = new Set([".claude"]);
11481
11440
  /**
11482
11441
  * 쓸 수 있는 폴더 — 코드(`src`), 정적 파일(`public`), 화면 문구 카탈로그(`messages`, 템플릿 `src/i18n`이 `messages/ko.json`을 import한다, #73).
11483
11442
  * 템플릿이 import하는 파일이 이 밖에 있으면 첫 작업 트리(source.tgz)를 풀 때 빠져 빌드·검사가 깨진다(#78)
@@ -11490,6 +11449,7 @@ const WRITABLE_DIRS = new Set([
11490
11449
  /** 작업 트리 루트에 둘 수 있는 이름(폴더·파일) — 컨테이너 스냅샷이 이 이름만 묶는다(`container-tree.ts`) */
11491
11450
  const TRACKED_ROOT_NAMES = [
11492
11451
  ...WRITABLE_DIRS,
11452
+ ...READONLY_DIRS,
11493
11453
  ...WRITABLE_ROOT_FILES,
11494
11454
  ...READONLY_ROOT_FILES
11495
11455
  ];
@@ -11519,9 +11479,10 @@ function accessOf(path) {
11519
11479
  if (READONLY_ROOT_FILES.has(first)) return "read";
11520
11480
  return null;
11521
11481
  }
11522
- if (!WRITABLE_DIRS.has(first)) return null;
11523
- if (segments.some((s) => s.startsWith("."))) return null;
11524
- return "write";
11482
+ const readonlyDir = READONLY_DIRS.has(first);
11483
+ if (!readonlyDir && !WRITABLE_DIRS.has(first)) return null;
11484
+ if (segments.slice(1).some((s) => s.startsWith("."))) return null;
11485
+ return readonlyDir ? "read" : "write";
11525
11486
  }
11526
11487
 
11527
11488
  //#endregion
@@ -11625,6 +11586,116 @@ async function gunzip(bytes, maxBytes) {
11625
11586
  return out;
11626
11587
  }
11627
11588
 
11589
+ //#endregion
11590
+ //#region src/local-build.ts
11591
+ /** 실패할 때 보여 주는 빌드 출력 줄 수 */
11592
+ const OUTPUT_TAIL_LINES = 40;
11593
+ /** 모아 두는 출력 상한(문자) — 긴 빌드 로그를 다 쥐고 있지 않는다 */
11594
+ const OUTPUT_KEEP_CHARS$1 = 64 * 1024;
11595
+ /** 빌드를 돌릴 수 있는 폴더인가 — `build` 스크립트와 설치된 의존성(`node_modules`)이 있어야 한다 */
11596
+ function planLocalBuild(dir) {
11597
+ let scripts;
11598
+ try {
11599
+ scripts = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).scripts;
11600
+ } catch {
11601
+ return { kind: "no-script" };
11602
+ }
11603
+ const build = scripts && typeof scripts === "object" ? scripts.build : void 0;
11604
+ if (typeof build !== "string" || !build.trim()) return { kind: "no-script" };
11605
+ if (!existsSync(join(dir, "node_modules"))) return { kind: "no-deps" };
11606
+ return { kind: "run" };
11607
+ }
11608
+ /** `npm run build`를 돌린다. 출력은 모아 두고(실패할 때 꼬리를 보여 준다) 화면에 흘리지 않는다 */
11609
+ function runNpmBuild(dir, env) {
11610
+ return new Promise((resolve$1, reject) => {
11611
+ let output = "";
11612
+ const keep = (chunk) => {
11613
+ output = (output + chunk.toString("utf8")).slice(-OUTPUT_KEEP_CHARS$1);
11614
+ };
11615
+ const child = spawn("npm", ["run", "build"], {
11616
+ cwd: dir,
11617
+ env,
11618
+ stdio: [
11619
+ "ignore",
11620
+ "pipe",
11621
+ "pipe"
11622
+ ],
11623
+ shell: process.platform === "win32"
11624
+ });
11625
+ child.stdout?.on("data", keep);
11626
+ child.stderr?.on("data", keep);
11627
+ child.once("error", reject);
11628
+ child.once("close", (code) => resolve$1({
11629
+ code,
11630
+ output: tailOf(output)
11631
+ }));
11632
+ });
11633
+ }
11634
+ function tailOf(output, lines = OUTPUT_TAIL_LINES) {
11635
+ return output.trimEnd().split(/\r?\n/).slice(-lines).join("\n");
11636
+ }
11637
+
11638
+ //#endregion
11639
+ //#region src/skills-install.ts
11640
+ /**
11641
+ * sayren 중앙 스킬 설치(2026-10-16 사용자 결정) — `pull`·`init`이 받은 폴더에서 `npx skills add https://sayren.io`를 돌려
11642
+ * `.claude/skills/{name}/`에 스킬을 넣는다. 설치는 실패해도 받기를 실패시키지 않는다(명령을 알려 준다).
11643
+ * 설치된 스킬은 로컬 설치물이라 올리지 않는다(`installedSkillNames` → `collectTree`).
11644
+ */
11645
+ const SKILLS_SOURCE = "https://sayren.io";
11646
+ /** 확인한 skills CLI 버전 — 디스커버리 0.2.0 색인을 읽는다 */
11647
+ const SKILLS_CLI = "skills@1.7.2";
11648
+ const SKILLS_ARGS = [
11649
+ "-y",
11650
+ SKILLS_CLI,
11651
+ "add",
11652
+ SKILLS_SOURCE,
11653
+ "--agent",
11654
+ "claude-code",
11655
+ "-y"
11656
+ ];
11657
+ const SKILLS_COMMAND = `npx ${SKILLS_CLI} add ${SKILLS_SOURCE} --agent claude-code`;
11658
+ /** skills CLI가 설치한 스킬을 적는 파일 */
11659
+ const SKILLS_LOCK_FILE = "skills-lock.json";
11660
+ const OUTPUT_KEEP_CHARS = 64 * 1024;
11661
+ /** `npx skills add`를 돌린다. 출력은 모아 두고(실패할 때 꼬리를 보여 준다) 화면에 흘리지 않는다 */
11662
+ function runSkillsInstall(dir, env) {
11663
+ return new Promise((resolve$1, reject) => {
11664
+ let output = "";
11665
+ const keep = (chunk) => {
11666
+ output = (output + chunk.toString("utf8")).slice(-OUTPUT_KEEP_CHARS);
11667
+ };
11668
+ const child = spawn("npx", SKILLS_ARGS, {
11669
+ cwd: dir,
11670
+ env,
11671
+ stdio: [
11672
+ "ignore",
11673
+ "pipe",
11674
+ "pipe"
11675
+ ],
11676
+ shell: process.platform === "win32"
11677
+ });
11678
+ child.stdout?.on("data", keep);
11679
+ child.stderr?.on("data", keep);
11680
+ child.once("error", reject);
11681
+ child.once("close", (code) => resolve$1({
11682
+ code,
11683
+ output: tailOf(output, OUTPUT_TAIL_LINES)
11684
+ }));
11685
+ });
11686
+ }
11687
+ /** skills CLI로 설치한 스킬 이름(`skills-lock.json`) — 없거나 읽지 못하면 빈 집합 */
11688
+ function installedSkillNames(root) {
11689
+ const path = join(root, SKILLS_LOCK_FILE);
11690
+ if (!existsSync(path)) return /* @__PURE__ */ new Set();
11691
+ try {
11692
+ const skills = JSON.parse(readFileSync(path, "utf8")).skills;
11693
+ return new Set(skills && typeof skills === "object" ? Object.keys(skills) : []);
11694
+ } catch {
11695
+ return /* @__PURE__ */ new Set();
11696
+ }
11697
+ }
11698
+
11628
11699
  //#endregion
11629
11700
  //#region src/tree.ts
11630
11701
  /**
@@ -11658,6 +11729,11 @@ const sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
11658
11729
  function collectTree(root) {
11659
11730
  const files = [];
11660
11731
  const skipped = [];
11732
+ const installedSkills = installedSkillNames(root);
11733
+ const isInstalledSkill = (rel) => {
11734
+ const [first, second, name] = rel.split("/");
11735
+ return first === ".claude" && second === "skills" && name !== void 0 && installedSkills.has(name);
11736
+ };
11661
11737
  const walk = (dir) => {
11662
11738
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
11663
11739
  const full = join(dir, entry.name);
@@ -11671,7 +11747,7 @@ function collectTree(root) {
11671
11747
  skipped.push(rel);
11672
11748
  continue;
11673
11749
  }
11674
- if (isNeverUpload(rel) || accessOf(rel) === null) {
11750
+ if (isNeverUpload(rel) || accessOf(rel) === null || isInstalledSkill(rel)) {
11675
11751
  skipped.push(rel);
11676
11752
  continue;
11677
11753
  }
@@ -11871,52 +11947,21 @@ function lockImporterSpecifiers(lock) {
11871
11947
  }
11872
11948
 
11873
11949
  //#endregion
11874
- //#region src/local-build.ts
11875
- /** 실패할 때 보여 주는 빌드 출력 줄 수 */
11876
- const OUTPUT_TAIL_LINES = 40;
11877
- /** 모아 두는 출력 상한(문자) — 긴 빌드 로그를 다 쥐고 있지 않는다 */
11878
- const OUTPUT_KEEP_CHARS = 64 * 1024;
11879
- /** 빌드를 돌릴 수 있는 폴더인가 — `build` 스크립트와 설치된 의존성(`node_modules`)이 있어야 한다 */
11880
- function planLocalBuild(dir) {
11881
- let scripts;
11882
- try {
11883
- scripts = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).scripts;
11884
- } catch {
11885
- return { kind: "no-script" };
11886
- }
11887
- const build = scripts && typeof scripts === "object" ? scripts.build : void 0;
11888
- if (typeof build !== "string" || !build.trim()) return { kind: "no-script" };
11889
- if (!existsSync(join(dir, "node_modules"))) return { kind: "no-deps" };
11890
- return { kind: "run" };
11891
- }
11892
- /** `npm run build`를 돌린다. 출력은 모아 두고(실패할 때 꼬리를 보여 준다) 화면에 흘리지 않는다 */
11893
- function runNpmBuild(dir, env) {
11894
- return new Promise((resolve$1, reject) => {
11895
- let output = "";
11896
- const keep = (chunk) => {
11897
- output = (output + chunk.toString("utf8")).slice(-OUTPUT_KEEP_CHARS);
11898
- };
11899
- const child = spawn("npm", ["run", "build"], {
11900
- cwd: dir,
11901
- env,
11902
- stdio: [
11903
- "ignore",
11904
- "pipe",
11905
- "pipe"
11906
- ],
11907
- shell: process.platform === "win32"
11908
- });
11909
- child.stdout?.on("data", keep);
11910
- child.stderr?.on("data", keep);
11911
- child.once("error", reject);
11912
- child.once("close", (code) => resolve$1({
11913
- code,
11914
- output: tailOf(output)
11915
- }));
11916
- });
11917
- }
11918
- function tailOf(output, lines = OUTPUT_TAIL_LINES) {
11919
- return output.trimEnd().split(/\r?\n/).slice(-lines).join("\n");
11950
+ //#region src/guide-skill.ts
11951
+ /**
11952
+ * 받은 폴더의 에이전트 안내 스킬(2026-10-16 사용자 결정) — 셀러의 Claude Code가 sayren 중앙 스킬을 HTTP로 읽게 한다.
11953
+ * 빈 템플릿이 싣는 `templates/storefront-blank/.claude/skills/sayren/SKILL.md`와 같은 내용이다(`guide-skill.test.ts`가 고정한다).
11954
+ */
11955
+ const GUIDE_SKILL_PATH = ".claude/skills/sayren/SKILL.md";
11956
+ const GUIDE_SKILL = "---\nname: sayren\ndescription: Use for any work on this sayren storefront — pages, product display, cart, checkout, payments, buyer login, account pages, analytics, design and code quality. Loads the current sayren skills over HTTP.\n---\n\n# sayren skills (central)\n\nThe sayren skills are maintained centrally and change without updating this project. Read them over HTTP at the start of a task instead of relying on memory.\n\n1. List the skills: `GET https://api.sayren.io/v1/public/agent-skills`\n The response is `{ \"data\": [{ \"name\", \"description\", \"version\", \"updatedAt\" }] }`.\n2. Read the skills that match the task: `GET https://api.sayren.io/v1/public/agent-skills/{name}`\n The response is `{ \"data\": { \"name\", \"description\", \"body\", \"version\", \"updatedAt\" } }`; `body` is the guide (markdown).\n\nStart points:\n- Any code change: `code-quality`, `sayren-storefront-rules`, `sdk-client`, `template-stack`\n- Product pages: `sayren-product-display`, `sdk-catalog`\n- Cart, checkout, payment: `sdk-cart`, `sdk-checkout`, `sdk-payments`\n- Buyer login and account pages: `sdk-buyer-auth`, `sdk-account`\n- Visual design: `design-quality`\n\nFollow the project's `CLAUDE.md` first; the skills add the sayren-specific details.\n";
11957
+ /**
11958
+ * 받은 트리에 안내 스킬이 없으면 넣는다 — 템플릿이 싣거나 셀러가 올린 내용이 우선이다. 트리에 넣으므로 다시 받을 때 지울 파일로 잡히지 않고
11959
+ * `deploy`가 원격 작업 트리에도 올린다(AI 상점 도우미가 같은 안내를 읽는다). 넣었으면 true
11960
+ */
11961
+ function withGuideSkill(files) {
11962
+ if (files.has(GUIDE_SKILL_PATH)) return false;
11963
+ files.set(GUIDE_SKILL_PATH, new TextEncoder().encode(GUIDE_SKILL));
11964
+ return true;
11920
11965
  }
11921
11966
 
11922
11967
  //#endregion
@@ -11978,7 +12023,7 @@ async function pull(flags, io) {
11978
12023
  return receiveSource(client, auth, source, flags, io, authDir);
11979
12024
  }
11980
12025
  /**
11981
- * 받은 작업 트리를 폴더에 쓰고 받은 폴더 파일(`.sayren/state.json`·`.env`·`.mcp.json`·`.gitignore`)을 맞춘다. `pull`과 `init`(#101)이 같이 쓴다.
12026
+ * 받은 작업 트리(+ 에이전트 안내 스킬 `withGuideSkill`)를 폴더에 쓰고 받은 폴더 파일(`.sayren/state.json`·`.env`·`.mcp.json`·`.gitignore`)을 맞춘다. `pull`과 `init`(#101)이 같이 쓴다.
11982
12027
  * 대상은 `--dir` → 받은 폴더 안이면 그 폴더 → 상점 코드다. 비어 있지 않은 폴더는 `--force`일 때 같은 사이트를 받은 폴더만 다시 받는다.
11983
12028
  */
11984
12029
  async function receiveSource(client, auth, source, flags, io, migratedDir) {
@@ -12039,6 +12084,7 @@ async function receiveSource(client, auth, source, flags, io, migratedDir) {
12039
12084
  const tgz = new Uint8Array(Buffer.concat(chunks));
12040
12085
  if (sha256Hex(tgz) !== source.archive.sha256) throw new CliError(cli_pull_archive_corrupt());
12041
12086
  const files = await unpackArchive(tgz);
12087
+ withGuideSkill(files);
12042
12088
  const collisions = caseCollisions(files.keys());
12043
12089
  if (collisions.length > 0) throw new CliError(cli_pull_case_collisions({ groups: collisions.map((group) => group.join(" · ")).join(", ") }));
12044
12090
  const diff = diffLocal(target, files);
@@ -12106,10 +12152,33 @@ async function receiveSource(client, auth, source, flags, io, migratedDir) {
12106
12152
  file: STATE_FILE,
12107
12153
  siteId: source.siteId
12108
12154
  }));
12155
+ await installSkills(target, io);
12109
12156
  if (source.editSessionOpen) io.out(cli_pull_edit_session_open());
12110
12157
  io.out(cli_pull_next_steps({ dir: pulledDir }));
12111
12158
  return 0;
12112
12159
  }
12160
+ /** sayren 중앙 스킬을 받은 폴더에 설치한다 — 실패해도 받기는 성공이다(직접 돌릴 명령을 알린다) */
12161
+ async function installSkills(dir, io) {
12162
+ io.out(cli_skills_installing({ source: SKILLS_SOURCE }));
12163
+ let result;
12164
+ try {
12165
+ result = await (io.installSkills ?? ((at) => runSkillsInstall(at, io.env)))(dir);
12166
+ } catch (error) {
12167
+ io.err(cli_skills_install_failed({
12168
+ command: SKILLS_COMMAND,
12169
+ detail: error instanceof Error ? error.message : String(error)
12170
+ }));
12171
+ return;
12172
+ }
12173
+ if (result.code === 0) {
12174
+ io.out(cli_skills_installed());
12175
+ return;
12176
+ }
12177
+ io.err(cli_skills_install_failed({
12178
+ command: SKILLS_COMMAND,
12179
+ detail: result.output || `exit ${result.code}`
12180
+ }));
12181
+ }
12113
12182
  /**
12114
12183
  * `sayren init [폴더]`(#101) — 템플릿을 골라 호스팅 사이트를 만들고(`POST /v1/storefront/site`) `pull`과 같은 폴더를 만든다.
12115
12184
  * 사이트가 이미 있으면 만들지 않고 `pull`을 안내한다(상점당 열린 사이트는 하나다). 만들기는 상점 한도(30일 3회)를 쓰므로 확인을 묻는다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sayren",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "imports": {
6
6
  "#paraglide/*": "./src/paraglide/*"
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "esbuild": "^0.28.2",
20
- "@sayren/store-sdk": "^0.30.16"
20
+ "@sayren/store-sdk": "^0.33.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@biomejs/biome": "^2.5.14",
@@ -28,7 +28,7 @@
28
28
  "tsx": "^4.20.3",
29
29
  "typescript": "^6.0.3",
30
30
  "vitest": "^5.0.1",
31
- "@sayren/mcp": "0.24.0"
31
+ "@sayren/mcp": "0.26.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"