@zalkera/cli 0.20.2
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/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/main.js +3128 -0
- package/package.json +45 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,3128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// ../core/dist/notice.js
|
|
8
|
+
function defang(text) {
|
|
9
|
+
let out = text;
|
|
10
|
+
for (let pass = 0; pass < DEFANG_PASSES; pass += 1) {
|
|
11
|
+
const next = out.replace(LINK_SHAPE, (_m, inner) => `\uFF3D\uFF08${inner}\uFF09`).replace(AUTOLINK_SHAPE, (_m, inner) => `\uFF1C${inner}\uFF1E`);
|
|
12
|
+
if (next === out)
|
|
13
|
+
return out;
|
|
14
|
+
out = next;
|
|
15
|
+
}
|
|
16
|
+
return out.replace(/[\]()<>]/g, " ");
|
|
17
|
+
}
|
|
18
|
+
function plainNotice(text, limit = 300) {
|
|
19
|
+
if (typeof text !== "string")
|
|
20
|
+
return "";
|
|
21
|
+
const cap = Math.min(limit, MAX_CAP);
|
|
22
|
+
const head = text.length > cap * 4 ? text.slice(0, cap * 4) : text;
|
|
23
|
+
const stripped = head.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]+/g, " ");
|
|
24
|
+
const tidy = defang(stripped).replace(/\s+/g, " ").trim();
|
|
25
|
+
return tidy.length > cap ? `${tidy.slice(0, cap)}\u2026` : tidy;
|
|
26
|
+
}
|
|
27
|
+
var INNER_BOUND, MAX_CAP, LINK_SHAPE, AUTOLINK_SHAPE, DEFANG_PASSES;
|
|
28
|
+
var init_notice = __esm({
|
|
29
|
+
"../core/dist/notice.js"() {
|
|
30
|
+
"use strict";
|
|
31
|
+
INNER_BOUND = 2048;
|
|
32
|
+
MAX_CAP = 512;
|
|
33
|
+
LINK_SHAPE = new RegExp(`\\]\\s*\\(\\s*([A-Za-z][A-Za-z0-9+.-]*:[^)]{0,${INNER_BOUND}})\\)`, "g");
|
|
34
|
+
AUTOLINK_SHAPE = new RegExp(`<\\s*([A-Za-z][A-Za-z0-9+.-]*:[^>\\s]{0,${INNER_BOUND}})\\s*>`, "g");
|
|
35
|
+
DEFANG_PASSES = 4;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ../core/dist/errors.js
|
|
40
|
+
var DevtoolsError;
|
|
41
|
+
var init_errors = __esm({
|
|
42
|
+
"../core/dist/errors.js"() {
|
|
43
|
+
"use strict";
|
|
44
|
+
init_notice();
|
|
45
|
+
DevtoolsError = class extends Error {
|
|
46
|
+
code;
|
|
47
|
+
hint;
|
|
48
|
+
/**
|
|
49
|
+
* 서버가 보낸 `errorCode` 원문. **분기용이지 표시용이 아니다** — 보여 줄 때는 [humanMessage] 를 쓴다.
|
|
50
|
+
*
|
|
51
|
+
* 이것이 없으면 「동의하면 계속할 수 있다」는 거절과 「어떻게 해도 안 된다」는 거절을 호출부가
|
|
52
|
+
* 구분하지 못한다. 서버가 "계속하려면 확인해 주세요"라고 적어 보내는데 확인할 자리가 없으면
|
|
53
|
+
* 그 문장이 곧 막다른 길이 된다.
|
|
54
|
+
*/
|
|
55
|
+
serverCode;
|
|
56
|
+
/**
|
|
57
|
+
* 서버가 **어느 항목이 걸렸는지** 짚어 준 목록(응답의 `errors[].field`). 대개 소스 경로다.
|
|
58
|
+
*
|
|
59
|
+
* ■ 왜 문면이 아니라 목록인가
|
|
60
|
+
*
|
|
61
|
+
* 배치 요청 하나가 여러 항목을 담으면 「무엇이 걸렸는가」는 문장이 아니라 목록이다. 문면을
|
|
62
|
+
* 뜯어 쓰면 서버의 **한국어 문구가 계약이 된다** — 백엔드의 `ErrorResponse.errorCode` KDoc 이
|
|
63
|
+
* 이미 「문자열 매칭을 유도하면 계약이 깨진다」고 못박은 자리다.
|
|
64
|
+
*
|
|
65
|
+
* ⚠ **서버가 정하는 값이다.** 표시에 쓸 때는 [humanMessage] 와 같은 소독을 지나야 한다 —
|
|
66
|
+
* 알림 본문은 링크를 렌더하고 그 링크는 명령을 실행할 수 있다(형제 `api.ts` 의 실측).
|
|
67
|
+
*/
|
|
68
|
+
paths;
|
|
69
|
+
// 파라미터 프로퍼티(`constructor(readonly x)`)를 쓰지 않는다 — Node 의 타입 스트립 실행이 그 문법을
|
|
70
|
+
// 받지 않아서, 쓰는 순간 테스트가 빌드 산출물을 거쳐야 한다(소스를 그대로 돌리는 이점이 사라진다).
|
|
71
|
+
constructor(code2, message, hint, cause, serverCode, paths) {
|
|
72
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
73
|
+
this.name = "DevtoolsError";
|
|
74
|
+
this.code = code2;
|
|
75
|
+
this.hint = hint;
|
|
76
|
+
this.serverCode = serverCode;
|
|
77
|
+
this.paths = paths ?? [];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 사람에게 보여 줄 전체 문장(메시지 + 다음 할 일). **여기서 소독한다.**
|
|
81
|
+
*
|
|
82
|
+
* ⚠ 이 접근자의 이름이 곧 경계다 — 나가는 자리가 알림창인지 출력 채널인지는 이 클래스가
|
|
83
|
+
* 모른다. 그래서 「알림에 닿기 전에 어딘가에서 소독됐겠지」에 기대지 않는다.
|
|
84
|
+
*
|
|
85
|
+
* 실제로 그 기대가 틀렸다(3회전 심의 실증): `safeWrite.ts`·`untar.ts` 는 **서버가 정한 zip·tar
|
|
86
|
+
* 항목 이름**을 그대로 메시지에 보간하는데(`받은 파일이 폴더 밖을 가리킵니다: ${name}`),
|
|
87
|
+
* 소독은 `api.ts` 의 응답 파싱 경로에만 있었다. 항목 이름이
|
|
88
|
+
* `../[열기](command:workbench.action.terminal.new)` 이면 그 문장이 비-모달 알림에서
|
|
89
|
+
* **누르면 명령이 도는 링크**가 됐다.
|
|
90
|
+
*
|
|
91
|
+
* 보간하는 자리를 열거해 고치면 다음에 생기는 자리가 또 샌다 — 이 레포가 두 번 겪은 형상이다.
|
|
92
|
+
* 그래서 자리가 아니라 **나가는 문 하나**에서 한다. [message]·[hint] 원문은 그대로 남으므로
|
|
93
|
+
* 로그·시험은 영향받지 않는다.
|
|
94
|
+
*/
|
|
95
|
+
get humanMessage() {
|
|
96
|
+
const body = plainNotice(this.message);
|
|
97
|
+
return this.hint ? `${body}
|
|
98
|
+
${plainNotice(this.hint)}` : body;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ../core/dist/nodeOnPath.js
|
|
105
|
+
var init_nodeOnPath = __esm({
|
|
106
|
+
"../core/dist/nodeOnPath.js"() {
|
|
107
|
+
"use strict";
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ../core/dist/dev.js
|
|
112
|
+
var init_dev = __esm({
|
|
113
|
+
"../core/dist/dev.js"() {
|
|
114
|
+
"use strict";
|
|
115
|
+
init_errors();
|
|
116
|
+
init_nodeOnPath();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ../core/dist/index.js
|
|
121
|
+
init_errors();
|
|
122
|
+
|
|
123
|
+
// ../core/dist/handshake.js
|
|
124
|
+
init_errors();
|
|
125
|
+
init_notice();
|
|
126
|
+
|
|
127
|
+
// ../core/dist/serverUrl.js
|
|
128
|
+
function httpUrl(value) {
|
|
129
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
130
|
+
return null;
|
|
131
|
+
let parsed;
|
|
132
|
+
try {
|
|
133
|
+
parsed = new URL(value);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed : null;
|
|
138
|
+
}
|
|
139
|
+
function isLoopback(url) {
|
|
140
|
+
return url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]" || url.hostname === "::1";
|
|
141
|
+
}
|
|
142
|
+
function apiBaseUrl(value) {
|
|
143
|
+
const url = httpUrl(value);
|
|
144
|
+
if (!url)
|
|
145
|
+
return null;
|
|
146
|
+
return url.protocol === "https:" || isLoopback(url) ? url : null;
|
|
147
|
+
}
|
|
148
|
+
function mcpServerName(value) {
|
|
149
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value) ? value : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ../core/dist/handshake.js
|
|
153
|
+
var HANDSHAKE_TIMEOUT_MS = 15 * 1e3;
|
|
154
|
+
var MAX_VERSION_SHOWN = 40;
|
|
155
|
+
async function fetchHandshake(apiBase, extensionVersion, fetchImpl = fetch, upgradeHow) {
|
|
156
|
+
const url = new URL("/api/devtools/handshake", withTrailingSlash(apiBase));
|
|
157
|
+
url.searchParams.set("extensionVersion", extensionVersion);
|
|
158
|
+
let response;
|
|
159
|
+
try {
|
|
160
|
+
response = await fetchImpl(url, {
|
|
161
|
+
headers: { accept: "application/json" },
|
|
162
|
+
signal: AbortSignal.timeout(HANDSHAKE_TIMEOUT_MS)
|
|
163
|
+
});
|
|
164
|
+
} catch (cause) {
|
|
165
|
+
throw new DevtoolsError("SERVER_UNREACHABLE", "\uC798\uCEE4\uB77C \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", `\uC8FC\uC18C\uAC00 \uB9DE\uB294\uC9C0(${apiBase}), \uC778\uD130\uB137\xB7\uC0AC\uB0B4\uB9DD \uD504\uB85D\uC2DC\uAC00 \uB9C9\uACE0 \uC788\uC9C0 \uC54A\uC740\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.`, cause);
|
|
166
|
+
}
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uC11C\uBC84\uAC00 \uC751\uB2F5\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4(HTTP ${response.status}).`, "\uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694. \uACC4\uC18D\uB418\uBA74 \uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694.");
|
|
169
|
+
}
|
|
170
|
+
let body;
|
|
171
|
+
try {
|
|
172
|
+
body = await response.json();
|
|
173
|
+
} catch (cause) {
|
|
174
|
+
throw new DevtoolsError("SERVER_UNREACHABLE", "\uC11C\uBC84 \uC751\uB2F5\uC744 \uC774\uD574\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", `${apiBase} \uAC00 \uC798\uCEE4\uB77C \uC11C\uBC84\uAC00 \uB9DE\uB294\uC9C0, \uC0AC\uB0B4\uB9DD \uD504\uB85D\uC2DC\uAC00 \uC751\uB2F5\uC744 \uBC14\uAFB8\uACE0 \uC788\uC9C0 \uC54A\uC740\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.`, cause);
|
|
175
|
+
}
|
|
176
|
+
const handshake = body?.data;
|
|
177
|
+
if (!handshake || typeof handshake.verdict !== "string") {
|
|
178
|
+
throw new DevtoolsError("SERVER_UNREACHABLE", "\uC11C\uBC84 \uC751\uB2F5\uC5D0 \uD544\uC694\uD55C \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", "\uC8FC\uC18C\uAC00 \uC798\uCEE4\uB77C \uC11C\uBC84\uAC00 \uB9DE\uB294\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
179
|
+
}
|
|
180
|
+
if (!handshake.auth || !apiBaseUrl(handshake.auth.issuer)) {
|
|
181
|
+
throw new DevtoolsError("SERVER_REJECTED", "\uC11C\uBC84\uAC00 \uBCF4\uB0B8 \uB85C\uADF8\uC778 \uC8FC\uC18C\uB97C \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.", "\uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694.");
|
|
182
|
+
}
|
|
183
|
+
if (handshake.mcp && (!apiBaseUrl(handshake.mcp.authServerMetadataUrl) || !apiBaseUrl(handshake.mcp.sourceUrlTemplate?.replace("{tenantCode}", "probe")) || !mcpServerName(handshake.mcp.serverName))) {
|
|
184
|
+
handshake.mcp = null;
|
|
185
|
+
}
|
|
186
|
+
handshake.message = handshake.message == null ? null : plainNotice(handshake.message);
|
|
187
|
+
handshake.minExtensionVersion = plainNotice(handshake.minExtensionVersion, MAX_VERSION_SHOWN);
|
|
188
|
+
handshake.recommendedExtensionVersion = plainNotice(handshake.recommendedExtensionVersion, MAX_VERSION_SHOWN);
|
|
189
|
+
if (handshake.verdict === "UPGRADE_REQUIRED") {
|
|
190
|
+
throw new DevtoolsError(
|
|
191
|
+
"EXTENSION_OUTDATED",
|
|
192
|
+
handshake.message ?? "\uC774 \uBC84\uC804\uC740 \uB354 \uC774\uC0C1 \uC11C\uBC84\uC640 \uB9DE\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
|
|
193
|
+
// 🔴 **나가는 길을 맨 앞에 둔다.** `humanMessage` 가 다시 300자로 죄므로, 서버가 정하는
|
|
194
|
+
// 글자가 앞에 오면 **긴 값 하나로 우리 안내를 통째로 밀어낼 수** 있다(심의 실측:
|
|
195
|
+
// 191자면 `npx zalkera@latest` 가 사라진다). 그러면 「업데이트하세요」만 남고
|
|
196
|
+
// 방법이 없는, 이 인자가 막으려던 그 교착이 서버 문자열 하나로 되살아난다.
|
|
197
|
+
(upgradeHow === void 0 ? "" : `${upgradeHow} `) + `\uC11C\uBC84\uAC00 \uC694\uAD6C\uD558\uB294 \uCD5C\uC18C \uBC84\uC804\uC740 ${handshake.minExtensionVersion} \uC785\uB2C8\uB2E4.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return handshake;
|
|
201
|
+
}
|
|
202
|
+
function withTrailingSlash(base) {
|
|
203
|
+
return base.endsWith("/") ? base : `${base}/`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ../core/dist/auth/pkce.js
|
|
207
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
208
|
+
function createPkce() {
|
|
209
|
+
const verifier = base64Url(randomBytes(96));
|
|
210
|
+
const challenge = base64Url(createHash("sha256").update(verifier).digest());
|
|
211
|
+
return { verifier, challenge, method: "S256" };
|
|
212
|
+
}
|
|
213
|
+
function createState() {
|
|
214
|
+
return base64Url(randomBytes(32));
|
|
215
|
+
}
|
|
216
|
+
function base64Url(buffer) {
|
|
217
|
+
return buffer.toString("base64url");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ../core/dist/auth/loopback.js
|
|
221
|
+
init_errors();
|
|
222
|
+
import { createServer } from "node:http";
|
|
223
|
+
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
224
|
+
var CANCEL_ERRORS = /* @__PURE__ */ new Set(["access_denied", "consent_required", "login_required", "interaction_required"]);
|
|
225
|
+
var DEFAULT_SUCCESS_HTML = `<!doctype html>
|
|
226
|
+
<html lang="ko"><head><meta charset="utf-8"><title>\uB85C\uADF8\uC778 \uC644\uB8CC</title></head>
|
|
227
|
+
<body style="font-family:system-ui,sans-serif;padding:3rem;text-align:center">
|
|
228
|
+
<h1 style="font-size:1.25rem">\uB85C\uADF8\uC778\uC774 \uB05D\uB0AC\uC2B5\uB2C8\uB2E4</h1>
|
|
229
|
+
<p style="color:#555">\uC774 \uCC3D\uC744 \uB2EB\uACE0 \uD3B8\uC9D1\uAE30\uB85C \uB3CC\uC544\uAC00\uC138\uC694.</p>
|
|
230
|
+
</body></html>`;
|
|
231
|
+
async function startLoopbackReceiver(options = {}) {
|
|
232
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
233
|
+
const successHtml = options.successHtml ?? DEFAULT_SUCCESS_HTML;
|
|
234
|
+
let resolveCode;
|
|
235
|
+
let rejectCode;
|
|
236
|
+
let settled = false;
|
|
237
|
+
const codePromise = new Promise((resolve9, reject) => {
|
|
238
|
+
resolveCode = (result) => {
|
|
239
|
+
settled = true;
|
|
240
|
+
resolve9(result);
|
|
241
|
+
};
|
|
242
|
+
rejectCode = (error) => {
|
|
243
|
+
settled = true;
|
|
244
|
+
reject(error);
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
codePromise.catch(() => {
|
|
248
|
+
});
|
|
249
|
+
const server = createServer((request, response) => {
|
|
250
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
251
|
+
const error = url.searchParams.get("error");
|
|
252
|
+
const code2 = url.searchParams.get("code");
|
|
253
|
+
const state = url.searchParams.get("state");
|
|
254
|
+
if (error) {
|
|
255
|
+
const cancelled = CANCEL_ERRORS.has(error);
|
|
256
|
+
respond(response, cancelled ? 200 : 400, cancelled ? "<p>\uB85C\uADF8\uC778\uC744 \uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4. \uC774 \uCC3D\uC740 \uB2EB\uC73C\uC154\uB3C4 \uB429\uB2C8\uB2E4.</p>" : `<p>\uB85C\uADF8\uC778\uC774 \uAC70\uC808\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${escapeHtml(error)}</p>`);
|
|
257
|
+
rejectCode(cancelled ? new DevtoolsError("CANCELLED", "\uB85C\uADF8\uC778\uC744 \uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4.") : new Error(url.searchParams.get("error_description") ?? error));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!code2 || !state) {
|
|
261
|
+
respond(response, 400, "<p>\uC798\uBABB\uB41C \uC694\uCCAD\uC785\uB2C8\uB2E4.</p>");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
respond(response, 200, successHtml);
|
|
265
|
+
resolveCode({ code: code2, state });
|
|
266
|
+
});
|
|
267
|
+
await new Promise((resolve9, reject) => {
|
|
268
|
+
server.once("error", reject);
|
|
269
|
+
server.listen(0, "127.0.0.1", resolve9);
|
|
270
|
+
});
|
|
271
|
+
const port = server.address().port;
|
|
272
|
+
const timer = setTimeout(() => {
|
|
273
|
+
rejectCode(new Error("\uB85C\uADF8\uC778\uC774 \uC2DC\uAC04 \uC548\uC5D0 \uB05D\uB098\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4."));
|
|
274
|
+
}, timeoutMs);
|
|
275
|
+
timer.unref?.();
|
|
276
|
+
const close = () => {
|
|
277
|
+
clearTimeout(timer);
|
|
278
|
+
server.close();
|
|
279
|
+
if (!settled)
|
|
280
|
+
rejectCode(new DevtoolsError("CANCELLED", "\uB85C\uADF8\uC778\uC744 \uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4."));
|
|
281
|
+
};
|
|
282
|
+
if (options.signal) {
|
|
283
|
+
if (options.signal.aborted)
|
|
284
|
+
close();
|
|
285
|
+
else
|
|
286
|
+
options.signal.addEventListener("abort", close, { once: true });
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
redirectUri: `http://127.0.0.1:${port}/callback`,
|
|
290
|
+
waitForCode: () => codePromise,
|
|
291
|
+
close
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function respond(response, status, html) {
|
|
295
|
+
response.writeHead(status, { "content-type": "text/html; charset=utf-8" });
|
|
296
|
+
response.end(html);
|
|
297
|
+
}
|
|
298
|
+
function escapeHtml(value) {
|
|
299
|
+
return value.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ../core/dist/auth/oauth.js
|
|
303
|
+
init_errors();
|
|
304
|
+
var REFRESH_SKEW_MS = 2e4;
|
|
305
|
+
async function login(config, store, options) {
|
|
306
|
+
const pkce = createPkce();
|
|
307
|
+
const state = createState();
|
|
308
|
+
const receiver = await startLoopbackReceiver({
|
|
309
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
310
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
311
|
+
});
|
|
312
|
+
try {
|
|
313
|
+
const issuer = httpUrl(trimSlash(config.issuer));
|
|
314
|
+
if (!issuer) {
|
|
315
|
+
throw new DevtoolsError("SERVER_REJECTED", "\uC11C\uBC84\uAC00 \uBCF4\uB0B8 \uB85C\uADF8\uC778 \uC8FC\uC18C\uB97C \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.", "\uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694.");
|
|
316
|
+
}
|
|
317
|
+
const authorizeUrl = new URL(`${trimSlash(issuer.toString())}/protocol/openid-connect/auth`);
|
|
318
|
+
authorizeUrl.searchParams.set("client_id", config.clientId);
|
|
319
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
320
|
+
authorizeUrl.searchParams.set("redirect_uri", receiver.redirectUri);
|
|
321
|
+
authorizeUrl.searchParams.set("scope", config.scopes.join(" "));
|
|
322
|
+
authorizeUrl.searchParams.set("state", state);
|
|
323
|
+
authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
324
|
+
authorizeUrl.searchParams.set("code_challenge_method", pkce.method);
|
|
325
|
+
if (options.signal?.aborted)
|
|
326
|
+
throw new DevtoolsError("CANCELLED", "\uB85C\uADF8\uC778\uC744 \uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4.");
|
|
327
|
+
await options.openBrowser(authorizeUrl.toString());
|
|
328
|
+
const result = await receiver.waitForCode();
|
|
329
|
+
if (result.state !== state) {
|
|
330
|
+
throw new DevtoolsError("NOT_AUTHENTICATED", "\uB85C\uADF8\uC778 \uC751\uB2F5\uC774 \uC774 \uC694\uCCAD\uC758 \uAC83\uC774 \uC544\uB2D9\uB2C8\uB2E4.", "\uBE0C\uB77C\uC6B0\uC800 \uCC3D\uC744 \uBAA8\uB450 \uB2EB\uACE0 \uB2E4\uC2DC \uB85C\uADF8\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
331
|
+
}
|
|
332
|
+
const tokens = await exchange(config, {
|
|
333
|
+
grant_type: "authorization_code",
|
|
334
|
+
code: result.code,
|
|
335
|
+
redirect_uri: receiver.redirectUri,
|
|
336
|
+
code_verifier: pkce.verifier
|
|
337
|
+
});
|
|
338
|
+
await store.write(tokens);
|
|
339
|
+
return tokens;
|
|
340
|
+
} finally {
|
|
341
|
+
receiver.close();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
var refreshing = /* @__PURE__ */ new WeakMap();
|
|
345
|
+
var logoutEpoch = /* @__PURE__ */ new WeakMap();
|
|
346
|
+
var epochOf = (store) => logoutEpoch.get(store) ?? 0;
|
|
347
|
+
async function getAccessToken(config, store) {
|
|
348
|
+
const inFlight = refreshing.get(store);
|
|
349
|
+
if (inFlight)
|
|
350
|
+
return inFlight;
|
|
351
|
+
const run = refreshToken(config, store);
|
|
352
|
+
refreshing.set(store, run);
|
|
353
|
+
try {
|
|
354
|
+
return await run;
|
|
355
|
+
} finally {
|
|
356
|
+
refreshing.delete(store);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function refreshToken(config, store) {
|
|
360
|
+
const epoch = epochOf(store);
|
|
361
|
+
const current = await store.read();
|
|
362
|
+
if (!current) {
|
|
363
|
+
throw new DevtoolsError("NOT_AUTHENTICATED", "\uB85C\uADF8\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.", "\uBA3C\uC800 \uC798\uCEE4\uB77C\uC5D0 \uB85C\uADF8\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
364
|
+
}
|
|
365
|
+
if (current.issuer !== config.issuer) {
|
|
366
|
+
await store.clear();
|
|
367
|
+
throw new DevtoolsError("NOT_AUTHENTICATED", "\uC11C\uBC84\uAC00 \uBC14\uB00C\uC5B4 \uB2E4\uC2DC \uB85C\uADF8\uC778\uD574\uC57C \uD569\uB2C8\uB2E4.");
|
|
368
|
+
}
|
|
369
|
+
if (current.expiresAt - REFRESH_SKEW_MS > Date.now())
|
|
370
|
+
return current.accessToken;
|
|
371
|
+
let refreshed;
|
|
372
|
+
try {
|
|
373
|
+
refreshed = await exchange(config, { grant_type: "refresh_token", refresh_token: current.refreshToken });
|
|
374
|
+
} catch (cause) {
|
|
375
|
+
await store.clear();
|
|
376
|
+
throw new DevtoolsError("NOT_AUTHENTICATED", "\uB85C\uADF8\uC778\uC774 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", "\uB2E4\uC2DC \uB85C\uADF8\uC778\uD574 \uC8FC\uC138\uC694.", cause);
|
|
377
|
+
}
|
|
378
|
+
if (epochOf(store) !== epoch) {
|
|
379
|
+
throw new DevtoolsError("NOT_AUTHENTICATED", "\uB85C\uADF8\uC544\uC6C3\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", "\uB2E4\uC2DC \uB85C\uADF8\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
380
|
+
}
|
|
381
|
+
await store.write(refreshed);
|
|
382
|
+
return refreshed.accessToken;
|
|
383
|
+
}
|
|
384
|
+
async function logout(store) {
|
|
385
|
+
logoutEpoch.set(store, epochOf(store) + 1);
|
|
386
|
+
await store.clear();
|
|
387
|
+
}
|
|
388
|
+
async function exchange(config, params) {
|
|
389
|
+
const body = new URLSearchParams({ client_id: config.clientId, ...params });
|
|
390
|
+
const response = await fetch(`${trimSlash(config.issuer)}/protocol/openid-connect/token`, {
|
|
391
|
+
method: "POST",
|
|
392
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
393
|
+
body
|
|
394
|
+
});
|
|
395
|
+
if (!response.ok) {
|
|
396
|
+
const detail = await safeText(response);
|
|
397
|
+
throw new DevtoolsError("NOT_AUTHENTICATED", "\uD1A0\uD070 \uBC1C\uAE09\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.", detail || void 0);
|
|
398
|
+
}
|
|
399
|
+
const token = await response.json();
|
|
400
|
+
if (!token.refresh_token) {
|
|
401
|
+
throw new DevtoolsError("NOT_AUTHENTICATED", "\uC11C\uBC84\uAC00 \uAC31\uC2E0 \uD1A0\uD070\uC744 \uC8FC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.", "\uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694(\uD655\uC7A5 \uD074\uB77C\uC774\uC5B8\uD2B8 \uC124\uC815 \uBB38\uC81C\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4).");
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
accessToken: token.access_token,
|
|
405
|
+
refreshToken: token.refresh_token,
|
|
406
|
+
expiresAt: Date.now() + token.expires_in * 1e3,
|
|
407
|
+
issuer: config.issuer
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async function safeText(response) {
|
|
411
|
+
try {
|
|
412
|
+
return (await response.text()).slice(0, 200);
|
|
413
|
+
} catch {
|
|
414
|
+
return "";
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function trimSlash(value) {
|
|
418
|
+
return value.replace(/\/+$/, "");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ../core/dist/api.js
|
|
422
|
+
init_errors();
|
|
423
|
+
init_notice();
|
|
424
|
+
var ZalkeraApi = class _ZalkeraApi {
|
|
425
|
+
fetchImpl;
|
|
426
|
+
options;
|
|
427
|
+
constructor(options) {
|
|
428
|
+
this.options = options;
|
|
429
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* 내 소속 테넌트 — 테넌트 선택(A3)에 쓴다. 이 경로만 `X-Tenant` 를 요구하지 않는다(닭과 달걀).
|
|
433
|
+
*
|
|
434
|
+
* 내가 고를 수 있는 사이트. **`isSuperAdmin` 을 함께 돌려준다** — 목록이 비었다고 해서
|
|
435
|
+
* "사이트가 없다"가 아니기 때문이다.
|
|
436
|
+
*
|
|
437
|
+
* 서버는 `admin_user_tenant` **소속만** 열거한다. SUPER_ADMIN 은 소속과 무관하게 전 테넌트에
|
|
438
|
+
* 접근하므로(`AdminUserAuth.canAccessTenant`), 소속 행이 없는 순수 super-admin 은 **목록이 빈 채로
|
|
439
|
+
* 모든 사이트를 다룰 수 있다.** 그 둘을 구분하지 않으면 사용자에게 거짓을 말하게 된다.
|
|
440
|
+
*/
|
|
441
|
+
async whoAmI() {
|
|
442
|
+
const me = await this.request("GET", "/api/me", { withTenant: false });
|
|
443
|
+
return { tenants: me.tenants ?? [], isSuperAdmin: me.isSuperAdmin === true };
|
|
444
|
+
}
|
|
445
|
+
async listMyTenants() {
|
|
446
|
+
return (await this.whoAmI()).tenants;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* 미리보기 키 발급(memo146 §3.4). **재발급하면 이 테넌트의 이전 미리보기 키는 즉시 끊긴다** —
|
|
450
|
+
* `revokedPrevious` 가 0 이 아니면 호출부는 그 사실을 사람에게 말해야 한다(말 없이 끊기면 고장으로 읽힌다).
|
|
451
|
+
*/
|
|
452
|
+
issuePreviewKey(label) {
|
|
453
|
+
return this.request("POST", "/api/partner/storefront-keys/preview", {
|
|
454
|
+
body: label ? { label } : {}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* 스토어프론트 키 폐기 — **로그아웃이 진짜 로그아웃이 되게 하는 조각**(심의 경고).
|
|
459
|
+
*
|
|
460
|
+
* 이것이 없으면 로그아웃해도 이미 발급된 미리보기 키가 TTL(기본 12시간)까지 살아 있고, 그 키를 들고 있는
|
|
461
|
+
* dev 서버는 계속 상용 데이터를 읽는다. "로그아웃했다"는 화면과 실제가 어긋나는 자리다.
|
|
462
|
+
*/
|
|
463
|
+
revokeStorefrontKey(keyId) {
|
|
464
|
+
return this.request("DELETE", `/api/partner/storefront-keys/${keyId}`);
|
|
465
|
+
}
|
|
466
|
+
/** 시작 소스 팩 목록(B1). 공개된 것만 온다 — 고를 수 없는 것은 안 보이는 편이 정직하다. */
|
|
467
|
+
listPresets() {
|
|
468
|
+
return this.request("GET", "/api/partner/site-preset/presets");
|
|
469
|
+
}
|
|
470
|
+
/** 팩 소스 zip 의 단기 URL + **sha256**(받는 쪽이 대조한다 — 그래야 "배달했으면 끝"이 검증 가능한 약속이 된다). */
|
|
471
|
+
presetSourceUrl(code2) {
|
|
472
|
+
return this.request("GET", `/api/partner/site-preset/presets/${encodeURIComponent(code2)}/source-url`);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* 버전 이력(최신순). B2「현재 사이트 내려받기」와 롤백이 여기서 대상을 고른다.
|
|
476
|
+
*
|
|
477
|
+
* @param limit **최근 몇 개만** 받을지. 없으면 전량이다.
|
|
478
|
+
*
|
|
479
|
+
* ⚠ **판은 발행할 때마다 늘고 줄지 않는다.** 몇 개면 되는 소비자가 전량을 받으면 그 비용이 원장
|
|
480
|
+
* 크기에 비례해 영원히 자란다 — 반영 확인 폴링이 그 자리다(판 1,000개 테넌트에서 매 폴마다
|
|
481
|
+
* 전량). 구 서버는 이 파라미터를 **조용히 무시**하고 전량을 준다 — 동작은 종전과 같다.
|
|
482
|
+
*/
|
|
483
|
+
listRevisions(limit) {
|
|
484
|
+
const path = "/api/partner/site-upload/revisions";
|
|
485
|
+
return this.request("GET", limit == null ? path : `${path}?limit=${limit}`);
|
|
486
|
+
}
|
|
487
|
+
/** 소스 tar.gz 다운로드 URL **+ sha256**(TENANT_ADMIN+). 소스 소유의 실물이고, 해시가 그 약속의 검산이다. */
|
|
488
|
+
sourceUrl(revisionNo) {
|
|
489
|
+
return this.request("GET", `/api/partner/site-upload/revisions/${revisionNo}/source-url`);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* 버전 전환(롤백 포함). READY 인 버전만 받는다.
|
|
493
|
+
*
|
|
494
|
+
* ⚠ **소비하는 필드만 형에 둔다.** `pointerMoved` 는 「판을 옮겼다」와 「아무것도 안 옮겼다」를
|
|
495
|
+
* 가른다 — 종전에는 둘 다 성공이라 화면이 무동작을 「바꿨습니다」로 말했다.
|
|
496
|
+
* **결여는 `true` 방향**이다(서버 기본값이 그렇고, 구서버는 이 필드를 안 보낸다).
|
|
497
|
+
*/
|
|
498
|
+
activateRevision(revisionNo, discardPendingChanges = false) {
|
|
499
|
+
return this.request("POST", `/api/partner/site-upload/revisions/${revisionNo}/activate`, { body: { discardPendingChanges } });
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* 「지금 편집 중인 것이 있는가」 — 우리는 **`revertTargetRevisionNo` 하나만** 본다.
|
|
503
|
+
*
|
|
504
|
+
* ⚠ **못 읽어도 막지 않는다.** 구서버·권한 부족·망 단절이면 부르는 쪽이 `null` 로 강하하고,
|
|
505
|
+
* 그때 동작은 종전과 같다(`switchCandidates` 의 KDoc). 「모르는 것으로는 막지 않는다」.
|
|
506
|
+
*/
|
|
507
|
+
draftState() {
|
|
508
|
+
return this.request("GET", "/api/partner/site-upload/draft");
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* 편집 중인 것을 **판 N+1 로 올린다**(memo184 §2.4 · `POST /draft/publish`).
|
|
512
|
+
*
|
|
513
|
+
* ⚠ **새 문을 내지 않는다** — 콘솔이 쓰는 그 문을 그대로 쓴다. 문이 둘이 되면 「발행했는가」를
|
|
514
|
+
* 두 곳에 물어야 하고, 한쪽만 고쳐지는 병이 시작된다.
|
|
515
|
+
* ⚠ **`actor` 를 본문에 안 싣는다.** 서버가 인증에서 만든다 — 실으면 아무 이름이나 박힌다.
|
|
516
|
+
*
|
|
517
|
+
* @param discardPendingChanges 게시 대기 AI 변경을 함께 버릴지. **명시 동의가 있을 때만** 참이다.
|
|
518
|
+
*/
|
|
519
|
+
publishDraft(label, discardPendingChanges = false) {
|
|
520
|
+
return this.request("POST", "/api/partner/site-upload/draft/publish", {
|
|
521
|
+
body: { ...label === void 0 ? {} : { label }, discardPendingChanges }
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
/** 이 클라이언트가 어느 사이트를 가리키는가. 장부(`sync.json`)가 소속을 적을 때 쓴다. */
|
|
525
|
+
tenantCode() {
|
|
526
|
+
return this.options.tenantCode();
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* 소스 여러 파일을 **한 번에** 고친다(memo184 T0-① · `POST /draft/edits`).
|
|
530
|
+
*
|
|
531
|
+
* ⚠ **한 요청이 계약이다.** 파일마다 따로 부르면 드래프트 세대가 N 개 생기고, 그 중간 세대가
|
|
532
|
+
* 다른 문(발행·되돌리기)의 대상이 될 수 있다 — 의도한 변경의 **반쪽이 원장에 영구히 서는**
|
|
533
|
+
* 형상이다. 나눠 보내지 마라.
|
|
534
|
+
*
|
|
535
|
+
* ⚠ 거절은 **어느 경로가** 걸렸는지를 `errors[].field` 로 준다([DevtoolsError.paths]).
|
|
536
|
+
* 문면을 뜯지 마라 — 서버의 한국어 문구가 계약이 된다.
|
|
537
|
+
*/
|
|
538
|
+
editDraft(edits) {
|
|
539
|
+
return this.request("POST", "/api/partner/site-upload/draft/edits", {
|
|
540
|
+
body: { edits }
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* 지금 편집 중인 것이 **무엇을 바꿔 놓았는가**(memo184 T0-② · `GET /draft/files`).
|
|
545
|
+
*
|
|
546
|
+
* ⚠ **선행조건의 정본이 이 문이다.** 로컬 도구는 다음 편집의 base sha 를 자기 장부가 아니라
|
|
547
|
+
* 여기서 받는다. 장부로 폴백하면 memo184 🔴1(남이 되돌린 뒤에도 「이미 반영됨」이라 말하는
|
|
548
|
+
* 거짓 성공)이 그대로 되살아난다.
|
|
549
|
+
* ⚠ `generation` 은 **불투명**하다. 값을 해석하거나 순서를 매기지 않는다 — 서버 내부 토큰이
|
|
550
|
+
* 클라이언트로 새지 않게 그렇게 정했다.
|
|
551
|
+
*
|
|
552
|
+
* 편집이 없으면 **빈 목록**이다(404 가 아니다 — 「없다」와 「못 물어봤다」를 가른다).
|
|
553
|
+
*/
|
|
554
|
+
draftFiles() {
|
|
555
|
+
return this.request("GET", "/api/partner/site-upload/draft/files");
|
|
556
|
+
}
|
|
557
|
+
/** 업로드 presign — zip 을 S3 로 **직접** 올린다(백엔드 미경유). */
|
|
558
|
+
presignArchive(fileName, byteSize) {
|
|
559
|
+
return this.request("POST", "/api/partner/site-archive/presign", {
|
|
560
|
+
body: { fileName, contentType: "application/zip", byteSize }
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* 업로드 확정 — 서버가 언팩·검사하고 새 버전을 만든다.
|
|
565
|
+
*
|
|
566
|
+
* @param baseRevisionNo 이 올리기가 **딛는다고 선언하는 판**(동시 업로드 최소 방어). 서버가 판을
|
|
567
|
+
* 만들기 전에 원장 꼬리와 대조하고 다르면 `UPLOAD_BASE_MOVED` 409 로 멈춘다.
|
|
568
|
+
* `null` 은 **무선언 — 현행 그대로 통과**다(모르는 것으로 막지 않는다).
|
|
569
|
+
*
|
|
570
|
+
* ⚠ **동의 재시도에서도 같은 값을 실어야 한다.** 백엔드는 `BaselineShiftGuard`(동의)를 기반
|
|
571
|
+
* 대조보다 **먼저** 지나므로, 동의 409 를 받고 재호출할 때 이 값을 빠뜨리면 **그 경로만 조용히
|
|
572
|
+
* 무선언**이 된다 — 방어가 있는 줄 알았는데 하필 편집이 있는 사이트에서만 없다.
|
|
573
|
+
*/
|
|
574
|
+
confirmArchive(storageKey, discardPendingChanges = false, baseRevisionNo = null) {
|
|
575
|
+
return this.request("POST", "/api/partner/site-archive/confirm", {
|
|
576
|
+
// 선언이 없으면 **필드 자체를 안 보낸다** — `null` 을 보내도 서버는 같게 다루지만, 안 보내는
|
|
577
|
+
// 쪽이 「주장하지 않는다」는 뜻에 정확하고 구 서버와도 같은 와이어다.
|
|
578
|
+
body: baseRevisionNo == null ? { storageKey, discardPendingChanges } : { storageKey, discardPendingChanges, baseRevisionNo }
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* 요청 상한. **Node 의 `fetch` 는 기본 타임아웃이 없다** — 서버가 연결만 붙들고 응답을 안 주면
|
|
583
|
+
* 호출자가 영원히 매달린다. 로그아웃의 키 폐기는 진행 표시조차 없어 화면이 그냥 멈춘 것처럼 보인다.
|
|
584
|
+
*
|
|
585
|
+
* 이 값은 **제어 평면 호출**(수 KB JSON)의 상한이다. 대용량 업로드는 이 클래스를 통과하지 않는다
|
|
586
|
+
* (presign 으로 받은 URL 에 직접 올린다) — 그래서 짧게 잡아도 큰 전송을 끊지 않는다.
|
|
587
|
+
*/
|
|
588
|
+
static REQUEST_TIMEOUT_MS = 3e4;
|
|
589
|
+
async request(method, path, init = {}) {
|
|
590
|
+
const headers = {
|
|
591
|
+
accept: "application/json",
|
|
592
|
+
authorization: `Bearer ${await this.options.accessToken()}`
|
|
593
|
+
};
|
|
594
|
+
if (init.withTenant !== false)
|
|
595
|
+
headers["x-tenant"] = this.options.tenantCode();
|
|
596
|
+
if (init.body !== void 0)
|
|
597
|
+
headers["content-type"] = "application/json";
|
|
598
|
+
let response;
|
|
599
|
+
try {
|
|
600
|
+
response = await this.fetchImpl(new URL(path, withTrailingSlash2(this.options.apiBase)), {
|
|
601
|
+
method,
|
|
602
|
+
headers,
|
|
603
|
+
signal: AbortSignal.timeout(_ZalkeraApi.REQUEST_TIMEOUT_MS),
|
|
604
|
+
...init.body === void 0 ? {} : { body: JSON.stringify(init.body) }
|
|
605
|
+
});
|
|
606
|
+
} catch (cause) {
|
|
607
|
+
const timedOut = cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError");
|
|
608
|
+
throw new DevtoolsError("SERVER_UNREACHABLE", timedOut ? "\uC798\uCEE4\uB77C \uC11C\uBC84\uAC00 \uC81C\uB54C \uC751\uB2F5\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4." : "\uC798\uCEE4\uB77C \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", timedOut ? "\uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694." : "\uC778\uD130\uB137\xB7\uC0AC\uB0B4\uB9DD \uD504\uB85D\uC2DC\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694.", cause);
|
|
609
|
+
}
|
|
610
|
+
if (!response.ok)
|
|
611
|
+
throw await toError(response);
|
|
612
|
+
let envelope;
|
|
613
|
+
try {
|
|
614
|
+
envelope = await response.json();
|
|
615
|
+
} catch (cause) {
|
|
616
|
+
throw new DevtoolsError("SERVER_UNREACHABLE", "\uC11C\uBC84 \uC751\uB2F5\uC744 \uC774\uD574\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", `${this.options.apiBase} \uAC00 \uC798\uCEE4\uB77C \uC11C\uBC84\uAC00 \uB9DE\uB294\uC9C0, \uC0AC\uB0B4\uB9DD \uD504\uB85D\uC2DC\uAC00 \uC751\uB2F5\uC744 \uBC14\uAFB8\uACE0 \uC788\uC9C0 \uC54A\uC740\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.`, cause);
|
|
617
|
+
}
|
|
618
|
+
if (envelope === null || typeof envelope !== "object" || !("data" in envelope)) {
|
|
619
|
+
throw new DevtoolsError("SERVER_UNREACHABLE", "\uC11C\uBC84 \uC751\uB2F5\uC5D0 \uD544\uC694\uD55C \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", `${this.options.apiBase} \uAC00 \uC798\uCEE4\uB77C \uC11C\uBC84\uAC00 \uB9DE\uB294\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.`);
|
|
620
|
+
}
|
|
621
|
+
return envelope.data;
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
async function toError(response) {
|
|
625
|
+
let serverMessage = "";
|
|
626
|
+
let errorCode = "";
|
|
627
|
+
let paths = [];
|
|
628
|
+
try {
|
|
629
|
+
const body = await response.json();
|
|
630
|
+
serverMessage = plainNotice(body.message, MAX_SERVER_MESSAGE);
|
|
631
|
+
errorCode = plainNotice(body.errorCode ?? "", MAX_ERROR_CODE);
|
|
632
|
+
paths = (Array.isArray(body.errors) ? body.errors : []).map((row) => typeof row?.field === "string" ? plainNotice(row.field, MAX_PATH) : "").filter((path) => path !== "").slice(0, MAX_REJECTED_PATHS);
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
if (response.status === 401) {
|
|
636
|
+
return new DevtoolsError("NOT_AUTHENTICATED", "\uB85C\uADF8\uC778\uC774 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", "\uB2E4\uC2DC \uB85C\uADF8\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
637
|
+
}
|
|
638
|
+
if (response.status === 403) {
|
|
639
|
+
return new DevtoolsError("FORBIDDEN", serverMessage || "\uC774 \uC791\uC5C5\uC744 \uD560 \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.", "\uC0AC\uC774\uD2B8 \uC18C\uC2A4\uB97C \uB2E4\uB8E8\uB824\uBA74 \uAD00\uB9AC\uC790 \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. \uACC4\uC815 \uAD8C\uD55C\uC744 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
640
|
+
}
|
|
641
|
+
return new DevtoolsError("SERVER_REJECTED", serverMessage || `\uC11C\uBC84\uAC00 \uC694\uCCAD\uC744 \uAC70\uC808\uD588\uC2B5\uB2C8\uB2E4(HTTP ${response.status}).`, errorCode ? `\uC624\uB958 \uCF54\uB4DC: ${errorCode}` : void 0, void 0, errorCode || void 0, paths);
|
|
642
|
+
}
|
|
643
|
+
var DISCARD_CONSENT_CODES = /* @__PURE__ */ new Set([
|
|
644
|
+
// 판이 옮겨진다 — 전환은 진행되고, 게시 안 한 AI 변경이 함께 사라진다(`BaselineShiftGuard`).
|
|
645
|
+
"PENDING_AI_CHANGES_CONFIRM_REQUIRED",
|
|
646
|
+
// **판은 그대로다** — 이미 켜진 판을 다시 고른 것이라 「지금으로 되돌리기」로 갈리고,
|
|
647
|
+
// 편집과 게시 대기 AI 변경만 버린다(`discardToCurrent`). 잃는 것이 더 크므로 문면이 다르다.
|
|
648
|
+
"DRAFT_DISCARD_CONFIRM_REQUIRED"
|
|
649
|
+
]);
|
|
650
|
+
function needsDiscardConsent(error) {
|
|
651
|
+
return error instanceof DevtoolsError && DISCARD_CONSENT_CODES.has(error.serverCode ?? "");
|
|
652
|
+
}
|
|
653
|
+
var DRAFT_IN_PROGRESS = "DRAFT_IN_PROGRESS";
|
|
654
|
+
function isDraftInProgress(error) {
|
|
655
|
+
return error instanceof DevtoolsError && error.serverCode === DRAFT_IN_PROGRESS;
|
|
656
|
+
}
|
|
657
|
+
var MAX_SERVER_MESSAGE = 300;
|
|
658
|
+
var MAX_ERROR_CODE = 64;
|
|
659
|
+
var MAX_PATH = 1024;
|
|
660
|
+
var MAX_REJECTED_PATHS = 500;
|
|
661
|
+
function withTrailingSlash2(base) {
|
|
662
|
+
return base.endsWith("/") ? base : `${base}/`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ../core/dist/safeWrite.js
|
|
666
|
+
init_errors();
|
|
667
|
+
import { lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
668
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
669
|
+
import { basename, join, normalize, relative, resolve, sep } from "node:path";
|
|
670
|
+
async function lstatOrAbsent(path, name) {
|
|
671
|
+
try {
|
|
672
|
+
return await lstat(path);
|
|
673
|
+
} catch (cause) {
|
|
674
|
+
const code2 = cause.code;
|
|
675
|
+
if (code2 === "ENOENT" || code2 === "ENOTDIR")
|
|
676
|
+
return null;
|
|
677
|
+
throw new DevtoolsError("SERVER_REJECTED", `${name} \uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD574 \uC4F0\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4(${code2 ?? "UNKNOWN"}).`, "\uD3F4\uB354 \uAD8C\uD55C\uC744 \uD655\uC778\uD558\uC2DC\uAC70\uB098, \uB2E4\uB978 \uBE48 \uD3F4\uB354\uB85C \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.", cause);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function safeSegments(root, name) {
|
|
681
|
+
const cleaned = name.replace(/^(\.\/)+/, "");
|
|
682
|
+
if (cleaned.startsWith("/") || /^[A-Za-z]:/.test(cleaned) || cleaned.includes("\0")) {
|
|
683
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uD30C\uC77C\uC5D0 \uC774\uC0C1\uD55C \uACBD\uB85C\uAC00 \uC788\uC2B5\uB2C8\uB2E4: ${name}`);
|
|
684
|
+
}
|
|
685
|
+
const path = resolve(root, normalize(cleaned));
|
|
686
|
+
if (path !== root && !path.startsWith(root + sep)) {
|
|
687
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uD30C\uC77C\uC774 \uD3F4\uB354 \uBC16\uC744 \uAC00\uB9AC\uD0B5\uB2C8\uB2E4: ${name}`);
|
|
688
|
+
}
|
|
689
|
+
const segments = relative(root, path).split(sep).filter((part) => part.length > 0);
|
|
690
|
+
return segments;
|
|
691
|
+
}
|
|
692
|
+
async function descend(root, segments, verified) {
|
|
693
|
+
let current = root;
|
|
694
|
+
for (const part of segments) {
|
|
695
|
+
const next = join(current, part);
|
|
696
|
+
if (verified.has(next)) {
|
|
697
|
+
current = next;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const info = await lstatOrAbsent(next, part);
|
|
701
|
+
if (info?.isSymbolicLink()) {
|
|
702
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uAFB8\uB7EC\uBBF8\uAC00 \uB9C1\uD06C\uB97C \uAC70\uCCD0 \uD3F4\uB354 \uBC16\uC5D0 \uC4F0\uB824\uACE0 \uD569\uB2C8\uB2E4: ${part}`, "\uC798\uBABB \uBC1B\uC740 \uC0C1\uD0DC\uB85C \uC9C4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694.");
|
|
703
|
+
}
|
|
704
|
+
if (!info) {
|
|
705
|
+
await mkdir(next).catch((error) => {
|
|
706
|
+
if (error?.code !== "EEXIST")
|
|
707
|
+
throw error;
|
|
708
|
+
});
|
|
709
|
+
} else if (!info.isDirectory()) {
|
|
710
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uAFB8\uB7EC\uBBF8\uC758 \uACBD\uB85C\uAC00 \uD30C\uC77C\uACFC \uACB9\uCE69\uB2C8\uB2E4: ${part}`);
|
|
711
|
+
}
|
|
712
|
+
verified.add(next);
|
|
713
|
+
current = next;
|
|
714
|
+
}
|
|
715
|
+
return current;
|
|
716
|
+
}
|
|
717
|
+
async function assertNotSymlink(path, name) {
|
|
718
|
+
const info = await lstatOrAbsent(path, name);
|
|
719
|
+
if (info?.isSymbolicLink()) {
|
|
720
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uAFB8\uB7EC\uBBF8\uAC00 \uAE30\uC874 \uB9C1\uD06C\uB97C \uB36E\uC5B4\uC4F0\uB824\uACE0 \uD569\uB2C8\uB2E4: ${name}`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async function writeOwnFile(path, data, mode = 420) {
|
|
724
|
+
const info = await lstat(path).catch(() => null);
|
|
725
|
+
if (info?.isSymbolicLink()) {
|
|
726
|
+
throw new DevtoolsError("NOT_A_SITE", `${basename(path)} \uC774 \uB9C1\uD06C\uB77C \uC4F0\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`, "\uC774 \uD30C\uC77C\uC740 \uD655\uC7A5\uC774 \uB9CC\uB4E4\uC5B4 \uC8FC\uB294 \uC790\uB9AC\uC785\uB2C8\uB2E4. \uB9C1\uD06C\uB97C \uC9C0\uC6B0\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.");
|
|
727
|
+
}
|
|
728
|
+
await writeViaRename(path, data, mode);
|
|
729
|
+
}
|
|
730
|
+
async function writeViaRename(path, data, mode = 420) {
|
|
731
|
+
const tmp = `${path}.zalkera-${randomBytes2(6).toString("hex")}.tmp`;
|
|
732
|
+
try {
|
|
733
|
+
await writeFile(tmp, data, typeof data === "string" ? { encoding: "utf8", mode, flag: "wx" } : { mode, flag: "wx" });
|
|
734
|
+
await rename(tmp, path);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
await rm(tmp, { force: true }).catch(() => void 0);
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async function ensureOwnDir(root, ...segments) {
|
|
741
|
+
let current = root;
|
|
742
|
+
for (const part of segments) {
|
|
743
|
+
const next = join(current, part);
|
|
744
|
+
const info = await lstat(next).catch(() => null);
|
|
745
|
+
if (info?.isSymbolicLink()) {
|
|
746
|
+
throw new DevtoolsError("NOT_A_SITE", `${part} \uC774 \uB9C1\uD06C\uB77C \uC4F0\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`, "\uC774 \uD3F4\uB354\uB294 \uD655\uC7A5\uC774 \uB9CC\uB4E4\uC5B4 \uC8FC\uB294 \uC790\uB9AC\uC785\uB2C8\uB2E4. \uB9C1\uD06C\uB97C \uC9C0\uC6B0\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.");
|
|
747
|
+
}
|
|
748
|
+
if (!info) {
|
|
749
|
+
await mkdir(next).catch((error) => {
|
|
750
|
+
if (error?.code !== "EEXIST")
|
|
751
|
+
throw error;
|
|
752
|
+
});
|
|
753
|
+
} else if (!info.isDirectory()) {
|
|
754
|
+
throw new DevtoolsError("NOT_A_SITE", `${part} \uC774 \uD3F4\uB354\uAC00 \uC544\uB2C8\uB77C \uC4F0\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`);
|
|
755
|
+
}
|
|
756
|
+
current = next;
|
|
757
|
+
}
|
|
758
|
+
return current;
|
|
759
|
+
}
|
|
760
|
+
function assertNotVendored(segments, name) {
|
|
761
|
+
if (!segments.includes("node_modules"))
|
|
762
|
+
return;
|
|
763
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uD30C\uC77C\uC5D0 node_modules \uAC00 \uB4E4\uC5B4 \uC788\uC2B5\uB2C8\uB2E4: ${name}`, "\uBC1B\uC740 \uAFB8\uB7EC\uBBF8\uAC00 \uC815\uC0C1\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694.");
|
|
764
|
+
}
|
|
765
|
+
async function writeExclusive(path, data, entryName, written) {
|
|
766
|
+
try {
|
|
767
|
+
await writeFile(path, data, { flag: "wx" });
|
|
768
|
+
written?.add(path);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
if (error.code !== "EEXIST")
|
|
771
|
+
throw error;
|
|
772
|
+
assertNotWrittenTwice(path, entryName, written);
|
|
773
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uAFB8\uB7EC\uBBF8\uAC00 \uC774\uBBF8 \uC788\uB294 \uD30C\uC77C\uC744 \uB36E\uC73C\uB824 \uD569\uB2C8\uB2E4: ${entryName}`, "\uADF8 \uD30C\uC77C\uC740 \uC774 \uD3F4\uB354\uC5D0 \uC6D0\uB798 \uC788\uB358 \uAC83\uC785\uB2C8\uB2E4. \uBE48 \uD3F4\uB354\uB97C \uC0C8\uB85C \uB9CC\uB4E4\uC5B4 \uB2E4\uC2DC \uBC1B\uC544 \uC8FC\uC138\uC694.");
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function assertNotWrittenTwice(path, entryName, written) {
|
|
777
|
+
if (!written?.has(path))
|
|
778
|
+
return;
|
|
779
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uAFB8\uB7EC\uBBF8\uAC00 \uAC19\uC740 \uD30C\uC77C\uC744 \uB450 \uBC88 \uB2F4\uACE0 \uC788\uC2B5\uB2C8\uB2E4: ${entryName}`, "\uBC1B\uC740 \uAC83\uC774 \uC798\uBABB\uB410\uC2B5\uB2C8\uB2E4 \u2014 \uBE48 \uD3F4\uB354\uB85C \uB2E4\uC2DC \uBC1B\uC544\uB3C4 \uAC19\uC740 \uC790\uB9AC\uC5D0\uC11C \uBA48\uCDA5\uB2C8\uB2E4. \uC798\uCEE4\uB77C\uC5D0 \uC54C\uB824 \uC8FC\uC138\uC694.");
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ../core/dist/project.js
|
|
783
|
+
init_errors();
|
|
784
|
+
import { existsSync } from "node:fs";
|
|
785
|
+
import { readFile, writeFile as writeFile2 } from "node:fs/promises";
|
|
786
|
+
import { join as join2 } from "node:path";
|
|
787
|
+
async function inspectProject(dir) {
|
|
788
|
+
const packagePath = join2(dir, "package.json");
|
|
789
|
+
if (!existsSync(packagePath)) {
|
|
790
|
+
throw new DevtoolsError("NOT_A_SITE", "\uC774 \uD3F4\uB354\uC5D0\uC11C \uC0AC\uC774\uD2B8 \uC18C\uC2A4\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", "\uC0AC\uC774\uD2B8 \uC18C\uC2A4 \uD3F4\uB354(package.json \uC774 \uC788\uB294 \uACF3)\uB97C \uACE8\uB77C \uC8FC\uC138\uC694.");
|
|
791
|
+
}
|
|
792
|
+
let parsed;
|
|
793
|
+
try {
|
|
794
|
+
parsed = JSON.parse(await readFile(packagePath, "utf8"));
|
|
795
|
+
} catch (cause) {
|
|
796
|
+
throw new DevtoolsError("NOT_A_SITE", "package.json \uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4(\uD615\uC2DD \uC624\uB958).", "\uD30C\uC77C\uC774 \uD3B8\uC9D1 \uC911 \uAE68\uC84C\uB294\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.", cause);
|
|
797
|
+
}
|
|
798
|
+
const deps = { ...parsed.dependencies, ...parsed.devDependencies };
|
|
799
|
+
return {
|
|
800
|
+
dir,
|
|
801
|
+
name: parsed.name ?? dir.split("/").filter(Boolean).pop() ?? dir,
|
|
802
|
+
hasNext: "next" in deps,
|
|
803
|
+
clientVersion: deps["@zalkera/client"] ?? null,
|
|
804
|
+
toolInDeps: TOOL_PACKAGES.filter((name) => name in deps),
|
|
805
|
+
hasNodeModules: existsSync(join2(dir, "node_modules"))
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
var TOOL_PACKAGES = ["@zalkera/cli", "@zalkera/devtools", "zalkera", "zalkera-cli"];
|
|
809
|
+
|
|
810
|
+
// ../core/dist/deps.js
|
|
811
|
+
init_errors();
|
|
812
|
+
|
|
813
|
+
// ../core/dist/untar.js
|
|
814
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
815
|
+
import { chmod, lstat as lstat2, mkdir as mkdir2, open, realpath, rm as rm2, symlink, writeFile as writeFile3 } from "node:fs/promises";
|
|
816
|
+
import { isAbsolute, join as join3, normalize as normalize2, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
817
|
+
|
|
818
|
+
// ../core/dist/limits.js
|
|
819
|
+
var MAX_DOWNLOAD_BYTES = 150 * 1024 * 1024;
|
|
820
|
+
var SOURCE_EXPANSION = 3;
|
|
821
|
+
var MAX_EXTRACT_BYTES = MAX_DOWNLOAD_BYTES * SOURCE_EXPANSION;
|
|
822
|
+
var MAX_ENTRY_BYTES = 200 * 1024 * 1024;
|
|
823
|
+
var MAX_ENTRIES = 2e5;
|
|
824
|
+
var MAX_NAME_BYTES = 8 * 1024 * 1024;
|
|
825
|
+
|
|
826
|
+
// ../core/dist/untar.js
|
|
827
|
+
init_errors();
|
|
828
|
+
async function extractTar(tar, targetDir, options = {}) {
|
|
829
|
+
const sink = await createSink(targetDir, options);
|
|
830
|
+
await forEachBufferedEntry(tar, (meta, data) => sink.consume(meta, data));
|
|
831
|
+
return sink.fileCount();
|
|
832
|
+
}
|
|
833
|
+
async function forEachBufferedEntry(tar, consume) {
|
|
834
|
+
let offset = 0;
|
|
835
|
+
while (offset + BLOCK <= tar.length) {
|
|
836
|
+
const header = tar.subarray(offset, offset + BLOCK);
|
|
837
|
+
if (isEndOfArchive(header))
|
|
838
|
+
break;
|
|
839
|
+
const meta = parseHeader(header);
|
|
840
|
+
offset += BLOCK;
|
|
841
|
+
if (offset + meta.size > tar.length) {
|
|
842
|
+
throw new DevtoolsError("SERVER_REJECTED", "\uBC1B\uC740 \uD30C\uC77C\uC774 \uC911\uAC04\uC5D0 \uB04A\uACBC\uC2B5\uB2C8\uB2E4.");
|
|
843
|
+
}
|
|
844
|
+
const data = tar.subarray(offset, offset + meta.size);
|
|
845
|
+
offset += Math.ceil(meta.size / BLOCK) * BLOCK;
|
|
846
|
+
await consume(meta, data);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async function readTarGzManifest(gzipped, options = {}) {
|
|
850
|
+
return readTarManifest(await gunzipBuffer(gzipped, options.maxBytes), options);
|
|
851
|
+
}
|
|
852
|
+
async function readTarManifest(tar, options = {}) {
|
|
853
|
+
const manifest = {};
|
|
854
|
+
const root = resolve2(sep2, "zalkera-manifest-root");
|
|
855
|
+
let entries = 0;
|
|
856
|
+
const entryCap = Math.min(options.maxEntries ?? MAX_ENTRIES, MAX_ENTRIES);
|
|
857
|
+
const names = createNameResolver();
|
|
858
|
+
await forEachBufferedEntry(tar, async (meta, data) => {
|
|
859
|
+
entries += 1;
|
|
860
|
+
if (entries > entryCap) {
|
|
861
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uD30C\uC77C\uC758 \uD56D\uBAA9\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4(${entryCap}\uAC1C \uCD08\uACFC).`, "\uC798\uBABB \uBC1B\uC740 \uC0C1\uD0DC\uB85C \uC9C4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694.");
|
|
862
|
+
}
|
|
863
|
+
const resolved = names.resolve(meta, data);
|
|
864
|
+
if (!resolved)
|
|
865
|
+
return;
|
|
866
|
+
const { name } = resolved;
|
|
867
|
+
assertKnownEntryType(meta.type, name);
|
|
868
|
+
if (meta.type !== "0" && meta.type !== "\0")
|
|
869
|
+
return;
|
|
870
|
+
const segments = safeSegments(root, name);
|
|
871
|
+
if (options.rejectVendored === true)
|
|
872
|
+
assertNotVendored(segments, name);
|
|
873
|
+
const path = segments.join("/");
|
|
874
|
+
if (path === "")
|
|
875
|
+
return;
|
|
876
|
+
manifest[path] = { sha256: createHash2("sha256").update(data).digest("hex"), bytes: meta.size };
|
|
877
|
+
});
|
|
878
|
+
return manifest;
|
|
879
|
+
}
|
|
880
|
+
var BLOCK = 512;
|
|
881
|
+
function isEndOfArchive(header) {
|
|
882
|
+
return header.every((byte) => byte === 0);
|
|
883
|
+
}
|
|
884
|
+
function parseHeader(header) {
|
|
885
|
+
const size = parseOctal(header.subarray(124, 136));
|
|
886
|
+
if (!Number.isFinite(size) || size < 0) {
|
|
887
|
+
throw new DevtoolsError("SERVER_REJECTED", "\uBC1B\uC740 \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uC2B5\uB2C8\uB2E4(\uAE38\uC774 \uD544\uB4DC \uC624\uB958).");
|
|
888
|
+
}
|
|
889
|
+
if (size > MAX_ENTRY_BYTES) {
|
|
890
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uD30C\uC77C\uC5D0 \uBE44\uC815\uC0C1\uC801\uC73C\uB85C \uD070 \uD56D\uBAA9\uC774 \uC788\uC2B5\uB2C8\uB2E4(${size}B).`, "\uC798\uBABB \uBC1B\uC740 \uC0C1\uD0DC\uB85C \uC9C4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694.");
|
|
891
|
+
}
|
|
892
|
+
const rawName = cstring(header.subarray(0, 100));
|
|
893
|
+
const prefix = cstring(header.subarray(345, 500));
|
|
894
|
+
return {
|
|
895
|
+
name: prefix ? `${prefix}/${rawName}` : rawName,
|
|
896
|
+
size,
|
|
897
|
+
type: String.fromCharCode(header[156] ?? 0),
|
|
898
|
+
mode: parseOctal(header.subarray(100, 108)) || 420,
|
|
899
|
+
linkTarget: cstring(header.subarray(157, 257))
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
function createNameResolver() {
|
|
903
|
+
let pendingName = null;
|
|
904
|
+
let pendingLink = null;
|
|
905
|
+
return {
|
|
906
|
+
resolve(entry, data) {
|
|
907
|
+
if (entry.type === "L") {
|
|
908
|
+
pendingName = cstring(data);
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
if (entry.type === "x" || entry.type === "g") {
|
|
912
|
+
const path = parsePaxPath(data);
|
|
913
|
+
if (path)
|
|
914
|
+
pendingName = path;
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
if (entry.type === "K") {
|
|
918
|
+
pendingLink = cstring(data);
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
const name = pendingName ?? entry.name;
|
|
922
|
+
pendingName = null;
|
|
923
|
+
const linkTarget = pendingLink ?? entry.linkTarget;
|
|
924
|
+
pendingLink = null;
|
|
925
|
+
return { name, linkTarget };
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function assertKnownEntryType(type, name) {
|
|
930
|
+
if (type === "0" || type === "\0" || type === "5" || type === "2" || type === "1")
|
|
931
|
+
return;
|
|
932
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uD30C\uC77C\uC5D0 \uB2E4\uB8F0 \uC218 \uC5C6\uB294 \uD56D\uBAA9\uC774 \uC788\uC2B5\uB2C8\uB2E4(\uD615\uC2DD ${type}): ${name}`, "\uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694. \uC798\uBABB \uBC1B\uC740 \uC0C1\uD0DC\uB85C \uC9C4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
|
933
|
+
}
|
|
934
|
+
async function createSink(targetDir, options) {
|
|
935
|
+
await mkdir2(targetDir, { recursive: true });
|
|
936
|
+
const root = await realpath(targetDir);
|
|
937
|
+
const verified = /* @__PURE__ */ new Set([root]);
|
|
938
|
+
let entries = 0;
|
|
939
|
+
const entryCap = Math.min(options.maxEntries ?? MAX_ENTRIES, MAX_ENTRIES);
|
|
940
|
+
const materializeLinks = options.symlinks === "materialize";
|
|
941
|
+
const preserveMode = options.preserveMode === true;
|
|
942
|
+
let count2 = 0;
|
|
943
|
+
const writtenPaths = /* @__PURE__ */ new Set();
|
|
944
|
+
const foldedPaths = /* @__PURE__ */ new Set();
|
|
945
|
+
const names = createNameResolver();
|
|
946
|
+
return {
|
|
947
|
+
fileCount: () => count2,
|
|
948
|
+
async consume(entry, data) {
|
|
949
|
+
entries += 1;
|
|
950
|
+
if (entries > entryCap) {
|
|
951
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uD30C\uC77C\uC5D0 \uD56D\uBAA9\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4(\uC0C1\uD55C ${entryCap.toLocaleString()}\uAC1C).`);
|
|
952
|
+
}
|
|
953
|
+
const resolved = names.resolve(entry, data);
|
|
954
|
+
if (!resolved)
|
|
955
|
+
return;
|
|
956
|
+
const { name, linkTarget } = resolved;
|
|
957
|
+
const segments = safeSegments(root, name);
|
|
958
|
+
if (options.rejectVendored === true)
|
|
959
|
+
assertNotVendored(segments, name);
|
|
960
|
+
if (entry.type === "5") {
|
|
961
|
+
if (options.decide)
|
|
962
|
+
return;
|
|
963
|
+
await descend(root, segments, verified);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (entry.type === "2") {
|
|
967
|
+
if (!materializeLinks)
|
|
968
|
+
return;
|
|
969
|
+
await writeSymlink(root, segments, name, linkTarget, verified);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (entry.type === "1")
|
|
973
|
+
return;
|
|
974
|
+
assertKnownEntryType(entry.type, name);
|
|
975
|
+
const leaf = segments[segments.length - 1];
|
|
976
|
+
if (!leaf)
|
|
977
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uD30C\uC77C\uC5D0 \uC774\uC0C1\uD55C \uACBD\uB85C\uAC00 \uC788\uC2B5\uB2C8\uB2E4: ${name}`);
|
|
978
|
+
const verdict = options.decide?.(segments.join("/")) ?? "create";
|
|
979
|
+
if (verdict === "skip")
|
|
980
|
+
return;
|
|
981
|
+
const parent = await descend(root, segments.slice(0, -1), verified);
|
|
982
|
+
const path = join3(parent, leaf);
|
|
983
|
+
await assertNotSymlink(path, name);
|
|
984
|
+
if (verdict === "replace") {
|
|
985
|
+
assertNotWrittenTwice(path, name, writtenPaths);
|
|
986
|
+
assertNotWrittenTwice(path.toLowerCase(), name, foldedPaths);
|
|
987
|
+
await writeViaRename(path, data, preserveMode && (entry.mode & 73) !== 0 ? 493 : 420);
|
|
988
|
+
writtenPaths.add(path);
|
|
989
|
+
foldedPaths.add(path.toLowerCase());
|
|
990
|
+
} else {
|
|
991
|
+
await writeExclusive(path, data, name, writtenPaths);
|
|
992
|
+
}
|
|
993
|
+
if (preserveMode && (entry.mode & 73) !== 0) {
|
|
994
|
+
await chmod(path, 493).catch(() => {
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
count2 += 1;
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
async function writeSymlink(root, segments, name, target, verified) {
|
|
1002
|
+
if (!target)
|
|
1003
|
+
return;
|
|
1004
|
+
if (isAbsolute(target) || /^[A-Za-z]:/.test(target)) {
|
|
1005
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uAFB8\uB7EC\uBBF8\uC5D0 \uC808\uB300\uACBD\uB85C \uB9C1\uD06C\uAC00 \uC788\uC2B5\uB2C8\uB2E4: ${name}`);
|
|
1006
|
+
}
|
|
1007
|
+
const leaf = segments[segments.length - 1];
|
|
1008
|
+
if (!leaf)
|
|
1009
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uAFB8\uB7EC\uBBF8\uC5D0 \uC774\uC0C1\uD55C \uB9C1\uD06C \uC774\uB984\uC774 \uC788\uC2B5\uB2C8\uB2E4: ${name}`);
|
|
1010
|
+
const parent = await descend(root, segments.slice(0, -1), verified);
|
|
1011
|
+
const resolved = resolve2(parent, target);
|
|
1012
|
+
if (resolved !== root && !resolved.startsWith(root + sep2)) {
|
|
1013
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uBC1B\uC740 \uAFB8\uB7EC\uBBF8\uC758 \uB9C1\uD06C\uAC00 \uD3F4\uB354 \uBC16\uC744 \uAC00\uB9AC\uD0B5\uB2C8\uB2E4: ${name}`);
|
|
1014
|
+
}
|
|
1015
|
+
const linkPath = join3(parent, leaf);
|
|
1016
|
+
await assertNotSymlink(linkPath, name);
|
|
1017
|
+
await symlink(relative2(parent, resolved) || ".", linkPath).catch((error) => {
|
|
1018
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uB9C1\uD06C\uB97C \uB9CC\uB4E4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4: ${name}`, "\uBC1B\uB294 \uD3F4\uB354\uB97C \uBE44\uC6B0\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.", error);
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
function cstring(buffer) {
|
|
1022
|
+
const end = buffer.indexOf(0);
|
|
1023
|
+
return buffer.subarray(0, end === -1 ? buffer.length : end).toString("utf8");
|
|
1024
|
+
}
|
|
1025
|
+
function parsePaxPath(data) {
|
|
1026
|
+
const text = data.toString("utf8");
|
|
1027
|
+
const match = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(text);
|
|
1028
|
+
return match?.[1] ?? null;
|
|
1029
|
+
}
|
|
1030
|
+
function parseOctal(buffer) {
|
|
1031
|
+
const text = cstring(buffer).trim();
|
|
1032
|
+
return text.length === 0 ? 0 : Number.parseInt(text, 8);
|
|
1033
|
+
}
|
|
1034
|
+
function resolveCap(maxBytes) {
|
|
1035
|
+
return maxBytes ?? MAX_EXTRACT_BYTES;
|
|
1036
|
+
}
|
|
1037
|
+
async function gunzipTar(input, maxBytes) {
|
|
1038
|
+
return gunzipBuffer(input, maxBytes);
|
|
1039
|
+
}
|
|
1040
|
+
async function gunzipBuffer(input, maxBytes) {
|
|
1041
|
+
const { gunzip: gunzipCb } = await import("node:zlib");
|
|
1042
|
+
const { promisify: promisify3 } = await import("node:util");
|
|
1043
|
+
const cap = resolveCap(maxBytes);
|
|
1044
|
+
try {
|
|
1045
|
+
return await promisify3(gunzipCb)(input, { maxOutputLength: cap });
|
|
1046
|
+
} catch (cause) {
|
|
1047
|
+
throw new DevtoolsError("SERVER_REJECTED", "\uBC1B\uC740 \uD30C\uC77C\uC774 \uB108\uBB34 \uD06C\uAC70\uB098 \uC190\uC0C1\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", "\uB2E4\uC2DC \uC2DC\uB3C4\uD574\uB3C4 \uAC19\uC73C\uBA74 \uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694.", cause);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// ../core/dist/payload.js
|
|
1052
|
+
var EXTRACT_RATIO_CAP = 8;
|
|
1053
|
+
var DISK_HEADROOM = 1 + 2 * EXTRACT_RATIO_CAP;
|
|
1054
|
+
var MIN_EXTRACT_CAP = 32 * 1024 * 1024;
|
|
1055
|
+
|
|
1056
|
+
// ../core/dist/index.js
|
|
1057
|
+
init_dev();
|
|
1058
|
+
|
|
1059
|
+
// ../core/dist/syncLedger.js
|
|
1060
|
+
var SYNC_LEDGER_PATH = ".zalkera/sync.json";
|
|
1061
|
+
var SYNC_LEDGER_FORMAT = 1;
|
|
1062
|
+
function parseSyncLedger(text) {
|
|
1063
|
+
let raw;
|
|
1064
|
+
try {
|
|
1065
|
+
raw = JSON.parse(text);
|
|
1066
|
+
} catch {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
if (typeof raw !== "object" || raw === null)
|
|
1070
|
+
return null;
|
|
1071
|
+
const o = raw;
|
|
1072
|
+
if (o.format !== SYNC_LEDGER_FORMAT)
|
|
1073
|
+
return null;
|
|
1074
|
+
if (typeof o.tenant !== "string" || o.tenant === "")
|
|
1075
|
+
return null;
|
|
1076
|
+
const base = o.base;
|
|
1077
|
+
if (!base || typeof base.revisionNo !== "number" || typeof base.tarSha256 !== "string")
|
|
1078
|
+
return null;
|
|
1079
|
+
const files = {};
|
|
1080
|
+
const rawFiles = o.files;
|
|
1081
|
+
if (typeof rawFiles !== "object" || rawFiles === null)
|
|
1082
|
+
return null;
|
|
1083
|
+
for (const [path, value] of Object.entries(rawFiles)) {
|
|
1084
|
+
const f = value;
|
|
1085
|
+
if (typeof f?.sha256 !== "string" || typeof f?.bytes !== "number")
|
|
1086
|
+
return null;
|
|
1087
|
+
files[path] = { sha256: f.sha256, bytes: f.bytes };
|
|
1088
|
+
}
|
|
1089
|
+
const server = o.server;
|
|
1090
|
+
if (server !== null && server !== void 0 && typeof server.generation !== "string")
|
|
1091
|
+
return null;
|
|
1092
|
+
const mine = {};
|
|
1093
|
+
const rawMine = o.mine;
|
|
1094
|
+
if (rawMine !== void 0) {
|
|
1095
|
+
if (typeof rawMine !== "object" || rawMine === null)
|
|
1096
|
+
return null;
|
|
1097
|
+
for (const [path, value] of Object.entries(rawMine)) {
|
|
1098
|
+
if (value !== null && typeof value !== "string")
|
|
1099
|
+
return null;
|
|
1100
|
+
mine[path] = value;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
format: SYNC_LEDGER_FORMAT,
|
|
1105
|
+
tenant: o.tenant,
|
|
1106
|
+
base: { revisionNo: base.revisionNo, tarSha256: base.tarSha256 },
|
|
1107
|
+
files,
|
|
1108
|
+
server: server == null ? null : { generation: server.generation },
|
|
1109
|
+
mine,
|
|
1110
|
+
pulledAt: typeof o.pulledAt === "string" ? o.pulledAt : "",
|
|
1111
|
+
pushedAt: typeof o.pushedAt === "string" ? o.pushedAt : null
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function serializeSyncLedger(ledger) {
|
|
1115
|
+
return `${JSON.stringify(ledger, null, 2)}
|
|
1116
|
+
`;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// ../core/dist/localMark.js
|
|
1120
|
+
import { join as join4 } from "node:path";
|
|
1121
|
+
var SOURCE_MARK_PATH = ".zalkera/source.json";
|
|
1122
|
+
var TENANT_CODE = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
1123
|
+
var MAX_REVISION_NO = 2147483647;
|
|
1124
|
+
function plausibleRevisionNo(no) {
|
|
1125
|
+
return typeof no === "number" && Number.isSafeInteger(no) && no > 0 && no <= MAX_REVISION_NO ? no : null;
|
|
1126
|
+
}
|
|
1127
|
+
function buildSourceMark(input) {
|
|
1128
|
+
const mark = { format: 1, ...input };
|
|
1129
|
+
return `${JSON.stringify(mark, null, 2)}
|
|
1130
|
+
`;
|
|
1131
|
+
}
|
|
1132
|
+
function parseSourceMark(text) {
|
|
1133
|
+
if (text === null)
|
|
1134
|
+
return null;
|
|
1135
|
+
let raw;
|
|
1136
|
+
try {
|
|
1137
|
+
raw = JSON.parse(text);
|
|
1138
|
+
} catch {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
if (typeof raw !== "object" || raw === null)
|
|
1142
|
+
return null;
|
|
1143
|
+
const o = raw;
|
|
1144
|
+
if (typeof o.tenant !== "string" || !TENANT_CODE.test(o.tenant))
|
|
1145
|
+
return null;
|
|
1146
|
+
if (o.format === 1) {
|
|
1147
|
+
if (typeof o.revisionNo !== "number" || !Number.isInteger(o.revisionNo))
|
|
1148
|
+
return null;
|
|
1149
|
+
if (typeof o.sha256 !== "string" || o.sha256.length === 0)
|
|
1150
|
+
return null;
|
|
1151
|
+
if (typeof o.fetchedAt !== "string")
|
|
1152
|
+
return null;
|
|
1153
|
+
return { format: 1, tenant: o.tenant, revisionNo: o.revisionNo, sha256: o.sha256, fetchedAt: o.fetchedAt };
|
|
1154
|
+
}
|
|
1155
|
+
if (o.format === 2 && o.origin === "published") {
|
|
1156
|
+
if (typeof o.revisionNo !== "number" || !Number.isInteger(o.revisionNo))
|
|
1157
|
+
return null;
|
|
1158
|
+
if (typeof o.publishedAt !== "string")
|
|
1159
|
+
return null;
|
|
1160
|
+
return {
|
|
1161
|
+
format: 2,
|
|
1162
|
+
origin: "published",
|
|
1163
|
+
tenant: o.tenant,
|
|
1164
|
+
revisionNo: o.revisionNo,
|
|
1165
|
+
publishedAt: o.publishedAt
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
if (o.format === 2 && o.origin === "linked") {
|
|
1169
|
+
if (typeof o.linkedAt !== "string")
|
|
1170
|
+
return null;
|
|
1171
|
+
return { format: 2, origin: "linked", tenant: o.tenant, linkedAt: o.linkedAt };
|
|
1172
|
+
}
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
async function writeSourceMarkTo(root, mark) {
|
|
1176
|
+
return writeMarkText(root, buildSourceMark(mark));
|
|
1177
|
+
}
|
|
1178
|
+
async function writeMarkText(root, text) {
|
|
1179
|
+
try {
|
|
1180
|
+
await ensureOwnDir(root, ".zalkera");
|
|
1181
|
+
await writeOwnFile(join4(root, SOURCE_MARK_PATH), text);
|
|
1182
|
+
return { ok: true, text: "" };
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// ../core/dist/provenance.js
|
|
1189
|
+
var PROVENANCE_PATH = ".zalkera/provenance.json";
|
|
1190
|
+
|
|
1191
|
+
// ../core/dist/zip.js
|
|
1192
|
+
init_errors();
|
|
1193
|
+
import { join as join5, relative as relative3, sep as sep3 } from "node:path";
|
|
1194
|
+
import { deflateRaw } from "node:zlib";
|
|
1195
|
+
import { promisify } from "node:util";
|
|
1196
|
+
var deflate = promisify(deflateRaw);
|
|
1197
|
+
var ALWAYS_EXCLUDED = /* @__PURE__ */ new Set([
|
|
1198
|
+
"node_modules",
|
|
1199
|
+
".git",
|
|
1200
|
+
// macOS 기본 압축이 만드는 부스러기. 발행에도 들여오기에도 실릴 이유가 없다.
|
|
1201
|
+
// ⚠ 조회가 소문자라 **항목도 소문자여야 한다**(위 .ds_store 와 같은 함정).
|
|
1202
|
+
"__macosx",
|
|
1203
|
+
".next",
|
|
1204
|
+
"dist",
|
|
1205
|
+
"out",
|
|
1206
|
+
".ds_store",
|
|
1207
|
+
// ⚠ 조회가 소문자라 **항목도 소문자여야 한다**(아래 참조)
|
|
1208
|
+
".turbo",
|
|
1209
|
+
".vercel",
|
|
1210
|
+
".claude",
|
|
1211
|
+
// ── 아래 넷은 심의 실측으로 추가됐다(2026-08-10) ──────────────────────────
|
|
1212
|
+
// 이 목록이 옛 배치 기준으로 쓰인 뒤 **우리가 파일을 늘렸는데** 목록을 안 늘렸다.
|
|
1213
|
+
//
|
|
1214
|
+
// `.mcp.json` 은 **우리가 만든다**(「에이전트 연결」). 그리고 그 파일은 남의 MCP 서버 항목을
|
|
1215
|
+
// 일부러 보존하는데(mcp.ts), stdio 형 서버는 관례적으로 `env: { GITHUB_TOKEN: … }` 를 들고 다닌다.
|
|
1216
|
+
// 우리가 만든 파일이 정본 소스가 되어 그 버전을 받는 **모두에게** 배포되는 자리였다.
|
|
1217
|
+
".mcp.json",
|
|
1218
|
+
// `.vscode/settings.json` 에는 **우리가 적은 `zalkera.tenant`** 가 들어 있다(`configTarget()`).
|
|
1219
|
+
// 그것이 zip 에 실려 유통되면, 그 소스를 받아 연 사람의 확장이 조용히 그 테넌트를 가리킨다.
|
|
1220
|
+
// 편집기 설정은 **배포되는 사이트의 내용이 아니다** — 빠져도 잃는 것이 없다.
|
|
1221
|
+
".vscode",
|
|
1222
|
+
".idea",
|
|
1223
|
+
// 홈 디렉터리 자격증명이 프로젝트 안에 복사돼 있는 경우가 실제로 있다.
|
|
1224
|
+
".ssh",
|
|
1225
|
+
".aws"
|
|
1226
|
+
]);
|
|
1227
|
+
function isValueLessTemplate(lower) {
|
|
1228
|
+
return lower.startsWith(".env") && /\.(example|sample|template)$/.test(lower);
|
|
1229
|
+
}
|
|
1230
|
+
var LIVE_SECRET = [
|
|
1231
|
+
["\uC798\uCEE4\uB77C \uC2A4\uD1A0\uC5B4\uD504\uB860\uD2B8 \uD0A4", /\boqsk_[0-9A-Za-z_-]{8,}/],
|
|
1232
|
+
["AWS \uC561\uC138\uC2A4\uD0A4", /\bAKIA[0-9A-Z]{16}\b/],
|
|
1233
|
+
["\uAC1C\uC778\uD0A4 \uBE14\uB85D", /-----BEGIN [A-Z ]{0,20}PRIVATE KEY-----/],
|
|
1234
|
+
["\uACB0\uC81C \uB77C\uC774\uBE0C \uC2DC\uD06C\uB9BF", /\b(?:sk_live_|live_sk_)[0-9A-Za-z]{8,}/],
|
|
1235
|
+
["GitHub \uD1A0\uD070", /\b(?:gh[pousr]_[0-9A-Za-z]{20,}|github_pat_[0-9A-Za-z_]{20,})\b/],
|
|
1236
|
+
["Slack \uD1A0\uD070", /\bxox[abprs]-[0-9A-Za-z-]{10,}\b/],
|
|
1237
|
+
["Google API \uD0A4", /\bAIza[0-9A-Za-z_-]{35}\b/],
|
|
1238
|
+
["npm \uD1A0\uD070", /\bnpm_[0-9A-Za-z]{36}\b/]
|
|
1239
|
+
];
|
|
1240
|
+
var AWS_DOC_PLACEHOLDER = /\bAKIA[0-9A-Z]{9}EXAMPLE\b/;
|
|
1241
|
+
var AWS_LIVE_KEY = /\bAKIA(?![0-9A-Z]{9}EXAMPLE\b)[0-9A-Z]{16}\b/;
|
|
1242
|
+
var URL_CREDENTIAL = /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]{3,}@([^\s/?#]+)/;
|
|
1243
|
+
var UNREACHABLE_HOST = /^(?:localhost|0\.0\.0\.0|::1|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+)$/i;
|
|
1244
|
+
var UNREACHABLE_SUFFIX = /\.(?:local|localhost|internal|test|invalid)$/i;
|
|
1245
|
+
function reachableHost(raw) {
|
|
1246
|
+
const host = raw.replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
|
|
1247
|
+
if (UNREACHABLE_HOST.test(host) || UNREACHABLE_SUFFIX.test(host))
|
|
1248
|
+
return false;
|
|
1249
|
+
return host.includes(".");
|
|
1250
|
+
}
|
|
1251
|
+
var TEMPLATE_SCAN_BYTES = 256 * 1024;
|
|
1252
|
+
function decodeTemplate(data) {
|
|
1253
|
+
if (data.length >= 4 && data[0] === 255 && data[1] === 254 && data[2] === 0 && data[3] === 0)
|
|
1254
|
+
return null;
|
|
1255
|
+
if (data.length >= 4 && data[0] === 0 && data[1] === 0 && data[2] === 254 && data[3] === 255)
|
|
1256
|
+
return null;
|
|
1257
|
+
if (data.length >= 2 && data[0] === 255 && data[1] === 254) {
|
|
1258
|
+
return data.subarray(2).toString("utf16le");
|
|
1259
|
+
}
|
|
1260
|
+
if (data.length >= 2 && data[0] === 254 && data[1] === 255) {
|
|
1261
|
+
const swapped = Buffer.from(data.subarray(2));
|
|
1262
|
+
swapped.swap16();
|
|
1263
|
+
return swapped.toString("utf16le");
|
|
1264
|
+
}
|
|
1265
|
+
if (data.includes(0))
|
|
1266
|
+
return null;
|
|
1267
|
+
return data.toString("utf8");
|
|
1268
|
+
}
|
|
1269
|
+
function templateHoldsSecret(text) {
|
|
1270
|
+
if (/-----BEGIN [A-Z ]{0,20}PRIVATE KEY-----/.test(text))
|
|
1271
|
+
return "\uAC1C\uC778\uD0A4 \uBE14\uB85D";
|
|
1272
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
1273
|
+
const line = raw.trim();
|
|
1274
|
+
if (line === "")
|
|
1275
|
+
continue;
|
|
1276
|
+
const eq = line.replace(/^#+\s*/, "").indexOf("=");
|
|
1277
|
+
if (eq < 0)
|
|
1278
|
+
continue;
|
|
1279
|
+
const value = line.replace(/^#+\s*/, "").slice(eq + 1).trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
1280
|
+
if (value === "")
|
|
1281
|
+
continue;
|
|
1282
|
+
for (const [what, pattern] of LIVE_SECRET) {
|
|
1283
|
+
if (!pattern.test(value))
|
|
1284
|
+
continue;
|
|
1285
|
+
if (what === "AWS \uC561\uC138\uC2A4\uD0A4" && AWS_DOC_PLACEHOLDER.test(value) && !AWS_LIVE_KEY.test(value)) {
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
return what;
|
|
1289
|
+
}
|
|
1290
|
+
const url = URL_CREDENTIAL.exec(value);
|
|
1291
|
+
if (url !== null && reachableHost(url[1] ?? ""))
|
|
1292
|
+
return "URL \uB0B4\uC7A5 \uC790\uACA9\uC99D\uBA85";
|
|
1293
|
+
}
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
function isSecretFile(name, isDirectory = false) {
|
|
1297
|
+
const lower = name.toLowerCase();
|
|
1298
|
+
if (!isDirectory && isValueLessTemplate(lower))
|
|
1299
|
+
return false;
|
|
1300
|
+
if (lower.startsWith(".env"))
|
|
1301
|
+
return true;
|
|
1302
|
+
if (SECRET_NAMES.has(lower))
|
|
1303
|
+
return true;
|
|
1304
|
+
if (SECRET_SUFFIXES.some((suffix) => lower.endsWith(suffix)))
|
|
1305
|
+
return true;
|
|
1306
|
+
if (lower.endsWith(".json") && (lower.startsWith("service-account") || lower.includes("firebase-adminsdk"))) {
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1309
|
+
return lower.endsWith(".env");
|
|
1310
|
+
}
|
|
1311
|
+
var SECRET_NAMES = /* @__PURE__ */ new Set([
|
|
1312
|
+
"id_rsa",
|
|
1313
|
+
"id_dsa",
|
|
1314
|
+
"id_ecdsa",
|
|
1315
|
+
"id_ed25519",
|
|
1316
|
+
"credentials.json",
|
|
1317
|
+
".npmrc",
|
|
1318
|
+
// 심의 추가(2026-08-10) — 셋 다 **평문 자격증명**을 담는 표준 파일명이다.
|
|
1319
|
+
".netrc",
|
|
1320
|
+
"_netrc",
|
|
1321
|
+
// 윈도 변형 — 같은 파일이다
|
|
1322
|
+
".git-credentials",
|
|
1323
|
+
".yarnrc.yml",
|
|
1324
|
+
// 도구별 자격증명 파일. 전부 **평문**이고, 홈이 아니라 프로젝트 루트에 놓이는 일이 흔하다.
|
|
1325
|
+
".dockercfg",
|
|
1326
|
+
".pgpass",
|
|
1327
|
+
// libpq — `host:port:db:user:password`
|
|
1328
|
+
".pypirc",
|
|
1329
|
+
// twine 업로드 토큰
|
|
1330
|
+
"kubeconfig",
|
|
1331
|
+
// 클러스터 자격증명(이름 그대로 두는 관례)
|
|
1332
|
+
"secrets.json"
|
|
1333
|
+
// .NET user-secrets 등의 관례명
|
|
1334
|
+
]);
|
|
1335
|
+
var SECRET_SUFFIXES = [".pem", ".key", ".p12", ".pfx", ".p8"];
|
|
1336
|
+
var EXCLUDED_PATHS = /* @__PURE__ */ new Set([".zalkera/source.json", SYNC_LEDGER_PATH, PROVENANCE_PATH]);
|
|
1337
|
+
var EXCLUDED_PREFIXES = [".zalkera/saved/"];
|
|
1338
|
+
function isExcludedPath(path) {
|
|
1339
|
+
return EXCLUDED_PATHS.has(path) || EXCLUDED_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
1340
|
+
}
|
|
1341
|
+
function isExcludedEntry(entryPath) {
|
|
1342
|
+
const path = entryPath.split(sep3).join("/").toLowerCase();
|
|
1343
|
+
if (isExcludedPath(path))
|
|
1344
|
+
return true;
|
|
1345
|
+
const segments = path.split("/").filter((s) => s !== "");
|
|
1346
|
+
if (segments.some((segment) => ALWAYS_EXCLUDED.has(segment)))
|
|
1347
|
+
return true;
|
|
1348
|
+
return segments.some((segment, at) => isSecretFile(segment, at < segments.length - 1));
|
|
1349
|
+
}
|
|
1350
|
+
function templateBreach(name, data) {
|
|
1351
|
+
if (!isValueLessTemplate(name.toLowerCase()))
|
|
1352
|
+
return null;
|
|
1353
|
+
if (data.byteLength > TEMPLATE_SCAN_BYTES)
|
|
1354
|
+
return "\uC11C\uC2DD\uCE58\uACE0 \uB108\uBB34 \uD07D\uB2C8\uB2E4";
|
|
1355
|
+
const text = decodeTemplate(data);
|
|
1356
|
+
if (text === null)
|
|
1357
|
+
return "\uAE00\uC790\uB85C \uC77D\uD788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4";
|
|
1358
|
+
return templateHoldsSecret(text);
|
|
1359
|
+
}
|
|
1360
|
+
var MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
1361
|
+
var MAX_RAW_BYTES = 300 * 1024 * 1024;
|
|
1362
|
+
|
|
1363
|
+
// ../core/dist/publish.js
|
|
1364
|
+
init_errors();
|
|
1365
|
+
var MAX_ARCHIVE_BYTES = 100 * 1024 * 1024;
|
|
1366
|
+
var TRANSFER_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
1367
|
+
|
|
1368
|
+
// ../core/dist/preview.js
|
|
1369
|
+
init_dev();
|
|
1370
|
+
init_errors();
|
|
1371
|
+
|
|
1372
|
+
// ../core/dist/fetchSource.js
|
|
1373
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1374
|
+
init_errors();
|
|
1375
|
+
|
|
1376
|
+
// ../core/dist/fetchTarget.js
|
|
1377
|
+
init_errors();
|
|
1378
|
+
function pickRevision(revisions) {
|
|
1379
|
+
const active = revisions.find((r) => r.isActive && r.status === "READY");
|
|
1380
|
+
if (active)
|
|
1381
|
+
return { revisionNo: active.revisionNo, why: "active" };
|
|
1382
|
+
let best = null;
|
|
1383
|
+
for (const r of revisions) {
|
|
1384
|
+
if (r.status !== "READY")
|
|
1385
|
+
continue;
|
|
1386
|
+
if (best === null || r.revisionNo > best.revisionNo)
|
|
1387
|
+
best = r;
|
|
1388
|
+
}
|
|
1389
|
+
return best === null ? null : { revisionNo: best.revisionNo, why: "latest-ready" };
|
|
1390
|
+
}
|
|
1391
|
+
function noRevisionError(revisions) {
|
|
1392
|
+
if (revisions.length === 0) {
|
|
1393
|
+
return new DevtoolsError("NOT_A_SITE", "\uC544\uC9C1 \uC62C\uB9B0 \uC0AC\uC774\uD2B8 \uC18C\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", "\u300C\uC608\uC81C zip \uB2E4\uC6B4\uB85C\uB4DC\u300D\uB85C \uC608\uC81C\uB97C \uBC1B\uC544 \u300Czip \uC73C\uB85C \uC2DC\uC791\u300D\uC73C\uB85C \uD478\uC2DC\uAC70\uB098, \uC798\uCEE4\uB77C \uCF58\uC194\uC5D0\uC11C \uC18C\uC2A4\uB97C \uBA3C\uC800 \uC62C\uB824 \uC8FC\uC138\uC694.");
|
|
1394
|
+
}
|
|
1395
|
+
return new DevtoolsError("NOT_A_SITE", "\uBC1B\uC744 \uC218 \uC788\uB294 \uD310\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.", "\uC62C\uB9B0 \uD310\uC774 \uC544\uC9C1 \uB9CC\uB4E4\uC5B4\uC9C0\uB294 \uC911\uC774\uAC70\uB098 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \u300C\uBC84\uC804 \uC774\uB825\u300D\uC5D0\uC11C \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// ../core/dist/download.js
|
|
1399
|
+
init_errors();
|
|
1400
|
+
async function openBounded(url, options) {
|
|
1401
|
+
if (apiBaseUrl(url) === null) {
|
|
1402
|
+
throw new DevtoolsError("SERVER_REJECTED", `${options.what} \uC8FC\uC18C\uB97C \uC2E0\uB8B0\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`, "\uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694. \uBC1B\uC9C0 \uC54A\uACE0 \uBA48\uCDC4\uC2B5\uB2C8\uB2E4.");
|
|
1403
|
+
}
|
|
1404
|
+
const response = await options.fetchImpl(url, { signal: AbortSignal.timeout(options.timeoutMs) });
|
|
1405
|
+
if (!response.ok || !response.body) {
|
|
1406
|
+
throw new DevtoolsError("SERVER_REJECTED", `${options.what}\uB97C \uB0B4\uB824\uBC1B\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4(HTTP ${response.status}).`, "\uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.");
|
|
1407
|
+
}
|
|
1408
|
+
return response.body;
|
|
1409
|
+
}
|
|
1410
|
+
function tooBig(what, limit) {
|
|
1411
|
+
return new DevtoolsError("SERVER_REJECTED", `${what}\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4(\uC0C1\uD55C ${Math.round(limit / 1024 / 1024)}MB).`, "\uBC1B\uC740 \uAC83\uC774 \uC815\uC0C1\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694.");
|
|
1412
|
+
}
|
|
1413
|
+
async function downloadBounded(url, options) {
|
|
1414
|
+
const body = await openBounded(url, options);
|
|
1415
|
+
const limit = options.maxBytes ?? MAX_DOWNLOAD_BYTES;
|
|
1416
|
+
const chunks = [];
|
|
1417
|
+
let total = 0;
|
|
1418
|
+
for await (const chunk of body) {
|
|
1419
|
+
total += chunk.byteLength;
|
|
1420
|
+
if (total > limit) {
|
|
1421
|
+
await body.cancel().catch(() => {
|
|
1422
|
+
});
|
|
1423
|
+
throw tooBig(options.what, limit);
|
|
1424
|
+
}
|
|
1425
|
+
chunks.push(Buffer.from(chunk));
|
|
1426
|
+
}
|
|
1427
|
+
return Buffer.concat(chunks, total);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// ../core/dist/fetchSource.js
|
|
1431
|
+
var TRANSFER_TIMEOUT_MS2 = 15 * 60 * 1e3;
|
|
1432
|
+
async function fetchVerifiedSourceTar(options) {
|
|
1433
|
+
const report = options.onProgress ?? (() => {
|
|
1434
|
+
});
|
|
1435
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1436
|
+
let revisionNo = options.revisionNo;
|
|
1437
|
+
if (revisionNo === void 0) {
|
|
1438
|
+
const revisions = await options.api.listRevisions();
|
|
1439
|
+
const choice = pickRevision(revisions);
|
|
1440
|
+
if (!choice)
|
|
1441
|
+
throw noRevisionError(revisions);
|
|
1442
|
+
revisionNo = choice.revisionNo;
|
|
1443
|
+
}
|
|
1444
|
+
report(`\uBC84\uC804 ${revisionNo} \uC18C\uC2A4\uB97C \uBC1B\uB294 \uC911\u2026`);
|
|
1445
|
+
const source = await options.api.sourceUrl(revisionNo);
|
|
1446
|
+
const buffer = await downloadBounded(source.url, {
|
|
1447
|
+
fetchImpl,
|
|
1448
|
+
timeoutMs: TRANSFER_TIMEOUT_MS2,
|
|
1449
|
+
what: "\uC18C\uC2A4"
|
|
1450
|
+
});
|
|
1451
|
+
if (!source.sha256) {
|
|
1452
|
+
throw new DevtoolsError("SERVER_REJECTED", "\uC11C\uBC84\uAC00 \uBB34\uACB0\uC131 \uD574\uC2DC\uB97C \uC8FC\uC9C0 \uC54A\uC544 \uC18C\uC2A4\uB97C \uAC80\uC99D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.", "\uC798\uCEE4\uB77C\uC5D0 \uBB38\uC758\uD574 \uC8FC\uC138\uC694. \uAC80\uC99D \uC5C6\uC774 \uC9C4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
|
1453
|
+
}
|
|
1454
|
+
const actual = createHash3("sha256").update(buffer).digest("hex");
|
|
1455
|
+
if (actual !== source.sha256) {
|
|
1456
|
+
throw new DevtoolsError("SERVER_REJECTED", "\uBC1B\uC740 \uC18C\uC2A4\uAC00 \uC6D0\uBCF8\uACFC \uB2E4\uB985\uB2C8\uB2E4(\uBB34\uACB0\uC131 \uD655\uC778 \uC2E4\uD328).", "\uB124\uD2B8\uC6CC\uD06C \uBB38\uC81C\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.");
|
|
1457
|
+
}
|
|
1458
|
+
return { revisionNo, buffer, sha256: source.sha256 };
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// ../core/dist/unzip.js
|
|
1462
|
+
init_errors();
|
|
1463
|
+
import { inflateRaw } from "node:zlib";
|
|
1464
|
+
import { promisify as promisify2 } from "node:util";
|
|
1465
|
+
var inflate = promisify2(inflateRaw);
|
|
1466
|
+
|
|
1467
|
+
// ../core/dist/importZip.js
|
|
1468
|
+
init_errors();
|
|
1469
|
+
|
|
1470
|
+
// ../core/dist/build.js
|
|
1471
|
+
init_errors();
|
|
1472
|
+
var DEFAULT_TIMEOUT_MS2 = 10 * 60 * 1e3;
|
|
1473
|
+
|
|
1474
|
+
// ../core/dist/tenantScope.js
|
|
1475
|
+
init_notice();
|
|
1476
|
+
|
|
1477
|
+
// ../core/dist/upgradeNotice.js
|
|
1478
|
+
var UPGRADE_NOTICE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1479
|
+
|
|
1480
|
+
// ../core/dist/index.js
|
|
1481
|
+
init_notice();
|
|
1482
|
+
|
|
1483
|
+
// ../core/dist/presets.js
|
|
1484
|
+
init_errors();
|
|
1485
|
+
var TRANSFER_TIMEOUT_MS3 = 15 * 60 * 1e3;
|
|
1486
|
+
|
|
1487
|
+
// ../core/dist/precheck.js
|
|
1488
|
+
var HEAVY_BYTES = 5 * 1024 * 1024;
|
|
1489
|
+
|
|
1490
|
+
// ../core/dist/mcp.js
|
|
1491
|
+
init_errors();
|
|
1492
|
+
|
|
1493
|
+
// ../core/dist/agents.js
|
|
1494
|
+
var MARKER = "<!-- zalkera-devtools \uAC00 \uB9CC\uB4E0 \uC2A4\uD141\uC785\uB2C8\uB2E4. \uB0B4\uC6A9\uC744 \uC5EC\uAE30 \uC4F0\uC9C0 \uB9D0\uACE0 \uC815\uBCF8\uC744 \uACE0\uCE58\uC138\uC694. -->";
|
|
1495
|
+
var AGENTS_STUB = `${MARKER}
|
|
1496
|
+
# \uC774 \uC0AC\uC774\uD2B8 \uC18C\uC2A4\uC758 \uADDC\uC57D
|
|
1497
|
+
|
|
1498
|
+
\uADDC\uC57D\uC758 \uC815\uBCF8\uC740 \uC774 \uD30C\uC77C\uC774 \uC544\uB2C8\uB77C **\`@zalkera/client\` \uC5D0 \uB3D9\uBD09\uB41C \`llms.txt\`** \uC785\uB2C8\uB2E4.
|
|
1499
|
+
|
|
1500
|
+
- \uC124\uCE58\uB3FC \uC788\uC73C\uBA74: \`node_modules/@zalkera/client/llms.txt\`
|
|
1501
|
+
- \uB370\uC774\uD130\uB294 \uC11C\uBC84\uAC00 \uC815\uBCF8\uC785\uB2C8\uB2E4 \u2014 \uC0C1\uD488\xB7\uAE00\xB7\uC124\uC815 \uAC12\uC744 \uC18C\uC2A4\uC5D0 \uBCF5\uC0AC\uD558\uC9C0 \uB9C8\uC138\uC694.
|
|
1502
|
+
- \`.env.local\` \uC740 \uC790\uACA9\uC99D\uBA85\uC774 \uB4E4\uC5B4 \uC788\uC5B4 **\uCEE4\uBC0B\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.**
|
|
1503
|
+
`;
|
|
1504
|
+
var CLAUDE_POINTER = `${MARKER}
|
|
1505
|
+
@AGENTS.md
|
|
1506
|
+
`;
|
|
1507
|
+
|
|
1508
|
+
// ../core/dist/reentrancy.js
|
|
1509
|
+
var BUSY = Symbol("busy");
|
|
1510
|
+
|
|
1511
|
+
// ../core/dist/errorNotice.js
|
|
1512
|
+
init_errors();
|
|
1513
|
+
init_notice();
|
|
1514
|
+
|
|
1515
|
+
// ../core/dist/whyBlocked.js
|
|
1516
|
+
init_notice();
|
|
1517
|
+
|
|
1518
|
+
// ../core/dist/sidebarPlan.js
|
|
1519
|
+
init_notice();
|
|
1520
|
+
|
|
1521
|
+
// ../core/dist/pullPlan.js
|
|
1522
|
+
function planPull(input) {
|
|
1523
|
+
const { incoming, ledger, local } = input;
|
|
1524
|
+
const plan = { deletes: [], writes: [], unchanged: [], untracked: [], conflicts: [] };
|
|
1525
|
+
for (const [path, known] of Object.entries(ledger)) {
|
|
1526
|
+
const here = local[path];
|
|
1527
|
+
const next = incoming[path];
|
|
1528
|
+
const clean = here !== void 0 && here.sha256 === known.sha256;
|
|
1529
|
+
if (next === void 0) {
|
|
1530
|
+
if (here === void 0)
|
|
1531
|
+
continue;
|
|
1532
|
+
if (clean)
|
|
1533
|
+
plan.deletes.push(path);
|
|
1534
|
+
else
|
|
1535
|
+
plan.conflicts.push({ path, reason: "modified" });
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
if (here === void 0) {
|
|
1539
|
+
plan.conflicts.push({ path, reason: "deleted-locally" });
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
if (next.sha256 === known.sha256) {
|
|
1543
|
+
if (clean)
|
|
1544
|
+
plan.unchanged.push(path);
|
|
1545
|
+
else
|
|
1546
|
+
plan.conflicts.push({ path, reason: "modified" });
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
if (clean || here.sha256 === next.sha256)
|
|
1550
|
+
plan.writes.push(path);
|
|
1551
|
+
else
|
|
1552
|
+
plan.conflicts.push({ path, reason: "modified" });
|
|
1553
|
+
}
|
|
1554
|
+
for (const [path, next] of Object.entries(incoming)) {
|
|
1555
|
+
if (ledger[path] !== void 0)
|
|
1556
|
+
continue;
|
|
1557
|
+
const here = local[path];
|
|
1558
|
+
if (here === void 0) {
|
|
1559
|
+
plan.writes.push(path);
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
if (here.sha256 === next.sha256)
|
|
1563
|
+
plan.unchanged.push(path);
|
|
1564
|
+
else
|
|
1565
|
+
plan.conflicts.push({ path, reason: "added-locally" });
|
|
1566
|
+
}
|
|
1567
|
+
for (const path of Object.keys(local)) {
|
|
1568
|
+
if (ledger[path] === void 0 && incoming[path] === void 0)
|
|
1569
|
+
plan.untracked.push(path);
|
|
1570
|
+
}
|
|
1571
|
+
plan.deletes.sort();
|
|
1572
|
+
plan.writes.sort();
|
|
1573
|
+
plan.unchanged.sort();
|
|
1574
|
+
plan.untracked.sort();
|
|
1575
|
+
plan.conflicts.sort((a, b) => a.path.localeCompare(b.path));
|
|
1576
|
+
return plan;
|
|
1577
|
+
}
|
|
1578
|
+
function applyAfterDiscard(plan, incoming) {
|
|
1579
|
+
const writes = [...plan.writes];
|
|
1580
|
+
const deletes = [...plan.deletes];
|
|
1581
|
+
for (const conflict of plan.conflicts) {
|
|
1582
|
+
if (incoming[conflict.path] !== void 0)
|
|
1583
|
+
writes.push(conflict.path);
|
|
1584
|
+
else
|
|
1585
|
+
deletes.push(conflict.path);
|
|
1586
|
+
}
|
|
1587
|
+
return { writes: writes.sort(), deletes: deletes.sort() };
|
|
1588
|
+
}
|
|
1589
|
+
var PATH_LIST_CAP = 10;
|
|
1590
|
+
function trimPaths(paths, cap = PATH_LIST_CAP, all = false) {
|
|
1591
|
+
if (all || paths.length <= cap)
|
|
1592
|
+
return [...paths];
|
|
1593
|
+
return [...paths.slice(0, cap), `\uC678 ${paths.length - cap}\uAC1C`];
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// ../core/dist/pull.js
|
|
1597
|
+
import { lstat as lstat4, mkdir as mkdir3, readFile as readFile2, realpath as realpath2, rename as rename2, rmdir, stat, unlink, writeFile as writeFile4 } from "node:fs/promises";
|
|
1598
|
+
init_errors();
|
|
1599
|
+
import { basename as basename2, dirname, join as join7, relative as relative4, resolve as resolve3, sep as sep5 } from "node:path";
|
|
1600
|
+
|
|
1601
|
+
// ../core/dist/workdir.js
|
|
1602
|
+
init_errors();
|
|
1603
|
+
import { createReadStream } from "node:fs";
|
|
1604
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1605
|
+
import { lstat as lstat3, readdir } from "node:fs/promises";
|
|
1606
|
+
import { join as join6, sep as sep4 } from "node:path";
|
|
1607
|
+
async function hashWorkdir(root, options = {}) {
|
|
1608
|
+
const manifest = {};
|
|
1609
|
+
const cap = Math.min(options.maxEntries ?? MAX_ENTRIES, MAX_ENTRIES);
|
|
1610
|
+
let seen = 0;
|
|
1611
|
+
async function walk(dir, prefix) {
|
|
1612
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch((error) => {
|
|
1613
|
+
if (error?.code === "ENOENT")
|
|
1614
|
+
return [];
|
|
1615
|
+
throw error;
|
|
1616
|
+
});
|
|
1617
|
+
for (const item of entries) {
|
|
1618
|
+
const relative5 = prefix === "" ? item.name : `${prefix}/${item.name}`;
|
|
1619
|
+
if (isExcludedEntry(relative5))
|
|
1620
|
+
continue;
|
|
1621
|
+
if (item.isSymbolicLink())
|
|
1622
|
+
continue;
|
|
1623
|
+
if (item.isDirectory()) {
|
|
1624
|
+
await walk(join6(dir, item.name), relative5);
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (!item.isFile())
|
|
1628
|
+
continue;
|
|
1629
|
+
seen += 1;
|
|
1630
|
+
if (seen > cap) {
|
|
1631
|
+
throw new DevtoolsError("LOCAL_TOO_LARGE", `\uC774 \uD3F4\uB354\uC758 \uD30C\uC77C\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4(${cap}\uAC1C \uCD08\uACFC).`, "\uC0AC\uC774\uD2B8 \uD3F4\uB354\uAC00 \uC544\uB2CC \uACF3\uC744 \uAC00\uB9AC\uD0A4\uACE0 \uC788\uC9C0 \uC54A\uC740\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
1632
|
+
}
|
|
1633
|
+
manifest[relative5] = { sha256: await hashFile(join6(dir, item.name)) };
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
await walk(root, "");
|
|
1637
|
+
return manifest;
|
|
1638
|
+
}
|
|
1639
|
+
async function hashFile(path) {
|
|
1640
|
+
const hash = createHash4("sha256");
|
|
1641
|
+
for await (const chunk of createReadStream(path))
|
|
1642
|
+
hash.update(chunk);
|
|
1643
|
+
return hash.digest("hex");
|
|
1644
|
+
}
|
|
1645
|
+
async function resolveExisting(root, path) {
|
|
1646
|
+
if (path === "" || path.includes("\0"))
|
|
1647
|
+
return null;
|
|
1648
|
+
const segments = path.split("/").filter((s) => s !== "");
|
|
1649
|
+
if (segments.length === 0)
|
|
1650
|
+
return null;
|
|
1651
|
+
if (segments.some((s) => s === "." || s === ".." || s.includes(sep4)))
|
|
1652
|
+
return null;
|
|
1653
|
+
let current = root;
|
|
1654
|
+
for (const part of segments) {
|
|
1655
|
+
const next = join6(current, part);
|
|
1656
|
+
const info = await lstat3(next).catch(() => null);
|
|
1657
|
+
if (!info || info.isSymbolicLink())
|
|
1658
|
+
return null;
|
|
1659
|
+
current = next;
|
|
1660
|
+
}
|
|
1661
|
+
return current;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// ../core/dist/pull.js
|
|
1665
|
+
var SAVED_SUFFIX = ".zalkera-saved";
|
|
1666
|
+
async function pullSiteSource(options) {
|
|
1667
|
+
const report = options.onProgress ?? (() => {
|
|
1668
|
+
});
|
|
1669
|
+
const root = resolve3(options.folder);
|
|
1670
|
+
const found = await readLedger(root);
|
|
1671
|
+
const foreignLedger2 = found !== null && found.tenant !== options.api.tenantCode();
|
|
1672
|
+
const ledger = foreignLedger2 ? null : found;
|
|
1673
|
+
report("\uC0AC\uC774\uD2B8 \uC18C\uC2A4\uB97C \uBC1B\uB294 \uC911\uC785\uB2C8\uB2E4\u2026");
|
|
1674
|
+
const tar = await fetchVerifiedSourceTar({
|
|
1675
|
+
api: options.api,
|
|
1676
|
+
revisionNo: options.revisionNo,
|
|
1677
|
+
onProgress: report,
|
|
1678
|
+
fetchImpl: options.fetchImpl
|
|
1679
|
+
});
|
|
1680
|
+
report("\uBB34\uC5C7\uC774 \uB2EC\uB77C\uC84C\uB294\uC9C0 \uACAC\uC8FC\uB294 \uC911\uC785\uB2C8\uB2E4\u2026");
|
|
1681
|
+
const untarred = await gunzipTar(tar.buffer, MAX_EXTRACT_BYTES);
|
|
1682
|
+
const declared = await readTarManifest(untarred, { rejectVendored: true });
|
|
1683
|
+
const { incoming, dropped } = withoutExcluded(declared);
|
|
1684
|
+
const local = await hashWorkdir(root);
|
|
1685
|
+
requireNoFoldedCollision(incoming, local);
|
|
1686
|
+
const deletionsUnknown = ledger === null && Object.keys(local).length > 0;
|
|
1687
|
+
const plan = planPull({ incoming, ledger: ledger?.files ?? {}, local });
|
|
1688
|
+
let savedTo = null;
|
|
1689
|
+
let movedAway = [];
|
|
1690
|
+
if (plan.conflicts.length > 0) {
|
|
1691
|
+
if (!options.discardLocal)
|
|
1692
|
+
throw conflictRefusal(plan, options.listAll === true);
|
|
1693
|
+
const aside = await setAside(root, plan);
|
|
1694
|
+
savedTo = aside.folder;
|
|
1695
|
+
movedAway = aside.moved;
|
|
1696
|
+
report(`\uACE0\uCE5C \uB0B4\uC6A9\uC744 ${basename2(savedTo)} \uC5D0 \uC62E\uACA8 \uB450\uC5C8\uC2B5\uB2C8\uB2E4.`);
|
|
1697
|
+
}
|
|
1698
|
+
const todo = savedTo ? applyAfterDiscard(plan, incoming) : { writes: plan.writes, deletes: plan.deletes };
|
|
1699
|
+
await requireNoLinkedTargets(root, todo.writes);
|
|
1700
|
+
const moved = new Set(movedAway);
|
|
1701
|
+
const deleted = await applyDeletes(root, todo.deletes) + todo.deletes.filter((path) => moved.has(path)).length;
|
|
1702
|
+
const writes = new Set(todo.writes);
|
|
1703
|
+
await extractTar(untarred, root, {
|
|
1704
|
+
// 여기도 켠다 — 읽기 훑기가 이미 걸렀지만, 이 함수는 공개 API 라 두 경로가 같은 판정을
|
|
1705
|
+
// 지나야 한다. 실제로 걸리는 자리는 위쪽이다.
|
|
1706
|
+
rejectVendored: true,
|
|
1707
|
+
decide: (path) => writes.has(path) ? "replace" : "skip"
|
|
1708
|
+
});
|
|
1709
|
+
const generation = await currentGeneration(options.api);
|
|
1710
|
+
const next = {
|
|
1711
|
+
format: SYNC_LEDGER_FORMAT,
|
|
1712
|
+
tenant: options.api.tenantCode(),
|
|
1713
|
+
base: { revisionNo: tar.revisionNo, tarSha256: tar.sha256 },
|
|
1714
|
+
files: Object.fromEntries(Object.entries(incoming).map(([path, entry]) => [path, { sha256: entry.sha256, bytes: entry.bytes }])),
|
|
1715
|
+
server: generation === null ? null : { generation },
|
|
1716
|
+
mine: {},
|
|
1717
|
+
pulledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1718
|
+
pushedAt: ledger?.pushedAt ?? null
|
|
1719
|
+
};
|
|
1720
|
+
const ledgerWritten = await writeLedger(root, next);
|
|
1721
|
+
await writeSourceMarkTo(root, {
|
|
1722
|
+
tenant: options.api.tenantCode(),
|
|
1723
|
+
revisionNo: tar.revisionNo,
|
|
1724
|
+
sha256: tar.sha256,
|
|
1725
|
+
fetchedAt: next.pulledAt
|
|
1726
|
+
}).catch(() => void 0);
|
|
1727
|
+
return {
|
|
1728
|
+
revisionNo: tar.revisionNo,
|
|
1729
|
+
written: todo.writes.length,
|
|
1730
|
+
deleted,
|
|
1731
|
+
unchanged: plan.unchanged.length,
|
|
1732
|
+
untracked: plan.untracked.length,
|
|
1733
|
+
savedTo,
|
|
1734
|
+
serverExcluded: dropped,
|
|
1735
|
+
foreignLedger: foreignLedger2,
|
|
1736
|
+
deletionsUnknown,
|
|
1737
|
+
ledgerWritten
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
function requireNoFoldedCollision(incoming, local) {
|
|
1741
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1742
|
+
for (const path of Object.keys(incoming)) {
|
|
1743
|
+
const folded = path.toLowerCase();
|
|
1744
|
+
const first = seen.get(folded);
|
|
1745
|
+
if (first !== void 0) {
|
|
1746
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uC0AC\uC774\uD2B8\uAC00 \uBCF4\uB0B8 \uD30C\uC77C \uC911 \uC774\uB984\uC774 \uB300\uC18C\uBB38\uC790\uB9CC \uB2E4\uB978 \uAC83\uC774 \uC788\uC2B5\uB2C8\uB2E4: ${first} \xB7 ${path}`, "\uCEF4\uD4E8\uD130\uC5D0 \uB530\uB77C \uAC19\uC740 \uD30C\uC77C\uB85C \uCDE8\uAE09\uB3FC \uD55C\uCABD\uC774 \uC0AC\uB77C\uC9D1\uB2C8\uB2E4. \uC544\uBB34\uAC83\uB3C4 \uBC1B\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 \uC798\uCEE4\uB77C\uC5D0 \uC54C\uB824 \uC8FC\uC138\uC694.");
|
|
1747
|
+
}
|
|
1748
|
+
seen.set(folded, path);
|
|
1749
|
+
}
|
|
1750
|
+
for (const path of Object.keys(local)) {
|
|
1751
|
+
const other = seen.get(path.toLowerCase());
|
|
1752
|
+
if (other === void 0 || other === path)
|
|
1753
|
+
continue;
|
|
1754
|
+
throw new DevtoolsError("PULL_WOULD_OVERWRITE", `\uC774 \uD3F4\uB354\uC758 ${path} \uC640 \uC0AC\uC774\uD2B8\uC758 ${other} \uB294 \uC774\uB984\uC774 \uB300\uC18C\uBB38\uC790\uB9CC \uB2E4\uB985\uB2C8\uB2E4. \uC544\uBB34\uAC83\uB3C4 \uBC1B\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`, "\uCEF4\uD4E8\uD130\uC5D0 \uB530\uB77C \uAC19\uC740 \uD30C\uC77C\uB85C \uCDE8\uAE09\uB3FC \uD55C\uCABD\uC774 \uC0AC\uB77C\uC9D1\uB2C8\uB2E4. \uC774 \uD3F4\uB354\uC758 \uD30C\uC77C \uC774\uB984\uC744 \uBC14\uAFB8\uAC70\uB098 \uC62E\uACA8 \uB450\uACE0 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.");
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
async function requireNoLinkedTargets(root, paths) {
|
|
1758
|
+
const checked = /* @__PURE__ */ new Set();
|
|
1759
|
+
for (const path of paths) {
|
|
1760
|
+
const segments = path.split("/").filter((s) => s !== "");
|
|
1761
|
+
let current = root;
|
|
1762
|
+
for (const part of segments) {
|
|
1763
|
+
current = join7(current, part);
|
|
1764
|
+
if (checked.has(current))
|
|
1765
|
+
continue;
|
|
1766
|
+
checked.add(current);
|
|
1767
|
+
const info = await lstat4(current).catch(() => null);
|
|
1768
|
+
if (!info?.isSymbolicLink())
|
|
1769
|
+
continue;
|
|
1770
|
+
throw new DevtoolsError("PULL_WOULD_OVERWRITE", `\uC774 \uD3F4\uB354\uC758 ${relative4(root, current).split(sep5).join("/")} \uAC00 \uB2E4\uB978 \uACF3\uC744 \uAC00\uB9AC\uD0A4\uB294 \uBC14\uB85C\uAC00\uAE30\uC785\uB2C8\uB2E4. \uC544\uBB34\uAC83\uB3C4 \uBC1B\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`, "\uADF8 \uBC14\uB85C\uAC00\uAE30\uB97C \uC9C0\uC6B0\uACE0 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694. \uADF8\uB300\uB85C \uB450\uBA74 \uC774 \uD3F4\uB354 \uBC16\uC758 \uAC83\uC744 \uB36E\uC5B4\uC4F0\uAC8C \uB429\uB2C8\uB2E4.");
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
function withoutExcluded(declared) {
|
|
1775
|
+
const incoming = {};
|
|
1776
|
+
const dropped = [];
|
|
1777
|
+
for (const [path, entry] of Object.entries(declared)) {
|
|
1778
|
+
if (isExcludedEntry(path))
|
|
1779
|
+
dropped.push(path);
|
|
1780
|
+
else
|
|
1781
|
+
incoming[path] = entry;
|
|
1782
|
+
}
|
|
1783
|
+
return { incoming, dropped: dropped.sort() };
|
|
1784
|
+
}
|
|
1785
|
+
async function readLedger(root) {
|
|
1786
|
+
const text = await readFile2(join7(root, SYNC_LEDGER_PATH), "utf8").catch(() => null);
|
|
1787
|
+
return text === null ? null : parseSyncLedger(text);
|
|
1788
|
+
}
|
|
1789
|
+
async function writeLedger(root, ledger) {
|
|
1790
|
+
try {
|
|
1791
|
+
const dir = await ensureOwnDir(root, ...SYNC_LEDGER_PATH.split("/").slice(0, -1));
|
|
1792
|
+
await writeOwnFile(join7(dir, basename2(SYNC_LEDGER_PATH)), serializeSyncLedger(ledger));
|
|
1793
|
+
} catch {
|
|
1794
|
+
return false;
|
|
1795
|
+
}
|
|
1796
|
+
await ignoreLedger(root).catch(() => {
|
|
1797
|
+
});
|
|
1798
|
+
return true;
|
|
1799
|
+
}
|
|
1800
|
+
async function ignoreLedger(root) {
|
|
1801
|
+
const info = join7(root, ".git", "info");
|
|
1802
|
+
const exists = await stat(join7(root, ".git")).then((s) => s.isDirectory()).catch(() => false);
|
|
1803
|
+
if (!exists)
|
|
1804
|
+
return;
|
|
1805
|
+
const path = join7(info, "exclude");
|
|
1806
|
+
const current = await readFile2(path, "utf8").catch(() => "");
|
|
1807
|
+
if (current.split(/\r?\n/).some((line) => line.trim() === SYNC_LEDGER_PATH))
|
|
1808
|
+
return;
|
|
1809
|
+
await mkdir3(info, { recursive: true });
|
|
1810
|
+
const prefix = current === "" || current.endsWith("\n") ? "" : "\n";
|
|
1811
|
+
await writeFile4(path, `${current}${prefix}${SYNC_LEDGER_PATH}
|
|
1812
|
+
`);
|
|
1813
|
+
}
|
|
1814
|
+
async function currentGeneration(api) {
|
|
1815
|
+
return await api.draftFiles().then((state) => state.generation ?? null).catch(() => null);
|
|
1816
|
+
}
|
|
1817
|
+
async function applyDeletes(root, paths) {
|
|
1818
|
+
let count2 = 0;
|
|
1819
|
+
const parents = /* @__PURE__ */ new Set();
|
|
1820
|
+
for (const path of paths) {
|
|
1821
|
+
const target = await resolveExisting(root, path);
|
|
1822
|
+
if (!target)
|
|
1823
|
+
continue;
|
|
1824
|
+
await unlink(target).catch(() => {
|
|
1825
|
+
});
|
|
1826
|
+
count2 += 1;
|
|
1827
|
+
let dir = dirname(target);
|
|
1828
|
+
while (dir.length > root.length && dir.startsWith(root)) {
|
|
1829
|
+
parents.add(dir);
|
|
1830
|
+
dir = dirname(dir);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
for (const dir of [...parents].sort((a, b) => b.length - a.length)) {
|
|
1834
|
+
await rmdir(dir).catch(() => {
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
return count2;
|
|
1838
|
+
}
|
|
1839
|
+
async function setAside(root, plan) {
|
|
1840
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1841
|
+
const real = await realpath2(root).catch(() => root);
|
|
1842
|
+
const saved = join7(dirname(real), `${basename2(real)}${SAVED_SUFFIX}`, stamp);
|
|
1843
|
+
const moved = [];
|
|
1844
|
+
for (const conflict of plan.conflicts) {
|
|
1845
|
+
const from = await resolveExisting(root, conflict.path);
|
|
1846
|
+
if (!from)
|
|
1847
|
+
continue;
|
|
1848
|
+
const to = join7(saved, conflict.path);
|
|
1849
|
+
await mkdir3(dirname(to), { recursive: true });
|
|
1850
|
+
await rename2(from, to);
|
|
1851
|
+
moved.push(conflict.path);
|
|
1852
|
+
}
|
|
1853
|
+
return { folder: saved, moved };
|
|
1854
|
+
}
|
|
1855
|
+
function conflictRefusal(plan, all) {
|
|
1856
|
+
const paths = plan.conflicts.map((c) => c.path);
|
|
1857
|
+
const list = trimPaths(paths, PATH_LIST_CAP, all).map((p) => ` \xB7 ${p}`).join("\n");
|
|
1858
|
+
return new DevtoolsError(
|
|
1859
|
+
"PULL_WOULD_OVERWRITE",
|
|
1860
|
+
`\uC774 \uD3F4\uB354\uC5D0\uC11C \uACE0\uCE5C \uAC83\uC774 ${paths.length}\uAC1C \uC788\uC5B4 \uC544\uBB34\uAC83\uB3C4 \uBC1B\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
|
|
1861
|
+
${list}`,
|
|
1862
|
+
// ⚠ **없는 명령을 대지 않는다.** 여기 적힌 둘은 실제로 있는 동사다 — 없는 것을 대면 작업을
|
|
1863
|
+
// 지키는 쪽이 죽은 문이 되고, 남는 유일한 길이 「내 작업을 밖으로 옮기는 것」이 된다.
|
|
1864
|
+
"\uACE0\uCE5C \uB0B4\uC6A9\uC744 \uBA3C\uC800 \uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uC62C\uB9AC\uB824\uBA74 `zalkera push`, \uBC84\uB9AC\uACE0 \uC0AC\uC774\uD2B8 \uAC83\uC73C\uB85C \uBC1B\uC73C\uB824\uBA74 `zalkera pull --discard-local` \uC744 \uC2E4\uD589\uD558\uC138\uC694. \uBC84\uB9AC\uB294 \uCABD\uC744 \uACE8\uB77C\uB3C4 \uACE0\uCE5C \uD30C\uC77C\uC740 \uC606 \uD3F4\uB354\uC5D0 \uC62E\uACA8 \uB461\uB2C8\uB2E4."
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// ../core/dist/baseline.js
|
|
1869
|
+
init_errors();
|
|
1870
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1871
|
+
import { join as join8, resolve as resolve4 } from "node:path";
|
|
1872
|
+
async function rebuildBaseline(options) {
|
|
1873
|
+
const report = options.onProgress ?? (() => {
|
|
1874
|
+
});
|
|
1875
|
+
const root = resolve4(options.folder);
|
|
1876
|
+
const previousText = await readFile3(join8(root, SYNC_LEDGER_PATH), "utf8").catch(() => null);
|
|
1877
|
+
const previous = previousText === null ? null : parseSyncLedger(previousText);
|
|
1878
|
+
report("\uD310\uC758 \uD30C\uC77C \uBAA9\uB85D\uC744 \uB2E4\uC2DC \uC77D\uB294 \uC911\uC785\uB2C8\uB2E4\u2026");
|
|
1879
|
+
const tar = await fetchVerifiedSourceTar({
|
|
1880
|
+
api: options.api,
|
|
1881
|
+
revisionNo: options.revisionNo,
|
|
1882
|
+
onProgress: report,
|
|
1883
|
+
fetchImpl: options.fetchImpl
|
|
1884
|
+
});
|
|
1885
|
+
const { incoming: manifest, dropped } = withoutExcluded(await readTarGzManifest(tar.buffer, { maxBytes: MAX_EXTRACT_BYTES }));
|
|
1886
|
+
const generation = await options.api.draftFiles().then((state) => state.generation ?? null).catch(() => null);
|
|
1887
|
+
const ledger = {
|
|
1888
|
+
format: SYNC_LEDGER_FORMAT,
|
|
1889
|
+
tenant: options.api.tenantCode(),
|
|
1890
|
+
base: { revisionNo: tar.revisionNo, tarSha256: tar.sha256 },
|
|
1891
|
+
files: Object.fromEntries(Object.entries(manifest).map(([path, entry]) => [path, { sha256: entry.sha256, bytes: entry.bytes }])),
|
|
1892
|
+
server: generation === null ? null : { generation },
|
|
1893
|
+
// ⚠ **`mine` 은 비운다.** 옛 장부의 `mine` 을 살리면 「내가 올린 것」이라는 주장이 근거 없이
|
|
1894
|
+
// 되살아난다 — 그 주장이 좌초 안내와 응답 유실 화해의 입력이다.
|
|
1895
|
+
mine: {},
|
|
1896
|
+
pulledAt: previous?.pulledAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1897
|
+
pushedAt: previous?.pushedAt ?? null
|
|
1898
|
+
};
|
|
1899
|
+
await writeSourceMarkTo(root, {
|
|
1900
|
+
tenant: options.api.tenantCode(),
|
|
1901
|
+
revisionNo: tar.revisionNo,
|
|
1902
|
+
sha256: tar.sha256,
|
|
1903
|
+
fetchedAt: ledger.pulledAt
|
|
1904
|
+
}).catch(() => void 0);
|
|
1905
|
+
const written = await writeLedger(root, ledger);
|
|
1906
|
+
if (!written) {
|
|
1907
|
+
throw new DevtoolsError("LEDGER_UNKNOWN", "\uAE30\uC900 \uAE30\uB85D\uC744 \uC4F0\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", "\uD3F4\uB354\uC758 `.zalkera` \uC790\uB9AC\uAC00 \uB9C1\uD06C\uC774\uAC70\uB098 \uC4F8 \uC218 \uC5C6\uB294 \uC0C1\uD0DC\uC778\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
1908
|
+
}
|
|
1909
|
+
const local = await hashWorkdir(root);
|
|
1910
|
+
const differing = Object.keys(manifest).filter((path) => local[path]?.sha256 !== manifest[path]?.sha256).concat(Object.keys(local).filter((path) => manifest[path] === void 0)).sort();
|
|
1911
|
+
return {
|
|
1912
|
+
revisionNo: tar.revisionNo,
|
|
1913
|
+
files: Object.keys(manifest).length,
|
|
1914
|
+
replaced: previous !== null,
|
|
1915
|
+
differing,
|
|
1916
|
+
serverExcluded: dropped
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// ../core/dist/syncStatus.js
|
|
1921
|
+
function syncStatus(input) {
|
|
1922
|
+
const { ledger, local, draft, activeRevisionNo: activeRevisionNo2 } = input;
|
|
1923
|
+
const blockers = [];
|
|
1924
|
+
if (!ledger)
|
|
1925
|
+
blockers.push("LEDGER_UNKNOWN");
|
|
1926
|
+
if (!draft)
|
|
1927
|
+
blockers.push("SERVER_UNREADABLE");
|
|
1928
|
+
if (draft?.strandedOnOldRevision)
|
|
1929
|
+
blockers.push("STRANDED");
|
|
1930
|
+
const base = ledger?.files ?? {};
|
|
1931
|
+
const changed = [];
|
|
1932
|
+
const removed = [];
|
|
1933
|
+
const added = [];
|
|
1934
|
+
for (const [path, known] of Object.entries(base)) {
|
|
1935
|
+
const here = local[path];
|
|
1936
|
+
if (here === void 0)
|
|
1937
|
+
removed.push(path);
|
|
1938
|
+
else if (here.sha256 !== known.sha256)
|
|
1939
|
+
changed.push(path);
|
|
1940
|
+
}
|
|
1941
|
+
for (const path of Object.keys(local)) {
|
|
1942
|
+
if (base[path] === void 0)
|
|
1943
|
+
added.push(path);
|
|
1944
|
+
}
|
|
1945
|
+
const seen = ledger?.server?.generation ?? null;
|
|
1946
|
+
const now = draft?.generation ?? null;
|
|
1947
|
+
const mineValid = draft !== null && seen !== null && seen === now;
|
|
1948
|
+
const baseRevisionNo = ledger?.base.revisionNo ?? null;
|
|
1949
|
+
return {
|
|
1950
|
+
tenant: ledger?.tenant ?? null,
|
|
1951
|
+
baseRevisionNo,
|
|
1952
|
+
activeRevisionNo: activeRevisionNo2,
|
|
1953
|
+
behind: baseRevisionNo !== null && activeRevisionNo2 !== null && activeRevisionNo2 > baseRevisionNo,
|
|
1954
|
+
changed: changed.sort(),
|
|
1955
|
+
removed: removed.sort(),
|
|
1956
|
+
added: added.sort(),
|
|
1957
|
+
draftPaths: (draft?.changed.length ?? 0) + (draft?.deleted.length ?? 0),
|
|
1958
|
+
mineValid,
|
|
1959
|
+
blockers
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
function ledgerCorrection(ledger, draft) {
|
|
1963
|
+
if (!ledger || !draft)
|
|
1964
|
+
return null;
|
|
1965
|
+
const seen = ledger.server?.generation ?? null;
|
|
1966
|
+
if (seen === (draft.generation ?? null))
|
|
1967
|
+
return null;
|
|
1968
|
+
if (seen === null && Object.keys(ledger.mine).length === 0)
|
|
1969
|
+
return null;
|
|
1970
|
+
return { ...ledger, server: null, mine: {} };
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// ../core/dist/push.js
|
|
1974
|
+
init_errors();
|
|
1975
|
+
init_notice();
|
|
1976
|
+
import { readFile as readFile4, stat as stat2 } from "node:fs/promises";
|
|
1977
|
+
import { basename as basename3, join as join9, resolve as resolve5 } from "node:path";
|
|
1978
|
+
|
|
1979
|
+
// ../core/dist/pushPlan.js
|
|
1980
|
+
function effectiveSha(base, draft, path) {
|
|
1981
|
+
if (draft.deleted.includes(path))
|
|
1982
|
+
return null;
|
|
1983
|
+
const written = draft.changed.find((row) => row.path === path);
|
|
1984
|
+
if (written)
|
|
1985
|
+
return written.sha256;
|
|
1986
|
+
return base[path]?.sha256 ?? null;
|
|
1987
|
+
}
|
|
1988
|
+
function planPush(input) {
|
|
1989
|
+
const { base, draft, local } = input;
|
|
1990
|
+
const mine = input.mine ?? {};
|
|
1991
|
+
const edits = [];
|
|
1992
|
+
const unseen = [];
|
|
1993
|
+
const paths = /* @__PURE__ */ new Set([
|
|
1994
|
+
...Object.keys(base),
|
|
1995
|
+
...draft.changed.map((row) => row.path),
|
|
1996
|
+
...draft.deleted,
|
|
1997
|
+
...Object.keys(local)
|
|
1998
|
+
]);
|
|
1999
|
+
for (const path of [...paths].sort()) {
|
|
2000
|
+
const now = effectiveSha(base, draft, path);
|
|
2001
|
+
const here = local[path]?.sha256 ?? null;
|
|
2002
|
+
if (here === now)
|
|
2003
|
+
continue;
|
|
2004
|
+
if (here === null) {
|
|
2005
|
+
edits.push({ path, sha256: null, baseSha256: now });
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
edits.push({ path, sha256: here, baseSha256: now });
|
|
2009
|
+
}
|
|
2010
|
+
for (const path of paths) {
|
|
2011
|
+
const touched = draft.changed.some((row) => row.path === path) || draft.deleted.includes(path);
|
|
2012
|
+
if (!touched)
|
|
2013
|
+
continue;
|
|
2014
|
+
const now = effectiveSha(base, draft, path);
|
|
2015
|
+
const here = local[path]?.sha256 ?? null;
|
|
2016
|
+
if (here === now)
|
|
2017
|
+
continue;
|
|
2018
|
+
if (Object.hasOwn(mine, path) && mine[path] === now)
|
|
2019
|
+
continue;
|
|
2020
|
+
unseen.push(path);
|
|
2021
|
+
}
|
|
2022
|
+
return { edits, unseen: unseen.sort() };
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// ../core/dist/push.js
|
|
2026
|
+
var MAX_PUSH_ENTRIES = 500;
|
|
2027
|
+
var MAX_PUSH_ATTEMPTS = 3;
|
|
2028
|
+
var PATH_NOT_ACCEPTED = "SOURCE_PATH_NOT_ACCEPTED";
|
|
2029
|
+
var SHAPE_STALE = "SOURCE_EDIT_SHAPE_STALE";
|
|
2030
|
+
var MAX_PREVIEW_URL = 2048;
|
|
2031
|
+
var MAX_WARNING = 300;
|
|
2032
|
+
var MAX_PUSH_FILE_BYTES = 1024 * 1024;
|
|
2033
|
+
var MAX_PUSH_BYTES = 3.5 * 1024 * 1024;
|
|
2034
|
+
async function pushSiteSource(options) {
|
|
2035
|
+
const report = options.onProgress ?? (() => {
|
|
2036
|
+
});
|
|
2037
|
+
const root = resolve5(options.folder);
|
|
2038
|
+
const ledger = await readLedger(root);
|
|
2039
|
+
if (!ledger)
|
|
2040
|
+
throw ledgerUnknown();
|
|
2041
|
+
if (ledger.tenant !== options.api.tenantCode())
|
|
2042
|
+
throw foreignLedger(ledger.tenant);
|
|
2043
|
+
report("\uC0AC\uC774\uD2B8 \uCABD \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uB294 \uC911\uC785\uB2C8\uB2E4\u2026");
|
|
2044
|
+
let draft = await readDraft(options.api);
|
|
2045
|
+
const rows = await revisionRows(options.api);
|
|
2046
|
+
const active = activeOf(rows);
|
|
2047
|
+
const reconciled = reconcile(ledger, draft);
|
|
2048
|
+
const local = await hashWorkdir(root);
|
|
2049
|
+
const owned = ownedBy(ledger, draft);
|
|
2050
|
+
if (active !== ledger.base.revisionNo) {
|
|
2051
|
+
const notYetLive = active < ledger.base.revisionNo;
|
|
2052
|
+
const mineRow = notYetLive ? rows.find((row) => row.revisionNo === ledger.base.revisionNo) : void 0;
|
|
2053
|
+
if (mineRow?.status === "BUILDING") {
|
|
2054
|
+
throw new DevtoolsError("PUSH_BASE_BUILDING", `${ledger.base.revisionNo}\uD310\uC744 \uC9D3\uB294 \uC911\uC774\uB77C \uC544\uC9C1 \uC62C\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`, "\uBE4C\uB4DC\uAC00 \uB05D\uB098\uBA74 \uADF8 \uBC84\uC804\uC774 \uCF1C\uC9D1\uB2C8\uB2E4 \u2014 `zalkera status` \uB85C \uD655\uC778\uD55C \uB4A4 \uB2E4\uC2DC \uC62C\uB824 \uC8FC\uC138\uC694. \uC774 \uD3F4\uB354\uB294 \uADF8\uB300\uB85C \uB450\uC154\uB3C4 \uB429\uB2C8\uB2E4.");
|
|
2055
|
+
}
|
|
2056
|
+
if (mineRow?.status === "FAILED") {
|
|
2057
|
+
throw new DevtoolsError("PUSH_BASE_BUILD_FAILED", `${ledger.base.revisionNo}\uD310\uC740 \uC9D3\uB2E4\uAC00 \uC2E4\uD328\uD574 \uCF1C\uC9C0\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 \u2014 \uC9C0\uAE08 \uCF1C\uC838 \uC788\uB294 \uAC83\uC740 ${active}\uD310\uC785\uB2C8\uB2E4.`, "`zalkera status` \uB85C \uC2E4\uD328 \uC0AC\uC720\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694. \uACE0\uCCD0\uC11C \uB2E4\uC2DC \uC62C\uB9AC\uB824\uBA74 `zalkera baseline` \uC73C\uB85C \uAE30\uC900\uC744 \uC9C0\uAE08 \uCF1C\uC9C4 \uBC84\uC804\uC73C\uB85C \uB9DE\uCD98 \uB4A4 \uC791\uC5C5\uD574 \uC8FC\uC138\uC694.");
|
|
2058
|
+
}
|
|
2059
|
+
const dirty = Object.keys(ledger.files).filter((path) => local[path]?.sha256 !== ledger.files[path]?.sha256).length;
|
|
2060
|
+
throw baseMoved(ledger.base.revisionNo, active, dirty);
|
|
2061
|
+
}
|
|
2062
|
+
let plan = planPush({ base: ledger.files, draft: viewOf(draft), local, mine: owned });
|
|
2063
|
+
requireSeen(plan, options);
|
|
2064
|
+
if (plan.edits.length === 0) {
|
|
2065
|
+
const written2 = await writeLedger(root, {
|
|
2066
|
+
...ledger,
|
|
2067
|
+
server: draft.generation === null ? null : { generation: draft.generation },
|
|
2068
|
+
mine: {}
|
|
2069
|
+
});
|
|
2070
|
+
return {
|
|
2071
|
+
sent: 0,
|
|
2072
|
+
removed: 0,
|
|
2073
|
+
generation: draft.generation,
|
|
2074
|
+
droppedByServer: [],
|
|
2075
|
+
retriedAfterConflict: false,
|
|
2076
|
+
reconciled,
|
|
2077
|
+
previewUrl: null,
|
|
2078
|
+
warning: written2 ? null : ledgerWriteFailed(0)
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
requireWithinLimits(plan.edits);
|
|
2082
|
+
report(`${plan.edits.length}\uAC1C\uB97C \uC62C\uB9AC\uB294 \uC911\uC785\uB2C8\uB2E4\u2026`);
|
|
2083
|
+
const dropped = [];
|
|
2084
|
+
let retried = false;
|
|
2085
|
+
let sent = null;
|
|
2086
|
+
for (let round = 0; round < MAX_PUSH_ATTEMPTS; round += 1) {
|
|
2087
|
+
requireWithinLimits(plan.edits);
|
|
2088
|
+
sent = await attempt(options.api, root, plan.edits, report);
|
|
2089
|
+
if (sent.kind === "ok")
|
|
2090
|
+
break;
|
|
2091
|
+
if (sent.kind === "lost") {
|
|
2092
|
+
const attempted = {};
|
|
2093
|
+
for (const edit of plan.edits)
|
|
2094
|
+
attempted[edit.path] = edit.sha256;
|
|
2095
|
+
const noted = await writeLedger(root, { ...ledger, server: null, mine: attempted });
|
|
2096
|
+
throw responseLost(sent.error, noted);
|
|
2097
|
+
}
|
|
2098
|
+
if (round === MAX_PUSH_ATTEMPTS - 1)
|
|
2099
|
+
throw sent.error;
|
|
2100
|
+
if (sent.kind === "excluded") {
|
|
2101
|
+
const named = sent.rejectedPaths.filter((path) => plan.edits.some((edit) => edit.path === path));
|
|
2102
|
+
if (named.length === 0)
|
|
2103
|
+
throw sent.error;
|
|
2104
|
+
dropped.push(...named);
|
|
2105
|
+
const kept = plan.edits.filter((edit) => !named.includes(edit.path));
|
|
2106
|
+
if (kept.length === 0)
|
|
2107
|
+
throw sent.error;
|
|
2108
|
+
report(`\uC0AC\uC774\uD2B8\uAC00 \uBC1B\uC9C0 \uC54A\uB294 \uACBD\uB85C ${named.length}\uAC1C\uB97C \uBE7C\uACE0 \uB2E4\uC2DC \uBCF4\uB0C5\uB2C8\uB2E4\u2026`);
|
|
2109
|
+
plan = { ...plan, edits: kept };
|
|
2110
|
+
continue;
|
|
2111
|
+
}
|
|
2112
|
+
if (sent.kind === "conflict") {
|
|
2113
|
+
report("\uC0AC\uC774\uD2B8 \uCABD\uC774 \uADF8 \uC0AC\uC774 \uB2EC\uB77C\uC838 \uB2E4\uC2DC \uC77D\uC2B5\uB2C8\uB2E4\u2026");
|
|
2114
|
+
draft = await readDraft(options.api);
|
|
2115
|
+
const fresh = await hashWorkdir(root);
|
|
2116
|
+
plan = planPush({ base: ledger.files, draft: viewOf(draft), local: fresh, mine: ownedBy(ledger, draft) });
|
|
2117
|
+
requireSeen(plan, options);
|
|
2118
|
+
plan = { ...plan, edits: plan.edits.filter((edit) => !dropped.includes(edit.path)) };
|
|
2119
|
+
if (plan.edits.length === 0) {
|
|
2120
|
+
const written2 = await writeLedger(root, {
|
|
2121
|
+
...ledger,
|
|
2122
|
+
server: draft.generation === null ? null : { generation: draft.generation },
|
|
2123
|
+
mine: {}
|
|
2124
|
+
});
|
|
2125
|
+
return {
|
|
2126
|
+
sent: 0,
|
|
2127
|
+
removed: 0,
|
|
2128
|
+
generation: draft.generation,
|
|
2129
|
+
droppedByServer: dropped,
|
|
2130
|
+
// 두 번째 요청이 안 나갔다 — 재계산은 했지만 **다시 보내지는 않았다.**
|
|
2131
|
+
retriedAfterConflict: false,
|
|
2132
|
+
reconciled,
|
|
2133
|
+
previewUrl: null,
|
|
2134
|
+
warning: written2 ? null : ledgerWriteFailed(0)
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
retried = true;
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
throw sent.error;
|
|
2141
|
+
}
|
|
2142
|
+
if (sent === null || sent.kind !== "ok")
|
|
2143
|
+
throw sent?.error ?? unreachable();
|
|
2144
|
+
const mine = {};
|
|
2145
|
+
for (const edit of plan.edits)
|
|
2146
|
+
mine[edit.path] = edit.sha256;
|
|
2147
|
+
const written = await writeLedger(root, {
|
|
2148
|
+
...ledger,
|
|
2149
|
+
server: sent.generation === null ? null : { generation: sent.generation },
|
|
2150
|
+
mine,
|
|
2151
|
+
pushedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2152
|
+
});
|
|
2153
|
+
return {
|
|
2154
|
+
sent: plan.edits.length,
|
|
2155
|
+
removed: plan.edits.filter((edit) => edit.sha256 === null).length,
|
|
2156
|
+
generation: sent.generation,
|
|
2157
|
+
droppedByServer: dropped,
|
|
2158
|
+
retriedAfterConflict: retried,
|
|
2159
|
+
reconciled,
|
|
2160
|
+
previewUrl: sent.previewUrl,
|
|
2161
|
+
warning: written ? sent.warning : ledgerWriteFailed(plan.edits.length)
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
var ledgerWriteFailed = (sent) => sent > 0 ? "\uC62C\uB9B0 \uAC83\uC740 \uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uBC18\uC601\uB410\uC9C0\uB9CC \uC774 \uD3F4\uB354\uC758 \uAE30\uC900 \uAE30\uB85D\uC744 \uC4F0\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. `zalkera baseline` \uC744 \uD55C \uBC88 \uC2E4\uD589\uD574 \uC8FC\uC138\uC694." : "\uC774 \uD3F4\uB354\uC758 \uAE30\uC900 \uAE30\uB85D\uC744 \uC4F0\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. `zalkera baseline` \uC744 \uD55C \uBC88 \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.";
|
|
2165
|
+
async function readDraft(api) {
|
|
2166
|
+
const draft = await api.draftFiles().catch(() => null);
|
|
2167
|
+
if (!draft) {
|
|
2168
|
+
throw new DevtoolsError("SERVER_UNREADABLE_DRAFT", "\uC9C0\uAE08 \uC0AC\uC774\uD2B8 \uCABD \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC9C0 \uBABB\uD574 \uC544\uBB34\uAC83\uB3C4 \uC62C\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.", "\uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.");
|
|
2169
|
+
}
|
|
2170
|
+
if (draft.strandedOnOldRevision) {
|
|
2171
|
+
throw new DevtoolsError("STRANDED_DRAFT", "\uC0AC\uC774\uD2B8 \uCABD\uC5D0\uC11C \uD3B8\uC9D1 \uC911\uC774\uB358 \uAC83\uC774 \uC9C0\uAE08 \uBC84\uC804 \uC704\uAC00 \uC544\uB2C8\uB77C \uC5EC\uAE30\uC5D0 \uC5B9\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.", "\uCF58\uC194\uC5D0\uC11C \uADF8 \uD3B8\uC9D1\uC744 \uB418\uB3CC\uB9B0 \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694. \uB418\uB3CC\uB9AC\uAE30 \uC804\uC5D0 \uBB34\uC5C7\uC774 \uB4E4\uC5B4 \uC788\uC5C8\uB294\uC9C0\uB294 \uCF58\uC194\uC5D0\uC11C \uBCFC \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
2172
|
+
}
|
|
2173
|
+
return draft;
|
|
2174
|
+
}
|
|
2175
|
+
var viewOf = (draft) => ({ changed: draft.changed, deleted: draft.deleted });
|
|
2176
|
+
function ownedBy(ledger, draft) {
|
|
2177
|
+
const seen = ledger.server?.generation ?? null;
|
|
2178
|
+
return seen !== null && seen === (draft.generation ?? null) ? ledger.mine : {};
|
|
2179
|
+
}
|
|
2180
|
+
async function revisionRows(api) {
|
|
2181
|
+
const rows = await api.listRevisions().catch(() => null);
|
|
2182
|
+
if (rows === null) {
|
|
2183
|
+
throw new DevtoolsError("SERVER_UNREADABLE_DRAFT", "\uC9C0\uAE08 \uC0AC\uC774\uD2B8\uC5D0 \uCF1C\uC838 \uC788\uB294 \uBC84\uC804\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD574 \uC544\uBB34\uAC83\uB3C4 \uC62C\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.", "\uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694. \uACC4\uC18D \uADF8\uB7EC\uBA74 \uC774 \uACC4\uC815\uC5D0 \uBC84\uC804 \uBAA9\uB85D\uC744 \uBCFC \uAD8C\uD55C\uC774 \uC788\uB294\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
2184
|
+
}
|
|
2185
|
+
return rows;
|
|
2186
|
+
}
|
|
2187
|
+
function activeOf(rows) {
|
|
2188
|
+
const active = rows.find((row) => row.isActive)?.revisionNo;
|
|
2189
|
+
if (active === void 0) {
|
|
2190
|
+
throw new DevtoolsError("SERVER_UNREADABLE_DRAFT", "\uC774 \uC0AC\uC774\uD2B8\uC5D0 \uCF1C\uC838 \uC788\uB294 \uBC84\uC804\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.", "\uCF58\uC194\uC5D0\uC11C \uC5B4\uB5A4 \uBC84\uC804\uC744 \uCF24\uC9C0 \uBA3C\uC800 \uC815\uD574 \uC8FC\uC138\uC694. \uADF8 \uC804\uC5D0\uB294 \uBB34\uC5C7 \uC704\uC5D0 \uC5B9\uB294 \uAC83\uC778\uC9C0 \uC54C \uC218 \uC5C6\uC5B4 \uC62C\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
|
|
2191
|
+
}
|
|
2192
|
+
return active;
|
|
2193
|
+
}
|
|
2194
|
+
function reconcile(ledger, draft) {
|
|
2195
|
+
const paths = Object.keys(ledger.mine);
|
|
2196
|
+
if (ledger.server !== null || paths.length === 0)
|
|
2197
|
+
return null;
|
|
2198
|
+
const applied = paths.every((path) => {
|
|
2199
|
+
const expected = ledger.mine[path] ?? null;
|
|
2200
|
+
if (expected === null)
|
|
2201
|
+
return draft.deleted.includes(path);
|
|
2202
|
+
return draft.changed.some((row) => row.path === path && row.sha256 === expected);
|
|
2203
|
+
});
|
|
2204
|
+
return applied ? "applied" : "not-applied";
|
|
2205
|
+
}
|
|
2206
|
+
function requireSeen(plan, options) {
|
|
2207
|
+
if (plan.unseen.length === 0 || options.overwriteUnseen === true)
|
|
2208
|
+
return;
|
|
2209
|
+
const list = trimPaths(plan.unseen, PATH_LIST_CAP, options.listAll === true).map((path) => ` \xB7 ${path}`).join("\n");
|
|
2210
|
+
throw new DevtoolsError("PUSH_WOULD_REVERT", `\uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uC774 \uD3F4\uB354\uAC00 \uC544\uC9C1 \uBABB \uBC1B\uC740 \uD3B8\uC9D1\uC774 ${plan.unseen.length}\uAC1C \uC788\uC5B4 \uC544\uBB34\uAC83\uB3C4 \uC62C\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
|
|
2211
|
+
${list}`, "`zalkera pull` \uB85C \uC9C0\uAE08 \uBC84\uC804\uC744 \uBC1B\uC740 \uB4A4 \uB2E4\uC2DC \uC62C\uB9AC\uAC70\uB098, \uADF8 \uD3B8\uC9D1\uC744 \uCF58\uC194\uC5D0\uC11C \uD655\uC778\uD574 \uC8FC\uC138\uC694. \uADF8\uB300\uB85C \uC62C\uB9AC\uBA74 \uADF8 \uD3B8\uC9D1\uC774 \uC0AC\uB77C\uC9D1\uB2C8\uB2E4.");
|
|
2212
|
+
}
|
|
2213
|
+
function requireWithinLimits(edits) {
|
|
2214
|
+
if (edits.length > MAX_PUSH_ENTRIES) {
|
|
2215
|
+
throw new DevtoolsError("PUSH_TOO_LARGE", `\uD55C \uBC88\uC5D0 \uC62C\uB9B4 \uC218 \uC788\uB294 \uD30C\uC77C\uC740 ${MAX_PUSH_ENTRIES}\uAC1C\uC778\uB370 ${edits.length}\uAC1C\uC785\uB2C8\uB2E4.`, "\uACE0\uCE5C \uAC83\uC774 \uC774\uB9CC\uD07C \uB9CE\uC73C\uBA74 \uD3B8\uC9D1\uC774 \uC544\uB2C8\uB77C \uC0C8 \uC18C\uC2A4\uC785\uB2C8\uB2E4 \u2014 \uCF58\uC194\uC5D0\uC11C \uC18C\uC2A4\uB97C \uD1B5\uC9F8\uB85C \uC62C\uB9AC\uB294 \uCABD\uC774 \uB9DE\uC2B5\uB2C8\uB2E4.");
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
async function attempt(api, root, edits, report) {
|
|
2219
|
+
const body = await bodyOf(root, edits);
|
|
2220
|
+
try {
|
|
2221
|
+
const result = await api.editDraft(body);
|
|
2222
|
+
return {
|
|
2223
|
+
kind: "ok",
|
|
2224
|
+
generation: result.generation,
|
|
2225
|
+
// 🔴 **서버가 정한 문자열은 소독을 지난다.** 같은 응답의 오류 갈래는 이미 지나는데
|
|
2226
|
+
// 성공 갈래의 이 둘만 안 지났다 — 그리고 이 둘은 `stdout` 으로 나간다. 제어문자가
|
|
2227
|
+
// 살아 있으면 서버가 **커서를 올려 방금 찍은 줄을 지우고** 다른 문장을 앉힐 수 있다
|
|
2228
|
+
// (심의 실측). `plainNotice` 가 제어문자를 걷는다.
|
|
2229
|
+
previewUrl: plainNotice(result.previewUrl ?? "", MAX_PREVIEW_URL) || null,
|
|
2230
|
+
warning: plainNotice(result.warning ?? "", MAX_WARNING) || null,
|
|
2231
|
+
rejectedPaths: []
|
|
2232
|
+
};
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
if (!(error instanceof DevtoolsError))
|
|
2235
|
+
throw error;
|
|
2236
|
+
if (error.code === "SERVER_UNREACHABLE") {
|
|
2237
|
+
return { kind: "lost", error, rejectedPaths: [], generation: null, previewUrl: null, warning: null };
|
|
2238
|
+
}
|
|
2239
|
+
const kind = error.serverCode === "DRAFT_PRECONDITION_FAILED" || error.serverCode === SHAPE_STALE ? "conflict" : error.serverCode === PATH_NOT_ACCEPTED && error.paths.length > 0 ? "excluded" : "other";
|
|
2240
|
+
if (kind === "conflict" && error.paths.length > 0) {
|
|
2241
|
+
report(`\uC0AC\uC774\uD2B8 \uCABD\uC774 \uB2EC\uB77C\uC9C4 \uD30C\uC77C ${error.paths.length}\uAC1C: ${trimPaths([...error.paths], 5).join(" \xB7 ")}`);
|
|
2242
|
+
}
|
|
2243
|
+
return { kind, error, rejectedPaths: [...error.paths], generation: null, previewUrl: null, warning: null };
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
async function bodyOf(root, edits) {
|
|
2247
|
+
const body = [];
|
|
2248
|
+
let total = 0;
|
|
2249
|
+
for (const edit of edits) {
|
|
2250
|
+
if (edit.sha256 === null) {
|
|
2251
|
+
body.push({ path: edit.path, remove: true, ...edit.baseSha256 === null ? {} : { baseSha256: edit.baseSha256 } });
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
const size = (await stat2(join9(root, edit.path))).size;
|
|
2255
|
+
if (size > MAX_PUSH_FILE_BYTES) {
|
|
2256
|
+
throw new DevtoolsError("PUSH_TOO_LARGE", `\uD30C\uC77C \uD558\uB098\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${edit.path} (${Math.round(size / 1024)}KB \xB7 \uC0C1\uD55C ${MAX_PUSH_FILE_BYTES / 1024}KB).`, "\uB3D9\uC601\uC0C1\xB7\uC6D0\uBCF8 \uC774\uBBF8\uC9C0\xB7\uBE4C\uB4DC \uC0B0\uCD9C\uBB3C\uC774 \uC18C\uC2A4 \uD3F4\uB354\uC5D0 \uB4E4\uC5B4 \uC788\uC9C0 \uC54A\uC740\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
2257
|
+
}
|
|
2258
|
+
if (total + size > MAX_PUSH_BYTES)
|
|
2259
|
+
throw tooLarge();
|
|
2260
|
+
const raw = await readFile4(join9(root, edit.path));
|
|
2261
|
+
const breach = templateBreach(basename3(edit.path), raw);
|
|
2262
|
+
if (breach !== null) {
|
|
2263
|
+
throw new DevtoolsError("PUSH_SECRET_TEMPLATE", `${edit.path} \uC5D0 \uC2E4\uC81C \uAC12\uC774 \uB4E4\uC5B4 \uC788\uC5B4 \uC62C\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4(${breach}).`, "`.env.example` \uAC19\uC740 \uC11C\uC2DD \uD30C\uC77C\uC740 \uAC12\uC744 \uBE44\uC6CC \uB450\uC5B4\uC57C \uC62C\uB77C\uAC11\uB2C8\uB2E4. \uC2E4\uC81C \uAC12\uC740 \uCF58\uC194\uC758 \uD658\uACBD\uBCC0\uC218 \uD654\uBA74\uC5D0\uC11C \uAD00\uB9AC\uD574 \uC8FC\uC138\uC694.");
|
|
2264
|
+
}
|
|
2265
|
+
if (!Buffer.from(raw.toString("utf8"), "utf8").equals(raw)) {
|
|
2266
|
+
throw new DevtoolsError("PUSH_NOT_TEXT", `\uAE00\uC790 \uD30C\uC77C\uC774 \uC544\uB2C8\uB77C \uC62C\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${edit.path}`, "\uC774 \uBC29\uBC95\uC740 \uAE00\uC790\uB85C \uB41C \uD30C\uC77C\uB9CC \uB2E4\uB8F9\uB2C8\uB2E4. \uADF8\uB9BC\xB7\uAE00\uAF34\xB7\uC555\uCD95 \uD30C\uC77C\uC774 \uBC14\uB00C\uC5C8\uC73C\uBA74 \uCF58\uC194\uC5D0\uC11C \uC18C\uC2A4\uB97C \uD1B5\uC9F8\uB85C \uC62C\uB824 \uC8FC\uC138\uC694 \u2014 \uADF8\uB7EC\uBA74 \uADF8 \uD30C\uC77C\uB4E4\uB3C4 \uADF8\uB300\uB85C \uAC11\uB2C8\uB2E4.");
|
|
2267
|
+
}
|
|
2268
|
+
total += size;
|
|
2269
|
+
body.push({
|
|
2270
|
+
path: edit.path,
|
|
2271
|
+
content: raw.toString("utf8"),
|
|
2272
|
+
...edit.baseSha256 === null ? {} : { baseSha256: edit.baseSha256 }
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
if (Buffer.byteLength(JSON.stringify({ edits: body }), "utf8") > MAX_PUSH_BYTES)
|
|
2276
|
+
throw tooLarge();
|
|
2277
|
+
return body;
|
|
2278
|
+
}
|
|
2279
|
+
function tooLarge() {
|
|
2280
|
+
return new DevtoolsError("PUSH_TOO_LARGE", `\uD55C \uBC88\uC5D0 \uC62C\uB9B4 \uC218 \uC788\uB294 \uCD1D\uB7C9(${Math.round(MAX_PUSH_BYTES / 1024 / 1024 * 10) / 10}MB)\uC744 \uB118\uC5C8\uC2B5\uB2C8\uB2E4.`, "\uB098\uB220 \uBCF4\uB0B4\uBA74 \uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uBC18\uCABD\uB9CC \uC11C\uB294 \uC0C1\uD0DC\uAC00 \uC0DD\uAE41\uB2C8\uB2E4 \u2014 \uCF58\uC194\uC5D0\uC11C \uC18C\uC2A4\uB97C \uD1B5\uC9F8\uB85C \uC62C\uB824 \uC8FC\uC138\uC694.");
|
|
2281
|
+
}
|
|
2282
|
+
function responseLost(cause, noted) {
|
|
2283
|
+
return new DevtoolsError("PUSH_RESPONSE_LOST", "\uC62C\uB838\uB294\uC9C0 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", noted ? "\uCF58\uC194\uC758 \u300C\uD3B8\uC9D1 \uC911\u300D\uC5D0\uC11C \uC9C0\uAE08 \uAC78\uB824 \uC788\uB294 \uB0B4\uC6A9\uC744 \uBA3C\uC800 \uBD10 \uC8FC\uC138\uC694. \uB2E4\uC74C\uC5D0 \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uC774 \uB3C4\uAD6C\uAC00 \uC0AC\uC774\uD2B8 \uCABD\uACFC \uB300\uC870\uD574 \uC815\uB9AC\uD569\uB2C8\uB2E4." : "\uCF58\uC194\uC758 \u300C\uD3B8\uC9D1 \uC911\u300D\uC5D0\uC11C \uC9C0\uAE08 \uAC78\uB824 \uC788\uB294 \uB0B4\uC6A9\uC744 \uBD10 \uC8FC\uC138\uC694. \uC774 \uD3F4\uB354\uC758 \uAE30\uC900 \uAE30\uB85D\uC744 \uC4F0\uC9C0 \uBABB\uD574 \uB2E4\uC74C \uC2E4\uD589\uC774 \uC790\uB3D9\uC73C\uB85C \uC815\uB9AC\uD558\uC9C0\uB294 \uBABB\uD569\uB2C8\uB2E4.", cause);
|
|
2284
|
+
}
|
|
2285
|
+
var ledgerUnknown = () => new DevtoolsError("LEDGER_UNKNOWN", "\uC774 \uD3F4\uB354\uC5D0 \uAE30\uC900 \uAE30\uB85D\uC774 \uC5C6\uC5B4 \uBB34\uC5C7\uC774 \uB2EC\uB77C\uC84C\uB294\uC9C0 \uC140 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.", "`zalkera baseline` \uC744 \uC2E4\uD589\uD558\uBA74 \uC9C0\uAE08 \uBC84\uC804\uC744 \uAE30\uC900\uC73C\uB85C \uB2E4\uC2DC \uC138\uC6C1\uB2C8\uB2E4. \uD3F4\uB354\uC758 \uD30C\uC77C\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
|
|
2286
|
+
var foreignLedger = (tenant) => new DevtoolsError("LEDGER_UNKNOWN", `\uC774 \uD3F4\uB354\uC758 \uAE30\uC900 \uAE30\uB85D\uC740 \uB2E4\uB978 \uC0AC\uC774\uD2B8(${tenant})\uC758 \uAC83\uC785\uB2C8\uB2E4.`, "`zalkera pull` \uB85C \uC774 \uC0AC\uC774\uD2B8\uC758 \uC18C\uC2A4\uB97C \uBC1B\uAC70\uB098, `zalkera baseline` \uC73C\uB85C \uAE30\uC900\uC744 \uB2E4\uC2DC \uC138\uC6CC \uC8FC\uC138\uC694.");
|
|
2287
|
+
function unreachable() {
|
|
2288
|
+
return new DevtoolsError("SERVER_REJECTED", "\uC62C\uB9AC\uAE30\uAC00 \uB05D\uB098\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.", "\uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.");
|
|
2289
|
+
}
|
|
2290
|
+
function baseMoved(mine, now, dirty) {
|
|
2291
|
+
const way = dirty === 0 ? `\uC774 \uD3F4\uB354\uB97C ${now}\uD310\uC5D0 \uB9DE\uCD94\uB824\uBA74 \`zalkera pull\` \uC744 \uC2E4\uD589\uD558\uC138\uC694.` : `\uC774 \uD3F4\uB354\uC5D0\uC11C \uACE0\uCE5C \uAC83\uC774 ${dirty}\uAC1C \uC788\uC5B4 \`zalkera pull\` \uB3C4 \uADF8\uB300\uB85C\uB294 \uB9C9\uD799\uB2C8\uB2E4. \`zalkera pull --discard-local\` \uC744 \uC2E4\uD589\uD558\uBA74 \uACE0\uCE5C \uD30C\uC77C\uC744 \uC606 \uD3F4\uB354\uC5D0 \uC62E\uACA8 \uB450\uACE0 \uC0C8 \uD310\uC744 \uBC1B\uC2B5\uB2C8\uB2E4 \u2014 \uADF8 \uB4A4 \uC62E\uACA8 \uB454 \uD30C\uC77C\uC744 \uBCF4\uACE0 \uB2E4\uC2DC \uACE0\uCCD0 \uC62C\uB9AC\uC2DC\uBA74 \uB429\uB2C8\uB2E4.`;
|
|
2292
|
+
return new DevtoolsError("PUSH_BASE_MOVED", `\uAE30\uC900\uC774 ${mine}\uD310\uC5D0\uC11C ${now}\uD310\uC73C\uB85C \uC6C0\uC9C1\uC5EC \uC544\uBB34\uAC83\uB3C4 \uC62C\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`, `${way} \uC9C0\uAE08 \uC62C\uB9AC\uBA74 ${mine}\uD310\uC744 \uBCF4\uACE0 \uACE0\uCE5C \uB0B4\uC6A9\uC774 ${now}\uD310 \uC704\uC5D0 \uC5B9\uD799\uB2C8\uB2E4.`);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// ../core/dist/draftLifecycle.js
|
|
2296
|
+
import { resolve as resolve6 } from "node:path";
|
|
2297
|
+
init_errors();
|
|
2298
|
+
import { rm as rm3 } from "node:fs/promises";
|
|
2299
|
+
async function publishDraft(options) {
|
|
2300
|
+
const report = options.onProgress ?? (() => {
|
|
2301
|
+
});
|
|
2302
|
+
const root = resolve6(options.folder);
|
|
2303
|
+
report("\uD3B8\uC9D1\uC744 \uC0C8 \uBC84\uC804\uC73C\uB85C \uC62C\uB9AC\uB294 \uC911\uC785\uB2C8\uB2E4\u2026");
|
|
2304
|
+
const published = await requireConsent(() => options.api.publishDraft(options.label, options.discardPendingChanges === true)).catch((error) => {
|
|
2305
|
+
if (error instanceof DevtoolsError && error.serverCode === "DRAFT_BASE_MOVED") {
|
|
2306
|
+
throw new DevtoolsError("PUBLISH_BASE_MOVED", "\uD3B8\uC9D1\uC744 \uC2DC\uC791\uD55C \uB4A4 \uC0AC\uC774\uD2B8 \uBC84\uC804\uC774 \uBC14\uB00C\uC5B4 \uC774 \uD3B8\uC9D1\uC744 \uC62C\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.", "`zalkera discard` \uB85C \uADF8 \uD3B8\uC9D1\uC744 \uBC84\uB9B0 \uB4A4, `zalkera pull` \uB85C \uC9C0\uAE08 \uBC84\uC804\uC744 \uBC1B\uC544 \uB2E4\uC2DC \uACE0\uCCD0 \uC8FC\uC138\uC694. \uBC84\uB9AC\uAE30\uB294 \uBB34\uC5C7\uC774 \uAC78\uB824 \uC788\uB294\uC9C0 \uBA3C\uC800 \uBCF4\uC5EC \uC90D\uB2C8\uB2E4.", error);
|
|
2307
|
+
}
|
|
2308
|
+
throw error;
|
|
2309
|
+
});
|
|
2310
|
+
report("\uC0C8 \uBC84\uC804\uC758 \uD30C\uC77C \uBAA9\uB85D\uC744 \uC77D\uB294 \uC911\uC785\uB2C8\uB2E4\u2026");
|
|
2311
|
+
const rebuilt = await rebuildBaseline({
|
|
2312
|
+
api: options.api,
|
|
2313
|
+
folder: root,
|
|
2314
|
+
revisionNo: published.revisionNo,
|
|
2315
|
+
onProgress: report,
|
|
2316
|
+
fetchImpl: options.fetchImpl
|
|
2317
|
+
}).catch(() => null);
|
|
2318
|
+
if (rebuilt === null) {
|
|
2319
|
+
await forgetLedger(root);
|
|
2320
|
+
return { ...published, ledgerRebuilt: false, files: null };
|
|
2321
|
+
}
|
|
2322
|
+
return { ...published, ledgerRebuilt: true, files: rebuilt.files };
|
|
2323
|
+
}
|
|
2324
|
+
async function rollbackRevision(options) {
|
|
2325
|
+
const report = options.onProgress ?? (() => {
|
|
2326
|
+
});
|
|
2327
|
+
const root = resolve6(options.folder);
|
|
2328
|
+
const active = await activeRevisionNo(options.api);
|
|
2329
|
+
if (options.revisionNo === active) {
|
|
2330
|
+
throw new DevtoolsError("ROLLBACK_IS_DISCARD", `${options.revisionNo}\uD310\uC740 \uC9C0\uAE08 \uCF1C\uC838 \uC788\uB294 \uBC84\uC804\uC785\uB2C8\uB2E4 \u2014 \uB418\uB3CC\uB9B4 \uAC83\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`, "\uD3B8\uC9D1 \uC911\uC778 \uAC83\uC744 \uBC84\uB9AC\uB824\uB294 \uAC83\uC774\uB77C\uBA74 `zalkera discard` \uB97C \uC4F0\uC138\uC694. \uADF8\uCABD\uC740 \uD310\uC744 \uC62E\uAE30\uC9C0 \uC54A\uACE0 \uD3B8\uC9D1\uB9CC \uBC84\uB9BD\uB2C8\uB2E4.");
|
|
2331
|
+
}
|
|
2332
|
+
report(`${options.revisionNo}\uD310\uC73C\uB85C \uB418\uB3CC\uB9AC\uB294 \uC911\uC785\uB2C8\uB2E4\u2026`);
|
|
2333
|
+
const result = await requireConsent(() => options.api.activateRevision(options.revisionNo, options.discardPending === true)).catch((error) => {
|
|
2334
|
+
if (isDraftInProgress(error)) {
|
|
2335
|
+
throw new DevtoolsError("ROLLBACK_BLOCKED_BY_DRAFT", "\uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uD3B8\uC9D1 \uC911\uC778 \uAC83\uC774 \uC788\uC5B4 \uBC84\uC804\uC744 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.", "`zalkera publish` \uB85C \uADF8 \uD3B8\uC9D1\uC744 \uC0C8 \uBC84\uC804\uC73C\uB85C \uB9CC\uB4E4\uAC70\uB098, `zalkera discard` \uB85C \uBC84\uB9B0 \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694. \uBC84\uB9AC\uAE30\uB294 \uBB34\uC5C7\uC774 \uAC78\uB824 \uC788\uB294\uC9C0 \uBA3C\uC800 \uBCF4\uC5EC \uC90D\uB2C8\uB2E4.", error);
|
|
2336
|
+
}
|
|
2337
|
+
throw error;
|
|
2338
|
+
});
|
|
2339
|
+
const landed = plausibleRevisionNo(result.revisionNo) ?? await activeRevisionNo(options.api).catch(() => null);
|
|
2340
|
+
if (landed === null) {
|
|
2341
|
+
await forgetLedger(root);
|
|
2342
|
+
return {
|
|
2343
|
+
revisionNo: null,
|
|
2344
|
+
requested: options.revisionNo,
|
|
2345
|
+
pointerMoved: result.pointerMoved ?? true,
|
|
2346
|
+
discardedDraft: result.discardedDraft ?? false,
|
|
2347
|
+
discardedPendingChanges: result.discardedPendingChanges ?? 0,
|
|
2348
|
+
ledgerRebuilt: false,
|
|
2349
|
+
differing: []
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
const rebuilt = await rebuildBaseline({
|
|
2353
|
+
api: options.api,
|
|
2354
|
+
folder: root,
|
|
2355
|
+
revisionNo: landed,
|
|
2356
|
+
onProgress: report,
|
|
2357
|
+
fetchImpl: options.fetchImpl
|
|
2358
|
+
}).catch(() => null);
|
|
2359
|
+
if (rebuilt === null)
|
|
2360
|
+
await forgetLedger(root);
|
|
2361
|
+
return {
|
|
2362
|
+
revisionNo: landed,
|
|
2363
|
+
requested: options.revisionNo,
|
|
2364
|
+
pointerMoved: result.pointerMoved ?? true,
|
|
2365
|
+
discardedDraft: result.discardedDraft ?? false,
|
|
2366
|
+
discardedPendingChanges: result.discardedPendingChanges ?? 0,
|
|
2367
|
+
ledgerRebuilt: rebuilt !== null,
|
|
2368
|
+
differing: rebuilt?.differing ?? []
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
async function discardDraft(options) {
|
|
2372
|
+
const report = options.onProgress ?? (() => {
|
|
2373
|
+
});
|
|
2374
|
+
const root = resolve6(options.folder);
|
|
2375
|
+
const active = await activeRevisionNo(options.api);
|
|
2376
|
+
const now = await options.api.draftFiles().catch(() => null);
|
|
2377
|
+
if (now === null || (now.generation ?? null) !== options.plan.generation) {
|
|
2378
|
+
throw new DevtoolsError("DRAFT_MOVED_WHILE_CONFIRMING", "\uD655\uC778\uD558\uC2DC\uB294 \uC0AC\uC774 \uC0AC\uC774\uD2B8 \uCABD \uD3B8\uC9D1\uC774 \uB2EC\uB77C\uC838 \uC544\uBB34\uAC83\uB3C4 \uBC84\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.", "`zalkera discard` \uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uC9C0\uAE08 \uAC78\uB824 \uC788\uB294 \uAC83\uC744 \uBCF4\uC5EC \uB4DC\uB9BD\uB2C8\uB2E4.");
|
|
2379
|
+
}
|
|
2380
|
+
report("\uD3B8\uC9D1 \uC911\uC778 \uAC83\uC744 \uBC84\uB9AC\uB294 \uC911\uC785\uB2C8\uB2E4\u2026");
|
|
2381
|
+
const result = await options.api.activateRevision(active, options.discardPending === true).catch(async (error) => {
|
|
2382
|
+
if (!needsDiscardConsent(error) || !(error instanceof DevtoolsError))
|
|
2383
|
+
throw error;
|
|
2384
|
+
if (creditsAtStake(error))
|
|
2385
|
+
throw asConsentRequired(error);
|
|
2386
|
+
report("\uC11C\uBC84\uAC00 \uD655\uC778\uC744 \uD55C \uBC88 \uB354 \uC694\uAD6C\uD574 \uBC29\uAE08 \uC8FC\uC2E0 \uB3D9\uC758\uB97C \uADF8\uB300\uB85C \uC787\uC2B5\uB2C8\uB2E4\u2026");
|
|
2387
|
+
return options.api.activateRevision(active, true);
|
|
2388
|
+
});
|
|
2389
|
+
const ledger = await readLedger(root);
|
|
2390
|
+
if (ledger)
|
|
2391
|
+
await writeLedger(root, { ...ledger, server: null, mine: {} });
|
|
2392
|
+
return {
|
|
2393
|
+
plan: options.plan,
|
|
2394
|
+
discardedPendingChanges: result.discardedPendingChanges ?? 0,
|
|
2395
|
+
hadDraft: result.discardedDraft ?? options.plan.paths.length > 0,
|
|
2396
|
+
// 🔴 **라이브가 움직였는지 싣는다.** 갈래는 서버가 정한다 — 우리가 읽은 활성 판이 그 사이
|
|
2397
|
+
// 낡았으면 서버는 이 호출을 **되돌리기로 처리해 포인터를 옮긴다.** 그 사실을 안 실으면
|
|
2398
|
+
// 배포 사건이 일어났는데 「아무 일도 없었다」로 읽힌다.
|
|
2399
|
+
pointerMoved: result.pointerMoved ?? false
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
async function forgetLedger(root) {
|
|
2403
|
+
const target = await resolveExisting(root, SYNC_LEDGER_PATH);
|
|
2404
|
+
if (target === null)
|
|
2405
|
+
return;
|
|
2406
|
+
await rm3(target, { force: true }).catch(() => void 0);
|
|
2407
|
+
}
|
|
2408
|
+
async function activeRevisionNo(api) {
|
|
2409
|
+
const rows = await api.listRevisions().catch(() => null);
|
|
2410
|
+
if (rows === null) {
|
|
2411
|
+
throw new DevtoolsError("SERVER_UNREADABLE_DRAFT", "\uC9C0\uAE08 \uC0AC\uC774\uD2B8\uC5D0 \uCF1C\uC838 \uC788\uB294 \uBC84\uC804\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", "\uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694. \uACC4\uC18D \uADF8\uB7EC\uBA74 \uC774 \uACC4\uC815\uC5D0 \uBC84\uC804 \uBAA9\uB85D\uC744 \uBCFC \uAD8C\uD55C\uC774 \uC788\uB294\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
2412
|
+
}
|
|
2413
|
+
const active = rows.find((row) => row.isActive)?.revisionNo;
|
|
2414
|
+
if (active !== void 0)
|
|
2415
|
+
return active;
|
|
2416
|
+
const fallback = await api.draftState().then((state) => state.revertTargetRevisionNo ?? null).catch(() => null);
|
|
2417
|
+
if (fallback !== null)
|
|
2418
|
+
return fallback;
|
|
2419
|
+
throw new DevtoolsError("SERVER_UNREADABLE_DRAFT", "\uC774 \uC0AC\uC774\uD2B8\uC5D0 \uCF1C\uC838 \uC788\uB294 \uBC84\uC804\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.", "\uCF58\uC194\uC5D0\uC11C \uC5B4\uB5A4 \uBC84\uC804\uC744 \uCF24\uC9C0 \uBA3C\uC800 \uC815\uD574 \uC8FC\uC138\uC694. \uADF8 \uC804\uC5D0\uB294 \uBB34\uC5C7 \uC704\uC5D0 \uC5B9\uB294 \uAC83\uC778\uC9C0 \uC54C \uC218 \uC5C6\uC5B4 \uC6D0\uC7A5\uC744 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
|
|
2420
|
+
}
|
|
2421
|
+
async function requireConsent(call) {
|
|
2422
|
+
try {
|
|
2423
|
+
return await call();
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
if (needsDiscardConsent(error) && error instanceof DevtoolsError)
|
|
2426
|
+
throw asConsentRequired(error);
|
|
2427
|
+
throw error;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
function asConsentRequired(error) {
|
|
2431
|
+
return new DevtoolsError("DISCARD_CONSENT_REQUIRED", error.message, creditsAtStake(error) ? "\uADF8\uB798\uB3C4 \uACC4\uC18D\uD558\uB824\uBA74 `--discard-pending` \uC744 \uBD99\uC5EC \uC8FC\uC138\uC694. \uC4F4 \uD06C\uB808\uB527\uC740 \uB3CC\uC544\uC624\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." : "\uADF8\uB798\uB3C4 \uACC4\uC18D\uD558\uB824\uBA74 `--discard-pending` \uC744 \uBD99\uC5EC \uC8FC\uC138\uC694.", error);
|
|
2432
|
+
}
|
|
2433
|
+
var PENDING_MARK = "pendingAiChanges";
|
|
2434
|
+
function creditsAtStake(error) {
|
|
2435
|
+
return error.paths.length === 0 || error.paths.includes(PENDING_MARK);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// ../core/dist/strandedPlan.js
|
|
2439
|
+
function planStranded(input) {
|
|
2440
|
+
const { ledger, draft } = input;
|
|
2441
|
+
if (!draft)
|
|
2442
|
+
return { verdict: "elsewhere", empty: false, paths: [], generation: null, reason: "server-unreadable" };
|
|
2443
|
+
const paths = [...draft.changed.map((row) => row.path), ...draft.deleted].sort();
|
|
2444
|
+
const generation = draft.generation ?? null;
|
|
2445
|
+
const empty = paths.length === 0;
|
|
2446
|
+
if (!ledger)
|
|
2447
|
+
return { verdict: "elsewhere", empty, paths, generation, reason: "no-ledger" };
|
|
2448
|
+
const seen = ledger.server?.generation ?? null;
|
|
2449
|
+
if (seen === null)
|
|
2450
|
+
return { verdict: "elsewhere", empty, paths, generation, reason: "generation-unknown" };
|
|
2451
|
+
if (seen !== (draft.generation ?? null)) {
|
|
2452
|
+
return { verdict: "elsewhere", empty, paths, generation, reason: "generation-differs" };
|
|
2453
|
+
}
|
|
2454
|
+
for (const path of draft.deleted) {
|
|
2455
|
+
if (!Object.hasOwn(ledger.mine, path))
|
|
2456
|
+
return { verdict: "elsewhere", empty, paths, generation, reason: "path-not-mine" };
|
|
2457
|
+
if (ledger.mine[path] !== null)
|
|
2458
|
+
return { verdict: "elsewhere", empty, paths, generation, reason: "sha-differs" };
|
|
2459
|
+
}
|
|
2460
|
+
for (const row of draft.changed) {
|
|
2461
|
+
if (!Object.hasOwn(ledger.mine, row.path)) {
|
|
2462
|
+
return { verdict: "elsewhere", empty, paths, generation, reason: "path-not-mine" };
|
|
2463
|
+
}
|
|
2464
|
+
if (ledger.mine[row.path] !== row.sha256) {
|
|
2465
|
+
return { verdict: "elsewhere", empty, paths, generation, reason: "sha-differs" };
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
return { verdict: "mine", empty, paths, generation, reason: "ledger-matches" };
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// src/main.ts
|
|
2472
|
+
import { spawn } from "node:child_process";
|
|
2473
|
+
|
|
2474
|
+
// src/args.ts
|
|
2475
|
+
function parseArgs(argv) {
|
|
2476
|
+
const positional = [];
|
|
2477
|
+
const flags = {};
|
|
2478
|
+
let literal = false;
|
|
2479
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
2480
|
+
const token = argv[i];
|
|
2481
|
+
if (literal) {
|
|
2482
|
+
positional.push(token);
|
|
2483
|
+
continue;
|
|
2484
|
+
}
|
|
2485
|
+
if (token === "--") {
|
|
2486
|
+
literal = true;
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
if (!token.startsWith("--")) {
|
|
2490
|
+
positional.push(token);
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
const body = token.slice(2);
|
|
2494
|
+
const eq = body.indexOf("=");
|
|
2495
|
+
if (eq >= 0) {
|
|
2496
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
const next = argv[i + 1];
|
|
2500
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
2501
|
+
flags[body] = next;
|
|
2502
|
+
i += 1;
|
|
2503
|
+
continue;
|
|
2504
|
+
}
|
|
2505
|
+
flags[body] = true;
|
|
2506
|
+
}
|
|
2507
|
+
return { command: positional.shift() ?? null, positional, flags };
|
|
2508
|
+
}
|
|
2509
|
+
function flagOn(flags, name) {
|
|
2510
|
+
const value = flags[name];
|
|
2511
|
+
if (value === void 0) return false;
|
|
2512
|
+
if (value === true) return true;
|
|
2513
|
+
return !["false", "0", "no", ""].includes(value.toLowerCase());
|
|
2514
|
+
}
|
|
2515
|
+
function flagValue(flags, name) {
|
|
2516
|
+
const value = flags[name];
|
|
2517
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// src/context.ts
|
|
2521
|
+
import { readFileSync } from "node:fs";
|
|
2522
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
2523
|
+
import { fileURLToPath } from "node:url";
|
|
2524
|
+
import { join as join11, resolve as resolve8 } from "node:path";
|
|
2525
|
+
|
|
2526
|
+
// src/tokenStore.ts
|
|
2527
|
+
import { chmod as chmod2, readFile as readFile5, rm as rm4 } from "node:fs/promises";
|
|
2528
|
+
import { homedir } from "node:os";
|
|
2529
|
+
import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute2, join as join10, resolve as resolve7, sep as sep6 } from "node:path";
|
|
2530
|
+
function tokenPath(env = process.env, home = homedir()) {
|
|
2531
|
+
const fallback = join10(home, ".config");
|
|
2532
|
+
const base = env.XDG_CONFIG_HOME;
|
|
2533
|
+
const inside = base !== void 0 && isAbsolute2(base) && (resolve7(base) === resolve7(home) || resolve7(base).startsWith(resolve7(home) + sep6));
|
|
2534
|
+
return join10(inside ? resolve7(base) : fallback, "zalkera", "auth.json");
|
|
2535
|
+
}
|
|
2536
|
+
var FileTokenStore = class {
|
|
2537
|
+
// ⚠ 매개변수 프로퍼티(`constructor(private path)`)를 안 쓴다 — Node 의 타입 제거 실행기가
|
|
2538
|
+
// 그 문법을 거절해 **시험이 아예 안 돈다.** 이 레포의 시험은 전부 그 실행기로 돈다.
|
|
2539
|
+
path;
|
|
2540
|
+
constructor(path = tokenPath()) {
|
|
2541
|
+
this.path = path;
|
|
2542
|
+
}
|
|
2543
|
+
async read() {
|
|
2544
|
+
const text = await readFile5(this.path, "utf8").catch(() => null);
|
|
2545
|
+
if (text === null) return null;
|
|
2546
|
+
try {
|
|
2547
|
+
const raw = JSON.parse(text);
|
|
2548
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
2549
|
+
const t = raw;
|
|
2550
|
+
if (typeof t.accessToken !== "string" || typeof t.refreshToken !== "string" || typeof t.expiresAt !== "number" || typeof t.issuer !== "string") {
|
|
2551
|
+
return null;
|
|
2552
|
+
}
|
|
2553
|
+
return {
|
|
2554
|
+
accessToken: t.accessToken,
|
|
2555
|
+
refreshToken: t.refreshToken,
|
|
2556
|
+
expiresAt: t.expiresAt,
|
|
2557
|
+
issuer: t.issuer
|
|
2558
|
+
};
|
|
2559
|
+
} catch {
|
|
2560
|
+
return null;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
async write(tokens) {
|
|
2564
|
+
const dir = await ensureOwnDir(dirname2(dirname2(this.path)), basename4(dirname2(this.path)));
|
|
2565
|
+
await chmod2(dir, 448).catch(() => {
|
|
2566
|
+
});
|
|
2567
|
+
await writeOwnFile(this.path, JSON.stringify(tokens), 384);
|
|
2568
|
+
await chmod2(this.path, 384);
|
|
2569
|
+
}
|
|
2570
|
+
async clear() {
|
|
2571
|
+
await rm4(this.path, { force: true });
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
|
|
2575
|
+
// src/context.ts
|
|
2576
|
+
var DEFAULT_SERVER = "https://api.zalkera.com";
|
|
2577
|
+
function serverUrlOf(env = process.env) {
|
|
2578
|
+
const raw = env.ZALKERA_SERVER ?? DEFAULT_SERVER;
|
|
2579
|
+
const url = apiBaseUrl(raw);
|
|
2580
|
+
if (!url) {
|
|
2581
|
+
throw new DevtoolsError(
|
|
2582
|
+
"SERVER_UNREACHABLE",
|
|
2583
|
+
`\uC11C\uBC84 \uC8FC\uC18C\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${raw}`,
|
|
2584
|
+
"`ZALKERA_SERVER` \uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694."
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
return url.toString().replace(/\/$/, "");
|
|
2588
|
+
}
|
|
2589
|
+
async function tenantOf(folder, explicit) {
|
|
2590
|
+
if (explicit) return TENANT_CODE.test(explicit) ? explicit : null;
|
|
2591
|
+
const mark = parseSourceMark(await readFile6(join11(folder, SOURCE_MARK_PATH), "utf8").catch(() => null));
|
|
2592
|
+
if (mark) return mark.tenant;
|
|
2593
|
+
const text = await readFile6(join11(folder, SYNC_LEDGER_PATH), "utf8").catch(() => null);
|
|
2594
|
+
return text === null ? null : parseSyncLedger(text)?.tenant ?? null;
|
|
2595
|
+
}
|
|
2596
|
+
async function openAuth(options = {}) {
|
|
2597
|
+
const handshake = await fetchHandshake(serverUrlOf(options.env ?? process.env), version(), fetch, UPGRADE_HOW);
|
|
2598
|
+
return { auth: handshake.auth, store: options.store ?? new FileTokenStore() };
|
|
2599
|
+
}
|
|
2600
|
+
async function openContext(options = {}) {
|
|
2601
|
+
const env = options.env ?? process.env;
|
|
2602
|
+
const folder = resolve8(options.folder ?? process.cwd());
|
|
2603
|
+
const serverUrl = serverUrlOf(env);
|
|
2604
|
+
const tenant = await tenantOf(folder, options.tenant);
|
|
2605
|
+
if (!tenant) {
|
|
2606
|
+
throw new DevtoolsError(
|
|
2607
|
+
"NOT_A_SITE",
|
|
2608
|
+
"\uC774 \uD3F4\uB354\uAC00 \uC5B4\uB290 \uC0AC\uC774\uD2B8\uC758 \uAC83\uC778\uC9C0 \uC54C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2609
|
+
"\uC0AC\uC774\uD2B8 \uD3F4\uB354 \uC548\uC5D0\uC11C \uC2E4\uD589\uD558\uAC70\uB098 `--site <\uC0AC\uC774\uD2B8\uCF54\uB4DC>` \uB97C \uBD99\uC5EC \uC8FC\uC138\uC694."
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
for (const line of await misplacedToolLines(folder)) process.stderr.write(`${line}
|
|
2613
|
+
`);
|
|
2614
|
+
const handshake = await fetchHandshake(serverUrl, version(), fetch, UPGRADE_HOW);
|
|
2615
|
+
const store = options.store ?? new FileTokenStore();
|
|
2616
|
+
const auth = handshake.auth;
|
|
2617
|
+
const api = new ZalkeraApi({
|
|
2618
|
+
apiBase: serverUrl,
|
|
2619
|
+
accessToken: () => getAccessToken(auth, store),
|
|
2620
|
+
tenantCode: () => tenant
|
|
2621
|
+
});
|
|
2622
|
+
return { api, auth, store, folder, tenant, serverUrl };
|
|
2623
|
+
}
|
|
2624
|
+
async function misplacedToolLines(folder) {
|
|
2625
|
+
const found = await inspectProject(folder).then((p) => p.toolInDeps).catch(() => []);
|
|
2626
|
+
if (found.length === 0) return [];
|
|
2627
|
+
return [
|
|
2628
|
+
`\u26A0 ${found.join(" \xB7 ")} \uAC00 \uC774 \uC0AC\uC774\uD2B8\uC758 \uC758\uC874\uC73C\uB85C \uB4E4\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.`,
|
|
2629
|
+
" \uC774\uAC83\uC740 \uC18C\uC2A4\uAC00 `import` \uD558\uB294 \uD328\uD0A4\uC9C0\uAC00 \uC544\uB2C8\uB77C \uD130\uBBF8\uB110\uC5D0\uC11C \uCE58\uB294 \uBA85\uB839\uC785\uB2C8\uB2E4.",
|
|
2630
|
+
" `package.json` \uC5D0\uC11C \uBE7C \uC8FC\uC138\uC694 \u2014 \uADF8\uB300\uB85C \uB450\uBA74 \uC0AC\uC774\uD2B8\uB97C \uC9C0\uC744 \uB54C \uD568\uAED8 \uC124\uCE58\uB429\uB2C8\uB2E4."
|
|
2631
|
+
];
|
|
2632
|
+
}
|
|
2633
|
+
var UPGRADE_HOW = "`npx @zalkera/cli@latest <\uBA85\uB839>` \uC73C\uB85C \uBD80\uB974\uAC70\uB098, \uC804\uC5ED \uC124\uCE58\uB97C \uC4F0\uC2E0\uB2E4\uBA74 `npm i -g @zalkera/cli@latest` \uB97C \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.";
|
|
2634
|
+
function version() {
|
|
2635
|
+
let raw;
|
|
2636
|
+
try {
|
|
2637
|
+
raw = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
|
|
2638
|
+
} catch (error) {
|
|
2639
|
+
throw new DevtoolsError(
|
|
2640
|
+
"INSTALL_BROKEN",
|
|
2641
|
+
"\uC774 \uB3C4\uAD6C\uC758 \uC124\uCE58\uAC00 \uC628\uC804\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 \uC790\uAE30 \uBC84\uC804\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
|
|
2642
|
+
"`npm i -g @zalkera/cli@latest` \uB85C \uB2E4\uC2DC \uC124\uCE58\uD558\uAC70\uB098, `npx @zalkera/cli@latest <\uBA85\uB839>` \uC73C\uB85C \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.",
|
|
2643
|
+
error
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
const declared = raw.version;
|
|
2647
|
+
if (typeof declared !== "string" || declared === "") {
|
|
2648
|
+
throw new DevtoolsError(
|
|
2649
|
+
"INSTALL_BROKEN",
|
|
2650
|
+
"\uC774 \uB3C4\uAD6C\uC758 \uC124\uCE58\uAC00 \uC628\uC804\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 \uBC84\uC804 \uD45C\uAE30\uAC00 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
2651
|
+
"`npm i -g @zalkera/cli@latest` \uB85C \uB2E4\uC2DC \uC124\uCE58\uD558\uAC70\uB098, `npx @zalkera/cli@latest <\uBA85\uB839>` \uC73C\uB85C \uC2E4\uD589\uD574 \uC8FC\uC138\uC694."
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
return declared;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// src/confirm.ts
|
|
2658
|
+
import { createInterface } from "node:readline/promises";
|
|
2659
|
+
async function confirm(options) {
|
|
2660
|
+
if (options.given === true) return true;
|
|
2661
|
+
const input = options.input ?? process.stdin;
|
|
2662
|
+
const output = options.output ?? process.stderr;
|
|
2663
|
+
if (input.isTTY !== true) {
|
|
2664
|
+
output.write(
|
|
2665
|
+
`${options.question}
|
|
2666
|
+
\uD130\uBBF8\uB110\uC774 \uC544\uB2C8\uB77C \uBB3C\uC5B4\uBCFC \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uACC4\uC18D\uD558\uB824\uBA74 \`${options.flag}\` \uB97C \uBD99\uC5EC \uC8FC\uC138\uC694.
|
|
2667
|
+
`
|
|
2668
|
+
);
|
|
2669
|
+
return false;
|
|
2670
|
+
}
|
|
2671
|
+
const rl = createInterface({ input, output });
|
|
2672
|
+
try {
|
|
2673
|
+
const answer = (await rl.question(`${options.question}
|
|
2674
|
+
> `)).trim();
|
|
2675
|
+
if (options.phrase !== void 0) return answer === options.phrase;
|
|
2676
|
+
return ["y", "yes", "\uC608"].includes(answer.toLowerCase());
|
|
2677
|
+
} finally {
|
|
2678
|
+
rl.close();
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
// src/report.ts
|
|
2683
|
+
var PATH_SHOW = 200;
|
|
2684
|
+
function bullets(paths, verbose) {
|
|
2685
|
+
return trimPaths(paths, PATH_LIST_CAP, verbose).map((p) => ` \xB7 ${plainNotice(p, PATH_SHOW)}`);
|
|
2686
|
+
}
|
|
2687
|
+
function block(title, paths, verbose) {
|
|
2688
|
+
if (paths.length === 0) return [];
|
|
2689
|
+
return [`${title} ${paths.length}\uAC1C`, ...bullets(paths, verbose)];
|
|
2690
|
+
}
|
|
2691
|
+
function pathLines(paths, verbose = false) {
|
|
2692
|
+
return bullets(paths, verbose);
|
|
2693
|
+
}
|
|
2694
|
+
function describeStatus(status, verbose = false) {
|
|
2695
|
+
const lines = [];
|
|
2696
|
+
lines.push(`\uC0AC\uC774\uD2B8: ${status.tenant ?? "(\uBAA8\uB984)"}`);
|
|
2697
|
+
lines.push(
|
|
2698
|
+
status.baseRevisionNo === null ? "\uC774 \uD3F4\uB354\uAC00 \uC120 \uD310: \uBAA8\uB984" : `\uC774 \uD3F4\uB354\uAC00 \uC120 \uD310: ${status.baseRevisionNo}\uD310`
|
|
2699
|
+
);
|
|
2700
|
+
if (status.activeRevisionNo !== null) lines.push(`\uC9C0\uAE08 \uC0AC\uC774\uD2B8\uC5D0 \uCF1C\uC9C4 \uD310: ${status.activeRevisionNo}\uD310`);
|
|
2701
|
+
if (status.behind) {
|
|
2702
|
+
lines.push("");
|
|
2703
|
+
lines.push(`\uAE30\uC900\uC774 ${status.baseRevisionNo}\uD310\uC5D0\uC11C ${status.activeRevisionNo}\uD310\uC73C\uB85C \uC6C0\uC9C1\uC600\uC2B5\uB2C8\uB2E4.`);
|
|
2704
|
+
const dirty = status.changed.length + status.removed.length;
|
|
2705
|
+
lines.push(
|
|
2706
|
+
dirty === 0 ? `\uC774 \uD3F4\uB354\uB97C ${status.activeRevisionNo}\uD310\uC5D0 \uB9DE\uCD94\uB824\uBA74 \`zalkera pull\` \uC744 \uC2E4\uD589\uD558\uC138\uC694.` : `\uC774 \uD3F4\uB354\uC5D0\uC11C \uACE0\uCE5C \uAC83\uC774 ${dirty}\uAC1C \uC788\uC5B4 \`zalkera pull\` \uC740 \uADF8\uB300\uB85C\uB294 \uB9C9\uD799\uB2C8\uB2E4. \`zalkera pull --discard-local\` \uC744 \uC2E4\uD589\uD558\uBA74 \uACE0\uCE5C \uD30C\uC77C\uC744 \uC606 \uD3F4\uB354\uC5D0 \uC62E\uACA8 \uB450\uACE0 \uC0C8 \uD310\uC744 \uBC1B\uC2B5\uB2C8\uB2E4.`
|
|
2707
|
+
);
|
|
2708
|
+
lines.push(
|
|
2709
|
+
`\uC9C0\uAE08 \uC62C\uB9AC\uBA74 ${status.baseRevisionNo}\uD310\uC744 \uBCF4\uACE0 \uACE0\uCE5C \uB0B4\uC6A9\uC774 ${status.activeRevisionNo}\uD310 \uC704\uC5D0 \uC5B9\uD799\uB2C8\uB2E4.`
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
const changes = [
|
|
2713
|
+
...block("\uACE0\uCE5C \uAC83", status.changed, verbose),
|
|
2714
|
+
...block("\uC9C0\uC6B4 \uAC83", status.removed, verbose),
|
|
2715
|
+
...block("\uC0C8\uB85C \uB9CC\uB4E0 \uAC83", status.added, verbose)
|
|
2716
|
+
];
|
|
2717
|
+
lines.push("");
|
|
2718
|
+
if (changes.length === 0) lines.push("\uC774 \uD3F4\uB354\uC5D0\uC11C \uACE0\uCE5C \uAC83\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
2719
|
+
else lines.push(...changes);
|
|
2720
|
+
if (status.draftPaths > 0) {
|
|
2721
|
+
lines.push("");
|
|
2722
|
+
lines.push(`\uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uC544\uC9C1 \uC548 \uCF20 \uD3B8\uC9D1\uC774 ${status.draftPaths}\uAC1C \uACBD\uB85C\uC5D0 \uC788\uC2B5\uB2C8\uB2E4.`);
|
|
2723
|
+
lines.push(
|
|
2724
|
+
status.mineValid ? "\uC774 \uD3F4\uB354\uC5D0\uC11C \uC62C\uB9B0 \uAC83\uC785\uB2C8\uB2E4. `zalkera publish` \uB85C \uCF1C\uAC70\uB098 `zalkera discard` \uB85C \uBC84\uB9B4 \uC218 \uC788\uC2B5\uB2C8\uB2E4." : "\uC774 \uD3F4\uB354\uC5D0\uC11C \uC62C\uB9B0 \uAC83\uC778\uC9C0\uB294 \uC54C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uADF8 \uC0AC\uC774 \uC0AC\uC774\uD2B8 \uCABD\uC774 \uB2EC\uB77C\uC84C\uC2B5\uB2C8\uB2E4. \uCF58\uC194\uC5D0\uC11C \uBB34\uC5C7\uC774 \uB4E4\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694."
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
for (const blocker of status.blockers) {
|
|
2728
|
+
lines.push("");
|
|
2729
|
+
lines.push(BLOCKER_TEXT[blocker]);
|
|
2730
|
+
}
|
|
2731
|
+
return lines.join("\n");
|
|
2732
|
+
}
|
|
2733
|
+
var BLOCKER_TEXT = {
|
|
2734
|
+
LEDGER_UNKNOWN: "\uC774 \uD3F4\uB354\uC5D0 \uAE30\uC900 \uAE30\uB85D\uC774 \uC5C6\uC5B4 \uC62C\uB9AC\uAE30\uB294 \uB9C9\uD600 \uC788\uC2B5\uB2C8\uB2E4.\n`zalkera baseline` \uC744 \uC2E4\uD589\uD558\uBA74 \uC9C0\uAE08 \uD310\uC744 \uAE30\uC900\uC73C\uB85C \uB2E4\uC2DC \uC138\uC6C1\uB2C8\uB2E4. \uD3F4\uB354\uC758 \uD30C\uC77C\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
|
|
2735
|
+
SERVER_UNREADABLE: "\uC9C0\uAE08 \uC0AC\uC774\uD2B8 \uCABD \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uADF8\uB798\uC11C \uC62C\uB9AC\uAE30\uB294 \uB9C9\uC544 \uB450\uC5C8\uC2B5\uB2C8\uB2E4.\n\uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
|
|
2736
|
+
// ⚠ **있는 명령만 댄다.** 좌초는 사람이 막혀서 다음 걸음을 찾는 자리다 — 없는 동사를 대면
|
|
2737
|
+
// 「모르는 명령입니다」로 끝난다. `discard` 는 T3 에서 실제로 생겼다.
|
|
2738
|
+
STRANDED: "\uC0AC\uC774\uD2B8 \uCABD\uC5D0\uC11C \uD3B8\uC9D1 \uC911\uC774\uB358 \uAC83\uC774 \uC9C0\uAE08 \uBC84\uC804 \uC704\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uADF8\uB300\uB85C\uB294 \uCF24 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n`zalkera discard` \uB85C \uADF8 \uD3B8\uC9D1\uC744 \uBC84\uB9B4 \uC218 \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uBC84\uB9AC\uAE30 \uC804\uC5D0 \uBB34\uC5C7\uC774 \uAC78\uB824 \uC788\uB294\uC9C0 \uBCF4\uC5EC \uC90D\uB2C8\uB2E4."
|
|
2739
|
+
};
|
|
2740
|
+
function describePush(result, verbose = false) {
|
|
2741
|
+
const lines = [];
|
|
2742
|
+
if (result.reconciled === "applied") {
|
|
2743
|
+
lines.push("\uC9C0\uB09C\uBC88\uC5D0 \uC62C\uB9B0 \uAC83\uC774 \uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uB4E4\uC5B4\uAC00 \uC788\uC5C8\uC2B5\uB2C8\uB2E4. \uADF8\uAC83\uC73C\uB85C \uC815\uB9AC\uD588\uC2B5\uB2C8\uB2E4.");
|
|
2744
|
+
} else if (result.reconciled === "not-applied") {
|
|
2745
|
+
lines.push("\uC9C0\uB09C\uBC88\uC5D0 \uC62C\uB9B0 \uAC83\uC774 \uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uC5C6\uC5C8\uC2B5\uB2C8\uB2E4.");
|
|
2746
|
+
}
|
|
2747
|
+
lines.push(
|
|
2748
|
+
result.sent > 0 ? `${result.sent}\uAC1C\uB97C \uC62C\uB838\uC2B5\uB2C8\uB2E4(\uADF8\uC911 \uC9C0\uC6B4 \uAC83 ${result.removed}\uAC1C).` : result.droppedByServer.length > 0 ? "\uC62C\uB9B0 \uAC83\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uB2EC\uB77C\uC9C4 \uAC83\uC774 \uBAA8\uB450 \uC0AC\uC774\uD2B8\uAC00 \uBC1B\uC9C0 \uC54A\uB294 \uACBD\uB85C\uC600\uC2B5\uB2C8\uB2E4." : "\uC62C\uB9B4 \uAC83\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uC774 \uD3F4\uB354\uC758 \uB0B4\uC6A9\uC774 \uC0AC\uC774\uD2B8 \uCABD\uACFC \uAC19\uC2B5\uB2C8\uB2E4."
|
|
2749
|
+
);
|
|
2750
|
+
if (result.retriedAfterConflict) {
|
|
2751
|
+
lines.push("\uC62C\uB9AC\uB294 \uC0AC\uC774\uC5D0 \uC0AC\uC774\uD2B8 \uCABD\uC774 \uB2EC\uB77C\uC838 \uB2E4\uC2DC \uC77D\uACE0 \uD55C \uBC88 \uB354 \uBCF4\uB0C8\uC2B5\uB2C8\uB2E4.");
|
|
2752
|
+
}
|
|
2753
|
+
if (result.droppedByServer.length > 0) {
|
|
2754
|
+
lines.push(
|
|
2755
|
+
`\uC0AC\uC774\uD2B8\uAC00 \uBC1B\uC9C0 \uC54A\uB294 \uACBD\uB85C ${result.droppedByServer.length}\uAC1C\uB294 \uBE7C\uACE0 \uBCF4\uB0C8\uC2B5\uB2C8\uB2E4.`,
|
|
2756
|
+
...bullets(result.droppedByServer, verbose)
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
if (result.previewUrl) lines.push(`\uBBF8\uB9AC\uBCF4\uAE30: ${result.previewUrl}`);
|
|
2760
|
+
if (result.warning) lines.push(result.warning);
|
|
2761
|
+
if (result.sent > 0) lines.push("\uC544\uC9C1 \uC0AC\uC774\uD2B8\uC5D0 \uCF1C\uC9C0\uC9C0\uB294 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 \uCF1C\uB824\uBA74 `zalkera publish` \uB97C \uC2E4\uD589\uD558\uC138\uC694.");
|
|
2762
|
+
return lines;
|
|
2763
|
+
}
|
|
2764
|
+
function describeStranded(plan, verbose = false) {
|
|
2765
|
+
const list = bullets(plan.paths, verbose);
|
|
2766
|
+
if (plan.verdict === "mine") {
|
|
2767
|
+
return [
|
|
2768
|
+
"\uC9C0\uAE08 \uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uAC78\uB824 \uC788\uB294 \uD3B8\uC9D1\uC740 \uC774 \uD3F4\uB354\uC5D0\uC11C \uC62C\uB9B0 \uAC83\uACFC \uAC19\uC2B5\uB2C8\uB2E4.",
|
|
2769
|
+
...list,
|
|
2770
|
+
"\uBC84\uB9AC\uACE0 \uB2E4\uC2DC \uC62C\uB824\uB3C4 \uC774 \uD3F4\uB354\uC758 \uB0B4\uC6A9\uC740 \uADF8\uB300\uB85C\uC785\uB2C8\uB2E4."
|
|
2771
|
+
];
|
|
2772
|
+
}
|
|
2773
|
+
if (plan.paths.length === 0) {
|
|
2774
|
+
return [
|
|
2775
|
+
"\u26A0 \uC9C0\uAE08 \uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uBB34\uC5C7\uC774 \uAC78\uB824 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
|
|
2776
|
+
"\uBC84\uB9AC\uBA74 \uB418\uCC3E\uC744 \uBC29\uBC95\uC774 \uC5C6\uC73C\uB2C8, \uCF58\uC194\uC758 \u300C\uD3B8\uC9D1 \uC911\u300D\uC5D0\uC11C \uBA3C\uC800 \uD655\uC778\uD574 \uC8FC\uC138\uC694."
|
|
2777
|
+
];
|
|
2778
|
+
}
|
|
2779
|
+
return [
|
|
2780
|
+
"\u26A0 \uC9C0\uAE08 \uC0AC\uC774\uD2B8 \uCABD\uC5D0 **\uC774 \uD3F4\uB354\uC5D0 \uC5C6\uB294 \uD3B8\uC9D1**\uC774 \uAC78\uB824 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
2781
|
+
...list,
|
|
2782
|
+
"\uCF58\uC194\uC774\uB098 AI \uB85C \uACE0\uCE5C \uB0B4\uC6A9\uC77C \uC218 \uC788\uACE0, \uBC84\uB9AC\uBA74 \uB418\uCC3E\uC744 \uBC29\uBC95\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2783
|
+
"\uBB34\uC5C7\uC774 \uAC78\uB824 \uC788\uB294\uC9C0 \uCF58\uC194\uC758 \u300C\uD3B8\uC9D1 \uC911\u300D\uC5D0\uC11C \uBA3C\uC800 \uD655\uC778\uD574 \uC8FC\uC138\uC694."
|
|
2784
|
+
];
|
|
2785
|
+
}
|
|
2786
|
+
var DISCARD_PHRASE = "\uBC84\uB9BD\uB2C8\uB2E4";
|
|
2787
|
+
|
|
2788
|
+
// src/main.ts
|
|
2789
|
+
function help() {
|
|
2790
|
+
return `\uC798\uCEE4\uB77C \u2014 \uC0AC\uC774\uD2B8 \uC18C\uC2A4\uB97C \uB85C\uCEEC\uC5D0\uC11C \uB2E4\uB8E8\uB294 \uB3C4\uAD6C (v${version()})
|
|
2791
|
+
|
|
2792
|
+
zalkera login \uBE0C\uB77C\uC6B0\uC800\uB85C \uB85C\uADF8\uC778
|
|
2793
|
+
zalkera logout \uB85C\uADF8\uC778 \uC815\uBCF4\uB97C \uC9C0\uC6B4\uB2E4
|
|
2794
|
+
zalkera status \uC774 \uD3F4\uB354\uC640 \uC0AC\uC774\uD2B8\uAC00 \uC5B4\uB5BB\uAC8C \uB2E4\uB978\uC9C0 \uBCF8\uB2E4
|
|
2795
|
+
zalkera pull \uC0AC\uC774\uD2B8\uC758 \uC9C0\uAE08 \uD310\uC744 \uC774 \uD3F4\uB354\uC5D0 \uBC1B\uB294\uB2E4
|
|
2796
|
+
zalkera push \uC774 \uD3F4\uB354\uC5D0\uC11C \uACE0\uCE5C \uAC83\uC744 \uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uC62C\uB9B0\uB2E4(\uCF1C\uC9C0\uC9C0\uB294 \uC54A\uB294\uB2E4)
|
|
2797
|
+
zalkera publish \uC62C\uB9B0 \uAC83\uC744 \uC0C8 \uBC84\uC804\uC73C\uB85C \uB9CC\uB4E0\uB2E4(\uADF8\uB798\uC57C \uC190\uB2D8\uC5D0\uAC8C \uBCF4\uC778\uB2E4)
|
|
2798
|
+
zalkera rollback <\uD310\uBC88\uD638> \uB77C\uC774\uBE0C\uB97C \uADF8 \uBC84\uC804\uC73C\uB85C \uB418\uB3CC\uB9B0\uB2E4
|
|
2799
|
+
zalkera discard \uC0AC\uC774\uD2B8 \uCABD\uC5D0\uC11C \uD3B8\uC9D1 \uC911\uC778 \uAC83\uC744 \uBC84\uB9B0\uB2E4(\uD310\uC740 \uC548 \uC62E\uAE34\uB2E4)
|
|
2800
|
+
zalkera baseline \uAE30\uC900 \uAE30\uB85D\uB9CC \uC9C0\uAE08 \uD310\uC73C\uB85C \uB2E4\uC2DC \uC138\uC6B4\uB2E4(\uD30C\uC77C\uC740 \uC548 \uAC74\uB4DC\uB9B0\uB2E4)
|
|
2801
|
+
|
|
2802
|
+
\uC635\uC158
|
|
2803
|
+
--site <\uC0AC\uC774\uD2B8\uCF54\uB4DC> \uD3F4\uB354\uAC00 \uC18C\uC18D\uC744 \uBAA8\uB97C \uB54C \uC9C0\uC815\uD55C\uB2E4
|
|
2804
|
+
--folder <\uACBD\uB85C> \uC774 \uD3F4\uB354 \uB300\uC2E0 \uB2E4\uB978 \uD3F4\uB354\uB97C \uB2E4\uB8EC\uB2E4
|
|
2805
|
+
--revision <\uD310\uBC88\uD638> \uBC1B\uC744 \uD310\uC744 \uC9C0\uC815\uD55C\uB2E4(pull \xB7 baseline)
|
|
2806
|
+
--discard-local pull \uC774 \uB9C9\uD790 \uB54C \uACE0\uCE5C \uAC83\uC744 \uC606 \uD3F4\uB354\uB85C \uC62E\uAE30\uACE0 \uC9C4\uD589\uD55C\uB2E4
|
|
2807
|
+
--overwrite-unseen push \uAC00 \uB9C9\uD790 \uB54C \uC0AC\uC774\uD2B8 \uCABD \uD3B8\uC9D1\uC744 \uB36E\uC5B4\uC4F0\uACE0 \uC9C4\uD589\uD55C\uB2E4(\uADF8 \uD3B8\uC9D1\uC740 \uC0AC\uB77C\uC9C4\uB2E4)
|
|
2808
|
+
--label <\uC774\uB984> publish \uAC00 \uBD99\uC77C \uBC84\uC804 \uC774\uB984
|
|
2809
|
+
--discard-pending \uAC8C\uC2DC \uB300\uAE30 AI \uBCC0\uACBD\uC744 \uD568\uAED8 \uBC84\uB9B0\uB2E4(publish\xB7rollback\xB7discard)
|
|
2810
|
+
\u26A0 \uC4F4 \uD06C\uB808\uB527\uC740 \uB3CC\uC544\uC624\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uD3B8\uC9D1\uB9CC \uBC84\uB9B4 \uB54C\uB294 \uD544\uC694 \uC5C6\uB2E4
|
|
2811
|
+
--yes discard \uD655\uC778\uC744 \uBBF8\uB9AC \uC900\uB2E4 \u2014 **\uB0B4\uAC00 \uC62C\uB9B0 \uAC83\uACFC \uAC19\uC744 \uB54C\uB9CC** \uBA39\uB294\uB2E4
|
|
2812
|
+
--confirm \uBC84\uB9BD\uB2C8\uB2E4 \uC5EC\uAE30 \uC5C6\uB294 \uD3B8\uC9D1\uC744 \uBC84\uB9B4 \uB54C \uD544\uC694\uD55C \uBB38\uAD6C(\uD130\uBBF8\uB110\uC774 \uC544\uB2D0 \uB54C)
|
|
2813
|
+
--verbose \uACBD\uB85C\uB97C \uC804\uBD80 \uBCF4\uC5EC \uC900\uB2E4
|
|
2814
|
+
|
|
2815
|
+
\uB85C\uADF8\uC778 \uC815\uBCF4\uB294 ${tokenPath()} \uC5D0 **\uD3C9\uBB38**\uC73C\uB85C \uC800\uC7A5\uB429\uB2C8\uB2E4.
|
|
2816
|
+
\uADF8 \uCEF4\uD4E8\uD130\uB97C \uC4F8 \uC218 \uC788\uB294 \uC0AC\uB78C\uC740 \uC774 \uC0AC\uC774\uD2B8\uB97C \uACE0\uCE58\uACE0 \uBC30\uD3EC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`;
|
|
2817
|
+
}
|
|
2818
|
+
async function main(argv) {
|
|
2819
|
+
const { command, positional, flags } = parseArgs(argv);
|
|
2820
|
+
if (command === "version" || flagOn(flags, "version")) {
|
|
2821
|
+
process.stdout.write(`${version()}
|
|
2822
|
+
`);
|
|
2823
|
+
return 0;
|
|
2824
|
+
}
|
|
2825
|
+
if (command === null || command === "help" || flagOn(flags, "help")) {
|
|
2826
|
+
process.stdout.write(`${help()}
|
|
2827
|
+
`);
|
|
2828
|
+
return 0;
|
|
2829
|
+
}
|
|
2830
|
+
const verbose = flagOn(flags, "verbose");
|
|
2831
|
+
const common = {
|
|
2832
|
+
folder: flagValue(flags, "folder") ?? void 0,
|
|
2833
|
+
tenant: flagValue(flags, "site") ?? void 0
|
|
2834
|
+
};
|
|
2835
|
+
const revisionNo = revisionOf(flags);
|
|
2836
|
+
switch (command) {
|
|
2837
|
+
case "login": {
|
|
2838
|
+
const auth = await openAuth(common);
|
|
2839
|
+
await login(auth.auth, auth.store, { openBrowser });
|
|
2840
|
+
process.stdout.write(`\uB85C\uADF8\uC778\uD588\uC2B5\uB2C8\uB2E4. \uB85C\uADF8\uC778 \uC815\uBCF4\uB294 ${tokenPath()} \uC5D0 \uC788\uC2B5\uB2C8\uB2E4.
|
|
2841
|
+
`);
|
|
2842
|
+
return 0;
|
|
2843
|
+
}
|
|
2844
|
+
case "logout": {
|
|
2845
|
+
await logout(new FileTokenStore());
|
|
2846
|
+
process.stdout.write("\uB85C\uADF8\uC778 \uC815\uBCF4\uB97C \uC9C0\uC6E0\uC2B5\uB2C8\uB2E4.\n");
|
|
2847
|
+
return 0;
|
|
2848
|
+
}
|
|
2849
|
+
case "status": {
|
|
2850
|
+
const context = await openContext(common);
|
|
2851
|
+
const draft = await context.api.draftFiles().catch(() => null);
|
|
2852
|
+
const active = await context.api.listRevisions(20).then((rows) => rows.find((r) => r.isActive)?.revisionNo ?? null).catch(() => null);
|
|
2853
|
+
const ledger = await readLedger(context.folder);
|
|
2854
|
+
const status = syncStatus({
|
|
2855
|
+
ledger,
|
|
2856
|
+
local: await hashWorkdir(context.folder),
|
|
2857
|
+
draft,
|
|
2858
|
+
activeRevisionNo: active
|
|
2859
|
+
});
|
|
2860
|
+
process.stdout.write(`${describeStatus(status, verbose)}
|
|
2861
|
+
`);
|
|
2862
|
+
const corrected = ledgerCorrection(ledger, draft);
|
|
2863
|
+
if (corrected) await writeLedger(context.folder, corrected);
|
|
2864
|
+
return status.blockers.length > 0 ? 1 : 0;
|
|
2865
|
+
}
|
|
2866
|
+
case "pull": {
|
|
2867
|
+
const context = await openContext(common);
|
|
2868
|
+
const result = await pullSiteSource({
|
|
2869
|
+
api: context.api,
|
|
2870
|
+
folder: context.folder,
|
|
2871
|
+
revisionNo,
|
|
2872
|
+
discardLocal: flagOn(flags, "discard-local"),
|
|
2873
|
+
listAll: verbose,
|
|
2874
|
+
onProgress: (message) => process.stderr.write(`${message}
|
|
2875
|
+
`)
|
|
2876
|
+
});
|
|
2877
|
+
const parts = [
|
|
2878
|
+
`${result.revisionNo}\uD310\uC744 \uBC1B\uC558\uC2B5\uB2C8\uB2E4.`,
|
|
2879
|
+
`\uC0C8\uB85C \uC4F4 \uAC83 ${result.written}\uAC1C \xB7 \uC9C0\uC6B4 \uAC83 ${result.deleted}\uAC1C \xB7 \uADF8\uB300\uB85C \uB454 \uAC83 ${result.unchanged}\uAC1C`
|
|
2880
|
+
];
|
|
2881
|
+
if (result.untracked > 0) parts.push(`\uC774 \uD3F4\uB354\uC5D0\uB9CC \uC788\uB294 \uD30C\uC77C ${result.untracked}\uAC1C\uB294 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`);
|
|
2882
|
+
if (result.savedTo) parts.push(`\uACE0\uCCD0 \uB450\uC5C8\uB358 \uAC83\uC740 ${result.savedTo} \uC5D0 \uC62E\uACA8 \uB450\uC5C8\uC2B5\uB2C8\uB2E4.`);
|
|
2883
|
+
if (result.foreignLedger) {
|
|
2884
|
+
parts.push("\uC774 \uD3F4\uB354\uC5D0 \uB2E4\uB978 \uC0AC\uC774\uD2B8\uC758 \uAE30\uC900 \uAE30\uB85D\uC774 \uC788\uC5B4 \uC4F0\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC9C0\uAE08\uC740 \uC774 \uC0AC\uC774\uD2B8 \uAC83\uC73C\uB85C \uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
2885
|
+
}
|
|
2886
|
+
if (result.deletionsUnknown) {
|
|
2887
|
+
parts.push(
|
|
2888
|
+
"\uC774 \uD3F4\uB354\uC5D0\uB294 \uAE30\uC900 \uAE30\uB85D\uC774 \uC5C6\uC5C8\uC2B5\uB2C8\uB2E4. \uADF8\uB798\uC11C \uC0AC\uC774\uD2B8\uC5D0\uC11C \uC9C0\uC6CC\uC9C4 \uD30C\uC77C\uC774 \uC5EC\uAE30 \uB0A8\uC544 \uC788\uC5B4\uB3C4 \uC774\uBC88\uC5D0\uB294 \uC9C0\uC6B0\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
|
|
2889
|
+
"\uC9C0\uAE08 \uB0A8\uC544 \uC788\uB294 \uAC83 \uC911 \uC0AC\uC774\uD2B8\uC5D0 \uC5C6\uB294 \uD30C\uC77C\uC774 \uC788\uB294\uC9C0 `zalkera status` \uB85C \uD655\uC778\uD574 \uC8FC\uC138\uC694."
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
if (result.serverExcluded.length > 0) {
|
|
2893
|
+
parts.push(
|
|
2894
|
+
`\uC0AC\uC774\uD2B8\uAC00 \uBCF4\uB0B8 \uAC83 \uC911 ${result.serverExcluded.length}\uAC1C\uB294 \uC774 \uB3C4\uAD6C\uAC00 \uB2E4\uB8E8\uC9C0 \uC54A\uB294 \uD30C\uC77C\uC774\uB77C \uBC1B\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`,
|
|
2895
|
+
...pathLines(result.serverExcluded, verbose)
|
|
2896
|
+
);
|
|
2897
|
+
}
|
|
2898
|
+
if (!result.ledgerWritten) {
|
|
2899
|
+
parts.push(
|
|
2900
|
+
"\uB2E4\uB9CC \uAE30\uC900 \uAE30\uB85D\uC744 \uC4F0\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uD30C\uC77C\uC740 \uC0C8 \uD310\uC785\uB2C8\uB2E4.",
|
|
2901
|
+
"`zalkera baseline` \uC744 \uD55C \uBC88 \uC2E4\uD589\uD574 \uC8FC\uC138\uC694 \u2014 \uD3F4\uB354\uC758 \uD30C\uC77C\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
process.stdout.write(`${parts.join("\n")}
|
|
2905
|
+
`);
|
|
2906
|
+
return result.ledgerWritten ? 0 : 1;
|
|
2907
|
+
}
|
|
2908
|
+
case "push": {
|
|
2909
|
+
const context = await openContext(common);
|
|
2910
|
+
const result = await pushSiteSource({
|
|
2911
|
+
api: context.api,
|
|
2912
|
+
folder: context.folder,
|
|
2913
|
+
// ⚠ **명시 동의다.** 이 손잡이가 없으면 사이트 쪽에 걸려 있던 남의 편집이 조용히
|
|
2914
|
+
// 사라진다 — 선행조건은 그것을 못 막는다.
|
|
2915
|
+
overwriteUnseen: flagOn(flags, "overwrite-unseen"),
|
|
2916
|
+
listAll: verbose,
|
|
2917
|
+
onProgress: (message) => process.stderr.write(`${message}
|
|
2918
|
+
`)
|
|
2919
|
+
});
|
|
2920
|
+
const parts = describePush(result, verbose);
|
|
2921
|
+
process.stdout.write(`${parts.join("\n")}
|
|
2922
|
+
`);
|
|
2923
|
+
return 0;
|
|
2924
|
+
}
|
|
2925
|
+
case "publish": {
|
|
2926
|
+
const context = await openContext(common);
|
|
2927
|
+
const result = await publishDraft({
|
|
2928
|
+
api: context.api,
|
|
2929
|
+
folder: context.folder,
|
|
2930
|
+
label: flagValue(flags, "label") ?? void 0,
|
|
2931
|
+
// ⚠ **명시 동의다.** 게시 대기 AI 변경이 함께 사라진다.
|
|
2932
|
+
discardPendingChanges: flagOn(flags, "discard-pending"),
|
|
2933
|
+
onProgress: (message) => process.stderr.write(`${message}
|
|
2934
|
+
`)
|
|
2935
|
+
});
|
|
2936
|
+
const lines = [
|
|
2937
|
+
`${result.revisionNo}\uD310\uC73C\uB85C \uC62C\uB838\uC2B5\uB2C8\uB2E4.`,
|
|
2938
|
+
result.siteType === "STATIC" ? "\uC9C0\uAE08 \uBC14\uB85C \uC190\uB2D8\uC5D0\uAC8C \uBCF4\uC785\uB2C8\uB2E4." : "\uC0AC\uC774\uD2B8\uB97C \uB2E4\uC2DC \uC9D3\uB294 \uC911\uC785\uB2C8\uB2E4 \u2014 \uB2E4 \uC9C0\uC5B4\uC9C0\uBA74 \uC790\uB3D9\uC73C\uB85C \uC190\uB2D8\uC5D0\uAC8C \uBCF4\uC785\uB2C8\uB2E4."
|
|
2939
|
+
];
|
|
2940
|
+
if (result.capabilityNote) lines.push(result.capabilityNote);
|
|
2941
|
+
if (!result.ledgerRebuilt) {
|
|
2942
|
+
lines.push(
|
|
2943
|
+
"",
|
|
2944
|
+
"\uB2E4\uB9CC \uC0C8 \uBC84\uC804\uC758 \uD30C\uC77C \uBAA9\uB85D\uC744 \uC77D\uC9C0 \uBABB\uD574 \uC774 \uD3F4\uB354\uC758 \uAE30\uC900 \uAE30\uB85D\uC744 \uC9C0\uC6E0\uC2B5\uB2C8\uB2E4.",
|
|
2945
|
+
"`zalkera baseline` \uC744 \uD55C \uBC88 \uC2E4\uD589\uD574 \uC8FC\uC138\uC694 \u2014 \uD3F4\uB354\uC758 \uD30C\uC77C\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
process.stdout.write(`${lines.join("\n")}
|
|
2949
|
+
`);
|
|
2950
|
+
return 0;
|
|
2951
|
+
}
|
|
2952
|
+
case "rollback": {
|
|
2953
|
+
const target = revisionArg(positional);
|
|
2954
|
+
const context = await openContext(common);
|
|
2955
|
+
const result = await rollbackRevision({
|
|
2956
|
+
api: context.api,
|
|
2957
|
+
folder: context.folder,
|
|
2958
|
+
revisionNo: target,
|
|
2959
|
+
// ⚠ 이 손잡이는 **게시 대기 AI 변경** 폐기 동의다 — 「편집 중인 것」이 아니다.
|
|
2960
|
+
// 편집이 있으면 서버가 플래그와 무관하게 되돌리기를 거절한다(가드 5층).
|
|
2961
|
+
discardPending: flagOn(flags, "discard-pending"),
|
|
2962
|
+
onProgress: (message) => process.stderr.write(`${message}
|
|
2963
|
+
`)
|
|
2964
|
+
});
|
|
2965
|
+
const lines = [
|
|
2966
|
+
result.revisionNo === null ? "\uB418\uB3CC\uB838\uC2B5\uB2C8\uB2E4 \u2014 \uB2E4\uB9CC \uC9C0\uAE08 \uCF1C\uC9C4 \uBC84\uC804\uC758 \uBC88\uD638\uB97C \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4." : result.pointerMoved ? `${result.revisionNo}\uD310\uC73C\uB85C \uB418\uB3CC\uB838\uC2B5\uB2C8\uB2E4.` : `\uC774\uBBF8 ${result.revisionNo}\uD310\uC774\uC5C8\uC2B5\uB2C8\uB2E4.`
|
|
2967
|
+
];
|
|
2968
|
+
if (result.revisionNo === null) {
|
|
2969
|
+
lines.push("`zalkera status` \uB85C \uD655\uC778\uD574 \uC8FC\uC138\uC694. **\uB2E4\uC2DC \uB418\uB3CC\uB9AC\uC9C0 \uB9C8\uC138\uC694** \u2014 \uAC19\uC740 \uB0B4\uC6A9\uC758 \uBC84\uC804\uC774 \uD558\uB098 \uB354 \uC0DD\uAE41\uB2C8\uB2E4.");
|
|
2970
|
+
}
|
|
2971
|
+
if (result.revisionNo !== null && result.revisionNo !== result.requested) {
|
|
2972
|
+
lines.push(
|
|
2973
|
+
`(${result.requested}\uD310\uC758 \uB0B4\uC6A9\uC73C\uB85C **\uC0C8 ${result.revisionNo}\uD310**\uC744 \uC138\uC6CC \uCF30\uC2B5\uB2C8\uB2E4 \u2014 \uC6D0\uC7A5\uC740 \uB418\uAC10\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.)`
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
if (result.discardedPendingChanges > 0) {
|
|
2977
|
+
lines.push(`\uAC8C\uC2DC \uB300\uAE30 \uBCC0\uACBD ${result.discardedPendingChanges}\uAC74\uB3C4 \uD568\uAED8 \uBC84\uB838\uC2B5\uB2C8\uB2E4.`);
|
|
2978
|
+
}
|
|
2979
|
+
lines.push("", "\uC774 \uD3F4\uB354\uC758 \uD30C\uC77C\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 \uB9DE\uCD94\uB824\uBA74 `zalkera pull` \uC744 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
2980
|
+
if (result.differing.length > 0) {
|
|
2981
|
+
lines.push(
|
|
2982
|
+
`\u26A0 \uC774 \uD3F4\uB354\uC758 ${result.differing.length}\uAC1C \uD30C\uC77C\uC774 ${result.revisionNo}\uD310\uACFC \uB2E4\uB985\uB2C8\uB2E4.`,
|
|
2983
|
+
...pathLines(result.differing, verbose),
|
|
2984
|
+
`\uC9C0\uAE08 \`zalkera push\` \uB97C \uD558\uBA74 \uC774 ${result.differing.length}\uAC1C\uAC00 **\uC804\uBD80** \uC62C\uB77C\uAC00 ${result.revisionNo}\uD310\uC758 \uB0B4\uC6A9\uC744 \uB36E\uC2B5\uB2C8\uB2E4.`
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
if (!result.ledgerRebuilt) {
|
|
2988
|
+
lines.push("", "\uC0C8 \uAE30\uC900\uC758 \uD30C\uC77C \uBAA9\uB85D\uC744 \uC77D\uC9C0 \uBABB\uD574 \uAE30\uC900 \uAE30\uB85D\uC744 \uC9C0\uC6E0\uC2B5\uB2C8\uB2E4. `zalkera baseline` \uC744 \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.");
|
|
2989
|
+
}
|
|
2990
|
+
process.stdout.write(`${lines.join("\n")}
|
|
2991
|
+
`);
|
|
2992
|
+
return 0;
|
|
2993
|
+
}
|
|
2994
|
+
case "discard": {
|
|
2995
|
+
const context = await openContext(common);
|
|
2996
|
+
const draft = await context.api.draftFiles().catch(() => null);
|
|
2997
|
+
const plan = planStranded({ ledger: await readLedger(context.folder), draft });
|
|
2998
|
+
if (plan.empty) {
|
|
2999
|
+
process.stdout.write("\uC0AC\uC774\uD2B8 \uCABD\uC5D0 \uD3B8\uC9D1 \uC911\uC778 \uAC83\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uBC84\uB9B4 \uAC83\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
|
|
3000
|
+
return 0;
|
|
3001
|
+
}
|
|
3002
|
+
process.stderr.write(`${describeStranded(plan, verbose).join("\n")}
|
|
3003
|
+
`);
|
|
3004
|
+
const agreed = plan.verdict === "mine" ? await confirm({
|
|
3005
|
+
question: "\uBC84\uB9B4\uAE4C\uC694? (y/n)",
|
|
3006
|
+
given: flagOn(flags, "yes"),
|
|
3007
|
+
flag: "--yes"
|
|
3008
|
+
}) : await confirm({
|
|
3009
|
+
question: `\uADF8\uB798\uB3C4 \uBC84\uB9AC\uB824\uBA74 \uC544\uB798\uB97C \uADF8\uB300\uB85C \uC785\uB825\uD574 \uC8FC\uC138\uC694: \`${DISCARD_PHRASE}\``,
|
|
3010
|
+
phrase: DISCARD_PHRASE,
|
|
3011
|
+
given: flagValue(flags, "confirm") === DISCARD_PHRASE,
|
|
3012
|
+
flag: `--confirm ${DISCARD_PHRASE}`
|
|
3013
|
+
});
|
|
3014
|
+
if (!agreed) {
|
|
3015
|
+
process.stdout.write("\uC544\uBB34\uAC83\uB3C4 \uBC84\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n");
|
|
3016
|
+
return 1;
|
|
3017
|
+
}
|
|
3018
|
+
const result = await discardDraft({
|
|
3019
|
+
api: context.api,
|
|
3020
|
+
folder: context.folder,
|
|
3021
|
+
discardPending: flagOn(flags, "discard-pending"),
|
|
3022
|
+
// 사람에게 보여 준 **그 판정**을 넘긴다 — 다시 조회하면 확인한 것과 버리는 것이 갈린다.
|
|
3023
|
+
plan,
|
|
3024
|
+
onProgress: (message) => process.stderr.write(`${message}
|
|
3025
|
+
`)
|
|
3026
|
+
});
|
|
3027
|
+
const lines = [result.hadDraft ? "\uD3B8\uC9D1 \uC911\uC774\uB358 \uAC83\uC744 \uBC84\uB838\uC2B5\uB2C8\uB2E4." : "\uBC84\uB9B4 \uD3B8\uC9D1\uC774 \uC5C6\uC5C8\uC2B5\uB2C8\uB2E4."];
|
|
3028
|
+
if (result.discardedPendingChanges > 0) {
|
|
3029
|
+
lines.push(`\uAC8C\uC2DC \uB300\uAE30 \uBCC0\uACBD ${result.discardedPendingChanges}\uAC74\uB3C4 \uD568\uAED8 \uBC84\uB838\uC2B5\uB2C8\uB2E4.`);
|
|
3030
|
+
}
|
|
3031
|
+
if (result.pointerMoved) {
|
|
3032
|
+
lines.push("\u26A0 \uADF8 \uC0AC\uC774 \uC0AC\uC774\uD2B8 \uCABD\uC774 \uB2EC\uB77C\uC838 **\uB77C\uC774\uBE0C \uBC84\uC804\uB3C4 \uD568\uAED8 \uC6C0\uC9C1\uC600\uC2B5\uB2C8\uB2E4** \u2014 `zalkera status` \uB85C \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
|
|
3033
|
+
}
|
|
3034
|
+
lines.push("\uC774 \uD3F4\uB354\uC758 \uD30C\uC77C\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
|
3035
|
+
process.stdout.write(`${lines.join("\n")}
|
|
3036
|
+
`);
|
|
3037
|
+
return 0;
|
|
3038
|
+
}
|
|
3039
|
+
case "baseline": {
|
|
3040
|
+
const context = await openContext(common);
|
|
3041
|
+
const result = await rebuildBaseline({
|
|
3042
|
+
api: context.api,
|
|
3043
|
+
folder: context.folder,
|
|
3044
|
+
revisionNo,
|
|
3045
|
+
onProgress: (message) => process.stderr.write(`${message}
|
|
3046
|
+
`)
|
|
3047
|
+
});
|
|
3048
|
+
const lines = [
|
|
3049
|
+
`${result.revisionNo}\uD310(\uD30C\uC77C ${result.files}\uAC1C)\uC744 \uAE30\uC900\uC73C\uB85C ${result.replaced ? "\uB2E4\uC2DC " : ""}\uC138\uC6E0\uC2B5\uB2C8\uB2E4.`,
|
|
3050
|
+
"\uD3F4\uB354\uC758 \uD30C\uC77C\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4."
|
|
3051
|
+
];
|
|
3052
|
+
if (result.differing.length > 0) {
|
|
3053
|
+
lines.push(
|
|
3054
|
+
"",
|
|
3055
|
+
`\u26A0 \uC774 \uD3F4\uB354\uC758 ${result.differing.length}\uAC1C \uD30C\uC77C\uC774 ${result.revisionNo}\uD310\uACFC \uB2E4\uB985\uB2C8\uB2E4.`,
|
|
3056
|
+
...pathLines(result.differing, verbose),
|
|
3057
|
+
`\uC9C0\uAE08 \`zalkera push\` \uB97C \uD558\uBA74 \uC774 ${result.differing.length}\uAC1C\uAC00 **\uC804\uBD80** \uC62C\uB77C\uAC00 ${result.revisionNo}\uD310\uC758 \uB0B4\uC6A9\uC744 \uB36E\uC2B5\uB2C8\uB2E4.`,
|
|
3058
|
+
"\uACE0\uCE5C \uAC83\uC774 \uADF8\uC911 \uC77C\uBD80\uBFD0\uC774\uB77C\uBA74 `zalkera pull` \uB85C \uBC1B\uB294 \uCABD\uC774 \uB9DE\uC2B5\uB2C8\uB2E4."
|
|
3059
|
+
);
|
|
3060
|
+
} else {
|
|
3061
|
+
lines.push("\uC9C0\uAE08 \uC0C1\uD0DC\uB294 `zalkera status` \uB85C \uBCFC \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
3062
|
+
}
|
|
3063
|
+
if (result.serverExcluded.length > 0) {
|
|
3064
|
+
lines.push(
|
|
3065
|
+
"",
|
|
3066
|
+
`\uC0AC\uC774\uD2B8\uAC00 \uBCF4\uB0B8 \uAC83 \uC911 ${result.serverExcluded.length}\uAC1C\uB294 \uC774 \uB3C4\uAD6C\uAC00 \uB2E4\uB8E8\uC9C0 \uC54A\uB294 \uD30C\uC77C\uC774\uB77C \uAE30\uC900\uC5D0 \uB123\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`,
|
|
3067
|
+
...pathLines(result.serverExcluded, verbose)
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
process.stdout.write(`${lines.join("\n")}
|
|
3071
|
+
`);
|
|
3072
|
+
return 0;
|
|
3073
|
+
}
|
|
3074
|
+
default:
|
|
3075
|
+
process.stderr.write(`\uBAA8\uB974\uB294 \uBA85\uB839\uC785\uB2C8\uB2E4: ${command}
|
|
3076
|
+
|
|
3077
|
+
${help()}
|
|
3078
|
+
`);
|
|
3079
|
+
return 2;
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
function revisionArg(positional) {
|
|
3083
|
+
const raw = positional[0];
|
|
3084
|
+
const value = Number(raw);
|
|
3085
|
+
if (raw === void 0 || !Number.isInteger(value) || value <= 0) {
|
|
3086
|
+
throw new DevtoolsError(
|
|
3087
|
+
"SERVER_REJECTED",
|
|
3088
|
+
`\uB418\uB3CC\uB9B4 \uBC84\uC804 \uBC88\uD638\uB97C \uC8FC\uC138\uC694(\uC608: \`zalkera rollback 5\`).${raw === void 0 ? "" : ` \uBC1B\uC740 \uAC12: ${raw}`}`,
|
|
3089
|
+
"`zalkera status` \uB85C \uC9C0\uAE08 \uCF1C\uC9C4 \uBC84\uC804\uC744 \uBCFC \uC218 \uC788\uC2B5\uB2C8\uB2E4."
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
3092
|
+
return value;
|
|
3093
|
+
}
|
|
3094
|
+
function revisionOf(flags) {
|
|
3095
|
+
const raw = flagValue(flags, "revision");
|
|
3096
|
+
if (raw === null) return void 0;
|
|
3097
|
+
const value = Number(raw);
|
|
3098
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
3099
|
+
throw new DevtoolsError("SERVER_REJECTED", `\uD310 \uBC88\uD638\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${raw}`);
|
|
3100
|
+
}
|
|
3101
|
+
return value;
|
|
3102
|
+
}
|
|
3103
|
+
async function openBrowser(url) {
|
|
3104
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
3105
|
+
await new Promise((resolve9, reject) => {
|
|
3106
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
3107
|
+
child.on("error", reject);
|
|
3108
|
+
child.on("spawn", () => {
|
|
3109
|
+
child.unref();
|
|
3110
|
+
resolve9();
|
|
3111
|
+
});
|
|
3112
|
+
}).catch(() => {
|
|
3113
|
+
process.stderr.write(`\uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uC774 \uC8FC\uC18C\uB97C \uC9C1\uC811 \uC5F4\uC5B4 \uC8FC\uC138\uC694:
|
|
3114
|
+
${url}
|
|
3115
|
+
`);
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
var code = await main(process.argv.slice(2)).catch((error) => {
|
|
3119
|
+
if (error instanceof DevtoolsError) {
|
|
3120
|
+
process.stderr.write(`${error.humanMessage}
|
|
3121
|
+
`);
|
|
3122
|
+
return 1;
|
|
3123
|
+
}
|
|
3124
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
3125
|
+
`);
|
|
3126
|
+
return 1;
|
|
3127
|
+
});
|
|
3128
|
+
process.exit(code);
|