connectbase-client 3.42.0 → 3.44.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/cli.js +415 -176
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,50 @@
3
3
  본 SDK 의 모든 주요 변경사항을 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 형식으로 기록합니다.
4
4
  버전은 [Semantic Versioning](https://semver.org/lang/ko/) 을 따릅니다.
5
5
 
6
+ ## [3.44.0] - 2026-07-03
7
+
8
+ ### Added — CLI 비대화형 init + SDK 문서 수명주기 정비
9
+
10
+ AI 에이전트(Claude Code 등)가 `init` 을 직접 실행할 수 있도록 비대화형 모드를 추가하고,
11
+ `.claude/docs/` 문서의 신선도/정리 체계를 도입했다.
12
+
13
+ - **비대화형 init**: `connectbase init --yes --secret-key cb_sk_...` — 프롬프트 없이 진행.
14
+ 비TTY 환경에서 `--yes` 없이 실행하면 hang 대신 필요한 플래그를 안내하고 즉시 종료.
15
+ `--app <앱ID>`/`--storage <스토리지ID>`/`--deploy-dir <dir>` 로 명시 지정, `--yes` 는
16
+ 프로젝트 이름과 같은 앱/스토리지가 있으면 재사용(반복 실행에 멱등). Secret Key 는
17
+ `CONNECTBASE_SECRET_KEY` 환경변수로도 전달 가능.
18
+ - **Public Key 스프롤 방지**: `.connectbaserc` 에 `appId` 를 저장하고, 같은 앱을 다시 init
19
+ 하면 기존 Public Key 를 재사용 (이전에는 실행마다 새 키 발급).
20
+ - **문서 생성 스탬프 + 매니페스트**: `.claude/docs/` 파일에 생성 CLI 버전/날짜 스탬프를 넣고
21
+ `.connectbase-manifest.json` 으로 추적 — 콘텐츠 해시로 신선도 비교, 서버에서 사라진 문서
22
+ 자동 정리(레거시 fullstack 파일 포함), `deploy` 성공 후 변경이 있을 때만 조용히 자동 갱신
23
+ (`--skip-docs` 로 생략 가능).
24
+ - **CLAUDE.md @import**: 참조 블록이 `@.claude/docs/project-rules.md` import 로 규칙을 매
25
+ 세션 자동 로드. MCP 미연결 시 `npx connectbase mcp` 안내 지시 포함. `mcp` 명령도 비대화형
26
+ 지원(`--secret-key`/env/.connectbaserc 자동 사용).
27
+
28
+ ### Changed / Security
29
+
30
+ - **키를 쿼리스트링에 싣지 않음**: 문서 다운로드가 `public_key` 쿼리 파라미터를 더 이상
31
+ 전송하지 않는다 (서버 측 키 치환은 플레이스홀더 불일치로 오래전부터 무동작 — 액세스 로그에
32
+ 키가 남는 위험만 있었음). `update` 의 secretKey fallback 도 제거.
33
+ - 모든 CLI 요청에 `User-Agent: connectbase-cli/<version>` 명시.
34
+ - `detectBuildDir`: Next.js 는 `output: 'export'` 미설정 시 경고 안내.
35
+ - 버전 비교가 prerelease(`-beta.1`) 형식에서 NaN 이 되던 문제 수정.
36
+ - `.gitignore` 항목 추가가 부분 문자열 오탐으로 건너뛰던 문제 수정 (라인 단위 정확 매칭).
37
+
38
+ ## [3.43.0] - 2026-07-02
39
+
40
+ ### Added — `--dev` 배포도 대용량 바이너리 지원
41
+
42
+ dev 배포(`connectbase deploy --dev`)는 DB 를 거치지 않고 파일을 dev Object Storage 경로에 직접
43
+ 올리므로 6 MiB(TiDB 엔트리) 한도가 애초에 없다. 3.41.0~3.42.0 은 dev 에서도 fail-fast 로 대용량을
44
+ 막았는데(과도한 제한), 이를 풀고 prod 와 동일하게 처리한다.
45
+
46
+ - 대용량 **바이너리**는 presign(`dev:true`)으로 dev 서빙 경로(`{id}/dev{path}`)에 직접 업로드하고 바디에서 제외(효율).
47
+ - 대응 core-server 가 없으면(구버전) 오프로드 없이 전량 base64 바디로 전송 — dev 는 DB 한도가 없어 안전.
48
+ - dev 는 이제 대용량 파일에 대해 fail-fast 하지 않는다.
49
+
6
50
  ## [3.42.0] - 2026-07-02
7
51
 
8
52
  ### Added — 웹 배포 대용량 바이너리 Object Storage 직접 업로드 (플랫폼 이슈 019f221b)
package/dist/cli.js CHANGED
@@ -349,8 +349,8 @@ function validateFileSizes(files) {
349
349
  warn("\uC62C\uB9AC\uACE0 URL \uB85C \uCC38\uC870\uD558\uC138\uC694. \uD30C\uC77C \uC2A4\uD1A0\uB9AC\uC9C0\uB294 \uD30C\uC77C\uB2F9 \uACE0\uC815 \uC0C1\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
350
350
  process.exit(1);
351
351
  }
352
- async function requestDeployPresign(baseStorageUrl, headers, files, timeoutMs) {
353
- const body = JSON.stringify({ files });
352
+ async function requestDeployPresign(baseStorageUrl, headers, files, timeoutMs, dev = false) {
353
+ const body = JSON.stringify(dev ? { files, dev: true } : { files });
354
354
  const res = await makeRequest(`${baseStorageUrl}/deploy/presign`, "POST", headers, body, { timeoutMs });
355
355
  if (res.status === 404) return null;
356
356
  if (res.status < 200 || res.status >= 300) {
@@ -466,6 +466,50 @@ async function offloadLargeBinaries(baseStorageUrl, headers, diff, _envLabel, ti
466
466
  f.content = "";
467
467
  }
468
468
  }
469
+ async function offloadDevLargeBinaries(baseStorageUrl, headers, files, timeoutMs) {
470
+ const large = files.filter((f) => f.isBinary && Buffer.byteLength(f.content, "utf8") > WEB_FILE_MAX_STORED_BYTES);
471
+ if (large.length === 0) return files;
472
+ const presignFiles = large.map((f) => ({
473
+ path: f.path,
474
+ size: Buffer.from(f.content, "base64").length,
475
+ mime_type: guessMimeType(f.path)
476
+ }));
477
+ let presigned;
478
+ try {
479
+ presigned = await requestDeployPresign(baseStorageUrl, headers, presignFiles, timeoutMs, true);
480
+ } catch (err) {
481
+ error(`dev \uB300\uC6A9\uB7C9 \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC900\uBE44 \uC2E4\uD328: ${err instanceof Error ? err.message : err}`);
482
+ process.exit(1);
483
+ }
484
+ if (presigned === null) {
485
+ info("\uC11C\uBC84\uAC00 \uB300\uC6A9\uB7C9 \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uB97C \uBBF8\uC9C0\uC6D0 \u2014 dev \uB294 \uC804\uB7C9 \uC5C5\uB85C\uB4DC\uB85C \uC9C4\uD589\uD569\uB2C8\uB2E4.");
486
+ return files;
487
+ }
488
+ if (!presigned.every((p) => p.object_key.includes("/dev/"))) {
489
+ info("\uC11C\uBC84\uAC00 dev \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uB97C \uBBF8\uC9C0\uC6D0(\uC751\uB2F5 \uD0A4\uAC00 dev \uACBD\uB85C \uC544\uB2D8) \u2014 dev \uB294 \uC804\uB7C9 \uC5C5\uB85C\uB4DC\uB85C \uC9C4\uD589\uD569\uB2C8\uB2E4.");
490
+ return files;
491
+ }
492
+ const urlByPath = new Map(presigned.map((p) => [p.path, p.upload_url]));
493
+ info(`\uB300\uC6A9\uB7C9 \uBC14\uC774\uB108\uB9AC ${large.length}\uAC1C\uB97C dev Object Storage \uB85C \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4...`);
494
+ const offloaded = /* @__PURE__ */ new Set();
495
+ for (const f of large) {
496
+ const uploadUrl = urlByPath.get(f.path);
497
+ if (!uploadUrl) {
498
+ error(`presign \uC751\uB2F5\uC5D0 \uC5C5\uB85C\uB4DC URL \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${f.path}`);
499
+ process.exit(1);
500
+ }
501
+ const bytes = Buffer.from(f.content, "base64");
502
+ try {
503
+ await putToPresignedUrl(uploadUrl, bytes, guessMimeType(f.path), timeoutMs);
504
+ } catch (err) {
505
+ error(`dev \uC5C5\uB85C\uB4DC \uC2E4\uD328 (${f.path}): ${err instanceof Error ? err.message : err}`);
506
+ process.exit(1);
507
+ }
508
+ success(`\uC9C1\uC811 \uC5C5\uB85C\uB4DC(dev): ${f.path} (${formatSize(bytes.length)})`);
509
+ offloaded.add(f.path);
510
+ }
511
+ return files.filter((f) => !offloaded.has(f.path));
512
+ }
469
513
  var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
470
514
  async function makeRequest(url, method, headers, body, reqOpts = {}) {
471
515
  return new Promise((resolve2, reject) => {
@@ -481,6 +525,8 @@ async function makeRequest(url, method, headers, body, reqOpts = {}) {
481
525
  family: 4,
482
526
  autoSelectFamily: false,
483
527
  headers: {
528
+ // 명시적 UA: 관측성 + WAF 봇 휴리스틱(비브라우저 UA 차단 규칙)과의 충돌 방지
529
+ "User-Agent": `connectbase-cli/${VERSION}`,
484
530
  ...headers,
485
531
  "Content-Type": "application/json",
486
532
  ...bodyBuffer ? { "Content-Length": bodyBuffer.length } : {}
@@ -572,8 +618,8 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
572
618
  const timeoutMs = computeDeployTimeout(totalSize, deployOpts.timeoutMs);
573
619
  log(`${colors.dim}\uD0C0\uC784\uC544\uC6C3: ${Math.round(timeoutMs / 1e3)}\uCD08${colors.reset}`);
574
620
  if (isDev) {
575
- validateFileSizes(files);
576
- await fullDeploy(baseStorageUrl, headers, files, envLabel, "deploy/dev", timeoutMs);
621
+ const toSend = await offloadDevLargeBinaries(baseStorageUrl, headers, files, timeoutMs);
622
+ await fullDeploy(baseStorageUrl, headers, toSend, envLabel, "deploy/dev", timeoutMs);
577
623
  return;
578
624
  }
579
625
  try {
@@ -861,7 +907,7 @@ ${colors.cyan}\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB85C\uADF8\uC778\uD558\uC13
861
907
  error("\uC778\uC99D \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.");
862
908
  process.exit(1);
863
909
  }
864
- async function init() {
910
+ async function init(config = {}, opts = {}) {
865
911
  log(`
866
912
  ${colors.cyan}Connect Base \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654${colors.reset}
867
913
  `);
@@ -872,49 +918,104 @@ ${colors.cyan}Connect Base \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654${colors.r
872
918
  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
873
919
  `);
874
920
  }
921
+ const nonInteractive = opts.yes === true;
922
+ if (!process.stdin.isTTY && !nonInteractive) {
923
+ error("\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC785\uB2C8\uB2E4. --yes \uD50C\uB798\uADF8\uC640 \uD568\uAED8 \uD544\uC694\uD55C \uAC12\uC744 \uC9C0\uC815\uD558\uC138\uC694");
924
+ log(`${colors.dim}\uC608: npx connectbase init --yes --secret-key cb_sk_... [--app <\uC571ID>] [--storage <\uC2A4\uD1A0\uB9AC\uC9C0ID>] [--deploy-dir ./dist]${colors.reset}`);
925
+ log(`${colors.dim}Secret Key\uB294 CONNECTBASE_SECRET_KEY \uD658\uACBD\uBCC0\uC218\uB85C\uB3C4 \uC804\uB2EC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4${colors.reset}`);
926
+ process.exit(1);
927
+ }
875
928
  const rcPath = path2.join(cwd, ".connectbaserc");
929
+ let existingRc = {};
876
930
  if (fs2.existsSync(rcPath)) {
877
- 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): `);
878
- if (overwrite.toLowerCase() !== "y") {
879
- info("\uCD08\uAE30\uD654\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
880
- return;
931
+ try {
932
+ existingRc = JSON.parse(fs2.readFileSync(rcPath, "utf-8"));
933
+ } catch {
934
+ }
935
+ if (nonInteractive) {
936
+ if (!opts.force) {
937
+ error(".connectbaserc\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uB36E\uC5B4\uC4F0\uB824\uBA74 --force\uB97C \uD568\uAED8 \uC0AC\uC6A9\uD558\uC138\uC694");
938
+ process.exit(1);
939
+ }
940
+ } else {
941
+ 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): `);
942
+ if (overwrite.toLowerCase() !== "y") {
943
+ info("\uCD08\uAE30\uD654\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
944
+ return;
945
+ }
881
946
  }
882
947
  }
883
- log(`
884
- ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${colors.reset}`);
885
- log(` ${colors.cyan}1${colors.reset}) Secret Key \uC9C1\uC811 \uC785\uB825 (cb_sk_...)`);
886
- log(` ${colors.cyan}2${colors.reset}) \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC73C\uB85C \uC790\uB3D9 \uBC1C\uAE09
887
- `);
888
- const authChoice = await prompt(`${colors.blue}?${colors.reset} \uC120\uD0DD (1/2): `);
889
- let secretKey = "";
890
- if (authChoice === "2") {
891
- try {
892
- secretKey = await browserAuthFlow();
893
- } catch (err) {
894
- error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
895
- 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");
948
+ let secretKey = config.secretKey || "";
949
+ if (secretKey) {
950
+ if (!secretKey.startsWith("cb_sk_")) {
951
+ error("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uD0A4 \uD615\uC2DD\uC785\uB2C8\uB2E4. cb_sk_\uB85C \uC2DC\uC791\uD558\uB294 Secret Key\uB97C \uC9C0\uC815\uD558\uC138\uC694");
896
952
  process.exit(1);
897
953
  }
954
+ info("Secret Key \uD655\uC778\uB428 (\uD50C\uB798\uADF8/\uD658\uACBD\uBCC0\uC218/.connectbaserc)");
955
+ } else if (nonInteractive) {
956
+ error("Secret Key\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4: --secret-key cb_sk_... \uB610\uB294 CONNECTBASE_SECRET_KEY \uD658\uACBD\uBCC0\uC218");
957
+ process.exit(1);
898
958
  } else {
899
- log(`${colors.dim}Secret Key (cb_sk_): \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uBC1C\uAE09${colors.reset}
959
+ log(`
960
+ ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${colors.reset}`);
961
+ log(` ${colors.cyan}1${colors.reset}) Secret Key \uC9C1\uC811 \uC785\uB825 (cb_sk_...)`);
962
+ log(` ${colors.cyan}2${colors.reset}) \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC73C\uB85C \uC790\uB3D9 \uBC1C\uAE09
900
963
  `);
901
- secretKey = await promptSecret(`${colors.blue}?${colors.reset} Secret Key: `);
902
- if (!secretKey) {
903
- error("Secret Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
904
- process.exit(1);
905
- }
906
- if (secretKey.startsWith("cb_pk_")) {
907
- error("Public Key\uAC00 \uC544\uB2CC Secret Key(cb_sk_)\uB97C \uC785\uB825\uD558\uC138\uC694");
908
- info("Secret Key\uB294 \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uC0DD\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
909
- process.exit(1);
910
- }
911
- if (!secretKey.startsWith("cb_sk_")) {
912
- 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");
913
- process.exit(1);
964
+ const authChoice = await prompt(`${colors.blue}?${colors.reset} \uC120\uD0DD (1/2): `);
965
+ if (authChoice === "2") {
966
+ try {
967
+ secretKey = await browserAuthFlow();
968
+ } catch (err) {
969
+ error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
970
+ 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");
971
+ process.exit(1);
972
+ }
973
+ } else {
974
+ log(`${colors.dim}Secret Key (cb_sk_): \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uBC1C\uAE09${colors.reset}
975
+ `);
976
+ secretKey = await promptSecret(`${colors.blue}?${colors.reset} Secret Key: `);
977
+ if (!secretKey) {
978
+ error("Secret Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
979
+ process.exit(1);
980
+ }
981
+ if (secretKey.startsWith("cb_pk_")) {
982
+ error("Public Key\uAC00 \uC544\uB2CC Secret Key(cb_sk_)\uB97C \uC785\uB825\uD558\uC138\uC694");
983
+ info("Secret Key\uB294 \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uC0DD\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
984
+ process.exit(1);
985
+ }
986
+ if (!secretKey.startsWith("cb_sk_")) {
987
+ 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");
988
+ process.exit(1);
989
+ }
914
990
  }
915
991
  }
916
992
  let appId = "";
917
993
  let publicKey = "";
994
+ const projectName = path2.basename(cwd);
995
+ const reuseKeyIfSameApp = (selectedAppId) => {
996
+ if (existingRc.appId === selectedAppId && existingRc.publicKey?.startsWith("cb_pk_")) {
997
+ publicKey = existingRc.publicKey;
998
+ success("\uAE30\uC874 Public Key \uC7AC\uC0AC\uC6A9 (.connectbaserc)");
999
+ return true;
1000
+ }
1001
+ return false;
1002
+ };
1003
+ const issuePublicKey = async (targetAppId) => {
1004
+ info("Public Key \uC0DD\uC131 \uC911...");
1005
+ const newKeyRes = await makeRequest(
1006
+ `${DEFAULT_BASE_URL}/v1/public/cli/apps/${targetAppId}/public-keys`,
1007
+ "POST",
1008
+ { "X-Public-Key": secretKey },
1009
+ JSON.stringify({})
1010
+ );
1011
+ if (newKeyRes.status === 201) {
1012
+ const keyData = newKeyRes.data;
1013
+ publicKey = keyData.key;
1014
+ success("Public Key \uBC1C\uAE09 \uC644\uB8CC");
1015
+ } else {
1016
+ warn("Public Key \uC790\uB3D9 \uC0DD\uC131 \uC2E4\uD328. \uC218\uB3D9\uC73C\uB85C \uC0DD\uC131\uD558\uC138\uC694");
1017
+ }
1018
+ };
918
1019
  try {
919
1020
  info("\uC571 \uBAA9\uB85D \uC870\uD68C \uC911...");
920
1021
  const appsRes = await makeRequest(
@@ -931,7 +1032,23 @@ ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${co
931
1032
  }
932
1033
  const appsData = appsRes.data;
933
1034
  const apps = appsData.apps || [];
934
- if (apps.length > 0) {
1035
+ if (opts.appId) {
1036
+ const found = apps.find((a) => a.id === opts.appId);
1037
+ if (!found) {
1038
+ error(`\uC9C0\uC815\uD55C \uC571\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.appId}`);
1039
+ process.exit(1);
1040
+ }
1041
+ appId = found.id;
1042
+ success(`\uC120\uD0DD\uB428: ${found.name}`);
1043
+ if (!reuseKeyIfSameApp(appId)) await issuePublicKey(appId);
1044
+ } else if (nonInteractive) {
1045
+ const found = apps.find((a) => a.name === projectName);
1046
+ if (found) {
1047
+ appId = found.id;
1048
+ success(`\uAE30\uC874 \uC571 \uC7AC\uC0AC\uC6A9: ${found.name}`);
1049
+ if (!reuseKeyIfSameApp(appId)) await issuePublicKey(appId);
1050
+ }
1051
+ } else if (apps.length > 0) {
935
1052
  log(`
936
1053
  ${colors.dim}\uB0B4 \uC571 \uBAA9\uB85D:${colors.reset}`);
937
1054
  apps.forEach((a, i) => {
@@ -945,25 +1062,11 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
945
1062
  if (num > 0 && num <= apps.length) {
946
1063
  appId = apps[num - 1].id;
947
1064
  success(`\uC120\uD0DD\uB428: ${apps[num - 1].name}`);
948
- info("Public Key \uC0DD\uC131 \uC911...");
949
- const newKeyRes = await makeRequest(
950
- `${DEFAULT_BASE_URL}/v1/public/cli/apps/${appId}/public-keys`,
951
- "POST",
952
- { "X-Public-Key": secretKey },
953
- JSON.stringify({})
954
- );
955
- if (newKeyRes.status === 201) {
956
- const keyData = newKeyRes.data;
957
- publicKey = keyData.key;
958
- success("Public Key \uBC1C\uAE09 \uC644\uB8CC");
959
- } else {
960
- warn("Public Key \uC790\uB3D9 \uC0DD\uC131 \uC2E4\uD328. \uC218\uB3D9\uC73C\uB85C \uC0DD\uC131\uD558\uC138\uC694");
961
- }
1065
+ if (!reuseKeyIfSameApp(appId)) await issuePublicKey(appId);
962
1066
  }
963
1067
  }
964
1068
  if (!appId) {
965
- const projectName = path2.basename(cwd);
966
- const appName = await prompt(`${colors.blue}?${colors.reset} \uC571 \uC774\uB984 (${projectName}): `) || projectName;
1069
+ const appName = nonInteractive ? projectName : await prompt(`${colors.blue}?${colors.reset} \uC571 \uC774\uB984 (${projectName}): `) || projectName;
967
1070
  info("\uC571 \uC0DD\uC131 \uC911...");
968
1071
  const createRes = await makeRequest(
969
1072
  `${DEFAULT_BASE_URL}/v1/public/cli/apps`,
@@ -990,6 +1093,10 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
990
1093
  }
991
1094
  } catch (err) {
992
1095
  error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
1096
+ if (nonInteractive) {
1097
+ info("\uB124\uD2B8\uC6CC\uD06C \uBCF5\uAD6C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uAC70\uB098, \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09\uD55C \uAC12\uC73C\uB85C .connectbaserc\uB97C \uC9C1\uC811 \uC791\uC131\uD558\uC138\uC694");
1098
+ process.exit(1);
1099
+ }
993
1100
  log(`${colors.dim}\uC218\uB3D9\uC73C\uB85C \uC785\uB825\uD558\uC138\uC694${colors.reset}`);
994
1101
  appId = await prompt(`${colors.blue}?${colors.reset} App ID: `);
995
1102
  publicKey = await prompt(`${colors.blue}?${colors.reset} Public Key (cb_pk_): `);
@@ -1004,77 +1111,92 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
1004
1111
  }
1005
1112
  const publicKeyForSdk = publicKey;
1006
1113
  let storageId = "";
1007
- try {
1008
- info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC870\uD68C \uC911...");
1009
- const listRes = await makeRequest(
1010
- `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
1011
- "GET",
1012
- { "X-Public-Key": publicKeyForSdk }
1013
- );
1014
- if (listRes.status === 200) {
1015
- const listData = listRes.data;
1016
- const storages = listData.storages || [];
1017
- if (storages.length > 0) {
1018
- log(`
1114
+ if (opts.storageId) {
1115
+ storageId = opts.storageId;
1116
+ success(`\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC9C0\uC815\uB428: ${storageId}`);
1117
+ } else {
1118
+ try {
1119
+ info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC870\uD68C \uC911...");
1120
+ const listRes = await makeRequest(
1121
+ `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
1122
+ "GET",
1123
+ { "X-Public-Key": publicKeyForSdk }
1124
+ );
1125
+ if (listRes.status === 200) {
1126
+ const listData = listRes.data;
1127
+ const storages = listData.storages || [];
1128
+ if (nonInteractive) {
1129
+ const found = storages.find((s) => s.name === projectName);
1130
+ if (found) {
1131
+ storageId = found.id;
1132
+ success(`\uAE30\uC874 \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC7AC\uC0AC\uC6A9: ${found.name}`);
1133
+ }
1134
+ } else if (storages.length > 0) {
1135
+ log(`
1019
1136
  ${colors.dim}\uAE30\uC874 \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0:${colors.reset}`);
1020
- storages.forEach((s, i) => {
1021
- log(` ${colors.cyan}${i + 1}${colors.reset}) ${s.name} (${colors.dim}${s.id}${colors.reset})`);
1022
- });
1023
- log(` ${colors.cyan}0${colors.reset}) \uC0C8\uB85C \uC0DD\uC131`);
1024
- const choice = await prompt(`
1137
+ storages.forEach((s, i) => {
1138
+ log(` ${colors.cyan}${i + 1}${colors.reset}) ${s.name} (${colors.dim}${s.id}${colors.reset})`);
1139
+ });
1140
+ log(` ${colors.cyan}0${colors.reset}) \uC0C8\uB85C \uC0DD\uC131`);
1141
+ const choice = await prompt(`
1025
1142
  ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
1026
- const num = parseInt(choice, 10);
1027
- if (num > 0 && num <= storages.length) {
1028
- storageId = storages[num - 1].id;
1029
- success(`\uC120\uD0DD\uB428: ${storages[num - 1].name}`);
1143
+ const num = parseInt(choice, 10);
1144
+ if (num > 0 && num <= storages.length) {
1145
+ storageId = storages[num - 1].id;
1146
+ success(`\uC120\uD0DD\uB428: ${storages[num - 1].name}`);
1147
+ }
1148
+ }
1149
+ if (!storageId) {
1150
+ const name = nonInteractive ? projectName : await prompt(`${colors.blue}?${colors.reset} \uC2A4\uD1A0\uB9AC\uC9C0 \uC774\uB984 (${projectName}): `) || projectName;
1151
+ info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC911...");
1152
+ const createRes = await makeRequest(
1153
+ `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
1154
+ "POST",
1155
+ { "X-Public-Key": publicKeyForSdk },
1156
+ JSON.stringify({ name })
1157
+ );
1158
+ if (createRes.status !== 200) {
1159
+ const data = createRes.data;
1160
+ error(`\uC0DD\uC131 \uC2E4\uD328: ${data?.error || data?.message || `HTTP ${createRes.status}`}`);
1161
+ process.exit(1);
1162
+ }
1163
+ const createData = createRes.data;
1164
+ storageId = createData.storage_web_id;
1165
+ success(`\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC644\uB8CC: ${createData.name}`);
1030
1166
  }
1167
+ } else {
1168
+ throw new Error(`HTTP ${listRes.status}`);
1169
+ }
1170
+ } catch (err) {
1171
+ error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
1172
+ if (nonInteractive) {
1173
+ info("\uB124\uD2B8\uC6CC\uD06C \uBCF5\uAD6C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uAC70\uB098, --storage <\uC2A4\uD1A0\uB9AC\uC9C0ID>\uC640 \uD568\uAED8 \uC2E4\uD589\uD558\uC138\uC694");
1174
+ process.exit(1);
1031
1175
  }
1176
+ log(`${colors.dim}\uC218\uB3D9\uC73C\uB85C Storage ID\uB97C \uC785\uB825\uD558\uC138\uC694${colors.reset}`);
1177
+ storageId = await prompt(`${colors.blue}?${colors.reset} Storage ID: `);
1032
1178
  if (!storageId) {
1033
- const projectName = path2.basename(cwd);
1034
- const name = await prompt(`${colors.blue}?${colors.reset} \uC2A4\uD1A0\uB9AC\uC9C0 \uC774\uB984 (${projectName}): `) || projectName;
1035
- info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC911...");
1036
- const createRes = await makeRequest(
1037
- `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
1038
- "POST",
1039
- { "X-Public-Key": publicKeyForSdk },
1040
- JSON.stringify({ name })
1041
- );
1042
- if (createRes.status !== 200) {
1043
- const data = createRes.data;
1044
- error(`\uC0DD\uC131 \uC2E4\uD328: ${data?.error || data?.message || `HTTP ${createRes.status}`}`);
1045
- process.exit(1);
1046
- }
1047
- const createData = createRes.data;
1048
- storageId = createData.storage_web_id;
1049
- success(`\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC644\uB8CC: ${createData.name}`);
1179
+ error("Storage ID\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
1180
+ process.exit(1);
1050
1181
  }
1051
- } else {
1052
- throw new Error(`HTTP ${listRes.status}`);
1053
- }
1054
- } catch (err) {
1055
- error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
1056
- log(`${colors.dim}\uC218\uB3D9\uC73C\uB85C Storage ID\uB97C \uC785\uB825\uD558\uC138\uC694${colors.reset}`);
1057
- storageId = await prompt(`${colors.blue}?${colors.reset} Storage ID: `);
1058
- if (!storageId) {
1059
- error("Storage ID\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
1060
- process.exit(1);
1061
1182
  }
1062
1183
  }
1063
- const defaultDir = detectBuildDir();
1064
- const deployDir = await prompt(`${colors.blue}?${colors.reset} \uBC30\uD3EC \uB514\uB809\uD1A0\uB9AC (${defaultDir}): `) || defaultDir;
1065
- const config = {
1184
+ const defaultDir = opts.deployDir || detectBuildDir();
1185
+ const deployDir = nonInteractive ? defaultDir : await prompt(`${colors.blue}?${colors.reset} \uBC30\uD3EC \uB514\uB809\uD1A0\uB9AC (${defaultDir}): `) || defaultDir;
1186
+ const rcData = {
1187
+ appId,
1066
1188
  publicKey: publicKey || "",
1067
1189
  storageId,
1068
1190
  deployDir
1069
1191
  };
1070
1192
  if (secretKey) {
1071
- config.secretKey = secretKey;
1193
+ rcData.secretKey = secretKey;
1072
1194
  }
1073
- fs2.writeFileSync(rcPath, JSON.stringify(config, null, 2) + "\n");
1195
+ fs2.writeFileSync(rcPath, JSON.stringify(rcData, null, 2) + "\n");
1074
1196
  success(".connectbaserc \uC0DD\uC131 \uC644\uB8CC");
1075
1197
  addToGitignore(".connectbaserc");
1076
1198
  addDeployScript(deployDir);
1077
- await setupClaudeCode(publicKeyForSdk, secretKey, projectRoot);
1199
+ await setupClaudeCode(secretKey, projectRoot);
1078
1200
  log(`
1079
1201
  ${colors.green}\uCD08\uAE30\uD654 \uC644\uB8CC!${colors.reset}
1080
1202
  `);
@@ -1088,9 +1210,20 @@ function detectBuildDir() {
1088
1210
  if (fs2.existsSync(path2.join(process.cwd(), "dist"))) return "./dist";
1089
1211
  if (fs2.existsSync(path2.join(process.cwd(), "build"))) return "./build";
1090
1212
  if (fs2.existsSync(path2.join(process.cwd(), "out"))) return "./out";
1091
- if (fs2.existsSync(path2.join(process.cwd(), ".next"))) return "./out";
1213
+ const nextConfig = ["next.config.js", "next.config.mjs", "next.config.ts"].map((f) => path2.join(process.cwd(), f)).find((f) => fs2.existsSync(f));
1214
+ if (nextConfig || fs2.existsSync(path2.join(process.cwd(), ".next"))) {
1215
+ if (nextConfig) {
1216
+ try {
1217
+ const src = fs2.readFileSync(nextConfig, "utf-8");
1218
+ if (!/output\s*:\s*['"]export['"]/.test(src)) {
1219
+ warn(`Next.js \uAC10\uC9C0: \uC815\uC801 \uBC30\uD3EC\uD558\uB824\uBA74 ${path2.basename(nextConfig)}\uC5D0 output: 'export' \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4`);
1220
+ }
1221
+ } catch {
1222
+ }
1223
+ }
1224
+ return "./out";
1225
+ }
1092
1226
  if (fs2.existsSync(path2.join(process.cwd(), "vite.config.ts")) || fs2.existsSync(path2.join(process.cwd(), "vite.config.js"))) return "./dist";
1093
- if (fs2.existsSync(path2.join(process.cwd(), "next.config.js")) || fs2.existsSync(path2.join(process.cwd(), "next.config.mjs"))) return "./out";
1094
1227
  return "./dist";
1095
1228
  }
1096
1229
  function addToGitignore(entry, basePath) {
@@ -1098,7 +1231,8 @@ function addToGitignore(entry, basePath) {
1098
1231
  const gitignorePath = path2.join(dir, ".gitignore");
1099
1232
  if (fs2.existsSync(gitignorePath)) {
1100
1233
  const content = fs2.readFileSync(gitignorePath, "utf-8");
1101
- if (!content.includes(entry)) {
1234
+ const lines = content.split(/\r?\n/).map((l) => l.trim());
1235
+ if (!lines.includes(entry)) {
1102
1236
  fs2.appendFileSync(gitignorePath, `
1103
1237
  # Connect Base
1104
1238
  ${entry}
@@ -1179,13 +1313,66 @@ function detectMonorepo(gitRoot) {
1179
1313
  }
1180
1314
  return result;
1181
1315
  }
1182
- async function downloadDocs(publicKey, templates, baseDir) {
1316
+ var DOCS_MANIFEST_FILE = ".connectbase-manifest.json";
1317
+ function readDocsManifest(docsDir) {
1318
+ try {
1319
+ const parsed = JSON.parse(fs2.readFileSync(path2.join(docsDir, DOCS_MANIFEST_FILE), "utf-8"));
1320
+ if (parsed && Array.isArray(parsed.files)) return parsed;
1321
+ } catch {
1322
+ }
1323
+ return null;
1324
+ }
1325
+ var LEGACY_DOC_FILES = [
1326
+ "intro.md",
1327
+ "mcp-workflow.md",
1328
+ "sdk-basic.md",
1329
+ "sdk-database.md",
1330
+ "sdk-storage.md",
1331
+ "sdk-realtime.md",
1332
+ "sdk-auth.md",
1333
+ "sdk-payment.md",
1334
+ "example.md",
1335
+ "mcp-config.md",
1336
+ "mcp-tools.md",
1337
+ "resources.md",
1338
+ "sdk-game.md",
1339
+ "sdk-push.md",
1340
+ "sdk-video.md",
1341
+ "sdk-ads.md",
1342
+ "sdk-ai.md",
1343
+ "sdk-error-tracker.md",
1344
+ "sdk-webrtc.md",
1345
+ "sdk-subscription.md",
1346
+ "sdk-functions.md",
1347
+ "sdk-webhooks.md",
1348
+ "deploy-guide.md",
1349
+ "app-packaging.md",
1350
+ "best-practices.md",
1351
+ "code-analysis.md",
1352
+ "sdk-queue.md",
1353
+ "sdk-knowledge.md",
1354
+ "sdk-workflow.md",
1355
+ "sdk-tunnel.md",
1356
+ "sdk-native.md",
1357
+ "sdk-analytics.md",
1358
+ "sdk-domain.md",
1359
+ "sdk-public-key.md",
1360
+ "sdk-invoice.md",
1361
+ "web-performance.md",
1362
+ "sdk-analytics-advanced.md",
1363
+ "sdk-endpoint.md",
1364
+ "sdk-web-search.md",
1365
+ "sdk-oauth-provider.md",
1366
+ "sdk-oauth-integration.md"
1367
+ ];
1368
+ async function downloadDocs(baseDir, opts = {}) {
1369
+ const quiet = opts.quiet === true;
1183
1370
  if (!baseDir) {
1184
1371
  baseDir = getProjectRoot();
1185
1372
  }
1186
1373
  const gitRoot = getGitRoot() || baseDir;
1187
1374
  const monorepo = detectMonorepo(gitRoot);
1188
- if (monorepo.type !== "none") {
1375
+ if (!quiet && monorepo.type !== "none") {
1189
1376
  info(`\uBAA8\uB178\uB808\uD3EC \uAC10\uC9C0: ${monorepo.type} (\uB8E8\uD2B8: ${gitRoot})`);
1190
1377
  if (monorepo.isSubPackage) {
1191
1378
  info(`\uD604\uC7AC \uC11C\uBE0C \uD328\uD0A4\uC9C0: ${monorepo.subPackageName || path2.basename(process.cwd())}`);
@@ -1196,14 +1383,10 @@ async function downloadDocs(publicKey, templates, baseDir) {
1196
1383
  if (!fs2.existsSync(docsDir)) {
1197
1384
  fs2.mkdirSync(docsDir, { recursive: true });
1198
1385
  }
1199
- if (!templates) {
1200
- templates = ["rules"];
1201
- }
1202
- info("SDK \uAC1C\uBC1C \uAC00\uC774\uB4DC \uB2E4\uC6B4\uB85C\uB4DC \uC911...");
1386
+ if (!quiet) info("SDK \uAC1C\uBC1C \uAC00\uC774\uB4DC \uB2E4\uC6B4\uB85C\uB4DC \uC911...");
1203
1387
  try {
1204
- const templateParam = templates.join(",");
1205
1388
  const res = await makeRequest(
1206
- `${DEFAULT_BASE_URL}/v1/storages/webs/claude-md?template=${encodeURIComponent(templateParam)}&format=sections&public_key=${encodeURIComponent(publicKey)}`,
1389
+ `${DEFAULT_BASE_URL}/v1/storages/webs/claude-md?template=rules&format=sections`,
1207
1390
  "GET",
1208
1391
  {}
1209
1392
  );
@@ -1212,18 +1395,59 @@ async function downloadDocs(publicKey, templates, baseDir) {
1212
1395
  }
1213
1396
  const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
1214
1397
  const sections = data.sections || [];
1398
+ const hasher = crypto.createHash("sha256");
1399
+ const newFiles = [];
1215
1400
  for (const section of sections) {
1216
- const filePath = path2.join(docsDir, section.filename);
1217
- fs2.writeFileSync(filePath, section.content);
1401
+ const safeName = path2.basename(section.filename);
1402
+ newFiles.push(safeName);
1403
+ hasher.update(safeName).update("\0").update(section.content).update("\0");
1404
+ }
1405
+ const contentHash = hasher.digest("hex");
1406
+ const prevManifest = readDocsManifest(docsDir);
1407
+ const allPresent = newFiles.every((f) => fs2.existsSync(path2.join(docsDir, f)));
1408
+ if (prevManifest && prevManifest.contentHash === contentHash && allPresent) {
1409
+ if (!quiet) success("SDK \uBB38\uC11C\uAC00 \uC774\uBBF8 \uCD5C\uC2E0\uC785\uB2C8\uB2E4");
1410
+ updateRootClaudeMd(rootClaudeMd, quiet);
1411
+ if (monorepo.isSubPackage) {
1412
+ createSubPackageClaudeMd(path2.join(process.cwd(), "CLAUDE.md"), gitRoot, quiet);
1413
+ }
1414
+ return "unchanged";
1218
1415
  }
1219
- success(`${gitRoot}/.claude/docs/ \uC5D0 ${sections.length}\uAC1C \uBB38\uC11C \uC800\uC7A5 \uC644\uB8CC`);
1220
- updateRootClaudeMd(rootClaudeMd);
1416
+ const stamp = `<!-- generated by connectbase-cli v${VERSION} (${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}) \u2014 \uC790\uB3D9 \uC0DD\uC131 \uD30C\uC77C\uC785\uB2C8\uB2E4. \uC9C1\uC811 \uC218\uC815\uD558\uC9C0 \uB9D0\uACE0 'npx connectbase update'\uB85C \uAC31\uC2E0\uD558\uC138\uC694 -->
1417
+
1418
+ `;
1419
+ for (const section of sections) {
1420
+ const safeName = path2.basename(section.filename);
1421
+ fs2.writeFileSync(path2.join(docsDir, safeName), stamp + section.content);
1422
+ }
1423
+ const staleCandidates = prevManifest ? prevManifest.files.map((f) => path2.basename(f)) : LEGACY_DOC_FILES;
1424
+ for (const old of staleCandidates) {
1425
+ if (newFiles.includes(old)) continue;
1426
+ const oldPath = path2.join(docsDir, old);
1427
+ if (fs2.existsSync(oldPath)) {
1428
+ try {
1429
+ fs2.unlinkSync(oldPath);
1430
+ if (!quiet) info(`\uC2A4\uD14C\uC77C \uBB38\uC11C \uC0AD\uC81C: ${old}`);
1431
+ } catch {
1432
+ }
1433
+ }
1434
+ }
1435
+ const manifest = {
1436
+ cliVersion: VERSION,
1437
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1438
+ contentHash,
1439
+ files: newFiles
1440
+ };
1441
+ fs2.writeFileSync(path2.join(docsDir, DOCS_MANIFEST_FILE), JSON.stringify(manifest, null, 2) + "\n");
1442
+ if (!quiet) success(`${gitRoot}/.claude/docs/ \uC5D0 ${sections.length}\uAC1C \uBB38\uC11C \uC800\uC7A5 \uC644\uB8CC`);
1443
+ updateRootClaudeMd(rootClaudeMd, quiet);
1221
1444
  if (monorepo.isSubPackage) {
1222
- const subClaudeMd = path2.join(process.cwd(), "CLAUDE.md");
1223
- createSubPackageClaudeMd(subClaudeMd, gitRoot);
1445
+ createSubPackageClaudeMd(path2.join(process.cwd(), "CLAUDE.md"), gitRoot, quiet);
1224
1446
  }
1225
- log(`${colors.dim}\u203B SDK \uBB38\uC11C\uB294 MCP search_sdk_docs\uB85C \uAC80\uC0C9\uD558\uC138\uC694${colors.reset}`);
1447
+ if (!quiet) log(`${colors.dim}\u203B SDK \uBB38\uC11C\uB294 MCP search_sdk_docs\uB85C \uAC80\uC0C9\uD558\uC138\uC694${colors.reset}`);
1448
+ return "updated";
1226
1449
  } catch {
1450
+ if (quiet) return "failed";
1227
1451
  warn("SDK \uAC00\uC774\uB4DC \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328, \uAE30\uBCF8 \uBB38\uC11C\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4");
1228
1452
  const fallbackContent = `# ConnectBase \uD504\uB85C\uC81D\uD2B8
1229
1453
 
@@ -1238,9 +1462,10 @@ async function downloadDocs(publicKey, templates, baseDir) {
1238
1462
  createSubPackageClaudeMd(subClaudeMd, gitRoot);
1239
1463
  }
1240
1464
  success(`.claude/docs/ \uC0DD\uC131 \uC644\uB8CC (\uAE30\uBCF8)`);
1465
+ return "failed";
1241
1466
  }
1242
1467
  }
1243
- function updateRootClaudeMd(claudeMdPath) {
1468
+ function updateRootClaudeMd(claudeMdPath, quiet = false) {
1244
1469
  const startMarker = "<!-- CONNECTBASE_START -->";
1245
1470
  const endMarker = "<!-- CONNECTBASE_END -->";
1246
1471
  const sdkBlock = `${startMarker}
@@ -1248,11 +1473,13 @@ function updateRootClaudeMd(claudeMdPath) {
1248
1473
 
1249
1474
  \uC774 \uD504\uB85C\uC81D\uD2B8\uB294 **ConnectBase**\uB97C \uBC31\uC5D4\uB4DC\uB85C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.
1250
1475
 
1251
- ### AI \uD544\uB3C5 \u2014 \uCF54\uB529 \uC804 \uBC18\uB4DC\uC2DC \uC218\uD589
1476
+ @.claude/docs/project-rules.md
1252
1477
 
1253
- 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)
1254
- 2. \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
1255
- 3. \uAC80\uC0C9 \uACB0\uACFC\uC758 \uCF54\uB4DC \uD328\uD134\uC744 \uB530\uB77C \uAD6C\uD604\uD558\uC138\uC694
1478
+ ### AI \uD544\uC218 \uC6CC\uD06C\uD50C\uB85C\uC6B0
1479
+
1480
+ 1. \uC704\uC5D0 import\uB41C project-rules.md\uC758 \uAE30\uB2A5 \uB9E4\uD551\uD45C\xB7\uBCF4\uC548 \uADDC\uCE59\uC744 \uB530\uB974\uC138\uC694
1481
+ 2. \uCF54\uB4DC\uB97C \uC791\uC131\uD558\uAE30 \uC804 \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C SDK \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uACE0, \uAC80\uC0C9 \uACB0\uACFC\uC758 \uCF54\uB4DC \uD328\uD134\uC744 \uADF8\uB300\uB85C \uB530\uB974\uC138\uC694
1482
+ 3. \`search_sdk_docs\` \uB3C4\uAD6C\uAC00 \uBCF4\uC774\uC9C0 \uC54A\uC73C\uBA74 MCP\uAC00 \uC5F0\uACB0\uB418\uC9C0 \uC54A\uC740 \uC0C1\uD0DC\uC785\uB2C8\uB2E4 \u2014 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \`npx connectbase mcp\` \uC2E4\uD589\uC744 \uC548\uB0B4\uD558\uC138\uC694 (Secret Key \uD544\uC694)
1256
1483
  ${endMarker}`;
1257
1484
  if (fs2.existsSync(claudeMdPath)) {
1258
1485
  let content = fs2.readFileSync(claudeMdPath, "utf-8");
@@ -1264,16 +1491,16 @@ ${endMarker}`;
1264
1491
  content = sdkBlock + "\n\n" + content.trimStart();
1265
1492
  }
1266
1493
  fs2.writeFileSync(claudeMdPath, content);
1267
- info("CLAUDE.md\uC5D0 ConnectBase \uCC38\uC870 \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC");
1494
+ if (!quiet) info("CLAUDE.md\uC5D0 ConnectBase \uCC38\uC870 \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC");
1268
1495
  } else {
1269
1496
  fs2.writeFileSync(claudeMdPath, `# \uD504\uB85C\uC81D\uD2B8
1270
1497
 
1271
1498
  ${sdkBlock}
1272
1499
  `);
1273
- success("CLAUDE.md \uC0DD\uC131 \uC644\uB8CC");
1500
+ if (!quiet) success("CLAUDE.md \uC0DD\uC131 \uC644\uB8CC");
1274
1501
  }
1275
1502
  }
1276
- function createSubPackageClaudeMd(subClaudeMdPath, gitRoot) {
1503
+ function createSubPackageClaudeMd(subClaudeMdPath, gitRoot, quiet = false) {
1277
1504
  const startMarker = "<!-- CONNECTBASE_SUB_START -->";
1278
1505
  const endMarker = "<!-- CONNECTBASE_SUB_END -->";
1279
1506
  const relPath = path2.relative(path2.dirname(subClaudeMdPath), gitRoot);
@@ -1284,9 +1511,9 @@ function createSubPackageClaudeMd(subClaudeMdPath, gitRoot) {
1284
1511
 
1285
1512
  ### AI \uD544\uB3C5
1286
1513
 
1287
- 1. \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC758 \`${relPath}/.claude/docs/project-rules.md\`\uB97C **Read tool\uB85C \uC77D\uC73C\uC138\uC694**
1288
- 2. \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C SDK \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
1289
- 3. \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC758 \`${relPath}/CLAUDE.md\`\uB3C4 \uCC38\uACE0\uD558\uC138\uC694
1514
+ 1. \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC758 \`${relPath}/.claude/docs/project-rules.md\`\uC758 \uAE30\uB2A5 \uB9E4\uD551\uD45C\xB7\uBCF4\uC548 \uADDC\uCE59\uC744 \uB530\uB974\uC138\uC694
1515
+ 2. \uCF54\uB4DC\uB97C \uC791\uC131\uD558\uAE30 \uC804 \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C SDK \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
1516
+ 3. \`search_sdk_docs\` \uB3C4\uAD6C\uAC00 \uBCF4\uC774\uC9C0 \uC54A\uC73C\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \`npx connectbase mcp\` \uC2E4\uD589\uC744 \uC548\uB0B4\uD558\uC138\uC694
1290
1517
  ${endMarker}`;
1291
1518
  if (fs2.existsSync(subClaudeMdPath)) {
1292
1519
  let content = fs2.readFileSync(subClaudeMdPath, "utf-8");
@@ -1306,7 +1533,7 @@ ${endMarker}`;
1306
1533
  ${subBlock}
1307
1534
  `);
1308
1535
  }
1309
- success(`\uC11C\uBE0C \uD328\uD0A4\uC9C0 CLAUDE.md \uC0DD\uC131 \uC644\uB8CC: ${subClaudeMdPath}`);
1536
+ if (!quiet) success(`\uC11C\uBE0C \uD328\uD0A4\uC9C0 CLAUDE.md \uC0DD\uC131 \uC644\uB8CC: ${subClaudeMdPath}`);
1310
1537
  }
1311
1538
  async function setupMcp(secretKey) {
1312
1539
  const gitRoot = getGitRoot();
@@ -1320,6 +1547,13 @@ async function setupMcp(secretKey) {
1320
1547
  }
1321
1548
  const mcpConfigPath = path2.join(root, ".mcp.json");
1322
1549
  if (!secretKey) {
1550
+ if (!process.stdin.isTTY) {
1551
+ error("Secret Key\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4 (\uBE44\uB300\uD654\uD615 \uD658\uACBD)");
1552
+ log(`${colors.dim}\uB2E4\uC74C \uC911 \uD558\uB098\uB85C \uD0A4\uB97C \uC804\uB2EC\uD558\uC138\uC694:${colors.reset}`);
1553
+ log(` ${colors.cyan}npx connectbase mcp --secret-key cb_sk_...${colors.reset}`);
1554
+ log(` ${colors.cyan}CONNECTBASE_SECRET_KEY=cb_sk_... npx connectbase mcp${colors.reset}`);
1555
+ process.exit(1);
1556
+ }
1323
1557
  log(`
1324
1558
  ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${colors.reset}`);
1325
1559
  log(` ${colors.cyan}1${colors.reset}) Secret Key \uC9C1\uC811 \uC785\uB825 (cb_sk_...)`);
@@ -1383,9 +1617,9 @@ ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${co
1383
1617
  log(`
1384
1618
  ${colors.dim}Claude Code\uC5D0\uC11C ConnectBase MCP \uB3C4\uAD6C\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.${colors.reset}`);
1385
1619
  }
1386
- async function setupClaudeCode(publicKey, secretKey, projectRoot) {
1620
+ async function setupClaudeCode(secretKey, projectRoot) {
1387
1621
  const root = projectRoot || getProjectRoot();
1388
- await downloadDocs(publicKey, void 0, root);
1622
+ await downloadDocs(root);
1389
1623
  await setupMcp(secretKey);
1390
1624
  }
1391
1625
  async function fetchLatestVersion(packageName) {
@@ -1404,13 +1638,18 @@ async function fetchLatestVersion(packageName) {
1404
1638
  return null;
1405
1639
  }
1406
1640
  function compareVersions(a, b) {
1407
- const pa = a.split(".").map(Number);
1408
- const pb = b.split(".").map(Number);
1641
+ const [aMain, aPre] = a.split("-", 2);
1642
+ const [bMain, bPre] = b.split("-", 2);
1643
+ const pa = aMain.split(".").map(Number);
1644
+ const pb = bMain.split(".").map(Number);
1409
1645
  for (let i = 0; i < 3; i++) {
1410
1646
  const diff = (pa[i] || 0) - (pb[i] || 0);
1411
1647
  if (diff !== 0) return diff > 0 ? 1 : -1;
1412
1648
  }
1413
- return 0;
1649
+ if (aPre === bPre) return 0;
1650
+ if (!aPre) return 1;
1651
+ if (!bPre) return -1;
1652
+ return aPre < bPre ? -1 : aPre > bPre ? 1 : 0;
1414
1653
  }
1415
1654
  function findSubPackagesWithConfig(gitRoot) {
1416
1655
  const results = [];
@@ -1482,36 +1721,7 @@ ${colors.cyan}ConnectBase Update${colors.reset}
1482
1721
  if (!opts.skipDocs) {
1483
1722
  log("");
1484
1723
  info("SDK \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uC911...");
1485
- let docsKey = config.publicKey || config.secretKey;
1486
- if (!docsKey) {
1487
- const rootRcPath = path2.join(projectRoot, ".connectbaserc");
1488
- if (fs2.existsSync(rootRcPath)) {
1489
- try {
1490
- const rc = JSON.parse(fs2.readFileSync(rootRcPath, "utf-8"));
1491
- docsKey = rc.publicKey || rc.secretKey;
1492
- } catch {
1493
- }
1494
- }
1495
- }
1496
- if (!docsKey && monorepo.type !== "none") {
1497
- const subPackages = findSubPackagesWithConfig(projectRoot);
1498
- for (const sp of subPackages) {
1499
- try {
1500
- const rc = JSON.parse(fs2.readFileSync(path2.join(sp, ".connectbaserc"), "utf-8"));
1501
- if (rc.publicKey || rc.secretKey) {
1502
- docsKey = rc.publicKey || rc.secretKey;
1503
- break;
1504
- }
1505
- } catch {
1506
- }
1507
- }
1508
- }
1509
- if (docsKey) {
1510
- await downloadDocs(docsKey, void 0, projectRoot);
1511
- } else {
1512
- warn("Public Key\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC5B4 \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4");
1513
- info(".connectbaserc \uD30C\uC77C\uC774\uB098 --public-key \uC635\uC158\uC73C\uB85C \uD0A4\uB97C \uC9C0\uC815\uD558\uC138\uC694");
1514
- }
1724
+ await downloadDocs(projectRoot);
1515
1725
  }
1516
1726
  if (!opts.skipMcp) {
1517
1727
  log("");
@@ -2598,8 +2808,12 @@ ${colors.yellow}\uC635\uC158:${colors.reset}
2598
2808
  --label <name> Endpoint binding \uC790\uB3D9 \uB4F1\uB85D (tunnel \uC804\uC6A9) \u2014 SDK \uC758 cb.endpoint.call(label) \uD638\uCD9C \uAC00\uB2A5
2599
2809
  --description <text> Endpoint binding \uC124\uBA85 (--label \uB3D9\uBC18 \uC2DC\uB9CC)
2600
2810
  -d, --dev Dev \uD658\uACBD\uC5D0 \uBC30\uD3EC (deploy \uC804\uC6A9)
2811
+ -y, --yes \uBE44\uB300\uD654\uD615 \uCD08\uAE30\uD654 \u2014 \uD504\uB86C\uD504\uD2B8 \uC5C6\uC774 \uAE30\uBCF8\uAC12/\uD50C\uB798\uADF8\uB85C \uC9C4\uD589 (init \uC804\uC6A9, CI/AI \uC5D0\uC774\uC804\uD2B8\uC6A9)
2812
+ -a, --app <id> \uAE30\uC874 \uC571 ID \uC9C0\uC815 (init \uC804\uC6A9)
2813
+ --secret-key <key> Secret Key (cb_sk_) \uC9C0\uC815 (init/mcp)
2814
+ --deploy-dir <dir> \uBC30\uD3EC \uB514\uB809\uD1A0\uB9AC \uC9C0\uC815 (init \uC804\uC6A9, \uAE30\uBCF8: \uC790\uB3D9 \uAC10\uC9C0)
2601
2815
  --check \uBC84\uC804\uB9CC \uD655\uC778 (update \uC804\uC6A9)
2602
- --skip-docs \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update \uC804\uC6A9)
2816
+ --skip-docs \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update/deploy)
2603
2817
  --skip-mcp MCP \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update \uC804\uC6A9)
2604
2818
  --setup-root \uBAA8\uB178\uB808\uD3EC \uB8E8\uD2B8\uC5D0 \uD3B8\uC758 \uC2A4\uD06C\uB9BD\uD2B8 \uCD94\uAC00 (update \uC804\uC6A9)
2605
2819
  (docs, mcp, update\uB294 \uBAA8\uB178\uB808\uD3EC\uB97C \uC790\uB3D9 \uAC10\uC9C0\uD558\uC5EC \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC5D0 \uC0DD\uC131)
@@ -2610,6 +2824,9 @@ ${colors.yellow}\uBE60\uB978 \uC2DC\uC791:${colors.reset}
2610
2824
  ${colors.dim}# 1. \uCD08\uAE30\uD654 (\uCD5C\uCD08 1\uD68C)${colors.reset}
2611
2825
  npx connectbase init
2612
2826
 
2827
+ ${colors.dim}# 1-1. \uBE44\uB300\uD654\uD615 \uCD08\uAE30\uD654 (CI/AI \uC5D0\uC774\uC804\uD2B8)${colors.reset}
2828
+ npx connectbase init --yes --secret-key cb_sk_...
2829
+
2613
2830
  ${colors.dim}# 2. SDK \uC804\uCCB4 \uC5C5\uB370\uC774\uD2B8 (\uBC84\uC804 \uCCB4\uD06C + \uBB38\uC11C + MCP)${colors.reset}
2614
2831
  npx connectbase update
2615
2832
 
@@ -2680,6 +2897,10 @@ function parseArgs(args) {
2680
2897
  result.options.appId = args[++i];
2681
2898
  } else if (arg === "--force") {
2682
2899
  result.options.force = "true";
2900
+ } else if (arg === "-y" || arg === "--yes") {
2901
+ result.options.yes = "true";
2902
+ } else if (arg === "--deploy-dir") {
2903
+ result.options.deployDir = args[++i];
2683
2904
  } else if (arg === "--public") {
2684
2905
  result.options.public = "true";
2685
2906
  } else if (arg === "--show-token") {
@@ -2727,7 +2948,13 @@ async function main() {
2727
2948
  baseUrl: parsed.options.baseUrl || fileConfig.baseUrl || DEFAULT_BASE_URL
2728
2949
  };
2729
2950
  if (parsed.command === "init") {
2730
- await init();
2951
+ await init(config, {
2952
+ yes: parsed.options.yes === "true",
2953
+ force: parsed.options.force === "true",
2954
+ appId: parsed.options.appId,
2955
+ storageId: parsed.options.storageId,
2956
+ deployDir: parsed.options.deployDir
2957
+ });
2731
2958
  } else if (parsed.command === "update") {
2732
2959
  await update(config, {
2733
2960
  checkOnly: parsed.options.check === "true",
@@ -2736,9 +2963,9 @@ async function main() {
2736
2963
  setupRoot: parsed.options.setupRoot === "true"
2737
2964
  });
2738
2965
  } else if (parsed.command === "docs") {
2739
- await downloadDocs(config.publicKey || "");
2966
+ await downloadDocs();
2740
2967
  } else if (parsed.command === "mcp") {
2741
- await setupMcp();
2968
+ await setupMcp(config.secretKey);
2742
2969
  } else if (parsed.command === "deploy") {
2743
2970
  const directory = parsed.args[0] || fileConfig.deployDir || ".";
2744
2971
  if (!config.publicKey) {
@@ -2757,6 +2984,18 @@ async function main() {
2757
2984
  else warn(`--timeout \uAC12\uC774 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC544 \uBB34\uC2DC\uB429\uB2C8\uB2E4: ${parsed.options.timeout}`);
2758
2985
  }
2759
2986
  await deploy(directory, config, isDev, deployOpts);
2987
+ if (parsed.options.skipDocs !== "true") {
2988
+ try {
2989
+ const docsRoot = getGitRoot() || getProjectRoot();
2990
+ if (fs2.existsSync(path2.join(docsRoot, ".claude", "docs", DOCS_MANIFEST_FILE))) {
2991
+ const refreshed = await downloadDocs(docsRoot, { quiet: true });
2992
+ if (refreshed === "updated") {
2993
+ info("SDK \uBB38\uC11C\uAC00 \uC0C8 \uBC84\uC804\uC73C\uB85C \uAC31\uC2E0\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (.claude/docs/)");
2994
+ }
2995
+ }
2996
+ } catch {
2997
+ }
2998
+ }
2760
2999
  } else if (parsed.command === "tunnel") {
2761
3000
  const portStr = parsed.args[0];
2762
3001
  if (!portStr) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "3.42.0",
3
+ "version": "3.44.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",