connectbase-client 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -24,12 +24,90 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
- var fs = __toESM(require("fs"));
28
- var path = __toESM(require("path"));
27
+ var fs2 = __toESM(require("fs"));
28
+ var path2 = __toESM(require("path"));
29
29
  var crypto = __toESM(require("crypto"));
30
30
  var https = __toESM(require("https"));
31
31
  var http = __toESM(require("http"));
32
32
  var readline = __toESM(require("readline"));
33
+
34
+ // src/tunnel-utils.ts
35
+ var fs = __toESM(require("fs"));
36
+ var path = __toESM(require("path"));
37
+ var os = __toESM(require("os"));
38
+ function getTunnelLockDir() {
39
+ const platform2 = os.platform();
40
+ if (platform2 === "darwin") {
41
+ return path.join(os.homedir(), "Library", "Application Support", "connectbase", "tunnel-locks");
42
+ } else if (platform2 === "win32") {
43
+ return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "connectbase", "tunnel-locks");
44
+ }
45
+ const stateHome = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
46
+ return path.join(stateHome, "connectbase", "tunnel-locks");
47
+ }
48
+ function isProcessAlive(pid) {
49
+ try {
50
+ process.kill(pid, 0);
51
+ return true;
52
+ } catch (e) {
53
+ return e.code === "EPERM";
54
+ }
55
+ }
56
+ function acquireTunnelLock(appID, port, force, version) {
57
+ const lockDir = getTunnelLockDir();
58
+ fs.mkdirSync(lockDir, { recursive: true });
59
+ const lockPath = path.join(lockDir, `${appID}-${port}.lock`);
60
+ const lockData = {
61
+ pid: process.pid,
62
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
63
+ appID,
64
+ port,
65
+ host: os.hostname(),
66
+ version
67
+ };
68
+ if (!force) {
69
+ try {
70
+ const fd = fs.openSync(lockPath, "wx");
71
+ fs.writeSync(fd, JSON.stringify(lockData, null, 2));
72
+ fs.closeSync(fd);
73
+ return lockPath;
74
+ } catch (e) {
75
+ if (e.code !== "EEXIST") {
76
+ return null;
77
+ }
78
+ }
79
+ try {
80
+ const existing = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
81
+ if (isProcessAlive(existing.pid)) {
82
+ return `LOCKED:${existing.pid}:${existing.startedAt}:${existing.host}`;
83
+ }
84
+ } catch {
85
+ }
86
+ }
87
+ fs.writeFileSync(lockPath, JSON.stringify(lockData, null, 2));
88
+ return lockPath;
89
+ }
90
+ function releaseTunnelLock(lockPath) {
91
+ try {
92
+ fs.unlinkSync(lockPath);
93
+ } catch {
94
+ }
95
+ }
96
+ function handleTunnelError(msg, appId, localPort) {
97
+ if (msg.code === "replaced") {
98
+ return {
99
+ action: "exit",
100
+ 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.`,
101
+ exitCode: 1
102
+ };
103
+ }
104
+ return {
105
+ action: "warn",
106
+ message: `\uD130\uB110 \uC5D0\uB7EC: ${msg.error || msg.message || "\uC54C \uC218 \uC5C6\uB294 \uC5D0\uB7EC"}`
107
+ };
108
+ }
109
+
110
+ // src/cli.ts
33
111
  var VERSION = "0.10.6";
34
112
  var DEFAULT_BASE_URL = "https://api.connectbase.world";
35
113
  var colors = {
@@ -118,10 +196,10 @@ function loadConfig() {
118
196
  storageId: process.env.CONNECTBASE_STORAGE_ID,
119
197
  baseUrl: process.env.CONNECTBASE_BASE_URL || DEFAULT_BASE_URL
120
198
  };
121
- const rcPath = path.join(process.cwd(), ".connectbaserc");
122
- if (fs.existsSync(rcPath)) {
199
+ const rcPath = path2.join(process.cwd(), ".connectbaserc");
200
+ if (fs2.existsSync(rcPath)) {
123
201
  try {
124
- const rcContent = JSON.parse(fs.readFileSync(rcPath, "utf-8"));
202
+ const rcContent = JSON.parse(fs2.readFileSync(rcPath, "utf-8"));
125
203
  if (rcContent.publicKey) config.publicKey = rcContent.publicKey;
126
204
  if (rcContent.secretKey) config.secretKey = rcContent.secretKey;
127
205
  if (rcContent.storageId) config.storageId = rcContent.storageId;
@@ -135,16 +213,16 @@ function loadConfig() {
135
213
  }
136
214
  function collectFiles(dir, baseDir = dir) {
137
215
  const files = [];
138
- const entries = fs.readdirSync(dir, { withFileTypes: true });
216
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
139
217
  for (const entry of entries) {
140
- const fullPath = path.join(dir, entry.name);
141
- const relativePath = path.relative(baseDir, fullPath);
218
+ const fullPath = path2.join(dir, entry.name);
219
+ const relativePath = path2.relative(baseDir, fullPath);
142
220
  if (entry.isDirectory()) {
143
221
  if (!entry.name.startsWith(".")) {
144
222
  files.push(...collectFiles(fullPath, baseDir));
145
223
  }
146
224
  } else if (entry.isFile()) {
147
- const ext = path.extname(entry.name).toLowerCase();
225
+ const ext = path2.extname(entry.name).toLowerCase();
148
226
  if (!ALLOWED_EXTENSIONS.has(ext)) {
149
227
  continue;
150
228
  }
@@ -154,9 +232,9 @@ function collectFiles(dir, baseDir = dir) {
154
232
  const isBinary = BINARY_EXTENSIONS.has(ext);
155
233
  let content;
156
234
  if (isBinary) {
157
- content = fs.readFileSync(fullPath).toString("base64");
235
+ content = fs2.readFileSync(fullPath).toString("base64");
158
236
  } else {
159
- content = fs.readFileSync(fullPath, "utf-8");
237
+ content = fs2.readFileSync(fullPath, "utf-8");
160
238
  }
161
239
  files.push({
162
240
  path: relativePath.replace(/\\/g, "/"),
@@ -212,15 +290,15 @@ async function makeRequest(url, method, headers, body) {
212
290
  });
213
291
  }
214
292
  async function deploy(directory, config, isDev = false) {
215
- const dir = path.resolve(directory);
216
- if (!fs.existsSync(dir)) {
293
+ const dir = path2.resolve(directory);
294
+ if (!fs2.existsSync(dir)) {
217
295
  error(`\uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dir}`);
218
296
  process.exit(1);
219
297
  }
220
- const pkgPath = path.join(process.cwd(), "package.json");
221
- if (fs.existsSync(pkgPath)) {
298
+ const pkgPath = path2.join(process.cwd(), "package.json");
299
+ if (fs2.existsSync(pkgPath)) {
222
300
  try {
223
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
301
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
224
302
  if (pkg.scripts?.build) {
225
303
  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}`);
226
304
  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}`);
@@ -228,12 +306,12 @@ async function deploy(directory, config, isDev = false) {
228
306
  } catch {
229
307
  }
230
308
  }
231
- if (!fs.statSync(dir).isDirectory()) {
309
+ if (!fs2.statSync(dir).isDirectory()) {
232
310
  error(`${dir}\uB294 \uB514\uB809\uD1A0\uB9AC\uAC00 \uC544\uB2D9\uB2C8\uB2E4`);
233
311
  process.exit(1);
234
312
  }
235
- const indexPath = path.join(dir, "index.html");
236
- if (!fs.existsSync(indexPath)) {
313
+ const indexPath = path2.join(dir, "index.html");
314
+ if (!fs2.existsSync(indexPath)) {
237
315
  error("index.html \uD30C\uC77C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
238
316
  process.exit(1);
239
317
  }
@@ -355,11 +433,11 @@ function getProjectRoot() {
355
433
  var CONSOLE_URL = DEFAULT_BASE_URL.replace("api.", "");
356
434
  function openBrowser(url) {
357
435
  const { exec } = require("child_process");
358
- const platform = process.platform;
436
+ const platform2 = process.platform;
359
437
  let command;
360
- if (platform === "darwin") {
438
+ if (platform2 === "darwin") {
361
439
  command = `open "${url}"`;
362
- } else if (platform === "win32") {
440
+ } else if (platform2 === "win32") {
363
441
  command = `start "" "${url}"`;
364
442
  } else {
365
443
  command = `xdg-open "${url}"`;
@@ -449,8 +527,8 @@ ${colors.cyan}Connect Base \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654${colors.r
449
527
  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
450
528
  `);
451
529
  }
452
- const rcPath = path.join(cwd, ".connectbaserc");
453
- if (fs.existsSync(rcPath)) {
530
+ const rcPath = path2.join(cwd, ".connectbaserc");
531
+ if (fs2.existsSync(rcPath)) {
454
532
  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): `);
455
533
  if (overwrite.toLowerCase() !== "y") {
456
534
  info("\uCD08\uAE30\uD654\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
@@ -539,7 +617,7 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
539
617
  }
540
618
  }
541
619
  if (!appId) {
542
- const projectName = path.basename(cwd);
620
+ const projectName = path2.basename(cwd);
543
621
  const appName = await prompt(`${colors.blue}?${colors.reset} \uC571 \uC774\uB984 (${projectName}): `) || projectName;
544
622
  info("\uC571 \uC0DD\uC131 \uC911...");
545
623
  const createRes = await makeRequest(
@@ -607,7 +685,7 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
607
685
  }
608
686
  }
609
687
  if (!storageId) {
610
- const projectName = path.basename(cwd);
688
+ const projectName = path2.basename(cwd);
611
689
  const name = await prompt(`${colors.blue}?${colors.reset} \uC2A4\uD1A0\uB9AC\uC9C0 \uC774\uB984 (${projectName}): `) || projectName;
612
690
  info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC911...");
613
691
  const createRes = await makeRequest(
@@ -647,7 +725,7 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
647
725
  if (secretKey) {
648
726
  config.secretKey = secretKey;
649
727
  }
650
- fs.writeFileSync(rcPath, JSON.stringify(config, null, 2) + "\n");
728
+ fs2.writeFileSync(rcPath, JSON.stringify(config, null, 2) + "\n");
651
729
  success(".connectbaserc \uC0DD\uC131 \uC644\uB8CC");
652
730
  addToGitignore(".connectbaserc");
653
731
  addDeployScript(deployDir);
@@ -662,21 +740,21 @@ ${colors.dim}Claude Code\uC5D0\uC11C MCP\uAC00 \uC790\uB3D9 \uC5F0\uACB0\uB429\u
662
740
  `);
663
741
  }
664
742
  function detectBuildDir() {
665
- if (fs.existsSync(path.join(process.cwd(), "dist"))) return "./dist";
666
- if (fs.existsSync(path.join(process.cwd(), "build"))) return "./build";
667
- if (fs.existsSync(path.join(process.cwd(), "out"))) return "./out";
668
- if (fs.existsSync(path.join(process.cwd(), ".next"))) return "./out";
669
- if (fs.existsSync(path.join(process.cwd(), "vite.config.ts")) || fs.existsSync(path.join(process.cwd(), "vite.config.js"))) return "./dist";
670
- if (fs.existsSync(path.join(process.cwd(), "next.config.js")) || fs.existsSync(path.join(process.cwd(), "next.config.mjs"))) return "./out";
743
+ if (fs2.existsSync(path2.join(process.cwd(), "dist"))) return "./dist";
744
+ if (fs2.existsSync(path2.join(process.cwd(), "build"))) return "./build";
745
+ if (fs2.existsSync(path2.join(process.cwd(), "out"))) return "./out";
746
+ if (fs2.existsSync(path2.join(process.cwd(), ".next"))) return "./out";
747
+ if (fs2.existsSync(path2.join(process.cwd(), "vite.config.ts")) || fs2.existsSync(path2.join(process.cwd(), "vite.config.js"))) return "./dist";
748
+ if (fs2.existsSync(path2.join(process.cwd(), "next.config.js")) || fs2.existsSync(path2.join(process.cwd(), "next.config.mjs"))) return "./out";
671
749
  return "./dist";
672
750
  }
673
751
  function addToGitignore(entry, basePath) {
674
752
  const dir = basePath || process.cwd();
675
- const gitignorePath = path.join(dir, ".gitignore");
676
- if (fs.existsSync(gitignorePath)) {
677
- const content = fs.readFileSync(gitignorePath, "utf-8");
753
+ const gitignorePath = path2.join(dir, ".gitignore");
754
+ if (fs2.existsSync(gitignorePath)) {
755
+ const content = fs2.readFileSync(gitignorePath, "utf-8");
678
756
  if (!content.includes(entry)) {
679
- fs.appendFileSync(gitignorePath, `
757
+ fs2.appendFileSync(gitignorePath, `
680
758
  # Connect Base
681
759
  ${entry}
682
760
  `);
@@ -685,20 +763,20 @@ ${entry}
685
763
  info(`.gitignore\uC5D0 \uC774\uBBF8 ${entry}\uAC00 \uD3EC\uD568\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4`);
686
764
  }
687
765
  } else {
688
- fs.writeFileSync(gitignorePath, `# Connect Base
766
+ fs2.writeFileSync(gitignorePath, `# Connect Base
689
767
  ${entry}
690
768
  `);
691
769
  success(".gitignore \uC0DD\uC131 \uC644\uB8CC");
692
770
  }
693
771
  }
694
772
  function addDeployScript(deployDir) {
695
- const pkgPath = path.join(process.cwd(), "package.json");
696
- if (!fs.existsSync(pkgPath)) {
773
+ const pkgPath = path2.join(process.cwd(), "package.json");
774
+ if (!fs2.existsSync(pkgPath)) {
697
775
  warn("package.json\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC5B4 deploy \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uCD94\uAC00\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4");
698
776
  return;
699
777
  }
700
778
  try {
701
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
779
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
702
780
  if (!pkg.scripts) pkg.scripts = {};
703
781
  if (pkg.scripts.deploy) {
704
782
  info(`package.json\uC5D0 \uC774\uBBF8 deploy \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC788\uC2B5\uB2C8\uB2E4: "${pkg.scripts.deploy}"`);
@@ -706,7 +784,7 @@ function addDeployScript(deployDir) {
706
784
  }
707
785
  const deployCmd = `connectbase-client deploy ${deployDir}`;
708
786
  pkg.scripts.deploy = pkg.scripts.build ? `${pkg.scripts.build} && ${deployCmd}` : deployCmd;
709
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
787
+ fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
710
788
  success(`package.json\uC5D0 deploy \uC2A4\uD06C\uB9BD\uD2B8 \uCD94\uAC00 \uC644\uB8CC`);
711
789
  } catch {
712
790
  warn("package.json \uC218\uC815\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4");
@@ -723,32 +801,32 @@ function getGitRoot() {
723
801
  function detectMonorepo(gitRoot) {
724
802
  const cwd = process.cwd();
725
803
  const result = { type: "none", root: gitRoot, isSubPackage: false };
726
- if (fs.existsSync(path.join(gitRoot, "pnpm-workspace.yaml"))) {
804
+ if (fs2.existsSync(path2.join(gitRoot, "pnpm-workspace.yaml"))) {
727
805
  result.type = "pnpm";
728
- } else if (fs.existsSync(path.join(gitRoot, "turbo.json"))) {
806
+ } else if (fs2.existsSync(path2.join(gitRoot, "turbo.json"))) {
729
807
  result.type = "turborepo";
730
- } else if (fs.existsSync(path.join(gitRoot, "nx.json"))) {
808
+ } else if (fs2.existsSync(path2.join(gitRoot, "nx.json"))) {
731
809
  result.type = "nx";
732
- } else if (fs.existsSync(path.join(gitRoot, "lerna.json"))) {
810
+ } else if (fs2.existsSync(path2.join(gitRoot, "lerna.json"))) {
733
811
  result.type = "lerna";
734
812
  } else {
735
- const rootPkgPath = path.join(gitRoot, "package.json");
736
- if (fs.existsSync(rootPkgPath)) {
813
+ const rootPkgPath = path2.join(gitRoot, "package.json");
814
+ if (fs2.existsSync(rootPkgPath)) {
737
815
  try {
738
- const pkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
816
+ const pkg = JSON.parse(fs2.readFileSync(rootPkgPath, "utf-8"));
739
817
  if (pkg.workspaces) {
740
- result.type = fs.existsSync(path.join(gitRoot, "yarn.lock")) ? "yarn" : "npm";
818
+ result.type = fs2.existsSync(path2.join(gitRoot, "yarn.lock")) ? "yarn" : "npm";
741
819
  }
742
820
  } catch {
743
821
  }
744
822
  }
745
823
  }
746
824
  if (result.type !== "none" && cwd !== gitRoot) {
747
- const cwdPkgPath = path.join(cwd, "package.json");
748
- if (fs.existsSync(cwdPkgPath)) {
825
+ const cwdPkgPath = path2.join(cwd, "package.json");
826
+ if (fs2.existsSync(cwdPkgPath)) {
749
827
  result.isSubPackage = true;
750
828
  try {
751
- const pkg = JSON.parse(fs.readFileSync(cwdPkgPath, "utf-8"));
829
+ const pkg = JSON.parse(fs2.readFileSync(cwdPkgPath, "utf-8"));
752
830
  result.subPackageName = pkg.name;
753
831
  } catch {
754
832
  }
@@ -765,13 +843,13 @@ async function downloadDocs(publicKey, templates, baseDir) {
765
843
  if (monorepo.type !== "none") {
766
844
  info(`\uBAA8\uB178\uB808\uD3EC \uAC10\uC9C0: ${monorepo.type} (\uB8E8\uD2B8: ${gitRoot})`);
767
845
  if (monorepo.isSubPackage) {
768
- info(`\uD604\uC7AC \uC11C\uBE0C \uD328\uD0A4\uC9C0: ${monorepo.subPackageName || path.basename(process.cwd())}`);
846
+ info(`\uD604\uC7AC \uC11C\uBE0C \uD328\uD0A4\uC9C0: ${monorepo.subPackageName || path2.basename(process.cwd())}`);
769
847
  }
770
848
  }
771
- const docsDir = path.join(gitRoot, ".claude", "docs");
772
- const rootClaudeMd = path.join(gitRoot, "CLAUDE.md");
773
- if (!fs.existsSync(docsDir)) {
774
- fs.mkdirSync(docsDir, { recursive: true });
849
+ const docsDir = path2.join(gitRoot, ".claude", "docs");
850
+ const rootClaudeMd = path2.join(gitRoot, "CLAUDE.md");
851
+ if (!fs2.existsSync(docsDir)) {
852
+ fs2.mkdirSync(docsDir, { recursive: true });
775
853
  }
776
854
  if (!templates) {
777
855
  templates = ["rules"];
@@ -790,13 +868,13 @@ async function downloadDocs(publicKey, templates, baseDir) {
790
868
  const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
791
869
  const sections = data.sections || [];
792
870
  for (const section of sections) {
793
- const filePath = path.join(docsDir, section.filename);
794
- fs.writeFileSync(filePath, section.content);
871
+ const filePath = path2.join(docsDir, section.filename);
872
+ fs2.writeFileSync(filePath, section.content);
795
873
  }
796
874
  success(`${gitRoot}/.claude/docs/ \uC5D0 ${sections.length}\uAC1C \uBB38\uC11C \uC800\uC7A5 \uC644\uB8CC`);
797
875
  updateRootClaudeMd(rootClaudeMd);
798
876
  if (monorepo.isSubPackage) {
799
- const subClaudeMd = path.join(process.cwd(), "CLAUDE.md");
877
+ const subClaudeMd = path2.join(process.cwd(), "CLAUDE.md");
800
878
  createSubPackageClaudeMd(subClaudeMd, gitRoot);
801
879
  }
802
880
  log(`${colors.dim}\u203B SDK \uBB38\uC11C\uB294 MCP search_sdk_docs\uB85C \uAC80\uC0C9\uD558\uC138\uC694${colors.reset}`);
@@ -808,10 +886,10 @@ async function downloadDocs(publicKey, templates, baseDir) {
808
886
 
809
887
  \uC0C1\uC138 \uAD6C\uD604\uBC95\uC740 MCP \`search_sdk_docs\` \uB3C4\uAD6C\uB85C \uAC80\uC0C9\uD558\uC138\uC694.
810
888
  `;
811
- fs.writeFileSync(path.join(docsDir, "project-rules.md"), fallbackContent);
889
+ fs2.writeFileSync(path2.join(docsDir, "project-rules.md"), fallbackContent);
812
890
  updateRootClaudeMd(rootClaudeMd);
813
891
  if (monorepo.isSubPackage) {
814
- const subClaudeMd = path.join(process.cwd(), "CLAUDE.md");
892
+ const subClaudeMd = path2.join(process.cwd(), "CLAUDE.md");
815
893
  createSubPackageClaudeMd(subClaudeMd, gitRoot);
816
894
  }
817
895
  success(`.claude/docs/ \uC0DD\uC131 \uC644\uB8CC (\uAE30\uBCF8)`);
@@ -831,8 +909,8 @@ function updateRootClaudeMd(claudeMdPath) {
831
909
  2. \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
832
910
  3. \uAC80\uC0C9 \uACB0\uACFC\uC758 \uCF54\uB4DC \uD328\uD134\uC744 \uB530\uB77C \uAD6C\uD604\uD558\uC138\uC694
833
911
  ${endMarker}`;
834
- if (fs.existsSync(claudeMdPath)) {
835
- let content = fs.readFileSync(claudeMdPath, "utf-8");
912
+ if (fs2.existsSync(claudeMdPath)) {
913
+ let content = fs2.readFileSync(claudeMdPath, "utf-8");
836
914
  const startIdx = content.indexOf(startMarker);
837
915
  const endIdx = content.indexOf(endMarker);
838
916
  if (startIdx !== -1 && endIdx !== -1) {
@@ -840,10 +918,10 @@ ${endMarker}`;
840
918
  } else {
841
919
  content = sdkBlock + "\n\n" + content.trimStart();
842
920
  }
843
- fs.writeFileSync(claudeMdPath, content);
921
+ fs2.writeFileSync(claudeMdPath, content);
844
922
  info("CLAUDE.md\uC5D0 ConnectBase \uCC38\uC870 \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC");
845
923
  } else {
846
- fs.writeFileSync(claudeMdPath, `# \uD504\uB85C\uC81D\uD2B8
924
+ fs2.writeFileSync(claudeMdPath, `# \uD504\uB85C\uC81D\uD2B8
847
925
 
848
926
  ${sdkBlock}
849
927
  `);
@@ -853,7 +931,7 @@ ${sdkBlock}
853
931
  function createSubPackageClaudeMd(subClaudeMdPath, gitRoot) {
854
932
  const startMarker = "<!-- CONNECTBASE_SUB_START -->";
855
933
  const endMarker = "<!-- CONNECTBASE_SUB_END -->";
856
- const relPath = path.relative(path.dirname(subClaudeMdPath), gitRoot);
934
+ const relPath = path2.relative(path2.dirname(subClaudeMdPath), gitRoot);
857
935
  const subBlock = `${startMarker}
858
936
  ## ConnectBase SDK
859
937
 
@@ -865,8 +943,8 @@ function createSubPackageClaudeMd(subClaudeMdPath, gitRoot) {
865
943
  2. \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C SDK \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
866
944
  3. \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC758 \`${relPath}/CLAUDE.md\`\uB3C4 \uCC38\uACE0\uD558\uC138\uC694
867
945
  ${endMarker}`;
868
- if (fs.existsSync(subClaudeMdPath)) {
869
- let content = fs.readFileSync(subClaudeMdPath, "utf-8");
946
+ if (fs2.existsSync(subClaudeMdPath)) {
947
+ let content = fs2.readFileSync(subClaudeMdPath, "utf-8");
870
948
  const startIdx = content.indexOf(startMarker);
871
949
  const endIdx = content.indexOf(endMarker);
872
950
  if (startIdx !== -1 && endIdx !== -1) {
@@ -876,9 +954,9 @@ ${endMarker}`;
876
954
  } else {
877
955
  return;
878
956
  }
879
- fs.writeFileSync(subClaudeMdPath, content);
957
+ fs2.writeFileSync(subClaudeMdPath, content);
880
958
  } else {
881
- fs.writeFileSync(subClaudeMdPath, `# ${path.basename(path.dirname(subClaudeMdPath))}
959
+ fs2.writeFileSync(subClaudeMdPath, `# ${path2.basename(path2.dirname(subClaudeMdPath))}
882
960
 
883
961
  ${subBlock}
884
962
  `);
@@ -895,7 +973,7 @@ async function setupMcp(secretKey) {
895
973
  info(`MCP \uC124\uC815\uC740 \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8(${root})\uC5D0 \uC0DD\uC131\uB429\uB2C8\uB2E4`);
896
974
  }
897
975
  }
898
- const mcpConfigPath = path.join(root, ".mcp.json");
976
+ const mcpConfigPath = path2.join(root, ".mcp.json");
899
977
  if (!secretKey) {
900
978
  log(`
901
979
  ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${colors.reset}`);
@@ -906,16 +984,16 @@ ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${co
906
984
  if (authChoice === "2") {
907
985
  try {
908
986
  secretKey = await browserAuthFlow();
909
- const skRcPath = path.join(process.cwd(), ".connectbaserc");
987
+ const skRcPath = path2.join(process.cwd(), ".connectbaserc");
910
988
  let rcData = {};
911
- if (fs.existsSync(skRcPath)) {
989
+ if (fs2.existsSync(skRcPath)) {
912
990
  try {
913
- rcData = JSON.parse(fs.readFileSync(skRcPath, "utf-8"));
991
+ rcData = JSON.parse(fs2.readFileSync(skRcPath, "utf-8"));
914
992
  } catch {
915
993
  }
916
994
  }
917
995
  rcData.secretKey = secretKey;
918
- fs.writeFileSync(skRcPath, JSON.stringify(rcData, null, 2) + "\n");
996
+ fs2.writeFileSync(skRcPath, JSON.stringify(rcData, null, 2) + "\n");
919
997
  addToGitignore(".connectbaserc", root);
920
998
  success("Secret Key \uBC1C\uAE09 \uBC0F \uC800\uC7A5 \uC644\uB8CC");
921
999
  } catch (err) {
@@ -934,21 +1012,21 @@ ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${co
934
1012
  }
935
1013
  };
936
1014
  let mcpConfig = { mcpServers: {} };
937
- if (fs.existsSync(mcpConfigPath)) {
1015
+ if (fs2.existsSync(mcpConfigPath)) {
938
1016
  try {
939
- mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
1017
+ mcpConfig = JSON.parse(fs2.readFileSync(mcpConfigPath, "utf-8"));
940
1018
  if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== "object") {
941
1019
  mcpConfig.mcpServers = {};
942
1020
  }
943
1021
  } catch {
944
1022
  const backupPath = mcpConfigPath + ".backup";
945
- fs.copyFileSync(mcpConfigPath, backupPath);
1023
+ fs2.copyFileSync(mcpConfigPath, backupPath);
946
1024
  warn(`.mcp.json \uD30C\uC2F1 \uC2E4\uD328, \uBC31\uC5C5 \uC0DD\uC131: ${backupPath}`);
947
1025
  mcpConfig = { mcpServers: {} };
948
1026
  }
949
1027
  }
950
1028
  mcpConfig.mcpServers["connect-base"] = mcpEntry;
951
- fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n");
1029
+ fs2.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n");
952
1030
  success(`${mcpConfigPath} \uC0DD\uC131 \uC644\uB8CC`);
953
1031
  addToGitignore(".mcp.json", root);
954
1032
  if (secretKey && secretKey !== "YOUR_SECRET_KEY_HERE") {
@@ -1124,7 +1202,7 @@ ${colors.blue}?${colors.reset} \uC571 \uC120\uD0DD (\uBC88\uD638): `);
1124
1202
  success(`\uC120\uD0DD\uB428: ${apps[num - 1].name}`);
1125
1203
  return apps[num - 1].id;
1126
1204
  }
1127
- const projectName = path.basename(process.cwd());
1205
+ const projectName = path2.basename(process.cwd());
1128
1206
  const appName = await prompt(`${colors.blue}?${colors.reset} \uC571 \uC774\uB984 (${projectName}): `) || projectName;
1129
1207
  info("\uC571 \uC0DD\uC131 \uC911...");
1130
1208
  const createRes = await makeRequest(
@@ -1146,22 +1224,32 @@ ${colors.blue}?${colors.reset} \uC571 \uC120\uD0DD (\uBC88\uD638): `);
1146
1224
  success(`\uC571 \uC0DD\uC131 \uC644\uB8CC: ${createData.app_name}`);
1147
1225
  return createData.app_id;
1148
1226
  }
1227
+ function acquireTunnelLock2(appID, port, force) {
1228
+ const result = acquireTunnelLock(appID, port, force, VERSION);
1229
+ if (result && result.startsWith("LOCKED:")) {
1230
+ const [, pid, startedAt, host] = result.split(":");
1231
+ error(`\uC774\uBBF8 \uC2E4\uD589 \uC911\uC778 \uD130\uB110\uC774 \uC788\uC2B5\uB2C8\uB2E4: PID ${pid}, \uC2DC\uC791 ${startedAt}, \uD638\uC2A4\uD2B8 ${host}`);
1232
+ info("\uC911\uBCF5 \uC2E4\uD589\uC744 \uBB34\uC2DC\uD558\uB824\uBA74 --force \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uC138\uC694");
1233
+ process.exit(2);
1234
+ }
1235
+ return result;
1236
+ }
1149
1237
  async function startTunnel(port, config, tunnelOpts) {
1150
1238
  let tunnelKey = config.secretKey || (config.publicKey?.startsWith("cb_sk_") ? config.publicKey : "");
1151
1239
  if (!tunnelKey) {
1152
1240
  info("Secret Key\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC73C\uB85C \uBC1C\uAE09\uD569\uB2C8\uB2E4...");
1153
1241
  try {
1154
1242
  const sk = await browserAuthFlow();
1155
- const rcPath2 = path.join(process.cwd(), ".connectbaserc");
1243
+ const rcPath2 = path2.join(process.cwd(), ".connectbaserc");
1156
1244
  let rcData = {};
1157
- if (fs.existsSync(rcPath2)) {
1245
+ if (fs2.existsSync(rcPath2)) {
1158
1246
  try {
1159
- rcData = JSON.parse(fs.readFileSync(rcPath2, "utf-8"));
1247
+ rcData = JSON.parse(fs2.readFileSync(rcPath2, "utf-8"));
1160
1248
  } catch {
1161
1249
  }
1162
1250
  }
1163
1251
  rcData.secretKey = sk;
1164
- fs.writeFileSync(rcPath2, JSON.stringify(rcData, null, 2) + "\n");
1252
+ fs2.writeFileSync(rcPath2, JSON.stringify(rcData, null, 2) + "\n");
1165
1253
  addToGitignore(".connectbaserc");
1166
1254
  success("Secret Key \uBC1C\uAE09 \uBC0F \uC800\uC7A5 \uC644\uB8CC");
1167
1255
  tunnelKey = sk;
@@ -1174,11 +1262,11 @@ async function startTunnel(port, config, tunnelOpts) {
1174
1262
  } else {
1175
1263
  config.secretKey = tunnelKey;
1176
1264
  }
1177
- const rcPath = path.join(process.cwd(), ".connectbaserc");
1265
+ const rcPath = path2.join(process.cwd(), ".connectbaserc");
1178
1266
  let savedAppId = tunnelOpts?.appId || "";
1179
1267
  if (!savedAppId) {
1180
1268
  try {
1181
- const rc = JSON.parse(fs.readFileSync(rcPath, "utf-8"));
1269
+ const rc = JSON.parse(fs2.readFileSync(rcPath, "utf-8"));
1182
1270
  if (rc.tunnelAppId) savedAppId = rc.tunnelAppId;
1183
1271
  } catch {
1184
1272
  }
@@ -1186,18 +1274,19 @@ async function startTunnel(port, config, tunnelOpts) {
1186
1274
  const appId = await resolveAppForTunnel(tunnelKey, config.baseUrl, savedAppId);
1187
1275
  try {
1188
1276
  let rcData = {};
1189
- if (fs.existsSync(rcPath)) {
1277
+ if (fs2.existsSync(rcPath)) {
1190
1278
  try {
1191
- rcData = JSON.parse(fs.readFileSync(rcPath, "utf-8"));
1279
+ rcData = JSON.parse(fs2.readFileSync(rcPath, "utf-8"));
1192
1280
  } catch {
1193
1281
  }
1194
1282
  }
1195
1283
  if (rcData.tunnelAppId !== appId) {
1196
1284
  rcData.tunnelAppId = appId;
1197
- fs.writeFileSync(rcPath, JSON.stringify(rcData, null, 2) + "\n");
1285
+ fs2.writeFileSync(rcPath, JSON.stringify(rcData, null, 2) + "\n");
1198
1286
  }
1199
1287
  } catch {
1200
1288
  }
1289
+ const lockPath = acquireTunnelLock2(appId, port, tunnelOpts?.force ?? false);
1201
1290
  const tunnelServerUrl = getTunnelServerUrl(config.baseUrl);
1202
1291
  const parsedUrl = new URL(tunnelServerUrl);
1203
1292
  const isHttps = parsedUrl.protocol === "https:";
@@ -1212,8 +1301,17 @@ async function startTunnel(port, config, tunnelOpts) {
1212
1301
  const maxReconnectAttempts = 10;
1213
1302
  let shouldReconnect = true;
1214
1303
  let socket = null;
1304
+ let lockReleased = false;
1305
+ const releaseLock = () => {
1306
+ if (lockPath && !lockReleased) {
1307
+ lockReleased = true;
1308
+ releaseTunnelLock(lockPath);
1309
+ }
1310
+ };
1311
+ process.on("exit", releaseLock);
1215
1312
  const cleanup = () => {
1216
1313
  shouldReconnect = false;
1314
+ releaseLock();
1217
1315
  if (socket) {
1218
1316
  try {
1219
1317
  socket.write(createWsCloseFrame(1e3));
@@ -1343,9 +1441,15 @@ ${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
1343
1441
  case "http_request":
1344
1442
  forwardRequest(msg, sock, localPort);
1345
1443
  break;
1346
- case "tunnel_error":
1347
- error(`\uD130\uB110 \uC5D0\uB7EC: ${msg.message}`);
1444
+ case "tunnel_error": {
1445
+ const result = handleTunnelError(msg, appId, localPort);
1446
+ error(result.message);
1447
+ if (result.action === "exit") {
1448
+ shouldReconnect = false;
1449
+ setTimeout(() => process.exit(result.exitCode ?? 1), 500);
1450
+ }
1348
1451
  break;
1452
+ }
1349
1453
  case "ping":
1350
1454
  sock.write(createWsTextFrame(JSON.stringify({ type: "pong" })));
1351
1455
  break;
@@ -1497,6 +1601,7 @@ ${colors.yellow}\uC635\uC158:${colors.reset}
1497
1601
  -u, --base-url <url> \uC11C\uBC84 URL (\uAE30\uBCF8: ${DEFAULT_BASE_URL})
1498
1602
  -t, --timeout <sec> \uD130\uB110 \uC694\uCCAD \uD0C0\uC784\uC544\uC6C3 (\uCD08, tunnel \uC804\uC6A9)
1499
1603
  --max-body <MB> \uD130\uB110 \uCD5C\uB300 \uBC14\uB514 \uD06C\uAE30 (MB, tunnel \uC804\uC6A9)
1604
+ --force \uD130\uB110 lockfile \uBB34\uC2DC (\uC911\uBCF5 \uC2E4\uD589 \uAC15\uC81C, tunnel \uC804\uC6A9)
1500
1605
  -d, --dev Dev \uD658\uACBD\uC5D0 \uBC30\uD3EC (deploy \uC804\uC6A9)
1501
1606
  (docs, mcp\uB294 \uBAA8\uB178\uB808\uD3EC\uB97C \uC790\uB3D9 \uAC10\uC9C0\uD558\uC5EC \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC5D0 \uC0DD\uC131)
1502
1607
  -h, --help \uB3C4\uC6C0\uB9D0 \uD45C\uC2DC
@@ -1562,6 +1667,8 @@ function parseArgs(args) {
1562
1667
  result.options.maxBody = args[++i];
1563
1668
  } else if (arg === "-a" || arg === "--app") {
1564
1669
  result.options.appId = args[++i];
1670
+ } else if (arg === "--force") {
1671
+ result.options.force = "true";
1565
1672
  } else if (arg === "-d" || arg === "--dev") {
1566
1673
  result.options.dev = "true";
1567
1674
  } else if (arg === "-h" || arg === "--help") {
@@ -1645,6 +1752,9 @@ async function main() {
1645
1752
  if (parsed.options.appId) {
1646
1753
  tunnelOpts.appId = parsed.options.appId;
1647
1754
  }
1755
+ if (parsed.options.force === "true") {
1756
+ tunnelOpts.force = true;
1757
+ }
1648
1758
  await startTunnel(port, config, tunnelOpts);
1649
1759
  } else {
1650
1760
  error(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${parsed.command}`);