sayren 0.3.0 → 0.3.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 +67 -67
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
10
|
|
|
11
11
|
//#region package.json
|
|
12
|
-
var version = "0.3.
|
|
12
|
+
var version = "0.3.1";
|
|
13
13
|
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/client.ts
|
|
@@ -21,8 +21,8 @@ var version = "0.3.0";
|
|
|
21
21
|
const CLI_VERSION = version;
|
|
22
22
|
const CLI_USER_AGENT = `sayren-cli/${CLI_VERSION}`;
|
|
23
23
|
/**
|
|
24
|
-
* 계정 토큰(#71)을 Bearer로, 고른
|
|
25
|
-
* 한 번 부른 뒤 다시 보낸다.
|
|
24
|
+
* 계정 토큰(#71)을 Bearer로, 고른 상점(`auth.storeId`)을 `X-Store-Id`로 싣는다. 401이면 `refresh`(자격 증명 파일의 refresh 회전)를
|
|
25
|
+
* 한 번 부른 뒤 다시 보낸다. 토큰·상점은 함수로 읽어 갱신·선택된 값을 따라간다.
|
|
26
26
|
*/
|
|
27
27
|
function clientOf(auth, fetchImpl = fetch, refresh) {
|
|
28
28
|
return createStoreClient({
|
|
@@ -57,7 +57,7 @@ function detailsOf(error) {
|
|
|
57
57
|
* 계약의 원천은 비공개 패키지 `@sayren/contracts`(`accounts.ts`)다. CLI·`@sayren/mcp`는 npm에 게시되므로 그 패키지에 의존하지 않고
|
|
58
58
|
* 값을 옮겨 두었다 — `account-token.test.ts`가 계약과 대조한다.
|
|
59
59
|
*
|
|
60
|
-
* 여기의 디코딩은 **서명을 보지 않는다**. 모양(헤더 `typ`)으로 경로를 고르고
|
|
60
|
+
* 여기의 디코딩은 **서명을 보지 않는다**. 모양(헤더 `typ`)으로 경로를 고르고 상점 범위를 읽는 데만 쓴다. 판정은 api(매 요청 서명·폐기
|
|
61
61
|
* 확인)와 게이트웨이(`apps/mcp-gateway/src/auth.ts`, JWKS 서명 확인)가 한다.
|
|
62
62
|
*/
|
|
63
63
|
/** JWT access token 헤더 `typ`(RFC 9068 §2.1) — 계약 `ACCOUNT_ACCESS_TOKEN_TYPE` */
|
|
@@ -68,7 +68,7 @@ const PROD_API_ORIGIN = "https://api.sayren.app";
|
|
|
68
68
|
const DEV_API_ORIGIN = "http://localhost:4001";
|
|
69
69
|
/** CLI의 accounts 클라이언트(public, PKCE, 루프백 임의 포트, 디바이스 코드) — accounts `token-config.ts` `CLI_CLIENT_ID` */
|
|
70
70
|
const CLI_CLIENT_ID = "sayren-cli";
|
|
71
|
-
/** CLI 로그인 스코프 —
|
|
71
|
+
/** CLI 로그인 스코프 — 상점 목록·상점 정보 조회·스토어프론트(pull·deploy) */
|
|
72
72
|
const CLI_LOGIN_SCOPE = "openid email offline_access stores:read store:r storefront:rw";
|
|
73
73
|
/** accounts 엔드포인트(Better Auth `basePath` `/api/auth`) */
|
|
74
74
|
function accountsEndpoints(accountsOrigin) {
|
|
@@ -125,24 +125,24 @@ function isAccountToken(token) {
|
|
|
125
125
|
return accountTokenClaimsOf(token) !== null;
|
|
126
126
|
}
|
|
127
127
|
/**
|
|
128
|
-
* 폐기된 옛
|
|
128
|
+
* 폐기된 옛 상점 API 토큰(`sy_pat_…`) — #71 R1 뒤 api가 받지 않는다(401 `TOKEN_TYPE_RETIRED`). 받는 쪽은 api에 보내지 않고
|
|
129
129
|
* 계정 액세스 토큰을 만들라고 바로 안내한다.
|
|
130
130
|
*/
|
|
131
131
|
function isRetiredStorePat(token) {
|
|
132
132
|
return token.startsWith("sy_pat_");
|
|
133
133
|
}
|
|
134
|
-
/** 토큰의
|
|
134
|
+
/** 토큰의 상점 범위가 상점 하나면 그 id(헤더 없이 쓸 수 있다). 아니면 null */
|
|
135
135
|
function singleStoreOf(claims) {
|
|
136
136
|
const stores = claims?.sayren_stores;
|
|
137
137
|
return Array.isArray(stores) && stores.length === 1 && typeof stores[0] === "string" ? stores[0] : null;
|
|
138
138
|
}
|
|
139
|
-
/** 이
|
|
139
|
+
/** 이 상점이 토큰의 범위 안인가 */
|
|
140
140
|
function storeInScope(claims, storeId) {
|
|
141
141
|
const stores = claims?.sayren_stores;
|
|
142
142
|
if (stores === "*") return true;
|
|
143
143
|
return Array.isArray(stores) && stores.includes(storeId);
|
|
144
144
|
}
|
|
145
|
-
/** 토큰 발급 시 선택한
|
|
145
|
+
/** 토큰 발급 시 선택한 상점 id 형식(관리 API `X-Store-Id`) */
|
|
146
146
|
const STORE_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
147
147
|
|
|
148
148
|
//#endregion
|
|
@@ -157,13 +157,13 @@ const STORE_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
|
157
157
|
* 스스로 OAuth(accounts)를 한다. 계정 토큰은 폴더가 아니라 사람에게 속한다(CLI는 사용자 설정 폴더에 둔다).
|
|
158
158
|
* - 폐기된 옛 API 토큰(`sy_pat_…`)이 든 `Authorization` 헤더는 쓸 때 지운다(R1 뒤 게이트웨이가 거절한다). 셀러가 직접 넣은 다른
|
|
159
159
|
* `Authorization`(계정 액세스 토큰)은 그대로 둔다.
|
|
160
|
-
* - 다른 서버 항목과 `sayren` 항목의 다른 헤더는 그대로 둔다. `sayren` 항목의 `type`·`url
|
|
160
|
+
* - 다른 서버 항목과 `sayren` 항목의 다른 헤더는 그대로 둔다. `sayren` 항목의 `type`·`url`·상점 헤더만 바꾼다.
|
|
161
161
|
*/
|
|
162
162
|
const MCP_JSON_FILE = ".mcp.json";
|
|
163
163
|
const MCP_SERVER_NAME = "sayren";
|
|
164
164
|
/** 원격 MCP 주소. 게이트웨이는 운영 api 하나만 바라본다 — 다른 api 주소를 쓰는 폴더도 이 주소를 쓴다 */
|
|
165
165
|
const REMOTE_MCP_URL = "https://mcp.sayren.app/mcp";
|
|
166
|
-
/** 관리 API의 테넌트 헤더와 같은 이름이다(게이트웨이·api가 토큰의
|
|
166
|
+
/** 관리 API의 테넌트 헤더와 같은 이름이다(게이트웨이·api가 토큰의 상점 범위와 대조한다) */
|
|
167
167
|
const STORE_ID_HEADER = "X-Store-Id";
|
|
168
168
|
var McpJsonError = class extends Error {
|
|
169
169
|
constructor(message) {
|
|
@@ -227,7 +227,7 @@ function mergeMcpJson(existing, entry) {
|
|
|
227
227
|
removedRetiredToken
|
|
228
228
|
};
|
|
229
229
|
}
|
|
230
|
-
/** `sayren` 항목의
|
|
230
|
+
/** `sayren` 항목의 상점(`X-Store-Id`). 항목·헤더가 없으면 null이다. 형식이 틀린 파일은 `McpJsonError` */
|
|
231
231
|
function sayrenStoreIdOf(text) {
|
|
232
232
|
const root = parse(text);
|
|
233
233
|
const entry = (isObject(root.mcpServers) ? root.mcpServers : {})[MCP_SERVER_NAME];
|
|
@@ -360,7 +360,7 @@ function readMcpText(dir) {
|
|
|
360
360
|
const path = join(dir, MCP_FILE);
|
|
361
361
|
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
362
362
|
}
|
|
363
|
-
/** `.mcp.json`의 `sayren`
|
|
363
|
+
/** `.mcp.json`의 `sayren` 상점(`X-Store-Id`). 파일·항목이 없으면 null이다 */
|
|
364
364
|
function readMcpStoreId(dir) {
|
|
365
365
|
const text = readMcpText(dir);
|
|
366
366
|
if (text === null) return null;
|
|
@@ -953,7 +953,7 @@ async function revokeRefresh(io, entry) {
|
|
|
953
953
|
//#endregion
|
|
954
954
|
//#region src/account-auth.ts
|
|
955
955
|
/**
|
|
956
|
-
* 명령이 토큰을 쓰기 전 준비(#71) — 자격 증명 파일 토큰의 갱신, 계정 토큰의
|
|
956
|
+
* 명령이 토큰을 쓰기 전 준비(#71) — 자격 증명 파일 토큰의 갱신, 계정 토큰의 상점 고르기.
|
|
957
957
|
*/
|
|
958
958
|
/** 자격 증명 파일에서 토큰을 읽는 함수(`resolveAuth`에 넘긴다) */
|
|
959
959
|
function credentialTokenReader(env) {
|
|
@@ -975,8 +975,8 @@ async function readyAuth(auth, io) {
|
|
|
975
975
|
};
|
|
976
976
|
}
|
|
977
977
|
/**
|
|
978
|
-
* 계정 토큰의
|
|
979
|
-
* 토큰의
|
|
978
|
+
* 계정 토큰의 상점(`X-Store-Id`) — 후보(`--store`·`--project` 등 명령이 준 순서) → `SAYREN_STORE_ID` → 폴더의 상태·`.mcp.json` →
|
|
979
|
+
* 토큰의 상점이 하나면 그 상점 → `GET /v1/me/stores`(하나면 그 상점, 여럿이면 대화형 선택, 비대화형이면 오류).
|
|
980
980
|
*/
|
|
981
981
|
async function selectStore(auth, client, io, candidates, folderStore = null) {
|
|
982
982
|
const claims = accountTokenClaimsOf(auth.token);
|
|
@@ -986,8 +986,8 @@ async function selectStore(auth, client, io, candidates, folderStore = null) {
|
|
|
986
986
|
folderStore
|
|
987
987
|
].find((value) => !!value?.trim());
|
|
988
988
|
if (explicit) {
|
|
989
|
-
if (!STORE_ID_PATTERN.test(explicit)) throw new CliError(
|
|
990
|
-
if (!storeInScope(claims, explicit)) throw new CliError(
|
|
989
|
+
if (!STORE_ID_PATTERN.test(explicit)) throw new CliError(`상점 id 형식이 아닙니다: ${explicit}`);
|
|
990
|
+
if (!storeInScope(claims, explicit)) throw new CliError(`상점 ${explicit}가 이 토큰의 상점 범위 밖입니다. 그 상점이 포함되게 다시 로그인하거나(npx sayren login) 다른 토큰을 쓰십시오`);
|
|
991
991
|
auth.storeId = explicit;
|
|
992
992
|
return explicit;
|
|
993
993
|
}
|
|
@@ -1000,21 +1000,21 @@ async function selectStore(auth, client, io, candidates, folderStore = null) {
|
|
|
1000
1000
|
try {
|
|
1001
1001
|
stores = (await client.auth.listMyStores()).filter((store) => storeInScope(claims, store.storeId));
|
|
1002
1002
|
} catch {
|
|
1003
|
-
throw new CliError("
|
|
1003
|
+
throw new CliError("상점을 고르지 못했습니다. --store <storeId> 또는 SAYREN_STORE_ID로 상점을 주십시오");
|
|
1004
1004
|
}
|
|
1005
|
-
if (stores.length === 0) throw new CliError("이 계정(토큰 범위)에 쓸 수 있는
|
|
1005
|
+
if (stores.length === 0) throw new CliError("이 계정(토큰 범위)에 쓸 수 있는 상점이 없습니다");
|
|
1006
1006
|
if (stores.length === 1) {
|
|
1007
1007
|
auth.storeId = stores[0].storeId;
|
|
1008
1008
|
return auth.storeId;
|
|
1009
1009
|
}
|
|
1010
1010
|
const labels = stores.map((store) => `${store.storeId} — ${store.storeName} (${store.role})`);
|
|
1011
|
-
const picked = io.select ? await io.select("
|
|
1011
|
+
const picked = io.select ? await io.select("상점을 고르십시오", labels) : null;
|
|
1012
1012
|
const index = picked ? labels.indexOf(picked) : -1;
|
|
1013
|
-
if (index < 0) throw new CliError(
|
|
1013
|
+
if (index < 0) throw new CliError(`상점이 여럿입니다. --store <storeId> 또는 SAYREN_STORE_ID로 고르십시오:\n${labels.map((label) => ` ${label}`).join("\n")}`);
|
|
1014
1014
|
auth.storeId = stores[index].storeId;
|
|
1015
1015
|
return auth.storeId;
|
|
1016
1016
|
}
|
|
1017
|
-
/** 계정 로그인 폴더의 `.mcp.json` 안내 — 토큰 없이
|
|
1017
|
+
/** 계정 로그인 폴더의 `.mcp.json` 안내 — 토큰 없이 주소·상점만 쓴다 */
|
|
1018
1018
|
const ACCOUNT_MCP_NOTE = `${MCP_FILE}에는 토큰을 쓰지 않았습니다 — MCP 클라이언트가 연결할 때 Sayren 계정으로 로그인합니다`;
|
|
1019
1019
|
|
|
1020
1020
|
//#endregion
|
|
@@ -1039,7 +1039,7 @@ const RULES = [
|
|
|
1039
1039
|
{
|
|
1040
1040
|
id: "tenant-header",
|
|
1041
1041
|
title: "테넌트는 `X-Store-Code` 헤더로 보낸다",
|
|
1042
|
-
why: "공유 API 호스트에서
|
|
1042
|
+
why: "공유 API 호스트에서 상점을 가르는 값이다. 빠지면 다른 상점 데이터가 보이거나 404가 난다",
|
|
1043
1043
|
fix: "`createStorefrontClient`에 `storeCode`를 넘긴다. SDK가 헤더를 붙인다. 서브도메인으로 서비스할 때만 생략한다",
|
|
1044
1044
|
example: "createStorefrontClient({ baseUrl: API_BASE_URL, storeCode: resolveStoreCode() });",
|
|
1045
1045
|
doc: "/guides/storefront-sdk",
|
|
@@ -1120,7 +1120,7 @@ const RULES = [
|
|
|
1120
1120
|
{
|
|
1121
1121
|
id: "checkout-origin",
|
|
1122
1122
|
title: "복귀 주소는 코드에 고정하지 않고 배포된 주소(요청 origin)에서 만든다",
|
|
1123
|
-
why: "주소를 코드에 고정하면 로컬·스테이징·운영 중 한 곳에서만 결제가 제대로 돌아온다.
|
|
1123
|
+
why: "주소를 코드에 고정하면 로컬·스테이징·운영 중 한 곳에서만 결제가 제대로 돌아온다. 상점 플랫폼 설정 › 결제의 결제 도메인은 선택형 허용 목록이다. 비워 두면 https 복귀 주소를 모두 받고, 등록하면 목록 밖의 복귀 주소를 서버가 거절한다",
|
|
1124
1124
|
fix: "서버 함수에서 `getRequestUrl()`의 origin으로 복귀 주소를 만든다. 결제 도메인을 등록해 두었다면 배포 도메인도 목록에 넣는다(테스트 결제는 localhost가 항상 허용된다)",
|
|
1125
1125
|
example: "const returnUrl = new URL(PAYMENT_RETURN_PATH, new URL(getRequestUrl()).origin);\nawait api.checkout.startPayment(checkoutId, { option, returnUrl: returnUrl.toString() });",
|
|
1126
1126
|
doc: "/guides/payment-settings",
|
|
@@ -1165,8 +1165,8 @@ const RULES = [
|
|
|
1165
1165
|
{
|
|
1166
1166
|
id: "buyer-signup",
|
|
1167
1167
|
title: "이메일·비밀번호 로그인을 받으면 가입 화면도 둔다(`auth.signUp`)",
|
|
1168
|
-
why: "비밀번호 로그인만 있고 가입이 없으면 소셜 로그인을 켜지 않은
|
|
1169
|
-
fix: "
|
|
1168
|
+
why: "비밀번호 로그인만 있고 가입이 없으면 소셜 로그인을 켜지 않은 상점에서 새 구매자가 계정을 만들 수 없다",
|
|
1169
|
+
fix: "상점 `loginMethods`에 `password`가 있으면 가입 화면을 둔다. 약관·개인정보 수집이용 동의(필수)와 마케팅 수신(선택)을 받아 서버 함수에서 `auth.signUp({ email, password, name, agreements, cartToken })`을 부르고, 받은 토큰 쌍을 HttpOnly 쿠키에 담는다",
|
|
1170
1170
|
example: "const tokens = await authFor().signUp({\n email, password, name,\n agreements: { terms: true, privacy: true, marketing },\n cartToken: readCartToken() ?? undefined, // 비회원 장바구니를 합친다\n});\nsignIn(tokens);",
|
|
1171
1171
|
doc: "/guides/storefront-auth",
|
|
1172
1172
|
templateFile: "src/routes/signup.tsx"
|
|
@@ -1183,7 +1183,7 @@ const RULES = [
|
|
|
1183
1183
|
{
|
|
1184
1184
|
id: "analytics-start",
|
|
1185
1185
|
title: "방문 분석은 브라우저에서 한 번 시작한다(`@sayren/storefront-sdk/analytics`)",
|
|
1186
|
-
why: "빠지면
|
|
1186
|
+
why: "빠지면 상점 플랫폼 애널리틱스가 비어 있다. 서버 파일에서 부르면 아무것도 하지 않는다",
|
|
1187
1187
|
fix: "루트 컴포넌트의 effect에서 `createAnalytics`를 한 번 부르고(설정은 loader가 내려준다), 화면은 그 인스턴스의 `track`으로 행동을 남긴다. 장바구니·결제 시작·구매는 서버가 기록하므로 보내지 않는다",
|
|
1188
1188
|
example: "import { createAnalytics } from \"@sayren/storefront-sdk/analytics\";\n\nuseEffect(() => {\n startAnalytics({ apiBaseUrl, storeCode, debug }); // 브라우저에서 한 번\n}, []);",
|
|
1189
1189
|
doc: "/guides/storefront-analytics",
|
|
@@ -1209,8 +1209,8 @@ const RULES = [
|
|
|
1209
1209
|
},
|
|
1210
1210
|
{
|
|
1211
1211
|
id: "checkout-availability",
|
|
1212
|
-
title: "주문을 받지 않는
|
|
1213
|
-
why: "결제 수단이 없거나 공개한 쇼핑몰의 실결제가 아직 열리지 않은
|
|
1212
|
+
title: "주문을 받지 않는 상점이면 주문 버튼을 그리지 않는다(`checkoutAvailable`)",
|
|
1213
|
+
why: "결제 수단이 없거나 공개한 쇼핑몰의 실결제가 아직 열리지 않은 상점은 결제 요청이 409 `PAYMENT_NOT_CONFIGURED`다. 버튼을 그대로 두면 구매자가 담고 주문서까지 가서야 결제가 안 된다는 것을 안다",
|
|
1214
1214
|
fix: "`GET /store`(`api.store.get()`)의 `checkoutAvailable`이 false면 장바구니 담기·바로구매·주문 버튼과 헤더의 장바구니를 숨기고 「지금은 온라인 주문을 받지 않습니다」를 보인다. `customerCenterPhone`이 있으면 문의 전화를 함께 보인다",
|
|
1215
1215
|
example: "const store = await apiFor().store.get();\nif (!store.checkoutAvailable) {\n // 주문 버튼 대신 안내\n return <p>지금은 온라인 주문을 받지 않습니다</p>;\n}",
|
|
1216
1216
|
doc: "/guides/storefront-sdk",
|
|
@@ -1236,14 +1236,14 @@ const SERVER_FILE = /\.server\.(ts|tsx)$/;
|
|
|
1236
1236
|
/** 토큰을 브라우저 저장소에 쓰는 코드 */
|
|
1237
1237
|
const BROWSER_TOKEN_STORAGE = /(localStorage|sessionStorage)\.setItem\(\s*[`'"][^`'"]*([Tt]oken|refresh|session|sayren_member)/;
|
|
1238
1238
|
/**
|
|
1239
|
-
* 직접 입력값이 새는 자리 — URL 검색 파라미터(`search`·`searchParams`), 브라우저 저장소,
|
|
1239
|
+
* 직접 입력값이 새는 자리 — URL 검색 파라미터(`search`·`searchParams`), 브라우저 저장소, 상점 플랫폼.
|
|
1240
1240
|
* 같은 식 안에서 `customInputs`가 보일 때만 잡는다.
|
|
1241
1241
|
*/
|
|
1242
1242
|
const CUSTOM_INPUT_LEAKS = [
|
|
1243
1243
|
[/search\s*[:=]\s*\{\{?[^}]*\bcustomInputs\b/, "URL 검색 파라미터"],
|
|
1244
1244
|
[/searchParams\.(set|append)\([^)]*\bcustomInputs\b/, "URL 검색 파라미터"],
|
|
1245
1245
|
[/(localStorage|sessionStorage)\.setItem\([^)]*\bcustomInputs\b/, "브라우저 저장소"],
|
|
1246
|
-
[/console\.(log|info|warn|error|debug)\([^)]*\bcustomInputs\b/, "
|
|
1246
|
+
[/console\.(log|info|warn|error|debug)\([^)]*\bcustomInputs\b/, "상점 플랫폼 로그"]
|
|
1247
1247
|
];
|
|
1248
1248
|
/**
|
|
1249
1249
|
* 만들어진 프로젝트의 소스를 규칙과 대조한다. 정적 검사라 모든 문제를 잡지는 못하지만,
|
|
@@ -1317,7 +1317,7 @@ function verifySources(files) {
|
|
|
1317
1317
|
if (!matches(/\b(auth|authFor\(\))\.signOut\(|\.signOut\(\s*accessToken\b/)) inProject("buyer-signout", "로그아웃에서 리프레시 토큰을 폐기하는 코드가 없다(`auth.signOut(accessToken)`)");
|
|
1318
1318
|
if (!has("reconsentRequired")) inProject("buyer-reconsent", "재동의가 필요한 구매자를 처리하는 코드가 없다(`reconsentRequired`)");
|
|
1319
1319
|
}
|
|
1320
|
-
if (has("startPayment(") && !has("checkoutAvailable")) inProject("checkout-availability", "결제를 시작하지만 `GET /store`의 `checkoutAvailable`을 읽는 코드가 없다. 주문을 받지 않는
|
|
1320
|
+
if (has("startPayment(") && !has("checkoutAvailable")) inProject("checkout-availability", "결제를 시작하지만 `GET /store`의 `checkoutAvailable`을 읽는 코드가 없다. 주문을 받지 않는 상점에서도 주문 버튼이 보인다");
|
|
1321
1321
|
if (has("createStorefrontClient(") && !has("@sayren/storefront-sdk/analytics")) inProject("analytics-start", "방문 분석을 시작하는 코드가 없다. `@sayren/storefront-sdk/analytics`의 createAnalytics를 브라우저에서 한 번 부른다");
|
|
1322
1322
|
const clientQuery = matches(/\buse(Suspense)?Query\(/);
|
|
1323
1323
|
if (clientQuery && !has("ensureQueryData(") && !has("prefetchQuery(")) inFile("loader-first", clientQuery, "TanStack Query로 데이터를 받지만 loader에서 `queryClient.ensureQueryData`로 미리 받는 코드가 없다. 서버 렌더에 데이터가 빠진다", /\buse(Suspense)?Query\(/);
|
|
@@ -1348,7 +1348,7 @@ function report(finding) {
|
|
|
1348
1348
|
/** 위반을 고치는 순서 — 에이전트가 무엇부터 할지 묻지 않게 한다 */
|
|
1349
1349
|
function nextSteps(findings, target) {
|
|
1350
1350
|
if (!findings.length) return target === "remote" ? [
|
|
1351
|
-
"사용자에게
|
|
1351
|
+
"사용자에게 상점 플랫폼 스토어프론트 공간의 [미리보기 열기]로 화면을 확인해 달라고 전한다.",
|
|
1352
1352
|
"`create_version`으로 버전을 만들고 `get_build`로 빌드 결과를 확인한다.",
|
|
1353
1353
|
"정적 검사가 잡지 못하는 것은 실제 결제 한 번이 잡는다. 발행 전에 테스트 결제를 권한다."
|
|
1354
1354
|
] : [
|
|
@@ -1858,7 +1858,7 @@ const MAX_BLOB_ROUNDS = 3;
|
|
|
1858
1858
|
/** 올리기 한 번의 내용 합계 상한 — base64(×4/3)가 api 본문 8MiB 안에 든다 */
|
|
1859
1859
|
const BLOB_BATCH_BYTES = 5 * 1024 * 1024;
|
|
1860
1860
|
/**
|
|
1861
|
-
* 토큰·api 주소를 정하고(자격 증명 파일 포함) 준비한 관리 API 클라이언트 —
|
|
1861
|
+
* 토큰·api 주소를 정하고(자격 증명 파일 포함) 준비한 관리 API 클라이언트 — 상점을 고른다(`candidates` 먼저).
|
|
1862
1862
|
*/
|
|
1863
1863
|
async function apiFor(flags, dir, state, io, candidates = []) {
|
|
1864
1864
|
const auth = resolveAuth(flags, dir, state, io.env, credentialTokenReader(io.env));
|
|
@@ -1895,10 +1895,10 @@ async function pull(flags, io) {
|
|
|
1895
1895
|
migrateLegacy(authDir, io.out);
|
|
1896
1896
|
const { auth, client } = await apiFor(flags, authDir, null, io, [flags.project]);
|
|
1897
1897
|
const hosting = await client.storefront.get();
|
|
1898
|
-
if (!hosting.site) throw new CliError("이
|
|
1898
|
+
if (!hosting.site) throw new CliError("이 상점에는 호스팅 사이트가 없습니다. 상점 플랫폼 › 스토어프론트에서 사이트를 만든 뒤 다시 받으십시오");
|
|
1899
1899
|
if (!hosting.editingEnabled) throw new CliError("지금은 스토어프론트 소스를 받을 수 없습니다(편집 기능이 꺼져 있습니다)");
|
|
1900
1900
|
const source = await client.storefront.getSource();
|
|
1901
|
-
if (flags.project && flags.project !== source.storeId) throw new CliError(`받은
|
|
1901
|
+
if (flags.project && flags.project !== source.storeId) throw new CliError(`받은 상점(${source.storeId})과 --project(${flags.project})가 다릅니다. --project 또는 SAYREN_STORE_ID를 확인하십시오`);
|
|
1902
1902
|
const store = {
|
|
1903
1903
|
storeId: source.storeId,
|
|
1904
1904
|
storeCode: source.storeCode
|
|
@@ -1941,7 +1941,7 @@ async function pull(flags, io) {
|
|
|
1941
1941
|
if (sha256Hex(tgz) !== source.archive.sha256) throw new CliError("받은 압축 파일이 손상됐습니다. 다시 시도하십시오");
|
|
1942
1942
|
const files = await unpackArchive(tgz);
|
|
1943
1943
|
const collisions = caseCollisions(files.keys());
|
|
1944
|
-
if (collisions.length > 0) throw new CliError(`대소문자만 다른 경로가 있어 받지 않았습니다(대소문자를 가리지 않는 파일 시스템에서 겹칩니다): ${collisions.map((group) => group.join(" · ")).join(", ")}.
|
|
1944
|
+
if (collisions.length > 0) throw new CliError(`대소문자만 다른 경로가 있어 받지 않았습니다(대소문자를 가리지 않는 파일 시스템에서 겹칩니다): ${collisions.map((group) => group.join(" · ")).join(", ")}. 상점 플랫폼·AI 편집으로 한쪽 이름을 바꾼 뒤 다시 받으십시오`);
|
|
1945
1945
|
const diff = diffLocal(target, files);
|
|
1946
1946
|
if (existing.length > 0) {
|
|
1947
1947
|
const watched = diff.overwrite.filter((path) => path === "package.json" || path === "vite.config.ts");
|
|
@@ -1978,12 +1978,12 @@ async function pull(flags, io) {
|
|
|
1978
1978
|
const written = writeMcpJson(target, { storeId: store.storeId });
|
|
1979
1979
|
io.out(`받음 → ${relativeOf(io, target)} (${files.size}개${removed ? `, 원격에 없는 ${removed}개 지움` : ""})`);
|
|
1980
1980
|
if (written === "retired-token-removed") io.out(`알림: ${RETIRED_MCP_TOKEN_NOTICE}`);
|
|
1981
|
-
io.out(`${MCP_FILE} 작성 — sayren 원격 MCP(${REMOTE_MCP_URL})와
|
|
1982
|
-
if (auth.apiOrigin !== DEFAULT_API_ORIGIN) io.out(`알림: 원격 MCP는 운영 api(${DEFAULT_API_ORIGIN})에만 연결됩니다. ${auth.apiOrigin}의
|
|
1983
|
-
io.out(`${ENV_FILE} 작성 — 스토어프론트 API
|
|
1981
|
+
io.out(`${MCP_FILE} 작성 — sayren 원격 MCP(${REMOTE_MCP_URL})와 상점. ${ACCOUNT_MCP_NOTE}`);
|
|
1982
|
+
if (auth.apiOrigin !== DEFAULT_API_ORIGIN) io.out(`알림: 원격 MCP는 운영 api(${DEFAULT_API_ORIGIN})에만 연결됩니다. ${auth.apiOrigin}의 상점으로는 ${MCP_FILE}의 연결이 거부됩니다`);
|
|
1983
|
+
io.out(`${ENV_FILE} 작성 — 스토어프론트 API 주소·상점 코드(개발 서버용)`);
|
|
1984
1984
|
io.out(`${STATE_FILE} 작성 — 사이트 ${source.siteId}·받은 작업 트리${addedIgnore.length ? ` (.gitignore에 ${addedIgnore.join(" ")} 추가)` : ""}`);
|
|
1985
|
-
if (source.editSessionOpen) io.out("알림:
|
|
1986
|
-
io.out(`\n다음:\n cd ${relativeOf(io, target)}\n npm install --ignore-scripts # 의존성의 설치 스크립트를 돌리지 않습니다\n npm run dev # http://localhost:4010 — 운영
|
|
1985
|
+
if (source.editSessionOpen) io.out("알림: 상점 플랫폼·AI 편집 환경이 열려 있습니다. 배포 전에 그 편집이 끝났는지 확인하십시오");
|
|
1986
|
+
io.out(`\n다음:\n cd ${relativeOf(io, target)}\n npm install --ignore-scripts # 의존성의 설치 스크립트를 돌리지 않습니다\n npm run dev # http://localhost:4010 — 운영 상점 데이터로 동작합니다\n npx sayren deploy # 빌드 → 버전 → 발행`);
|
|
1987
1987
|
return 0;
|
|
1988
1988
|
}
|
|
1989
1989
|
async function verify(flags, io) {
|
|
@@ -2028,10 +2028,10 @@ async function deploy(flags, io) {
|
|
|
2028
2028
|
printChecks(tree, checks, io);
|
|
2029
2029
|
if (checks.blockingRules.length > 0 || checks.problems.length > 0) throw new CliError("로컬 검사를 통과하지 못해 배포하지 않았습니다. 위 항목을 고친 뒤 다시 시도하십시오");
|
|
2030
2030
|
const site = (await client.storefront.get()).site;
|
|
2031
|
-
if (!site || site.siteId !== rc.siteId) throw new CliError(`이 폴더의 사이트(${rc.siteId})가
|
|
2031
|
+
if (!site || site.siteId !== rc.siteId) throw new CliError(`이 폴더의 사이트(${rc.siteId})가 상점의 지금 사이트와 다릅니다. npx sayren pull로 다시 받으십시오`);
|
|
2032
2032
|
if (flags.force && !flags.yes) {
|
|
2033
2033
|
if (!io.confirm) throw new CliError("--force는 원격 작업 트리를 덮어씁니다. 확인할 수 없는 환경이면 --yes를 함께 주십시오");
|
|
2034
|
-
if (!await io.confirm("원격 작업 트리(
|
|
2034
|
+
if (!await io.confirm("원격 작업 트리(상점 플랫폼·AI 편집 포함)를 이 폴더 내용으로 덮어쓰고, 열린 편집 환경을 끕니다. 계속하시겠습니까?")) {
|
|
2035
2035
|
io.out("취소했습니다");
|
|
2036
2036
|
return 1;
|
|
2037
2037
|
}
|
|
@@ -2043,7 +2043,7 @@ async function deploy(flags, io) {
|
|
|
2043
2043
|
if (error instanceof ApprovalPendingError) {
|
|
2044
2044
|
io.out("작업 트리 바꾸기는 승인이 필요합니다(에이전트 토큰이 package.json·vite.config.ts를 바꿉니다). 승인된 뒤 npx sayren deploy를 다시 돌리면 빌드·발행합니다");
|
|
2045
2045
|
if (error.approval.approvalUrl) io.out(`승인 요청: ${error.approval.approvalUrl}`);
|
|
2046
|
-
else io.out(`승인 요청 id: ${error.approvalId} (
|
|
2046
|
+
else io.out(`승인 요청 id: ${error.approvalId} (상점 플랫폼 › 설정 › 보안 › 승인 요청)`);
|
|
2047
2047
|
return EXIT_APPROVAL_PENDING;
|
|
2048
2048
|
}
|
|
2049
2049
|
throw error;
|
|
@@ -2061,7 +2061,7 @@ async function deploy(flags, io) {
|
|
|
2061
2061
|
});
|
|
2062
2062
|
} catch (error) {
|
|
2063
2063
|
const code = codeOf(error);
|
|
2064
|
-
if (code === "STOREFRONT_BUILD_IN_PROGRESS") throw new CliError("이 사이트에 끝나지 않은 빌드가 있습니다(
|
|
2064
|
+
if (code === "STOREFRONT_BUILD_IN_PROGRESS") throw new CliError("이 사이트에 끝나지 않은 빌드가 있습니다(상점 플랫폼·AI 또는 다른 배포). 작업 트리는 올렸습니다 — npx sayren status로 확인한 뒤 다시 배포하십시오");
|
|
2065
2065
|
if (code === "SOURCE_CHANGED") throw new CliError("올린 직후 다른 편집이 끼어 버전을 만들지 않았습니다. npx sayren pull --force로 받아 합친 뒤 다시 배포하십시오");
|
|
2066
2066
|
throw error;
|
|
2067
2067
|
}
|
|
@@ -2073,7 +2073,7 @@ async function deploy(flags, io) {
|
|
|
2073
2073
|
}
|
|
2074
2074
|
io.out(`빌드 성공 — v${created.version.number}${build.durationMs ? ` (${Math.round(build.durationMs / 1e3)}초)` : ""}`);
|
|
2075
2075
|
if (flags.publish === false) {
|
|
2076
|
-
io.out(`발행하지 않았습니다(--no-publish).
|
|
2076
|
+
io.out(`발행하지 않았습니다(--no-publish). 상점 플랫폼 › 스토어프론트 › 버전에서 v${created.version.number}를 발행할 수 있습니다`);
|
|
2077
2077
|
return 0;
|
|
2078
2078
|
}
|
|
2079
2079
|
try {
|
|
@@ -2085,10 +2085,10 @@ async function deploy(flags, io) {
|
|
|
2085
2085
|
if (error instanceof ApprovalPendingError) {
|
|
2086
2086
|
io.out(`발행은 승인이 필요합니다(에이전트 토큰). 셀러가 승인하면 v${created.version.number}가 발행됩니다`);
|
|
2087
2087
|
if (error.approval.approvalUrl) io.out(`승인 요청: ${error.approval.approvalUrl}`);
|
|
2088
|
-
else io.out(`승인 요청 id: ${error.approvalId} (
|
|
2088
|
+
else io.out(`승인 요청 id: ${error.approvalId} (상점 플랫폼 › 설정 › 보안 › 승인 요청)`);
|
|
2089
2089
|
return EXIT_APPROVAL_PENDING;
|
|
2090
2090
|
}
|
|
2091
|
-
if (codeOf(error) === "STOREFRONT_SITE_CHANGED") throw new CliError(`다른 사람이 그 사이 다른 버전을 발행했습니다. v${created.version.number}는 만들어 뒀습니다 —
|
|
2091
|
+
if (codeOf(error) === "STOREFRONT_SITE_CHANGED") throw new CliError(`다른 사람이 그 사이 다른 버전을 발행했습니다. v${created.version.number}는 만들어 뒀습니다 — 상점 플랫폼에서 확인한 뒤 발행하십시오`);
|
|
2092
2092
|
throw error;
|
|
2093
2093
|
}
|
|
2094
2094
|
}
|
|
@@ -2139,9 +2139,9 @@ async function replaceSource(client, tree, rc, force, io) {
|
|
|
2139
2139
|
dependencyWarnings: []
|
|
2140
2140
|
};
|
|
2141
2141
|
}
|
|
2142
|
-
throw new CliError("원격 작업 트리가 pull 이후 바뀌었습니다(
|
|
2142
|
+
throw new CliError("원격 작업 트리가 pull 이후 바뀌었습니다(상점 플랫폼·AI 편집). 덮어쓰지 않았습니다.\n · 원격을 받으려면: npx sayren pull --force (로컬 변경은 먼저 커밋·백업하십시오)\n · 이 폴더 내용으로 덮어쓰려면: npx sayren deploy --force");
|
|
2143
2143
|
}
|
|
2144
|
-
if (code === "EDIT_SESSION_OPEN") throw new CliError("
|
|
2144
|
+
if (code === "EDIT_SESSION_OPEN") throw new CliError("상점 플랫폼·AI 편집 환경이 열려 있어 올리지 않았습니다. 편집을 끝낸 뒤 다시 시도하거나 --force로 편집 환경을 끄고 진행하십시오");
|
|
2145
2145
|
if (code === "DEPENDENCY_REJECTED") throw new CliError(`쓸 수 없는 npm 의존성이 있어 올리지 않았습니다.\n${(Array.isArray(details.rejected) ? details.rejected : []).map((item) => ` · ${findingLine(item)}`).join("\n")}`);
|
|
2146
2146
|
if (code === "DEPENDENCY_CHECK_UNAVAILABLE") throw new CliError("의존성 확인을 지금 할 수 없습니다. 잠시 뒤 다시 시도하십시오");
|
|
2147
2147
|
if (code === "SOURCE_TOO_MANY_NEW_FILES") throw new CliError(`한 번에 올릴 수 있는 새 파일은 ${String(details.max ?? 800)}개까지입니다. 파일을 나눠 여러 번 배포하십시오(--no-publish로 중간 배포)`);
|
|
@@ -2297,7 +2297,7 @@ async function pasteAccountToken(flags, token, io) {
|
|
|
2297
2297
|
await writeAccountMcpJson(dir, state, io);
|
|
2298
2298
|
return 0;
|
|
2299
2299
|
}
|
|
2300
|
-
/** 쓸 수 있는
|
|
2300
|
+
/** 쓸 수 있는 상점 수 — api가 계정 토큰을 아직 받지 않거나 `stores:read`가 없으면 알리기만 한다 */
|
|
2301
2301
|
async function reportStores(apiOrigin, token, io) {
|
|
2302
2302
|
const auth = {
|
|
2303
2303
|
token,
|
|
@@ -2309,16 +2309,16 @@ async function reportStores(apiOrigin, token, io) {
|
|
|
2309
2309
|
const stores = await clientOf(auth, io.fetch).auth.listMyStores();
|
|
2310
2310
|
const claims = accountTokenClaimsOf(token);
|
|
2311
2311
|
const scoped = claims?.sayren_stores === "*" || !Array.isArray(claims?.sayren_stores) ? stores : stores.filter((store) => claims.sayren_stores.includes(store.storeId));
|
|
2312
|
-
io.out(`쓸 수 있는
|
|
2312
|
+
io.out(`쓸 수 있는 상점 ${scoped.length}개`);
|
|
2313
2313
|
} catch {
|
|
2314
|
-
io.err("알림:
|
|
2314
|
+
io.err("알림: 상점 목록을 불러오지 못했습니다(권한 stores:read 또는 api 연결을 확인하십시오)");
|
|
2315
2315
|
}
|
|
2316
2316
|
}
|
|
2317
|
-
/** 받은 폴더면 `.mcp.json`을 토큰 없는 항목(
|
|
2317
|
+
/** 받은 폴더면 `.mcp.json`을 토큰 없는 항목(주소·상점)으로 맞춘다. 폐기된 옛 토큰 헤더는 지우고 알린다 */
|
|
2318
2318
|
async function writeAccountMcpJson(dir, state, io) {
|
|
2319
2319
|
if (!state || !isPulledDir(dir)) return;
|
|
2320
2320
|
if (writeMcpJson(dir, { storeId: state.storeId }) === "retired-token-removed") io.err(`알림: ${RETIRED_MCP_TOKEN_NOTICE}`);
|
|
2321
|
-
io.out(`${relativeOf(io, join(dir, MCP_FILE))} 작성 — 원격 MCP 주소와
|
|
2321
|
+
io.out(`${relativeOf(io, join(dir, MCP_FILE))} 작성 — 원격 MCP 주소와 상점. ${ACCOUNT_MCP_NOTE}`);
|
|
2322
2322
|
}
|
|
2323
2323
|
/** `sayren logout` — refresh를 폐기하고(실패는 알리기만) 자격 증명 파일의 항목을 지운다 */
|
|
2324
2324
|
async function logout(flags, io) {
|
|
@@ -2335,7 +2335,7 @@ async function logout(flags, io) {
|
|
|
2335
2335
|
if (!revoked) io.err("알림: 계정 서버에서 로그인을 폐기하지 못했습니다(토큰은 이 컴퓨터에서 지웠습니다)");
|
|
2336
2336
|
return 0;
|
|
2337
2337
|
}
|
|
2338
|
-
/** `sayren whoami` — 쓰는 토큰의
|
|
2338
|
+
/** `sayren whoami` — 쓰는 토큰의 종류·계정·상점 범위 */
|
|
2339
2339
|
async function whoami(flags, io) {
|
|
2340
2340
|
const dir = resolve(io.cwd, flags.dir ?? ".");
|
|
2341
2341
|
const auth = resolveAuth(flags, dir, existsSync(dir) ? readState(dir) : null, io.env, credentialTokenReader(io.env));
|
|
@@ -2345,9 +2345,9 @@ async function whoami(flags, io) {
|
|
|
2345
2345
|
const entry = auth.tokenSource === "credentials" ? readCredential(io.env, auth.apiOrigin) : null;
|
|
2346
2346
|
const email = (await client.auth.me().catch(() => null))?.email ?? entry?.email ?? "확인 불가";
|
|
2347
2347
|
const kind = claims?.sayren_kind === "pat" ? "액세스 토큰" : "로그인";
|
|
2348
|
-
const scope = claims?.sayren_stores === "*" ? "모든
|
|
2348
|
+
const scope = claims?.sayren_stores === "*" ? "모든 상점" : Array.isArray(claims?.sayren_stores) ? claims.sayren_stores.join(", ") || "없음" : "확인 불가";
|
|
2349
2349
|
io.out(`${email} — ${kind}(${auth.tokenSource === "credentials" ? credentialsPathOf(io.env) : auth.tokenSource})`);
|
|
2350
|
-
io.out(`
|
|
2350
|
+
io.out(` 상점 범위 ${scope}`);
|
|
2351
2351
|
if (claims?.scope) io.out(` 권한 ${claims.scope}`);
|
|
2352
2352
|
return 0;
|
|
2353
2353
|
}
|
|
@@ -2372,26 +2372,26 @@ const USAGE = `sayren ${CLI_VERSION} — 호스팅 스토어프론트 CLI
|
|
|
2372
2372
|
명령:
|
|
2373
2373
|
login Sayren 계정으로 로그인합니다. 로그인 토큰은 사용자 설정 폴더(~/.config/sayren/credentials.json)에 둡니다
|
|
2374
2374
|
logout 로그인을 폐기하고 이 컴퓨터의 토큰을 지웁니다
|
|
2375
|
-
whoami 쓰는 계정·토큰
|
|
2376
|
-
pull 호스팅 중인 사이트의 작업 트리를 폴더(기본:
|
|
2375
|
+
whoami 쓰는 계정·토큰 종류·상점 범위
|
|
2376
|
+
pull 호스팅 중인 사이트의 작업 트리를 폴더(기본: 상점 코드)로 받고 .mcp.json·.env·.sayren/state.json을 씁니다
|
|
2377
2377
|
deploy 로컬 검사 → 작업 트리 올리기 → 빌드 → 발행. 원격이 pull 이후 바뀌었으면 덮어쓰지 않습니다
|
|
2378
2378
|
status 사이트·발행 버전·최근 빌드·편집 환경
|
|
2379
2379
|
verify 로컬 규칙·경로·크기 검사(서버 빌드와 같은 규칙)
|
|
2380
2380
|
|
|
2381
2381
|
토큰은 Sayren 계정 토큰만 받고 --token → SAYREN_TOKEN 환경 변수 → 로그인(credentials.json) 순서로 읽습니다.
|
|
2382
|
-
계정 토큰은 계정에 발급됩니다 —
|
|
2382
|
+
계정 토큰은 계정에 발급됩니다 — 상점은 --store → SAYREN_STORE_ID → 폴더 설정 순서로 고릅니다(하나면 자동).
|
|
2383
2383
|
옛 API 토큰(sy_pat_…)은 더 쓸 수 없습니다. .mcp.json에는 토큰을 쓰지 않습니다(MCP 클라이언트가 스스로 로그인합니다).
|
|
2384
2384
|
.mcp.json·.env·.sayren/은 .gitignore에 더하고 배포에 올리지 않습니다.
|
|
2385
2385
|
|
|
2386
2386
|
옵션:
|
|
2387
|
-
--project <storeId> pull: 받을
|
|
2388
|
-
--dir <폴더> 작업 폴더(기본: 지금 폴더, pull은
|
|
2387
|
+
--project <storeId> pull: 받을 상점(--store와 같습니다)
|
|
2388
|
+
--dir <폴더> 작업 폴더(기본: 지금 폴더, pull은 상점 코드)
|
|
2389
2389
|
--force pull: 같은 사이트를 받은 폴더에 원격을 다시 받습니다(지울·덮을 파일을 보여 주고 묻습니다)
|
|
2390
2390
|
deploy: 원격 변경·열린 편집 환경을 무시하고 덮어씁니다(확인을 묻습니다)
|
|
2391
2391
|
--yes --force의 확인을 묻지 않습니다(비대화형)
|
|
2392
2392
|
--no-publish deploy: 버전까지만 만들고 발행하지 않습니다
|
|
2393
2393
|
--message <메모> deploy: 버전 메모
|
|
2394
|
-
--store <storeId> 다룰
|
|
2394
|
+
--store <storeId> 다룰 상점(SAYREN_STORE_ID)
|
|
2395
2395
|
--device login: 브라우저 대신 기기 코드로 로그인합니다(SSH 등)
|
|
2396
2396
|
--paste login: 액세스 토큰을 입력받습니다
|
|
2397
2397
|
--api-origin <주소> api 주소(기본 https://api.sayren.app, SAYREN_API_ORIGIN). https만, localhost만 http
|
|
@@ -2554,7 +2554,7 @@ function describeError(error) {
|
|
|
2554
2554
|
if (error instanceof ApiError) {
|
|
2555
2555
|
if (error.status === 401 && error.code === "TOKEN_TYPE_RETIRED") return RETIRED_TOKEN_MESSAGE;
|
|
2556
2556
|
if (error.status === 401) return "토큰이 올바르지 않거나 만료됐습니다(401). npx sayren login으로 다시 로그인하거나 새 액세스 토큰을 쓰십시오";
|
|
2557
|
-
if (error.status === 403 && error.code === "STORE_NOT_GRANTED") return "이 토큰의
|
|
2557
|
+
if (error.status === 403 && error.code === "STORE_NOT_GRANTED") return "이 토큰의 상점 범위 밖입니다. 그 상점을 포함해 다시 로그인하십시오(npx sayren login)";
|
|
2558
2558
|
if (error.status === 403 && error.code === "INSUFFICIENT_SCOPE") return "토큰에 storefront:rw 스코프가 없습니다. 스코프를 더한 토큰을 쓰십시오";
|
|
2559
2559
|
if (error.code === "TOO_MANY_REQUESTS") return "요청이 너무 많습니다. 잠시 뒤 다시 시도하십시오";
|
|
2560
2560
|
return `${error.message} (${error.code}, HTTP ${error.status})`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sayren",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "sayren 스토어프론트 CLI — 호스팅 중인 사이트의 소스를 내려받고(pull) 로컬에서 고친 뒤 배포(deploy)합니다",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"node": ">=20.10"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@sayren/store-sdk": "^0.28.
|
|
16
|
+
"@sayren/store-sdk": "^0.28.1"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@biomejs/biome": "^2.5.14",
|