sayren 0.16.1 → 0.17.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 +400 -47
  2. package/package.json +4 -4
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import { createInterface } from "node:readline/promises";
5
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
- import { dirname, extname, join, relative, resolve, sep } from "node:path";
8
+ import { delimiter, dirname, extname, join, relative, resolve, sep } from "node:path";
9
9
  import { pathToFileURL } from "node:url";
10
10
  import { gzipSync } from "node:zlib";
11
11
  import { createHash, randomBytes } from "node:crypto";
@@ -6621,6 +6621,42 @@ const cli_edit_session_open = ((inputs = {}, options = {}) => {
6621
6621
  return ko_cli_edit_session_open(inputs);
6622
6622
  });
6623
6623
 
6624
+ //#endregion
6625
+ //#region src/paraglide/messages/cli_env_approval.js
6626
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6627
+ /** @typedef {{}} Cli_Env_ApprovalInputs */
6628
+ const ko_cli_env_approval = () => {
6629
+ return `env 변경은 승인 대기입니다. 상점 플랫폼에서 승인하면 반영됩니다`;
6630
+ };
6631
+ /**
6632
+
6633
+ * @param {Cli_Env_ApprovalInputs} inputs
6634
+ * @param {{ locale?: "ko" }} options
6635
+ * @returns {LocalizedString}
6636
+ */
6637
+ const cli_env_approval = ((inputs = {}, options = {}) => {
6638
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6639
+ return ko_cli_env_approval(inputs);
6640
+ });
6641
+
6642
+ //#endregion
6643
+ //#region src/paraglide/messages/cli_env_empty.js
6644
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6645
+ /** @typedef {{}} Cli_Env_EmptyInputs */
6646
+ const ko_cli_env_empty = () => {
6647
+ return `설정한 env가 없습니다. npx sayren env add <키> <값>으로 추가하십시오`;
6648
+ };
6649
+ /**
6650
+
6651
+ * @param {Cli_Env_EmptyInputs} inputs
6652
+ * @param {{ locale?: "ko" }} options
6653
+ * @returns {LocalizedString}
6654
+ */
6655
+ const cli_env_empty = ((inputs = {}, options = {}) => {
6656
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6657
+ return ko_cli_env_empty(inputs);
6658
+ });
6659
+
6624
6660
  //#endregion
6625
6661
  //#region src/paraglide/messages/cli_env_not_linked.js
6626
6662
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -6639,6 +6675,24 @@ const cli_env_not_linked = ((inputs, options = {}) => {
6639
6675
  return ko_cli_env_not_linked(inputs);
6640
6676
  });
6641
6677
 
6678
+ //#endregion
6679
+ //#region src/paraglide/messages/cli_env_pull_skipped.js
6680
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6681
+ /** @typedef {{ keys: NonNullable<unknown> }} Cli_Env_Pull_SkippedInputs */
6682
+ const ko_cli_env_pull_skipped = (i) => {
6683
+ return `.env 한 줄로 같은 값을 쓸 수 없어 건너뛴 env가 있습니다: ${i?.keys}. 로컬에서는 직접 넣으십시오`;
6684
+ };
6685
+ /**
6686
+
6687
+ * @param {Cli_Env_Pull_SkippedInputs} inputs
6688
+ * @param {{ locale?: "ko" }} options
6689
+ * @returns {LocalizedString}
6690
+ */
6691
+ const cli_env_pull_skipped = ((inputs, options = {}) => {
6692
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6693
+ return ko_cli_env_pull_skipped(inputs);
6694
+ });
6695
+
6642
6696
  //#endregion
6643
6697
  //#region src/paraglide/messages/cli_env_pulled.js
6644
6698
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -6657,6 +6711,78 @@ const cli_env_pulled = ((inputs, options = {}) => {
6657
6711
  return ko_cli_env_pulled(inputs);
6658
6712
  });
6659
6713
 
6714
+ //#endregion
6715
+ //#region src/paraglide/messages/cli_env_pulled_vars.js
6716
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6717
+ /** @typedef {{ file: NonNullable<unknown>, count: NonNullable<unknown> }} Cli_Env_Pulled_VarsInputs */
6718
+ const ko_cli_env_pulled_vars = (i) => {
6719
+ return `${i?.file}에 일반 env ${i?.count}개를 썼습니다(비밀 값은 배포된 사이트에만 들어가 받지 않습니다)`;
6720
+ };
6721
+ /**
6722
+
6723
+ * @param {Cli_Env_Pulled_VarsInputs} inputs
6724
+ * @param {{ locale?: "ko" }} options
6725
+ * @returns {LocalizedString}
6726
+ */
6727
+ const cli_env_pulled_vars = ((inputs, options = {}) => {
6728
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6729
+ return ko_cli_env_pulled_vars(inputs);
6730
+ });
6731
+
6732
+ //#endregion
6733
+ //#region src/paraglide/messages/cli_env_removed.js
6734
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6735
+ /** @typedef {{ key: NonNullable<unknown> }} Cli_Env_RemovedInputs */
6736
+ const ko_cli_env_removed = (i) => {
6737
+ return `env ${i?.key}을(를) 지웠습니다. 올라가 있는 버전에서도 곧바로 빠집니다`;
6738
+ };
6739
+ /**
6740
+
6741
+ * @param {Cli_Env_RemovedInputs} inputs
6742
+ * @param {{ locale?: "ko" }} options
6743
+ * @returns {LocalizedString}
6744
+ */
6745
+ const cli_env_removed = ((inputs, options = {}) => {
6746
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6747
+ return ko_cli_env_removed(inputs);
6748
+ });
6749
+
6750
+ //#endregion
6751
+ //#region src/paraglide/messages/cli_env_secret_mask.js
6752
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6753
+ /** @typedef {{}} Cli_Env_Secret_MaskInputs */
6754
+ const ko_cli_env_secret_mask = () => {
6755
+ return `(비밀 값)`;
6756
+ };
6757
+ /**
6758
+
6759
+ * @param {Cli_Env_Secret_MaskInputs} inputs
6760
+ * @param {{ locale?: "ko" }} options
6761
+ * @returns {LocalizedString}
6762
+ */
6763
+ const cli_env_secret_mask = ((inputs = {}, options = {}) => {
6764
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6765
+ return ko_cli_env_secret_mask(inputs);
6766
+ });
6767
+
6768
+ //#endregion
6769
+ //#region src/paraglide/messages/cli_env_set.js
6770
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6771
+ /** @typedef {{ key: NonNullable<unknown> }} Cli_Env_SetInputs */
6772
+ const ko_cli_env_set = (i) => {
6773
+ return `env ${i?.key}을(를) 설정했습니다. 올라가 있는 버전에 곧바로 반영됩니다`;
6774
+ };
6775
+ /**
6776
+
6777
+ * @param {Cli_Env_SetInputs} inputs
6778
+ * @param {{ locale?: "ko" }} options
6779
+ * @returns {LocalizedString}
6780
+ */
6781
+ const cli_env_set = ((inputs, options = {}) => {
6782
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6783
+ return ko_cli_env_set(inputs);
6784
+ });
6785
+
6660
6786
  //#endregion
6661
6787
  //#region src/paraglide/messages/cli_env_store_code_unknown.js
6662
6788
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -6680,7 +6806,7 @@ const cli_env_store_code_unknown = ((inputs = {}, options = {}) => {
6680
6806
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6681
6807
  /** @typedef {{}} Cli_Env_UsageInputs */
6682
6808
  const ko_cli_env_usage = () => {
6683
- return `사용법: npx sayren env pull [--dir <폴더>]`;
6809
+ return `사용법: npx sayren env pull [--dir <폴더>] · env ls · env add <키> <값> [--secret] · env rm <키>`;
6684
6810
  };
6685
6811
  /**
6686
6812
 
@@ -10070,7 +10196,7 @@ async function selectStore(auth, client, io, candidates, folderStore = null) {
10070
10196
 
10071
10197
  //#endregion
10072
10198
  //#region package.json
10073
- var version = "0.16.1";
10199
+ var version = "0.17.0";
10074
10200
 
10075
10201
  //#endregion
10076
10202
  //#region src/client.ts
@@ -11086,11 +11212,114 @@ async function appMain(positional, flags, io) {
11086
11212
  }
11087
11213
  }
11088
11214
 
11215
+ //#endregion
11216
+ //#region ../../apps/hosting-sandbox/src/project-config.ts
11217
+ /**
11218
+ * 프로젝트 설정 `sayren.json`(#298 P1). 셀러 계약은 `package.json` 스크립트와 이 파일뿐이다 — 플랫폼은 프레임워크를 감지하거나 빌드를 보정하지 않는다.
11219
+ * - `buildCommand`: 빌드 명령. 없으면 `package.json`의 `build` 스크립트, 그것도 없으면 빌드하지 않는다(순수 HTML)
11220
+ * - `output.static`: 정적 파일 폴더(작업 트리 기준). `.`은 루트
11221
+ * - `output.server`: Web 표준 fetch 핸들러(`export default { fetch(request, env) }`)를 내보내는 ES 모듈. 없으면 정적 전용 사이트다
11222
+ *
11223
+ * 파일이 없으면 #298 이전 사이트(옛 basic·시작 코드)의 경로를 기본값으로 쓴다. 파일이 있으면 적힌 그대로다(`output.server`를 빼면 정적 전용).
11224
+ * 런타임 이름(Workers·wrangler)은 이 계약에 넣지 않는다 — 런타임을 옮겨도(#333) 이 파일은 그대로 둔다.
11225
+ * Node API를 쓰지 않는다(Sandbox Worker·CLI가 같이 쓴다).
11226
+ */
11227
+ const PROJECT_CONFIG_FILE = "sayren.json";
11228
+ /** `sayren.json`이 없는 사이트의 기본값 — #298 이전 빌드가 쓰던 TanStack Start 출력 경로 */
11229
+ const LEGACY_PROJECT_CONFIG = {
11230
+ buildCommand: null,
11231
+ output: {
11232
+ static: "dist/client",
11233
+ server: "dist/server/server.js"
11234
+ }
11235
+ };
11236
+ const MAX_COMMAND_LENGTH = 1e3;
11237
+ const MAX_OUTPUT_PATH_LENGTH = 256;
11238
+ const OUTPUT_SEGMENT_RE = /^[A-Za-z0-9._@+-]+$/;
11239
+ /** 출력 경로 문제(없으면 null). 작업 트리 안의 상대 경로만 — 설치물·비밀 값 파일은 가리킬 수 없다 */
11240
+ function outputPathProblem(path, allowRoot) {
11241
+ if (path === ".") return allowRoot ? null : "루트(.)는 쓸 수 없습니다";
11242
+ if (path.length === 0 || path.length > MAX_OUTPUT_PATH_LENGTH) return "길이가 맞지 않습니다";
11243
+ if (path.startsWith("/")) return "작업 트리 기준 상대 경로여야 합니다";
11244
+ for (const seg of path.split("/")) {
11245
+ if (seg === "" || seg === "." || seg === "..") return "빈 조각·.·..은 쓸 수 없습니다";
11246
+ if (!OUTPUT_SEGMENT_RE.test(seg)) return "영문·숫자·._@+-만 쓸 수 있습니다";
11247
+ if (seg === "node_modules" || seg.startsWith(".env")) return "설치물·.env 파일은 가리킬 수 없습니다";
11248
+ }
11249
+ return null;
11250
+ }
11251
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
11252
+ /** 트리(경로 → 내용)에서 `sayren.json` 내용을 꺼낸다. 없으면 null, UTF-8이 아니면 JSON 오류로 보이게 빈 문자열 */
11253
+ function projectConfigOf(files) {
11254
+ const bytes = files.get(PROJECT_CONFIG_FILE);
11255
+ if (!bytes) return null;
11256
+ try {
11257
+ return new TextDecoder("utf-8", {
11258
+ fatal: true,
11259
+ ignoreBOM: false
11260
+ }).decode(bytes);
11261
+ } catch {
11262
+ return "";
11263
+ }
11264
+ }
11265
+ /** `sayren.json` 내용(없으면 null)을 읽는다. 모르는 키는 거절한다(오타가 조용히 기본값으로 빌드되지 않게) */
11266
+ function parseProjectConfig(content) {
11267
+ if (content === null) return {
11268
+ ok: true,
11269
+ config: LEGACY_PROJECT_CONFIG
11270
+ };
11271
+ const fail = (error) => ({
11272
+ ok: false,
11273
+ error: `${PROJECT_CONFIG_FILE}: ${error}`
11274
+ });
11275
+ let raw;
11276
+ try {
11277
+ raw = JSON.parse(content);
11278
+ } catch {
11279
+ return fail("JSON 형식이 아닙니다");
11280
+ }
11281
+ if (!isRecord(raw)) return fail("객체여야 합니다");
11282
+ const unknownKey = Object.keys(raw).find((k) => k !== "buildCommand" && k !== "output");
11283
+ if (unknownKey !== void 0) return fail(`모르는 키입니다: ${unknownKey}`);
11284
+ let buildCommand = null;
11285
+ if (raw.buildCommand !== void 0) {
11286
+ const cmd = raw.buildCommand;
11287
+ if (typeof cmd !== "string" || !cmd.trim()) return fail("buildCommand는 빈 문자열이 아니어야 합니다");
11288
+ if (cmd.length > MAX_COMMAND_LENGTH || cmd.includes("\0")) return fail(`buildCommand는 ${MAX_COMMAND_LENGTH}자 이하여야 합니다`);
11289
+ buildCommand = cmd;
11290
+ }
11291
+ const output = raw.output;
11292
+ if (!isRecord(output)) return fail("output.static이 필요합니다");
11293
+ const outKey = Object.keys(output).find((k) => k !== "static" && k !== "server");
11294
+ if (outKey !== void 0) return fail(`모르는 키입니다: output.${outKey}`);
11295
+ if (typeof output.static !== "string") return fail("output.static이 필요합니다");
11296
+ const staticProblem = outputPathProblem(output.static, true);
11297
+ if (staticProblem) return fail(`output.static — ${staticProblem}`);
11298
+ let server = null;
11299
+ if (output.server !== void 0) {
11300
+ if (typeof output.server !== "string") return fail("output.server는 문자열이어야 합니다");
11301
+ const serverProblem = outputPathProblem(output.server, false);
11302
+ if (serverProblem) return fail(`output.server — ${serverProblem}`);
11303
+ if (!/\.m?js$/.test(output.server)) return fail("output.server는 .js·.mjs 모듈이어야 합니다");
11304
+ server = output.server;
11305
+ }
11306
+ return {
11307
+ ok: true,
11308
+ config: {
11309
+ buildCommand,
11310
+ output: {
11311
+ static: output.static,
11312
+ server
11313
+ }
11314
+ }
11315
+ };
11316
+ }
11317
+
11089
11318
  //#endregion
11090
11319
  //#region ../mcp/src/rules.ts
11091
- /** 규칙의 심각도 — 없으면 `error` */
11320
+ /** 규칙의 심각도 — 없으면 `warning` */
11092
11321
  function severityOf(rule) {
11093
- return rule?.severity ?? "error";
11322
+ return rule?.severity ?? "warning";
11094
11323
  }
11095
11324
  /** 문서 사이트 — `doc` 경로를 절대 주소로 만들 때 쓴다 */
11096
11325
  const DOCS_BASE_URL = "https://docs.sayren.io";
@@ -11131,12 +11360,13 @@ const RULES = [
11131
11360
  }),
11132
11361
  defineRule({
11133
11362
  id: "no-module-token",
11363
+ severity: "error",
11134
11364
  title: mcp_rule_no_module_token_title,
11135
11365
  why: mcp_rule_no_module_token_why,
11136
11366
  fix: mcp_rule_no_module_token_fix,
11137
11367
  example: mcp_rule_no_module_token_example,
11138
11368
  doc: "/guides/storefront-auth",
11139
- templateFile: "src/lib/session.server.ts"
11369
+ templateFile: "src/lib/api.server.ts"
11140
11370
  }),
11141
11371
  defineRule({
11142
11372
  id: "loader-first",
@@ -11145,7 +11375,7 @@ const RULES = [
11145
11375
  fix: mcp_rule_loader_first_fix,
11146
11376
  example: mcp_rule_loader_first_example,
11147
11377
  doc: "/guides/mcp-customize",
11148
- templateFile: "src/routes/products.index.tsx"
11378
+ templateFile: "src/routes/index.tsx"
11149
11379
  }),
11150
11380
  defineRule({
11151
11381
  id: "payment-browser-launch",
@@ -11153,8 +11383,7 @@ const RULES = [
11153
11383
  why: mcp_rule_payment_browser_launch_why,
11154
11384
  fix: mcp_rule_payment_browser_launch_fix,
11155
11385
  example: mcp_rule_payment_browser_launch_example,
11156
- doc: "/guides/checkout-flow",
11157
- templateFile: "src/lib/payments.ts"
11386
+ doc: "/guides/checkout-flow"
11158
11387
  }),
11159
11388
  defineRule({
11160
11389
  id: "payment-return-route",
@@ -11162,8 +11391,7 @@ const RULES = [
11162
11391
  why: mcp_rule_payment_return_route_why,
11163
11392
  fix: mcp_rule_payment_return_route_fix,
11164
11393
  example: "useEffect(() => {\n void payments.result().then((result) => {\n if (result.status === \"COMPLETED\") navigate({ to: \"/checkout/complete\", search: { orderId: result.orderId } });\n });\n}, []);",
11165
- doc: "/guides/checkout-flow",
11166
- templateFile: "src/routes/checkout.return.tsx"
11394
+ doc: "/guides/checkout-flow"
11167
11395
  }),
11168
11396
  defineRule({
11169
11397
  id: "payment-processing-status",
@@ -11171,8 +11399,7 @@ const RULES = [
11171
11399
  why: mcp_rule_payment_processing_status_why,
11172
11400
  fix: mcp_rule_payment_processing_status_fix,
11173
11401
  example: mcp_rule_payment_processing_status_example,
11174
- doc: "/guides/checkout-flow",
11175
- templateFile: "src/routes/checkout.return.tsx"
11402
+ doc: "/guides/checkout-flow"
11176
11403
  }),
11177
11404
  defineRule({
11178
11405
  id: "payment-amounts-final",
@@ -11180,8 +11407,7 @@ const RULES = [
11180
11407
  why: mcp_rule_payment_amounts_final_why,
11181
11408
  fix: mcp_rule_payment_amounts_final_fix,
11182
11409
  example: mcp_rule_payment_amounts_final_example,
11183
- doc: "/guides/shipping-fees",
11184
- templateFile: "src/routes/checkout.index.tsx"
11410
+ doc: "/guides/shipping-fees"
11185
11411
  }),
11186
11412
  defineRule({
11187
11413
  id: "payment-retry",
@@ -11189,8 +11415,7 @@ const RULES = [
11189
11415
  why: mcp_rule_payment_retry_why,
11190
11416
  fix: mcp_rule_payment_retry_fix,
11191
11417
  example: mcp_rule_payment_retry_example,
11192
- doc: "/guides/checkout-flow",
11193
- templateFile: "src/routes/checkout.return.tsx"
11418
+ doc: "/guides/checkout-flow"
11194
11419
  }),
11195
11420
  defineRule({
11196
11421
  id: "checkout-origin",
@@ -11198,8 +11423,7 @@ const RULES = [
11198
11423
  why: mcp_rule_checkout_origin_why,
11199
11424
  fix: mcp_rule_checkout_origin_fix,
11200
11425
  example: "const returnUrl = new URL(PAYMENT_RETURN_PATH, new URL(getRequestUrl()).origin);\nawait api.checkout.startPayment(checkoutId, { option, returnUrl: returnUrl.toString() });",
11201
- doc: "/guides/payment-settings",
11202
- templateFile: "src/routes/checkout.index.tsx"
11426
+ doc: "/guides/payment-settings"
11203
11427
  }),
11204
11428
  defineRule({
11205
11429
  id: "delivery-quote-preview",
@@ -11207,11 +11431,11 @@ const RULES = [
11207
11431
  why: mcp_rule_delivery_quote_preview_why,
11208
11432
  fix: mcp_rule_delivery_quote_preview_fix,
11209
11433
  example: mcp_rule_delivery_quote_preview_example,
11210
- doc: "/guides/shipping-fees",
11211
- templateFile: "src/routes/checkout.index.tsx"
11434
+ doc: "/guides/shipping-fees"
11212
11435
  }),
11213
11436
  defineRule({
11214
11437
  id: "buyer-token-cookie",
11438
+ severity: "error",
11215
11439
  title: mcp_rule_buyer_token_cookie_title,
11216
11440
  why: mcp_rule_buyer_token_cookie_why,
11217
11441
  fix: mcp_rule_buyer_token_cookie_fix,
@@ -11225,8 +11449,7 @@ const RULES = [
11225
11449
  why: mcp_rule_buyer_session_refresh_why,
11226
11450
  fix: mcp_rule_buyer_session_refresh_fix,
11227
11451
  example: mcp_rule_buyer_session_refresh_example,
11228
- doc: "/guides/storefront-auth",
11229
- templateFile: "src/lib/session.server.ts"
11452
+ doc: "/guides/storefront-auth"
11230
11453
  }),
11231
11454
  defineRule({
11232
11455
  id: "buyer-reconsent",
@@ -11234,8 +11457,7 @@ const RULES = [
11234
11457
  why: mcp_rule_buyer_reconsent_why,
11235
11458
  fix: mcp_rule_buyer_reconsent_fix,
11236
11459
  example: mcp_rule_buyer_reconsent_example,
11237
- doc: "/guides/storefront-auth",
11238
- templateFile: "src/routes/account.tsx"
11460
+ doc: "/guides/storefront-auth"
11239
11461
  }),
11240
11462
  defineRule({
11241
11463
  id: "buyer-signup",
@@ -11243,8 +11465,7 @@ const RULES = [
11243
11465
  why: mcp_rule_buyer_signup_why,
11244
11466
  fix: mcp_rule_buyer_signup_fix,
11245
11467
  example: mcp_rule_buyer_signup_example,
11246
- doc: "/guides/storefront-auth",
11247
- templateFile: "src/routes/signup.tsx"
11468
+ doc: "/guides/storefront-auth"
11248
11469
  }),
11249
11470
  defineRule({
11250
11471
  id: "buyer-signout",
@@ -11252,8 +11473,7 @@ const RULES = [
11252
11473
  why: mcp_rule_buyer_signout_why,
11253
11474
  fix: mcp_rule_buyer_signout_fix,
11254
11475
  example: "const logout = createServerFn({ method: \"POST\" }).handler(async () => {\n const accessToken = readToken();\n if (accessToken) await authFor().signOut(accessToken).catch(() => undefined);\n clearSessionCookie();\n});",
11255
- doc: "/guides/storefront-auth",
11256
- templateFile: "src/routes/account.tsx"
11476
+ doc: "/guides/storefront-auth"
11257
11477
  }),
11258
11478
  defineRule({
11259
11479
  id: "analytics-start",
@@ -11275,12 +11495,12 @@ const RULES = [
11275
11495
  }),
11276
11496
  defineRule({
11277
11497
  id: "custom-input-private",
11498
+ severity: "error",
11278
11499
  title: mcp_rule_custom_input_private_title,
11279
11500
  why: mcp_rule_custom_input_private_why,
11280
11501
  fix: mcp_rule_custom_input_private_fix,
11281
11502
  example: mcp_rule_custom_input_private_example,
11282
- doc: "/guides/product-options",
11283
- templateFile: "src/lib/direct-checkout.server.ts"
11503
+ doc: "/guides/product-options"
11284
11504
  }),
11285
11505
  defineRule({
11286
11506
  id: "checkout-availability",
@@ -11288,9 +11508,7 @@ const RULES = [
11288
11508
  why: mcp_rule_checkout_availability_why,
11289
11509
  fix: mcp_rule_checkout_availability_fix,
11290
11510
  example: mcp_rule_checkout_availability_example,
11291
- doc: "/guides/storefront-sdk",
11292
- templateFile: "src/lib/ordering.ts",
11293
- severity: "warning"
11511
+ doc: "/guides/storefront-sdk"
11294
11512
  }),
11295
11513
  defineRule({
11296
11514
  id: "product-link",
@@ -11298,9 +11516,7 @@ const RULES = [
11298
11516
  why: mcp_rule_product_link_why,
11299
11517
  fix: mcp_rule_product_link_fix,
11300
11518
  example: mcp_rule_product_link_example,
11301
- doc: "/guides/order-numbers",
11302
- templateFile: "src/lib/product-path.ts",
11303
- severity: "warning"
11519
+ doc: "/guides/order-numbers"
11304
11520
  }),
11305
11521
  defineRule({
11306
11522
  id: "server-only-import",
@@ -11368,7 +11584,7 @@ function verifySources(files, options = {}) {
11368
11584
  detail
11369
11585
  });
11370
11586
  };
11371
- /** 프로젝트 전체를 보고 판정한 위반 — 고칠 자리는 규칙의 `templateFile`이 알려 준다 */
11587
+ /** 프로젝트 전체를 보고 판정한 위반 — 고칠 방법은 규칙의 `fix`, 시작 코드에 있는 자리는 `templateFile`이 알려 준다 */
11372
11588
  const inProject = (ruleId, detail) => {
11373
11589
  findings.push({
11374
11590
  ruleId,
@@ -12095,6 +12311,8 @@ function runLocalChecks(tree) {
12095
12311
  const report$1 = verifyTree(map);
12096
12312
  const blockingRules = report$1.results.filter((result) => result.status === "violation" && result.severity !== "warning");
12097
12313
  const problems = [...limitProblems(tree)];
12314
+ const project = parseProjectConfig(projectConfigOf(map));
12315
+ if (!project.ok) problems.push(project.error);
12098
12316
  const warnings = [];
12099
12317
  const text = (path) => {
12100
12318
  const bytes = map.get(path);
@@ -12201,8 +12419,20 @@ function withGuideSkill(files) {
12201
12419
  const OUTPUT_TAIL_LINES = 40;
12202
12420
  /** 모아 두는 출력 상한(문자) — 긴 빌드 로그를 다 쥐고 있지 않는다 */
12203
12421
  const OUTPUT_KEEP_CHARS = 64 * 1024;
12204
- /** 빌드를 돌릴 수 있는 폴더인가 — `build` 스크립트와 설치된 의존성(`node_modules`)이 있어야 한다 */
12422
+ /**
12423
+ * 빌드를 돌릴 수 있는 폴더인가 — 서버 빌드와 같은 순서로 명령을 고른다(`sayren.json` buildCommand → `build` 스크립트).
12424
+ * `package.json`이 있으면 설치된 의존성(`node_modules`)이 있어야 한다
12425
+ */
12205
12426
  function planLocalBuild(dir) {
12427
+ const hasManifest = existsSync(join(dir, "package.json"));
12428
+ const command = buildCommandOf(dir);
12429
+ if (command !== null) {
12430
+ if (hasManifest && !existsSync(join(dir, "node_modules"))) return { kind: "no-deps" };
12431
+ return {
12432
+ kind: "run",
12433
+ command
12434
+ };
12435
+ }
12206
12436
  let scripts;
12207
12437
  try {
12208
12438
  scripts = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).scripts;
@@ -12212,16 +12442,33 @@ function planLocalBuild(dir) {
12212
12442
  const build = scripts && typeof scripts === "object" ? scripts.build : void 0;
12213
12443
  if (typeof build !== "string" || !build.trim()) return { kind: "no-script" };
12214
12444
  if (!existsSync(join(dir, "node_modules"))) return { kind: "no-deps" };
12215
- return { kind: "run" };
12445
+ return {
12446
+ kind: "run",
12447
+ command: null
12448
+ };
12216
12449
  }
12217
- /** `npm run build`를 돌린다. 출력은 모아 두고(실패할 때 꼬리를 보여 준다) 화면에 흘리지 않는다 */
12218
- function runNpmBuild(dir, env) {
12450
+ /** `sayren.json`의 buildCommand. 파일이 없거나 형식이 틀리면 null(형식 오류는 로컬 검사가 먼저 막는다) */
12451
+ function buildCommandOf(dir) {
12452
+ let content;
12453
+ try {
12454
+ content = readFileSync(join(dir, PROJECT_CONFIG_FILE), "utf8");
12455
+ } catch {
12456
+ return null;
12457
+ }
12458
+ const parsed = parseProjectConfig(content);
12459
+ return parsed.ok ? parsed.config.buildCommand : null;
12460
+ }
12461
+ /**
12462
+ * 빌드를 돌린다 — `command`가 없으면 `npm run build`, 있으면 그 명령을 셸로(셀러가 자기 컴퓨터에서 적은 명령이다, `node_modules/.bin`을 PATH에 얹는다).
12463
+ * 출력은 모아 두고(실패할 때 꼬리를 보여 준다) 화면에 흘리지 않는다
12464
+ */
12465
+ function runNpmBuild(dir, env, command = null) {
12219
12466
  return new Promise((resolve$1, reject) => {
12220
12467
  let output = "";
12221
12468
  const keep = (chunk) => {
12222
12469
  output = (output + chunk.toString("utf8")).slice(-OUTPUT_KEEP_CHARS);
12223
12470
  };
12224
- const child = spawn("npm", ["run", "build"], {
12471
+ const child = command === null ? spawn("npm", ["run", "build"], {
12225
12472
  cwd: dir,
12226
12473
  env,
12227
12474
  stdio: [
@@ -12230,6 +12477,18 @@ function runNpmBuild(dir, env) {
12230
12477
  "pipe"
12231
12478
  ],
12232
12479
  shell: process.platform === "win32"
12480
+ }) : spawn(command, {
12481
+ cwd: dir,
12482
+ env: {
12483
+ ...env,
12484
+ PATH: [join(dir, "node_modules", ".bin"), env.PATH].filter(Boolean).join(delimiter)
12485
+ },
12486
+ stdio: [
12487
+ "ignore",
12488
+ "pipe",
12489
+ "pipe"
12490
+ ],
12491
+ shell: true
12233
12492
  });
12234
12493
  child.stdout?.on("data", keep);
12235
12494
  child.stderr?.on("data", keep);
@@ -12602,14 +12861,96 @@ async function envPull(flags, io) {
12602
12861
  store: state.storeId
12603
12862
  }, dir, state, io);
12604
12863
  const storeCode = await storeCodeOf(client, state.storeId);
12864
+ const lines = (await client.storefront.listVars().then((r) => r.vars.filter((v) => !v.secret)).catch((error) => {
12865
+ if (error instanceof ApiError && (error.status === 403 || error.status === 404)) return [];
12866
+ throw error;
12867
+ })).map((v) => ({
12868
+ key: v.key,
12869
+ line: dotenvValue(v.value ?? "")
12870
+ }));
12871
+ const skipped = lines.filter((l) => l.line === null).map((l) => l.key);
12605
12872
  writeStoreEnv(dir, auth.apiOrigin, storeCode);
12873
+ updateEnvFile(dir, Object.fromEntries(lines.filter((l) => l.line !== null).map((l) => [l.key, l.line])));
12606
12874
  ensureGitignore(dir);
12607
12875
  io.out(cli_env_pulled({
12608
12876
  file: ENV_FILE,
12609
12877
  storeCode
12610
12878
  }));
12879
+ if (lines.length > skipped.length) io.out(cli_env_pulled_vars({
12880
+ file: ENV_FILE,
12881
+ count: lines.length - skipped.length
12882
+ }));
12883
+ if (skipped.length > 0) io.err(cli_env_pull_skipped({ keys: skipped.join(", ") }));
12884
+ return 0;
12885
+ }
12886
+ /**
12887
+ * `.env` 한 줄의 값(dotenv + Vite `loadEnv`의 dotenv-expand 규칙) — `$`는 `\$`로 막아 확장되지 않게 한다. 안전한 문자만이면 그대로,
12888
+ * 작은따옴표·줄바꿈이 없으면 작은따옴표(안의 문자를 그대로 읽는다), 줄바꿈만 있고 큰따옴표가 없으면 큰따옴표(`\n`). 작은따옴표와 줄바꿈이
12889
+ * 같이 있거나 큰따옴표와 줄바꿈이 같이 있으면 같은 값으로 읽히게 쓸 수 없어 null이다(쓰지 않고 알린다)
12890
+ */
12891
+ function dotenvValue(value) {
12892
+ const escaped = value.replaceAll("$", "\\$");
12893
+ if (/^[A-Za-z0-9_./:@+,=-]*$/.test(value)) return value;
12894
+ if (!value.includes("'") && !/[\r\n]/.test(value)) return `'${escaped}'`;
12895
+ if (!value.includes("\"") && !value.includes("\\")) return `"${escaped.replace(/\r?\n/g, "\\n")}"`;
12896
+ return null;
12897
+ }
12898
+ /** 연결된 폴더의 관리 API 클라이언트 — 셀러 env 명령(`env ls·add·rm`) */
12899
+ async function linkedClient(flags, io) {
12900
+ const dir = resolve(io.cwd, flags.dir ?? ".");
12901
+ migrateLegacy(dir, io.out);
12902
+ const state = readState(dir);
12903
+ if (!state) throw new CliError(cli_env_not_linked({ file: STATE_FILE }));
12904
+ return (await apiFor({
12905
+ ...flags,
12906
+ store: state.storeId
12907
+ }, dir, state, io)).client;
12908
+ }
12909
+ /** `sayren env ls` — 셀러 env 목록(비밀 값은 값 없이) */
12910
+ async function envList(flags, io) {
12911
+ const { vars } = await (await linkedClient(flags, io)).storefront.listVars();
12912
+ if (vars.length === 0) {
12913
+ io.out(cli_env_empty());
12914
+ return 0;
12915
+ }
12916
+ for (const v of vars) io.out(`${v.key}=${v.secret ? cli_env_secret_mask() : v.value ?? ""}`);
12611
12917
  return 0;
12612
12918
  }
12919
+ /** `sayren env add <키> <값> [--secret]` — 넣거나 바꾼다. 에이전트 토큰이면 승인 대기다 */
12920
+ async function envAdd(flags, io) {
12921
+ if (!flags.key || flags.value === void 0) throw new CliError(cli_env_usage());
12922
+ const client = await linkedClient(flags, io);
12923
+ try {
12924
+ await client.storefront.putVar(flags.key, {
12925
+ value: flags.value,
12926
+ secret: flags.secret === true
12927
+ });
12928
+ } catch (error) {
12929
+ if (error instanceof ApprovalPendingError) return envApprovalPending(error, io);
12930
+ throw error;
12931
+ }
12932
+ io.out(cli_env_set({ key: flags.key }));
12933
+ return 0;
12934
+ }
12935
+ /** `sayren env rm <키>` */
12936
+ async function envRemove(flags, io) {
12937
+ if (!flags.key) throw new CliError(cli_env_usage());
12938
+ const client = await linkedClient(flags, io);
12939
+ try {
12940
+ await client.storefront.deleteVar(flags.key);
12941
+ } catch (error) {
12942
+ if (error instanceof ApprovalPendingError) return envApprovalPending(error, io);
12943
+ throw error;
12944
+ }
12945
+ io.out(cli_env_removed({ key: flags.key }));
12946
+ return 0;
12947
+ }
12948
+ function envApprovalPending(error, io) {
12949
+ io.out(cli_env_approval());
12950
+ if (error.approval.approvalUrl) io.out(cli_approval_url({ url: error.approval.approvalUrl }));
12951
+ else io.out(cli_approval_id({ id: error.approvalId }));
12952
+ return EXIT_APPROVAL_PENDING;
12953
+ }
12613
12954
  async function verify(flags, io) {
12614
12955
  const dir = resolve(io.cwd, flags.dir ?? ".");
12615
12956
  const tree = collectTree(dir);
@@ -12775,7 +13116,7 @@ async function localBuild(dir, skip, io) {
12775
13116
  const started = io.now?.() ?? Date.now();
12776
13117
  let result;
12777
13118
  try {
12778
- result = await (io.runBuild ?? ((at) => runNpmBuild(at, io.env)))(dir);
13119
+ result = await (io.runBuild ?? ((at, command) => runNpmBuild(at, io.env, command)))(dir, plan.command);
12779
13120
  } catch (error) {
12780
13121
  throw new CliError(cli_deploy_build_local_spawn_failed({ message: error instanceof Error ? error.message : String(error) }));
12781
13122
  }
@@ -13471,12 +13812,24 @@ async function main(argv, io) {
13471
13812
  template: str(parsed.flags.template)
13472
13813
  }, io);
13473
13814
  case "link": return link(common, io);
13474
- case "env":
13475
- if (parsed.positional[0] !== "pull") throw new CliError(cli_env_usage());
13476
- return envPull({
13815
+ case "env": switch (parsed.positional[0]) {
13816
+ case "pull": return envPull({
13477
13817
  ...common,
13478
13818
  dir: str(parsed.flags.dir) ?? parsed.positional[1]
13479
13819
  }, io);
13820
+ case "ls": return envList(common, io);
13821
+ case "add": return envAdd({
13822
+ ...common,
13823
+ key: parsed.positional[1],
13824
+ value: parsed.positional[2],
13825
+ secret: parsed.flags.secret === true
13826
+ }, io);
13827
+ case "rm": return envRemove({
13828
+ ...common,
13829
+ key: parsed.positional[1]
13830
+ }, io);
13831
+ default: throw new CliError(cli_env_usage());
13832
+ }
13480
13833
  case "init": return init();
13481
13834
  case "pull": return pull({
13482
13835
  ...common,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sayren",
3
- "version": "0.16.1",
3
+ "version": "0.17.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.58.0"
20
+ "@sayren/store-sdk": "^0.59.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@biomejs/biome": "^2.5.14",
@@ -28,8 +28,8 @@
28
28
  "tsx": "^4.20.3",
29
29
  "typescript": "^6.0.3",
30
30
  "vitest": "^5.0.1",
31
- "@sayren/mcp": "0.30.0",
32
- "@sayren/app": "0.9.0"
31
+ "@sayren/app": "0.9.0",
32
+ "@sayren/mcp": "0.31.0"
33
33
  },
34
34
  "publishConfig": {
35
35
  "access": "public"