@saeroon/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,4595 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import { createRequire } from "module";
6
+
7
+ // src/commands/login.ts
8
+ import chalk3 from "chalk";
9
+ import { createInterface } from "readline/promises";
10
+ import { stdin, stdout } from "process";
11
+
12
+ // src/lib/config.ts
13
+ import chalk from "chalk";
14
+ import { readFile, writeFile, mkdir, chmod, appendFile } from "fs/promises";
15
+ import { resolve, dirname } from "path";
16
+ import { existsSync } from "fs";
17
+ import { isIP } from "net";
18
+
19
+ // src/lib/safe-json.ts
20
+ import sjson from "secure-json-parse";
21
+ function safeJsonParse(text) {
22
+ return sjson.parse(text, void 0, {
23
+ protoAction: "remove",
24
+ constructorAction: "remove"
25
+ });
26
+ }
27
+
28
+ // src/lib/config.ts
29
+ var CONFIG_DIR = ".saeroon";
30
+ var CONFIG_FILE = `${CONFIG_DIR}/config.json`;
31
+ var DEFAULT_API_BASE_URL = "https://api.saeroon.com";
32
+ async function loadConfig() {
33
+ const configPath = resolve(process.cwd(), CONFIG_FILE);
34
+ if (!existsSync(configPath)) {
35
+ return {};
36
+ }
37
+ try {
38
+ const content = await readFile(configPath, "utf-8");
39
+ return safeJsonParse(content);
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+ async function saveConfig(config) {
45
+ const configPath = resolve(process.cwd(), CONFIG_FILE);
46
+ const configDir = dirname(configPath);
47
+ if (!existsSync(configDir)) {
48
+ await mkdir(configDir, { recursive: true });
49
+ }
50
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
51
+ await chmod(configPath, 384);
52
+ await ensureGitignore();
53
+ }
54
+ async function ensureGitignore() {
55
+ const gitignorePath = resolve(process.cwd(), ".gitignore");
56
+ const entry = ".saeroon/";
57
+ if (existsSync(gitignorePath)) {
58
+ const content = await readFile(gitignorePath, "utf-8");
59
+ const lines = content.split("\n").map((l) => l.trim());
60
+ if (lines.includes(entry)) {
61
+ return;
62
+ }
63
+ const prefix = content.endsWith("\n") ? "" : "\n";
64
+ await appendFile(gitignorePath, `${prefix}${entry}
65
+ `, "utf-8");
66
+ } else {
67
+ await writeFile(gitignorePath, `${entry}
68
+ `, "utf-8");
69
+ }
70
+ }
71
+ async function resolveApiKey(cliApiKey) {
72
+ const envApiKey = process.env.SAEROON_API_KEY;
73
+ if (envApiKey) {
74
+ return envApiKey;
75
+ }
76
+ if (cliApiKey) {
77
+ console.warn(
78
+ chalk.yellow(
79
+ "\uACBD\uACE0: --api-key \uC778\uC790\uB294 \uD504\uB85C\uC138\uC2A4 \uBAA9\uB85D(ps)\uC5D0 \uB178\uCD9C\uB429\uB2C8\uB2E4. SAEROON_API_KEY \uD658\uACBD \uBCC0\uC218 \uB610\uB294 `saeroon login`\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4."
80
+ )
81
+ );
82
+ return cliApiKey;
83
+ }
84
+ const config = await loadConfig();
85
+ if (config.apiKey) {
86
+ return config.apiKey;
87
+ }
88
+ console.error(chalk.red("API Key\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4."));
89
+ console.error(
90
+ chalk.yellow(
91
+ "npx @saeroon/cli login \uC73C\uB85C \uC124\uC815\uD558\uAC70\uB098 SAEROON_API_KEY \uD658\uACBD \uBCC0\uC218\uB97C \uC0AC\uC6A9\uD558\uC138\uC694."
92
+ )
93
+ );
94
+ process.exit(1);
95
+ }
96
+ async function getApiBaseUrl() {
97
+ const config = await loadConfig();
98
+ const url = config.apiBaseUrl || DEFAULT_API_BASE_URL;
99
+ return validateApiBaseUrl(url);
100
+ }
101
+ var PRIVATE_IP_PATTERNS = [
102
+ /^10\./,
103
+ /^172\.(1[6-9]|2\d|3[01])\./,
104
+ /^192\.168\./,
105
+ /^169\.254\./,
106
+ /^0\./,
107
+ /^::1$/,
108
+ /^fc00:/i,
109
+ /^fe80:/i
110
+ ];
111
+ function validateApiBaseUrl(urlStr) {
112
+ let parsed;
113
+ try {
114
+ parsed = new URL(urlStr);
115
+ } catch {
116
+ throw new Error(`\uC798\uBABB\uB41C API URL \uD615\uC2DD: ${urlStr}`);
117
+ }
118
+ if (parsed.protocol === "http:") {
119
+ const host = parsed.hostname;
120
+ if (host !== "localhost" && host !== "127.0.0.1" && host !== "::1") {
121
+ throw new Error(
122
+ `\uC548\uC804\uD558\uC9C0 \uC54A\uC740 \uD504\uB85C\uD1A0\uCF5C: ${parsed.protocol} (localhost \uC678\uC5D0\uB294 https\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4)`
123
+ );
124
+ }
125
+ } else if (parsed.protocol !== "https:") {
126
+ throw new Error(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD504\uB85C\uD1A0\uCF5C: ${parsed.protocol}`);
127
+ }
128
+ const hostname = parsed.hostname;
129
+ if (isIP(hostname) && hostname !== "127.0.0.1" && hostname !== "::1") {
130
+ for (const pattern of PRIVATE_IP_PATTERNS) {
131
+ if (pattern.test(hostname)) {
132
+ throw new Error(`\uB0B4\uBD80 \uB124\uD2B8\uC6CC\uD06C \uC8FC\uC18C\uB294 \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${hostname}`);
133
+ }
134
+ }
135
+ }
136
+ return urlStr;
137
+ }
138
+
139
+ // src/lib/api-client.ts
140
+ import { readFile as readFile2 } from "fs/promises";
141
+ import { basename, resolve as resolve3 } from "path";
142
+
143
+ // src/lib/path-guard.ts
144
+ import { resolve as resolve2, sep } from "path";
145
+ function assertWithinCwd(absolutePath) {
146
+ const cwd = resolve2(process.cwd());
147
+ if (!absolutePath.startsWith(cwd + sep) && absolutePath !== cwd) {
148
+ throw new Error(
149
+ `\uBCF4\uC548 \uC704\uBC18: \uACBD\uB85C\uAC00 \uD504\uB85C\uC81D\uD2B8 \uB514\uB809\uD1A0\uB9AC\uB97C \uBC97\uC5B4\uB0A9\uB2C8\uB2E4`
150
+ );
151
+ }
152
+ }
153
+
154
+ // src/lib/api-client.ts
155
+ var ApiError = class extends Error {
156
+ constructor(code, message, statusCode) {
157
+ super(message);
158
+ this.code = code;
159
+ this.statusCode = statusCode;
160
+ this.name = "ApiError";
161
+ }
162
+ };
163
+ var SaeroonApiClient = class _SaeroonApiClient {
164
+ constructor(apiKey, baseUrl) {
165
+ this.apiKey = apiKey;
166
+ this.baseUrl = validateApiBaseUrl(baseUrl);
167
+ }
168
+ baseUrl;
169
+ // --- Preview ---
170
+ /**
171
+ * Preview 사이트를 생성. 스키마 유효성 검사 + 프리뷰 URL 반환.
172
+ * POST /api/v1/hosting/developer/preview-sites
173
+ */
174
+ async createPreviewSite(schema) {
175
+ return this.request(
176
+ "POST",
177
+ "/api/v1/hosting/developer/preview-sites",
178
+ { schema },
179
+ true
180
+ );
181
+ }
182
+ /**
183
+ * Preview 사이트 정보를 조회.
184
+ * GET /api/v1/hosting/preview-sites/{id}
185
+ */
186
+ async getPreviewSite(id) {
187
+ return this.request(
188
+ "GET",
189
+ `/api/v1/hosting/preview-sites/${encodeURIComponent(id)}`,
190
+ void 0,
191
+ true
192
+ );
193
+ }
194
+ /**
195
+ * 기존 Preview 사이트 스키마 업데이트.
196
+ * PUT /api/v1/hosting/preview-sites/{id}
197
+ */
198
+ async updatePreviewSite(id, schema) {
199
+ return this.request(
200
+ "PUT",
201
+ `/api/v1/hosting/preview-sites/${encodeURIComponent(id)}`,
202
+ { schema },
203
+ true
204
+ );
205
+ }
206
+ // --- Developer Preview (WebSocket session) ---
207
+ /**
208
+ * Developer Preview 세션을 생성. WebSocket 연결 정보 반환.
209
+ * POST /api/v1/hosting/developer/preview
210
+ */
211
+ async createPreviewSession(schema, device) {
212
+ return this.request(
213
+ "POST",
214
+ "/api/v1/hosting/developer/preview",
215
+ { schemaJson: JSON.stringify(schema), device },
216
+ true
217
+ );
218
+ }
219
+ // --- Validation ---
220
+ /**
221
+ * 스키마 유효성 검사. Preview 사이트 생성 API의 validation 결과를 활용.
222
+ * POST /api/v1/hosting/preview-sites
223
+ */
224
+ async validateSchema(schema) {
225
+ return this.createPreviewSite(schema);
226
+ }
227
+ // --- Block Catalog ---
228
+ /**
229
+ * 블록 카탈로그 전체 목록 조회. 인증 불필요 (public).
230
+ * GET /api/v1/hosting/blocks/catalog
231
+ */
232
+ async getBlockCatalog() {
233
+ return this.request(
234
+ "GET",
235
+ "/api/v1/hosting/blocks/catalog",
236
+ void 0,
237
+ false
238
+ );
239
+ }
240
+ /**
241
+ * 특정 블록 타입의 상세 정보 조회. 인증 불필요 (public).
242
+ * GET /api/v1/hosting/blocks/catalog/{blockType}
243
+ */
244
+ async getBlockDetail(blockType) {
245
+ return this.request(
246
+ "GET",
247
+ `/api/v1/hosting/blocks/catalog/${encodeURIComponent(blockType)}`,
248
+ void 0,
249
+ false
250
+ );
251
+ }
252
+ // --- Template Submission ---
253
+ /**
254
+ * 템플릿을 마켓플레이스에 제출.
255
+ * POST /api/v1/hosting/templates
256
+ */
257
+ async submitTemplate(schema, metadata) {
258
+ return this.request(
259
+ "POST",
260
+ "/api/v1/hosting/templates",
261
+ { schema, ...metadata },
262
+ true
263
+ );
264
+ }
265
+ /**
266
+ * 제출된 템플릿의 심사 상태 조회.
267
+ * GET /api/v1/hosting/templates/{id}/status
268
+ */
269
+ async getTemplateStatus(id) {
270
+ return this.request(
271
+ "GET",
272
+ `/api/v1/hosting/templates/${encodeURIComponent(id)}/status`,
273
+ void 0,
274
+ true
275
+ );
276
+ }
277
+ // --- Developer Template Management ---
278
+ /**
279
+ * 개발자의 템플릿 목록 조회.
280
+ * GET /api/v1/hosting/developer/templates
281
+ */
282
+ async getTemplates() {
283
+ return this.request(
284
+ "GET",
285
+ "/api/v1/hosting/developer/templates",
286
+ void 0,
287
+ true
288
+ );
289
+ }
290
+ /**
291
+ * 소유 사이트를 마켓플레이스 템플릿으로 등록.
292
+ * POST /api/v1/hosting/developer/templates/register
293
+ */
294
+ async registerTemplate(request) {
295
+ return this.request(
296
+ "POST",
297
+ "/api/v1/hosting/developer/templates/register",
298
+ request,
299
+ true
300
+ );
301
+ }
302
+ /**
303
+ * 템플릿 메타데이터 수정.
304
+ * PATCH /api/v1/hosting/developer/templates/{id}
305
+ */
306
+ async updateTemplate(id, request) {
307
+ return this.request(
308
+ "PATCH",
309
+ `/api/v1/hosting/developer/templates/${encodeURIComponent(id)}`,
310
+ request,
311
+ true
312
+ );
313
+ }
314
+ /**
315
+ * 소스 사이트 스키마를 템플릿에 동기화 + 새 버전 발행.
316
+ * POST /api/v1/hosting/developer/templates/{id}/sync-version
317
+ */
318
+ async syncTemplateVersion(id) {
319
+ return this.request(
320
+ "POST",
321
+ `/api/v1/hosting/developer/templates/${encodeURIComponent(id)}/sync-version`,
322
+ void 0,
323
+ true
324
+ );
325
+ }
326
+ // --- Site Management (Developer API Key 인증) ---
327
+ /**
328
+ * 사이트 스키마 조회. draft=true면 Draft 스키마 반환.
329
+ * GET /api/v1/hosting/developer/sites/{siteId}/schema
330
+ */
331
+ async getSiteSchema(siteId, draft) {
332
+ const query = draft ? "?draft=true" : "";
333
+ return this.request(
334
+ "GET",
335
+ `/api/v1/hosting/developer/sites/${encodeURIComponent(siteId)}/schema${query}`,
336
+ void 0,
337
+ true
338
+ );
339
+ }
340
+ /**
341
+ * 사이트 스키마 업데이트 (Draft에 저장).
342
+ * PUT /api/v1/hosting/developer/sites/{siteId}/schema
343
+ */
344
+ async updateSiteSchema(siteId, schema) {
345
+ await this.request(
346
+ "PUT",
347
+ `/api/v1/hosting/developer/sites/${encodeURIComponent(siteId)}/schema`,
348
+ { schema },
349
+ true
350
+ );
351
+ }
352
+ /**
353
+ * 사이트 발행. Draft 스키마를 Live로 전환.
354
+ * POST /api/v1/hosting/developer/sites/{siteId}/publish
355
+ */
356
+ async publishSite(siteId) {
357
+ return this.request(
358
+ "POST",
359
+ `/api/v1/hosting/developer/sites/${encodeURIComponent(siteId)}/publish`,
360
+ void 0,
361
+ true
362
+ );
363
+ }
364
+ // --- Asset Management ---
365
+ /**
366
+ * 서버에서 이미 존재하는 에셋을 content-hash 기반으로 확인 (중복 업로드 방지).
367
+ * POST /api/v1/hosting/developer/assets/check
368
+ */
369
+ async checkAssets(siteId, hashes) {
370
+ return this.request(
371
+ "POST",
372
+ "/api/v1/hosting/developer/assets/check",
373
+ { siteId, hashes },
374
+ true
375
+ );
376
+ }
377
+ /**
378
+ * 단일 에셋 파일을 업로드.
379
+ * POST /api/v1/hosting/developer/sites/{siteId}/assets
380
+ * multipart/form-data로 전송 (JSON이 아님).
381
+ */
382
+ async uploadAsset(siteId, filePath, contentHash, localPath) {
383
+ const url = `${this.baseUrl}/api/v1/hosting/developer/sites/${encodeURIComponent(siteId)}/assets`;
384
+ const absolutePath = resolve3(process.cwd(), filePath);
385
+ assertWithinCwd(absolutePath);
386
+ const fileBuffer = await readFile2(absolutePath);
387
+ const fileName = basename(absolutePath);
388
+ const formData = new FormData();
389
+ formData.append("file", new Blob([fileBuffer]), fileName);
390
+ formData.append("contentHash", contentHash);
391
+ formData.append("localPath", localPath);
392
+ const doUpload = async (attempt) => {
393
+ let resp;
394
+ try {
395
+ resp = await fetch(url, {
396
+ method: "POST",
397
+ headers: {
398
+ Authorization: `Bearer ${this.apiKey}`
399
+ },
400
+ body: formData,
401
+ signal: AbortSignal.timeout(12e4)
402
+ });
403
+ } catch (error) {
404
+ if (error instanceof TypeError && error.code === "ECONNREFUSED") {
405
+ throw new ApiError(
406
+ "CONNECTION_REFUSED",
407
+ `API \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${this.baseUrl}`,
408
+ 0
409
+ );
410
+ }
411
+ throw new ApiError(
412
+ "NETWORK_ERROR",
413
+ `\uB124\uD2B8\uC6CC\uD06C \uC5D0\uB7EC: ${error instanceof Error ? error.message : String(error)}`,
414
+ 0
415
+ );
416
+ }
417
+ if (resp.status === 429 && attempt < _SaeroonApiClient.MAX_RETRIES) {
418
+ const retryAfter = parseInt(resp.headers.get("Retry-After") ?? "5", 10);
419
+ const waitSeconds = Math.min(retryAfter, 60);
420
+ console.warn(
421
+ `[API] \uC5D0\uC14B \uC5C5\uB85C\uB4DC rate limit \uCD08\uACFC (${attempt + 1}/${_SaeroonApiClient.MAX_RETRIES}). ${waitSeconds}\uCD08 \uD6C4 \uC7AC\uC2DC\uB3C4...`
422
+ );
423
+ await new Promise((r) => setTimeout(r, waitSeconds * 1e3));
424
+ return doUpload(attempt + 1);
425
+ }
426
+ if (!resp.ok) {
427
+ let errorBody = {};
428
+ try {
429
+ const errorText = await resp.text();
430
+ errorBody = safeJsonParse(errorText);
431
+ } catch {
432
+ }
433
+ throw new ApiError(
434
+ errorBody.code ?? `HTTP_${resp.status}`,
435
+ errorBody.message ?? `\uC5D0\uC14B \uC5C5\uB85C\uB4DC \uC2E4\uD328: ${resp.statusText}`,
436
+ resp.status
437
+ );
438
+ }
439
+ const responseText = await resp.text();
440
+ return safeJsonParse(responseText);
441
+ };
442
+ return doUpload(0);
443
+ }
444
+ // --- Generic Request Helper ---
445
+ /** 최대 재시도 횟수 (429 Rate Limit) */
446
+ static MAX_RETRIES = 3;
447
+ /**
448
+ * HTTP 요청 공통 처리.
449
+ * Content-Type, Authorization 헤더 자동 설정, 에러 응답 시 ApiError throw.
450
+ * 429 응답 시 Retry-After 기반 자동 재시도 (최대 3회).
451
+ */
452
+ async request(method, path, body, requireAuth = true, retryCount = 0) {
453
+ const url = `${this.baseUrl}${path}`;
454
+ const headers = {
455
+ "Content-Type": "application/json"
456
+ };
457
+ if (requireAuth) {
458
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
459
+ }
460
+ const init = {
461
+ method,
462
+ headers,
463
+ signal: AbortSignal.timeout(3e4)
464
+ };
465
+ if (body !== void 0 && method !== "GET" && method !== "HEAD") {
466
+ init.body = JSON.stringify(body);
467
+ }
468
+ let response;
469
+ try {
470
+ response = await fetch(url, init);
471
+ } catch (error) {
472
+ if (error instanceof DOMException && error.name === "TimeoutError") {
473
+ throw new ApiError(
474
+ "TIMEOUT",
475
+ `API \uC694\uCCAD \uC2DC\uAC04 \uCD08\uACFC (30\uCD08): ${url}`,
476
+ 0
477
+ );
478
+ }
479
+ if (error instanceof TypeError && error.code === "ECONNREFUSED") {
480
+ throw new ApiError(
481
+ "CONNECTION_REFUSED",
482
+ `API \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${this.baseUrl}`,
483
+ 0
484
+ );
485
+ }
486
+ throw new ApiError(
487
+ "NETWORK_ERROR",
488
+ `\uB124\uD2B8\uC6CC\uD06C \uC5D0\uB7EC: ${error instanceof Error ? error.message : String(error)}`,
489
+ 0
490
+ );
491
+ }
492
+ if (response.status === 429 && retryCount < _SaeroonApiClient.MAX_RETRIES) {
493
+ const retryAfter = parseInt(response.headers.get("Retry-After") ?? "5", 10);
494
+ const waitSeconds = Math.min(retryAfter, 60);
495
+ console.warn(
496
+ `[API] Rate limit \uCD08\uACFC (${retryCount + 1}/${_SaeroonApiClient.MAX_RETRIES}). ${waitSeconds}\uCD08 \uD6C4 \uC7AC\uC2DC\uB3C4...`
497
+ );
498
+ await new Promise((r) => setTimeout(r, waitSeconds * 1e3));
499
+ return this.request(method, path, body, requireAuth, retryCount + 1);
500
+ }
501
+ const remaining = response.headers.get("X-RateLimit-Remaining");
502
+ if (remaining !== null && parseInt(remaining, 10) <= 5) {
503
+ console.warn(`[API] Rate limit \uC794\uC5EC \uC694\uCCAD: ${remaining}\uD68C`);
504
+ }
505
+ if (response.status === 204) {
506
+ return void 0;
507
+ }
508
+ if (!response.ok) {
509
+ let errorBody = {};
510
+ try {
511
+ const errorText = await response.text();
512
+ errorBody = safeJsonParse(errorText);
513
+ } catch {
514
+ }
515
+ throw new ApiError(
516
+ errorBody.code ?? `HTTP_${response.status}`,
517
+ errorBody.message ?? `API \uC694\uCCAD \uC2E4\uD328: ${response.statusText}`,
518
+ response.status
519
+ );
520
+ }
521
+ const text = await response.text();
522
+ return safeJsonParse(text);
523
+ }
524
+ // --- AI Generation ---
525
+ /**
526
+ * AI를 사용하여 사이트 스키마를 생성.
527
+ * POST /api/v1/hosting/ai/generate
528
+ *
529
+ * 참고: 실제 백엔드가 SSE를 반환하는 경우 향후 SSE 파싱으로 전환 가능.
530
+ * 현재는 단일 JSON 응답으로 처리.
531
+ */
532
+ async generateSite(options) {
533
+ return this.request(
534
+ "POST",
535
+ "/api/v1/hosting/ai/generate",
536
+ options,
537
+ true
538
+ );
539
+ }
540
+ };
541
+
542
+ // src/lib/output.ts
543
+ import chalk2 from "chalk";
544
+ var DEVELOPER_DOCS_BASE = "https://developers.saeroon.com";
545
+ function buildDocsLink(error) {
546
+ if (!error.path) return null;
547
+ const propMatch = error.path.match(/\.props\.(\w+)$/);
548
+ const propName = propMatch ? propMatch[1] : null;
549
+ if (error.step === 1) return null;
550
+ if (propName) {
551
+ return `${DEVELOPER_DOCS_BASE}/blocks#${propName}`;
552
+ }
553
+ return null;
554
+ }
555
+ function formatValidationErrors(errors) {
556
+ const errorCount = errors.filter((e) => e.severity === "error").length;
557
+ const warningCount = errors.filter((e) => e.severity === "warning").length;
558
+ if (errors.length === 0) {
559
+ console.log(chalk2.green("\uC720\uD6A8\uC131 \uAC80\uC0AC \uD1B5\uACFC! \uC5D0\uB7EC \uC5C6\uC74C."));
560
+ return;
561
+ }
562
+ console.log(
563
+ chalk2.bold(
564
+ `
565
+ \uAC80\uC99D \uACB0\uACFC: ${chalk2.red(`${errorCount} \uC5D0\uB7EC`)}, ${chalk2.yellow(`${warningCount} \uACBD\uACE0`)}`
566
+ )
567
+ );
568
+ console.log("");
569
+ const byStep = /* @__PURE__ */ new Map();
570
+ for (const error of errors) {
571
+ const group = byStep.get(error.step) ?? [];
572
+ group.push(error);
573
+ byStep.set(error.step, group);
574
+ }
575
+ const stepNames = {
576
+ 1: "Block Type Validation",
577
+ 2: "Property Validation",
578
+ 3: "Structure Validation",
579
+ 4: "Security Validation",
580
+ 5: "Size Validation"
581
+ };
582
+ for (const [step, stepErrors] of [...byStep.entries()].sort(
583
+ (a, b) => a[0] - b[0]
584
+ )) {
585
+ console.log(chalk2.bold.underline(`Step ${step}: ${stepNames[step] ?? "Unknown"}`));
586
+ for (const error of stepErrors) {
587
+ const icon = error.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("\u26A0");
588
+ const pathStr = error.path ? chalk2.dim(` @ ${error.path}`) : "";
589
+ console.log(` ${icon} ${error.message}${pathStr}`);
590
+ if (error.expected) {
591
+ console.log(chalk2.dim(` expected: ${error.expected}`));
592
+ }
593
+ if (error.actual) {
594
+ console.log(chalk2.dim(` actual: ${error.actual}`));
595
+ }
596
+ if (error.fix) {
597
+ console.log(chalk2.cyan(` fix: ${error.fix}`));
598
+ }
599
+ const docsLink = buildDocsLink(error);
600
+ if (docsLink) {
601
+ console.log(chalk2.blue(` docs: ${docsLink}`));
602
+ }
603
+ }
604
+ console.log("");
605
+ }
606
+ }
607
+ function formatScoreBar(label, score) {
608
+ const filledCount = Math.round(score.score / 10);
609
+ const emptyCount = 10 - filledCount;
610
+ const filled = "\u2588".repeat(filledCount);
611
+ const empty = "\u2591".repeat(emptyCount);
612
+ let colorFn;
613
+ if (score.score >= 80) {
614
+ colorFn = chalk2.green;
615
+ } else if (score.score >= 60) {
616
+ colorFn = chalk2.yellow;
617
+ } else {
618
+ colorFn = chalk2.red;
619
+ }
620
+ const paddedLabel = label.padEnd(16);
621
+ console.log(` ${paddedLabel}${colorFn(filled)}${chalk2.dim(empty)} ${colorFn(`${score.score}/100`)}`);
622
+ }
623
+ function formatQualityReport(quality) {
624
+ if (!quality) {
625
+ console.log(chalk2.dim(" \uD488\uC9C8 \uC810\uC218 \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
626
+ return;
627
+ }
628
+ console.log(chalk2.bold("\n\uD488\uC9C8 \uB9AC\uD3EC\uD2B8:"));
629
+ console.log("");
630
+ formatScoreBar("SEO", quality.seo);
631
+ formatScoreBar("GEO", quality.geo);
632
+ formatScoreBar("Performance", quality.performance);
633
+ formatScoreBar("Accessibility", quality.accessibility);
634
+ console.log("");
635
+ const allCategories = [
636
+ { name: "SEO", score: quality.seo },
637
+ { name: "GEO", score: quality.geo },
638
+ { name: "Performance", score: quality.performance },
639
+ { name: "Accessibility", score: quality.accessibility }
640
+ ];
641
+ let hasIssues = false;
642
+ for (const { name, score } of allCategories) {
643
+ if (score.issues.length === 0) {
644
+ continue;
645
+ }
646
+ hasIssues = true;
647
+ console.log(chalk2.bold(` ${name} \uC774\uC288:`));
648
+ for (const issue of score.issues) {
649
+ let icon;
650
+ if (issue.severity === "error") {
651
+ icon = chalk2.red("\u2717");
652
+ } else if (issue.severity === "warning") {
653
+ icon = chalk2.yellow("\u26A0");
654
+ } else {
655
+ icon = chalk2.blue("\u2139");
656
+ }
657
+ console.log(` ${icon} ${chalk2.dim(`[${issue.code}]`)} ${issue.message}`);
658
+ if (issue.fix) {
659
+ console.log(chalk2.cyan(` fix: ${issue.fix}`));
660
+ }
661
+ if (issue.impact) {
662
+ console.log(chalk2.dim(` impact: ${issue.impact}`));
663
+ }
664
+ if (issue.docsLink) {
665
+ console.log(chalk2.blue(` docs: ${issue.docsLink}`));
666
+ }
667
+ }
668
+ console.log("");
669
+ }
670
+ if (!hasIssues) {
671
+ console.log(chalk2.green(" \uC774\uC288 \uC5C6\uC74C!"));
672
+ console.log("");
673
+ }
674
+ }
675
+ function formatBytes(bytes) {
676
+ if (bytes === 0) return "0 B";
677
+ const units = ["B", "KB", "MB", "GB"];
678
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
679
+ const value = bytes / Math.pow(1024, i);
680
+ return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
681
+ }
682
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
683
+ function spinner(message) {
684
+ let frameIndex = 0;
685
+ let stopped = false;
686
+ const interval = setInterval(() => {
687
+ if (stopped) return;
688
+ const frame = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length];
689
+ process.stdout.write(`\r${chalk2.cyan(frame)} ${message}`);
690
+ frameIndex++;
691
+ }, 80);
692
+ return {
693
+ stop(finalMessage) {
694
+ if (stopped) return;
695
+ stopped = true;
696
+ clearInterval(interval);
697
+ process.stdout.write("\r" + " ".repeat(message.length + 4) + "\r");
698
+ if (finalMessage) {
699
+ console.log(finalMessage);
700
+ }
701
+ }
702
+ };
703
+ }
704
+ function formatBlockTable(blocks) {
705
+ if (blocks.length === 0) {
706
+ console.log(chalk2.dim(" \uBE14\uB85D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
707
+ return;
708
+ }
709
+ const byCategory = /* @__PURE__ */ new Map();
710
+ for (const block of blocks) {
711
+ const group = byCategory.get(block.category) ?? [];
712
+ group.push(block);
713
+ byCategory.set(block.category, group);
714
+ }
715
+ const maxTypeLength = Math.max(...blocks.map((b) => b.type.length), 4);
716
+ console.log(chalk2.bold(`
717
+ \uBE14\uB85D \uCE74\uD0C8\uB85C\uADF8 (${blocks.length}\uAC1C):
718
+ `));
719
+ for (const [category, categoryBlocks] of [...byCategory.entries()].sort()) {
720
+ console.log(chalk2.bold.underline(` ${category}`));
721
+ console.log("");
722
+ for (const block of categoryBlocks.sort((a, b) => a.type.localeCompare(b.type))) {
723
+ const paddedType = block.type.padEnd(maxTypeLength + 2);
724
+ console.log(` ${chalk2.cyan(paddedType)}${chalk2.dim(block.description)}`);
725
+ }
726
+ console.log("");
727
+ }
728
+ }
729
+ function formatBlockDetail(block) {
730
+ console.log(chalk2.bold(`
731
+ ${block.type}`));
732
+ console.log(chalk2.dim(` \uCE74\uD14C\uACE0\uB9AC: ${block.category}`));
733
+ console.log(chalk2.dim(` \uC124\uBA85: ${block.description}`));
734
+ if (block.props.length === 0) {
735
+ console.log(chalk2.dim("\n Props \uC5C6\uC74C."));
736
+ return;
737
+ }
738
+ console.log(chalk2.bold("\n Props:\n"));
739
+ const maxNameLen = Math.max(...block.props.map((p) => p.name.length), 4);
740
+ const maxTypeLen = Math.max(...block.props.map((p) => p.type.length), 4);
741
+ const nameHeader = "Name".padEnd(maxNameLen + 2);
742
+ const typeHeader = "Type".padEnd(maxTypeLen + 2);
743
+ console.log(chalk2.dim(` ${nameHeader}${typeHeader}Required Default`));
744
+ console.log(chalk2.dim(` ${"\u2500".repeat(maxNameLen + 2)}${"\u2500".repeat(maxTypeLen + 2)}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
745
+ for (const prop of block.props) {
746
+ const paddedName = prop.name.padEnd(maxNameLen + 2);
747
+ const paddedType = prop.type.padEnd(maxTypeLen + 2);
748
+ const requiredStr = prop.required ? chalk2.red("yes ") : chalk2.dim("no ");
749
+ const defaultStr = prop.default ? chalk2.dim(prop.default) : chalk2.dim("-");
750
+ console.log(` ${chalk2.cyan(paddedName)}${paddedType}${requiredStr} ${defaultStr}`);
751
+ }
752
+ console.log("");
753
+ }
754
+ function formatTemplateList(templates) {
755
+ if (templates.length === 0) {
756
+ console.log(chalk2.dim(" \uB4F1\uB85D\uB41C \uD15C\uD50C\uB9BF\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
757
+ console.log(chalk2.dim(" saeroon template register \uB85C \uCCAB \uD15C\uD50C\uB9BF\uC744 \uB4F1\uB85D\uD558\uC138\uC694."));
758
+ return;
759
+ }
760
+ console.log(chalk2.bold(`
761
+ \uB0B4 \uD15C\uD50C\uB9BF (${templates.length}\uAC1C):
762
+ `));
763
+ const maxNameLen = Math.max(...templates.map((t) => t.name.length), 4);
764
+ const nameHeader = "Name".padEnd(maxNameLen + 2);
765
+ console.log(chalk2.dim(` ${nameHeader}Category Status Ver Sales Price`));
766
+ console.log(chalk2.dim(` ${"\u2500".repeat(maxNameLen + 2)}${"\u2500".repeat(17)}${"\u2500".repeat(12)}${"\u2500".repeat(6)}${"\u2500".repeat(8)}${"\u2500".repeat(10)}`));
767
+ for (const t of templates) {
768
+ const name = t.name.padEnd(maxNameLen + 2);
769
+ const category = (t.category ?? "-").padEnd(17);
770
+ const status = formatTemplateStatus(t.status).padEnd(20);
771
+ const version2 = `v${t.version}`.padEnd(6);
772
+ const sales = String(t.salesCount).padEnd(8);
773
+ const price = t.priceKrw > 0 ? `\u20A9${t.priceKrw.toLocaleString()}` : chalk2.dim("\uBB34\uB8CC");
774
+ console.log(` ${chalk2.cyan(name)}${category}${status}${version2}${sales}${price}`);
775
+ }
776
+ console.log("");
777
+ }
778
+ function formatTemplateDetail(t) {
779
+ console.log(chalk2.bold(`
780
+ ${t.name}`));
781
+ console.log(` ID: ${chalk2.dim(t.id)}`);
782
+ console.log(` \uC0C1\uD0DC: ${formatTemplateStatus(t.status)}`);
783
+ console.log(` \uBC84\uC804: v${t.version}`);
784
+ console.log(` \uCE74\uD14C\uACE0\uB9AC: ${t.category}`);
785
+ if (t.description) {
786
+ console.log(` \uC124\uBA85: ${chalk2.dim(t.description)}`);
787
+ }
788
+ if (t.tags && t.tags.length > 0) {
789
+ console.log(` \uD0DC\uADF8: ${chalk2.dim(t.tags.join(", "))}`);
790
+ }
791
+ console.log(` \uACF5\uAC1C\uBBF8\uB9AC\uBCF4\uAE30: ${t.isPublicPreview ? chalk2.green("\uCF1C\uC9D0") : chalk2.dim("\uAEBC\uC9D0")}`);
792
+ console.log(` \uAC00\uACA9(KRW): ${t.priceKrw > 0 ? `\u20A9${t.priceKrw.toLocaleString()}` : chalk2.dim("\uBB34\uB8CC")}`);
793
+ if (t.priceUsd > 0) {
794
+ console.log(` \uAC00\uACA9(USD): $${t.priceUsd}`);
795
+ }
796
+ if (t.sourceSlug) {
797
+ console.log(` \uC18C\uC2A4 \uC0AC\uC774\uD2B8: ${chalk2.cyan(t.sourceSlug)}`);
798
+ }
799
+ console.log(` \uD310\uB9E4: ${t.salesCount}\uAC74`);
800
+ console.log(` \uC218\uC775: \u20A9${t.totalRevenue.toLocaleString()}`);
801
+ console.log(` \uC0DD\uC131: ${chalk2.dim(t.createdAt)}`);
802
+ if (t.updatedAt) {
803
+ console.log(` \uC218\uC815: ${chalk2.dim(t.updatedAt)}`);
804
+ }
805
+ console.log("");
806
+ }
807
+ function formatTemplateStatus(status) {
808
+ switch (status.toLowerCase()) {
809
+ case "published":
810
+ return chalk2.green("Published");
811
+ case "draft":
812
+ return chalk2.yellow("Draft");
813
+ case "pending":
814
+ return chalk2.blue("Pending");
815
+ case "rejected":
816
+ return chalk2.red("Rejected");
817
+ default:
818
+ return chalk2.dim(status);
819
+ }
820
+ }
821
+ function formatUploadProgress(current, total, localPath, originalSize, storedSize, format) {
822
+ const progress = `[${current}/${total}]`;
823
+ const sizeInfo = `${formatBytes(originalSize)} \u2192 ${formatBytes(storedSize)} ${format}`;
824
+ const bar = chalk2.green("\u2501".repeat(10));
825
+ console.log(` ${chalk2.bold(progress)} ${localPath} ${bar} ${chalk2.green("done")} ${chalk2.dim(`(${sizeInfo})`)}`);
826
+ }
827
+
828
+ // src/commands/login.ts
829
+ function readPassword(prompt) {
830
+ return new Promise((resolvePromise) => {
831
+ process.stdout.write(prompt);
832
+ if (!process.stdin.isTTY) {
833
+ const rl = createInterface({ input: stdin, output: stdout, terminal: false });
834
+ rl.once("line", (line) => {
835
+ rl.close();
836
+ resolvePromise(line.trim());
837
+ });
838
+ return;
839
+ }
840
+ stdin.setRawMode(true);
841
+ stdin.resume();
842
+ stdin.setEncoding("utf-8");
843
+ let input = "";
844
+ const onData = (char) => {
845
+ const code = char.charCodeAt(0);
846
+ if (char === "\n" || char === "\r") {
847
+ stdin.setRawMode(false);
848
+ stdin.pause();
849
+ stdin.removeListener("data", onData);
850
+ process.stdout.write("\n");
851
+ resolvePromise(input.trim());
852
+ } else if (code === 3) {
853
+ stdin.setRawMode(false);
854
+ stdin.pause();
855
+ stdin.removeListener("data", onData);
856
+ process.stdout.write("\n");
857
+ process.exit(130);
858
+ } else if (code === 127 || code === 8) {
859
+ if (input.length > 0) {
860
+ input = input.slice(0, -1);
861
+ process.stdout.write("\b \b");
862
+ }
863
+ } else if (code >= 32) {
864
+ input += char;
865
+ process.stdout.write("*");
866
+ }
867
+ };
868
+ stdin.on("data", onData);
869
+ });
870
+ }
871
+ async function commandLogin() {
872
+ console.log(chalk3.bold("\nSaeroon Developer CLI \uB85C\uADF8\uC778\n"));
873
+ try {
874
+ const existingConfig = await loadConfig();
875
+ const prompt = existingConfig.apiKey ? `API Key (\uD604\uC7AC: ${existingConfig.apiKey.slice(0, 4)}****): ` : "API Key: ";
876
+ const apiKey = await readPassword(prompt);
877
+ if (!apiKey && !existingConfig.apiKey) {
878
+ console.error(chalk3.red("API Key\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694."));
879
+ process.exit(1);
880
+ }
881
+ const finalApiKey = apiKey || existingConfig.apiKey;
882
+ const apiBaseUrl = existingConfig.apiBaseUrl || await getApiBaseUrl();
883
+ const client = new SaeroonApiClient(finalApiKey, apiBaseUrl);
884
+ const verifySpinner = spinner("API \uC11C\uBC84 \uC5F0\uACB0 \uD655\uC778 \uC911...");
885
+ try {
886
+ await client.getBlockCatalog();
887
+ verifySpinner.stop(chalk3.green("API \uC11C\uBC84 \uC5F0\uACB0 \uD655\uC778 \uC644\uB8CC."));
888
+ } catch {
889
+ verifySpinner.stop(
890
+ chalk3.yellow("API \uC11C\uBC84 \uC5F0\uACB0\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. API Key\uB294 \uC800\uC7A5\uB429\uB2C8\uB2E4.")
891
+ );
892
+ }
893
+ const config = {
894
+ ...existingConfig,
895
+ apiKey: finalApiKey
896
+ };
897
+ await saveConfig(config);
898
+ const maskedKey = finalApiKey.slice(0, 4) + "****";
899
+ console.log(chalk3.green(`
900
+ \uB85C\uADF8\uC778 \uC644\uB8CC!`));
901
+ console.log(chalk3.dim(` API Key: ${maskedKey}`));
902
+ console.log(chalk3.dim(` \uC124\uC815 \uD30C\uC77C: .saeroon/config.json`));
903
+ console.log("");
904
+ } catch (error) {
905
+ console.error(chalk3.red(`\uB85C\uADF8\uC778 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`));
906
+ process.exit(1);
907
+ }
908
+ }
909
+
910
+ // src/commands/whoami.ts
911
+ import chalk4 from "chalk";
912
+ async function commandWhoami() {
913
+ const config = await loadConfig();
914
+ if (!config.apiKey) {
915
+ console.error(chalk4.red("\n\uC778\uC99D\uB418\uC9C0 \uC54A\uC740 \uC0C1\uD0DC\uC785\uB2C8\uB2E4."));
916
+ console.error(
917
+ chalk4.yellow("npx @saeroon/cli login \uC73C\uB85C \uB85C\uADF8\uC778\uD574\uC8FC\uC138\uC694.\n")
918
+ );
919
+ process.exit(1);
920
+ }
921
+ const maskedKey = config.apiKey.slice(0, 8) + "...";
922
+ const apiBaseUrl = config.apiBaseUrl || DEFAULT_API_BASE_URL;
923
+ console.log(chalk4.bold("\nSaeroon Developer CLI \uC0C1\uD0DC\n"));
924
+ console.log(` API Key: ${chalk4.cyan(maskedKey)}`);
925
+ console.log(` API URL: ${chalk4.dim(apiBaseUrl)}`);
926
+ if (config.defaultDevice) {
927
+ console.log(` \uB514\uBC14\uC774\uC2A4: ${chalk4.dim(config.defaultDevice)}`);
928
+ }
929
+ if (config.siteId) {
930
+ console.log(` \uC0AC\uC774\uD2B8 ID: ${chalk4.dim(config.siteId)}`);
931
+ }
932
+ if (config.templateId) {
933
+ console.log(` \uD15C\uD50C\uB9BF ID: ${chalk4.dim(config.templateId)}`);
934
+ }
935
+ if (!config.siteId && !config.templateId) {
936
+ console.log(chalk4.dim("\n \uC5F0\uACB0\uB41C \uC0AC\uC774\uD2B8/\uD15C\uD50C\uB9BF\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
937
+ }
938
+ console.log("");
939
+ }
940
+
941
+ // src/commands/init.ts
942
+ import chalk5 from "chalk";
943
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
944
+ import { readFileSync, existsSync as existsSync2 } from "fs";
945
+ import { resolve as resolve4 } from "path";
946
+
947
+ // src/templates/cursorrules.ts
948
+ var CURSORRULES_TEMPLATE = `# Saeroon Hosting Schema Project
949
+
950
+ ## What is this?
951
+ This project creates a website using Saeroon Hosting's schema-based builder.
952
+ Sites are defined as JSON (StandardSiteSchema), not code.
953
+ Edit \`schema.json\` to build and customize your site.
954
+
955
+ ## Schema Structure (v1.20.0)
956
+ \`\`\`
957
+ {
958
+ "$schema": "https://hosting.saeroon.com/schema/v1.20.0/site-schema.json",
959
+ "schemaVersion": "1.20.0",
960
+ "id": "my-site",
961
+ "global": { "name": "...", "theme": {...} },
962
+ "pages": [
963
+ {
964
+ "id": "home",
965
+ "path": "/",
966
+ "pageType": "home",
967
+ "name": "Home",
968
+ "seo": { "title": "..." },
969
+ "rootBlockId": "root",
970
+ "blocks": {
971
+ "root": { "type": "container", "props": {...}, "children": ["block-1"] },
972
+ "block-1": { "type": "heading-block", "props": { "text": "Hello", "level": "h1" } }
973
+ }
974
+ }
975
+ ]
976
+ }
977
+ \`\`\`
978
+
979
+ ## Block Architecture
980
+ - Blocks are stored in a **flat map**: \`blocks: Record<string, Block>\`
981
+ - Parent-child via \`children: string[]\` (IDs referencing other blocks)
982
+ - \`rootBlockId\` must reference an existing block in the page
983
+ - Container blocks can have children; leaf blocks cannot
984
+
985
+ ## Available Blocks (85 types)
986
+
987
+ ### Element (9) \u2014 Basic building blocks
988
+ container, text-block, heading-block, button-block, image-block, video-block, divider, spacer, icon-block
989
+
990
+ ### Feature (70) \u2014 Rich functionality blocks
991
+ **Media**: image-slider, map-block, lottie-block, model-block, image-sequence-block
992
+ **Forms**: contact-form, newsletter, demolition-calculator
993
+ **Commerce (21)**: product-grid, product-gallery, product-price, product-filter, product-search, variant-selector, stock-badge, related-products, add-to-cart, cart-widget, cart-drawer, cart-contents, cart-summary, coupon-input, checkout-form, order-confirmation, order-history, order-lookup, review-list, wishlist, recently-viewed
994
+ **Booking (13)**: booking-calendar, booking-service-list, booking-service-detail, booking-checkout, booking-confirmation, booking-my-bookings, booking-staff-list, booking-guest-cancel, booking-class-schedule, booking-course-detail, booking-course-progress, booking-resource-calendar, booking-resource-list
995
+ **Member (3)**: auth-block, member-profile-block, member-only-section
996
+ **Board (2)**: board-block, board-detail-block
997
+ **UI/UX**: faq-accordion, testimonials-section, gallery-block, tabs-section, countdown-timer, marquee-block, before-after-slider, before-after-gallery, content-showcase, staff-showcase, service-detail-block, stats-counter, site-menu, scroll-to-top, anchor-nav
998
+ **Interactive**: floating-social-widget, modal-block, sticky-cta-bar, medical-booking-block, booking-button
999
+ **Trademark**: trademark-search-block, trademark-detail-block, nice-class-browser-block
1000
+
1001
+ ### Structure (6) \u2014 Semantic HTML elements
1002
+ header-block, footer-block, nav-block, main-block, aside-block, article-block
1003
+
1004
+ ## Responsive Design
1005
+ Use \`deviceOverrides\` for responsive properties:
1006
+ - \`props\` = mobile default (base)
1007
+ - \`deviceOverrides.fold\` = 520px+
1008
+ - \`deviceOverrides.tablet\` = 768px+
1009
+ - \`deviceOverrides.laptop\` = 1280px+
1010
+ - \`deviceOverrides.desktop\` = 1536px+
1011
+
1012
+ Example:
1013
+ \`\`\`json
1014
+ {
1015
+ "type": "heading-block",
1016
+ "props": { "text": "Hello", "fontSize": 24 },
1017
+ "deviceOverrides": {
1018
+ "desktop": { "fontSize": 48 },
1019
+ "laptop": { "fontSize": 36 }
1020
+ }
1021
+ }
1022
+ \`\`\`
1023
+
1024
+ ## Visibility Rules (v1.19.0 \u2014 Conditional Rendering)
1025
+ Show/hide blocks based on conditions without code:
1026
+ \`\`\`json
1027
+ {
1028
+ "visibilityRules": {
1029
+ "operator": "and",
1030
+ "conditions": [
1031
+ { "source": "user", "field": "isLoggedIn", "op": "eq", "value": true }
1032
+ ],
1033
+ "fallback": "hide"
1034
+ }
1035
+ }
1036
+ \`\`\`
1037
+ Sources: user, page, site, collection, cart, booking, datetime, url, visitor
1038
+ Operators: eq, neq, gt, gte, lt, lte, contains, not_contains, exists, empty, between
1039
+
1040
+ ## Data Binding & Display Formats (v1.20.0)
1041
+ Bind collection data to blocks with format control:
1042
+ \`\`\`json
1043
+ {
1044
+ "dataBinding": {
1045
+ "mode": "bound",
1046
+ "source": { "type": "collection", "collectionId": "products" },
1047
+ "fieldMap": {
1048
+ "title": { "sourceField": "name", "targetProp": "text" },
1049
+ "price": { "sourceField": "price", "targetProp": "text", "displayFormat": { "type": "currency", "options": { "currency": "KRW" } } }
1050
+ }
1051
+ }
1052
+ }
1053
+ \`\`\`
1054
+ Format types: currency, number, percentage, compact, ordinal, date, datetime, relativeTime, calendar, truncate, uppercase, lowercase, capitalize, stripHtml, placeholder, template
1055
+
1056
+ ## Interactions (102 effects)
1057
+ Categories: entrance (16), scroll (5), hover (14), text (6), loop (5), emphasis (6), scene, SVG (2), particle (4), page transitions (6)
1058
+
1059
+ ## Key Constraints
1060
+ 1. Every page needs \`rootBlockId\` pointing to an existing block
1061
+ 2. Leaf blocks (text-block, image-block, etc.) cannot have children
1062
+ 3. Container blocks can nest other blocks via \`children\`
1063
+ 4. Pages use flat block maps: \`blocks: Record<string, Block>\`
1064
+
1065
+ ## CLI Commands
1066
+ - Validate: \`npx @saeroon/cli validate\`
1067
+ - Preview: \`npx @saeroon/cli preview\`
1068
+ - Block catalog: \`npx @saeroon/cli blocks [block-type]\`
1069
+ - Add block: \`npx @saeroon/cli add <block-type> [--parent <id>]\`
1070
+ - Upload assets: \`npx @saeroon/cli upload <path>\`
1071
+ - Deploy: \`npx @saeroon/cli deploy\`
1072
+
1073
+ ## MCP Server (Enhanced AI Assistance)
1074
+ Add to your Cursor MCP settings for richer block/schema assistance:
1075
+ \`\`\`json
1076
+ {
1077
+ "mcpServers": {
1078
+ "saeroon": {
1079
+ "command": "npx",
1080
+ "args": ["@saeroon/mcp-server"]
1081
+ }
1082
+ }
1083
+ }
1084
+ \`\`\`
1085
+
1086
+ ## References
1087
+ - Full docs: https://hosting.saeroon.com/llms-full.txt
1088
+ - Block explorer: https://developers.saeroon.com/blocks
1089
+ - API docs: https://developers.saeroon.com/docs
1090
+ `;
1091
+
1092
+ // src/templates/claude-md.ts
1093
+ var CLAUDE_MD_TEMPLATE = `# Saeroon Hosting Schema Project
1094
+
1095
+ ## Overview
1096
+ This project defines a website using Saeroon Hosting's StandardSiteSchema.
1097
+ Sites are JSON-defined (no code execution). Edit \`schema.json\` to build the site.
1098
+
1099
+ ## Schema Version
1100
+ Current: 1.20.0
1101
+
1102
+ ## Quick Reference
1103
+ - Validate: \`npx @saeroon/cli validate\`
1104
+ - Preview: \`npx @saeroon/cli preview\`
1105
+ - Block catalog: \`npx @saeroon/cli blocks\`
1106
+ - Deploy: \`npx @saeroon/cli deploy\`
1107
+
1108
+ ## Schema Structure
1109
+ \`\`\`
1110
+ version: "1.20.0"
1111
+ settings: { title, description, favicon, ogImage }
1112
+ pages:
1113
+ [pageId]:
1114
+ slug: "/"
1115
+ title: "Page Title"
1116
+ rootBlockId: "root" // must reference a block in the blocks map
1117
+ blocks:
1118
+ [blockId]: { type, props, children?, interaction?, deviceOverrides?, visibilityRules?, dataBinding? }
1119
+ \`\`\`
1120
+
1121
+ ## Block System
1122
+ 85 block types across categories:
1123
+ - Element (9): container, text-block, heading-block, button-block, image-block, video-block, divider, spacer, icon-block
1124
+ - Feature (70): forms, commerce (21), booking (13), member (3), board (2), UI/UX, interactive, media, trademark
1125
+ - Structure (6): header-block, footer-block, nav-block, main-block, aside-block, article-block
1126
+
1127
+ Each block has: id (string key), type (BlockType), props (block-specific), and optional children/interaction/deviceOverrides.
1128
+
1129
+ ## Key Constraints
1130
+ - Pages use flat block maps: \`blocks: Record<string, Block>\`
1131
+ - rootBlockId must reference an existing block in the page
1132
+ - Container blocks can have children; leaf blocks cannot
1133
+ - deviceOverrides: fold(0-520px), tablet(521-768px), laptop(769-1280px), desktop(1281-1536px)
1134
+ - Interaction effects: entrance, scroll, hover, text, loop, emphasis, scene, SVG, particle
1135
+ - visibilityRules: conditional rendering (9 sources, 11 operators, SSR/CSR split evaluation)
1136
+ - dataBinding.fieldMap[].displayFormat: 16 format types (currency, date, truncate, etc.)
1137
+
1138
+ ## Common Patterns
1139
+
1140
+ ### Basic Page Structure
1141
+ Root container > header-block + content containers + footer-block
1142
+
1143
+ ### Section Pattern
1144
+ Container (full width, padding, maxWidth) > heading + content blocks
1145
+
1146
+ ### Responsive
1147
+ \`props\` = mobile base, \`deviceOverrides.desktop.fontSize = 48\` for larger screens
1148
+
1149
+ ## MCP Server
1150
+ For richer AI assistance, configure the Saeroon MCP server:
1151
+ Add to \`.claude/settings.json\` or project MCP config:
1152
+ \`\`\`json
1153
+ {
1154
+ "mcpServers": {
1155
+ "saeroon": {
1156
+ "command": "npx",
1157
+ "args": ["@saeroon/mcp-server"]
1158
+ }
1159
+ }
1160
+ }
1161
+ \`\`\`
1162
+
1163
+ ## Full Documentation
1164
+ https://hosting.saeroon.com/llms-full.txt
1165
+ `;
1166
+
1167
+ // src/templates/starters/restaurant.ts
1168
+ var RESTAURANT_STARTER = {
1169
+ version: "1.16.0",
1170
+ settings: {
1171
+ title: "\uB9DB\uC788\uB294 \uD55C\uC2DD\uB2F9",
1172
+ description: "\uC815\uC131\uC744 \uB2F4\uC740 \uD55C\uC2DD \uC694\uB9AC \uC804\uBB38\uC810. \uC2E0\uC120\uD55C \uC7AC\uB8CC\uB85C \uB9CC\uB4E0 \uAC74\uAC15\uD55C \uD55C \uB07C\uB97C \uC81C\uACF5\uD569\uB2C8\uB2E4."
1173
+ },
1174
+ pages: [
1175
+ {
1176
+ id: "home",
1177
+ slug: "/",
1178
+ title: "Home",
1179
+ blocks: [
1180
+ {
1181
+ id: "hero-1",
1182
+ type: "hero",
1183
+ props: {
1184
+ heading: "\uC815\uC131\uC744 \uB2F4\uC740 \uD55C \uB07C",
1185
+ subheading: "\uC2E0\uC120\uD55C \uC81C\uCCA0 \uC7AC\uB8CC\uB85C \uB9E4\uC77C \uC544\uCE68 \uC900\uBE44\uD558\uB294 \uAC74\uAC15\uD55C \uD55C\uC2DD \uC694\uB9AC",
1186
+ ctaText: "\uC608\uC57D\uD558\uAE30",
1187
+ ctaUrl: "#booking",
1188
+ backgroundImage: "https://images.unsplash.com/photo-1590301157890-4810ed352733?w=1920&q=80",
1189
+ height: 600
1190
+ }
1191
+ },
1192
+ {
1193
+ id: "heading-1",
1194
+ type: "heading-block",
1195
+ props: {
1196
+ text: "\uB300\uD45C \uBA54\uB274",
1197
+ level: 2
1198
+ }
1199
+ },
1200
+ {
1201
+ id: "text-1",
1202
+ type: "text-block",
1203
+ props: {
1204
+ content: "<p><strong>\uB41C\uC7A5\uCC0C\uAC1C \uC815\uC2DD</strong> \u2014 \uC9C1\uC811 \uB2F4\uADFC \uB41C\uC7A5\uC73C\uB85C \uB053\uC778 \uAE4A\uC740 \uB9DB\uC758 \uCC0C\uAC1C\uC640 \uBC18\uCC2C 5\uC885 (\u20A912,000)</p><p><strong>\uBD88\uACE0\uAE30 \uC815\uC2DD</strong> \u2014 \uC591\uB150 \uC18C\uBD88\uACE0\uAE30\uC640 \uACC4\uC808 \uB098\uBB3C, \uB41C\uC7A5\uAD6D (\u20A915,000)</p><p><strong>\uD574\uBB3C\uD30C\uC804</strong> \u2014 \uBC14\uC0AD\uD558\uAC8C \uBD80\uCE5C \uD574\uBB3C\uD30C\uC804, \uB9C9\uAC78\uB9AC\uC640 \uD568\uAED8 (\u20A918,000)</p><p><strong>\uBE44\uBE54\uBC25</strong> \u2014 \uB3CC\uC1A5\uC5D0 \uC9C0\uC740 \uBC25 \uC704\uC5D0 \uC2E0\uC120\uD55C \uB098\uBB3C\uACFC \uACE0\uCD94\uC7A5 (\u20A911,000)</p><p><strong>\uAC08\uBE44\uCC1C</strong> \u2014 \uBD80\uB4DC\uB7FD\uAC8C \uC870\uB9B0 \uC18C\uAC08\uBE44\uC640 \uBB34, \uB2F9\uADFC, \uBC24 (\u20A925,000)</p>"
1205
+ }
1206
+ },
1207
+ {
1208
+ id: "gallery-1",
1209
+ type: "gallery-block",
1210
+ props: {
1211
+ items: [
1212
+ { src: "https://images.unsplash.com/photo-1580651214613-f4692d6d138f?w=600&q=80", alt: "\uB41C\uC7A5\uCC0C\uAC1C \uC815\uC2DD", caption: "\uB41C\uC7A5\uCC0C\uAC1C \uC815\uC2DD" },
1213
+ { src: "https://images.unsplash.com/photo-1583187855824-3be0308a44cd?w=600&q=80", alt: "\uBD88\uACE0\uAE30", caption: "\uBD88\uACE0\uAE30 \uC815\uC2DD" },
1214
+ { src: "https://images.unsplash.com/photo-1590301157890-4810ed352733?w=600&q=80", alt: "\uBE44\uBE54\uBC25", caption: "\uB3CC\uC1A5 \uBE44\uBE54\uBC25" },
1215
+ { src: "https://images.unsplash.com/photo-1567533708067-7280e09c6cc5?w=600&q=80", alt: "\uAC08\uBE44\uCC1C", caption: "\uC18C\uAC08\uBE44\uCC1C" }
1216
+ ],
1217
+ columns: 2,
1218
+ gap: 16
1219
+ }
1220
+ },
1221
+ {
1222
+ id: "booking-1",
1223
+ type: "booking-calendar",
1224
+ props: {
1225
+ variant: "inline",
1226
+ showTime: true
1227
+ }
1228
+ },
1229
+ {
1230
+ id: "map-1",
1231
+ type: "map-block",
1232
+ props: {
1233
+ address: "\uC11C\uC6B8\uD2B9\uBCC4\uC2DC \uC885\uB85C\uAD6C \uC0BC\uCCAD\uB85C 100",
1234
+ lat: 37.5796,
1235
+ lng: 126.9831,
1236
+ zoom: 15,
1237
+ height: 350
1238
+ }
1239
+ }
1240
+ ]
1241
+ }
1242
+ ]
1243
+ };
1244
+
1245
+ // src/templates/starters/portfolio.ts
1246
+ var PORTFOLIO_STARTER = {
1247
+ version: "1.16.0",
1248
+ settings: {
1249
+ title: "Jane Doe \u2014 Designer & Developer",
1250
+ description: "Creative portfolio showcasing design and development work."
1251
+ },
1252
+ pages: [
1253
+ {
1254
+ id: "home",
1255
+ slug: "/",
1256
+ title: "Home",
1257
+ blocks: [
1258
+ {
1259
+ id: "hero-1",
1260
+ type: "hero",
1261
+ props: {
1262
+ heading: "Hi, I'm Jane Doe",
1263
+ subheading: "Designer & developer crafting thoughtful digital experiences.",
1264
+ ctaText: "View Work",
1265
+ ctaUrl: "#gallery",
1266
+ height: 500
1267
+ }
1268
+ },
1269
+ {
1270
+ id: "gallery-1",
1271
+ type: "gallery-block",
1272
+ props: {
1273
+ items: [
1274
+ { src: "https://images.unsplash.com/photo-1545235617-7a424c1a60cc?w=600&q=80", alt: "Brand identity project", caption: "Brand Identity \u2014 Lumen Studio" },
1275
+ { src: "https://images.unsplash.com/photo-1559028012-481c04fa702d?w=600&q=80", alt: "Mobile app design", caption: "Mobile App \u2014 Finova" },
1276
+ { src: "https://images.unsplash.com/photo-1507238691740-187a5b1d37b8?w=600&q=80", alt: "Website redesign", caption: "Website \u2014 Greenleaf Co." },
1277
+ { src: "https://images.unsplash.com/photo-1558655146-d09347e92766?w=600&q=80", alt: "Dashboard UI", caption: "Dashboard \u2014 Analytics Pro" },
1278
+ { src: "https://images.unsplash.com/photo-1561070791-2526d30994b5?w=600&q=80", alt: "Packaging design", caption: "Packaging \u2014 Bloom Tea" },
1279
+ { src: "https://images.unsplash.com/photo-1586717791821-3f44a563fa4c?w=600&q=80", alt: "Illustration series", caption: "Illustration \u2014 Seasons" }
1280
+ ],
1281
+ columns: 3,
1282
+ gap: 12
1283
+ }
1284
+ },
1285
+ {
1286
+ id: "heading-1",
1287
+ type: "heading-block",
1288
+ props: {
1289
+ text: "About Me",
1290
+ level: 2
1291
+ }
1292
+ },
1293
+ {
1294
+ id: "text-1",
1295
+ type: "text-block",
1296
+ props: {
1297
+ content: "<p>I'm a multidisciplinary designer and front-end developer based in Seoul. With 8 years of experience, I help startups and agencies build products that are both beautiful and functional.</p><p>My toolkit includes Figma, React, TypeScript, and a strong eye for typography and motion. I believe great design is invisible \u2014 it just works.</p>"
1298
+ }
1299
+ },
1300
+ {
1301
+ id: "contact-1",
1302
+ type: "contact-form",
1303
+ props: {
1304
+ fields: [
1305
+ { name: "name", label: "Name", type: "text", required: true },
1306
+ { name: "email", label: "Email", type: "email", required: true },
1307
+ { name: "message", label: "Message", type: "textarea", required: true }
1308
+ ],
1309
+ submitText: "Send Message",
1310
+ recipientEmail: "hello@example.com"
1311
+ }
1312
+ }
1313
+ ]
1314
+ }
1315
+ ]
1316
+ };
1317
+
1318
+ // src/templates/starters/business.ts
1319
+ var BUSINESS_STARTER = {
1320
+ version: "1.16.0",
1321
+ settings: {
1322
+ title: "Apex Consulting",
1323
+ description: "Strategic consulting for growing businesses. We help you scale smarter."
1324
+ },
1325
+ pages: [
1326
+ {
1327
+ id: "home",
1328
+ slug: "/",
1329
+ title: "Home",
1330
+ blocks: [
1331
+ {
1332
+ id: "hero-1",
1333
+ type: "hero",
1334
+ props: {
1335
+ heading: "Grow Your Business With Confidence",
1336
+ subheading: "Strategic consulting, operational excellence, and digital transformation for modern companies.",
1337
+ ctaText: "Get a Free Consultation",
1338
+ ctaUrl: "#contact",
1339
+ backgroundImage: "https://images.unsplash.com/photo-1497366216548-37526070297c?w=1920&q=80",
1340
+ height: 550
1341
+ }
1342
+ },
1343
+ {
1344
+ id: "feature-grid-1",
1345
+ type: "feature-grid",
1346
+ props: {
1347
+ items: [
1348
+ { title: "Strategy & Planning", description: "Data-driven strategies tailored to your market position and growth goals.", icon: "target" },
1349
+ { title: "Operations", description: "Streamline processes and reduce costs without sacrificing quality.", icon: "settings" },
1350
+ { title: "Digital Transformation", description: "Modernize your tech stack and workflows for the digital age.", icon: "zap" },
1351
+ { title: "Talent & Culture", description: "Build high-performing teams with the right people and processes.", icon: "users" }
1352
+ ],
1353
+ columns: 2
1354
+ }
1355
+ },
1356
+ {
1357
+ id: "pricing-1",
1358
+ type: "pricing-table",
1359
+ props: {
1360
+ plans: [
1361
+ {
1362
+ name: "Starter",
1363
+ price: 1500,
1364
+ currency: "USD",
1365
+ period: "month",
1366
+ features: ["2 strategy sessions / month", "Email support", "Monthly report", "Basic analytics dashboard"],
1367
+ ctaText: "Get Started",
1368
+ highlighted: false
1369
+ },
1370
+ {
1371
+ name: "Growth",
1372
+ price: 3500,
1373
+ currency: "USD",
1374
+ period: "month",
1375
+ features: ["Weekly strategy sessions", "Priority support", "Custom KPI dashboard", "Quarterly business review", "Team workshops"],
1376
+ ctaText: "Start Growing",
1377
+ highlighted: true
1378
+ },
1379
+ {
1380
+ name: "Enterprise",
1381
+ price: 8e3,
1382
+ currency: "USD",
1383
+ period: "month",
1384
+ features: ["Unlimited sessions", "Dedicated consultant", "Full analytics suite", "Board-level reporting", "On-site workshops", "24/7 support"],
1385
+ ctaText: "Contact Us",
1386
+ highlighted: false
1387
+ }
1388
+ ]
1389
+ }
1390
+ },
1391
+ {
1392
+ id: "testimonials-1",
1393
+ type: "testimonials-section",
1394
+ props: {
1395
+ items: [
1396
+ { name: "Sarah Kim", role: "CEO, TechBridge", content: "Apex helped us double our revenue in 18 months. Their strategic insights were a game-changer for our team.", avatar: "" },
1397
+ { name: "David Park", role: "COO, Greenfield Labs", content: "The operational improvements alone saved us $200K annually. Highly recommend their consulting services.", avatar: "" },
1398
+ { name: "Emily Chen", role: "Founder, NovaCraft", content: "Professional, thorough, and genuinely invested in our success. Apex is the real deal.", avatar: "" }
1399
+ ],
1400
+ layout: "grid"
1401
+ }
1402
+ },
1403
+ {
1404
+ id: "contact-1",
1405
+ type: "contact-form",
1406
+ props: {
1407
+ fields: [
1408
+ { name: "name", label: "Full Name", type: "text", required: true },
1409
+ { name: "email", label: "Work Email", type: "email", required: true },
1410
+ { name: "company", label: "Company", type: "text", required: false },
1411
+ { name: "message", label: "How can we help?", type: "textarea", required: true }
1412
+ ],
1413
+ submitText: "Request Consultation",
1414
+ recipientEmail: "hello@example.com"
1415
+ }
1416
+ }
1417
+ ]
1418
+ }
1419
+ ]
1420
+ };
1421
+
1422
+ // src/templates/starters/saas.ts
1423
+ var SAAS_STARTER = {
1424
+ version: "1.16.0",
1425
+ settings: {
1426
+ title: "Flowboard \u2014 Project Management Made Simple",
1427
+ description: "The all-in-one project management tool for modern teams. Plan, track, and ship faster."
1428
+ },
1429
+ pages: [
1430
+ {
1431
+ id: "home",
1432
+ slug: "/",
1433
+ title: "Home",
1434
+ blocks: [
1435
+ {
1436
+ id: "hero-1",
1437
+ type: "hero",
1438
+ props: {
1439
+ heading: "Ship Projects Faster",
1440
+ subheading: "Flowboard brings your tasks, docs, and team communication into one powerful workspace.",
1441
+ ctaText: "Start Free Trial",
1442
+ ctaUrl: "#pricing",
1443
+ height: 520
1444
+ }
1445
+ },
1446
+ {
1447
+ id: "feature-grid-1",
1448
+ type: "feature-grid",
1449
+ props: {
1450
+ items: [
1451
+ { title: "Task Boards", description: "Kanban, list, and timeline views to manage work your way.", icon: "layout" },
1452
+ { title: "Real-Time Collaboration", description: "Edit docs together, comment on tasks, and stay in sync.", icon: "users" },
1453
+ { title: "Automations", description: "Automate repetitive work with powerful no-code rules.", icon: "zap" },
1454
+ { title: "Analytics", description: "Track velocity, burndown, and team performance at a glance.", icon: "bar-chart" },
1455
+ { title: "Integrations", description: "Connect with Slack, GitHub, Figma, and 50+ tools.", icon: "plug" },
1456
+ { title: "Enterprise Security", description: "SOC 2, SSO, and granular permissions for teams of any size.", icon: "shield" }
1457
+ ],
1458
+ columns: 3
1459
+ }
1460
+ },
1461
+ {
1462
+ id: "pricing-1",
1463
+ type: "pricing-table",
1464
+ props: {
1465
+ plans: [
1466
+ {
1467
+ name: "Free",
1468
+ price: 0,
1469
+ currency: "USD",
1470
+ period: "month",
1471
+ features: ["Up to 5 users", "3 projects", "Basic task boards", "Community support"],
1472
+ ctaText: "Get Started",
1473
+ highlighted: false
1474
+ },
1475
+ {
1476
+ name: "Pro",
1477
+ price: 12,
1478
+ currency: "USD",
1479
+ period: "month",
1480
+ features: ["Unlimited users", "Unlimited projects", "Automations", "Advanced analytics", "Priority support"],
1481
+ ctaText: "Start Free Trial",
1482
+ highlighted: true
1483
+ },
1484
+ {
1485
+ name: "Enterprise",
1486
+ price: 39,
1487
+ currency: "USD",
1488
+ period: "month",
1489
+ features: ["Everything in Pro", "SSO & SAML", "Custom roles", "Audit log", "Dedicated CSM", "99.9% SLA"],
1490
+ ctaText: "Contact Sales",
1491
+ highlighted: false
1492
+ }
1493
+ ]
1494
+ }
1495
+ },
1496
+ {
1497
+ id: "faq-1",
1498
+ type: "faq-accordion",
1499
+ props: {
1500
+ items: [
1501
+ { question: "Is there a free plan?", answer: "Yes! Our Free plan supports up to 5 users and 3 projects with no time limit." },
1502
+ { question: "Can I import from other tools?", answer: "Absolutely. We support one-click imports from Trello, Asana, Jira, and Notion." },
1503
+ { question: "How does the free trial work?", answer: "Start a 14-day Pro trial with no credit card required. Downgrade to Free anytime." },
1504
+ { question: "Is my data secure?", answer: "We are SOC 2 Type II certified. All data is encrypted at rest and in transit." },
1505
+ { question: "Do you offer discounts for startups?", answer: "Yes \u2014 eligible startups get 50% off Pro for the first year. Contact us to apply." }
1506
+ ]
1507
+ }
1508
+ },
1509
+ {
1510
+ id: "cta-1",
1511
+ type: "cta-banner",
1512
+ props: {
1513
+ heading: "Ready to streamline your workflow?",
1514
+ description: "Join 10,000+ teams shipping faster with Flowboard.",
1515
+ ctaText: "Start Free Trial",
1516
+ ctaUrl: "#pricing",
1517
+ variant: "highlight"
1518
+ }
1519
+ }
1520
+ ]
1521
+ }
1522
+ ]
1523
+ };
1524
+
1525
+ // src/templates/starters/index.ts
1526
+ var STARTER_TEMPLATES = {
1527
+ restaurant: { name: "Restaurant", description: "\uC74C\uC2DD\uC810 \u2014 \uBA54\uB274, \uAC24\uB7EC\uB9AC, \uC608\uC57D, \uC9C0\uB3C4", schema: RESTAURANT_STARTER },
1528
+ portfolio: { name: "Portfolio", description: "\uD3EC\uD2B8\uD3F4\uB9AC\uC624 \u2014 \uAC24\uB7EC\uB9AC, \uC18C\uAC1C, \uC5F0\uB77D\uCC98", schema: PORTFOLIO_STARTER },
1529
+ business: { name: "Business", description: "\uBE44\uC988\uB2C8\uC2A4 \u2014 \uC11C\uBE44\uC2A4, \uAC00\uACA9, \uD6C4\uAE30, \uC5F0\uB77D\uCC98", schema: BUSINESS_STARTER },
1530
+ saas: { name: "SaaS", description: "SaaS \u2014 \uAE30\uB2A5, \uC694\uAE08, FAQ, CTA", schema: SAAS_STARTER }
1531
+ };
1532
+
1533
+ // src/commands/init.ts
1534
+ var SCHEMA_URL = "https://hosting.saeroon.com/schema/v1.20.0/site-schema.json";
1535
+ var MINIMAL_SCHEMA = {
1536
+ $schema: SCHEMA_URL,
1537
+ schemaVersion: "1.20.0",
1538
+ id: "my-site",
1539
+ featureTier: 0,
1540
+ global: {
1541
+ name: "My Site",
1542
+ description: "Created with @saeroon/cli CLI",
1543
+ theme: {
1544
+ primary: "#2563EB",
1545
+ background: "#FFFFFF",
1546
+ text: "#111827",
1547
+ fontFamily: "Pretendard, system-ui, sans-serif"
1548
+ }
1549
+ },
1550
+ pages: [
1551
+ {
1552
+ id: "home",
1553
+ path: "/",
1554
+ pageType: "home",
1555
+ name: "\uD648",
1556
+ seo: { title: "My Site" },
1557
+ rootBlockId: "root",
1558
+ blocks: {
1559
+ root: {
1560
+ type: "container",
1561
+ props: { displayMode: "flex", flexDirection: "column", gap: "0px" },
1562
+ children: ["hero-1"]
1563
+ },
1564
+ "hero-1": {
1565
+ type: "hero",
1566
+ props: {
1567
+ title: "Welcome to My Site",
1568
+ subtitle: "Built with Saeroon Hosting",
1569
+ height: 500
1570
+ }
1571
+ }
1572
+ }
1573
+ }
1574
+ ]
1575
+ };
1576
+ async function commandInit(options) {
1577
+ console.log(chalk5.bold("\nSaeroon \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654\n"));
1578
+ const schemaPath = resolve4(process.cwd(), "schema.json");
1579
+ const configPath = resolve4(process.cwd(), "saeroon.config.json");
1580
+ if (existsSync2(schemaPath)) {
1581
+ console.log(chalk5.yellow("\uAE30\uC874 schema.json\uC774 \uB36E\uC5B4\uC50C\uC6CC\uC9D1\uB2C8\uB2E4."));
1582
+ }
1583
+ if (options.template) {
1584
+ await initFromStarter(options.template, schemaPath, configPath);
1585
+ } else if (options.fromTemplate) {
1586
+ await initFromTemplate(options.fromTemplate, schemaPath, configPath);
1587
+ } else if (options.fromSite) {
1588
+ await initFromSite(options.fromSite, schemaPath, configPath);
1589
+ } else {
1590
+ await initDefault(schemaPath, configPath);
1591
+ }
1592
+ await generateAIContextFiles();
1593
+ console.log(chalk5.bold("\n\uC2DC\uC791\uD558\uAE30:"));
1594
+ console.log(chalk5.dim(" 1. schema.json\uC744 \uD3B8\uC9D1\uD558\uC138\uC694"));
1595
+ console.log(chalk5.dim(" 2. npx @saeroon/cli validate schema.json \uC73C\uB85C \uC720\uD6A8\uC131 \uAC80\uC0AC"));
1596
+ console.log(chalk5.dim(" 3. npx @saeroon/cli preview schema.json \uC73C\uB85C \uC2E4\uC2DC\uAC04 \uD504\uB9AC\uBDF0"));
1597
+ console.log(chalk5.dim(" 4. MCP \uC11C\uBC84\uB97C \uC124\uC815\uD558\uBA74 AI \uB3C4\uC6C0\uC744 \uBC1B\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4"));
1598
+ console.log(chalk5.dim(" \u203B npx @saeroon/cli init --template restaurant (\uC2A4\uD0C0\uD130 \uC0AC\uC6A9)"));
1599
+ console.log("");
1600
+ }
1601
+ async function initDefault(schemaPath, configPath) {
1602
+ await writeFile2(
1603
+ schemaPath,
1604
+ JSON.stringify(MINIMAL_SCHEMA, null, 2) + "\n",
1605
+ "utf-8"
1606
+ );
1607
+ console.log(chalk5.green(` schema.json \uC0DD\uC131 \uC644\uB8CC`));
1608
+ const projectConfig = {
1609
+ schemaFile: "schema.json"
1610
+ };
1611
+ await writeFile2(
1612
+ configPath,
1613
+ JSON.stringify(projectConfig, null, 2) + "\n",
1614
+ "utf-8"
1615
+ );
1616
+ console.log(chalk5.green(` saeroon.config.json \uC0DD\uC131 \uC644\uB8CC`));
1617
+ }
1618
+ async function initFromStarter(category, schemaPath, configPath) {
1619
+ const starter = STARTER_TEMPLATES[category];
1620
+ if (!starter) {
1621
+ const available = Object.keys(STARTER_TEMPLATES).join(", ");
1622
+ console.log(chalk5.red(`\uC54C \uC218 \uC5C6\uB294 \uC2A4\uD0C0\uD130: ${category}`));
1623
+ console.log(chalk5.dim(`\uC0AC\uC6A9 \uAC00\uB2A5: ${available}`));
1624
+ process.exit(1);
1625
+ }
1626
+ console.log(chalk5.blue(` ${starter.name} \uC2A4\uD0C0\uD130 \uC801\uC6A9 \uC911...`));
1627
+ const schemaWithRef = { $schema: SCHEMA_URL, ...starter.schema };
1628
+ await writeFile2(schemaPath, JSON.stringify(schemaWithRef, null, 2) + "\n", "utf-8");
1629
+ console.log(chalk5.green(` schema.json \uC0DD\uC131 \uC644\uB8CC (${starter.name})`));
1630
+ const projectConfig = { schemaFile: "schema.json" };
1631
+ await writeFile2(configPath, JSON.stringify(projectConfig, null, 2) + "\n", "utf-8");
1632
+ console.log(chalk5.green(` saeroon.config.json \uC0DD\uC131 \uC644\uB8CC`));
1633
+ }
1634
+ async function initFromTemplate(templateId, schemaPath, configPath) {
1635
+ const apiKey = await resolveApiKey();
1636
+ const apiBaseUrl = await getApiBaseUrl();
1637
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
1638
+ const fetchSpinner = spinner(`\uD15C\uD50C\uB9BF ${templateId} \uC2A4\uD0A4\uB9C8\uB97C \uAC00\uC838\uC624\uB294 \uC911...`);
1639
+ try {
1640
+ const templateInfo = await client.getTemplateStatus(templateId);
1641
+ if (templateInfo.status !== "approved") {
1642
+ fetchSpinner.stop(
1643
+ chalk5.yellow(`\uD15C\uD50C\uB9BF \uC0C1\uD0DC: ${templateInfo.status}. \uC2B9\uC778\uB41C \uD15C\uD50C\uB9BF\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`)
1644
+ );
1645
+ process.exit(1);
1646
+ }
1647
+ const templateSchema = await client.getBlockDetail(templateId);
1648
+ fetchSpinner.stop(chalk5.green(` \uD15C\uD50C\uB9BF ${templateId} \uC2A4\uD0A4\uB9C8 \uAC00\uC838\uC624\uAE30 \uC644\uB8CC`));
1649
+ const schemaWithRef = { $schema: SCHEMA_URL, ...templateSchema };
1650
+ await writeFile2(
1651
+ schemaPath,
1652
+ JSON.stringify(schemaWithRef, null, 2) + "\n",
1653
+ "utf-8"
1654
+ );
1655
+ console.log(chalk5.green(` schema.json \uC0DD\uC131 \uC644\uB8CC`));
1656
+ const projectConfig = {
1657
+ schemaFile: "schema.json",
1658
+ templateId
1659
+ };
1660
+ await writeFile2(
1661
+ configPath,
1662
+ JSON.stringify(projectConfig, null, 2) + "\n",
1663
+ "utf-8"
1664
+ );
1665
+ console.log(chalk5.green(` saeroon.config.json \uC0DD\uC131 \uC644\uB8CC (templateId: ${templateId})`));
1666
+ } catch (error) {
1667
+ fetchSpinner.stop(
1668
+ chalk5.red(
1669
+ `\uD15C\uD50C\uB9BF \uAC00\uC838\uC624\uAE30 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
1670
+ )
1671
+ );
1672
+ process.exit(1);
1673
+ }
1674
+ }
1675
+ async function initFromSite(siteId, schemaPath, configPath) {
1676
+ const apiKey = await resolveApiKey();
1677
+ const apiBaseUrl = await getApiBaseUrl();
1678
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
1679
+ const fetchSpinner = spinner(`\uC0AC\uC774\uD2B8 ${siteId} \uC2A4\uD0A4\uB9C8\uB97C \uAC00\uC838\uC624\uB294 \uC911...`);
1680
+ try {
1681
+ const siteSchema = await client.getSiteSchema(siteId, true);
1682
+ fetchSpinner.stop(chalk5.green(` \uC0AC\uC774\uD2B8 ${siteId} \uC2A4\uD0A4\uB9C8 \uAC00\uC838\uC624\uAE30 \uC644\uB8CC`));
1683
+ const parsedSchema = safeJsonParse(siteSchema.schemaJson);
1684
+ const schemaWithRef = { $schema: SCHEMA_URL, ...parsedSchema };
1685
+ await writeFile2(
1686
+ schemaPath,
1687
+ JSON.stringify(schemaWithRef, null, 2) + "\n",
1688
+ "utf-8"
1689
+ );
1690
+ console.log(chalk5.green(` schema.json \uC0DD\uC131 \uC644\uB8CC`));
1691
+ const projectConfig = {
1692
+ schemaFile: "schema.json",
1693
+ siteId
1694
+ };
1695
+ await writeFile2(
1696
+ configPath,
1697
+ JSON.stringify(projectConfig, null, 2) + "\n",
1698
+ "utf-8"
1699
+ );
1700
+ console.log(chalk5.green(` saeroon.config.json \uC0DD\uC131 \uC644\uB8CC (siteId: ${siteId})`));
1701
+ } catch (error) {
1702
+ fetchSpinner.stop(
1703
+ chalk5.red(
1704
+ `\uC0AC\uC774\uD2B8 \uC2A4\uD0A4\uB9C8 \uAC00\uC838\uC624\uAE30 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
1705
+ )
1706
+ );
1707
+ process.exit(1);
1708
+ }
1709
+ }
1710
+ var VSCODE_SETTINGS = {
1711
+ "json.schemas": [
1712
+ {
1713
+ url: SCHEMA_URL,
1714
+ fileMatch: ["schema.json", "*.site.json"]
1715
+ }
1716
+ ]
1717
+ };
1718
+ var CLAUDE_SETTINGS = {
1719
+ mcpServers: {
1720
+ saeroon: {
1721
+ command: "npx",
1722
+ args: ["@saeroon/mcp-server"]
1723
+ }
1724
+ }
1725
+ };
1726
+ async function generateAIContextFiles() {
1727
+ const cwd = process.cwd();
1728
+ console.log(chalk5.bold("\nAI context \uD30C\uC77C \uC0DD\uC131\n"));
1729
+ const cursorrulesPath = resolve4(cwd, ".cursorrules");
1730
+ await writeFile2(cursorrulesPath, CURSORRULES_TEMPLATE, "utf-8");
1731
+ console.log(chalk5.green(" .cursorrules \uC0DD\uC131 \uC644\uB8CC"));
1732
+ const claudeMdPath = resolve4(cwd, "CLAUDE.md");
1733
+ await writeFile2(claudeMdPath, CLAUDE_MD_TEMPLATE, "utf-8");
1734
+ console.log(chalk5.green(" CLAUDE.md \uC0DD\uC131 \uC644\uB8CC"));
1735
+ const vscodeDirPath = resolve4(cwd, ".vscode");
1736
+ const vscodeSettingsPath = resolve4(vscodeDirPath, "settings.json");
1737
+ if (!existsSync2(vscodeDirPath)) {
1738
+ await mkdir2(vscodeDirPath, { recursive: true });
1739
+ }
1740
+ if (existsSync2(vscodeSettingsPath)) {
1741
+ try {
1742
+ const existingRaw = readFileSync(vscodeSettingsPath, "utf-8");
1743
+ const existing = safeJsonParse(existingRaw);
1744
+ existing["json.schemas"] = VSCODE_SETTINGS["json.schemas"];
1745
+ await writeFile2(
1746
+ vscodeSettingsPath,
1747
+ JSON.stringify(existing, null, 2) + "\n",
1748
+ "utf-8"
1749
+ );
1750
+ console.log(chalk5.green(" .vscode/settings.json \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC (json.schemas \uCD94\uAC00)"));
1751
+ } catch {
1752
+ await writeFile2(
1753
+ vscodeSettingsPath,
1754
+ JSON.stringify(VSCODE_SETTINGS, null, 2) + "\n",
1755
+ "utf-8"
1756
+ );
1757
+ console.log(chalk5.green(" .vscode/settings.json \uC0DD\uC131 \uC644\uB8CC"));
1758
+ }
1759
+ } else {
1760
+ await writeFile2(
1761
+ vscodeSettingsPath,
1762
+ JSON.stringify(VSCODE_SETTINGS, null, 2) + "\n",
1763
+ "utf-8"
1764
+ );
1765
+ console.log(chalk5.green(" .vscode/settings.json \uC0DD\uC131 \uC644\uB8CC"));
1766
+ }
1767
+ const claudeDirPath = resolve4(cwd, ".claude");
1768
+ const claudeSettingsPath = resolve4(claudeDirPath, "settings.json");
1769
+ if (!existsSync2(claudeDirPath)) {
1770
+ await mkdir2(claudeDirPath, { recursive: true });
1771
+ }
1772
+ if (existsSync2(claudeSettingsPath)) {
1773
+ try {
1774
+ const existingRaw = readFileSync(claudeSettingsPath, "utf-8");
1775
+ const existing = safeJsonParse(existingRaw);
1776
+ const existingMcp = existing.mcpServers ?? {};
1777
+ existingMcp.saeroon = CLAUDE_SETTINGS.mcpServers.saeroon;
1778
+ existing.mcpServers = existingMcp;
1779
+ await writeFile2(
1780
+ claudeSettingsPath,
1781
+ JSON.stringify(existing, null, 2) + "\n",
1782
+ "utf-8"
1783
+ );
1784
+ console.log(chalk5.green(" .claude/settings.json \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC (mcpServers.saeroon \uCD94\uAC00)"));
1785
+ } catch {
1786
+ await writeFile2(
1787
+ claudeSettingsPath,
1788
+ JSON.stringify(CLAUDE_SETTINGS, null, 2) + "\n",
1789
+ "utf-8"
1790
+ );
1791
+ console.log(chalk5.green(" .claude/settings.json \uC0DD\uC131 \uC644\uB8CC"));
1792
+ }
1793
+ } else {
1794
+ await writeFile2(
1795
+ claudeSettingsPath,
1796
+ JSON.stringify(CLAUDE_SETTINGS, null, 2) + "\n",
1797
+ "utf-8"
1798
+ );
1799
+ console.log(chalk5.green(" .claude/settings.json \uC0DD\uC131 \uC644\uB8CC"));
1800
+ }
1801
+ }
1802
+
1803
+ // src/commands/preview.ts
1804
+ import chalk7 from "chalk";
1805
+ import { resolve as resolve6 } from "path";
1806
+ import { existsSync as existsSync4 } from "fs";
1807
+ import openBrowser from "open";
1808
+
1809
+ // src/lib/schema-reader.ts
1810
+ import chalk6 from "chalk";
1811
+ import { readFile as readFile3 } from "fs/promises";
1812
+ import { resolve as resolve5 } from "path";
1813
+ import { existsSync as existsSync3 } from "fs";
1814
+ function resolveSchemaPath(input) {
1815
+ const filePath = input ?? "schema.json";
1816
+ return resolve5(process.cwd(), filePath);
1817
+ }
1818
+ async function loadSchemaFile(schemaPath) {
1819
+ const absolutePath = resolve5(process.cwd(), schemaPath);
1820
+ if (!existsSync3(absolutePath)) {
1821
+ console.error(chalk6.red(`\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${schemaPath}`));
1822
+ process.exit(1);
1823
+ }
1824
+ try {
1825
+ const content = await readFile3(absolutePath, "utf-8");
1826
+ return safeJsonParse(content);
1827
+ } catch (error) {
1828
+ if (error instanceof SyntaxError) {
1829
+ console.error(chalk6.red(`JSON \uAD6C\uBB38 \uC5D0\uB7EC: ${error.message}`));
1830
+ } else {
1831
+ console.error(
1832
+ chalk6.red(
1833
+ `\uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
1834
+ )
1835
+ );
1836
+ }
1837
+ process.exit(1);
1838
+ }
1839
+ }
1840
+
1841
+ // src/watcher.ts
1842
+ import { watch } from "chokidar";
1843
+ import { readFile as readFile4 } from "fs/promises";
1844
+ var FileWatcher = class {
1845
+ filePath;
1846
+ options;
1847
+ watcher = null;
1848
+ debounceTimer = null;
1849
+ constructor(filePath, options) {
1850
+ this.filePath = filePath;
1851
+ this.options = {
1852
+ debounceMs: 500,
1853
+ ...options
1854
+ };
1855
+ }
1856
+ /**
1857
+ * 파일 감시 시작. 초기 로드 시에도 onChange를 호출.
1858
+ */
1859
+ start() {
1860
+ if (this.watcher) {
1861
+ return;
1862
+ }
1863
+ this.watcher = watch(this.filePath, {
1864
+ persistent: true,
1865
+ // 초기 add 이벤트도 수신하여 첫 로드 처리
1866
+ ignoreInitial: false,
1867
+ // 심링크 추적 차단 (보안: cwd 외부 파일 접근 방지)
1868
+ followSymlinks: false,
1869
+ // 에디터의 atomic save 지원
1870
+ awaitWriteFinish: {
1871
+ stabilityThreshold: 100,
1872
+ pollInterval: 50
1873
+ }
1874
+ });
1875
+ this.watcher.on("add", () => this.scheduleProcess());
1876
+ this.watcher.on("change", () => this.scheduleProcess());
1877
+ this.watcher.on("error", (error) => {
1878
+ this.options.onError({
1879
+ type: "unknown",
1880
+ message: `\uD30C\uC77C \uAC10\uC2DC \uC5D0\uB7EC: ${error instanceof Error ? error.message : String(error)}`
1881
+ });
1882
+ });
1883
+ this.watcher.on("unlink", () => {
1884
+ this.options.onError({
1885
+ type: "read",
1886
+ message: `\uD30C\uC77C\uC774 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${this.filePath}`
1887
+ });
1888
+ });
1889
+ }
1890
+ /**
1891
+ * 파일 감시 중지 및 리소스 정리.
1892
+ */
1893
+ stop() {
1894
+ if (this.debounceTimer) {
1895
+ clearTimeout(this.debounceTimer);
1896
+ this.debounceTimer = null;
1897
+ }
1898
+ if (this.watcher) {
1899
+ this.watcher.close();
1900
+ this.watcher = null;
1901
+ }
1902
+ }
1903
+ /**
1904
+ * 디바운스를 적용하여 파일 처리 예약.
1905
+ */
1906
+ scheduleProcess() {
1907
+ if (this.debounceTimer) {
1908
+ clearTimeout(this.debounceTimer);
1909
+ }
1910
+ this.debounceTimer = setTimeout(() => {
1911
+ this.debounceTimer = null;
1912
+ this.processFile();
1913
+ }, this.options.debounceMs);
1914
+ }
1915
+ /**
1916
+ * 파일을 읽고 JSON으로 파싱. 에러 발생 시 상세 위치 정보 제공.
1917
+ */
1918
+ async processFile() {
1919
+ let content;
1920
+ try {
1921
+ content = await readFile4(this.filePath, "utf-8");
1922
+ } catch (error) {
1923
+ const message = error instanceof Error ? error.message : String(error);
1924
+ this.options.onError({
1925
+ type: "read",
1926
+ message: `\uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${message}`
1927
+ });
1928
+ return;
1929
+ }
1930
+ if (content.trim().length === 0) {
1931
+ this.options.onError({
1932
+ type: "parse",
1933
+ message: "\uD30C\uC77C\uC774 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4."
1934
+ });
1935
+ return;
1936
+ }
1937
+ try {
1938
+ const schema = safeJsonParse(content);
1939
+ this.options.onChange(schema);
1940
+ } catch (error) {
1941
+ if (error instanceof SyntaxError) {
1942
+ const { line, column } = this.extractJsonErrorPosition(
1943
+ content,
1944
+ error.message
1945
+ );
1946
+ this.options.onError({
1947
+ type: "parse",
1948
+ message: `JSON \uAD6C\uBB38 \uC5D0\uB7EC: ${error.message}`,
1949
+ line,
1950
+ column
1951
+ });
1952
+ } else {
1953
+ this.options.onError({
1954
+ type: "parse",
1955
+ message: `JSON \uD30C\uC2F1 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
1956
+ });
1957
+ }
1958
+ }
1959
+ }
1960
+ /**
1961
+ * JSON 파싱 에러 메시지에서 위치 정보 추출.
1962
+ * Node.js의 SyntaxError 메시지에서 "position N" 패턴을 파싱하여
1963
+ * 라인/컬럼 번호로 변환.
1964
+ */
1965
+ extractJsonErrorPosition(content, errorMessage) {
1966
+ const positionMatch = errorMessage.match(/position\s+(\d+)/);
1967
+ if (!positionMatch) {
1968
+ return { line: void 0, column: void 0 };
1969
+ }
1970
+ const position = parseInt(positionMatch[1], 10);
1971
+ const beforeError = content.slice(0, position);
1972
+ const lines = beforeError.split("\n");
1973
+ const line = lines.length;
1974
+ const column = (lines[lines.length - 1]?.length ?? 0) + 1;
1975
+ return { line, column };
1976
+ }
1977
+ };
1978
+
1979
+ // src/preview-client.ts
1980
+ import WebSocket from "ws";
1981
+ var MAX_RETRIES = 5;
1982
+ var BASE_RETRY_DELAY_MS = 1e3;
1983
+ var PreviewClient = class {
1984
+ options;
1985
+ session = null;
1986
+ ws = null;
1987
+ retryCount = 0;
1988
+ isDisconnecting = false;
1989
+ pingInterval = null;
1990
+ lastSchema = null;
1991
+ constructor(options) {
1992
+ this.options = {
1993
+ device: "desktop",
1994
+ ...options
1995
+ };
1996
+ }
1997
+ /**
1998
+ * Preview API에 연결. 세션을 생성하고 WebSocket 연결을 수립.
1999
+ * @param schema 초기 스키마
2000
+ */
2001
+ async connect(schema) {
2002
+ this.isDisconnecting = false;
2003
+ this.lastSchema = schema;
2004
+ this.session = await this.createSession(schema);
2005
+ await this.connectWebSocket();
2006
+ }
2007
+ /**
2008
+ * 스키마를 업데이트하여 새로운 프리뷰를 요청.
2009
+ * WebSocket이 연결되어 있지 않으면 에러를 발생.
2010
+ */
2011
+ async updateSchema(schema) {
2012
+ this.lastSchema = schema;
2013
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
2014
+ throw new Error(
2015
+ "WebSocket\uC774 \uC5F0\uACB0\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. connect()\uB97C \uBA3C\uC800 \uD638\uCD9C\uD558\uC138\uC694."
2016
+ );
2017
+ }
2018
+ const message = {
2019
+ type: "update",
2020
+ payload: {
2021
+ schema,
2022
+ device: this.options.device ?? "desktop"
2023
+ }
2024
+ };
2025
+ this.ws.send(JSON.stringify(message));
2026
+ }
2027
+ /**
2028
+ * 연결 종료 및 리소스 정리.
2029
+ */
2030
+ async disconnect() {
2031
+ this.isDisconnecting = true;
2032
+ this.stopPingInterval();
2033
+ if (this.ws) {
2034
+ this.ws.close(1e3, "Client disconnect");
2035
+ this.ws = null;
2036
+ }
2037
+ this.session = null;
2038
+ this.retryCount = 0;
2039
+ this.lastSchema = null;
2040
+ this.options.onStatusChange?.(false);
2041
+ }
2042
+ /** 현재 세션 정보 */
2043
+ getSession() {
2044
+ return this.session;
2045
+ }
2046
+ /** WebSocket 연결 상태 */
2047
+ isConnected() {
2048
+ return this.ws?.readyState === WebSocket.OPEN;
2049
+ }
2050
+ // --- Private Methods ---
2051
+ /**
2052
+ * POST /api/v1/developer/preview 호출하여 Preview 세션 생성.
2053
+ */
2054
+ async createSession(schema) {
2055
+ const url = `${this.options.apiBaseUrl}/api/v1/developer/preview`;
2056
+ const response = await fetch(url, {
2057
+ method: "POST",
2058
+ headers: {
2059
+ "Content-Type": "application/json",
2060
+ Authorization: `Bearer ${this.options.apiKey}`
2061
+ },
2062
+ body: JSON.stringify({
2063
+ schema,
2064
+ device: this.options.device
2065
+ }),
2066
+ signal: AbortSignal.timeout(3e4)
2067
+ });
2068
+ if (!response.ok) {
2069
+ const errorBody = await response.text().catch(() => "");
2070
+ throw new Error(
2071
+ `Preview \uC138\uC158 \uC0DD\uC131 \uC2E4\uD328 (HTTP ${response.status}): ${errorBody || response.statusText}`
2072
+ );
2073
+ }
2074
+ const responseText = await response.text();
2075
+ const data = safeJsonParse(responseText);
2076
+ return data;
2077
+ }
2078
+ /**
2079
+ * WebSocket 연결 수립 및 이벤트 핸들러 등록.
2080
+ */
2081
+ connectWebSocket() {
2082
+ return new Promise((resolve13, reject) => {
2083
+ if (!this.session) {
2084
+ reject(new Error("\uC138\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
2085
+ return;
2086
+ }
2087
+ const ws = new WebSocket(this.session.wsEndpoint, {
2088
+ headers: {
2089
+ Authorization: `Bearer ${this.options.apiKey}`
2090
+ }
2091
+ });
2092
+ ws.on("open", () => {
2093
+ this.retryCount = 0;
2094
+ this.options.onStatusChange?.(true);
2095
+ this.startPingInterval();
2096
+ resolve13();
2097
+ });
2098
+ ws.on("message", (data) => {
2099
+ this.handleMessage(data);
2100
+ });
2101
+ ws.on("close", (code, reason) => {
2102
+ this.options.onStatusChange?.(false);
2103
+ this.stopPingInterval();
2104
+ if (!this.isDisconnecting && code !== 1e3) {
2105
+ this.attemptReconnect();
2106
+ }
2107
+ });
2108
+ ws.on("error", (error) => {
2109
+ if (this.ws === null) {
2110
+ reject(
2111
+ new Error(`WebSocket \uC5F0\uACB0 \uC2E4\uD328: ${error.message}`)
2112
+ );
2113
+ return;
2114
+ }
2115
+ });
2116
+ this.ws = ws;
2117
+ });
2118
+ }
2119
+ /**
2120
+ * WebSocket 메시지 핸들링. render 결과와 validation 에러를 구분하여 콜백 호출.
2121
+ */
2122
+ handleMessage(data) {
2123
+ let message;
2124
+ try {
2125
+ message = safeJsonParse(data.toString());
2126
+ } catch {
2127
+ return;
2128
+ }
2129
+ switch (message.type) {
2130
+ case "render":
2131
+ this.options.onRendered?.(message.payload);
2132
+ break;
2133
+ case "error":
2134
+ this.options.onError?.(message.payload.errors);
2135
+ break;
2136
+ case "pong":
2137
+ break;
2138
+ }
2139
+ }
2140
+ /**
2141
+ * 연결 끊김 시 지수 백오프로 재연결 시도.
2142
+ * 최대 MAX_RETRIES 회까지 시도하며, 각 시도 간 대기 시간은 2배씩 증가.
2143
+ */
2144
+ attemptReconnect() {
2145
+ if (this.retryCount >= MAX_RETRIES) {
2146
+ this.options.onError?.([
2147
+ {
2148
+ step: 1,
2149
+ severity: "error",
2150
+ path: "",
2151
+ message: `WebSocket \uC7AC\uC5F0\uACB0 \uC2E4\uD328: ${MAX_RETRIES}\uD68C \uC2DC\uB3C4 \uD6C4 \uD3EC\uAE30`
2152
+ }
2153
+ ]);
2154
+ return;
2155
+ }
2156
+ this.retryCount++;
2157
+ const delay = BASE_RETRY_DELAY_MS * Math.pow(2, this.retryCount - 1);
2158
+ setTimeout(async () => {
2159
+ if (this.isDisconnecting) {
2160
+ return;
2161
+ }
2162
+ try {
2163
+ if (this.lastSchema) {
2164
+ this.session = await this.createSession(this.lastSchema);
2165
+ }
2166
+ await this.connectWebSocket();
2167
+ if (this.lastSchema) {
2168
+ await this.updateSchema(this.lastSchema);
2169
+ }
2170
+ } catch {
2171
+ this.attemptReconnect();
2172
+ }
2173
+ }, delay);
2174
+ }
2175
+ /**
2176
+ * 30초 간격 ping으로 WebSocket 연결 유지.
2177
+ */
2178
+ startPingInterval() {
2179
+ this.stopPingInterval();
2180
+ this.pingInterval = setInterval(() => {
2181
+ if (this.ws?.readyState === WebSocket.OPEN) {
2182
+ const message = { type: "ping" };
2183
+ this.ws.send(JSON.stringify(message));
2184
+ }
2185
+ }, 3e4);
2186
+ }
2187
+ stopPingInterval() {
2188
+ if (this.pingInterval) {
2189
+ clearInterval(this.pingInterval);
2190
+ this.pingInterval = null;
2191
+ }
2192
+ }
2193
+ };
2194
+
2195
+ // src/commands/preview.ts
2196
+ function timestamp() {
2197
+ return chalk7.dim(
2198
+ `[${(/* @__PURE__ */ new Date()).toLocaleTimeString("ko-KR", { hour12: false })}]`
2199
+ );
2200
+ }
2201
+ async function startRestPreview(schemaPath, absolutePath, client) {
2202
+ let previewUrl = null;
2203
+ let currentPreviewId = null;
2204
+ let isFirstUpload = true;
2205
+ const watcher = new FileWatcher(absolutePath, {
2206
+ debounceMs: 500,
2207
+ async onChange(schema) {
2208
+ try {
2209
+ if (isFirstUpload) {
2210
+ const uploadSpinner = spinner("Validating...");
2211
+ const result = await client.createPreviewSite(schema);
2212
+ previewUrl = result.previewUrl;
2213
+ currentPreviewId = result.id;
2214
+ const { stats } = result.validation;
2215
+ const statusText = result.validation.passed ? chalk7.green(`Validating... done (${stats.blockCount} blocks, ${stats.pageCount} pages)`) : chalk7.yellow(`Validating... done (${result.validation.errors.length} errors)`);
2216
+ uploadSpinner.stop(statusText);
2217
+ const allIssues = [
2218
+ ...result.validation.errors,
2219
+ ...result.validation.warnings
2220
+ ];
2221
+ if (allIssues.length > 0) {
2222
+ formatValidationErrors(allIssues);
2223
+ }
2224
+ formatQualityReport(result.quality);
2225
+ console.log(`
2226
+ Watching ${chalk7.cyan(schemaPath)} for changes...`);
2227
+ console.log(`Preview URL: ${chalk7.cyan(previewUrl)}`);
2228
+ console.log(chalk7.dim(" \u2192 Open in browser (cmd+click)"));
2229
+ console.log("");
2230
+ await openBrowser(previewUrl);
2231
+ isFirstUpload = false;
2232
+ } else {
2233
+ console.log(`${timestamp()} File changed: ${chalk7.cyan(schemaPath)}`);
2234
+ const uploadSpinner = spinner("Validating...");
2235
+ const startTime = Date.now();
2236
+ let result;
2237
+ if (currentPreviewId) {
2238
+ try {
2239
+ result = await client.updatePreviewSite(currentPreviewId, schema);
2240
+ } catch (error) {
2241
+ if (error instanceof ApiError && error.statusCode === 404) {
2242
+ console.log(`${timestamp()} ${chalk7.yellow("Preview expired, creating new one...")}`);
2243
+ result = await client.createPreviewSite(schema);
2244
+ currentPreviewId = result.id;
2245
+ } else {
2246
+ throw error;
2247
+ }
2248
+ }
2249
+ } else {
2250
+ result = await client.createPreviewSite(schema);
2251
+ currentPreviewId = result.id;
2252
+ }
2253
+ const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
2254
+ const { stats } = result.validation;
2255
+ if (result.validation.passed) {
2256
+ uploadSpinner.stop(
2257
+ `${timestamp()} ${chalk7.green(`Validating... done (${stats.blockCount} blocks, ${stats.pageCount} pages)`)}`
2258
+ );
2259
+ } else {
2260
+ uploadSpinner.stop(
2261
+ `${timestamp()} ${chalk7.yellow(`Validating... done (${result.validation.errors.length} errors)`)}`
2262
+ );
2263
+ }
2264
+ const allIssues = [
2265
+ ...result.validation.errors,
2266
+ ...result.validation.warnings
2267
+ ];
2268
+ if (allIssues.length > 0) {
2269
+ formatValidationErrors(allIssues);
2270
+ }
2271
+ if (result.previewUrl !== previewUrl) {
2272
+ previewUrl = result.previewUrl;
2273
+ console.log(`${timestamp()} Preview URL: ${chalk7.cyan(previewUrl)}`);
2274
+ }
2275
+ console.log(`${timestamp()} ${chalk7.green(`Preview updated (${elapsed}s)`)}`);
2276
+ console.log("");
2277
+ }
2278
+ } catch (error) {
2279
+ if (error instanceof ApiError) {
2280
+ console.error(
2281
+ `${timestamp()} ${chalk7.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)}`
2282
+ );
2283
+ } else {
2284
+ console.error(
2285
+ `${timestamp()} ${chalk7.red(`\uD504\uB9AC\uBDF0 \uC5D0\uB7EC: ${error instanceof Error ? error.message : String(error)}`)}`
2286
+ );
2287
+ }
2288
+ }
2289
+ },
2290
+ onError(error) {
2291
+ if (error.line !== void 0 && error.column !== void 0) {
2292
+ console.error(
2293
+ `${timestamp()} ${chalk7.red(`${error.message} (${error.line}:${error.column})`)}`
2294
+ );
2295
+ } else {
2296
+ console.error(`${timestamp()} ${chalk7.red(error.message)}`);
2297
+ }
2298
+ }
2299
+ });
2300
+ watcher.start();
2301
+ const cleanup = () => {
2302
+ console.log(chalk7.dim("\n\uC885\uB8CC \uC911..."));
2303
+ watcher.stop();
2304
+ process.exit(0);
2305
+ };
2306
+ process.on("SIGINT", cleanup);
2307
+ process.on("SIGTERM", cleanup);
2308
+ }
2309
+ async function startWsPreview(schemaPath, absolutePath, apiKey, apiBaseUrl, device) {
2310
+ const initialSchema = await loadSchemaFile(schemaPath);
2311
+ console.log(chalk7.dim("WebSocket \uD504\uB9AC\uBDF0 \uC138\uC158\uC744 \uC0DD\uC131\uD558\uB294 \uC911..."));
2312
+ const client = new PreviewClient({
2313
+ apiKey,
2314
+ apiBaseUrl,
2315
+ device,
2316
+ onRendered(result) {
2317
+ console.log(
2318
+ `${timestamp()} ${chalk7.green(`\uB80C\uB354\uB9C1 \uC644\uB8CC (${result.renderTime}ms)`)}`
2319
+ );
2320
+ },
2321
+ onError(errors) {
2322
+ formatValidationErrors(errors);
2323
+ },
2324
+ onStatusChange(connected) {
2325
+ if (connected) {
2326
+ const session2 = client.getSession();
2327
+ if (session2) {
2328
+ console.log(
2329
+ `${timestamp()} ${chalk7.green("WebSocket \uC5F0\uACB0\uB428")}`
2330
+ );
2331
+ }
2332
+ } else {
2333
+ console.log(
2334
+ `${timestamp()} ${chalk7.yellow("\uC5F0\uACB0 \uB04A\uAE40. \uC7AC\uC5F0\uACB0 \uC911...")}`
2335
+ );
2336
+ }
2337
+ }
2338
+ });
2339
+ await client.connect(initialSchema);
2340
+ const session = client.getSession();
2341
+ if (session) {
2342
+ console.log(`
2343
+ Watching ${chalk7.cyan(schemaPath)} for changes...`);
2344
+ console.log(`Preview URL: ${chalk7.cyan(session.previewUrl)}`);
2345
+ console.log(chalk7.dim(" \u2192 Open in browser (cmd+click)"));
2346
+ console.log("");
2347
+ await openBrowser(session.previewUrl);
2348
+ }
2349
+ const watcher = new FileWatcher(absolutePath, {
2350
+ debounceMs: 300,
2351
+ // WebSocket은 REST보다 짧은 디바운스
2352
+ async onChange(schema) {
2353
+ try {
2354
+ console.log(`${timestamp()} File changed: ${chalk7.cyan(schemaPath)}`);
2355
+ const startTime = Date.now();
2356
+ await client.updateSchema(schema);
2357
+ const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
2358
+ console.log(`${timestamp()} ${chalk7.green(`Schema sent (${elapsed}s)`)}`);
2359
+ } catch (error) {
2360
+ console.error(
2361
+ `${timestamp()} ${chalk7.red(`\uC5C5\uB370\uC774\uD2B8 \uC5D0\uB7EC: ${error instanceof Error ? error.message : String(error)}`)}`
2362
+ );
2363
+ }
2364
+ },
2365
+ onError(error) {
2366
+ if (error.line !== void 0 && error.column !== void 0) {
2367
+ console.error(
2368
+ `${timestamp()} ${chalk7.red(`${error.message} (${error.line}:${error.column})`)}`
2369
+ );
2370
+ } else {
2371
+ console.error(`${timestamp()} ${chalk7.red(error.message)}`);
2372
+ }
2373
+ }
2374
+ });
2375
+ watcher.start();
2376
+ const cleanup = async () => {
2377
+ console.log(chalk7.dim("\n\uC885\uB8CC \uC911..."));
2378
+ watcher.stop();
2379
+ await client.disconnect();
2380
+ process.exit(0);
2381
+ };
2382
+ process.on("SIGINT", cleanup);
2383
+ process.on("SIGTERM", cleanup);
2384
+ }
2385
+ async function commandPreview(schemaPath, options) {
2386
+ const resolvedPath = schemaPath ?? "schema.json";
2387
+ const absolutePath = resolve6(process.cwd(), resolvedPath);
2388
+ if (!existsSync4(absolutePath)) {
2389
+ console.error(chalk7.red(`\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${resolvedPath}`));
2390
+ process.exit(1);
2391
+ }
2392
+ const apiKey = await resolveApiKey(options.apiKey);
2393
+ const config = await loadConfig();
2394
+ const apiBaseUrl = await getApiBaseUrl();
2395
+ const device = options.device ?? config.defaultDevice ?? "desktop";
2396
+ const mode = options.mode ?? "rest";
2397
+ console.log(chalk7.bold("\nSaeroon Developer Preview\n"));
2398
+ console.log(chalk7.dim(` \uD30C\uC77C: ${absolutePath}`));
2399
+ console.log(chalk7.dim(` \uB514\uBC14\uC774\uC2A4: ${device}`));
2400
+ console.log(chalk7.dim(` \uBAA8\uB4DC: ${mode}`));
2401
+ console.log(chalk7.dim(` API: ${apiBaseUrl}`));
2402
+ console.log("");
2403
+ if (mode === "ws") {
2404
+ try {
2405
+ await startWsPreview(resolvedPath, absolutePath, apiKey, apiBaseUrl, device);
2406
+ } catch (error) {
2407
+ if (error instanceof ApiError) {
2408
+ console.error(chalk7.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
2409
+ } else {
2410
+ console.error(
2411
+ chalk7.red(
2412
+ `WebSocket \uD504\uB9AC\uBDF0 \uC2DC\uC791 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
2413
+ )
2414
+ );
2415
+ }
2416
+ process.exit(1);
2417
+ }
2418
+ } else {
2419
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
2420
+ await startRestPreview(resolvedPath, absolutePath, client);
2421
+ }
2422
+ }
2423
+
2424
+ // src/commands/validate.ts
2425
+ import chalk8 from "chalk";
2426
+
2427
+ // src/lib/local-validator.ts
2428
+ var VALID_BLOCK_TYPES = /* @__PURE__ */ new Set([
2429
+ // Element (9)
2430
+ "text-block",
2431
+ "heading-block",
2432
+ "button-block",
2433
+ "image-block",
2434
+ "video-block",
2435
+ "divider",
2436
+ "spacer",
2437
+ "icon-block",
2438
+ "container",
2439
+ // Feature (70)
2440
+ "image-slider",
2441
+ "map-block",
2442
+ "contact-form",
2443
+ "demolition-calculator",
2444
+ "floating-social-widget",
2445
+ "modal-block",
2446
+ "sticky-cta-bar",
2447
+ "product-grid",
2448
+ "cart-widget",
2449
+ "product-gallery",
2450
+ "product-price",
2451
+ "stock-badge",
2452
+ "variant-selector",
2453
+ "product-filter",
2454
+ "product-search",
2455
+ "related-products",
2456
+ "add-to-cart",
2457
+ "cart-contents",
2458
+ "cart-summary",
2459
+ "coupon-input",
2460
+ "checkout-form",
2461
+ "order-confirmation",
2462
+ "order-history",
2463
+ "order-lookup",
2464
+ "review-list",
2465
+ "cart-drawer",
2466
+ "wishlist",
2467
+ "recently-viewed",
2468
+ "booking-calendar",
2469
+ "medical-booking-block",
2470
+ "booking-button",
2471
+ "service-detail-block",
2472
+ "booking-service-list",
2473
+ "booking-service-detail",
2474
+ "booking-checkout",
2475
+ "booking-confirmation",
2476
+ "booking-my-bookings",
2477
+ "booking-staff-list",
2478
+ "booking-guest-cancel",
2479
+ "booking-class-schedule",
2480
+ "booking-course-detail",
2481
+ "booking-course-progress",
2482
+ "booking-resource-calendar",
2483
+ "booking-resource-list",
2484
+ "auth-block",
2485
+ "member-profile-block",
2486
+ "member-only-section",
2487
+ "board-block",
2488
+ "board-detail-block",
2489
+ "faq-accordion",
2490
+ "gallery-block",
2491
+ "before-after-slider",
2492
+ "testimonials-section",
2493
+ "tabs-section",
2494
+ "countdown-timer",
2495
+ "newsletter",
2496
+ "marquee-block",
2497
+ "before-after-gallery",
2498
+ "content-showcase",
2499
+ "staff-showcase",
2500
+ "site-menu",
2501
+ "stats-counter",
2502
+ "scroll-to-top",
2503
+ "anchor-nav",
2504
+ "trademark-search-block",
2505
+ "trademark-detail-block",
2506
+ "nice-class-browser-block",
2507
+ "lottie-block",
2508
+ "model-block",
2509
+ "image-sequence-block",
2510
+ // Structure (6)
2511
+ "header-block",
2512
+ "footer-block",
2513
+ "nav-block",
2514
+ "main-block",
2515
+ "aside-block",
2516
+ "article-block",
2517
+ // Pattern (13, legacy but valid)
2518
+ "hero",
2519
+ "cta-banner",
2520
+ "feature-grid",
2521
+ "pricing-table",
2522
+ "team-profile-section",
2523
+ "logo-cloud",
2524
+ "social-proof",
2525
+ "timeline",
2526
+ "announcement-list",
2527
+ "service-card-grid",
2528
+ "business-hours",
2529
+ "split-landing-hero",
2530
+ "event-banner"
2531
+ ]);
2532
+ function validateSchemaLocal(schema) {
2533
+ const errors = [];
2534
+ if (!schema || typeof schema !== "object") {
2535
+ errors.push({ severity: "error", message: "\uC2A4\uD0A4\uB9C8\uAC00 \uC720\uD6A8\uD55C JSON \uAC1D\uCCB4\uAC00 \uC544\uB2D9\uB2C8\uB2E4.", step: 1 });
2536
+ return errors;
2537
+ }
2538
+ const s = schema;
2539
+ if (!s.schemaVersion && !s.version) {
2540
+ errors.push({ severity: "error", message: '\uD544\uC218 \uD544\uB4DC \uB204\uB77D: "schemaVersion"', path: "schemaVersion", step: 1 });
2541
+ }
2542
+ if (!s.global && !s.settings) {
2543
+ errors.push({ severity: "error", message: '\uD544\uC218 \uD544\uB4DC \uB204\uB77D: "global"', path: "global", step: 1 });
2544
+ }
2545
+ if (!s.pages) {
2546
+ errors.push({ severity: "error", message: '\uD544\uC218 \uD544\uB4DC \uB204\uB77D: "pages"', path: "pages", step: 1 });
2547
+ }
2548
+ const version2 = s.schemaVersion ?? s.version;
2549
+ if (version2 && typeof version2 === "string") {
2550
+ const parts = version2.split(".").map(Number);
2551
+ if (parts[0] !== 1 || parts[1] !== void 0 && parts[1] < 15) {
2552
+ errors.push({
2553
+ severity: "warning",
2554
+ message: `\uC2A4\uD0A4\uB9C8 \uBC84\uC804 "${version2}"\uC774 \uC624\uB798\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uCD5C\uC2E0: 1.20.0`,
2555
+ path: "schemaVersion",
2556
+ step: 1
2557
+ });
2558
+ }
2559
+ }
2560
+ const global = s.global ?? s.settings;
2561
+ if (global && typeof global === "object") {
2562
+ if (!global.name && !global.title) {
2563
+ errors.push({
2564
+ severity: "error",
2565
+ message: "global.name (\uB610\uB294 settings.title) \uC740 \uD544\uC218\uC785\uB2C8\uB2E4.",
2566
+ path: "global.name",
2567
+ step: 2
2568
+ });
2569
+ }
2570
+ }
2571
+ if (!s.pages) return errors;
2572
+ const pages = s.pages;
2573
+ const isArray = Array.isArray(pages);
2574
+ const pageEntries = isArray ? pages.map((p, i) => [String(p.id ?? i), p]) : Object.entries(pages);
2575
+ for (const [pageId, page] of pageEntries) {
2576
+ const p = page;
2577
+ const pagePath = `pages.${pageId}`;
2578
+ if (!p.blocks || typeof p.blocks !== "object") {
2579
+ errors.push({
2580
+ severity: "error",
2581
+ message: `\uD398\uC774\uC9C0 "${pageId}": blocks \uB9F5\uC774 \uC5C6\uAC70\uB098 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`,
2582
+ path: `${pagePath}.blocks`,
2583
+ step: 3
2584
+ });
2585
+ continue;
2586
+ }
2587
+ if (!p.rootBlockId) {
2588
+ errors.push({
2589
+ severity: "error",
2590
+ message: `\uD398\uC774\uC9C0 "${pageId}": rootBlockId\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
2591
+ path: `${pagePath}.rootBlockId`,
2592
+ step: 3
2593
+ });
2594
+ }
2595
+ const blocks = p.blocks;
2596
+ if (p.rootBlockId && typeof p.rootBlockId === "string") {
2597
+ if (!blocks[p.rootBlockId]) {
2598
+ errors.push({
2599
+ severity: "error",
2600
+ message: `\uD398\uC774\uC9C0 "${pageId}": rootBlockId "${p.rootBlockId}"\uAC00 blocks\uC5D0 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`,
2601
+ path: `${pagePath}.rootBlockId`,
2602
+ step: 3
2603
+ });
2604
+ }
2605
+ }
2606
+ for (const [blockId, block] of Object.entries(blocks)) {
2607
+ const blockPath = `${pagePath}.blocks.${blockId}`;
2608
+ if (!block.type) {
2609
+ errors.push({
2610
+ severity: "error",
2611
+ message: `\uBE14\uB85D "${blockId}": type\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`,
2612
+ path: `${blockPath}.type`,
2613
+ step: 2
2614
+ });
2615
+ continue;
2616
+ }
2617
+ const blockType = String(block.type);
2618
+ if (!VALID_BLOCK_TYPES.has(blockType)) {
2619
+ errors.push({
2620
+ severity: "warning",
2621
+ message: `\uBE14\uB85D "${blockId}": \uC54C \uC218 \uC5C6\uB294 \uBE14\uB85D \uD0C0\uC785 "${blockType}"`,
2622
+ path: `${blockPath}.type`,
2623
+ step: 2
2624
+ });
2625
+ }
2626
+ if (!block.props && blockType !== "container") {
2627
+ errors.push({
2628
+ severity: "warning",
2629
+ message: `\uBE14\uB85D "${blockId}": props\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
2630
+ path: `${blockPath}.props`,
2631
+ step: 2
2632
+ });
2633
+ }
2634
+ if (block.children && Array.isArray(block.children)) {
2635
+ for (const childId of block.children) {
2636
+ if (typeof childId === "string" && !blocks[childId]) {
2637
+ errors.push({
2638
+ severity: "error",
2639
+ message: `\uBE14\uB85D "${blockId}": children "${childId}"\uAC00 blocks\uC5D0 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`,
2640
+ path: `${blockPath}.children`,
2641
+ step: 3
2642
+ });
2643
+ }
2644
+ }
2645
+ }
2646
+ }
2647
+ }
2648
+ return errors;
2649
+ }
2650
+
2651
+ // src/commands/validate.ts
2652
+ async function commandValidate(schemaPath, options) {
2653
+ console.log(chalk8.bold("\n\uC2A4\uD0A4\uB9C8 \uC720\uD6A8\uC131 \uAC80\uC0AC\n"));
2654
+ console.log(chalk8.dim(` \uD30C\uC77C: ${schemaPath}`));
2655
+ if (options.local) {
2656
+ console.log(chalk8.dim(" \uBAA8\uB4DC: \uB85C\uCEEC (\uC624\uD504\uB77C\uC778)"));
2657
+ }
2658
+ console.log("");
2659
+ const schema = await loadSchemaFile(schemaPath);
2660
+ if (options.local) {
2661
+ const localErrors = validateSchemaLocal(schema);
2662
+ const errors = localErrors.filter((e) => e.severity === "error");
2663
+ const warnings = localErrors.filter((e) => e.severity === "warning");
2664
+ if (localErrors.length === 0) {
2665
+ console.log(chalk8.green("\uC720\uD6A8\uC131 \uAC80\uC0AC \uD1B5\uACFC! \uC5D0\uB7EC \uC5C6\uC74C."));
2666
+ } else {
2667
+ const byStep = /* @__PURE__ */ new Map();
2668
+ for (const e of localErrors) {
2669
+ const list = byStep.get(e.step) ?? [];
2670
+ list.push(e);
2671
+ byStep.set(e.step, list);
2672
+ }
2673
+ const stepNames = {
2674
+ 1: "Schema Structure",
2675
+ 2: "Block Properties",
2676
+ 3: "Reference Integrity"
2677
+ };
2678
+ for (const [step, items] of [...byStep.entries()].sort((a, b) => a[0] - b[0])) {
2679
+ console.log(chalk8.bold(` Step ${step}: ${stepNames[step] ?? "Validation"}`));
2680
+ for (const item of items) {
2681
+ const icon = item.severity === "error" ? chalk8.red("\u2716") : chalk8.yellow("\u26A0");
2682
+ const pathStr = item.path ? chalk8.dim(` (${item.path})`) : "";
2683
+ console.log(` ${icon} ${item.message}${pathStr}`);
2684
+ }
2685
+ console.log("");
2686
+ }
2687
+ console.log(chalk8.dim(` \uC5D0\uB7EC: ${errors.length}\uAC1C, \uACBD\uACE0: ${warnings.length}\uAC1C`));
2688
+ }
2689
+ console.log(chalk8.dim("\n \uD488\uC9C8 \uC810\uC218: \uB85C\uCEEC \uBAA8\uB4DC\uC5D0\uC11C\uB294 \uC0AC\uC6A9 \uBD88\uAC00"));
2690
+ console.log(chalk8.dim(" --local \uC81C\uAC70 \uC2DC \uC11C\uBC84 \uC810\uC218 \uD655\uC778 \uAC00\uB2A5\n"));
2691
+ if (errors.length > 0) {
2692
+ console.log(chalk8.red.bold("\uC5D0\uB7EC\uB97C \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."));
2693
+ process.exit(1);
2694
+ } else {
2695
+ console.log(chalk8.green.bold("\uC2A4\uD0A4\uB9C8\uAC00 \uC720\uD6A8\uD569\uB2C8\uB2E4!"));
2696
+ process.exit(0);
2697
+ }
2698
+ return;
2699
+ }
2700
+ const apiKey = await resolveApiKey(options.apiKey);
2701
+ const apiBaseUrl = await getApiBaseUrl();
2702
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
2703
+ const validateSpinner = spinner("\uC11C\uBC84\uC5D0 \uC720\uD6A8\uC131 \uAC80\uC0AC \uC694\uCCAD \uC911...");
2704
+ try {
2705
+ const result = await client.validateSchema(schema);
2706
+ validateSpinner.stop("");
2707
+ const allIssues = [
2708
+ ...result.validation.errors,
2709
+ ...result.validation.warnings
2710
+ ];
2711
+ formatValidationErrors(allIssues);
2712
+ formatQualityReport(result.quality);
2713
+ console.log(
2714
+ chalk8.cyan(`Preview URL: ${result.previewUrl}`)
2715
+ );
2716
+ console.log(chalk8.dim(`\uB9CC\uB8CC: ${result.expiresAt}
2717
+ `));
2718
+ if (result.validation.passed) {
2719
+ console.log(chalk8.green.bold("\uC2A4\uD0A4\uB9C8\uAC00 \uC720\uD6A8\uD569\uB2C8\uB2E4!"));
2720
+ process.exit(0);
2721
+ } else {
2722
+ console.log(chalk8.red.bold("\uC5D0\uB7EC\uB97C \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."));
2723
+ process.exit(1);
2724
+ }
2725
+ } catch (error) {
2726
+ if (error instanceof ApiError) {
2727
+ validateSpinner.stop(
2728
+ chalk8.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
2729
+ );
2730
+ } else {
2731
+ validateSpinner.stop(
2732
+ chalk8.red(
2733
+ `\uAC80\uC99D \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
2734
+ )
2735
+ );
2736
+ }
2737
+ process.exit(1);
2738
+ }
2739
+ }
2740
+
2741
+ // src/commands/deploy.ts
2742
+ import chalk9 from "chalk";
2743
+ import { createInterface as createInterface2 } from "readline/promises";
2744
+ import { stdin as stdin2, stdout as stdout2 } from "process";
2745
+
2746
+ // src/lib/project-config.ts
2747
+ import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
2748
+ import { resolve as resolve7 } from "path";
2749
+ import { existsSync as existsSync5 } from "fs";
2750
+ async function loadProjectConfig() {
2751
+ const configPath = resolve7(process.cwd(), "saeroon.config.json");
2752
+ if (!existsSync5(configPath)) {
2753
+ return null;
2754
+ }
2755
+ try {
2756
+ const content = await readFile5(configPath, "utf-8");
2757
+ return safeJsonParse(content);
2758
+ } catch {
2759
+ return null;
2760
+ }
2761
+ }
2762
+ async function saveProjectConfig(updates) {
2763
+ const configPath = resolve7(process.cwd(), "saeroon.config.json");
2764
+ const existing = await loadProjectConfig() ?? { schemaFile: "schema.json" };
2765
+ const merged = { ...existing, ...updates };
2766
+ await writeFile3(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2767
+ }
2768
+
2769
+ // src/lib/asset-processor.ts
2770
+ import { createHash } from "crypto";
2771
+ import { createReadStream } from "fs";
2772
+ import { readFile as readFile6, stat } from "fs/promises";
2773
+ import { resolve as resolve8 } from "path";
2774
+ import { existsSync as existsSync6 } from "fs";
2775
+ var IMAGE_PROPERTY_NAMES = /* @__PURE__ */ new Set([
2776
+ "src",
2777
+ "thumbnail",
2778
+ "backgroundImage",
2779
+ "image",
2780
+ "logo",
2781
+ "favicon",
2782
+ "ogImage",
2783
+ "poster",
2784
+ "avatar",
2785
+ "icon",
2786
+ "cover",
2787
+ "banner",
2788
+ "photo",
2789
+ "heroImage",
2790
+ "profileImage"
2791
+ ]);
2792
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
2793
+ ".jpg",
2794
+ ".jpeg",
2795
+ ".png",
2796
+ ".gif",
2797
+ ".webp",
2798
+ ".svg",
2799
+ ".avif"
2800
+ ]);
2801
+ function isImageValue(value) {
2802
+ if (typeof value !== "string" || value.length === 0) return false;
2803
+ if (isLocalImagePath(value)) return true;
2804
+ if (value.startsWith("http://") || value.startsWith("https://")) {
2805
+ return hasImageExtension(value);
2806
+ }
2807
+ return false;
2808
+ }
2809
+ function isLocalImagePath(value) {
2810
+ if (value.startsWith("./assets/") || value.startsWith("assets/")) {
2811
+ return hasImageExtension(value);
2812
+ }
2813
+ if (value.startsWith("./") && hasImageExtension(value)) {
2814
+ return true;
2815
+ }
2816
+ return false;
2817
+ }
2818
+ function hasImageExtension(value) {
2819
+ const cleanPath = value.split("?")[0]?.split("#")[0] ?? value;
2820
+ const lastDot = cleanPath.lastIndexOf(".");
2821
+ if (lastDot === -1) return false;
2822
+ const ext = cleanPath.substring(lastDot).toLowerCase();
2823
+ return IMAGE_EXTENSIONS.has(ext);
2824
+ }
2825
+ function extractImageReferences(schema) {
2826
+ const references = [];
2827
+ function walk(node, currentPath, parentKey) {
2828
+ if (node === null || node === void 0) return;
2829
+ if (typeof node === "string") {
2830
+ const isKnownImageProp = IMAGE_PROPERTY_NAMES.has(parentKey);
2831
+ if (isKnownImageProp && isImageValue(node)) {
2832
+ references.push({
2833
+ path: currentPath,
2834
+ value: node,
2835
+ isLocal: isLocalImagePath(node)
2836
+ });
2837
+ } else if (!isKnownImageProp && isLocalImagePath(node)) {
2838
+ references.push({
2839
+ path: currentPath,
2840
+ value: node,
2841
+ isLocal: true
2842
+ });
2843
+ }
2844
+ return;
2845
+ }
2846
+ if (Array.isArray(node)) {
2847
+ for (let i = 0; i < node.length; i++) {
2848
+ walk(node[i], `${currentPath}[${i}]`, parentKey);
2849
+ }
2850
+ return;
2851
+ }
2852
+ if (typeof node === "object") {
2853
+ const obj = node;
2854
+ for (const key of Object.keys(obj)) {
2855
+ const childPath = currentPath ? `${currentPath}.${key}` : key;
2856
+ walk(obj[key], childPath, key);
2857
+ }
2858
+ }
2859
+ }
2860
+ walk(schema, "", "");
2861
+ return references;
2862
+ }
2863
+ async function computeFileHashes(localRefs) {
2864
+ const results = [];
2865
+ const seen = /* @__PURE__ */ new Set();
2866
+ for (const ref of localRefs) {
2867
+ if (seen.has(ref.value)) continue;
2868
+ seen.add(ref.value);
2869
+ const absolutePath = resolve8(process.cwd(), ref.value);
2870
+ assertWithinCwd(absolutePath);
2871
+ if (!existsSync6(absolutePath)) {
2872
+ throw new Error(`\uB85C\uCEEC \uC774\uBBF8\uC9C0 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${ref.value}`);
2873
+ }
2874
+ const hash = await computeSha256(absolutePath);
2875
+ results.push({
2876
+ localPath: ref.value,
2877
+ sha256: hash
2878
+ });
2879
+ }
2880
+ return results;
2881
+ }
2882
+ async function computeSha256(filePath) {
2883
+ return new Promise((resolve13, reject) => {
2884
+ const hash = createHash("sha256");
2885
+ const stream = createReadStream(filePath);
2886
+ stream.on("data", (chunk) => hash.update(chunk));
2887
+ stream.on("end", () => resolve13(hash.digest("hex")));
2888
+ stream.on("error", (err) => reject(err));
2889
+ });
2890
+ }
2891
+ function replaceImageUrls(schema, urlMap) {
2892
+ if (urlMap.size === 0) return schema;
2893
+ const cloned = JSON.parse(JSON.stringify(schema));
2894
+ function walk(node) {
2895
+ if (node === null || node === void 0) return;
2896
+ if (Array.isArray(node)) {
2897
+ for (let i = 0; i < node.length; i++) {
2898
+ const item = node[i];
2899
+ if (typeof item === "string" && urlMap.has(item)) {
2900
+ node[i] = urlMap.get(item);
2901
+ } else {
2902
+ walk(item);
2903
+ }
2904
+ }
2905
+ return;
2906
+ }
2907
+ if (typeof node === "object") {
2908
+ const obj = node;
2909
+ for (const key of Object.keys(obj)) {
2910
+ const val = obj[key];
2911
+ if (typeof val === "string" && urlMap.has(val)) {
2912
+ obj[key] = urlMap.get(val);
2913
+ } else {
2914
+ walk(val);
2915
+ }
2916
+ }
2917
+ }
2918
+ }
2919
+ walk(cloned);
2920
+ return cloned;
2921
+ }
2922
+ async function getFileSize(filePath) {
2923
+ const absolutePath = resolve8(process.cwd(), filePath);
2924
+ assertWithinCwd(absolutePath);
2925
+ const fileStat = await stat(absolutePath);
2926
+ return fileStat.size;
2927
+ }
2928
+
2929
+ // src/commands/deploy.ts
2930
+ function createConcurrencyLimiter(concurrency) {
2931
+ let active = 0;
2932
+ const queue = [];
2933
+ function next() {
2934
+ if (queue.length > 0 && active < concurrency) {
2935
+ active++;
2936
+ const resolve13 = queue.shift();
2937
+ resolve13?.();
2938
+ }
2939
+ }
2940
+ return async function limit(fn) {
2941
+ if (active >= concurrency) {
2942
+ await new Promise((resolve13) => {
2943
+ queue.push(resolve13);
2944
+ });
2945
+ } else {
2946
+ active++;
2947
+ }
2948
+ try {
2949
+ return await fn();
2950
+ } finally {
2951
+ active--;
2952
+ next();
2953
+ }
2954
+ };
2955
+ }
2956
+ async function processAssets(client, siteId, schema, dryRun) {
2957
+ const allRefs = extractImageReferences(schema);
2958
+ const localRefs = allRefs.filter((ref) => ref.isLocal);
2959
+ if (localRefs.length === 0) {
2960
+ return schema;
2961
+ }
2962
+ console.log(chalk9.bold("\uC774\uBBF8\uC9C0 \uC5D0\uC14B \uCC98\uB9AC\n"));
2963
+ console.log(chalk9.dim(` \uBC1C\uACAC\uB41C \uC774\uBBF8\uC9C0 \uCC38\uC870: ${allRefs.length}\uAC1C (\uB85C\uCEEC: ${localRefs.length}\uAC1C)`));
2964
+ console.log("");
2965
+ const hashSpinner = spinner("\uD30C\uC77C \uD574\uC2DC \uACC4\uC0B0 \uC911...");
2966
+ let hashes;
2967
+ try {
2968
+ hashes = await computeFileHashes(localRefs);
2969
+ hashSpinner.stop(chalk9.green(`${hashes.length}\uAC1C \uD30C\uC77C\uC758 \uD574\uC2DC \uACC4\uC0B0 \uC644\uB8CC.`));
2970
+ } catch (error) {
2971
+ hashSpinner.stop(
2972
+ chalk9.red(
2973
+ `\uD574\uC2DC \uACC4\uC0B0 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
2974
+ )
2975
+ );
2976
+ process.exit(1);
2977
+ }
2978
+ const checkSpinner = spinner("\uC11C\uBC84\uC5D0\uC11C \uAE30\uC874 \uC5D0\uC14B \uD655\uC778 \uC911...");
2979
+ let existing;
2980
+ let newAssets;
2981
+ try {
2982
+ const checkResult = await client.checkAssets(siteId, hashes);
2983
+ existing = checkResult.existing;
2984
+ newAssets = checkResult.new;
2985
+ checkSpinner.stop("");
2986
+ } catch (error) {
2987
+ if (error instanceof ApiError) {
2988
+ checkSpinner.stop(
2989
+ chalk9.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
2990
+ );
2991
+ } else {
2992
+ checkSpinner.stop(
2993
+ chalk9.red(
2994
+ `\uC5D0\uC14B \uD655\uC778 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
2995
+ )
2996
+ );
2997
+ }
2998
+ process.exit(1);
2999
+ }
3000
+ const fileSizes = /* @__PURE__ */ new Map();
3001
+ for (const hash of hashes) {
3002
+ const size = await getFileSize(hash.localPath);
3003
+ fileSizes.set(hash.localPath, size);
3004
+ }
3005
+ if (dryRun) {
3006
+ printDryRunReport(existing, newAssets, fileSizes);
3007
+ process.exit(0);
3008
+ }
3009
+ for (const asset of existing) {
3010
+ console.log(chalk9.dim(` = ${asset.localPath}`) + chalk9.dim(" (\uC774\uBBF8 \uC5C5\uB85C\uB4DC\uB428)"));
3011
+ }
3012
+ if (newAssets.length > 0) {
3013
+ console.log("");
3014
+ console.log(chalk9.bold(`${newAssets.length}\uAC1C \uC5D0\uC14B \uC5C5\uB85C\uB4DC \uC2DC\uC791...
3015
+ `));
3016
+ const hashMap = /* @__PURE__ */ new Map();
3017
+ for (const h of hashes) {
3018
+ hashMap.set(h.localPath, h.sha256);
3019
+ }
3020
+ const uploadResults = await uploadAssetsWithProgress(
3021
+ client,
3022
+ siteId,
3023
+ newAssets,
3024
+ hashMap,
3025
+ fileSizes
3026
+ );
3027
+ const failures = uploadResults.filter((r) => r.status === "rejected");
3028
+ if (failures.length > 0) {
3029
+ console.error("");
3030
+ console.error(chalk9.red(`${failures.length}\uAC1C \uC5D0\uC14B \uC5C5\uB85C\uB4DC \uC2E4\uD328:`));
3031
+ for (const f of failures) {
3032
+ if (f.status === "rejected") {
3033
+ console.error(chalk9.red(` - ${f.reason}`));
3034
+ }
3035
+ }
3036
+ process.exit(1);
3037
+ }
3038
+ for (const r of uploadResults) {
3039
+ if (r.status === "fulfilled") {
3040
+ existing.push({
3041
+ localPath: r.value.localPath,
3042
+ cdnUrl: r.value.cdnUrl
3043
+ });
3044
+ }
3045
+ }
3046
+ console.log("");
3047
+ console.log(chalk9.green(`\uBAA8\uB4E0 \uC5D0\uC14B \uC5C5\uB85C\uB4DC \uC644\uB8CC!
3048
+ `));
3049
+ }
3050
+ const urlMap = /* @__PURE__ */ new Map();
3051
+ for (const asset of existing) {
3052
+ urlMap.set(asset.localPath, asset.cdnUrl);
3053
+ }
3054
+ return replaceImageUrls(schema, urlMap);
3055
+ }
3056
+ function printDryRunReport(existing, newAssets, fileSizes) {
3057
+ console.log(chalk9.bold("\n[Dry Run] \uC5D0\uC14B \uC5C5\uB85C\uB4DC \uB9AC\uD3EC\uD2B8\n"));
3058
+ for (const localPath of newAssets) {
3059
+ const size = fileSizes.get(localPath) ?? 0;
3060
+ console.log(chalk9.green(` + ${localPath}`) + chalk9.dim(` (${formatBytes(size)}, new)`));
3061
+ }
3062
+ for (const asset of existing) {
3063
+ console.log(chalk9.dim(` = ${asset.localPath} (\uC774\uBBF8 \uC5C5\uB85C\uB4DC\uB428)`));
3064
+ }
3065
+ const totalImages = newAssets.length + existing.length;
3066
+ const totalUploadSize = newAssets.reduce(
3067
+ (acc, path) => acc + (fileSizes.get(path) ?? 0),
3068
+ 0
3069
+ );
3070
+ console.log("");
3071
+ console.log(chalk9.bold(` ${totalImages}\uAC1C \uC774\uBBF8\uC9C0, ${newAssets.length}\uAC1C \uC2E0\uADDC \uC5C5\uB85C\uB4DC \uD544\uC694`));
3072
+ if (newAssets.length > 0) {
3073
+ console.log(chalk9.bold(` \uC608\uC0C1 \uC5C5\uB85C\uB4DC \uD06C\uAE30: ${formatBytes(totalUploadSize)}`));
3074
+ }
3075
+ console.log("");
3076
+ console.log(chalk9.yellow("--dry-run \uD50C\uB798\uADF8\uB97C \uC81C\uAC70\uD558\uBA74 \uC2E4\uC81C \uC5C5\uB85C\uB4DC \uBC0F \uBC30\uD3EC\uAC00 \uC2E4\uD589\uB429\uB2C8\uB2E4."));
3077
+ console.log("");
3078
+ }
3079
+ async function uploadAssetsWithProgress(client, siteId, newAssets, hashMap, fileSizes) {
3080
+ const limit = createConcurrencyLimiter(5);
3081
+ let completed = 0;
3082
+ const total = newAssets.length;
3083
+ const tasks = newAssets.map(
3084
+ (localPath) => limit(async () => {
3085
+ const contentHash = hashMap.get(localPath);
3086
+ if (!contentHash) {
3087
+ throw new Error(`\uD574\uC2DC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${localPath}`);
3088
+ }
3089
+ const result = await client.uploadAsset(siteId, localPath, contentHash, localPath);
3090
+ completed++;
3091
+ const originalSize = fileSizes.get(localPath) ?? 0;
3092
+ formatUploadProgress(completed, total, localPath, originalSize, result.storedSize, result.format);
3093
+ return result;
3094
+ })
3095
+ );
3096
+ return Promise.allSettled(tasks);
3097
+ }
3098
+ async function commandDeploy(options) {
3099
+ const target = options.target ?? "staging";
3100
+ const dryRun = options.dryRun ?? false;
3101
+ const syncTemplate = options.syncTemplate ?? false;
3102
+ if (target !== "staging" && target !== "production") {
3103
+ console.error(chalk9.red(`\uC798\uBABB\uB41C \uBC30\uD3EC \uB300\uC0C1: ${target}`));
3104
+ console.error(chalk9.yellow("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uB300\uC0C1: staging, production"));
3105
+ process.exit(1);
3106
+ }
3107
+ const projectConfig = await loadProjectConfig();
3108
+ if (!projectConfig?.siteId) {
3109
+ console.error(chalk9.red("\nsiteId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
3110
+ console.error(chalk9.yellow("\uB2E4\uC74C \uC911 \uD558\uB098\uB97C \uC2E4\uD589\uD558\uC138\uC694:"));
3111
+ console.error(chalk9.dim(" 1. npx @saeroon/cli init --from-site <siteId> \uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654"));
3112
+ console.error(chalk9.dim(' 2. saeroon.config.json\uC5D0 "siteId" \uD544\uB4DC\uB97C \uC9C1\uC811 \uCD94\uAC00'));
3113
+ console.error("");
3114
+ process.exit(1);
3115
+ }
3116
+ const siteId = projectConfig.siteId;
3117
+ const schemaFile = projectConfig.schemaFile ?? "schema.json";
3118
+ const apiKey = await resolveApiKey(options.apiKey);
3119
+ const apiBaseUrl = await getApiBaseUrl();
3120
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
3121
+ console.log(chalk9.bold(`
3122
+ Saeroon Deploy \u2192 ${target}${dryRun ? " (dry-run)" : ""}
3123
+ `));
3124
+ console.log(chalk9.dim(` \uC0AC\uC774\uD2B8 ID: ${siteId}`));
3125
+ console.log(chalk9.dim(` \uC2A4\uD0A4\uB9C8: ${schemaFile}`));
3126
+ console.log(chalk9.dim(` \uB300\uC0C1: ${target}`));
3127
+ if (dryRun) {
3128
+ console.log(chalk9.dim(` \uBAA8\uB4DC: dry-run (\uC2E4\uC81C \uC5C5\uB85C\uB4DC/\uBC30\uD3EC \uC5C6\uC74C)`));
3129
+ }
3130
+ console.log("");
3131
+ const schema = await loadSchemaFile(schemaFile);
3132
+ const processedSchema = await processAssets(client, siteId, schema, dryRun);
3133
+ const validateSpinner = spinner("\uC2A4\uD0A4\uB9C8 \uC720\uD6A8\uC131 \uAC80\uC0AC \uC911...");
3134
+ let validationResult;
3135
+ try {
3136
+ validationResult = await client.validateSchema(processedSchema);
3137
+ validateSpinner.stop("");
3138
+ } catch (error) {
3139
+ if (error instanceof ApiError) {
3140
+ validateSpinner.stop(
3141
+ chalk9.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
3142
+ );
3143
+ } else {
3144
+ validateSpinner.stop(
3145
+ chalk9.red(
3146
+ `\uC720\uD6A8\uC131 \uAC80\uC0AC \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
3147
+ )
3148
+ );
3149
+ }
3150
+ process.exit(1);
3151
+ }
3152
+ const allIssues = [
3153
+ ...validationResult.validation.errors,
3154
+ ...validationResult.validation.warnings
3155
+ ];
3156
+ if (allIssues.length > 0) {
3157
+ formatValidationErrors(allIssues);
3158
+ }
3159
+ formatQualityReport(validationResult.quality);
3160
+ if (!validationResult.validation.passed) {
3161
+ console.error(chalk9.red("\n\uC720\uD6A8\uC131 \uAC80\uC0AC \uC2E4\uD328. \uBC30\uD3EC\uAC00 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
3162
+ console.error(chalk9.yellow("\uC5D0\uB7EC\uB97C \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.\n"));
3163
+ process.exit(1);
3164
+ }
3165
+ if (target === "staging") {
3166
+ await deployToStaging(client, siteId, processedSchema, syncTemplate, projectConfig.templateId);
3167
+ } else {
3168
+ await deployToProduction(client, siteId, processedSchema, syncTemplate, projectConfig.templateId);
3169
+ }
3170
+ }
3171
+ async function deployToStaging(client, siteId, schema, _syncTemplate, templateId) {
3172
+ const deploySpinner = spinner("Staging\uC5D0 \uC2A4\uD0A4\uB9C8 \uBC30\uD3EC \uC911...");
3173
+ try {
3174
+ await client.updateSiteSchema(siteId, schema);
3175
+ deploySpinner.stop(chalk9.green("Staging \uBC30\uD3EC \uC644\uB8CC!"));
3176
+ console.log("");
3177
+ console.log(chalk9.cyan(`Staging URL: https://${siteId}.staging.hosting.saeroon.com`));
3178
+ console.log(chalk9.dim(" \u2192 Draft \uC2A4\uD0A4\uB9C8\uAC00 \uC5C5\uB370\uC774\uD2B8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC2A4\uD14C\uC774\uC9D5\uC5D0\uC11C \uD655\uC778\uD558\uC138\uC694."));
3179
+ console.log("");
3180
+ console.log(chalk9.dim("\uD504\uB85C\uB355\uC158 \uBC30\uD3EC: npx @saeroon/cli deploy --target production"));
3181
+ if (templateId) {
3182
+ console.log(chalk9.dim("\uD15C\uD50C\uB9BF \uB3D9\uAE30\uD654: npx @saeroon/cli deploy --target production --sync-template"));
3183
+ }
3184
+ console.log("");
3185
+ } catch (error) {
3186
+ if (error instanceof ApiError) {
3187
+ deploySpinner.stop(
3188
+ chalk9.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
3189
+ );
3190
+ } else {
3191
+ deploySpinner.stop(
3192
+ chalk9.red(
3193
+ `Staging \uBC30\uD3EC \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
3194
+ )
3195
+ );
3196
+ }
3197
+ process.exit(1);
3198
+ }
3199
+ }
3200
+ async function deployToProduction(client, siteId, schema, syncTemplate, templateId) {
3201
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
3202
+ try {
3203
+ const answer = await rl.question(
3204
+ chalk9.yellow("\uC815\uB9D0 \uD504\uB85C\uB355\uC158\uC5D0 \uBC30\uD3EC\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? (y/N): ")
3205
+ );
3206
+ if (answer.trim().toLowerCase() !== "y") {
3207
+ console.log(chalk9.dim("\n\uBC30\uD3EC\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
3208
+ return;
3209
+ }
3210
+ } finally {
3211
+ rl.close();
3212
+ }
3213
+ console.log("");
3214
+ const updateSpinner = spinner("\uC2A4\uD0A4\uB9C8 \uC5C5\uB370\uC774\uD2B8 \uC911...");
3215
+ try {
3216
+ await client.updateSiteSchema(siteId, schema);
3217
+ updateSpinner.stop(chalk9.green("\uC2A4\uD0A4\uB9C8 \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC."));
3218
+ } catch (error) {
3219
+ if (error instanceof ApiError) {
3220
+ updateSpinner.stop(
3221
+ chalk9.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
3222
+ );
3223
+ } else {
3224
+ updateSpinner.stop(
3225
+ chalk9.red(
3226
+ `\uC2A4\uD0A4\uB9C8 \uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
3227
+ )
3228
+ );
3229
+ }
3230
+ process.exit(1);
3231
+ }
3232
+ const publishSpinner = spinner("\uD504\uB85C\uB355\uC158\uC5D0 \uBC1C\uD589 \uC911...");
3233
+ try {
3234
+ const result = await client.publishSite(siteId);
3235
+ publishSpinner.stop(chalk9.green("\uD504\uB85C\uB355\uC158 \uBC30\uD3EC \uC644\uB8CC!"));
3236
+ console.log("");
3237
+ console.log(chalk9.cyan(`Live URL: ${result.url}`));
3238
+ console.log(chalk9.dim(` \uBC1C\uD589 \uC2DC\uAC01: ${result.publishedAt}`));
3239
+ console.log("");
3240
+ } catch (error) {
3241
+ if (error instanceof ApiError) {
3242
+ publishSpinner.stop(
3243
+ chalk9.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
3244
+ );
3245
+ } else {
3246
+ publishSpinner.stop(
3247
+ chalk9.red(
3248
+ `\uD504\uB85C\uB355\uC158 \uBC1C\uD589 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
3249
+ )
3250
+ );
3251
+ }
3252
+ process.exit(1);
3253
+ }
3254
+ if (syncTemplate && templateId) {
3255
+ await syncTemplateAfterDeploy(client, templateId);
3256
+ } else if (!syncTemplate && templateId) {
3257
+ console.log(chalk9.dim("Tip: --sync-template \uD50C\uB798\uADF8\uB85C \uB9C8\uCF13\uD50C\uB808\uC774\uC2A4 \uD15C\uD50C\uB9BF\uB3C4 \uD568\uAED8 \uB3D9\uAE30\uD654\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."));
3258
+ console.log("");
3259
+ }
3260
+ }
3261
+ async function syncTemplateAfterDeploy(client, templateId) {
3262
+ const syncSpinner = spinner("\uB9C8\uCF13\uD50C\uB808\uC774\uC2A4 \uD15C\uD50C\uB9BF \uB3D9\uAE30\uD654 \uC911...");
3263
+ try {
3264
+ const result = await client.syncTemplateVersion(templateId);
3265
+ syncSpinner.stop(chalk9.green(`\uD15C\uD50C\uB9BF v${result.version} \uB3D9\uAE30\uD654 \uC644\uB8CC!`));
3266
+ console.log(chalk9.dim(` \uD15C\uD50C\uB9BF: ${result.name}`));
3267
+ console.log("");
3268
+ } catch (error) {
3269
+ if (error instanceof ApiError) {
3270
+ syncSpinner.stop(
3271
+ chalk9.yellow(`\uD15C\uD50C\uB9BF \uB3D9\uAE30\uD654 \uC2E4\uD328 (${error.statusCode}): ${error.message}`)
3272
+ );
3273
+ } else {
3274
+ syncSpinner.stop(
3275
+ chalk9.yellow(
3276
+ `\uD15C\uD50C\uB9BF \uB3D9\uAE30\uD654 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
3277
+ )
3278
+ );
3279
+ }
3280
+ console.log(chalk9.dim(" \uC218\uB3D9 \uB3D9\uAE30\uD654: saeroon template sync"));
3281
+ console.log("");
3282
+ }
3283
+ }
3284
+
3285
+ // src/commands/publish.ts
3286
+ import chalk10 from "chalk";
3287
+ import { createInterface as createInterface3 } from "readline/promises";
3288
+ import { stdin as stdin3, stdout as stdout3 } from "process";
3289
+ var TEMPLATE_CATEGORIES = [
3290
+ "Business",
3291
+ "Portfolio",
3292
+ "Blog",
3293
+ "E-Commerce",
3294
+ "Landing Page",
3295
+ "Restaurant",
3296
+ "Real Estate",
3297
+ "Agency",
3298
+ "Personal",
3299
+ "Education",
3300
+ "Healthcare",
3301
+ "Event",
3302
+ "Other"
3303
+ ];
3304
+ async function commandPublish(schemaPath, options) {
3305
+ console.log("");
3306
+ console.log(chalk10.yellow("\u26A0 `publish` \uBA85\uB839\uC740 \uD5A5\uD6C4 \uBC84\uC804\uC5D0\uC11C \uC81C\uAC70\uB429\uB2C8\uB2E4."));
3307
+ console.log(chalk10.yellow(" \uC18C\uC2A4 \uC0AC\uC774\uD2B8 \uAE30\uBC18 \uD15C\uD50C\uB9BF \uAD00\uB9AC\uB85C \uC804\uD658\uD558\uC138\uC694:"));
3308
+ console.log("");
3309
+ console.log(chalk10.dim(" \uD15C\uD50C\uB9BF \uB4F1\uB85D: saeroon template register"));
3310
+ console.log(chalk10.dim(" \uBC84\uC804 \uB3D9\uAE30\uD654: saeroon template sync"));
3311
+ console.log(chalk10.dim(" \uC0C1\uD0DC \uD655\uC778: saeroon template status"));
3312
+ console.log(chalk10.dim(" \uBC30\uD3EC+\uB3D9\uAE30\uD654: saeroon deploy --target production --sync-template"));
3313
+ console.log("");
3314
+ const resolvedPath = resolveSchemaPath(schemaPath);
3315
+ const displayPath = schemaPath ?? "schema.json";
3316
+ const apiKey = await resolveApiKey(options.apiKey);
3317
+ const apiBaseUrl = await getApiBaseUrl();
3318
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
3319
+ console.log(chalk10.bold("\uB9C8\uCF13\uD50C\uB808\uC774\uC2A4 \uD15C\uD50C\uB9BF \uB4F1\uB85D (\uB808\uAC70\uC2DC)\n"));
3320
+ console.log(chalk10.dim(` \uD30C\uC77C: ${displayPath}`));
3321
+ console.log("");
3322
+ const schema = await loadSchemaFile(displayPath);
3323
+ const validateSpinner = spinner("\uC2A4\uD0A4\uB9C8 \uC720\uD6A8\uC131 \uAC80\uC0AC \uC911...");
3324
+ let validationResult;
3325
+ try {
3326
+ validationResult = await client.validateSchema(schema);
3327
+ validateSpinner.stop("");
3328
+ } catch (error) {
3329
+ if (error instanceof ApiError) {
3330
+ validateSpinner.stop(
3331
+ chalk10.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
3332
+ );
3333
+ } else {
3334
+ validateSpinner.stop(
3335
+ chalk10.red(
3336
+ `\uC720\uD6A8\uC131 \uAC80\uC0AC \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
3337
+ )
3338
+ );
3339
+ }
3340
+ process.exit(1);
3341
+ }
3342
+ const allIssues = [
3343
+ ...validationResult.validation.errors,
3344
+ ...validationResult.validation.warnings
3345
+ ];
3346
+ if (allIssues.length > 0) {
3347
+ formatValidationErrors(allIssues);
3348
+ }
3349
+ formatQualityReport(validationResult.quality);
3350
+ if (!validationResult.validation.passed) {
3351
+ console.error(chalk10.red("\n\uC720\uD6A8\uC131 \uAC80\uC0AC \uC2E4\uD328. \uD15C\uD50C\uB9BF\uC744 \uB4F1\uB85D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
3352
+ console.error(chalk10.yellow("\uC5D0\uB7EC\uB97C \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.\n"));
3353
+ process.exit(1);
3354
+ }
3355
+ console.log(chalk10.green("\uC720\uD6A8\uC131 \uAC80\uC0AC \uD1B5\uACFC!\n"));
3356
+ const rl = createInterface3({ input: stdin3, output: stdout3 });
3357
+ let metadata;
3358
+ try {
3359
+ metadata = await collectMetadata(rl);
3360
+ } finally {
3361
+ rl.close();
3362
+ }
3363
+ console.log("");
3364
+ const submitSpinner = spinner("\uD15C\uD50C\uB9BF\uC744 \uB9C8\uCF13\uD50C\uB808\uC774\uC2A4\uC5D0 \uC81C\uCD9C \uC911...");
3365
+ try {
3366
+ const result = await client.submitTemplate(schema, metadata);
3367
+ submitSpinner.stop(chalk10.green("\uD15C\uD50C\uB9BF \uC81C\uCD9C \uC644\uB8CC!"));
3368
+ console.log("");
3369
+ console.log(chalk10.bold("\uC81C\uCD9C \uC815\uBCF4:"));
3370
+ console.log(` Template ID: ${chalk10.cyan(result.templateId)}`);
3371
+ console.log(` \uC0C1\uD0DC: ${formatStatus(result.status)}`);
3372
+ console.log(` \uC81C\uCD9C \uC2DC\uAC01: ${chalk10.dim(result.submittedAt)}`);
3373
+ if (result.reviewUrl) {
3374
+ console.log("");
3375
+ console.log(` \uC2EC\uC0AC \uD398\uC774\uC9C0: ${chalk10.cyan(result.reviewUrl)}`);
3376
+ }
3377
+ console.log("");
3378
+ console.log(chalk10.dim("\uC2EC\uC0AC \uC0C1\uD0DC \uD655\uC778: npx @saeroon/cli blocks (\uD5A5\uD6C4 \uC9C0\uC6D0 \uC608\uC815)"));
3379
+ console.log("");
3380
+ } catch (error) {
3381
+ if (error instanceof ApiError) {
3382
+ submitSpinner.stop(
3383
+ chalk10.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
3384
+ );
3385
+ } else {
3386
+ submitSpinner.stop(
3387
+ chalk10.red(
3388
+ `\uD15C\uD50C\uB9BF \uC81C\uCD9C \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
3389
+ )
3390
+ );
3391
+ }
3392
+ process.exit(1);
3393
+ }
3394
+ }
3395
+ async function collectMetadata(rl) {
3396
+ console.log(chalk10.bold("\uD15C\uD50C\uB9BF \uBA54\uD0C0\uB370\uC774\uD130 \uC785\uB825:\n"));
3397
+ let name = "";
3398
+ while (!name) {
3399
+ name = (await rl.question(" \uC774\uB984: ")).trim();
3400
+ if (!name) {
3401
+ console.log(chalk10.red(" \uC774\uB984\uC740 \uD544\uC218\uC785\uB2C8\uB2E4."));
3402
+ }
3403
+ }
3404
+ let description = "";
3405
+ while (!description) {
3406
+ description = (await rl.question(" \uC124\uBA85: ")).trim();
3407
+ if (!description) {
3408
+ console.log(chalk10.red(" \uC124\uBA85\uC740 \uD544\uC218\uC785\uB2C8\uB2E4."));
3409
+ }
3410
+ }
3411
+ console.log("");
3412
+ console.log(chalk10.dim(" \uCE74\uD14C\uACE0\uB9AC \uBAA9\uB85D:"));
3413
+ TEMPLATE_CATEGORIES.forEach((cat, i) => {
3414
+ console.log(chalk10.dim(` ${i + 1}. ${cat}`));
3415
+ });
3416
+ console.log("");
3417
+ let category = "";
3418
+ while (!category) {
3419
+ const categoryInput = (await rl.question(" \uCE74\uD14C\uACE0\uB9AC (\uBC88\uD638 \uB610\uB294 \uC774\uB984): ")).trim();
3420
+ const categoryIndex = parseInt(categoryInput, 10);
3421
+ if (!isNaN(categoryIndex) && categoryIndex >= 1 && categoryIndex <= TEMPLATE_CATEGORIES.length) {
3422
+ category = TEMPLATE_CATEGORIES[categoryIndex - 1];
3423
+ } else {
3424
+ const matched = TEMPLATE_CATEGORIES.find(
3425
+ (c) => c.toLowerCase() === categoryInput.toLowerCase()
3426
+ );
3427
+ if (matched) {
3428
+ category = matched;
3429
+ } else if (categoryInput) {
3430
+ category = categoryInput;
3431
+ } else {
3432
+ console.log(chalk10.red(" \uCE74\uD14C\uACE0\uB9AC\uB97C \uC120\uD0DD\uD574\uC8FC\uC138\uC694."));
3433
+ }
3434
+ }
3435
+ }
3436
+ const tagsInput = (await rl.question(" \uD0DC\uADF8 (\uCF64\uB9C8 \uAD6C\uBD84, \uC120\uD0DD): ")).trim();
3437
+ const tags = tagsInput ? tagsInput.split(",").map((t) => t.trim()).filter(Boolean) : void 0;
3438
+ console.log("");
3439
+ console.log(chalk10.dim(" \uC785\uB825 \uD655\uC778:"));
3440
+ console.log(chalk10.dim(` \uC774\uB984: ${name}`));
3441
+ console.log(chalk10.dim(` \uC124\uBA85: ${description}`));
3442
+ console.log(chalk10.dim(` \uCE74\uD14C\uACE0\uB9AC: ${category}`));
3443
+ if (tags && tags.length > 0) {
3444
+ console.log(chalk10.dim(` \uD0DC\uADF8: ${tags.join(", ")}`));
3445
+ }
3446
+ return {
3447
+ name,
3448
+ description,
3449
+ category,
3450
+ tags
3451
+ };
3452
+ }
3453
+ function formatStatus(status) {
3454
+ switch (status) {
3455
+ case "pending":
3456
+ return chalk10.yellow("\uB300\uAE30 \uC911 (pending)");
3457
+ case "reviewing":
3458
+ return chalk10.blue("\uC2EC\uC0AC \uC911 (reviewing)");
3459
+ case "approved":
3460
+ return chalk10.green("\uC2B9\uC778\uB428 (approved)");
3461
+ case "rejected":
3462
+ return chalk10.red("\uAC70\uC808\uB428 (rejected)");
3463
+ }
3464
+ }
3465
+
3466
+ // src/commands/blocks.ts
3467
+ import chalk11 from "chalk";
3468
+ async function commandBlocks(blockType) {
3469
+ const apiBaseUrl = await getApiBaseUrl();
3470
+ const client = new SaeroonApiClient("", apiBaseUrl);
3471
+ if (blockType) {
3472
+ await showBlockDetail(client, blockType);
3473
+ } else {
3474
+ await showBlockCatalog(client);
3475
+ }
3476
+ }
3477
+ async function showBlockCatalog(client) {
3478
+ const fetchSpinner = spinner("\uBE14\uB85D \uCE74\uD0C8\uB85C\uADF8\uB97C \uAC00\uC838\uC624\uB294 \uC911...");
3479
+ try {
3480
+ const catalog = await client.getBlockCatalog();
3481
+ fetchSpinner.stop(chalk11.green(`${catalog.totalCount}\uAC1C\uC758 \uBE14\uB85D\uC744 \uC870\uD68C\uD588\uC2B5\uB2C8\uB2E4.`));
3482
+ formatBlockTable(catalog.blocks);
3483
+ console.log(
3484
+ chalk11.dim("\uD2B9\uC815 \uBE14\uB85D\uC758 \uC0C1\uC138 \uC815\uBCF4: npx @saeroon/cli blocks <block-type>\n")
3485
+ );
3486
+ } catch (error) {
3487
+ fetchSpinner.stop(
3488
+ chalk11.red(
3489
+ `\uBE14\uB85D \uCE74\uD0C8\uB85C\uADF8 \uC870\uD68C \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
3490
+ )
3491
+ );
3492
+ process.exit(1);
3493
+ }
3494
+ }
3495
+ async function showBlockDetail(client, blockType) {
3496
+ const fetchSpinner = spinner(`${blockType} \uBE14\uB85D \uC815\uBCF4\uB97C \uAC00\uC838\uC624\uB294 \uC911...`);
3497
+ try {
3498
+ const detail = await client.getBlockDetail(blockType);
3499
+ fetchSpinner.stop(chalk11.green(`${blockType} \uBE14\uB85D \uC815\uBCF4\uB97C \uC870\uD68C\uD588\uC2B5\uB2C8\uB2E4.`));
3500
+ formatBlockDetail(detail);
3501
+ } catch (error) {
3502
+ fetchSpinner.stop(
3503
+ chalk11.red(
3504
+ `\uBE14\uB85D \uC0C1\uC138 \uC870\uD68C \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
3505
+ )
3506
+ );
3507
+ process.exit(1);
3508
+ }
3509
+ }
3510
+
3511
+ // src/commands/add.ts
3512
+ import chalk12 from "chalk";
3513
+ import { writeFile as writeFile4 } from "fs/promises";
3514
+ import { resolve as resolve9 } from "path";
3515
+ import { createInterface as createInterface4 } from "readline/promises";
3516
+ import { stdin as stdin4, stdout as stdout4 } from "process";
3517
+ var CONTAINER_TYPES = [
3518
+ "container",
3519
+ "header-block",
3520
+ "footer-block",
3521
+ "nav-block",
3522
+ "main-block",
3523
+ "aside-block",
3524
+ "article-block",
3525
+ "member-only-section"
3526
+ ];
3527
+ async function commandAdd(blockType, options) {
3528
+ console.log(chalk12.bold(`
3529
+ \uBE14\uB85D \uCD94\uAC00: ${blockType}
3530
+ `));
3531
+ const fetchSpinner = spinner(`${blockType} \uBE14\uB85D \uC815\uBCF4 \uC870\uD68C \uC911...`);
3532
+ let defaultProps = {};
3533
+ let canHaveChildren = false;
3534
+ try {
3535
+ const client = new SaeroonApiClient("", "");
3536
+ const detail = await client.getBlockDetail(blockType);
3537
+ for (const prop of detail.props) {
3538
+ if (prop.default !== void 0) {
3539
+ defaultProps[prop.name] = prop.default;
3540
+ }
3541
+ }
3542
+ canHaveChildren = CONTAINER_TYPES.includes(blockType);
3543
+ fetchSpinner.stop(chalk12.green(` ${blockType} \uC815\uBCF4 \uB85C\uB4DC \uC644\uB8CC`));
3544
+ } catch {
3545
+ fetchSpinner.stop(chalk12.yellow(` ${blockType} \uCE74\uD0C8\uB85C\uADF8 \uC870\uD68C \uC2E4\uD328 \u2014 \uBE48 props\uB85C \uC9C4\uD589`));
3546
+ }
3547
+ const schemaPath = resolveSchemaPath();
3548
+ const schema = await loadSchemaFile(schemaPath);
3549
+ const pages = schema.pages;
3550
+ if (!pages || !Array.isArray(pages) || pages.length === 0) {
3551
+ console.error(chalk12.red("schema.json\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
3552
+ process.exit(1);
3553
+ }
3554
+ let targetPage;
3555
+ if (options.page) {
3556
+ const found = pages.find((p) => p.id === options.page);
3557
+ if (!found) {
3558
+ console.error(chalk12.red(`\uD398\uC774\uC9C0 "${options.page}"\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`));
3559
+ console.error(chalk12.dim(`\uC0AC\uC6A9 \uAC00\uB2A5: ${pages.map((p) => p.id).join(", ")}`));
3560
+ process.exit(1);
3561
+ }
3562
+ targetPage = found;
3563
+ } else if (pages.length === 1) {
3564
+ targetPage = pages[0];
3565
+ } else {
3566
+ console.log(chalk12.bold("\uD398\uC774\uC9C0 \uC120\uD0DD:"));
3567
+ pages.forEach((p, i) => {
3568
+ console.log(chalk12.dim(` ${i + 1}. ${p.id} (${p.path ?? p.slug ?? ""})`));
3569
+ });
3570
+ const rl = createInterface4({ input: stdin4, output: stdout4 });
3571
+ const answer = await rl.question(chalk12.cyan(" \uBC88\uD638 \uC785\uB825: "));
3572
+ rl.close();
3573
+ const index = parseInt(answer, 10) - 1;
3574
+ if (isNaN(index) || index < 0 || index >= pages.length) {
3575
+ console.error(chalk12.red("\uC798\uBABB\uB41C \uC120\uD0DD\uC785\uB2C8\uB2E4."));
3576
+ process.exit(1);
3577
+ }
3578
+ targetPage = pages[index];
3579
+ }
3580
+ const blocks = targetPage.blocks;
3581
+ if (!blocks || typeof blocks !== "object") {
3582
+ console.error(chalk12.red(`\uD398\uC774\uC9C0 "${targetPage.id}"\uC5D0 blocks \uB9F5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`));
3583
+ process.exit(1);
3584
+ }
3585
+ const blockId = options.id ?? `${blockType}-${randomId()}`;
3586
+ if (blocks[blockId]) {
3587
+ console.error(chalk12.red(`\uBE14\uB85D ID "${blockId}"\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4.`));
3588
+ process.exit(1);
3589
+ }
3590
+ let parentId;
3591
+ if (options.parent) {
3592
+ if (!blocks[options.parent]) {
3593
+ console.error(chalk12.red(`\uBD80\uBAA8 \uBE14\uB85D "${options.parent}"\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`));
3594
+ process.exit(1);
3595
+ }
3596
+ parentId = options.parent;
3597
+ } else {
3598
+ const containers = Object.entries(blocks).filter(([, b]) => {
3599
+ const type = b.type;
3600
+ return type === "container" || type === "main-block" || type === "article-block" || type === "aside-block" || type === "header-block" || type === "footer-block" || type === "nav-block" || type === "member-only-section" || Array.isArray(b.children);
3601
+ });
3602
+ if (containers.length === 0) {
3603
+ console.error(chalk12.red("\uCEE8\uD14C\uC774\uB108 \uBE14\uB85D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. --parent \uC635\uC158\uC744 \uC9C0\uC815\uD558\uC138\uC694."));
3604
+ process.exit(1);
3605
+ }
3606
+ if (containers.length === 1) {
3607
+ parentId = containers[0][0];
3608
+ } else {
3609
+ console.log(chalk12.bold("\n\uBD80\uBAA8 \uBE14\uB85D \uC120\uD0DD:"));
3610
+ containers.forEach(([id, b], i) => {
3611
+ const childCount = Array.isArray(b.children) ? b.children.length : 0;
3612
+ console.log(chalk12.dim(` ${i + 1}. ${id} (${b.type}, children: ${childCount})`));
3613
+ });
3614
+ const rl = createInterface4({ input: stdin4, output: stdout4 });
3615
+ const answer = await rl.question(chalk12.cyan(" \uBC88\uD638 \uC785\uB825: "));
3616
+ rl.close();
3617
+ const index = parseInt(answer, 10) - 1;
3618
+ if (isNaN(index) || index < 0 || index >= containers.length) {
3619
+ console.error(chalk12.red("\uC798\uBABB\uB41C \uC120\uD0DD\uC785\uB2C8\uB2E4."));
3620
+ process.exit(1);
3621
+ }
3622
+ parentId = containers[index][0];
3623
+ }
3624
+ }
3625
+ const newBlock = {
3626
+ type: blockType,
3627
+ props: defaultProps
3628
+ };
3629
+ if (canHaveChildren) {
3630
+ newBlock.children = [];
3631
+ }
3632
+ blocks[blockId] = newBlock;
3633
+ const parent = blocks[parentId];
3634
+ if (!Array.isArray(parent.children)) {
3635
+ parent.children = [];
3636
+ }
3637
+ const children = parent.children;
3638
+ if (options.after) {
3639
+ const afterIndex = children.indexOf(options.after);
3640
+ if (afterIndex === -1) {
3641
+ console.error(chalk12.yellow(` --after "${options.after}" \uB97C \uCC3E\uC744 \uC218 \uC5C6\uC5B4 \uB05D\uC5D0 \uCD94\uAC00\uD569\uB2C8\uB2E4.`));
3642
+ children.push(blockId);
3643
+ } else {
3644
+ children.splice(afterIndex + 1, 0, blockId);
3645
+ }
3646
+ } else {
3647
+ children.push(blockId);
3648
+ }
3649
+ await writeFile4(
3650
+ resolve9(process.cwd(), "schema.json"),
3651
+ JSON.stringify(schema, null, 2) + "\n",
3652
+ "utf-8"
3653
+ );
3654
+ console.log("");
3655
+ console.log(chalk12.green.bold("\uBE14\uB85D \uCD94\uAC00 \uC644\uB8CC!"));
3656
+ console.log(chalk12.dim(` ID: ${blockId}`));
3657
+ console.log(chalk12.dim(` Type: ${blockType}`));
3658
+ console.log(chalk12.dim(` Parent: ${parentId}`));
3659
+ console.log(chalk12.dim(` Page: ${targetPage.id}`));
3660
+ if (options.after) {
3661
+ console.log(chalk12.dim(` After: ${options.after}`));
3662
+ }
3663
+ console.log("");
3664
+ console.log(chalk12.dim(" \uB2E4\uC74C \uB2E8\uACC4:"));
3665
+ console.log(chalk12.dim(" 1. schema.json\uC5D0\uC11C \uBE14\uB85D props\uB97C \uD3B8\uC9D1\uD558\uC138\uC694"));
3666
+ console.log(chalk12.dim(" 2. npx @saeroon/cli preview \uB85C \uD655\uC778"));
3667
+ console.log("");
3668
+ }
3669
+ function randomId() {
3670
+ return Math.random().toString(36).slice(2, 10);
3671
+ }
3672
+
3673
+ // src/commands/upload.ts
3674
+ import chalk13 from "chalk";
3675
+ import { readFile as readFile8, writeFile as writeFile5, readdir, stat as stat2 } from "fs/promises";
3676
+ import { resolve as resolve10, extname, relative } from "path";
3677
+ import { existsSync as existsSync7 } from "fs";
3678
+ var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".avif"]);
3679
+ async function commandUpload(filePath, options) {
3680
+ console.log(chalk13.bold("\n\uC5D0\uC14B \uC5C5\uB85C\uB4DC\n"));
3681
+ const config = await loadProjectConfig();
3682
+ const siteId = options.siteId ?? config?.siteId;
3683
+ if (!siteId) {
3684
+ console.error(chalk13.red("siteId\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."));
3685
+ console.error(chalk13.dim(" saeroon.config.json\uC5D0 siteId\uB97C \uC124\uC815\uD558\uAC70\uB098 --site-id \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uC138\uC694."));
3686
+ console.error(chalk13.dim(" npx @saeroon/cli init --from-site <siteId> \uB85C \uCD08\uAE30\uD654\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."));
3687
+ process.exit(1);
3688
+ }
3689
+ const apiKey = await resolveApiKey(options.apiKey);
3690
+ const apiBaseUrl = await getApiBaseUrl();
3691
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
3692
+ const absolutePath = resolve10(process.cwd(), filePath);
3693
+ if (!existsSync7(absolutePath)) {
3694
+ console.error(chalk13.red(`\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}`));
3695
+ process.exit(1);
3696
+ }
3697
+ const fileStat = await stat2(absolutePath);
3698
+ let localPaths;
3699
+ if (fileStat.isDirectory()) {
3700
+ localPaths = await collectImageFiles(absolutePath, process.cwd());
3701
+ if (localPaths.length === 0) {
3702
+ console.log(chalk13.yellow("\uC5C5\uB85C\uB4DC\uD560 \uC774\uBBF8\uC9C0 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
3703
+ process.exit(0);
3704
+ }
3705
+ console.log(chalk13.dim(` \uBC1C\uACAC\uB41C \uC774\uBBF8\uC9C0: ${localPaths.length}\uAC1C`));
3706
+ } else {
3707
+ localPaths = [relative(process.cwd(), absolutePath)];
3708
+ }
3709
+ const refs = localPaths.map((p) => ({
3710
+ path: "",
3711
+ value: p,
3712
+ isLocal: true
3713
+ }));
3714
+ const hashSpinner = spinner("\uD574\uC2DC \uACC4\uC0B0 \uC911...");
3715
+ const hashes = await computeFileHashes(refs);
3716
+ hashSpinner.stop(chalk13.green(` ${hashes.length}\uAC1C \uD30C\uC77C \uD574\uC2DC \uC644\uB8CC`));
3717
+ const checkSpinner = spinner("\uC11C\uBC84 \uD655\uC778 \uC911...");
3718
+ let existingUrls;
3719
+ try {
3720
+ const checkResult = await client.checkAssets(siteId, hashes.map((h) => ({ localPath: h.localPath, sha256: h.sha256 })));
3721
+ existingUrls = new Map(
3722
+ (checkResult.existing ?? []).map((item) => [item.localPath, item.cdnUrl])
3723
+ );
3724
+ checkSpinner.stop(chalk13.green(` \uC774\uBBF8 \uC5C5\uB85C\uB4DC\uB428: ${existingUrls.size}\uAC1C, \uC2E0\uADDC: ${hashes.length - existingUrls.size}\uAC1C`));
3725
+ } catch {
3726
+ existingUrls = /* @__PURE__ */ new Map();
3727
+ checkSpinner.stop(chalk13.yellow(" \uC11C\uBC84 \uD655\uC778 \uC2E4\uD328 \u2014 \uC804\uCCB4 \uC5C5\uB85C\uB4DC \uC9C4\uD589"));
3728
+ }
3729
+ const urlMap = /* @__PURE__ */ new Map();
3730
+ let uploadCount = 0;
3731
+ for (const asset of hashes) {
3732
+ const existing = existingUrls.get(asset.localPath);
3733
+ if (existing) {
3734
+ urlMap.set(asset.localPath, existing);
3735
+ continue;
3736
+ }
3737
+ const uploadSpinner = spinner(`\uC5C5\uB85C\uB4DC: ${asset.localPath}`);
3738
+ try {
3739
+ const result = await client.uploadAsset(siteId, asset.localPath, asset.sha256, asset.localPath);
3740
+ urlMap.set(asset.localPath, result.cdnUrl);
3741
+ uploadSpinner.stop(chalk13.green(` ${asset.localPath} \u2192 ${result.cdnUrl}`));
3742
+ uploadCount++;
3743
+ } catch (error) {
3744
+ uploadSpinner.stop(chalk13.red(` ${asset.localPath} \uC5C5\uB85C\uB4DC \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`));
3745
+ }
3746
+ }
3747
+ if (options.replaceIn && urlMap.size > 0) {
3748
+ const targetPath = resolve10(process.cwd(), options.replaceIn);
3749
+ if (!existsSync7(targetPath)) {
3750
+ console.error(chalk13.red(`\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${options.replaceIn}`));
3751
+ } else {
3752
+ const content = await readFile8(targetPath, "utf-8");
3753
+ let replaced = content;
3754
+ for (const [localPath, cdnUrl] of urlMap) {
3755
+ const escaped = localPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3756
+ const withDot = localPath.startsWith("./") ? localPath : `./${localPath}`;
3757
+ const withoutDot = localPath.startsWith("./") ? localPath.slice(2) : localPath;
3758
+ replaced = replaced.replaceAll(withDot, cdnUrl).replaceAll(withoutDot, cdnUrl);
3759
+ }
3760
+ await writeFile5(targetPath, replaced, "utf-8");
3761
+ console.log(chalk13.green(`
3762
+ ${options.replaceIn} \uB0B4 \uACBD\uB85C \uAD50\uCCB4 \uC644\uB8CC`));
3763
+ }
3764
+ }
3765
+ console.log("");
3766
+ console.log(chalk13.bold("\uC5C5\uB85C\uB4DC \uACB0\uACFC:"));
3767
+ console.log(chalk13.dim(` \uCD1D \uD30C\uC77C: ${hashes.length}`));
3768
+ console.log(chalk13.dim(` \uC2E0\uADDC \uC5C5\uB85C\uB4DC: ${uploadCount}`));
3769
+ console.log(chalk13.dim(` \uC774\uBBF8 \uC874\uC7AC: ${existingUrls.size}`));
3770
+ if (urlMap.size > 0 && !fileStat.isDirectory()) {
3771
+ const cdnUrl = urlMap.values().next().value;
3772
+ console.log(chalk13.cyan(`
3773
+ CDN URL: ${cdnUrl}`));
3774
+ } else if (urlMap.size > 0 && fileStat.isDirectory()) {
3775
+ console.log(chalk13.bold("\nURL \uB9E4\uD551:"));
3776
+ for (const [local, cdn] of urlMap) {
3777
+ console.log(chalk13.dim(` ${local}`));
3778
+ console.log(chalk13.cyan(` \u2192 ${cdn}`));
3779
+ }
3780
+ }
3781
+ console.log("");
3782
+ }
3783
+ async function collectImageFiles(dir, cwd) {
3784
+ const results = [];
3785
+ const entries = await readdir(dir, { withFileTypes: true });
3786
+ for (const entry of entries) {
3787
+ const fullPath = resolve10(dir, entry.name);
3788
+ if (entry.isDirectory()) {
3789
+ const nested = await collectImageFiles(fullPath, cwd);
3790
+ results.push(...nested);
3791
+ } else if (entry.isFile() && IMAGE_EXTENSIONS2.has(extname(entry.name).toLowerCase())) {
3792
+ results.push(relative(cwd, fullPath));
3793
+ }
3794
+ }
3795
+ return results;
3796
+ }
3797
+
3798
+ // src/commands/generate.ts
3799
+ import chalk14 from "chalk";
3800
+ import { writeFile as writeFile6 } from "fs/promises";
3801
+ import { resolve as resolve11 } from "path";
3802
+ import open from "open";
3803
+ var SCHEMA_URL2 = "https://hosting.saeroon.com/schema/v1.20.0/site-schema.json";
3804
+ async function commandGenerate(options) {
3805
+ console.log(chalk14.bold("\nAI \uC0AC\uC774\uD2B8 \uC0DD\uC131\n"));
3806
+ if (!options.ref && !options.prompt) {
3807
+ console.error(chalk14.red("--ref <url> \uB610\uB294 --prompt <text> \uC911 \uD558\uB098\uB294 \uD544\uC218\uC785\uB2C8\uB2E4."));
3808
+ console.error(chalk14.dim(' \uC608: npx @saeroon/cli generate --prompt "\uCE74\uD398 \uD648\uD398\uC774\uC9C0"'));
3809
+ console.error(chalk14.dim(" \uC608: npx @saeroon/cli generate --ref https://example.com"));
3810
+ process.exit(1);
3811
+ }
3812
+ const apiKey = await resolveApiKey(options.apiKey);
3813
+ const apiBaseUrl = await getApiBaseUrl();
3814
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
3815
+ const outputPath = resolve11(process.cwd(), options.output ?? "schema.json");
3816
+ const genSpinner = spinner(
3817
+ options.ref ? `\uB808\uD37C\uB7F0\uC2A4 \uBD84\uC11D \uC911: ${options.ref}` : "\uD504\uB86C\uD504\uD2B8 \uAE30\uBC18 \uC0DD\uC131 \uC911..."
3818
+ );
3819
+ try {
3820
+ const result = await client.generateSite({
3821
+ referenceUrl: options.ref,
3822
+ prompt: options.prompt
3823
+ });
3824
+ genSpinner.stop(chalk14.green(" \uC0DD\uC131 \uC644\uB8CC!"));
3825
+ const schemaWithRef = { $schema: SCHEMA_URL2, ...result.schema };
3826
+ await writeFile6(
3827
+ outputPath,
3828
+ JSON.stringify(schemaWithRef, null, 2) + "\n",
3829
+ "utf-8"
3830
+ );
3831
+ console.log(chalk14.green(` ${options.output ?? "schema.json"} \uC800\uC7A5 \uC644\uB8CC`));
3832
+ const pages = Array.isArray(result.schema.pages) ? result.schema.pages : [];
3833
+ const blockCount = pages.reduce((acc, p) => {
3834
+ const blocks = p.blocks;
3835
+ return acc + (blocks ? Object.keys(blocks).length : 0);
3836
+ }, 0);
3837
+ console.log("");
3838
+ console.log(chalk14.bold("\uC0DD\uC131 \uACB0\uACFC:"));
3839
+ console.log(chalk14.dim(` \uD398\uC774\uC9C0: ${pages.length}\uAC1C`));
3840
+ console.log(chalk14.dim(` \uBE14\uB85D: ${blockCount}\uAC1C`));
3841
+ if (result.previewUrl) {
3842
+ console.log(chalk14.cyan(`
3843
+ Preview: ${result.previewUrl}`));
3844
+ try {
3845
+ await open(result.previewUrl);
3846
+ } catch {
3847
+ }
3848
+ }
3849
+ console.log("");
3850
+ console.log(chalk14.dim(" \uB2E4\uC74C \uB2E8\uACC4:"));
3851
+ console.log(chalk14.dim(" 1. schema.json\uC744 \uD655\uC778\uD558\uACE0 \uC218\uC815\uD558\uC138\uC694"));
3852
+ console.log(chalk14.dim(" 2. npx @saeroon/cli validate --local \uB85C \uAC80\uC99D"));
3853
+ console.log(chalk14.dim(" 3. npx @saeroon/cli preview \uB85C \uD504\uB9AC\uBDF0"));
3854
+ console.log("");
3855
+ } catch (error) {
3856
+ genSpinner.stop(
3857
+ chalk14.red(
3858
+ `\uC0DD\uC131 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
3859
+ )
3860
+ );
3861
+ process.exit(1);
3862
+ }
3863
+ }
3864
+
3865
+ // src/commands/compare.ts
3866
+ import chalk15 from "chalk";
3867
+ import { resolve as resolve12 } from "path";
3868
+ import { execSync, spawnSync } from "child_process";
3869
+ async function commandCompare(options) {
3870
+ console.log(chalk15.bold("\n\uC2DC\uAC01 \uBE44\uAD50 (Visual Diff)\n"));
3871
+ if (!options.ref || !options.preview) {
3872
+ console.error(chalk15.red("--ref <url> \uACFC --preview <url> \uC740 \uBAA8\uB450 \uD544\uC218\uC785\uB2C8\uB2E4."));
3873
+ console.error(chalk15.dim(" \uC608: npx @saeroon/cli compare --ref https://example.com --preview https://preview.saeroon.com/abc"));
3874
+ process.exit(1);
3875
+ }
3876
+ const playwrightAvailable = checkPlaywright();
3877
+ if (!playwrightAvailable) {
3878
+ console.log(chalk15.yellow("Playwright\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."));
3879
+ console.log(chalk15.dim(" \uB2E4\uC74C \uBA85\uB839\uC73C\uB85C \uC124\uCE58\uD558\uC138\uC694:"));
3880
+ console.log(chalk15.cyan(" npx playwright install chromium\n"));
3881
+ process.exit(1);
3882
+ }
3883
+ const width = parseInt(options.width ?? "1280", 10);
3884
+ const height = parseInt(options.height ?? "800", 10);
3885
+ const outputPath = resolve12(process.cwd(), options.output ?? "compare-result.png");
3886
+ const captureSpinner = spinner("\uB808\uD37C\uB7F0\uC2A4 \uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC911...");
3887
+ try {
3888
+ const refScreenshot = resolve12(process.cwd(), ".compare-ref.png");
3889
+ const previewScreenshot = resolve12(process.cwd(), ".compare-preview.png");
3890
+ captureScreenshot(options.ref, refScreenshot, width, height);
3891
+ captureSpinner.stop(chalk15.green(" \uB808\uD37C\uB7F0\uC2A4 \uCEA1\uCC98 \uC644\uB8CC"));
3892
+ const previewSpinner = spinner("\uD504\uB9AC\uBDF0 \uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC911...");
3893
+ captureScreenshot(options.preview, previewScreenshot, width, height);
3894
+ previewSpinner.stop(chalk15.green(" \uD504\uB9AC\uBDF0 \uCEA1\uCC98 \uC644\uB8CC"));
3895
+ const diffSpinner = spinner("\uC2DC\uAC01 \uBE44\uAD50 \uC0DD\uC131 \uC911...");
3896
+ const hasMagick = checkCommand("magick");
3897
+ if (hasMagick) {
3898
+ try {
3899
+ execSync(
3900
+ `magick compare -metric AE "${refScreenshot}" "${previewScreenshot}" "${outputPath}" 2>&1`,
3901
+ { stdio: "pipe" }
3902
+ );
3903
+ } catch {
3904
+ execSync(
3905
+ `magick composite -blend 50x50 "${refScreenshot}" "${previewScreenshot}" "${outputPath}"`,
3906
+ { stdio: "pipe" }
3907
+ );
3908
+ }
3909
+ diffSpinner.stop(chalk15.green(" Diff \uC774\uBBF8\uC9C0 \uC0DD\uC131 \uC644\uB8CC (ImageMagick)"));
3910
+ } else {
3911
+ execSync(
3912
+ `magick montage "${refScreenshot}" "${previewScreenshot}" -geometry +2+2 -tile 2x1 "${outputPath}" 2>/dev/null || cp "${refScreenshot}" "${outputPath}"`,
3913
+ { stdio: "pipe" }
3914
+ );
3915
+ diffSpinner.stop(chalk15.yellow(" \uB098\uB780\uD788 \uBE44\uAD50 \uC774\uBBF8\uC9C0 \uC0DD\uC131 (ImageMagick \uC5C6\uC74C \u2014 \uC624\uBC84\uB808\uC774 \uBE44\uAD50\uB294 magick \uC124\uCE58 \uD544\uC694)"));
3916
+ }
3917
+ try {
3918
+ execSync(`rm -f "${refScreenshot}" "${previewScreenshot}"`, { stdio: "pipe" });
3919
+ } catch {
3920
+ }
3921
+ console.log("");
3922
+ console.log(chalk15.green.bold("\uBE44\uAD50 \uC644\uB8CC!"));
3923
+ console.log(chalk15.dim(` \uCD9C\uB825: ${outputPath}`));
3924
+ console.log(chalk15.dim(` \uD574\uC0C1\uB3C4: ${width}\xD7${height}`));
3925
+ console.log(chalk15.dim(` \uB808\uD37C\uB7F0\uC2A4: ${options.ref}`));
3926
+ console.log(chalk15.dim(` \uD504\uB9AC\uBDF0: ${options.preview}`));
3927
+ console.log("");
3928
+ } catch (error) {
3929
+ console.error(chalk15.red(`\uBE44\uAD50 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`));
3930
+ process.exit(1);
3931
+ }
3932
+ }
3933
+ function checkPlaywright() {
3934
+ try {
3935
+ execSync("npx playwright --version", { stdio: "pipe" });
3936
+ return true;
3937
+ } catch {
3938
+ return false;
3939
+ }
3940
+ }
3941
+ function checkCommand(cmd) {
3942
+ try {
3943
+ execSync(`which ${cmd}`, { stdio: "pipe" });
3944
+ return true;
3945
+ } catch {
3946
+ return false;
3947
+ }
3948
+ }
3949
+ function captureScreenshot(url, outputPath, width, height) {
3950
+ const result = spawnSync("npx", [
3951
+ "playwright",
3952
+ "screenshot",
3953
+ "--browser",
3954
+ "chromium",
3955
+ "--viewport-size",
3956
+ `${width},${height}`,
3957
+ "--wait-for-timeout",
3958
+ "3000",
3959
+ "--full-page",
3960
+ url,
3961
+ outputPath
3962
+ ], {
3963
+ stdio: "pipe",
3964
+ timeout: 6e4
3965
+ });
3966
+ if (result.status !== 0) {
3967
+ const stderr = result.stderr?.toString() ?? "";
3968
+ throw new Error(`\uC2A4\uD06C\uB9B0\uC0F7 \uCEA1\uCC98 \uC2E4\uD328: ${stderr || "unknown error"}`);
3969
+ }
3970
+ }
3971
+
3972
+ // src/commands/diff.ts
3973
+ import chalk16 from "chalk";
3974
+ function compareSchemas(draft, published) {
3975
+ const draftObj = draft;
3976
+ const pubObj = published;
3977
+ const result = {
3978
+ pages: { added: [], removed: [], modified: [] },
3979
+ blocks: { added: [], removed: [], modified: [] },
3980
+ settings: { changed: [] },
3981
+ hasDifferences: false
3982
+ };
3983
+ comparePages(
3984
+ draftObj?.pages ?? [],
3985
+ pubObj?.pages ?? [],
3986
+ result
3987
+ );
3988
+ compareSettings(
3989
+ draftObj?.settings ?? {},
3990
+ pubObj?.settings ?? {},
3991
+ result
3992
+ );
3993
+ result.hasDifferences = result.pages.added.length > 0 || result.pages.removed.length > 0 || result.pages.modified.length > 0 || result.blocks.added.length > 0 || result.blocks.removed.length > 0 || result.blocks.modified.length > 0 || result.settings.changed.length > 0;
3994
+ return result;
3995
+ }
3996
+ function comparePages(draftPages, pubPages, result) {
3997
+ const draftMap = /* @__PURE__ */ new Map();
3998
+ const pubMap = /* @__PURE__ */ new Map();
3999
+ for (const page of draftPages) {
4000
+ const p = page;
4001
+ const id = p.id;
4002
+ if (id) draftMap.set(id, p);
4003
+ }
4004
+ for (const page of pubPages) {
4005
+ const p = page;
4006
+ const id = p.id;
4007
+ if (id) pubMap.set(id, p);
4008
+ }
4009
+ for (const [id, page] of draftMap) {
4010
+ if (!pubMap.has(id)) {
4011
+ const title = page.title ?? id;
4012
+ result.pages.added.push(title);
4013
+ const blocks = page.blocks ?? [];
4014
+ for (const block of blocks) {
4015
+ const b = block;
4016
+ result.blocks.added.push(`${title}/${b.type ?? b.id ?? "unknown"}`);
4017
+ }
4018
+ }
4019
+ }
4020
+ for (const [id, page] of pubMap) {
4021
+ if (!draftMap.has(id)) {
4022
+ const title = page.title ?? id;
4023
+ result.pages.removed.push(title);
4024
+ const blocks = page.blocks ?? [];
4025
+ for (const block of blocks) {
4026
+ const b = block;
4027
+ result.blocks.removed.push(`${title}/${b.type ?? b.id ?? "unknown"}`);
4028
+ }
4029
+ }
4030
+ }
4031
+ for (const [id, draftPage] of draftMap) {
4032
+ const pubPage = pubMap.get(id);
4033
+ if (!pubPage) continue;
4034
+ const title = draftPage.title ?? id;
4035
+ const pageModified = draftPage.title !== pubPage.title || draftPage.slug !== pubPage.slug;
4036
+ if (pageModified) {
4037
+ result.pages.modified.push(title);
4038
+ }
4039
+ const draftBlocks = Array.isArray(draftPage.blocks) ? draftPage.blocks : Object.values(draftPage.blocks ?? {});
4040
+ const pubBlocks = Array.isArray(pubPage.blocks) ? pubPage.blocks : Object.values(pubPage.blocks ?? {});
4041
+ compareBlocks(title, draftBlocks, pubBlocks, result);
4042
+ }
4043
+ }
4044
+ function compareBlocks(pageName, draftBlocks, pubBlocks, result) {
4045
+ const draftMap = /* @__PURE__ */ new Map();
4046
+ const pubMap = /* @__PURE__ */ new Map();
4047
+ for (const block of draftBlocks) {
4048
+ const b = block;
4049
+ const id = b.id;
4050
+ if (id) draftMap.set(id, b);
4051
+ }
4052
+ for (const block of pubBlocks) {
4053
+ const b = block;
4054
+ const id = b.id;
4055
+ if (id) pubMap.set(id, b);
4056
+ }
4057
+ for (const [id, block] of draftMap) {
4058
+ if (!pubMap.has(id)) {
4059
+ const type = block.type ?? id;
4060
+ result.blocks.added.push(`${pageName}/${type}`);
4061
+ }
4062
+ }
4063
+ for (const [id, block] of pubMap) {
4064
+ if (!draftMap.has(id)) {
4065
+ const type = block.type ?? id;
4066
+ result.blocks.removed.push(`${pageName}/${type}`);
4067
+ }
4068
+ }
4069
+ for (const [id, draftBlock] of draftMap) {
4070
+ const pubBlock = pubMap.get(id);
4071
+ if (!pubBlock) continue;
4072
+ if (JSON.stringify(draftBlock) !== JSON.stringify(pubBlock)) {
4073
+ const type = draftBlock.type ?? id;
4074
+ result.blocks.modified.push(`${pageName}/${type}`);
4075
+ }
4076
+ }
4077
+ }
4078
+ function compareSettings(draftSettings, pubSettings, result) {
4079
+ const allKeys = /* @__PURE__ */ new Set([
4080
+ ...Object.keys(draftSettings),
4081
+ ...Object.keys(pubSettings)
4082
+ ]);
4083
+ for (const key of allKeys) {
4084
+ const draftVal = draftSettings[key];
4085
+ const pubVal = pubSettings[key];
4086
+ if (JSON.stringify(draftVal) !== JSON.stringify(pubVal)) {
4087
+ result.settings.changed.push({
4088
+ key,
4089
+ draft: draftVal,
4090
+ published: pubVal
4091
+ });
4092
+ }
4093
+ }
4094
+ }
4095
+ function printDiffResult(result) {
4096
+ if (!result.hasDifferences) {
4097
+ console.log(chalk16.green("\nDraft\uC640 Published \uC2A4\uD0A4\uB9C8\uAC00 \uB3D9\uC77C\uD569\uB2C8\uB2E4. \uBCC0\uACBD \uC0AC\uD56D \uC5C6\uC74C.\n"));
4098
+ return;
4099
+ }
4100
+ console.log(chalk16.bold("\nStaging vs Production \uC2A4\uD0A4\uB9C8 \uBE44\uAD50:\n"));
4101
+ if (result.pages.added.length > 0 || result.pages.removed.length > 0 || result.pages.modified.length > 0) {
4102
+ console.log(chalk16.bold.underline(" Pages"));
4103
+ for (const page of result.pages.added) {
4104
+ console.log(` ${chalk16.green("+")} ${page}`);
4105
+ }
4106
+ for (const page of result.pages.removed) {
4107
+ console.log(` ${chalk16.red("-")} ${page}`);
4108
+ }
4109
+ for (const page of result.pages.modified) {
4110
+ console.log(` ${chalk16.yellow("~")} ${page}`);
4111
+ }
4112
+ console.log("");
4113
+ }
4114
+ if (result.blocks.added.length > 0 || result.blocks.removed.length > 0 || result.blocks.modified.length > 0) {
4115
+ console.log(chalk16.bold.underline(" Blocks"));
4116
+ for (const block of result.blocks.added) {
4117
+ console.log(` ${chalk16.green("+")} ${block}`);
4118
+ }
4119
+ for (const block of result.blocks.removed) {
4120
+ console.log(` ${chalk16.red("-")} ${block}`);
4121
+ }
4122
+ for (const block of result.blocks.modified) {
4123
+ console.log(` ${chalk16.yellow("~")} ${block}`);
4124
+ }
4125
+ console.log("");
4126
+ }
4127
+ if (result.settings.changed.length > 0) {
4128
+ console.log(chalk16.bold.underline(" Settings"));
4129
+ for (const { key, draft, published } of result.settings.changed) {
4130
+ if (published === void 0) {
4131
+ console.log(` ${chalk16.green("+")} ${key}: ${chalk16.green(formatValue(draft))}`);
4132
+ } else if (draft === void 0) {
4133
+ console.log(` ${chalk16.red("-")} ${key}: ${chalk16.red(formatValue(published))}`);
4134
+ } else {
4135
+ console.log(` ${chalk16.yellow("~")} ${key}:`);
4136
+ console.log(` ${chalk16.red(`- ${formatValue(published)}`)}`);
4137
+ console.log(` ${chalk16.green(`+ ${formatValue(draft)}`)}`);
4138
+ }
4139
+ }
4140
+ console.log("");
4141
+ }
4142
+ const totalChanges = result.pages.added.length + result.pages.removed.length + result.pages.modified.length + result.blocks.added.length + result.blocks.removed.length + result.blocks.modified.length + result.settings.changed.length;
4143
+ console.log(chalk16.dim(` \uCD1D ${totalChanges}\uAC74\uC758 \uBCC0\uACBD \uC0AC\uD56D
4144
+ `));
4145
+ }
4146
+ function formatValue(value) {
4147
+ if (value === void 0) return "undefined";
4148
+ if (value === null) return "null";
4149
+ const str = typeof value === "string" ? value : JSON.stringify(value);
4150
+ if (str.length > 80) {
4151
+ return str.slice(0, 77) + "...";
4152
+ }
4153
+ return str;
4154
+ }
4155
+ async function commandDiff(options) {
4156
+ const projectConfig = await loadProjectConfig();
4157
+ if (!projectConfig?.siteId) {
4158
+ console.error(chalk16.red("\nsiteId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
4159
+ console.error(chalk16.yellow("\uB2E4\uC74C \uC911 \uD558\uB098\uB97C \uC2E4\uD589\uD558\uC138\uC694:"));
4160
+ console.error(chalk16.dim(" 1. npx @saeroon/cli init --from-site <siteId> \uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654"));
4161
+ console.error(chalk16.dim(' 2. saeroon.config.json\uC5D0 "siteId" \uD544\uB4DC\uB97C \uC9C1\uC811 \uCD94\uAC00'));
4162
+ console.error("");
4163
+ process.exit(1);
4164
+ }
4165
+ const siteId = projectConfig.siteId;
4166
+ const apiKey = await resolveApiKey(options.apiKey);
4167
+ const apiBaseUrl = await getApiBaseUrl();
4168
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
4169
+ console.log(chalk16.bold("\nStaging vs Production \uC2A4\uD0A4\uB9C8 \uBE44\uAD50\n"));
4170
+ console.log(chalk16.dim(` \uC0AC\uC774\uD2B8 ID: ${siteId}`));
4171
+ console.log("");
4172
+ const fetchSpinner = spinner("Draft \uBC0F Published \uC2A4\uD0A4\uB9C8\uB97C \uAC00\uC838\uC624\uB294 \uC911...");
4173
+ let draftSchema;
4174
+ let publishedSchema;
4175
+ try {
4176
+ const [draftResult, publishedResult] = await Promise.all([
4177
+ client.getSiteSchema(siteId, true),
4178
+ client.getSiteSchema(siteId, false)
4179
+ ]);
4180
+ draftSchema = safeJsonParse(draftResult.schemaJson);
4181
+ publishedSchema = safeJsonParse(publishedResult.schemaJson);
4182
+ fetchSpinner.stop("");
4183
+ console.log(chalk16.dim(` Draft: ${draftResult.isDraft ? "Draft" : "Published"} (editVersion: ${draftResult.editVersion})`));
4184
+ console.log(chalk16.dim(` Published: ${publishedResult.isDraft ? "Draft" : "Published"} (editVersion: ${publishedResult.editVersion})`));
4185
+ } catch (error) {
4186
+ if (error instanceof ApiError) {
4187
+ fetchSpinner.stop(
4188
+ chalk16.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`)
4189
+ );
4190
+ } else {
4191
+ fetchSpinner.stop(
4192
+ chalk16.red(
4193
+ `\uC2A4\uD0A4\uB9C8 \uC870\uD68C \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`
4194
+ )
4195
+ );
4196
+ }
4197
+ process.exit(1);
4198
+ }
4199
+ const diffResult = compareSchemas(draftSchema, publishedSchema);
4200
+ printDiffResult(diffResult);
4201
+ }
4202
+
4203
+ // src/commands/template.ts
4204
+ import chalk17 from "chalk";
4205
+ import { createInterface as createInterface5 } from "readline/promises";
4206
+ import { stdin as stdin5, stdout as stdout5 } from "process";
4207
+ var TEMPLATE_CATEGORIES2 = [
4208
+ "Business",
4209
+ "Portfolio",
4210
+ "Blog",
4211
+ "E-Commerce",
4212
+ "Landing Page",
4213
+ "Restaurant",
4214
+ "Real Estate",
4215
+ "Agency",
4216
+ "Personal",
4217
+ "Education",
4218
+ "Healthcare",
4219
+ "Event",
4220
+ "Other"
4221
+ ];
4222
+ async function commandTemplateRegister(options) {
4223
+ const projectConfig = await loadProjectConfig();
4224
+ if (!projectConfig?.siteId) {
4225
+ console.error(chalk17.red("\nsiteId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
4226
+ console.error(chalk17.yellow("\uB2E4\uC74C \uC911 \uD558\uB098\uB97C \uC2E4\uD589\uD558\uC138\uC694:"));
4227
+ console.error(chalk17.dim(" 1. npx @saeroon/cli init --from-site <siteId> \uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654"));
4228
+ console.error(chalk17.dim(' 2. saeroon.config.json\uC5D0 "siteId" \uD544\uB4DC\uB97C \uC9C1\uC811 \uCD94\uAC00'));
4229
+ console.error("");
4230
+ process.exit(1);
4231
+ }
4232
+ if (projectConfig.templateId) {
4233
+ console.error(chalk17.yellow("\n\uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0\uB294 \uC774\uBBF8 \uD15C\uD50C\uB9BF\uC774 \uB4F1\uB85D\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4."));
4234
+ console.error(chalk17.dim(` templateId: ${projectConfig.templateId}`));
4235
+ console.error(chalk17.dim(" \uBA54\uD0C0\uB370\uC774\uD130 \uC218\uC815: saeroon template update"));
4236
+ console.error(chalk17.dim(" \uBC84\uC804 \uB3D9\uAE30\uD654: saeroon template sync"));
4237
+ console.error("");
4238
+ process.exit(1);
4239
+ }
4240
+ const apiKey = await resolveApiKey(options.apiKey);
4241
+ const apiBaseUrl = await getApiBaseUrl();
4242
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
4243
+ console.log(chalk17.bold("\n\uD15C\uD50C\uB9BF \uB4F1\uB85D\n"));
4244
+ console.log(chalk17.dim(` \uC18C\uC2A4 \uC0AC\uC774\uD2B8: ${projectConfig.siteId}`));
4245
+ console.log("");
4246
+ let name = options.name ?? "";
4247
+ let category = options.category ?? "";
4248
+ let description = "";
4249
+ let tags;
4250
+ let priceKrw = 0;
4251
+ if (!name || !category) {
4252
+ const rl = createInterface5({ input: stdin5, output: stdout5 });
4253
+ try {
4254
+ if (!name) {
4255
+ while (!name) {
4256
+ name = (await rl.question(" \uC774\uB984: ")).trim();
4257
+ if (!name) {
4258
+ console.log(chalk17.red(" \uC774\uB984\uC740 \uD544\uC218\uC785\uB2C8\uB2E4."));
4259
+ }
4260
+ }
4261
+ }
4262
+ if (!category) {
4263
+ console.log("");
4264
+ console.log(chalk17.dim(" \uCE74\uD14C\uACE0\uB9AC \uBAA9\uB85D:"));
4265
+ TEMPLATE_CATEGORIES2.forEach((cat, i) => {
4266
+ console.log(chalk17.dim(` ${i + 1}. ${cat}`));
4267
+ });
4268
+ console.log("");
4269
+ while (!category) {
4270
+ const input = (await rl.question(" \uCE74\uD14C\uACE0\uB9AC (\uBC88\uD638 \uB610\uB294 \uC774\uB984): ")).trim();
4271
+ const idx = parseInt(input, 10);
4272
+ if (!isNaN(idx) && idx >= 1 && idx <= TEMPLATE_CATEGORIES2.length) {
4273
+ category = TEMPLATE_CATEGORIES2[idx - 1];
4274
+ } else {
4275
+ const matched = TEMPLATE_CATEGORIES2.find(
4276
+ (c) => c.toLowerCase() === input.toLowerCase()
4277
+ );
4278
+ if (matched) {
4279
+ category = matched;
4280
+ } else if (input) {
4281
+ category = input;
4282
+ } else {
4283
+ console.log(chalk17.red(" \uCE74\uD14C\uACE0\uB9AC\uB97C \uC120\uD0DD\uD574\uC8FC\uC138\uC694."));
4284
+ }
4285
+ }
4286
+ }
4287
+ }
4288
+ description = (await rl.question(" \uC124\uBA85 (\uC120\uD0DD): ")).trim();
4289
+ const tagsInput = (await rl.question(" \uD0DC\uADF8 (\uCF64\uB9C8 \uAD6C\uBD84, \uC120\uD0DD): ")).trim();
4290
+ tags = tagsInput ? tagsInput.split(",").map((t) => t.trim()).filter(Boolean) : void 0;
4291
+ const priceInput = (await rl.question(" \uAC00\uACA9 KRW (0 = \uBB34\uB8CC): ")).trim();
4292
+ priceKrw = parseInt(priceInput, 10) || 0;
4293
+ } finally {
4294
+ rl.close();
4295
+ }
4296
+ }
4297
+ console.log("");
4298
+ const registerSpinner = spinner("\uD15C\uD50C\uB9BF \uB4F1\uB85D \uC911...");
4299
+ try {
4300
+ const request = {
4301
+ name,
4302
+ description: description || void 0,
4303
+ category,
4304
+ tags,
4305
+ isPublicPreview: true,
4306
+ priceKrw,
4307
+ sourceSiteId: projectConfig.siteId
4308
+ };
4309
+ const result = await client.registerTemplate(request);
4310
+ registerSpinner.stop(chalk17.green("\uD15C\uD50C\uB9BF \uB4F1\uB85D \uC644\uB8CC!"));
4311
+ await saveProjectConfig({ templateId: result.id });
4312
+ if (options.json) {
4313
+ console.log(JSON.stringify(result, null, 2));
4314
+ } else {
4315
+ formatTemplateDetail(result);
4316
+ console.log(chalk17.dim(" templateId\uAC00 saeroon.config.json\uC5D0 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
4317
+ console.log(chalk17.dim(" \uBC84\uC804 \uB3D9\uAE30\uD654: saeroon template sync"));
4318
+ console.log("");
4319
+ }
4320
+ } catch (error) {
4321
+ if (error instanceof ApiError) {
4322
+ registerSpinner.stop(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4323
+ } else {
4324
+ registerSpinner.stop(
4325
+ chalk17.red(`\uB4F1\uB85D \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
4326
+ );
4327
+ }
4328
+ process.exit(1);
4329
+ }
4330
+ }
4331
+ async function commandTemplateSync(options) {
4332
+ const projectConfig = await loadProjectConfig();
4333
+ if (!projectConfig?.templateId) {
4334
+ console.error(chalk17.red("\ntemplateId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
4335
+ console.error(chalk17.yellow("\uBA3C\uC800 \uD15C\uD50C\uB9BF\uC744 \uB4F1\uB85D\uD558\uC138\uC694:"));
4336
+ console.error(chalk17.dim(" saeroon template register"));
4337
+ console.error("");
4338
+ process.exit(1);
4339
+ }
4340
+ if (!projectConfig.siteId) {
4341
+ console.error(chalk17.red("\nsiteId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
4342
+ process.exit(1);
4343
+ }
4344
+ const apiKey = await resolveApiKey(options.apiKey);
4345
+ const apiBaseUrl = await getApiBaseUrl();
4346
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
4347
+ console.log(chalk17.bold("\n\uD15C\uD50C\uB9BF \uBC84\uC804 \uB3D9\uAE30\uD654\n"));
4348
+ console.log(chalk17.dim(` \uC18C\uC2A4 \uC0AC\uC774\uD2B8: ${projectConfig.siteId}`));
4349
+ console.log(chalk17.dim(` \uD15C\uD50C\uB9BF ID: ${projectConfig.templateId}`));
4350
+ console.log("");
4351
+ if (!options.force) {
4352
+ const diffSpinner = spinner("\uC2A4\uD0A4\uB9C8 \uBCC0\uACBD \uC0AC\uD56D \uD655\uC778 \uC911...");
4353
+ try {
4354
+ const [draftResult, publishedResult] = await Promise.all([
4355
+ client.getSiteSchema(projectConfig.siteId, true),
4356
+ client.getSiteSchema(projectConfig.siteId, false)
4357
+ ]);
4358
+ diffSpinner.stop("");
4359
+ const diff = compareSchemas(safeJsonParse(draftResult.schemaJson), safeJsonParse(publishedResult.schemaJson));
4360
+ if (!diff.hasDifferences) {
4361
+ console.log(chalk17.green(" \uBCC0\uACBD \uC0AC\uD56D \uC5C6\uC74C. \uC774\uBBF8 \uCD5C\uC2E0 \uC0C1\uD0DC\uC785\uB2C8\uB2E4.\n"));
4362
+ return;
4363
+ }
4364
+ printSyncDiffSummary(diff);
4365
+ const rl = createInterface5({ input: stdin5, output: stdout5 });
4366
+ try {
4367
+ const answer = await rl.question(
4368
+ chalk17.yellow(" \uC774 \uBCC0\uACBD \uC0AC\uD56D\uC73C\uB85C \uC0C8 \uBC84\uC804\uC744 \uBC1C\uD589\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? (y/N): ")
4369
+ );
4370
+ if (answer.trim().toLowerCase() !== "y") {
4371
+ console.log(chalk17.dim("\n \uB3D9\uAE30\uD654\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
4372
+ return;
4373
+ }
4374
+ } finally {
4375
+ rl.close();
4376
+ }
4377
+ } catch (error) {
4378
+ if (error instanceof ApiError) {
4379
+ diffSpinner.stop(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4380
+ } else {
4381
+ diffSpinner.stop(
4382
+ chalk17.red(`\uC2A4\uD0A4\uB9C8 \uBE44\uAD50 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
4383
+ );
4384
+ }
4385
+ process.exit(1);
4386
+ }
4387
+ }
4388
+ console.log("");
4389
+ const syncSpinner = spinner("\uC0C8 \uBC84\uC804 \uBC1C\uD589 \uC911...");
4390
+ try {
4391
+ const result = await client.syncTemplateVersion(projectConfig.templateId);
4392
+ syncSpinner.stop(chalk17.green(`\uD15C\uD50C\uB9BF v${result.version} \uBC1C\uD589 \uC644\uB8CC!`));
4393
+ if (options.json) {
4394
+ console.log(JSON.stringify(result, null, 2));
4395
+ } else {
4396
+ console.log("");
4397
+ console.log(` \uBC84\uC804: v${result.version}`);
4398
+ console.log(` \uC774\uB984: ${result.name}`);
4399
+ console.log(` \uBC1C\uD589: ${chalk17.dim(result.updatedAt ?? "")}`);
4400
+ console.log("");
4401
+ }
4402
+ } catch (error) {
4403
+ if (error instanceof ApiError) {
4404
+ syncSpinner.stop(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4405
+ } else {
4406
+ syncSpinner.stop(
4407
+ chalk17.red(`\uB3D9\uAE30\uD654 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
4408
+ );
4409
+ }
4410
+ process.exit(1);
4411
+ }
4412
+ }
4413
+ async function commandTemplateStatus(options) {
4414
+ const apiKey = await resolveApiKey(options.apiKey);
4415
+ const apiBaseUrl = await getApiBaseUrl();
4416
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
4417
+ const fetchSpinner = spinner("\uD15C\uD50C\uB9BF \uBAA9\uB85D \uC870\uD68C \uC911...");
4418
+ try {
4419
+ const templates = await client.getTemplates();
4420
+ fetchSpinner.stop("");
4421
+ if (options.json) {
4422
+ console.log(JSON.stringify(templates, null, 2));
4423
+ } else {
4424
+ formatTemplateList(templates);
4425
+ }
4426
+ } catch (error) {
4427
+ if (error instanceof ApiError) {
4428
+ fetchSpinner.stop(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4429
+ } else {
4430
+ fetchSpinner.stop(
4431
+ chalk17.red(`\uC870\uD68C \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
4432
+ );
4433
+ }
4434
+ process.exit(1);
4435
+ }
4436
+ }
4437
+ async function commandTemplateUpdate(options) {
4438
+ const projectConfig = await loadProjectConfig();
4439
+ if (!projectConfig?.templateId) {
4440
+ console.error(chalk17.red("\ntemplateId\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n"));
4441
+ console.error(chalk17.yellow("\uBA3C\uC800 \uD15C\uD50C\uB9BF\uC744 \uB4F1\uB85D\uD558\uC138\uC694:"));
4442
+ console.error(chalk17.dim(" saeroon template register"));
4443
+ console.error("");
4444
+ process.exit(1);
4445
+ }
4446
+ const apiKey = await resolveApiKey(options.apiKey);
4447
+ const apiBaseUrl = await getApiBaseUrl();
4448
+ const client = new SaeroonApiClient(apiKey, apiBaseUrl);
4449
+ const request = {};
4450
+ let hasChanges = false;
4451
+ if (options.name !== void 0) {
4452
+ request.name = options.name;
4453
+ hasChanges = true;
4454
+ }
4455
+ if (options.description !== void 0) {
4456
+ request.description = options.description;
4457
+ hasChanges = true;
4458
+ }
4459
+ if (options.category !== void 0) {
4460
+ request.category = options.category;
4461
+ hasChanges = true;
4462
+ }
4463
+ if (options.tags !== void 0) {
4464
+ request.tags = options.tags.split(",").map((t) => t.trim()).filter(Boolean);
4465
+ hasChanges = true;
4466
+ }
4467
+ if (options.priceKrw !== void 0) {
4468
+ request.priceKrw = parseInt(options.priceKrw, 10) || 0;
4469
+ hasChanges = true;
4470
+ }
4471
+ if (!hasChanges) {
4472
+ const rl = createInterface5({ input: stdin5, output: stdout5 });
4473
+ try {
4474
+ const currentSpinner = spinner("\uD604\uC7AC \uD15C\uD50C\uB9BF \uC815\uBCF4 \uC870\uD68C \uC911...");
4475
+ let templates;
4476
+ try {
4477
+ templates = await client.getTemplates();
4478
+ currentSpinner.stop("");
4479
+ } catch (error) {
4480
+ if (error instanceof ApiError) {
4481
+ currentSpinner.stop(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4482
+ } else {
4483
+ currentSpinner.stop(chalk17.red("\uC870\uD68C \uC2E4\uD328"));
4484
+ }
4485
+ process.exit(1);
4486
+ }
4487
+ const current = templates.find((t) => t.id === projectConfig.templateId);
4488
+ if (!current) {
4489
+ console.error(chalk17.red(`
4490
+ \uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${projectConfig.templateId}
4491
+ `));
4492
+ process.exit(1);
4493
+ }
4494
+ console.log(chalk17.bold("\n\uD604\uC7AC \uD15C\uD50C\uB9BF \uC815\uBCF4:"));
4495
+ formatTemplateDetail(current);
4496
+ console.log(chalk17.bold("\uC218\uC815\uD560 \uD56D\uBAA9\uC744 \uC785\uB825\uD558\uC138\uC694 (\uBE48 \uAC12 = \uBCC0\uACBD \uC5C6\uC74C):\n"));
4497
+ const newName = (await rl.question(` \uC774\uB984 [${current.name}]: `)).trim();
4498
+ if (newName) {
4499
+ request.name = newName;
4500
+ hasChanges = true;
4501
+ }
4502
+ const newDesc = (await rl.question(` \uC124\uBA85 [${current.description ?? ""}]: `)).trim();
4503
+ if (newDesc) {
4504
+ request.description = newDesc;
4505
+ hasChanges = true;
4506
+ }
4507
+ const newTags = (await rl.question(` \uD0DC\uADF8 [${current.tags?.join(", ") ?? ""}]: `)).trim();
4508
+ if (newTags) {
4509
+ request.tags = newTags.split(",").map((t) => t.trim()).filter(Boolean);
4510
+ hasChanges = true;
4511
+ }
4512
+ const newPrice = (await rl.question(` \uAC00\uACA9 KRW [${current.priceKrw}]: `)).trim();
4513
+ if (newPrice) {
4514
+ request.priceKrw = parseInt(newPrice, 10) || 0;
4515
+ hasChanges = true;
4516
+ }
4517
+ } finally {
4518
+ rl.close();
4519
+ }
4520
+ }
4521
+ if (!hasChanges) {
4522
+ console.log(chalk17.dim("\n \uBCC0\uACBD \uC0AC\uD56D \uC5C6\uC74C.\n"));
4523
+ return;
4524
+ }
4525
+ console.log("");
4526
+ const updateSpinner = spinner("\uD15C\uD50C\uB9BF \uBA54\uD0C0\uB370\uC774\uD130 \uC218\uC815 \uC911...");
4527
+ try {
4528
+ const result = await client.updateTemplate(projectConfig.templateId, request);
4529
+ updateSpinner.stop(chalk17.green("\uD15C\uD50C\uB9BF \uC218\uC815 \uC644\uB8CC!"));
4530
+ if (options.json) {
4531
+ console.log(JSON.stringify(result, null, 2));
4532
+ } else {
4533
+ formatTemplateDetail(result);
4534
+ }
4535
+ } catch (error) {
4536
+ if (error instanceof ApiError) {
4537
+ updateSpinner.stop(chalk17.red(`API \uC5D0\uB7EC (${error.statusCode}): ${error.message}`));
4538
+ } else {
4539
+ updateSpinner.stop(
4540
+ chalk17.red(`\uC218\uC815 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`)
4541
+ );
4542
+ }
4543
+ process.exit(1);
4544
+ }
4545
+ }
4546
+ function printSyncDiffSummary(diff) {
4547
+ const parts = [];
4548
+ const pageChanges = diff.pages.added.length + diff.pages.removed.length + diff.pages.modified.length;
4549
+ const blockChanges = diff.blocks.added.length + diff.blocks.removed.length + diff.blocks.modified.length;
4550
+ const settingChanges = diff.settings.changed.length;
4551
+ if (pageChanges > 0) {
4552
+ const detail = [];
4553
+ if (diff.pages.added.length > 0) detail.push(chalk17.green(`+${diff.pages.added.length}`));
4554
+ if (diff.pages.removed.length > 0) detail.push(chalk17.red(`-${diff.pages.removed.length}`));
4555
+ if (diff.pages.modified.length > 0) detail.push(chalk17.yellow(`~${diff.pages.modified.length}`));
4556
+ parts.push(`\uD398\uC774\uC9C0 ${detail.join(" ")}`);
4557
+ }
4558
+ if (blockChanges > 0) {
4559
+ const detail = [];
4560
+ if (diff.blocks.added.length > 0) detail.push(chalk17.green(`+${diff.blocks.added.length}`));
4561
+ if (diff.blocks.removed.length > 0) detail.push(chalk17.red(`-${diff.blocks.removed.length}`));
4562
+ if (diff.blocks.modified.length > 0) detail.push(chalk17.yellow(`~${diff.blocks.modified.length}`));
4563
+ parts.push(`\uBE14\uB85D ${detail.join(" ")}`);
4564
+ }
4565
+ if (settingChanges > 0) {
4566
+ parts.push(`\uC124\uC815 ${chalk17.yellow(`~${settingChanges}`)}`);
4567
+ }
4568
+ console.log(` \uBCC0\uACBD \uAC10\uC9C0: ${parts.join(", ")}`);
4569
+ console.log("");
4570
+ }
4571
+
4572
+ // src/index.ts
4573
+ var require2 = createRequire(import.meta.url);
4574
+ var { version } = require2("../package.json");
4575
+ var program = new Command();
4576
+ program.name("saeroon-dev").description("Saeroon Hosting developer CLI \u2014 preview, validate, and deploy sites & templates").version(version);
4577
+ program.command("login").description("API \uD0A4 \uC124\uC815").action(commandLogin);
4578
+ program.command("whoami").description("\uD604\uC7AC \uC778\uC99D \uC0C1\uD0DC \uD655\uC778").action(commandWhoami);
4579
+ program.command("init").description("\uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654 (schema.json + \uC124\uC815 \uD30C\uC77C \uC0DD\uC131)").option("--from-template <id>", "\uAE30\uC874 \uD15C\uD50C\uB9BF\uC5D0\uC11C \uC2DC\uC791").option("--from-site <siteId>", "\uACE0\uAC1D \uC0AC\uC774\uD2B8\uC5D0\uC11C \uC2DC\uC791 (\uAD8C\uD55C \uD544\uC694)").option("--template <category>", "\uBE4C\uD2B8\uC778 \uC2A4\uD0C0\uD130 \uC0AC\uC6A9 (restaurant, portfolio, business, saas)").action(commandInit);
4580
+ program.command("preview").description("\uC2A4\uD0A4\uB9C8 \uD30C\uC77C\uC744 \uAC10\uC2DC\uD558\uACE0 \uC2E4\uC2DC\uAC04 \uD504\uB9AC\uBDF0 \uC81C\uACF5").argument("[schema-path]", "\uC2A4\uD0A4\uB9C8 JSON \uD30C\uC77C \uACBD\uB85C", "schema.json").option("--api-key <key>", "API Key").option("--device <device>", "\uD504\uB9AC\uBDF0 \uB514\uBC14\uC774\uC2A4: mobile|tablet|desktop", "desktop").option("--mode <mode>", "\uD504\uB9AC\uBDF0 \uBAA8\uB4DC: rest|ws", "rest").action(commandPreview);
4581
+ program.command("validate").description("\uC2A4\uD0A4\uB9C8 \uC720\uD6A8\uC131 \uAC80\uC99D + \uD488\uC9C8 \uC810\uC218 \uD655\uC778").argument("[schema-path]", "\uAC80\uC99D\uD560 \uC2A4\uD0A4\uB9C8 JSON \uD30C\uC77C \uACBD\uB85C", "schema.json").option("--api-key <key>", "API Key").option("--local", "\uB85C\uCEEC \uAC80\uC99D\uB9CC \uC218\uD589 (\uB124\uD2B8\uC6CC\uD06C \uBD88\uD544\uC694)").action(commandValidate);
4582
+ program.command("blocks").description("\uBE14\uB85D \uCE74\uD0C8\uB85C\uADF8 \uC870\uD68C").argument("[block-type]", "\uD2B9\uC815 \uBE14\uB85D \uD0C0\uC785 \uC0C1\uC138 \uC870\uD68C").action(commandBlocks);
4583
+ program.command("add").description("schema.json\uC5D0 \uBE14\uB85D \uCD94\uAC00 (\uAE30\uBCF8 props \uD3EC\uD568)").argument("<block-type>", "\uCD94\uAC00\uD560 \uBE14\uB85D \uD0C0\uC785 (e.g., heading-block, image-block)").option("--id <id>", "\uBE14\uB85D ID (\uBBF8\uC9C0\uC815 \uC2DC \uC790\uB3D9 \uC0DD\uC131)").option("--parent <parentId>", "\uBD80\uBAA8 \uBE14\uB85D ID").option("--after <siblingId>", "\uC0BD\uC785 \uC704\uCE58 (\uD615\uC81C \uBE14\uB85D \uB4A4)").option("--page <pageId>", "\uB300\uC0C1 \uD398\uC774\uC9C0 ID").action(commandAdd);
4584
+ program.command("generate").description("AI\uB85C \uC0AC\uC774\uD2B8 \uC2A4\uD0A4\uB9C8 \uC0DD\uC131").option("--ref <url>", "\uB808\uD37C\uB7F0\uC2A4 URL (\uC0AC\uC774\uD2B8\uB97C \uBD84\uC11D\uD558\uC5EC \uC720\uC0AC\uD55C \uC2A4\uD0A4\uB9C8 \uC0DD\uC131)").option("--prompt <text>", "\uD504\uB86C\uD504\uD2B8 \uD14D\uC2A4\uD2B8 (\uC124\uBA85 \uAE30\uBC18 \uC0DD\uC131)").option("--output <file>", "\uCD9C\uB825 \uD30C\uC77C \uACBD\uB85C", "schema.json").option("--api-key <key>", "API Key").action(commandGenerate);
4585
+ program.command("compare").description("\uB808\uD37C\uB7F0\uC2A4 \u2194 \uD504\uB9AC\uBDF0 \uC2DC\uAC01 \uBE44\uAD50 (Playwright \uC2A4\uD06C\uB9B0\uC0F7)").option("--ref <url>", "\uB808\uD37C\uB7F0\uC2A4 URL").option("--preview <url>", "\uD504\uB9AC\uBDF0 URL").option("--output <file>", "\uCD9C\uB825 \uD30C\uC77C \uACBD\uB85C", "compare-result.png").option("--width <px>", "\uBDF0\uD3EC\uD2B8 \uB108\uBE44", "1280").option("--height <px>", "\uBDF0\uD3EC\uD2B8 \uB192\uC774", "800").action(commandCompare);
4586
+ program.command("upload").description("\uC774\uBBF8\uC9C0\uB97C Saeroon CDN\uC5D0 \uC5C5\uB85C\uB4DC").argument("<path>", "\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uB610\uB294 \uB514\uB809\uD1A0\uB9AC \uACBD\uB85C").option("--replace-in <file>", "\uC5C5\uB85C\uB4DC \uD6C4 \uD30C\uC77C \uB0B4 \uB85C\uCEEC \uACBD\uB85C\uB97C CDN URL\uB85C \uAD50\uCCB4").option("--site-id <id>", "\uC0AC\uC774\uD2B8 ID").option("--api-key <key>", "API Key").action(commandUpload);
4587
+ program.command("deploy").description("\uC0AC\uC774\uD2B8\uC5D0 \uC2A4\uD0A4\uB9C8 \uBC30\uD3EC").option("--api-key <key>", "API Key").option("--target <target>", "\uBC30\uD3EC \uB300\uC0C1: staging|production", "staging").option("--dry-run", "\uC2E4\uC81C \uC5C5\uB85C\uB4DC/\uBC30\uD3EC \uC5C6\uC774 \uC5D0\uC14B \uB9AC\uD3EC\uD2B8\uB9CC \uCD9C\uB825").option("--sync-template", "Production \uBC30\uD3EC \uD6C4 \uB9C8\uCF13\uD50C\uB808\uC774\uC2A4 \uD15C\uD50C\uB9BF \uBC84\uC804 \uC790\uB3D9 \uB3D9\uAE30\uD654").action(commandDeploy);
4588
+ program.command("publish").description("[deprecated] \uB9C8\uCF13\uD50C\uB808\uC774\uC2A4\uC5D0 \uD15C\uD50C\uB9BF \uB4F1\uB85D \u2192 saeroon template register \uC0AC\uC6A9").argument("[schema-path]", "\uC81C\uCD9C\uD560 \uC2A4\uD0A4\uB9C8 JSON \uD30C\uC77C \uACBD\uB85C", "schema.json").option("--api-key <key>", "API Key").action(commandPublish);
4589
+ program.command("diff").description("Staging vs Production \uC2A4\uD0A4\uB9C8 \uBE44\uAD50").option("--api-key <key>", "API Key").action(commandDiff);
4590
+ var templateCmd = program.command("template").description("\uB9C8\uCF13\uD50C\uB808\uC774\uC2A4 \uD15C\uD50C\uB9BF \uAD00\uB9AC");
4591
+ templateCmd.command("register").description("\uC18C\uC720 \uC0AC\uC774\uD2B8\uB97C \uB9C8\uCF13\uD50C\uB808\uC774\uC2A4 \uD15C\uD50C\uB9BF\uC73C\uB85C \uB4F1\uB85D").option("--api-key <key>", "API Key").option("--name <name>", "\uD15C\uD50C\uB9BF \uC774\uB984").option("--category <category>", "\uCE74\uD14C\uACE0\uB9AC").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").action(commandTemplateRegister);
4592
+ templateCmd.command("sync").description("\uC18C\uC2A4 \uC0AC\uC774\uD2B8 \uC2A4\uD0A4\uB9C8\uB97C \uD15C\uD50C\uB9BF\uC5D0 \uB3D9\uAE30\uD654 + \uC0C8 \uBC84\uC804 \uBC1C\uD589").option("--api-key <key>", "API Key").option("--force", "\uBCC0\uACBD \uD655\uC778 \uC5C6\uC774 \uC989\uC2DC \uB3D9\uAE30\uD654").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").action(commandTemplateSync);
4593
+ templateCmd.command("status").description("\uB0B4 \uD15C\uD50C\uB9BF \uBAA9\uB85D \uBC0F \uC0C1\uD0DC \uC870\uD68C").option("--api-key <key>", "API Key").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").action(commandTemplateStatus);
4594
+ templateCmd.command("update").description("\uD15C\uD50C\uB9BF \uBA54\uD0C0\uB370\uC774\uD130 \uC218\uC815").option("--api-key <key>", "API Key").option("--name <name>", "\uD15C\uD50C\uB9BF \uC774\uB984").option("--description <desc>", "\uC124\uBA85").option("--category <category>", "\uCE74\uD14C\uACE0\uB9AC").option("--tags <tags>", "\uD0DC\uADF8 (\uCF64\uB9C8 \uAD6C\uBD84)").option("--price-krw <price>", "\uAC00\uACA9 KRW").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").action(commandTemplateUpdate);
4595
+ program.parse();