connectbase-client 3.25.0 → 3.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6,6 +6,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
9
13
  var __copyProps = (to, from, except, desc) => {
10
14
  if (from && typeof from === "object" || typeof from === "function") {
11
15
  for (let key of __getOwnPropNames(from))
@@ -22,15 +26,114 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
26
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
27
  mod
24
28
  ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
25
30
 
26
31
  // src/cli.ts
27
- var fs = __toESM(require("fs"));
28
- var path = __toESM(require("path"));
32
+ var cli_exports = {};
33
+ __export(cli_exports, {
34
+ computeDeployDiff: () => computeDeployDiff,
35
+ normalizeRelativePath: () => normalizeRelativePath,
36
+ parseArgs: () => parseArgs,
37
+ registerEndpointBinding: () => registerEndpointBinding,
38
+ sha256Hex: () => sha256Hex
39
+ });
40
+ module.exports = __toCommonJS(cli_exports);
41
+ var fs2 = __toESM(require("fs"));
42
+ var path2 = __toESM(require("path"));
29
43
  var crypto = __toESM(require("crypto"));
30
44
  var https = __toESM(require("https"));
31
45
  var http = __toESM(require("http"));
32
46
  var readline = __toESM(require("readline"));
33
- var VERSION = "3.25.0";
47
+
48
+ // src/tunnel-utils.ts
49
+ var fs = __toESM(require("fs"));
50
+ var path = __toESM(require("path"));
51
+ var os = __toESM(require("os"));
52
+ function getTunnelLockDir() {
53
+ const platform2 = os.platform();
54
+ if (platform2 === "darwin") {
55
+ return path.join(os.homedir(), "Library", "Application Support", "connectbase", "tunnel-locks");
56
+ } else if (platform2 === "win32") {
57
+ return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "connectbase", "tunnel-locks");
58
+ }
59
+ const stateHome = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
60
+ return path.join(stateHome, "connectbase", "tunnel-locks");
61
+ }
62
+ function isProcessAlive(pid) {
63
+ try {
64
+ process.kill(pid, 0);
65
+ return true;
66
+ } catch (e) {
67
+ return e.code === "EPERM";
68
+ }
69
+ }
70
+ function acquireTunnelLock(appID, port, force, version) {
71
+ const lockDir = getTunnelLockDir();
72
+ fs.mkdirSync(lockDir, { recursive: true });
73
+ const lockPath = path.join(lockDir, `${appID}-${port}.lock`);
74
+ const lockData = {
75
+ pid: process.pid,
76
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
77
+ appID,
78
+ port,
79
+ host: os.hostname(),
80
+ version
81
+ };
82
+ if (!force) {
83
+ try {
84
+ const fd = fs.openSync(lockPath, "wx");
85
+ fs.writeSync(fd, JSON.stringify(lockData, null, 2));
86
+ fs.closeSync(fd);
87
+ return lockPath;
88
+ } catch (e) {
89
+ if (e.code !== "EEXIST") {
90
+ return null;
91
+ }
92
+ }
93
+ try {
94
+ const existing = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
95
+ if (isProcessAlive(existing.pid)) {
96
+ return `LOCKED:${existing.pid}:${existing.startedAt}:${existing.host}`;
97
+ }
98
+ } catch {
99
+ }
100
+ }
101
+ fs.writeFileSync(lockPath, JSON.stringify(lockData, null, 2));
102
+ return lockPath;
103
+ }
104
+ function releaseTunnelLock(lockPath) {
105
+ try {
106
+ fs.unlinkSync(lockPath);
107
+ } catch {
108
+ }
109
+ }
110
+ function handleTunnelError(msg, appId, localPort) {
111
+ if (msg.code === "replaced") {
112
+ return {
113
+ action: "exit",
114
+ message: `\u26A0 \uB2E4\uB978 \uC138\uC158\uC774 \uAC19\uC740 \uC571+\uD3EC\uD2B8(${appId}:${localPort})\uB85C \uC5F0\uACB0\uB418\uC5B4 \uC774 \uC138\uC158\uC774 \uC885\uB8CC\uB429\uB2C8\uB2E4. \uB3D9\uC77C \uBA38\uC2E0\uC5D0\uC11C \uB450 \uBC88 \uC2E4\uD589\uD558\uC9C0 \uB9C8\uC138\uC694.`,
115
+ exitCode: 1
116
+ };
117
+ }
118
+ return {
119
+ action: "warn",
120
+ message: `\uD130\uB110 \uC5D0\uB7EC: ${msg.error || msg.message || "\uC54C \uC218 \uC5C6\uB294 \uC5D0\uB7EC"}`
121
+ };
122
+ }
123
+
124
+ // src/cli.ts
125
+ function getPackageVersion() {
126
+ try {
127
+ const pkgPath = path2.join(__dirname, "..", "package.json");
128
+ if (fs2.existsSync(pkgPath)) {
129
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
130
+ return pkg.version || "0.0.0";
131
+ }
132
+ } catch {
133
+ }
134
+ return "3.25.1";
135
+ }
136
+ var VERSION = getPackageVersion();
34
137
  var DEFAULT_BASE_URL = "https://api.connectbase.world";
35
138
  var colors = {
36
139
  reset: "\x1B[0m",
@@ -111,17 +214,27 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
111
214
  ".zip",
112
215
  ".wasm"
113
216
  ]);
217
+ function sha256Hex(value) {
218
+ return crypto.createHash("sha256").update(value, "utf8").digest("hex");
219
+ }
220
+ function normalizeRelativePath(relativePath) {
221
+ let p = relativePath.replace(/\\/g, "/");
222
+ if (!p.startsWith("/")) p = `/${p}`;
223
+ return p;
224
+ }
114
225
  function loadConfig() {
115
226
  const config = {
116
- apiKey: process.env.CONNECTBASE_API_KEY,
227
+ publicKey: process.env.CONNECTBASE_PUBLIC_KEY,
228
+ secretKey: process.env.CONNECTBASE_SECRET_KEY,
117
229
  storageId: process.env.CONNECTBASE_STORAGE_ID,
118
230
  baseUrl: process.env.CONNECTBASE_BASE_URL || DEFAULT_BASE_URL
119
231
  };
120
- const rcPath = path.join(process.cwd(), ".connectbaserc");
121
- if (fs.existsSync(rcPath)) {
232
+ const rcPath = path2.join(process.cwd(), ".connectbaserc");
233
+ if (fs2.existsSync(rcPath)) {
122
234
  try {
123
- const rcContent = JSON.parse(fs.readFileSync(rcPath, "utf-8"));
124
- if (rcContent.apiKey) config.apiKey = rcContent.apiKey;
235
+ const rcContent = JSON.parse(fs2.readFileSync(rcPath, "utf-8"));
236
+ if (rcContent.publicKey) config.publicKey = rcContent.publicKey;
237
+ if (rcContent.secretKey) config.secretKey = rcContent.secretKey;
125
238
  if (rcContent.storageId) config.storageId = rcContent.storageId;
126
239
  if (rcContent.baseUrl) config.baseUrl = rcContent.baseUrl;
127
240
  if (rcContent.deployDir) config.deployDir = rcContent.deployDir;
@@ -133,16 +246,16 @@ function loadConfig() {
133
246
  }
134
247
  function collectFiles(dir, baseDir = dir) {
135
248
  const files = [];
136
- const entries = fs.readdirSync(dir, { withFileTypes: true });
249
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
137
250
  for (const entry of entries) {
138
- const fullPath = path.join(dir, entry.name);
139
- const relativePath = path.relative(baseDir, fullPath);
251
+ const fullPath = path2.join(dir, entry.name);
252
+ const relativePath = path2.relative(baseDir, fullPath);
140
253
  if (entry.isDirectory()) {
141
254
  if (!entry.name.startsWith(".")) {
142
255
  files.push(...collectFiles(fullPath, baseDir));
143
256
  }
144
257
  } else if (entry.isFile()) {
145
- const ext = path.extname(entry.name).toLowerCase();
258
+ const ext = path2.extname(entry.name).toLowerCase();
146
259
  if (!ALLOWED_EXTENSIONS.has(ext)) {
147
260
  continue;
148
261
  }
@@ -152,15 +265,16 @@ function collectFiles(dir, baseDir = dir) {
152
265
  const isBinary = BINARY_EXTENSIONS.has(ext);
153
266
  let content;
154
267
  if (isBinary) {
155
- content = fs.readFileSync(fullPath).toString("base64");
268
+ content = fs2.readFileSync(fullPath).toString("base64");
156
269
  } else {
157
- content = fs.readFileSync(fullPath, "utf-8");
270
+ content = fs2.readFileSync(fullPath, "utf-8");
158
271
  }
272
+ const normalized = normalizeRelativePath(relativePath);
159
273
  files.push({
160
- path: relativePath.replace(/\\/g, "/"),
161
- // Windows 경로 변환
274
+ path: normalized,
162
275
  content,
163
- isBinary
276
+ isBinary,
277
+ hash: sha256Hex(content)
164
278
  });
165
279
  }
166
280
  }
@@ -229,28 +343,28 @@ function computeDeployTimeout(totalBytes, override) {
229
343
  return Math.max(DEPLOY_TIMEOUT_MIN_MS, DEPLOY_TIMEOUT_BASE_MS + sizeBased);
230
344
  }
231
345
  async function deploy(directory, config, isDev = false, deployOpts = {}) {
232
- const dir = path.resolve(directory);
233
- if (!fs.existsSync(dir)) {
346
+ const dir = path2.resolve(directory);
347
+ if (!fs2.existsSync(dir)) {
234
348
  error(`\uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dir}`);
235
349
  process.exit(1);
236
350
  }
237
- const pkgPath = path.join(process.cwd(), "package.json");
238
- if (fs.existsSync(pkgPath)) {
351
+ const pkgPath = path2.join(process.cwd(), "package.json");
352
+ if (fs2.existsSync(pkgPath)) {
239
353
  try {
240
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
354
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
241
355
  if (pkg.scripts?.build) {
242
- warn(`\uBC30\uD3EC \uC804 \uBE4C\uB4DC\uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694: ${colors.cyan}npm run build && npx connectbase-client deploy${colors.reset}`);
243
- warn(`\uB610\uB294 package.json\uC5D0 deploy \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uB4F1\uB85D\uD558\uC138\uC694: ${colors.cyan}npx connectbase-client init${colors.reset}`);
356
+ warn(`\uBC30\uD3EC \uC804 \uBE4C\uB4DC\uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694: ${colors.cyan}npm run build && npx connectbase deploy${colors.reset}`);
357
+ warn(`\uB610\uB294 package.json\uC5D0 deploy \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uB4F1\uB85D\uD558\uC138\uC694: ${colors.cyan}npx connectbase init${colors.reset}`);
244
358
  }
245
359
  } catch {
246
360
  }
247
361
  }
248
- if (!fs.statSync(dir).isDirectory()) {
362
+ if (!fs2.statSync(dir).isDirectory()) {
249
363
  error(`${dir}\uB294 \uB514\uB809\uD1A0\uB9AC\uAC00 \uC544\uB2D9\uB2C8\uB2E4`);
250
364
  process.exit(1);
251
365
  }
252
- const indexPath = path.join(dir, "index.html");
253
- if (!fs.existsSync(indexPath)) {
366
+ const indexPath = path2.join(dir, "index.html");
367
+ if (!fs2.existsSync(indexPath)) {
254
368
  error("index.html \uD30C\uC77C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
255
369
  process.exit(1);
256
370
  }
@@ -265,60 +379,149 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
265
379
  const totalSize = files.reduce((sum, f) => sum + f.content.length, 0);
266
380
  const sizeKB = (totalSize / 1024).toFixed(1);
267
381
  log(`${colors.green}\u2713${colors.reset} ${files.length}\uAC1C \uD30C\uC77C \uBC1C\uACAC (${sizeKB} KB)`);
268
- const deployPath = isDev ? "deploy/dev" : "deploy";
269
- const url = `${config.baseUrl}/v1/public/storages/webs/${config.storageId}/${deployPath}`;
270
382
  const envLabel = isDev ? "Dev" : "Prod";
383
+ const baseStorageUrl = `${config.baseUrl}/v1/public/storages/webs/${config.storageId}`;
384
+ const headers = { "X-Public-Key": config.publicKey };
271
385
  const timeoutMs = computeDeployTimeout(totalSize, deployOpts.timeoutMs);
272
386
  log(`${colors.dim}\uD0C0\uC784\uC544\uC6C3: ${Math.round(timeoutMs / 1e3)}\uCD08${colors.reset}`);
273
- process.stdout.write(`${colors.blue}\u27F3${colors.reset} ${envLabel} \uBC30\uD3EC \uC911...`);
274
- let heartbeat = null;
275
- if (process.stdout.isTTY) {
276
- const startedAt = Date.now();
277
- heartbeat = setInterval(() => {
278
- const elapsed = Math.floor((Date.now() - startedAt) / 1e3);
279
- process.stdout.write(`\r${colors.blue}\u27F3${colors.reset} ${envLabel} \uBC30\uD3EC \uC911... ${colors.dim}(${elapsed}s)${colors.reset}`);
280
- }, 1e3);
387
+ if (isDev) {
388
+ await fullDeploy(baseStorageUrl, headers, files, envLabel, "deploy/dev", timeoutMs);
389
+ return;
281
390
  }
282
391
  try {
283
- const response = await makeRequest(
284
- url,
285
- "POST",
286
- {
287
- "X-API-Key": config.apiKey
288
- },
289
- JSON.stringify({
290
- files: files.map((f) => ({
291
- path: f.path,
292
- content: f.content,
293
- is_binary: f.isBinary
294
- }))
295
- }),
296
- { timeoutMs }
297
- );
392
+ const manifest = await tryFetchManifest(baseStorageUrl, headers);
393
+ if (!manifest) {
394
+ await fullDeploy(baseStorageUrl, headers, files, envLabel, "deploy", timeoutMs);
395
+ return;
396
+ }
397
+ const diff = computeDeployDiff(files, manifest);
398
+ if (diff.upsert.length === 0 && diff.delete.length === 0) {
399
+ success(`\uBCC0\uACBD\uC0AC\uD56D \uC5C6\uC74C (${files.length}\uAC1C \uD30C\uC77C \uC77C\uCE58)`);
400
+ log(`
401
+ ${colors.cyan}URL: https://${config.storageId}.web.connectbase.world${colors.reset}
402
+ `);
403
+ return;
404
+ }
405
+ info(`\uBCC0\uACBD: ${colors.green}+${diff.upsert.length}${colors.reset} / ${colors.red}-${diff.delete.length}${colors.reset} (\uC804\uCCB4 ${files.length}\uAC1C \uC911)`);
406
+ const uploadSize = diff.upsert.reduce((s, f) => s + f.content.length, 0);
407
+ info(`\uC5C5\uB85C\uB4DC \uD06C\uAE30: ${(uploadSize / 1024).toFixed(1)} KB`);
408
+ await incrementalDeploy(baseStorageUrl, headers, diff, manifest.revision, envLabel, timeoutMs);
409
+ } catch (err) {
410
+ process.stdout.write("\r \r");
411
+ error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
412
+ process.exit(1);
413
+ }
414
+ }
415
+ async function tryFetchManifest(baseStorageUrl, headers) {
416
+ const res = await makeRequest(`${baseStorageUrl}/deploy/manifest`, "GET", headers);
417
+ if (res.status === 404) return null;
418
+ if (res.status < 200 || res.status >= 300) {
419
+ const data = res.data;
420
+ const msg = typeof data === "object" && data !== null ? data.message || data.error || JSON.stringify(data) : `HTTP ${res.status}`;
421
+ throw new Error(`manifest \uC870\uD68C \uC2E4\uD328: ${msg}`);
422
+ }
423
+ const parsed = res.data;
424
+ if (!parsed || !Array.isArray(parsed.files)) {
425
+ return null;
426
+ }
427
+ return parsed;
428
+ }
429
+ function computeDeployDiff(local, manifest) {
430
+ const localByPath = /* @__PURE__ */ new Map();
431
+ for (const f of local) localByPath.set(f.path, f);
432
+ const remoteByPath = /* @__PURE__ */ new Map();
433
+ for (const m of manifest.files) remoteByPath.set(m.path, m);
434
+ const upsert = [];
435
+ for (const f of local) {
436
+ const remote = remoteByPath.get(f.path);
437
+ if (!remote || remote.hash !== f.hash || !remote.hash) {
438
+ upsert.push({
439
+ path: f.path,
440
+ content: f.content,
441
+ is_binary: f.isBinary,
442
+ hash: f.hash
443
+ });
444
+ }
445
+ }
446
+ const toDelete = [];
447
+ for (const m of manifest.files) {
448
+ if (!localByPath.has(m.path)) toDelete.push(m.path);
449
+ }
450
+ return { upsert, delete: toDelete };
451
+ }
452
+ function startUploadHeartbeat(envLabel, suffix = "\uBC30\uD3EC \uC911") {
453
+ if (!process.stdout.isTTY) return null;
454
+ const startedAt = Date.now();
455
+ return setInterval(() => {
456
+ const elapsed = Math.floor((Date.now() - startedAt) / 1e3);
457
+ process.stdout.write(`\r${colors.blue}\u27F3${colors.reset} ${envLabel} ${suffix}... ${colors.dim}(${elapsed}s)${colors.reset}`);
458
+ }, 1e3);
459
+ }
460
+ async function incrementalDeploy(baseStorageUrl, headers, diff, baseRevision, envLabel, timeoutMs) {
461
+ process.stdout.write(`${colors.blue}\u27F3${colors.reset} ${envLabel} \uC99D\uBD84 \uBC30\uD3EC \uC911...`);
462
+ let heartbeat = startUploadHeartbeat(envLabel, "\uC99D\uBD84 \uBC30\uD3EC \uC911");
463
+ const body = JSON.stringify({
464
+ upsert: diff.upsert,
465
+ delete: diff.delete,
466
+ base_revision: baseRevision
467
+ });
468
+ const res = await makeRequest(`${baseStorageUrl}/deploy/incremental`, "POST", headers, body, { timeoutMs });
469
+ if (heartbeat) clearInterval(heartbeat);
470
+ process.stdout.write("\r \r");
471
+ if (res.status === 409) {
472
+ warn("\uC11C\uBC84 revision \uC774 \uBCC0\uACBD\uB418\uC5C8\uC2B5\uB2C8\uB2E4. manifest \uC7AC\uC870\uD68C \uD6C4 \uC7AC\uC2DC\uB3C4\uD569\uB2C8\uB2E4.");
473
+ const manifest = await tryFetchManifest(baseStorageUrl, headers);
474
+ if (!manifest) {
475
+ error("\uC7AC\uC2DC\uB3C4 manifest \uC870\uD68C \uC2E4\uD328");
476
+ process.exit(1);
477
+ }
478
+ const body2 = JSON.stringify({
479
+ upsert: diff.upsert,
480
+ delete: diff.delete,
481
+ base_revision: manifest.revision
482
+ });
483
+ process.stdout.write(`${colors.blue}\u27F3${colors.reset} ${envLabel} \uC99D\uBD84 \uBC30\uD3EC \uC7AC\uC2DC\uB3C4...`);
484
+ heartbeat = startUploadHeartbeat(envLabel, "\uC99D\uBD84 \uBC30\uD3EC \uC7AC\uC2DC\uB3C4");
485
+ const res2 = await makeRequest(`${baseStorageUrl}/deploy/incremental`, "POST", headers, body2, { timeoutMs });
298
486
  if (heartbeat) clearInterval(heartbeat);
299
- process.stdout.write("\r \r");
300
- if (response.status >= 200 && response.status < 300) {
301
- success(`${envLabel} \uBC30\uD3EC \uC644\uB8CC!`);
302
- const data = response.data;
303
- if (data?.dev_url) {
304
- log(`
487
+ process.stdout.write("\r \r");
488
+ handleDeployResponse(res2, envLabel);
489
+ return;
490
+ }
491
+ handleDeployResponse(res, envLabel);
492
+ }
493
+ async function fullDeploy(baseStorageUrl, headers, files, envLabel, endpoint, timeoutMs) {
494
+ process.stdout.write(`${colors.blue}\u27F3${colors.reset} ${envLabel} \uBC30\uD3EC \uC911...`);
495
+ const heartbeat = startUploadHeartbeat(envLabel);
496
+ const body = JSON.stringify({
497
+ files: files.map((f) => ({
498
+ path: f.path,
499
+ content: f.content,
500
+ is_binary: f.isBinary
501
+ }))
502
+ });
503
+ const res = await makeRequest(`${baseStorageUrl}/${endpoint}`, "POST", headers, body, { timeoutMs });
504
+ if (heartbeat) clearInterval(heartbeat);
505
+ process.stdout.write("\r \r");
506
+ handleDeployResponse(res, envLabel);
507
+ }
508
+ function handleDeployResponse(response, envLabel) {
509
+ if (response.status >= 200 && response.status < 300) {
510
+ success(`${envLabel} \uBC30\uD3EC \uC644\uB8CC!`);
511
+ const data = response.data;
512
+ if (data?.dev_url) {
513
+ log(`
305
514
  ${colors.yellow}Dev URL: ${data.dev_url}${colors.reset}
306
515
  `);
307
- } else if (data?.url) {
308
- log(`
516
+ } else if (data?.url) {
517
+ log(`
309
518
  ${colors.cyan}URL: ${data.url}${colors.reset}
310
519
  `);
311
- }
312
- } else {
313
- const data = response.data;
314
- const errorMsg = typeof data === "object" && data !== null ? data.message || data.error || JSON.stringify(data) : typeof data === "string" ? data : `HTTP ${response.status}`;
315
- error(`\uBC30\uD3EC \uC2E4\uD328: ${errorMsg}`);
316
- process.exit(1);
317
520
  }
318
- } catch (err) {
319
- if (heartbeat) clearInterval(heartbeat);
320
- process.stdout.write("\r \r");
321
- error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
521
+ } else {
522
+ const data = response.data;
523
+ const errorMsg = typeof data === "object" && data !== null ? data.message || data.error || JSON.stringify(data) : typeof data === "string" ? data : `HTTP ${response.status}`;
524
+ error(`\uBC30\uD3EC \uC2E4\uD328: ${errorMsg}`);
322
525
  process.exit(1);
323
526
  }
324
527
  }
@@ -334,77 +537,329 @@ function prompt(question) {
334
537
  });
335
538
  });
336
539
  }
540
+ function promptSecret(question) {
541
+ return new Promise((resolve2) => {
542
+ process.stdout.write(question);
543
+ const input = [];
544
+ if (!process.stdin.isTTY) {
545
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
546
+ rl.question("", (answer) => {
547
+ rl.close();
548
+ resolve2(answer.trim());
549
+ });
550
+ return;
551
+ }
552
+ process.stdin.setRawMode(true);
553
+ process.stdin.resume();
554
+ process.stdin.setEncoding("utf-8");
555
+ const onData = (data) => {
556
+ for (const char of data) {
557
+ const code = char.charCodeAt(0);
558
+ if (char === "\r" || char === "\n") {
559
+ process.stdin.setRawMode(false);
560
+ process.stdin.pause();
561
+ process.stdin.removeListener("data", onData);
562
+ process.stdout.write("\n");
563
+ resolve2(input.join("").trim());
564
+ return;
565
+ } else if (code === 3) {
566
+ process.stdin.setRawMode(false);
567
+ process.stdin.pause();
568
+ process.stdout.write("\n");
569
+ process.exit(0);
570
+ } else if (code === 127 || code === 8) {
571
+ if (input.length > 0) {
572
+ input.pop();
573
+ process.stdout.write("\b \b");
574
+ }
575
+ } else if (code >= 32) {
576
+ input.push(char);
577
+ process.stdout.write("*");
578
+ }
579
+ }
580
+ };
581
+ process.stdin.on("data", onData);
582
+ });
583
+ }
584
+ function getProjectRoot() {
585
+ const gitRoot = getGitRoot();
586
+ return gitRoot || process.cwd();
587
+ }
588
+ var CONSOLE_URL = DEFAULT_BASE_URL.replace("api.", "");
589
+ function openBrowser(url) {
590
+ const { exec } = require("child_process");
591
+ const platform2 = process.platform;
592
+ let command;
593
+ if (platform2 === "darwin") {
594
+ command = `open "${url}"`;
595
+ } else if (platform2 === "win32") {
596
+ command = `start "" "${url}"`;
597
+ } else {
598
+ command = `xdg-open "${url}"`;
599
+ }
600
+ exec(command, () => {
601
+ });
602
+ }
603
+ async function browserAuthFlow() {
604
+ info("\uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D \uC138\uC158\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4...");
605
+ const startRes = await makeRequest(
606
+ `${DEFAULT_BASE_URL}/v1/public/cli-auth/start`,
607
+ "POST",
608
+ {}
609
+ );
610
+ if (startRes.status !== 200) {
611
+ const errData = startRes.data;
612
+ const detail = errData?.error || errData?.message || `HTTP ${startRes.status}`;
613
+ error(`\uC778\uC99D \uC138\uC158 \uC2DC\uC791\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4: ${detail}`);
614
+ process.exit(1);
615
+ }
616
+ const { session_id } = startRes.data;
617
+ const authUrl = `${CONSOLE_URL}/auth/cli-auth?session=${session_id}`;
618
+ log(`
619
+ ${colors.cyan}\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB85C\uADF8\uC778\uD558\uC138\uC694:${colors.reset}`);
620
+ log(`${colors.dim}${authUrl}${colors.reset}
621
+ `);
622
+ openBrowser(authUrl);
623
+ const pollInterval = 3e3;
624
+ const maxAttempts = 100;
625
+ let attempts = 0;
626
+ let consecutive404 = 0;
627
+ const max404Retries = 5;
628
+ const spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
629
+ while (attempts < maxAttempts) {
630
+ const frame = spinnerFrames[attempts % spinnerFrames.length];
631
+ process.stdout.write(`\r${colors.blue}${frame}${colors.reset} \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC2B9\uC778 \uB300\uAE30 \uC911...`);
632
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
633
+ try {
634
+ const pollRes = await makeRequest(
635
+ `${DEFAULT_BASE_URL}/v1/public/cli-auth/poll/${session_id}`,
636
+ "GET",
637
+ {}
638
+ );
639
+ if (pollRes.status === 200) {
640
+ consecutive404 = 0;
641
+ const data = pollRes.data;
642
+ if (data.status === "approved" && data.secret_key) {
643
+ process.stdout.write("\r \r");
644
+ success("\uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!");
645
+ return data.secret_key;
646
+ }
647
+ if (data.status === "expired") {
648
+ process.stdout.write("\r \r");
649
+ error("\uC778\uC99D \uC138\uC158\uC774 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.");
650
+ process.exit(1);
651
+ }
652
+ if (data.status === "denied") {
653
+ process.stdout.write("\r \r");
654
+ error("\uC778\uC99D\uC774 \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
655
+ process.exit(1);
656
+ }
657
+ }
658
+ if (pollRes.status === 404) {
659
+ consecutive404++;
660
+ if (consecutive404 >= max404Retries) {
661
+ process.stdout.write("\r \r");
662
+ error("\uC778\uC99D \uC138\uC158\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.");
663
+ process.exit(1);
664
+ }
665
+ }
666
+ } catch {
667
+ }
668
+ attempts++;
669
+ }
670
+ process.stdout.write("\r \r");
671
+ error("\uC778\uC99D \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.");
672
+ process.exit(1);
673
+ }
337
674
  async function init() {
338
675
  log(`
339
676
  ${colors.cyan}Connect Base \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654${colors.reset}
340
677
  `);
341
- const rcPath = path.join(process.cwd(), ".connectbaserc");
342
- if (fs.existsSync(rcPath)) {
678
+ const cwd = process.cwd();
679
+ const projectRoot = getProjectRoot();
680
+ if (cwd !== projectRoot) {
681
+ info(`\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 \uAC10\uC9C0: ${projectRoot}`);
682
+ info(`MCP/\uBB38\uC11C\uB294 \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC5D0, \uBC30\uD3EC \uC124\uC815\uC740 \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uC5D0 \uC0DD\uC131\uB429\uB2C8\uB2E4
683
+ `);
684
+ }
685
+ const rcPath = path2.join(cwd, ".connectbaserc");
686
+ if (fs2.existsSync(rcPath)) {
343
687
  const overwrite = await prompt(`${colors.yellow}\u26A0${colors.reset} .connectbaserc\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uB36E\uC5B4\uC4F0\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? (y/N): `);
344
688
  if (overwrite.toLowerCase() !== "y") {
345
689
  info("\uCD08\uAE30\uD654\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
346
690
  return;
347
691
  }
348
692
  }
349
- log(`${colors.dim}Public Key (cb_pk_): SDK\uC6A9 \u2014 \uC6F9/\uC571\uC5D0\uC11C \uC0AC\uC6A9${colors.reset}`);
350
- log(`${colors.dim}Secret Key (cb_sk_): MCP/\uAD00\uB9AC\uC790\uC6A9 \u2014 \uC808\uB300 \uB178\uCD9C \uAE08\uC9C0${colors.reset}
693
+ log(`
694
+ ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${colors.reset}`);
695
+ log(` ${colors.cyan}1${colors.reset}) Secret Key \uC9C1\uC811 \uC785\uB825 (cb_sk_...)`);
696
+ log(` ${colors.cyan}2${colors.reset}) \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC73C\uB85C \uC790\uB3D9 \uBC1C\uAE09
351
697
  `);
352
- const apiKey = await prompt(`${colors.blue}?${colors.reset} API Key (Public Key \uAD8C\uC7A5): `);
353
- if (!apiKey) {
354
- error("API Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
355
- process.exit(1);
356
- }
357
- if (apiKey.startsWith("cb_sk_")) {
358
- warn("Secret Key\uAC00 \uC785\uB825\uB418\uC5C8\uC2B5\uB2C8\uB2E4. SDK\uC5D0\uB294 Public Key (cb_pk_) \uC0AC\uC6A9\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4.");
359
- warn("Secret Key\uB294 \uC804\uCCB4 \uAD8C\uD55C\uC744 \uAC00\uC9C0\uBBC0\uB85C \uCF54\uB4DC\uC5D0 \uB178\uCD9C\uB418\uC9C0 \uC54A\uB3C4\uB85D \uC8FC\uC758\uD558\uC138\uC694.");
698
+ const authChoice = await prompt(`${colors.blue}?${colors.reset} \uC120\uD0DD (1/2): `);
699
+ let secretKey = "";
700
+ if (authChoice === "2") {
701
+ try {
702
+ secretKey = await browserAuthFlow();
703
+ } catch (err) {
704
+ error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
705
+ info("\uC778\uD130\uB137 \uC5F0\uACB0\uC744 \uD655\uC778\uD558\uAC70\uB098, Secret Key \uC9C1\uC811 \uC785\uB825(\uC635\uC158 1)\uC744 \uC2DC\uB3C4\uD558\uC138\uC694");
706
+ process.exit(1);
707
+ }
708
+ } else {
709
+ log(`${colors.dim}Secret Key (cb_sk_): \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uBC1C\uAE09${colors.reset}
710
+ `);
711
+ secretKey = await promptSecret(`${colors.blue}?${colors.reset} Secret Key: `);
712
+ if (!secretKey) {
713
+ error("Secret Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
714
+ process.exit(1);
715
+ }
716
+ if (secretKey.startsWith("cb_pk_")) {
717
+ error("Public Key\uAC00 \uC544\uB2CC Secret Key(cb_sk_)\uB97C \uC785\uB825\uD558\uC138\uC694");
718
+ info("Secret Key\uB294 \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uC0DD\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
719
+ process.exit(1);
720
+ }
721
+ if (!secretKey.startsWith("cb_sk_")) {
722
+ error("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uD0A4 \uD615\uC2DD\uC785\uB2C8\uB2E4. cb_sk_\uB85C \uC2DC\uC791\uD558\uB294 Secret Key\uB97C \uC785\uB825\uD558\uC138\uC694");
723
+ process.exit(1);
724
+ }
360
725
  }
361
- let storageId = "";
726
+ let appId = "";
727
+ let publicKey = "";
362
728
  try {
363
- info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC870\uD68C \uC911...");
364
- const listRes = await makeRequest(
365
- `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
729
+ info("\uC571 \uBAA9\uB85D \uC870\uD68C \uC911...");
730
+ const appsRes = await makeRequest(
731
+ `${DEFAULT_BASE_URL}/v1/public/cli/apps`,
366
732
  "GET",
367
- { "X-API-Key": apiKey }
733
+ { "X-Public-Key": secretKey }
368
734
  );
369
- if (listRes.status !== 200) {
370
- error(`API Key \uC778\uC99D \uC2E4\uD328 (${listRes.status})`);
735
+ if (appsRes.status === 401) {
736
+ error("Secret Key\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uCF58\uC194\uC5D0\uC11C \uD0A4\uB97C \uD655\uC778\uD558\uC138\uC694");
371
737
  process.exit(1);
372
738
  }
373
- const listData = listRes.data;
374
- const storages = listData.storages || [];
375
- if (storages.length > 0) {
739
+ if (appsRes.status !== 200) {
740
+ throw new Error(`HTTP ${appsRes.status}`);
741
+ }
742
+ const appsData = appsRes.data;
743
+ const apps = appsData.apps || [];
744
+ if (apps.length > 0) {
376
745
  log(`
377
- ${colors.dim}\uAE30\uC874 \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0:${colors.reset}`);
378
- storages.forEach((s, i) => {
379
- log(` ${colors.cyan}${i + 1}${colors.reset}) ${s.name} (${colors.dim}${s.id}${colors.reset})`);
746
+ ${colors.dim}\uB0B4 \uC571 \uBAA9\uB85D:${colors.reset}`);
747
+ apps.forEach((a, i) => {
748
+ const date = a.created_at ? a.created_at.substring(0, 10) : "";
749
+ log(` ${colors.cyan}${i + 1}${colors.reset}) ${a.name} ${colors.dim}(${date})${colors.reset}`);
380
750
  });
381
- log(` ${colors.cyan}0${colors.reset}) \uC0C8\uB85C \uC0DD\uC131`);
751
+ log(` ${colors.cyan}0${colors.reset}) \uC0C8 \uC571 \uB9CC\uB4E4\uAE30`);
382
752
  const choice = await prompt(`
383
753
  ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
384
754
  const num = parseInt(choice, 10);
385
- if (num > 0 && num <= storages.length) {
386
- storageId = storages[num - 1].id;
387
- success(`\uC120\uD0DD\uB428: ${storages[num - 1].name}`);
755
+ if (num > 0 && num <= apps.length) {
756
+ appId = apps[num - 1].id;
757
+ success(`\uC120\uD0DD\uB428: ${apps[num - 1].name}`);
758
+ info("Public Key \uC0DD\uC131 \uC911...");
759
+ const newKeyRes = await makeRequest(
760
+ `${DEFAULT_BASE_URL}/v1/public/cli/apps/${appId}/public-keys`,
761
+ "POST",
762
+ { "X-Public-Key": secretKey },
763
+ JSON.stringify({})
764
+ );
765
+ if (newKeyRes.status === 201) {
766
+ const keyData = newKeyRes.data;
767
+ publicKey = keyData.key;
768
+ success("Public Key \uBC1C\uAE09 \uC644\uB8CC");
769
+ } else {
770
+ warn("Public Key \uC790\uB3D9 \uC0DD\uC131 \uC2E4\uD328. \uC218\uB3D9\uC73C\uB85C \uC0DD\uC131\uD558\uC138\uC694");
771
+ }
388
772
  }
389
773
  }
390
- if (!storageId) {
391
- const projectName = path.basename(process.cwd());
392
- const name = await prompt(`${colors.blue}?${colors.reset} \uC2A4\uD1A0\uB9AC\uC9C0 \uC774\uB984 (${projectName}): `) || projectName;
393
- info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC911...");
774
+ if (!appId) {
775
+ const projectName = path2.basename(cwd);
776
+ const appName = await prompt(`${colors.blue}?${colors.reset} \uC571 \uC774\uB984 (${projectName}): `) || projectName;
777
+ info("\uC571 \uC0DD\uC131 \uC911...");
394
778
  const createRes = await makeRequest(
395
- `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
779
+ `${DEFAULT_BASE_URL}/v1/public/cli/apps`,
396
780
  "POST",
397
- { "X-API-Key": apiKey },
398
- JSON.stringify({ name })
781
+ { "X-Public-Key": secretKey },
782
+ JSON.stringify({ name: appName })
399
783
  );
400
- if (createRes.status !== 200) {
784
+ if (createRes.status === 402) {
785
+ error("\uC571 \uC0DD\uC131 \uD55C\uB3C4 \uCD08\uACFC. \uD50C\uB79C \uC5C5\uADF8\uB808\uC774\uB4DC\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
786
+ process.exit(1);
787
+ }
788
+ if (createRes.status !== 201) {
401
789
  const data = createRes.data;
402
- error(`\uC0DD\uC131 \uC2E4\uD328: ${data?.error || data?.message || `HTTP ${createRes.status}`}`);
790
+ error(`\uC571 \uC0DD\uC131 \uC2E4\uD328: ${data?.error || `HTTP ${createRes.status}`}`);
403
791
  process.exit(1);
404
792
  }
405
793
  const createData = createRes.data;
406
- storageId = createData.storage_web_id;
407
- success(`\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC644\uB8CC: ${createData.name}`);
794
+ appId = createData.app_id;
795
+ publicKey = createData.public_key;
796
+ success(`\uC571 \uC0DD\uC131 \uC644\uB8CC: ${createData.app_name}`);
797
+ if (publicKey) {
798
+ success("Public Key \uC790\uB3D9 \uBC1C\uAE09 \uC644\uB8CC");
799
+ }
800
+ }
801
+ } catch (err) {
802
+ error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
803
+ log(`${colors.dim}\uC218\uB3D9\uC73C\uB85C \uC785\uB825\uD558\uC138\uC694${colors.reset}`);
804
+ appId = await prompt(`${colors.blue}?${colors.reset} App ID: `);
805
+ publicKey = await prompt(`${colors.blue}?${colors.reset} Public Key (cb_pk_): `);
806
+ if (!appId || !publicKey) {
807
+ error("App ID\uC640 Public Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
808
+ process.exit(1);
809
+ }
810
+ }
811
+ if (!publicKey) {
812
+ error("Public Key\uAC00 \uC5C6\uC5B4 \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0\uB97C \uC870\uD68C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uCF58\uC194\uC5D0\uC11C Public Key\uB97C \uBC1C\uAE09\uBC1B\uC544 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694");
813
+ process.exit(1);
814
+ }
815
+ const publicKeyForSdk = publicKey;
816
+ let storageId = "";
817
+ try {
818
+ info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC870\uD68C \uC911...");
819
+ const listRes = await makeRequest(
820
+ `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
821
+ "GET",
822
+ { "X-Public-Key": publicKeyForSdk }
823
+ );
824
+ if (listRes.status === 200) {
825
+ const listData = listRes.data;
826
+ const storages = listData.storages || [];
827
+ if (storages.length > 0) {
828
+ log(`
829
+ ${colors.dim}\uAE30\uC874 \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0:${colors.reset}`);
830
+ storages.forEach((s, i) => {
831
+ log(` ${colors.cyan}${i + 1}${colors.reset}) ${s.name} (${colors.dim}${s.id}${colors.reset})`);
832
+ });
833
+ log(` ${colors.cyan}0${colors.reset}) \uC0C8\uB85C \uC0DD\uC131`);
834
+ const choice = await prompt(`
835
+ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
836
+ const num = parseInt(choice, 10);
837
+ if (num > 0 && num <= storages.length) {
838
+ storageId = storages[num - 1].id;
839
+ success(`\uC120\uD0DD\uB428: ${storages[num - 1].name}`);
840
+ }
841
+ }
842
+ if (!storageId) {
843
+ const projectName = path2.basename(cwd);
844
+ const name = await prompt(`${colors.blue}?${colors.reset} \uC2A4\uD1A0\uB9AC\uC9C0 \uC774\uB984 (${projectName}): `) || projectName;
845
+ info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC911...");
846
+ const createRes = await makeRequest(
847
+ `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
848
+ "POST",
849
+ { "X-Public-Key": publicKeyForSdk },
850
+ JSON.stringify({ name })
851
+ );
852
+ if (createRes.status !== 200) {
853
+ const data = createRes.data;
854
+ error(`\uC0DD\uC131 \uC2E4\uD328: ${data?.error || data?.message || `HTTP ${createRes.status}`}`);
855
+ process.exit(1);
856
+ }
857
+ const createData = createRes.data;
858
+ storageId = createData.storage_web_id;
859
+ success(`\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC644\uB8CC: ${createData.name}`);
860
+ }
861
+ } else {
862
+ throw new Error(`HTTP ${listRes.status}`);
408
863
  }
409
864
  } catch (err) {
410
865
  error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
@@ -418,41 +873,43 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
418
873
  const defaultDir = detectBuildDir();
419
874
  const deployDir = await prompt(`${colors.blue}?${colors.reset} \uBC30\uD3EC \uB514\uB809\uD1A0\uB9AC (${defaultDir}): `) || defaultDir;
420
875
  const config = {
421
- apiKey,
876
+ publicKey: publicKey || "",
422
877
  storageId,
423
878
  deployDir
424
879
  };
425
- fs.writeFileSync(rcPath, JSON.stringify(config, null, 2) + "\n");
880
+ if (secretKey) {
881
+ config.secretKey = secretKey;
882
+ }
883
+ fs2.writeFileSync(rcPath, JSON.stringify(config, null, 2) + "\n");
426
884
  success(".connectbaserc \uC0DD\uC131 \uC644\uB8CC");
427
885
  addToGitignore(".connectbaserc");
428
886
  addDeployScript(deployDir);
429
- const setupClaude = await prompt(`
430
- ${colors.blue}?${colors.reset} Claude Code \uC124\uC815\uC744 \uCD94\uAC00\uD560\uAE4C\uC694? (CLAUDE.md, MCP \uC124\uC815) (Y/n): `);
431
- if (setupClaude.toLowerCase() !== "n") {
432
- await setupClaudeCode(apiKey);
433
- }
887
+ await setupClaudeCode(publicKeyForSdk, secretKey, projectRoot);
434
888
  log(`
435
889
  ${colors.green}\uCD08\uAE30\uD654 \uC644\uB8CC!${colors.reset}
436
890
  `);
437
891
  log(`${colors.dim}\uBC30\uD3EC\uD558\uB824\uBA74:${colors.reset}`);
438
- log(` ${colors.cyan}npm run deploy${colors.reset}
892
+ log(` ${colors.cyan}npm run deploy${colors.reset}`);
893
+ log(`
894
+ ${colors.dim}Claude Code\uC5D0\uC11C MCP\uAC00 \uC790\uB3D9 \uC5F0\uACB0\uB429\uB2C8\uB2E4${colors.reset}
439
895
  `);
440
896
  }
441
897
  function detectBuildDir() {
442
- if (fs.existsSync(path.join(process.cwd(), "dist"))) return "./dist";
443
- if (fs.existsSync(path.join(process.cwd(), "build"))) return "./build";
444
- if (fs.existsSync(path.join(process.cwd(), "out"))) return "./out";
445
- if (fs.existsSync(path.join(process.cwd(), ".next"))) return "./out";
446
- if (fs.existsSync(path.join(process.cwd(), "vite.config.ts")) || fs.existsSync(path.join(process.cwd(), "vite.config.js"))) return "./dist";
447
- if (fs.existsSync(path.join(process.cwd(), "next.config.js")) || fs.existsSync(path.join(process.cwd(), "next.config.mjs"))) return "./out";
898
+ if (fs2.existsSync(path2.join(process.cwd(), "dist"))) return "./dist";
899
+ if (fs2.existsSync(path2.join(process.cwd(), "build"))) return "./build";
900
+ if (fs2.existsSync(path2.join(process.cwd(), "out"))) return "./out";
901
+ if (fs2.existsSync(path2.join(process.cwd(), ".next"))) return "./out";
902
+ if (fs2.existsSync(path2.join(process.cwd(), "vite.config.ts")) || fs2.existsSync(path2.join(process.cwd(), "vite.config.js"))) return "./dist";
903
+ if (fs2.existsSync(path2.join(process.cwd(), "next.config.js")) || fs2.existsSync(path2.join(process.cwd(), "next.config.mjs"))) return "./out";
448
904
  return "./dist";
449
905
  }
450
- function addToGitignore(entry) {
451
- const gitignorePath = path.join(process.cwd(), ".gitignore");
452
- if (fs.existsSync(gitignorePath)) {
453
- const content = fs.readFileSync(gitignorePath, "utf-8");
906
+ function addToGitignore(entry, basePath) {
907
+ const dir = basePath || process.cwd();
908
+ const gitignorePath = path2.join(dir, ".gitignore");
909
+ if (fs2.existsSync(gitignorePath)) {
910
+ const content = fs2.readFileSync(gitignorePath, "utf-8");
454
911
  if (!content.includes(entry)) {
455
- fs.appendFileSync(gitignorePath, `
912
+ fs2.appendFileSync(gitignorePath, `
456
913
  # Connect Base
457
914
  ${entry}
458
915
  `);
@@ -461,28 +918,28 @@ ${entry}
461
918
  info(`.gitignore\uC5D0 \uC774\uBBF8 ${entry}\uAC00 \uD3EC\uD568\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4`);
462
919
  }
463
920
  } else {
464
- fs.writeFileSync(gitignorePath, `# Connect Base
921
+ fs2.writeFileSync(gitignorePath, `# Connect Base
465
922
  ${entry}
466
923
  `);
467
924
  success(".gitignore \uC0DD\uC131 \uC644\uB8CC");
468
925
  }
469
926
  }
470
927
  function addDeployScript(deployDir) {
471
- const pkgPath = path.join(process.cwd(), "package.json");
472
- if (!fs.existsSync(pkgPath)) {
928
+ const pkgPath = path2.join(process.cwd(), "package.json");
929
+ if (!fs2.existsSync(pkgPath)) {
473
930
  warn("package.json\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC5B4 deploy \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uCD94\uAC00\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4");
474
931
  return;
475
932
  }
476
933
  try {
477
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
934
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
478
935
  if (!pkg.scripts) pkg.scripts = {};
479
936
  if (pkg.scripts.deploy) {
480
937
  info(`package.json\uC5D0 \uC774\uBBF8 deploy \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC788\uC2B5\uB2C8\uB2E4: "${pkg.scripts.deploy}"`);
481
938
  return;
482
939
  }
483
- const deployCmd = `connectbase-client deploy ${deployDir}`;
940
+ const deployCmd = `connectbase deploy ${deployDir}`;
484
941
  pkg.scripts.deploy = pkg.scripts.build ? `${pkg.scripts.build} && ${deployCmd}` : deployCmd;
485
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
942
+ fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
486
943
  success(`package.json\uC5D0 deploy \uC2A4\uD06C\uB9BD\uD2B8 \uCD94\uAC00 \uC644\uB8CC`);
487
944
  } catch {
488
945
  warn("package.json \uC218\uC815\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4");
@@ -496,21 +953,58 @@ function getGitRoot() {
496
953
  return null;
497
954
  }
498
955
  }
499
- async function downloadDocs(apiKey, templates, monorepo) {
500
- let baseDir = process.cwd();
501
- if (monorepo) {
502
- const gitRoot = getGitRoot();
503
- if (gitRoot) {
504
- baseDir = gitRoot;
505
- info(`\uBAA8\uB178\uB808\uD3EC \uB8E8\uD2B8 \uAC10\uC9C0: ${gitRoot}`);
506
- } else {
507
- warn("git root\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.");
956
+ function detectMonorepo(gitRoot) {
957
+ const cwd = process.cwd();
958
+ const result = { type: "none", root: gitRoot, isSubPackage: false };
959
+ if (fs2.existsSync(path2.join(gitRoot, "pnpm-workspace.yaml"))) {
960
+ result.type = "pnpm";
961
+ } else if (fs2.existsSync(path2.join(gitRoot, "turbo.json"))) {
962
+ result.type = "turborepo";
963
+ } else if (fs2.existsSync(path2.join(gitRoot, "nx.json"))) {
964
+ result.type = "nx";
965
+ } else if (fs2.existsSync(path2.join(gitRoot, "lerna.json"))) {
966
+ result.type = "lerna";
967
+ } else {
968
+ const rootPkgPath = path2.join(gitRoot, "package.json");
969
+ if (fs2.existsSync(rootPkgPath)) {
970
+ try {
971
+ const pkg = JSON.parse(fs2.readFileSync(rootPkgPath, "utf-8"));
972
+ if (pkg.workspaces) {
973
+ result.type = fs2.existsSync(path2.join(gitRoot, "yarn.lock")) ? "yarn" : "npm";
974
+ }
975
+ } catch {
976
+ }
977
+ }
978
+ }
979
+ if (result.type !== "none" && cwd !== gitRoot) {
980
+ const cwdPkgPath = path2.join(cwd, "package.json");
981
+ if (fs2.existsSync(cwdPkgPath)) {
982
+ result.isSubPackage = true;
983
+ try {
984
+ const pkg = JSON.parse(fs2.readFileSync(cwdPkgPath, "utf-8"));
985
+ result.subPackageName = pkg.name;
986
+ } catch {
987
+ }
988
+ }
989
+ }
990
+ return result;
991
+ }
992
+ async function downloadDocs(publicKey, templates, baseDir) {
993
+ if (!baseDir) {
994
+ baseDir = getProjectRoot();
995
+ }
996
+ const gitRoot = getGitRoot() || baseDir;
997
+ const monorepo = detectMonorepo(gitRoot);
998
+ if (monorepo.type !== "none") {
999
+ info(`\uBAA8\uB178\uB808\uD3EC \uAC10\uC9C0: ${monorepo.type} (\uB8E8\uD2B8: ${gitRoot})`);
1000
+ if (monorepo.isSubPackage) {
1001
+ info(`\uD604\uC7AC \uC11C\uBE0C \uD328\uD0A4\uC9C0: ${monorepo.subPackageName || path2.basename(process.cwd())}`);
508
1002
  }
509
1003
  }
510
- const docsDir = path.join(baseDir, ".claude", "docs");
511
- const rootClaudeMd = path.join(baseDir, "CLAUDE.md");
512
- if (!fs.existsSync(docsDir)) {
513
- fs.mkdirSync(docsDir, { recursive: true });
1004
+ const docsDir = path2.join(gitRoot, ".claude", "docs");
1005
+ const rootClaudeMd = path2.join(gitRoot, "CLAUDE.md");
1006
+ if (!fs2.existsSync(docsDir)) {
1007
+ fs2.mkdirSync(docsDir, { recursive: true });
514
1008
  }
515
1009
  if (!templates) {
516
1010
  templates = ["rules"];
@@ -519,7 +1013,7 @@ async function downloadDocs(apiKey, templates, monorepo) {
519
1013
  try {
520
1014
  const templateParam = templates.join(",");
521
1015
  const res = await makeRequest(
522
- `${DEFAULT_BASE_URL}/v1/storages/webs/claude-md?template=${encodeURIComponent(templateParam)}&format=sections&api_key=${encodeURIComponent(apiKey)}`,
1016
+ `${DEFAULT_BASE_URL}/v1/storages/webs/claude-md?template=${encodeURIComponent(templateParam)}&format=sections&public_key=${encodeURIComponent(publicKey)}`,
523
1017
  "GET",
524
1018
  {}
525
1019
  );
@@ -529,11 +1023,15 @@ async function downloadDocs(apiKey, templates, monorepo) {
529
1023
  const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
530
1024
  const sections = data.sections || [];
531
1025
  for (const section of sections) {
532
- const filePath = path.join(docsDir, section.filename);
533
- fs.writeFileSync(filePath, section.content);
1026
+ const filePath = path2.join(docsDir, section.filename);
1027
+ fs2.writeFileSync(filePath, section.content);
534
1028
  }
535
- success(`.claude/docs/ \uC5D0 ${sections.length}\uAC1C \uBB38\uC11C \uC800\uC7A5 \uC644\uB8CC`);
1029
+ success(`${gitRoot}/.claude/docs/ \uC5D0 ${sections.length}\uAC1C \uBB38\uC11C \uC800\uC7A5 \uC644\uB8CC`);
536
1030
  updateRootClaudeMd(rootClaudeMd);
1031
+ if (monorepo.isSubPackage) {
1032
+ const subClaudeMd = path2.join(process.cwd(), "CLAUDE.md");
1033
+ createSubPackageClaudeMd(subClaudeMd, gitRoot);
1034
+ }
537
1035
  log(`${colors.dim}\u203B SDK \uBB38\uC11C\uB294 MCP search_sdk_docs\uB85C \uAC80\uC0C9\uD558\uC138\uC694${colors.reset}`);
538
1036
  } catch {
539
1037
  warn("SDK \uAC00\uC774\uB4DC \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328, \uAE30\uBCF8 \uBB38\uC11C\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4");
@@ -543,8 +1041,12 @@ async function downloadDocs(apiKey, templates, monorepo) {
543
1041
 
544
1042
  \uC0C1\uC138 \uAD6C\uD604\uBC95\uC740 MCP \`search_sdk_docs\` \uB3C4\uAD6C\uB85C \uAC80\uC0C9\uD558\uC138\uC694.
545
1043
  `;
546
- fs.writeFileSync(path.join(docsDir, "project-rules.md"), fallbackContent);
1044
+ fs2.writeFileSync(path2.join(docsDir, "project-rules.md"), fallbackContent);
547
1045
  updateRootClaudeMd(rootClaudeMd);
1046
+ if (monorepo.isSubPackage) {
1047
+ const subClaudeMd = path2.join(process.cwd(), "CLAUDE.md");
1048
+ createSubPackageClaudeMd(subClaudeMd, gitRoot);
1049
+ }
548
1050
  success(`.claude/docs/ \uC0DD\uC131 \uC644\uB8CC (\uAE30\uBCF8)`);
549
1051
  }
550
1052
  }
@@ -555,93 +1057,401 @@ function updateRootClaudeMd(claudeMdPath) {
555
1057
  ## ConnectBase SDK
556
1058
 
557
1059
  \uC774 \uD504\uB85C\uC81D\uD2B8\uB294 **ConnectBase**\uB97C \uBC31\uC5D4\uB4DC\uB85C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.
558
- \uC0AC\uC6A9\uC790\uB294 **\uBE44\uAC1C\uBC1C\uC790**\uC785\uB2C8\uB2E4. \uC26C\uC6B4 \uB9D0\uB85C \uC124\uBA85\uD558\uACE0, \uBC14\uB85C \uC2E4\uD589 \uAC00\uB2A5\uD55C \uC644\uC131\uB41C \uCF54\uB4DC\uB97C \uC81C\uACF5\uD558\uC138\uC694.
559
1060
 
560
1061
  ### AI \uD544\uB3C5 \u2014 \uCF54\uB529 \uC804 \uBC18\uB4DC\uC2DC \uC218\uD589
561
1062
 
562
- 1. \`.claude/docs/project-rules.md\` \uD30C\uC77C\uC744 **Read tool\uB85C \uC77D\uC73C\uC138\uC694** (\uAE30\uB2A5 \uB9E4\uD551\uD45C, \uBCF4\uC548 \uADDC\uCE59 \uD3EC\uD568)
563
- 2. \uC0AC\uC6A9\uC790 \uC694\uCCAD\uC5D0 \uB9DE\uB294 \uAE30\uB2A5\uC744 \uB9E4\uD551\uD45C\uC5D0\uC11C \uCC3E\uC73C\uC138\uC694
564
- 3. \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
565
- 4. \uC644\uC131\uB41C \uCF54\uB4DC\uB97C \uC791\uC131\uD558\uC138\uC694 (\uC2A4\uB2C8\uD3AB X, \uBC14\uB85C \uC2E4\uD589 \uAC00\uB2A5\uD55C \uC804\uCCB4 \uCF54\uB4DC)
566
-
567
- ### \uBE44\uAC1C\uBC1C\uC790 \uC751\uB300 \uC6D0\uCE59
568
-
569
- - \uC804\uBB38 \uC6A9\uC5B4 \uB300\uC2E0 \uC26C\uC6B4 \uB9D0 \uC0AC\uC6A9 ("API" \u2192 "\uAE30\uB2A5", "DB" \u2192 "\uC800\uC7A5\uC18C")
570
- - \uC5D0\uB7EC \uBC1C\uC0DD \uC2DC \uC6D0\uC778\uACFC \uD574\uACB0\uCC45\uC744 \uD55C \uC904\uB85C \uC124\uBA85
571
- - \uCF54\uB4DC \uC791\uC131 \uC804 "\uC774\uB807\uAC8C \uB9CC\uB4E4\uAC8C\uC694~" \uD55C \uC904 \uC694\uC57D \uBA3C\uC800
572
- - \uBAA8\uB974\uB294 \uAC8C \uC788\uC73C\uBA74 \uB9CC\uB4E4\uAE30 \uC804\uC5D0 \uBA3C\uC800 \uC9C8\uBB38
1063
+ 1. \`.claude/docs/project-rules.md\`\uB97C **Read tool\uB85C \uC77D\uC73C\uC138\uC694** (\uAE30\uB2A5 \uB9E4\uD551\uD45C, \uBCF4\uC548 \uADDC\uCE59, \uC5D0\uB7EC \uAC00\uC774\uB4DC)
1064
+ 2. \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
1065
+ 3. \uAC80\uC0C9 \uACB0\uACFC\uC758 \uCF54\uB4DC \uD328\uD134\uC744 \uB530\uB77C \uAD6C\uD604\uD558\uC138\uC694
573
1066
  ${endMarker}`;
574
- if (fs.existsSync(claudeMdPath)) {
575
- let content = fs.readFileSync(claudeMdPath, "utf-8");
1067
+ if (fs2.existsSync(claudeMdPath)) {
1068
+ let content = fs2.readFileSync(claudeMdPath, "utf-8");
576
1069
  const startIdx = content.indexOf(startMarker);
577
1070
  const endIdx = content.indexOf(endMarker);
578
1071
  if (startIdx !== -1 && endIdx !== -1) {
579
1072
  content = content.substring(0, startIdx) + sdkBlock + content.substring(endIdx + endMarker.length);
580
1073
  } else {
581
- content = content.trimEnd() + "\n\n" + sdkBlock + "\n";
1074
+ content = sdkBlock + "\n\n" + content.trimStart();
582
1075
  }
583
- fs.writeFileSync(claudeMdPath, content);
1076
+ fs2.writeFileSync(claudeMdPath, content);
584
1077
  info("CLAUDE.md\uC5D0 ConnectBase \uCC38\uC870 \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC");
585
1078
  } else {
586
- fs.writeFileSync(claudeMdPath, `# \uD504\uB85C\uC81D\uD2B8
1079
+ fs2.writeFileSync(claudeMdPath, `# \uD504\uB85C\uC81D\uD2B8
587
1080
 
588
1081
  ${sdkBlock}
589
1082
  `);
590
1083
  success("CLAUDE.md \uC0DD\uC131 \uC644\uB8CC");
591
1084
  }
592
1085
  }
593
- async function setupClaudeCode(apiKey) {
594
- const mcpConfigPath = path.join(process.cwd(), ".mcp.json");
595
- await downloadDocs(apiKey);
596
- const mcpConfig = {
597
- mcpServers: {
598
- "connect-base": {
599
- type: "http",
600
- url: "https://mcp.connectbase.world/mcp",
601
- headers: {
602
- Authorization: "Bearer YOUR_SECRET_KEY_HERE"
1086
+ function createSubPackageClaudeMd(subClaudeMdPath, gitRoot) {
1087
+ const startMarker = "<!-- CONNECTBASE_SUB_START -->";
1088
+ const endMarker = "<!-- CONNECTBASE_SUB_END -->";
1089
+ const relPath = path2.relative(path2.dirname(subClaudeMdPath), gitRoot);
1090
+ const subBlock = `${startMarker}
1091
+ ## ConnectBase SDK
1092
+
1093
+ \uC774 \uD328\uD0A4\uC9C0\uB294 **ConnectBase**\uB97C \uBC31\uC5D4\uB4DC\uB85C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.
1094
+
1095
+ ### AI \uD544\uB3C5
1096
+
1097
+ 1. \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC758 \`${relPath}/.claude/docs/project-rules.md\`\uB97C **Read tool\uB85C \uC77D\uC73C\uC138\uC694**
1098
+ 2. \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C SDK \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
1099
+ 3. \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC758 \`${relPath}/CLAUDE.md\`\uB3C4 \uCC38\uACE0\uD558\uC138\uC694
1100
+ ${endMarker}`;
1101
+ if (fs2.existsSync(subClaudeMdPath)) {
1102
+ let content = fs2.readFileSync(subClaudeMdPath, "utf-8");
1103
+ const startIdx = content.indexOf(startMarker);
1104
+ const endIdx = content.indexOf(endMarker);
1105
+ if (startIdx !== -1 && endIdx !== -1) {
1106
+ content = content.substring(0, startIdx) + subBlock + content.substring(endIdx + endMarker.length);
1107
+ } else if (content.indexOf("<!-- CONNECTBASE_START -->") === -1) {
1108
+ content = content.trimEnd() + "\n\n" + subBlock + "\n";
1109
+ } else {
1110
+ return;
1111
+ }
1112
+ fs2.writeFileSync(subClaudeMdPath, content);
1113
+ } else {
1114
+ fs2.writeFileSync(subClaudeMdPath, `# ${path2.basename(path2.dirname(subClaudeMdPath))}
1115
+
1116
+ ${subBlock}
1117
+ `);
1118
+ }
1119
+ success(`\uC11C\uBE0C \uD328\uD0A4\uC9C0 CLAUDE.md \uC0DD\uC131 \uC644\uB8CC: ${subClaudeMdPath}`);
1120
+ }
1121
+ async function setupMcp(secretKey) {
1122
+ const gitRoot = getGitRoot();
1123
+ const root = gitRoot || process.cwd();
1124
+ const monorepo = detectMonorepo(root);
1125
+ if (monorepo.type !== "none") {
1126
+ info(`\uBAA8\uB178\uB808\uD3EC \uAC10\uC9C0: ${monorepo.type} (\uB8E8\uD2B8: ${root})`);
1127
+ if (monorepo.isSubPackage) {
1128
+ info(`MCP \uC124\uC815\uC740 \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8(${root})\uC5D0 \uC0DD\uC131\uB429\uB2C8\uB2E4`);
1129
+ }
1130
+ }
1131
+ const mcpConfigPath = path2.join(root, ".mcp.json");
1132
+ if (!secretKey) {
1133
+ log(`
1134
+ ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${colors.reset}`);
1135
+ log(` ${colors.cyan}1${colors.reset}) Secret Key \uC9C1\uC811 \uC785\uB825 (cb_sk_...)`);
1136
+ log(` ${colors.cyan}2${colors.reset}) \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC73C\uB85C \uC790\uB3D9 \uBC1C\uAE09
1137
+ `);
1138
+ const authChoice = await prompt(`${colors.blue}?${colors.reset} \uC120\uD0DD (1/2): `);
1139
+ if (authChoice === "2") {
1140
+ try {
1141
+ secretKey = await browserAuthFlow();
1142
+ const skRcPath = path2.join(process.cwd(), ".connectbaserc");
1143
+ let rcData = {};
1144
+ if (fs2.existsSync(skRcPath)) {
1145
+ try {
1146
+ rcData = JSON.parse(fs2.readFileSync(skRcPath, "utf-8"));
1147
+ } catch {
1148
+ }
603
1149
  }
1150
+ rcData.secretKey = secretKey;
1151
+ fs2.writeFileSync(skRcPath, JSON.stringify(rcData, null, 2) + "\n");
1152
+ addToGitignore(".connectbaserc", root);
1153
+ success("Secret Key \uBC1C\uAE09 \uBC0F \uC800\uC7A5 \uC644\uB8CC");
1154
+ } catch (err) {
1155
+ error(`\uC778\uC99D \uC2E4\uD328: ${err instanceof Error ? err.message : err}`);
1156
+ process.exit(1);
604
1157
  }
1158
+ } else {
1159
+ secretKey = await promptSecret(`${colors.blue}?${colors.reset} Secret Key: `);
1160
+ }
1161
+ }
1162
+ const mcpEntry = {
1163
+ type: "http",
1164
+ url: "https://mcp.connectbase.world/mcp",
1165
+ headers: {
1166
+ Authorization: `Bearer ${secretKey || "YOUR_SECRET_KEY_HERE"}`
605
1167
  }
606
1168
  };
607
- fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n");
608
- success(".mcp.json \uC0DD\uC131 \uC644\uB8CC");
609
- addToGitignore(".mcp.json");
610
- warn("MCP \uC11C\uBC84\uB294 Secret Key (cb_sk_)\uB9CC \uD5C8\uC6A9\uD569\uB2C8\uB2E4.");
611
- info(".mcp.json \uD30C\uC77C\uC758 YOUR_SECRET_KEY_HERE\uB97C Secret Key\uB85C \uAD50\uCCB4\uD558\uC138\uC694.");
612
- info("Secret Key\uB294 \uCF58\uC194 > \uC124\uC815 > API Keys\uC5D0\uC11C \uC0DD\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
613
- }
614
- function createWsTextFrame(payload) {
615
- const data = Buffer.from(payload, "utf-8");
616
- const len = data.length;
617
- const maskKey = crypto.randomBytes(4);
618
- let header;
619
- if (len < 126) {
620
- header = Buffer.alloc(2);
621
- header[0] = 129;
622
- header[1] = 128 | len;
623
- } else if (len < 65536) {
624
- header = Buffer.alloc(4);
625
- header[0] = 129;
626
- header[1] = 128 | 126;
627
- header.writeUInt16BE(len, 2);
628
- } else {
629
- header = Buffer.alloc(10);
630
- header[0] = 129;
631
- header[1] = 128 | 127;
632
- header.writeBigUInt64BE(BigInt(len), 2);
1169
+ let mcpConfig = { mcpServers: {} };
1170
+ if (fs2.existsSync(mcpConfigPath)) {
1171
+ try {
1172
+ mcpConfig = JSON.parse(fs2.readFileSync(mcpConfigPath, "utf-8"));
1173
+ if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== "object") {
1174
+ mcpConfig.mcpServers = {};
1175
+ }
1176
+ } catch {
1177
+ const backupPath = mcpConfigPath + ".backup";
1178
+ fs2.copyFileSync(mcpConfigPath, backupPath);
1179
+ warn(`.mcp.json \uD30C\uC2F1 \uC2E4\uD328, \uBC31\uC5C5 \uC0DD\uC131: ${backupPath}`);
1180
+ mcpConfig = { mcpServers: {} };
1181
+ }
633
1182
  }
634
- const masked = Buffer.alloc(data.length);
635
- for (let i = 0; i < data.length; i++) {
636
- masked[i] = data[i] ^ maskKey[i % 4];
1183
+ mcpConfig.mcpServers["connect-base"] = mcpEntry;
1184
+ fs2.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n");
1185
+ success(`${mcpConfigPath} \uC0DD\uC131 \uC644\uB8CC`);
1186
+ addToGitignore(".mcp.json", root);
1187
+ if (secretKey && secretKey !== "YOUR_SECRET_KEY_HERE") {
1188
+ success("MCP \uC11C\uBC84 \uC124\uC815 \uC644\uB8CC (Secret Key \uC801\uC6A9\uB428)");
1189
+ } else {
1190
+ warn("MCP \uC11C\uBC84\uB294 Secret Key (cb_sk_)\uB9CC \uD5C8\uC6A9\uD569\uB2C8\uB2E4.");
1191
+ info(".mcp.json \uD30C\uC77C\uC758 YOUR_SECRET_KEY_HERE\uB97C Secret Key\uB85C \uAD50\uCCB4\uD558\uC138\uC694.");
637
1192
  }
638
- return Buffer.concat([header, maskKey, masked]);
1193
+ log(`
1194
+ ${colors.dim}Claude Code\uC5D0\uC11C ConnectBase MCP \uB3C4\uAD6C\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.${colors.reset}`);
639
1195
  }
640
- function createWsCloseFrame(code) {
641
- const maskKey = crypto.randomBytes(4);
642
- const payload = Buffer.alloc(2);
643
- payload.writeUInt16BE(code, 0);
644
- const masked = Buffer.alloc(2);
1196
+ async function setupClaudeCode(publicKey, secretKey, projectRoot) {
1197
+ const root = projectRoot || getProjectRoot();
1198
+ await downloadDocs(publicKey, void 0, root);
1199
+ await setupMcp(secretKey);
1200
+ }
1201
+ async function fetchLatestVersion(packageName) {
1202
+ try {
1203
+ const res = await makeRequest(
1204
+ `https://registry.npmjs.org/${packageName}/latest`,
1205
+ "GET",
1206
+ {}
1207
+ );
1208
+ if (res.status === 200) {
1209
+ const data = res.data;
1210
+ return data.version || null;
1211
+ }
1212
+ } catch {
1213
+ }
1214
+ return null;
1215
+ }
1216
+ function compareVersions(a, b) {
1217
+ const pa = a.split(".").map(Number);
1218
+ const pb = b.split(".").map(Number);
1219
+ for (let i = 0; i < 3; i++) {
1220
+ const diff = (pa[i] || 0) - (pb[i] || 0);
1221
+ if (diff !== 0) return diff > 0 ? 1 : -1;
1222
+ }
1223
+ return 0;
1224
+ }
1225
+ function findSubPackagesWithConfig(gitRoot) {
1226
+ const results = [];
1227
+ const candidates = ["apps", "packages", "projects", "services", "libs"];
1228
+ for (const candidate of candidates) {
1229
+ const dir = path2.join(gitRoot, candidate);
1230
+ if (!fs2.existsSync(dir)) continue;
1231
+ try {
1232
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
1233
+ for (const entry of entries) {
1234
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
1235
+ const subDir = path2.join(dir, entry.name);
1236
+ if (fs2.existsSync(path2.join(subDir, ".connectbaserc"))) {
1237
+ results.push(subDir);
1238
+ }
1239
+ }
1240
+ } catch {
1241
+ }
1242
+ }
1243
+ try {
1244
+ const entries = fs2.readdirSync(gitRoot, { withFileTypes: true });
1245
+ for (const entry of entries) {
1246
+ if (!entry.isDirectory() || entry.name.startsWith(".") || candidates.includes(entry.name)) continue;
1247
+ if (entry.name === "node_modules") continue;
1248
+ const subDir = path2.join(gitRoot, entry.name);
1249
+ if (fs2.existsSync(path2.join(subDir, ".connectbaserc")) && !results.includes(subDir)) {
1250
+ results.push(subDir);
1251
+ }
1252
+ }
1253
+ } catch {
1254
+ }
1255
+ return results;
1256
+ }
1257
+ async function update(config, opts) {
1258
+ log(`
1259
+ ${colors.cyan}ConnectBase Update${colors.reset}
1260
+ `);
1261
+ const gitRoot = getGitRoot();
1262
+ const projectRoot = gitRoot || process.cwd();
1263
+ const monorepo = detectMonorepo(projectRoot);
1264
+ info("\uCD5C\uC2E0 \uBC84\uC804 \uD655\uC778 \uC911...");
1265
+ const latestVersion = await fetchLatestVersion("connectbase-client");
1266
+ if (latestVersion) {
1267
+ const cmp = compareVersions(VERSION, latestVersion);
1268
+ if (cmp >= 0) {
1269
+ success(`\uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4 (v${VERSION})`);
1270
+ } else {
1271
+ warn(`\uC0C8 \uBC84\uC804\uC774 \uC788\uC2B5\uB2C8\uB2E4: v${VERSION} \u2192 v${colors.green}${latestVersion}${colors.reset}`);
1272
+ log(` ${colors.cyan}npm install connectbase-client@latest${colors.reset}`);
1273
+ log(` ${colors.cyan}pnpm add connectbase-client@latest${colors.reset}`);
1274
+ log("");
1275
+ }
1276
+ } else {
1277
+ warn("npm \uB808\uC9C0\uC2A4\uD2B8\uB9AC\uC5D0\uC11C \uBC84\uC804 \uC815\uBCF4\uB97C \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
1278
+ }
1279
+ if (opts.checkOnly) {
1280
+ return;
1281
+ }
1282
+ if (monorepo.type !== "none") {
1283
+ info(`\uBAA8\uB178\uB808\uD3EC \uAC10\uC9C0: ${monorepo.type} (\uB8E8\uD2B8: ${projectRoot})`);
1284
+ const subPackages = findSubPackagesWithConfig(projectRoot);
1285
+ if (subPackages.length > 0) {
1286
+ info(`ConnectBase\uB97C \uC0AC\uC6A9\uD558\uB294 \uC11C\uBE0C \uD328\uD0A4\uC9C0 ${subPackages.length}\uAC1C \uBC1C\uACAC:`);
1287
+ for (const sp of subPackages) {
1288
+ log(` ${colors.dim}\u2022 ${path2.relative(projectRoot, sp)}${colors.reset}`);
1289
+ }
1290
+ }
1291
+ }
1292
+ if (!opts.skipDocs) {
1293
+ log("");
1294
+ info("SDK \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uC911...");
1295
+ let docsKey = config.publicKey || config.secretKey;
1296
+ if (!docsKey) {
1297
+ const rootRcPath = path2.join(projectRoot, ".connectbaserc");
1298
+ if (fs2.existsSync(rootRcPath)) {
1299
+ try {
1300
+ const rc = JSON.parse(fs2.readFileSync(rootRcPath, "utf-8"));
1301
+ docsKey = rc.publicKey || rc.secretKey;
1302
+ } catch {
1303
+ }
1304
+ }
1305
+ }
1306
+ if (!docsKey && monorepo.type !== "none") {
1307
+ const subPackages = findSubPackagesWithConfig(projectRoot);
1308
+ for (const sp of subPackages) {
1309
+ try {
1310
+ const rc = JSON.parse(fs2.readFileSync(path2.join(sp, ".connectbaserc"), "utf-8"));
1311
+ if (rc.publicKey || rc.secretKey) {
1312
+ docsKey = rc.publicKey || rc.secretKey;
1313
+ break;
1314
+ }
1315
+ } catch {
1316
+ }
1317
+ }
1318
+ }
1319
+ if (docsKey) {
1320
+ await downloadDocs(docsKey, void 0, projectRoot);
1321
+ } else {
1322
+ warn("Public Key\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC5B4 \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4");
1323
+ info(".connectbaserc \uD30C\uC77C\uC774\uB098 --public-key \uC635\uC158\uC73C\uB85C \uD0A4\uB97C \uC9C0\uC815\uD558\uC138\uC694");
1324
+ }
1325
+ }
1326
+ if (!opts.skipMcp) {
1327
+ log("");
1328
+ const mcpConfigPath = path2.join(projectRoot, ".mcp.json");
1329
+ if (fs2.existsSync(mcpConfigPath)) {
1330
+ let secretKey = config.secretKey;
1331
+ if (!secretKey) {
1332
+ const rootRcPath = path2.join(projectRoot, ".connectbaserc");
1333
+ if (fs2.existsSync(rootRcPath)) {
1334
+ try {
1335
+ const rc = JSON.parse(fs2.readFileSync(rootRcPath, "utf-8"));
1336
+ secretKey = rc.secretKey;
1337
+ } catch {
1338
+ }
1339
+ }
1340
+ }
1341
+ if (!secretKey && monorepo.type !== "none") {
1342
+ const subPackages = findSubPackagesWithConfig(projectRoot);
1343
+ for (const sp of subPackages) {
1344
+ try {
1345
+ const rc = JSON.parse(fs2.readFileSync(path2.join(sp, ".connectbaserc"), "utf-8"));
1346
+ if (rc.secretKey) {
1347
+ secretKey = rc.secretKey;
1348
+ break;
1349
+ }
1350
+ } catch {
1351
+ }
1352
+ }
1353
+ }
1354
+ if (secretKey) {
1355
+ try {
1356
+ const mcpConfig = JSON.parse(fs2.readFileSync(mcpConfigPath, "utf-8"));
1357
+ const servers = mcpConfig.mcpServers || {};
1358
+ if (servers["connect-base"]) {
1359
+ servers["connect-base"] = {
1360
+ type: "http",
1361
+ url: "https://mcp.connectbase.world/mcp",
1362
+ headers: { Authorization: `Bearer ${secretKey}` }
1363
+ };
1364
+ mcpConfig.mcpServers = servers;
1365
+ fs2.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n");
1366
+ success("MCP \uC124\uC815 \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC");
1367
+ } else {
1368
+ info('.mcp.json\uC5D0 connect-base \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. "connectbase mcp"\uB85C \uC124\uC815\uD558\uC138\uC694');
1369
+ }
1370
+ } catch {
1371
+ warn(".mcp.json \uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328");
1372
+ }
1373
+ } else {
1374
+ info("Secret Key\uAC00 \uC5C6\uC5B4 MCP \uC124\uC815 \uC5C5\uB370\uC774\uD2B8\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4");
1375
+ }
1376
+ } else {
1377
+ info('.mcp.json\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. "connectbase mcp"\uB85C \uCD5C\uCD08 \uC124\uC815\uD558\uC138\uC694');
1378
+ }
1379
+ }
1380
+ if (opts.setupRoot || monorepo.type !== "none" && monorepo.isSubPackage) {
1381
+ log("");
1382
+ setupMonorepoRoot(projectRoot, monorepo);
1383
+ }
1384
+ log(`
1385
+ ${colors.green}\uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!${colors.reset}
1386
+ `);
1387
+ }
1388
+ function setupMonorepoRoot(projectRoot, monorepo) {
1389
+ const rootPkgPath = path2.join(projectRoot, "package.json");
1390
+ if (!fs2.existsSync(rootPkgPath)) {
1391
+ warn("\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC5D0 package.json\uC774 \uC5C6\uC5B4 \uD3B8\uC758 \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uCD94\uAC00\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
1392
+ return;
1393
+ }
1394
+ try {
1395
+ const pkg = JSON.parse(fs2.readFileSync(rootPkgPath, "utf-8"));
1396
+ if (!pkg.scripts) pkg.scripts = {};
1397
+ let changed = false;
1398
+ if (!pkg.scripts["cb:update"]) {
1399
+ pkg.scripts["cb:update"] = "npx connectbase update";
1400
+ changed = true;
1401
+ }
1402
+ if (!pkg.scripts["cb:docs"]) {
1403
+ pkg.scripts["cb:docs"] = "npx connectbase docs";
1404
+ changed = true;
1405
+ }
1406
+ if (!pkg.scripts["cb:mcp"]) {
1407
+ pkg.scripts["cb:mcp"] = "npx connectbase mcp";
1408
+ changed = true;
1409
+ }
1410
+ if (changed) {
1411
+ fs2.writeFileSync(rootPkgPath, JSON.stringify(pkg, null, 2) + "\n");
1412
+ success("\uBAA8\uB178\uB808\uD3EC \uB8E8\uD2B8\uC5D0 \uD3B8\uC758 \uC2A4\uD06C\uB9BD\uD2B8 \uCD94\uAC00 \uC644\uB8CC");
1413
+ log(` ${colors.dim}\uC0AC\uC6A9\uBC95:${colors.reset}`);
1414
+ log(` ${colors.cyan}${monorepo.type === "pnpm" ? "pnpm" : monorepo.type === "yarn" ? "yarn" : "npm run"} cb:update${colors.reset} \u2014 SDK \uC5C5\uB370\uC774\uD2B8 + \uBB38\uC11C \uAC31\uC2E0`);
1415
+ log(` ${colors.cyan}${monorepo.type === "pnpm" ? "pnpm" : monorepo.type === "yarn" ? "yarn" : "npm run"} cb:docs${colors.reset} \u2014 \uBB38\uC11C\uB9CC \uAC31\uC2E0`);
1416
+ log(` ${colors.cyan}${monorepo.type === "pnpm" ? "pnpm" : monorepo.type === "yarn" ? "yarn" : "npm run"} cb:mcp${colors.reset} \u2014 MCP \uC124\uC815`);
1417
+ } else {
1418
+ info("\uBAA8\uB178\uB808\uD3EC \uB8E8\uD2B8\uC5D0 \uC774\uBBF8 \uD3B8\uC758 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC124\uC815\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4");
1419
+ }
1420
+ } catch {
1421
+ warn("\uB8E8\uD2B8 package.json \uC218\uC815\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4");
1422
+ }
1423
+ }
1424
+ function createWsTextFrame(payload) {
1425
+ const data = Buffer.from(payload, "utf-8");
1426
+ const len = data.length;
1427
+ const maskKey = crypto.randomBytes(4);
1428
+ let header;
1429
+ if (len < 126) {
1430
+ header = Buffer.alloc(2);
1431
+ header[0] = 129;
1432
+ header[1] = 128 | len;
1433
+ } else if (len < 65536) {
1434
+ header = Buffer.alloc(4);
1435
+ header[0] = 129;
1436
+ header[1] = 128 | 126;
1437
+ header.writeUInt16BE(len, 2);
1438
+ } else {
1439
+ header = Buffer.alloc(10);
1440
+ header[0] = 129;
1441
+ header[1] = 128 | 127;
1442
+ header.writeBigUInt64BE(BigInt(len), 2);
1443
+ }
1444
+ const masked = Buffer.alloc(data.length);
1445
+ for (let i = 0; i < data.length; i++) {
1446
+ masked[i] = data[i] ^ maskKey[i % 4];
1447
+ }
1448
+ return Buffer.concat([header, maskKey, masked]);
1449
+ }
1450
+ function createWsCloseFrame(code) {
1451
+ const maskKey = crypto.randomBytes(4);
1452
+ const payload = Buffer.alloc(2);
1453
+ payload.writeUInt16BE(code, 0);
1454
+ const masked = Buffer.alloc(2);
645
1455
  masked[0] = payload[0] ^ maskKey[0];
646
1456
  masked[1] = payload[1] ^ maskKey[1];
647
1457
  const header = Buffer.alloc(2);
@@ -656,10 +1466,159 @@ function createWsPongFrame() {
656
1466
  header[1] = 128 | 0;
657
1467
  return Buffer.concat([header, maskKey]);
658
1468
  }
1469
+ function createWsBinaryFrame(payload) {
1470
+ const len = payload.length;
1471
+ const maskKey = crypto.randomBytes(4);
1472
+ let header;
1473
+ if (len < 126) {
1474
+ header = Buffer.alloc(2);
1475
+ header[0] = 130;
1476
+ header[1] = 128 | len;
1477
+ } else if (len < 65536) {
1478
+ header = Buffer.alloc(4);
1479
+ header[0] = 130;
1480
+ header[1] = 128 | 126;
1481
+ header.writeUInt16BE(len, 2);
1482
+ } else {
1483
+ header = Buffer.alloc(10);
1484
+ header[0] = 130;
1485
+ header[1] = 128 | 127;
1486
+ header.writeBigUInt64BE(BigInt(len), 2);
1487
+ }
1488
+ const masked = Buffer.alloc(len);
1489
+ for (let i = 0; i < len; i++) {
1490
+ masked[i] = payload[i] ^ maskKey[i % 4];
1491
+ }
1492
+ return Buffer.concat([header, maskKey, masked]);
1493
+ }
1494
+ var UpstreamWsFrameParser = class {
1495
+ constructor(handlers) {
1496
+ this.buffer = Buffer.alloc(0);
1497
+ this.fragmentBuffer = null;
1498
+ this.h = handlers;
1499
+ }
1500
+ feed(chunk) {
1501
+ this.buffer = Buffer.concat([this.buffer, chunk]);
1502
+ try {
1503
+ this.parse();
1504
+ } catch (e) {
1505
+ this.h.onError(e instanceof Error ? e : new Error(String(e)));
1506
+ }
1507
+ }
1508
+ parse() {
1509
+ while (this.buffer.length >= 2) {
1510
+ const firstByte = this.buffer[0];
1511
+ const secondByte = this.buffer[1];
1512
+ const fin = (firstByte & 128) !== 0;
1513
+ const rsv1 = (firstByte & 64) !== 0;
1514
+ const opcode = firstByte & 15;
1515
+ const isMasked = (secondByte & 128) !== 0;
1516
+ let payloadLen = secondByte & 127;
1517
+ let offset = 2;
1518
+ if (payloadLen === 126) {
1519
+ if (this.buffer.length < 4) return;
1520
+ payloadLen = this.buffer.readUInt16BE(2);
1521
+ offset = 4;
1522
+ } else if (payloadLen === 127) {
1523
+ if (this.buffer.length < 10) return;
1524
+ const big = this.buffer.readBigUInt64BE(2);
1525
+ if (big > BigInt(Number.MAX_SAFE_INTEGER)) {
1526
+ throw new Error(`payload too large: ${big}`);
1527
+ }
1528
+ payloadLen = Number(big);
1529
+ offset = 10;
1530
+ }
1531
+ let maskKey = null;
1532
+ if (isMasked) {
1533
+ if (this.buffer.length < offset + 4) return;
1534
+ maskKey = this.buffer.subarray(offset, offset + 4);
1535
+ offset += 4;
1536
+ }
1537
+ if (this.buffer.length < offset + payloadLen) return;
1538
+ if (rsv1 && (opcode === 1 || opcode === 2 || opcode === 0)) {
1539
+ throw new Error("upstream sent compressed (RSV1) frame without negotiation");
1540
+ }
1541
+ let payload = this.buffer.subarray(offset, offset + payloadLen);
1542
+ if (maskKey) {
1543
+ const unmasked = Buffer.alloc(payloadLen);
1544
+ for (let i = 0; i < payloadLen; i++) {
1545
+ unmasked[i] = payload[i] ^ maskKey[i % 4];
1546
+ }
1547
+ payload = unmasked;
1548
+ }
1549
+ this.buffer = this.buffer.subarray(offset + payloadLen);
1550
+ const payloadCopy = Buffer.from(payload);
1551
+ switch (opcode) {
1552
+ case 0:
1553
+ if (this.fragmentBuffer == null) {
1554
+ throw new Error("continuation frame without preceding non-final data frame");
1555
+ }
1556
+ this.fragmentBuffer = Buffer.concat([this.fragmentBuffer, payloadCopy]);
1557
+ if (fin) {
1558
+ const out = this.fragmentBuffer;
1559
+ this.fragmentBuffer = null;
1560
+ this.h.onPayload(out);
1561
+ }
1562
+ break;
1563
+ case 1:
1564
+ // TEXT
1565
+ case 2:
1566
+ if (fin) {
1567
+ this.h.onPayload(payloadCopy);
1568
+ } else {
1569
+ this.fragmentBuffer = payloadCopy;
1570
+ }
1571
+ break;
1572
+ case 8:
1573
+ this.h.onClose();
1574
+ break;
1575
+ case 9:
1576
+ this.h.onPing(payloadCopy);
1577
+ break;
1578
+ case 10:
1579
+ break;
1580
+ default:
1581
+ throw new Error(`unknown WS opcode 0x${opcode.toString(16)}`);
1582
+ }
1583
+ }
1584
+ }
1585
+ };
1586
+ function createUpstreamTextFrame(payload) {
1587
+ return buildClientFrame(129, payload);
1588
+ }
1589
+ function createUpstreamPongFrame(payload) {
1590
+ return buildClientFrame(138, payload);
1591
+ }
1592
+ function buildClientFrame(firstByte, payload) {
1593
+ const len = payload.length;
1594
+ const maskKey = crypto.randomBytes(4);
1595
+ let header;
1596
+ if (len < 126) {
1597
+ header = Buffer.alloc(2);
1598
+ header[0] = firstByte;
1599
+ header[1] = 128 | len;
1600
+ } else if (len < 65536) {
1601
+ header = Buffer.alloc(4);
1602
+ header[0] = firstByte;
1603
+ header[1] = 128 | 126;
1604
+ header.writeUInt16BE(len, 2);
1605
+ } else {
1606
+ header = Buffer.alloc(10);
1607
+ header[0] = firstByte;
1608
+ header[1] = 128 | 127;
1609
+ header.writeBigUInt64BE(BigInt(len), 2);
1610
+ }
1611
+ const masked = Buffer.alloc(len);
1612
+ for (let i = 0; i < len; i++) {
1613
+ masked[i] = payload[i] ^ maskKey[i % 4];
1614
+ }
1615
+ return Buffer.concat([header, maskKey, masked]);
1616
+ }
659
1617
  var WsFrameParser = class {
660
1618
  constructor(handlers) {
661
1619
  this.buffer = Buffer.alloc(0);
662
1620
  this.onMessage = handlers.onMessage;
1621
+ this.onBinary = handlers.onBinary;
663
1622
  this.onClose = handlers.onClose;
664
1623
  this.onPing = handlers.onPing;
665
1624
  }
@@ -704,6 +1663,13 @@ var WsFrameParser = class {
704
1663
  case 1:
705
1664
  this.onMessage(payload.toString("utf-8"));
706
1665
  break;
1666
+ case 2:
1667
+ if (this.onBinary) {
1668
+ const copy = Buffer.alloc(payload.length);
1669
+ payload.copy(copy);
1670
+ this.onBinary(copy);
1671
+ }
1672
+ break;
707
1673
  case 8:
708
1674
  this.onClose();
709
1675
  break;
@@ -725,27 +1691,195 @@ function getTunnelServerUrl(baseUrl) {
725
1691
  }
726
1692
  return baseUrl.replace(/:\d+/, ":8090");
727
1693
  }
728
- async function startTunnel(port, config, tunnelOpts) {
729
- if (!config.apiKey) {
730
- error("API Key\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. -k \uC635\uC158 \uB610\uB294 CONNECTBASE_API_KEY \uD658\uACBD\uBCC0\uC218\uB97C \uC124\uC815\uD558\uC138\uC694");
1694
+ async function resolveApp(secretKey, baseUrl, appIdOption) {
1695
+ if (appIdOption) {
1696
+ return { appId: appIdOption };
1697
+ }
1698
+ let apps = [];
1699
+ try {
1700
+ info("\uC571 \uBAA9\uB85D \uC870\uD68C \uC911...");
1701
+ const appsRes = await makeRequest(
1702
+ `${baseUrl}/v1/public/cli/apps`,
1703
+ "GET",
1704
+ { "X-Public-Key": secretKey }
1705
+ );
1706
+ if (appsRes.status === 401) {
1707
+ error("Secret Key\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uCF58\uC194\uC5D0\uC11C \uD0A4\uB97C \uD655\uC778\uD558\uC138\uC694");
1708
+ process.exit(1);
1709
+ }
1710
+ if (appsRes.status !== 200) {
1711
+ throw new Error(`HTTP ${appsRes.status}`);
1712
+ }
1713
+ const appsData = appsRes.data;
1714
+ apps = appsData.apps || [];
1715
+ } catch (err) {
1716
+ error(`\uC571 \uBAA9\uB85D \uC870\uD68C \uC2E4\uD328: ${err instanceof Error ? err.message : err}`);
731
1717
  process.exit(1);
732
1718
  }
1719
+ if (apps.length === 1) {
1720
+ success(`\uC571 \uC790\uB3D9 \uC120\uD0DD: ${apps[0].name}`);
1721
+ return { appId: apps[0].id };
1722
+ }
1723
+ if (apps.length > 0) {
1724
+ log(`
1725
+ ${colors.dim}\uB0B4 \uC571 \uBAA9\uB85D:${colors.reset}`);
1726
+ apps.forEach((a, i) => {
1727
+ const date = a.created_at ? a.created_at.substring(0, 10) : "";
1728
+ log(` ${colors.cyan}${i + 1}${colors.reset}) ${a.name} ${colors.dim}(${date})${colors.reset}`);
1729
+ });
1730
+ }
1731
+ log(` ${colors.cyan}0${colors.reset}) \uC0C8 \uC571 \uB9CC\uB4E4\uAE30`);
1732
+ const choice = await prompt(`
1733
+ ${colors.blue}?${colors.reset} \uC571 \uC120\uD0DD (\uBC88\uD638): `);
1734
+ const num = parseInt(choice, 10);
1735
+ if (num > 0 && num <= apps.length) {
1736
+ success(`\uC120\uD0DD\uB428: ${apps[num - 1].name}`);
1737
+ return { appId: apps[num - 1].id };
1738
+ }
1739
+ const projectName = path2.basename(process.cwd());
1740
+ const appName = await prompt(`${colors.blue}?${colors.reset} \uC571 \uC774\uB984 (${projectName}): `) || projectName;
1741
+ info("\uC571 \uC0DD\uC131 \uC911...");
1742
+ const createRes = await makeRequest(
1743
+ `${baseUrl}/v1/public/cli/apps`,
1744
+ "POST",
1745
+ { "X-Public-Key": secretKey },
1746
+ JSON.stringify({ name: appName })
1747
+ );
1748
+ if (createRes.status === 402) {
1749
+ error("\uC571 \uC0DD\uC131 \uD55C\uB3C4 \uCD08\uACFC. \uD50C\uB79C \uC5C5\uADF8\uB808\uC774\uB4DC\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
1750
+ process.exit(1);
1751
+ }
1752
+ if (createRes.status !== 201) {
1753
+ const data = createRes.data;
1754
+ error(`\uC571 \uC0DD\uC131 \uC2E4\uD328: ${data?.error || `HTTP ${createRes.status}`}`);
1755
+ process.exit(1);
1756
+ }
1757
+ const createData = createRes.data;
1758
+ success(`\uC571 \uC0DD\uC131 \uC644\uB8CC: ${createData.app_name}`);
1759
+ return { appId: createData.app_id, publicKey: createData.public_key };
1760
+ }
1761
+ async function registerEndpointBinding(baseUrl, appId, secretKey, tunnelId, label, description) {
1762
+ try {
1763
+ const apiBase = baseUrl.replace(/\/+$/, "");
1764
+ const res = await fetch(`${apiBase}/v1/apps/${encodeURIComponent(appId)}/endpoints/cli`, {
1765
+ method: "POST",
1766
+ headers: {
1767
+ "Content-Type": "application/json",
1768
+ "X-Public-Key": secretKey
1769
+ },
1770
+ body: JSON.stringify({
1771
+ label,
1772
+ tunnel_id: tunnelId,
1773
+ description: description ?? `CLI tunnel start (${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)})`
1774
+ })
1775
+ });
1776
+ if (res.status === 201) {
1777
+ success(`Endpoint "${label}" \uC790\uB3D9 \uB4F1\uB85D \uC644\uB8CC`);
1778
+ log(`${colors.green}\u2192${colors.reset} SDK: ${colors.cyan}cb.endpoint.call("${label}", { path: "/...", method: "POST", body: ... })${colors.reset}`);
1779
+ } else if (res.status === 409) {
1780
+ warn(`"${label}" \uB77C\uBCA8\uC774 \uC774\uBBF8 \uB4F1\uB85D\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC0C8 tunnel_id \uB85C \uAC31\uC2E0\uD558\uB824\uBA74 \uCF58\uC194\uC5D0\uC11C \uC218\uB3D9 PATCH.`);
1781
+ } else {
1782
+ const text = await res.text().catch(() => "");
1783
+ error(`Endpoint "${label}" \uC790\uB3D9 \uB4F1\uB85D \uC2E4\uD328 (status ${res.status}): ${text || "(\uBE48 \uC751\uB2F5)"}`);
1784
+ if (res.status === 401) {
1785
+ info("cb_sk_ Secret Key \uC778\uC9C0, \uCF58\uC194\uC5D0\uC11C \uD68C\uC218\uB418\uC9C0 \uC54A\uC558\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.");
1786
+ }
1787
+ }
1788
+ } catch (err) {
1789
+ error(`Endpoint "${label}" \uC790\uB3D9 \uB4F1\uB85D \uC911 \uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
1790
+ }
1791
+ }
1792
+ function acquireTunnelLock2(appID, port, force) {
1793
+ const result = acquireTunnelLock(appID, port, force, VERSION);
1794
+ if (result && result.startsWith("LOCKED:")) {
1795
+ const [, pid, startedAt, host] = result.split(":");
1796
+ error(`\uC774\uBBF8 \uC2E4\uD589 \uC911\uC778 \uD130\uB110\uC774 \uC788\uC2B5\uB2C8\uB2E4: PID ${pid}, \uC2DC\uC791 ${startedAt}, \uD638\uC2A4\uD2B8 ${host}`);
1797
+ info("\uC911\uBCF5 \uC2E4\uD589\uC744 \uBB34\uC2DC\uD558\uB824\uBA74 --force \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uC138\uC694");
1798
+ process.exit(2);
1799
+ }
1800
+ return result;
1801
+ }
1802
+ async function startTunnel(port, config, tunnelOpts) {
1803
+ let tunnelKey = config.secretKey || (config.publicKey?.startsWith("cb_sk_") ? config.publicKey : "");
1804
+ if (!tunnelKey) {
1805
+ info("Secret Key\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC73C\uB85C \uBC1C\uAE09\uD569\uB2C8\uB2E4...");
1806
+ try {
1807
+ const sk = await browserAuthFlow();
1808
+ const rcPath2 = path2.join(process.cwd(), ".connectbaserc");
1809
+ let rcData = {};
1810
+ if (fs2.existsSync(rcPath2)) {
1811
+ try {
1812
+ rcData = JSON.parse(fs2.readFileSync(rcPath2, "utf-8"));
1813
+ } catch {
1814
+ }
1815
+ }
1816
+ rcData.secretKey = sk;
1817
+ fs2.writeFileSync(rcPath2, JSON.stringify(rcData, null, 2) + "\n");
1818
+ addToGitignore(".connectbaserc");
1819
+ success("Secret Key \uBC1C\uAE09 \uBC0F \uC800\uC7A5 \uC644\uB8CC");
1820
+ tunnelKey = sk;
1821
+ config.secretKey = sk;
1822
+ } catch (err) {
1823
+ error(`\uC778\uC99D \uC2E4\uD328: ${err instanceof Error ? err.message : err}`);
1824
+ info("-k \uC635\uC158\uC73C\uB85C Secret Key\uB97C \uC9C1\uC811 \uC804\uB2EC\uD560 \uC218\uB3C4 \uC788\uC2B5\uB2C8\uB2E4");
1825
+ process.exit(1);
1826
+ }
1827
+ } else {
1828
+ config.secretKey = tunnelKey;
1829
+ }
1830
+ const rcPath = path2.join(process.cwd(), ".connectbaserc");
1831
+ let savedAppId = tunnelOpts?.appId || "";
1832
+ if (!savedAppId) {
1833
+ try {
1834
+ const rc = JSON.parse(fs2.readFileSync(rcPath, "utf-8"));
1835
+ if (rc.tunnelAppId) savedAppId = rc.tunnelAppId;
1836
+ } catch {
1837
+ }
1838
+ }
1839
+ const { appId } = await resolveApp(tunnelKey, config.baseUrl, savedAppId);
1840
+ try {
1841
+ let rcData = {};
1842
+ if (fs2.existsSync(rcPath)) {
1843
+ try {
1844
+ rcData = JSON.parse(fs2.readFileSync(rcPath, "utf-8"));
1845
+ } catch {
1846
+ }
1847
+ }
1848
+ if (rcData.tunnelAppId !== appId) {
1849
+ rcData.tunnelAppId = appId;
1850
+ fs2.writeFileSync(rcPath, JSON.stringify(rcData, null, 2) + "\n");
1851
+ }
1852
+ } catch {
1853
+ }
1854
+ const lockPath = acquireTunnelLock2(appId, port, tunnelOpts?.force ?? false);
733
1855
  const tunnelServerUrl = getTunnelServerUrl(config.baseUrl);
734
1856
  const parsedUrl = new URL(tunnelServerUrl);
735
1857
  const isHttps = parsedUrl.protocol === "https:";
736
- let wsPath = `/v1/tunnel/connect?api_key=${encodeURIComponent(config.apiKey)}`;
1858
+ let wsPath = `/v2/tunnel/connect?app_id=${encodeURIComponent(appId)}&local_port=${port}`;
737
1859
  if (tunnelOpts?.timeout) {
738
1860
  wsPath += `&timeout=${tunnelOpts.timeout}`;
739
1861
  }
740
1862
  if (tunnelOpts?.maxBody) {
741
1863
  wsPath += `&max_body=${tunnelOpts.maxBody}`;
742
1864
  }
1865
+ if (tunnelOpts?.public) {
1866
+ wsPath += `&public=1`;
1867
+ }
743
1868
  let reconnectAttempts = 0;
744
1869
  const maxReconnectAttempts = 10;
745
1870
  let shouldReconnect = true;
746
1871
  let socket = null;
1872
+ let lockReleased = false;
1873
+ const releaseLock = () => {
1874
+ if (lockPath && !lockReleased) {
1875
+ lockReleased = true;
1876
+ releaseTunnelLock(lockPath);
1877
+ }
1878
+ };
1879
+ process.on("exit", releaseLock);
747
1880
  const cleanup = () => {
748
1881
  shouldReconnect = false;
1882
+ releaseLock();
749
1883
  if (socket) {
750
1884
  try {
751
1885
  socket.write(createWsCloseFrame(1e3));
@@ -779,7 +1913,8 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
779
1913
  "Upgrade": "websocket",
780
1914
  "Connection": "Upgrade",
781
1915
  "Sec-WebSocket-Key": wsKey,
782
- "Sec-WebSocket-Version": "13"
1916
+ "Sec-WebSocket-Version": "13",
1917
+ "Authorization": `Bearer ${config.secretKey ?? config.publicKey}`
783
1918
  }
784
1919
  };
785
1920
  const req = lib.request(reqOptions);
@@ -790,11 +1925,22 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
790
1925
  onMessage: (data) => {
791
1926
  try {
792
1927
  const msg = JSON.parse(data);
793
- handleMessage(msg, sock, port);
1928
+ handleMessage(msg, sock, port).catch((e) => {
1929
+ error(`\uBA54\uC2DC\uC9C0 \uCC98\uB9AC \uC911 \uC608\uC678: ${e instanceof Error ? e.message : e}`);
1930
+ });
794
1931
  } catch (e) {
795
1932
  warn(`\uBA54\uC2DC\uC9C0 \uD30C\uC2F1 \uC2E4\uD328: ${e}`);
796
1933
  }
797
1934
  },
1935
+ onBinary: (data) => {
1936
+ if (data.length < 8) {
1937
+ warn(`v2 binary frame too short (${data.length} bytes)`);
1938
+ return;
1939
+ }
1940
+ const streamId = data.readBigUInt64BE(0).toString();
1941
+ const payload = data.subarray(8);
1942
+ routeStreamData(streamId, payload);
1943
+ },
798
1944
  onClose: () => {
799
1945
  info("\uC11C\uBC84\uAC00 \uC5F0\uACB0\uC744 \uC885\uB8CC\uD588\uC2B5\uB2C8\uB2E4");
800
1946
  sock.destroy();
@@ -858,95 +2004,314 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
858
2004
  info(`${(delay / 1e3).toFixed(0)}\uCD08 \uD6C4 \uC7AC\uC5F0\uACB0 \uC2DC\uB3C4... (${reconnectAttempts}/${maxReconnectAttempts})`);
859
2005
  setTimeout(connect, delay);
860
2006
  }
861
- function handleMessage(msg, sock, localPort) {
862
- switch (msg.type) {
863
- case "tunnel_ready":
864
- success(`\uD130\uB110 \uD65C\uC131\uD654!`);
865
- log(`${colors.green}\u2192${colors.reset} URL: ${colors.cyan}${msg.url}${colors.reset}`);
866
- log(`${colors.green}\u2192${colors.reset} \uB85C\uCEEC: ${colors.cyan}http://localhost:${localPort}${colors.reset}`);
867
- if (msg.timeout || msg.max_body) {
868
- log(`${colors.green}\u2192${colors.reset} \uC124\uC815: timeout=${colors.cyan}${msg.timeout}s${colors.reset}, max-body=${colors.cyan}${msg.max_body}MB${colors.reset}`);
869
- }
870
- log(`
871
- ${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
872
- `);
873
- break;
874
- case "http_request":
875
- forwardRequest(msg, sock, localPort);
876
- break;
877
- case "tunnel_error":
878
- error(`\uD130\uB110 \uC5D0\uB7EC: ${msg.message}`);
879
- break;
880
- case "ping":
881
- sock.write(createWsTextFrame(JSON.stringify({ type: "pong" })));
882
- break;
2007
+ const streams = /* @__PURE__ */ new Map();
2008
+ function routeStreamData(streamId, payload) {
2009
+ const f = streams.get(streamId);
2010
+ if (!f) return;
2011
+ f.feedBody(payload);
2012
+ }
2013
+ function sendControl(sock, msg) {
2014
+ try {
2015
+ sock.write(createWsTextFrame(JSON.stringify(msg)));
2016
+ } catch {
883
2017
  }
884
2018
  }
885
- function forwardRequest(msg, sock, localPort) {
886
- const requestId = msg.request_id;
887
- const method = msg.method;
888
- const reqPath = msg.path;
889
- const query = msg.query || "";
890
- const headers = msg.headers || {};
891
- const bodyBase64 = msg.body;
2019
+ function sendBinary(sock, streamIdStr, payload) {
2020
+ try {
2021
+ const header = Buffer.alloc(8);
2022
+ header.writeBigUInt64BE(BigInt(streamIdStr), 0);
2023
+ sock.write(createWsBinaryFrame(Buffer.concat([header, payload])));
2024
+ } catch {
2025
+ }
2026
+ }
2027
+ function startHTTPStream(sock, streamId, open, localPort) {
2028
+ const method = open.method || "GET";
2029
+ const reqPath = open.path || "/";
2030
+ const query = open.query || "";
2031
+ const headers = open.headers || {};
892
2032
  const fullPath = query ? `${reqPath}?${query}` : reqPath;
893
2033
  const localHeaders = {};
894
- for (const [key, value] of Object.entries(headers)) {
895
- if (key.toLowerCase() !== "host") {
896
- localHeaders[key] = value;
2034
+ for (const [k, v] of Object.entries(headers)) {
2035
+ if (k.toLowerCase() !== "host") localHeaders[k] = v;
2036
+ }
2037
+ localHeaders["host"] = `localhost:${localPort}`;
2038
+ let started = false;
2039
+ let cancelled = false;
2040
+ const localReq = http.request(
2041
+ { hostname: "127.0.0.1", port: localPort, path: fullPath, method, headers: localHeaders },
2042
+ (res) => {
2043
+ const respHeaders = {};
2044
+ for (const [k, v] of Object.entries(res.headers)) {
2045
+ if (v == null) continue;
2046
+ respHeaders[k] = Array.isArray(v) ? v.join(", ") : v;
2047
+ }
2048
+ started = true;
2049
+ sendControl(sock, {
2050
+ type: "stream_response",
2051
+ stream_id: streamId,
2052
+ status: res.statusCode || 502,
2053
+ headers: respHeaders
2054
+ });
2055
+ const methodColor = method === "GET" ? colors.green : method === "POST" ? colors.blue : colors.yellow;
2056
+ log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${methodColor}${method}${colors.reset} ${reqPath} \u2192 ${res.statusCode}`);
2057
+ res.on("data", (chunk) => {
2058
+ if (cancelled) return;
2059
+ sendBinary(sock, streamId, chunk);
2060
+ });
2061
+ res.on("end", () => {
2062
+ if (cancelled) return;
2063
+ sendControl(sock, { type: "stream_eof", stream_id: streamId, side: "upstream" });
2064
+ sendControl(sock, { type: "stream_close", stream_id: streamId });
2065
+ streams.delete(streamId);
2066
+ });
2067
+ res.on("error", (err) => {
2068
+ sendControl(sock, { type: "stream_close", stream_id: streamId, error: `upstream_error: ${err.message}` });
2069
+ streams.delete(streamId);
2070
+ });
2071
+ }
2072
+ );
2073
+ localReq.on("error", (err) => {
2074
+ warn(`\uB85C\uCEEC \uC11C\uBC84 \uC5F0\uACB0 \uC2E4\uD328 (${method} ${reqPath}): ${err.message}`);
2075
+ if (!started) {
2076
+ sendControl(sock, {
2077
+ type: "stream_response",
2078
+ stream_id: streamId,
2079
+ status: 502,
2080
+ headers: { "content-type": "application/json" }
2081
+ });
2082
+ sendBinary(sock, streamId, Buffer.from(JSON.stringify({ error: `Local server error: ${err.message}` })));
2083
+ }
2084
+ sendControl(sock, { type: "stream_close", stream_id: streamId, error: `upstream_error: ${err.message}` });
2085
+ streams.delete(streamId);
2086
+ });
2087
+ const forwarder = {
2088
+ kind: "http",
2089
+ feedBody: (chunk) => {
2090
+ if (!cancelled) localReq.write(chunk);
2091
+ },
2092
+ endBody: () => {
2093
+ if (!cancelled) localReq.end();
2094
+ },
2095
+ cancel: () => {
2096
+ cancelled = true;
2097
+ try {
2098
+ localReq.destroy();
2099
+ } catch {
2100
+ }
897
2101
  }
2102
+ };
2103
+ streams.set(streamId, forwarder);
2104
+ }
2105
+ function startWSStream(sock, streamId, open, localPort) {
2106
+ const reqPath = open.path || "/";
2107
+ const query = open.query || "";
2108
+ const headers = open.headers || {};
2109
+ const fullPath = query ? `${reqPath}?${query}` : reqPath;
2110
+ const localHeaders = {};
2111
+ for (const [k, v] of Object.entries(headers)) {
2112
+ const lk = k.toLowerCase();
2113
+ if (lk === "host") continue;
2114
+ if (lk === "sec-websocket-extensions") continue;
2115
+ localHeaders[k] = v;
898
2116
  }
899
2117
  localHeaders["host"] = `localhost:${localPort}`;
900
- const reqOptions = {
2118
+ localHeaders["connection"] = "Upgrade";
2119
+ localHeaders["upgrade"] = "websocket";
2120
+ let cancelled = false;
2121
+ let upstream = null;
2122
+ const req = http.request({
901
2123
  hostname: "127.0.0.1",
902
2124
  port: localPort,
903
2125
  path: fullPath,
904
- method,
2126
+ method: "GET",
905
2127
  headers: localHeaders
906
- };
907
- const localReq = http.request(reqOptions, (res) => {
908
- const chunks = [];
909
- res.on("data", (chunk) => chunks.push(chunk));
910
- res.on("end", () => {
911
- const body = Buffer.concat(chunks);
912
- const responseHeaders = {};
913
- for (const [key, value] of Object.entries(res.headers)) {
914
- if (value) responseHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
915
- }
916
- const response = {
917
- type: "http_response",
918
- request_id: requestId,
919
- status: res.statusCode || 200,
920
- headers: responseHeaders,
921
- body: body.length > 0 ? body.toString("base64") : ""
922
- };
923
- try {
924
- sock.write(createWsTextFrame(JSON.stringify(response)));
925
- const methodColor = method === "GET" ? colors.green : method === "POST" ? colors.blue : colors.yellow;
926
- log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${methodColor}${method}${colors.reset} ${reqPath} \u2192 ${res.statusCode}`);
927
- } catch {
928
- warn(`\uC751\uB2F5 \uC804\uC1A1 \uC2E4\uD328: ${requestId}`);
2128
+ });
2129
+ req.on("upgrade", (res, sk) => {
2130
+ upstream = sk;
2131
+ const respHeaders = {};
2132
+ for (const [k, v] of Object.entries(res.headers)) {
2133
+ if (v == null) continue;
2134
+ respHeaders[k] = Array.isArray(v) ? v.join(", ") : v;
2135
+ }
2136
+ sendControl(sock, {
2137
+ type: "stream_response",
2138
+ stream_id: streamId,
2139
+ status: res.statusCode || 101,
2140
+ headers: respHeaders,
2141
+ websocket: true
2142
+ });
2143
+ log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${colors.cyan}WS${colors.reset} ${reqPath} \u2192 101`);
2144
+ const upstreamParser = new UpstreamWsFrameParser({
2145
+ onPayload: (payload) => {
2146
+ if (cancelled) return;
2147
+ sendBinary(sock, streamId, payload);
2148
+ },
2149
+ onPing: (payload) => {
2150
+ if (cancelled || !upstream) return;
2151
+ upstream.write(createUpstreamPongFrame(payload));
2152
+ },
2153
+ onClose: () => {
2154
+ if (cancelled) return;
2155
+ sendControl(sock, { type: "stream_close", stream_id: streamId });
2156
+ streams.delete(streamId);
2157
+ },
2158
+ onError: (err) => {
2159
+ sendControl(sock, { type: "stream_close", stream_id: streamId, error: `upstream_frame_error: ${err.message}` });
2160
+ streams.delete(streamId);
929
2161
  }
930
2162
  });
2163
+ sk.on("data", (chunk) => {
2164
+ if (cancelled) return;
2165
+ upstreamParser.feed(chunk);
2166
+ });
2167
+ sk.on("close", () => {
2168
+ if (cancelled) return;
2169
+ sendControl(sock, { type: "stream_close", stream_id: streamId });
2170
+ streams.delete(streamId);
2171
+ });
2172
+ sk.on("error", (err) => {
2173
+ sendControl(sock, { type: "stream_close", stream_id: streamId, error: `upstream_error: ${err.message}` });
2174
+ streams.delete(streamId);
2175
+ });
931
2176
  });
932
- localReq.on("error", (err) => {
933
- const response = {
934
- type: "http_response",
935
- request_id: requestId,
936
- status: 502,
937
- headers: { "content-type": "application/json" },
938
- body: Buffer.from(JSON.stringify({ error: `Local server error: ${err.message}` })).toString("base64")
939
- };
940
- try {
941
- sock.write(createWsTextFrame(JSON.stringify(response)));
942
- } catch {
2177
+ req.on("response", (res) => {
2178
+ const respHeaders = {};
2179
+ for (const [k, v] of Object.entries(res.headers)) {
2180
+ if (v == null) continue;
2181
+ respHeaders[k] = Array.isArray(v) ? v.join(", ") : v;
943
2182
  }
944
- warn(`\uB85C\uCEEC \uC11C\uBC84 \uC5F0\uACB0 \uC2E4\uD328 (${method} ${reqPath}): ${err.message}`);
2183
+ sendControl(sock, {
2184
+ type: "stream_response",
2185
+ stream_id: streamId,
2186
+ status: res.statusCode || 502,
2187
+ headers: respHeaders,
2188
+ websocket: false
2189
+ });
2190
+ sendControl(sock, { type: "stream_close", stream_id: streamId });
2191
+ streams.delete(streamId);
2192
+ });
2193
+ req.on("error", (err) => {
2194
+ sendControl(sock, { type: "stream_close", stream_id: streamId, error: `upstream_error: ${err.message}` });
2195
+ streams.delete(streamId);
945
2196
  });
946
- if (bodyBase64) {
947
- localReq.write(Buffer.from(bodyBase64, "base64"));
2197
+ req.end();
2198
+ const forwarder = {
2199
+ kind: "ws",
2200
+ // Encode the v2 payload as an upstream WS frame BEFORE writing to
2201
+ // the socket. The prior implementation wrote raw payload to the
2202
+ // socket which is not a valid WS frame; upstream WS servers (e.g.
2203
+ // ComfyUI) would error out. Default to TEXT opcode — the v2
2204
+ // protocol does not propagate the original opcode from the client,
2205
+ // and most WS APIs (JSON-based) use text. Binary client→upstream
2206
+ // is a known limitation pending opcode propagation in v2.
2207
+ feedBody: (chunk) => {
2208
+ if (cancelled || !upstream) return;
2209
+ upstream.write(createUpstreamTextFrame(chunk));
2210
+ },
2211
+ endBody: () => {
2212
+ },
2213
+ cancel: () => {
2214
+ cancelled = true;
2215
+ try {
2216
+ upstream?.destroy();
2217
+ } catch {
2218
+ }
2219
+ }
2220
+ };
2221
+ streams.set(streamId, forwarder);
2222
+ }
2223
+ async function handleMessage(msg, sock, localPort) {
2224
+ switch (msg.type) {
2225
+ case "tunnel_ready": {
2226
+ const proxyToken = typeof msg.proxy_token === "string" ? msg.proxy_token : "";
2227
+ const tunnelUrl = typeof msg.url === "string" ? msg.url : "";
2228
+ const isPublic = msg.public === true;
2229
+ success(`\uD130\uB110 \uD65C\uC131\uD654!`);
2230
+ log(`${colors.green}\u2192${colors.reset} URL: ${colors.cyan}${tunnelUrl}${colors.reset}`);
2231
+ log(`${colors.green}\u2192${colors.reset} \uB85C\uCEEC: ${colors.cyan}http://localhost:${localPort}${colors.reset}`);
2232
+ if (msg.timeout || msg.max_body) {
2233
+ log(`${colors.green}\u2192${colors.reset} \uC124\uC815: timeout=${colors.cyan}${msg.timeout}s${colors.reset}, max-body=${colors.cyan}${msg.max_body}MB${colors.reset}`);
2234
+ }
2235
+ if (isPublic) {
2236
+ log("");
2237
+ log(`${colors.yellow}\u26A0 \uACF5\uAC1C \uBAA8\uB4DC (--public):${colors.reset} proxy_token \uAC80\uC99D\uC774 \uBE44\uD65C\uC131\uD654\uB410\uC2B5\uB2C8\uB2E4.`);
2238
+ log(` ${colors.dim}tunnelID \uB97C \uC544\uB294 \uB204\uAD6C\uB098 \uC774 URL \uB85C \uB85C\uCEEC \uC11C\uBE44\uC2A4\uC5D0 \uC811\uADFC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.${colors.reset}`);
2239
+ log(` ${colors.dim}\uC6F9\uD6C5 \uC218\uC2E0 / \uC678\uBD80 \uC11C\uBE44\uC2A4 \uC5F0\uB3D9 \uBAA9\uC801\uC5D0 \uD55C\uD574 \uC0AC\uC6A9\uD558\uACE0, \uBBFC\uAC10 \uB370\uC774\uD130\uB294 \uCDE8\uAE09\uD558\uC9C0 \uB9C8\uC138\uC694.${colors.reset}`);
2240
+ log("");
2241
+ log(`${colors.dim} $ curl ${tunnelUrl}/${colors.reset}`);
2242
+ } else if (tunnelOpts?.showToken && proxyToken && tunnelUrl) {
2243
+ log("");
2244
+ log(`${colors.green}\u2192${colors.reset} \uD1A0\uD070: ${colors.yellow}${proxyToken}${colors.reset}`);
2245
+ log(`
2246
+ ${colors.dim}\uC678\uBD80 \uC9C1\uC811 \uD638\uCD9C \uC2DC \uB2E4\uC74C \uD5E4\uB354 \uB610\uB294 \uCFFC\uB9AC\uB85C \uD1A0\uD070\uC744 \uC804\uB2EC\uD558\uC138\uC694:${colors.reset}`);
2247
+ log(`${colors.dim} $ curl -H "X-Proxy-Token: ${proxyToken}" ${tunnelUrl}/${colors.reset}`);
2248
+ log(`${colors.dim} $ curl "${tunnelUrl}/?proxy_token=${proxyToken}"${colors.reset}`);
2249
+ }
2250
+ if (tunnelOpts?.label) {
2251
+ const tunnelId = typeof msg.tunnel_id === "string" ? msg.tunnel_id : "";
2252
+ if (!tunnelId) {
2253
+ error(`tunnel_ready \uBA54\uC2DC\uC9C0\uC5D0 tunnel_id \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 endpoint "${tunnelOpts.label}" \uC790\uB3D9 \uB4F1\uB85D skip`);
2254
+ } else {
2255
+ await registerEndpointBinding(
2256
+ config.baseUrl,
2257
+ appId,
2258
+ tunnelKey,
2259
+ tunnelId,
2260
+ tunnelOpts.label,
2261
+ tunnelOpts.description
2262
+ );
2263
+ }
2264
+ }
2265
+ log(`
2266
+ ${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
2267
+ `);
2268
+ break;
2269
+ }
2270
+ case "stream_open": {
2271
+ const sid = String(msg.stream_id ?? "");
2272
+ if (!sid) {
2273
+ warn("stream_open with empty stream_id");
2274
+ break;
2275
+ }
2276
+ const kind = msg.kind || "http";
2277
+ if (kind === "ws") {
2278
+ startWSStream(sock, sid, msg, localPort);
2279
+ } else {
2280
+ startHTTPStream(sock, sid, msg, localPort);
2281
+ }
2282
+ break;
2283
+ }
2284
+ case "stream_eof": {
2285
+ const sid = String(msg.stream_id ?? "");
2286
+ const side = msg.side;
2287
+ if (side === "client") {
2288
+ const f = streams.get(sid);
2289
+ f?.endBody();
2290
+ }
2291
+ break;
2292
+ }
2293
+ case "stream_close": {
2294
+ const sid = String(msg.stream_id ?? "");
2295
+ const f = streams.get(sid);
2296
+ if (f) {
2297
+ f.cancel();
2298
+ streams.delete(sid);
2299
+ }
2300
+ break;
2301
+ }
2302
+ case "tunnel_error": {
2303
+ const result = handleTunnelError(msg, appId, localPort);
2304
+ error(result.message);
2305
+ if (result.action === "exit") {
2306
+ shouldReconnect = false;
2307
+ setTimeout(() => process.exit(result.exitCode ?? 1), 500);
2308
+ }
2309
+ break;
2310
+ }
2311
+ case "ping":
2312
+ sock.write(createWsTextFrame(JSON.stringify({ type: "pong" })));
2313
+ break;
948
2314
  }
949
- localReq.end();
950
2315
  }
951
2316
  connect();
952
2317
  await new Promise(() => {
@@ -954,14 +2319,16 @@ ${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
954
2319
  }
955
2320
  function showHelp() {
956
2321
  log(`
957
- ${colors.cyan}connectbase-client${colors.reset} - Connect Base SDK & CLI
2322
+ ${colors.cyan}connectbase${colors.reset} - Connect Base SDK & CLI
958
2323
 
959
2324
  ${colors.yellow}\uC0AC\uC6A9\uBC95:${colors.reset}
960
- npx connectbase-client <command> [options]
2325
+ npx connectbase <command> [options]
961
2326
 
962
2327
  ${colors.yellow}\uBA85\uB839\uC5B4:${colors.reset}
963
- init \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654 (\uC124\uC815 \uD30C\uC77C \uC0DD\uC131)
964
- docs SDK \uBB38\uC11C \uB2E4\uC6B4\uB85C\uB4DC/\uC5C5\uB370\uC774\uD2B8 (--monorepo: git root\uC5D0 \uC0DD\uC131)
2328
+ init \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654 (\uC571 \uC0DD\uC131, MCP \uC124\uC815, SDK \uBB38\uC11C \uB2E4\uC6B4\uB85C\uB4DC)
2329
+ update SDK \uBC84\uC804 \uCCB4\uD06C + \uBB38\uC11C/MCP \uC77C\uAD04 \uC5C5\uB370\uC774\uD2B8 (\uBAA8\uB178\uB808\uD3EC \uB8E8\uD2B8 \uC9C0\uC6D0)
2330
+ docs SDK \uBB38\uC11C \uB2E4\uC6B4\uB85C\uB4DC/\uC5C5\uB370\uC774\uD2B8 (\uBAA8\uB178\uB808\uD3EC \uC790\uB3D9 \uAC10\uC9C0)
2331
+ mcp MCP \uC11C\uBC84 \uC124\uC815 (.mcp.json \uC0DD\uC131/\uC5C5\uB370\uC774\uD2B8, \uBAA8\uB178\uB808\uD3EC \uC790\uB3D9 \uAC10\uC9C0)
965
2332
  deploy <directory> \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0\uC5D0 \uD30C\uC77C \uBC30\uD3EC (--dev: Dev \uD658\uACBD)
966
2333
  tunnel <port> \uB85C\uCEEC \uC11C\uBE44\uC2A4\uB97C \uC778\uD130\uB137\uC5D0 \uB178\uCD9C
967
2334
 
@@ -974,27 +2341,45 @@ ${colors.yellow}\uCF54\uB4DC \uBD84\uC11D (MCP \uD1B5\uD574 \uC0AC\uC6A9):${colo
974
2341
 
975
2342
  ${colors.yellow}\uC635\uC158:${colors.reset}
976
2343
  -s, --storage <id> \uC2A4\uD1A0\uB9AC\uC9C0 ID
977
- -k, --api-key <key> API Key
2344
+ -k, --public-key <key> API Key
978
2345
  -u, --base-url <url> \uC11C\uBC84 URL (\uAE30\uBCF8: ${DEFAULT_BASE_URL})
979
2346
  -t, --timeout <sec> \uC694\uCCAD \uD0C0\uC784\uC544\uC6C3 (\uCD08, tunnel/deploy \uACF5\uD1B5. deploy \uBBF8\uC9C0\uC815 \uC2DC \uD30C\uC77C \uD06C\uAE30\uC5D0 \uB530\uB77C \uC790\uB3D9 \uC0B0\uC815)
980
2347
  --max-body <MB> \uD130\uB110 \uCD5C\uB300 \uBC14\uB514 \uD06C\uAE30 (MB, tunnel \uC804\uC6A9)
2348
+ --force \uD130\uB110 lockfile \uBB34\uC2DC (\uC911\uBCF5 \uC2E4\uD589 \uAC15\uC81C, tunnel \uC804\uC6A9)
2349
+ --public proxy_token \uAC80\uC99D \uBE44\uD65C\uC131\uD654 \u2014 \uC6F9\uD6C5/\uC678\uBD80 \uC9C1\uC811 \uD638\uCD9C\uC6A9 (tunnel \uC804\uC6A9)
2350
+ --show-token proxy_token \uACFC curl \uC608\uC2DC\uB97C \uCD9C\uB825 (--public \uC544\uB2CC \uACBD\uC6B0)
2351
+ --label <name> Endpoint binding \uC790\uB3D9 \uB4F1\uB85D (tunnel \uC804\uC6A9) \u2014 SDK \uC758 cb.endpoint.call(label) \uD638\uCD9C \uAC00\uB2A5
2352
+ --description <text> Endpoint binding \uC124\uBA85 (--label \uB3D9\uBC18 \uC2DC\uB9CC)
981
2353
  -d, --dev Dev \uD658\uACBD\uC5D0 \uBC30\uD3EC (deploy \uC804\uC6A9)
982
- --monorepo \uBAA8\uB178\uB808\uD3EC git root\uC5D0 \uBB38\uC11C \uC0DD\uC131 (docs \uC804\uC6A9)
2354
+ --check \uBC84\uC804\uB9CC \uD655\uC778 (update \uC804\uC6A9)
2355
+ --skip-docs \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update \uC804\uC6A9)
2356
+ --skip-mcp MCP \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update \uC804\uC6A9)
2357
+ --setup-root \uBAA8\uB178\uB808\uD3EC \uB8E8\uD2B8\uC5D0 \uD3B8\uC758 \uC2A4\uD06C\uB9BD\uD2B8 \uCD94\uAC00 (update \uC804\uC6A9)
2358
+ (docs, mcp, update\uB294 \uBAA8\uB178\uB808\uD3EC\uB97C \uC790\uB3D9 \uAC10\uC9C0\uD558\uC5EC \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC5D0 \uC0DD\uC131)
983
2359
  -h, --help \uB3C4\uC6C0\uB9D0 \uD45C\uC2DC
984
2360
  -v, --version \uBC84\uC804 \uD45C\uC2DC
985
2361
 
986
2362
  ${colors.yellow}\uBE60\uB978 \uC2DC\uC791:${colors.reset}
987
2363
  ${colors.dim}# 1. \uCD08\uAE30\uD654 (\uCD5C\uCD08 1\uD68C)${colors.reset}
988
- npx connectbase-client init
2364
+ npx connectbase init
2365
+
2366
+ ${colors.dim}# 2. SDK \uC804\uCCB4 \uC5C5\uB370\uC774\uD2B8 (\uBC84\uC804 \uCCB4\uD06C + \uBB38\uC11C + MCP)${colors.reset}
2367
+ npx connectbase update
2368
+
2369
+ ${colors.dim}# 2-1. \uBAA8\uB178\uB808\uD3EC \uB8E8\uD2B8\uC5D0 \uD3B8\uC758 \uC2A4\uD06C\uB9BD\uD2B8 \uC124\uCE58${colors.reset}
2370
+ npx connectbase update --setup-root
989
2371
 
990
- ${colors.dim}# 2. SDK \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8${colors.reset}
2372
+ ${colors.dim}# 3. SDK \uBB38\uC11C\uB9CC \uC5C5\uB370\uC774\uD2B8${colors.reset}
991
2373
  npx connectbase docs
992
2374
 
993
- ${colors.dim}# 3. Prod \uBC30\uD3EC${colors.reset}
2375
+ ${colors.dim}# 4. MCP \uC11C\uBC84 \uC124\uC815${colors.reset}
2376
+ npx connectbase mcp
2377
+
2378
+ ${colors.dim}# 4. Prod \uBC30\uD3EC${colors.reset}
994
2379
  npm run deploy
995
2380
 
996
2381
  ${colors.dim}# 4. Dev \uD658\uACBD \uBC30\uD3EC (\uB0B4\uBD80 QA\uC6A9)${colors.reset}
997
- npx connectbase-client deploy ./dist --dev
2382
+ npx connectbase deploy ./dist --dev
998
2383
 
999
2384
  ${colors.dim}# 5. \uD130\uB110 (\uAE30\uBCF8)${colors.reset}
1000
2385
  npx connectbase tunnel 3000
@@ -1003,13 +2388,16 @@ ${colors.yellow}\uBE60\uB978 \uC2DC\uC791:${colors.reset}
1003
2388
  npx connectbase tunnel 7860 --timeout 300 --max-body 50
1004
2389
 
1005
2390
  ${colors.yellow}\uD658\uACBD\uBCC0\uC218:${colors.reset}
1006
- CONNECTBASE_API_KEY API Key
2391
+ CONNECTBASE_PUBLIC_KEY Public Key (cb_pk_*)
2392
+ CONNECTBASE_SECRET_KEY Secret Key (cb_sk_*, \uD130\uB110\uC6A9)
2393
+ CONNECTBASE_PUBLIC_KEY (deprecated) \uB808\uAC70\uC2DC \u2014 publicKey \uB610\uB294 secretKey \uB85C \uC0AC\uC6A9
1007
2394
  CONNECTBASE_STORAGE_ID \uC2A4\uD1A0\uB9AC\uC9C0 ID
1008
2395
  CONNECTBASE_BASE_URL \uC11C\uBC84 URL
1009
2396
 
1010
2397
  ${colors.yellow}\uC124\uC815 \uD30C\uC77C (.connectbaserc):${colors.reset}
1011
2398
  {
1012
- "apiKey": "your-api-key",
2399
+ "publicKey": "cb_pk_...",
2400
+ "secretKey": "cb_sk_...",
1013
2401
  "storageId": "your-storage-id",
1014
2402
  "deployDir": "./dist"
1015
2403
  }
@@ -1025,18 +2413,38 @@ function parseArgs(args) {
1025
2413
  const arg = args[i];
1026
2414
  if (arg === "-s" || arg === "--storage") {
1027
2415
  result.options.storageId = args[++i];
1028
- } else if (arg === "-k" || arg === "--api-key") {
1029
- result.options.apiKey = args[++i];
2416
+ } else if (arg === "-k" || arg === "--public-key" || arg === "--public-key") {
2417
+ result.options.publicKey = args[++i];
2418
+ } else if (arg === "--secret-key") {
2419
+ result.options.secretKey = args[++i];
1030
2420
  } else if (arg === "-u" || arg === "--base-url") {
1031
2421
  result.options.baseUrl = args[++i];
1032
2422
  } else if (arg === "-t" || arg === "--timeout") {
1033
2423
  result.options.timeout = args[++i];
1034
2424
  } else if (arg === "--max-body") {
1035
2425
  result.options.maxBody = args[++i];
2426
+ } else if (arg === "--label") {
2427
+ result.options.label = args[++i];
2428
+ } else if (arg === "--description") {
2429
+ result.options.description = args[++i];
2430
+ } else if (arg === "-a" || arg === "--app") {
2431
+ result.options.appId = args[++i];
2432
+ } else if (arg === "--force") {
2433
+ result.options.force = "true";
2434
+ } else if (arg === "--public") {
2435
+ result.options.public = "true";
2436
+ } else if (arg === "--show-token") {
2437
+ result.options.showToken = "true";
1036
2438
  } else if (arg === "-d" || arg === "--dev") {
1037
2439
  result.options.dev = "true";
1038
- } else if (arg === "--monorepo") {
1039
- result.options.monorepo = "true";
2440
+ } else if (arg === "--check") {
2441
+ result.options.check = "true";
2442
+ } else if (arg === "--skip-docs") {
2443
+ result.options.skipDocs = "true";
2444
+ } else if (arg === "--skip-mcp") {
2445
+ result.options.skipMcp = "true";
2446
+ } else if (arg === "--setup-root") {
2447
+ result.options.setupRoot = "true";
1040
2448
  } else if (arg === "-h" || arg === "--help") {
1041
2449
  result.options.help = "true";
1042
2450
  } else if (arg === "-v" || arg === "--version") {
@@ -1055,7 +2463,7 @@ function parseArgs(args) {
1055
2463
  async function main() {
1056
2464
  const parsed = parseArgs(process.argv.slice(2));
1057
2465
  if (parsed.options.version) {
1058
- log(`connectbase-client v${VERSION}`);
2466
+ log(`connectbase v${VERSION}`);
1059
2467
  return;
1060
2468
  }
1061
2469
  if (parsed.options.help || !parsed.command) {
@@ -1064,30 +2472,32 @@ async function main() {
1064
2472
  }
1065
2473
  const fileConfig = loadConfig();
1066
2474
  const config = {
1067
- apiKey: parsed.options.apiKey || fileConfig.apiKey,
2475
+ publicKey: parsed.options.publicKey || fileConfig.publicKey,
2476
+ secretKey: parsed.options.secretKey || fileConfig.secretKey,
1068
2477
  storageId: parsed.options.storageId || fileConfig.storageId,
1069
2478
  baseUrl: parsed.options.baseUrl || fileConfig.baseUrl || DEFAULT_BASE_URL
1070
2479
  };
1071
2480
  if (parsed.command === "init") {
1072
2481
  await init();
2482
+ } else if (parsed.command === "update") {
2483
+ await update(config, {
2484
+ checkOnly: parsed.options.check === "true",
2485
+ skipDocs: parsed.options.skipDocs === "true",
2486
+ skipMcp: parsed.options.skipMcp === "true",
2487
+ setupRoot: parsed.options.setupRoot === "true"
2488
+ });
1073
2489
  } else if (parsed.command === "docs") {
1074
- let docsApiKey = config.apiKey;
1075
- if (!docsApiKey) {
1076
- docsApiKey = await prompt(`${colors.blue}?${colors.reset} API Key: `);
1077
- if (!docsApiKey) {
1078
- error("API Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
1079
- process.exit(1);
1080
- }
1081
- }
1082
- await downloadDocs(docsApiKey, void 0, parsed.options.monorepo === "true");
2490
+ await downloadDocs(config.publicKey || "");
2491
+ } else if (parsed.command === "mcp") {
2492
+ await setupMcp();
1083
2493
  } else if (parsed.command === "deploy") {
1084
2494
  const directory = parsed.args[0] || fileConfig.deployDir || ".";
1085
- if (!config.apiKey) {
1086
- error('API Key\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. "npx connectbase-client init"\uC73C\uB85C \uC124\uC815\uD558\uAC70\uB098 -k \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uC138\uC694');
2495
+ if (!config.publicKey) {
2496
+ error('Public Key\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. "npx connectbase init"\uC73C\uB85C \uC124\uC815\uD558\uAC70\uB098 -k \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uC138\uC694');
1087
2497
  process.exit(1);
1088
2498
  }
1089
2499
  if (!config.storageId) {
1090
- error('\uC2A4\uD1A0\uB9AC\uC9C0 ID\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. "npx connectbase-client init"\uC73C\uB85C \uC124\uC815\uD558\uAC70\uB098 -s \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uC138\uC694');
2500
+ error('\uC2A4\uD1A0\uB9AC\uC9C0 ID\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. "npx connectbase init"\uC73C\uB85C \uC124\uC815\uD558\uAC70\uB098 -s \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uC138\uC694');
1091
2501
  process.exit(1);
1092
2502
  }
1093
2503
  const isDev = parsed.options.dev === "true";
@@ -1101,7 +2511,7 @@ async function main() {
1101
2511
  } else if (parsed.command === "tunnel") {
1102
2512
  const portStr = parsed.args[0];
1103
2513
  if (!portStr) {
1104
- error("\uD3EC\uD2B8 \uBC88\uD638\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \uC608: npx connectbase-client tunnel 8084");
2514
+ error("\uD3EC\uD2B8 \uBC88\uD638\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \uC608: npx connectbase tunnel 8084");
1105
2515
  process.exit(1);
1106
2516
  }
1107
2517
  const port = parseInt(portStr, 10);
@@ -1118,6 +2528,24 @@ async function main() {
1118
2528
  const m = parseInt(parsed.options.maxBody, 10);
1119
2529
  if (!isNaN(m) && m > 0) tunnelOpts.maxBody = m;
1120
2530
  }
2531
+ if (parsed.options.appId) {
2532
+ tunnelOpts.appId = parsed.options.appId;
2533
+ }
2534
+ if (parsed.options.force === "true") {
2535
+ tunnelOpts.force = true;
2536
+ }
2537
+ if (parsed.options.public === "true") {
2538
+ tunnelOpts.public = true;
2539
+ }
2540
+ if (parsed.options.showToken === "true") {
2541
+ tunnelOpts.showToken = true;
2542
+ }
2543
+ if (parsed.options.label) {
2544
+ tunnelOpts.label = parsed.options.label;
2545
+ }
2546
+ if (parsed.options.description) {
2547
+ tunnelOpts.description = parsed.options.description;
2548
+ }
1121
2549
  await startTunnel(port, config, tunnelOpts);
1122
2550
  } else {
1123
2551
  error(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${parsed.command}`);
@@ -1129,3 +2557,11 @@ main().catch((err) => {
1129
2557
  error(err.message);
1130
2558
  process.exit(1);
1131
2559
  });
2560
+ // Annotate the CommonJS export names for ESM import in node:
2561
+ 0 && (module.exports = {
2562
+ computeDeployDiff,
2563
+ normalizeRelativePath,
2564
+ parseArgs,
2565
+ registerEndpointBinding,
2566
+ sha256Hex
2567
+ });