@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/README.md +283 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4595 -0
- package/package.json +51 -0
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();
|