connectbase-client 3.43.0 → 3.45.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 +32 -0
  2. package/dist/cli.js +488 -185
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,38 @@
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
+
6
38
  ## [3.43.0] - 2026-07-02
7
39
 
8
40
  ### Added — `--dev` 배포도 대용량 바이너리 지원
package/dist/cli.js CHANGED
@@ -429,7 +429,10 @@ async function offloadLargeBinaries(baseStorageUrl, headers, diff, _envLabel, ti
429
429
  const presignFiles = largeBinaries.map((f) => ({
430
430
  path: f.path,
431
431
  size: Buffer.from(f.content, "base64").length,
432
- mime_type: guessMimeType(f.path)
432
+ mime_type: guessMimeType(f.path),
433
+ // 신형 서버: hash 제공 시 content-addressed blob 키({id}/blobs/{hash})로 presign 되고,
434
+ // 동일 blob 이 이미 있으면 already_exists 로 업로드를 생략할 수 있다. 구버전 서버는 무시.
435
+ hash: f.hash
433
436
  }));
434
437
  let presigned;
435
438
  try {
@@ -445,22 +448,30 @@ async function offloadLargeBinaries(baseStorageUrl, headers, diff, _envLabel, ti
445
448
  warn(`\uB300\uC6A9\uB7C9 \uC5D0\uC14B\uC740 \uD30C\uC77C \uC2A4\uD1A0\uB9AC\uC9C0(${colors.cyan}cb.storage${colors.reset})\uC5D0 \uC62C\uB9AC\uACE0 URL \uB85C \uCC38\uC870\uD558\uAC70\uB098, \uC11C\uBC84 \uC5C5\uB370\uC774\uD2B8 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`);
446
449
  process.exit(1);
447
450
  }
448
- const urlByPath = new Map(presigned.map((p) => [p.path, p.upload_url]));
451
+ const entryByPath = new Map(presigned.map((p) => [p.path, p]));
449
452
  info(`\uB300\uC6A9\uB7C9 \uBC14\uC774\uB108\uB9AC ${largeBinaries.length}\uAC1C\uB97C Object Storage \uB85C \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4...`);
450
453
  for (const f of largeBinaries) {
451
- const uploadUrl = urlByPath.get(f.path);
452
- if (!uploadUrl) {
453
- error(`presign \uC751\uB2F5\uC5D0 \uC5C5\uB85C\uB4DC URL \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${f.path}`);
454
+ const entry = entryByPath.get(f.path);
455
+ if (!entry) {
456
+ error(`presign \uC751\uB2F5\uC5D0 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${f.path}`);
454
457
  process.exit(1);
455
458
  }
456
459
  const bytes = Buffer.from(f.content, "base64");
457
- try {
458
- await putToPresignedUrl(uploadUrl, bytes, guessMimeType(f.path), timeoutMs);
459
- } catch (err) {
460
- error(`\uC5C5\uB85C\uB4DC \uC2E4\uD328 (${f.path}): ${err instanceof Error ? err.message : err}`);
461
- process.exit(1);
460
+ if (entry.already_exists) {
461
+ success(`\uC5C5\uB85C\uB4DC \uC0DD\uB7B5(\uAE30\uC874 blob \uC7AC\uC0AC\uC6A9): ${f.path} (${formatSize(bytes.length)})`);
462
+ } else {
463
+ if (!entry.upload_url) {
464
+ error(`presign \uC751\uB2F5\uC5D0 \uC5C5\uB85C\uB4DC URL \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${f.path}`);
465
+ process.exit(1);
466
+ }
467
+ try {
468
+ await putToPresignedUrl(entry.upload_url, bytes, guessMimeType(f.path), timeoutMs);
469
+ } catch (err) {
470
+ error(`\uC5C5\uB85C\uB4DC \uC2E4\uD328 (${f.path}): ${err instanceof Error ? err.message : err}`);
471
+ process.exit(1);
472
+ }
473
+ success(`\uC9C1\uC811 \uC5C5\uB85C\uB4DC: ${f.path} (${formatSize(bytes.length)})`);
462
474
  }
463
- success(`\uC9C1\uC811 \uC5C5\uB85C\uB4DC: ${f.path} (${formatSize(bytes.length)})`);
464
475
  f.pre_uploaded = true;
465
476
  f.size = bytes.length;
466
477
  f.content = "";
@@ -525,6 +536,8 @@ async function makeRequest(url, method, headers, body, reqOpts = {}) {
525
536
  family: 4,
526
537
  autoSelectFamily: false,
527
538
  headers: {
539
+ // 명시적 UA: 관측성 + WAF 봇 휴리스틱(비브라우저 UA 차단 규칙)과의 충돌 방지
540
+ "User-Agent": `connectbase-cli/${VERSION}`,
528
541
  ...headers,
529
542
  "Content-Type": "application/json",
530
543
  ...bodyBuffer ? { "Content-Length": bodyBuffer.length } : {}
@@ -741,8 +754,25 @@ async function fullDeploy(baseStorageUrl, headers, files, envLabel, endpoint, ti
741
754
  }
742
755
  function handleDeployResponse(response, envLabel) {
743
756
  if (response.status >= 200 && response.status < 300) {
744
- success(`${envLabel} \uBC30\uD3EC \uC644\uB8CC!`);
745
757
  const data = response.data;
758
+ const dep = data?.deployment ?? null;
759
+ if (dep?.deployment_id) {
760
+ if (dep.promoted) {
761
+ success(`${envLabel} \uBC30\uD3EC \uC644\uB8CC! (release #${dep.seq})`);
762
+ if (dep.production_url) log(`
763
+ ${colors.cyan}Production: ${dep.production_url}${colors.reset}`);
764
+ if (dep.qa_url) log(`${colors.dim}QA: ${dep.qa_url}${colors.reset}
765
+ `);
766
+ } else {
767
+ success(`QA \uD658\uACBD\uC5D0 \uBC30\uD3EC\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (release #${dep.seq}) \u2014 production \uC740 \uC544\uC9C1 \uC774\uC804 \uBC84\uC804\uC785\uB2C8\uB2E4`);
768
+ if (dep.qa_url) log(`
769
+ ${colors.yellow}QA URL: ${dep.qa_url}${colors.reset}`);
770
+ log(`${colors.dim}\uAC80\uC99D \uD6C4 \uBC18\uC601: ${colors.cyan}npx connectbase promote${colors.reset}
771
+ `);
772
+ }
773
+ return;
774
+ }
775
+ success(`${envLabel} \uBC30\uD3EC \uC644\uB8CC!`);
746
776
  if (data?.dev_url) {
747
777
  log(`
748
778
  ${colors.yellow}Dev URL: ${data.dev_url}${colors.reset}
@@ -759,6 +789,64 @@ ${colors.cyan}URL: ${data.url}${colors.reset}
759
789
  process.exit(1);
760
790
  }
761
791
  }
792
+ async function promote(config, deploymentId) {
793
+ const baseStorageUrl = `${config.baseUrl}/v1/public/storages/webs/${config.storageId}`;
794
+ const headers = { "X-Public-Key": config.publicKey };
795
+ info(deploymentId ? `release ${deploymentId} \uB97C production \uC73C\uB85C \uC2B9\uACA9\uD569\uB2C8\uB2E4...` : "\uCD5C\uC2E0 \uBC30\uD3EC\uB97C production \uC73C\uB85C \uC2B9\uACA9\uD569\uB2C8\uB2E4...");
796
+ const body = JSON.stringify(deploymentId ? { deployment_id: deploymentId } : {});
797
+ const res = await makeRequest(`${baseStorageUrl}/promote`, "POST", headers, body);
798
+ if (res.status === 404 && !res.data?.error) {
799
+ error("\uC11C\uBC84\uAC00 promote \uB97C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (\uAD6C\uBC84\uC804 \uC11C\uBC84) \u2014 \uC11C\uBC84 \uC5C5\uB370\uC774\uD2B8 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694");
800
+ process.exit(1);
801
+ }
802
+ if (res.status < 200 || res.status >= 300) {
803
+ const data2 = res.data;
804
+ const msg = typeof data2 === "object" && data2 !== null ? data2.error || data2.message || JSON.stringify(data2) : `HTTP ${res.status}`;
805
+ error(`promote \uC2E4\uD328: ${msg}`);
806
+ process.exit(1);
807
+ }
808
+ const data = res.data;
809
+ success(`production \uC2B9\uACA9 \uC644\uB8CC! (release #${data.seq})`);
810
+ if (data.production_url) {
811
+ log(`
812
+ ${colors.cyan}Production: ${data.production_url}${colors.reset}
813
+ `);
814
+ }
815
+ }
816
+ async function listReleases(config) {
817
+ const baseStorageUrl = `${config.baseUrl}/v1/public/storages/webs/${config.storageId}`;
818
+ const headers = { "X-Public-Key": config.publicKey };
819
+ const res = await makeRequest(`${baseStorageUrl}/deployments`, "GET", headers);
820
+ if (res.status < 200 || res.status >= 300) {
821
+ const data2 = res.data;
822
+ const msg = typeof data2 === "object" && data2 !== null ? data2.error || data2.message || JSON.stringify(data2) : `HTTP ${res.status}`;
823
+ error(`\uBC30\uD3EC \uC774\uB825 \uC870\uD68C \uC2E4\uD328: ${msg}`);
824
+ process.exit(1);
825
+ }
826
+ const data = res.data;
827
+ if (!data.deployments || data.deployments.length === 0) {
828
+ info("\uBC30\uD3EC \uC774\uB825\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 deploy \uD558\uC138\uC694.");
829
+ return;
830
+ }
831
+ log(`
832
+ ${colors.cyan}\uBC30\uD3EC \uC774\uB825${colors.reset} (promote_mode: ${data.promote_mode || "auto"}, \uCD1D ${data.total}\uAC1C)
833
+ `);
834
+ for (const d of data.deployments) {
835
+ const markers = [];
836
+ if (d.is_production) markers.push(`${colors.green}production${colors.reset}`);
837
+ if (d.is_qa) markers.push(`${colors.yellow}qa${colors.reset}`);
838
+ const marker = markers.length > 0 ? ` \u2190 ${markers.join(", ")}` : "";
839
+ const statusColor = d.status === "ready" ? colors.green : d.status === "failed" ? colors.red : colors.yellow;
840
+ log(
841
+ ` #${String(d.seq).padEnd(4)} ${statusColor}${d.status.padEnd(9)}${colors.reset} ${String(d.file_count).padStart(4)}\uAC1C ${formatSize(d.total_size).padStart(9)} ${d.created_at} ${colors.dim}${d.id}${colors.reset}${marker}`
842
+ );
843
+ }
844
+ log("");
845
+ if (data.production_url) log(`${colors.cyan}Production: ${data.production_url}${colors.reset}`);
846
+ if (data.qa_url) log(`${colors.yellow}QA: ${data.qa_url}${colors.reset}`);
847
+ log(`${colors.dim}\uB864\uBC31: npx connectbase promote <deployment_id>${colors.reset}
848
+ `);
849
+ }
762
850
  function prompt(question) {
763
851
  const rl = readline.createInterface({
764
852
  input: process.stdin,
@@ -905,7 +993,7 @@ ${colors.cyan}\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB85C\uADF8\uC778\uD558\uC13
905
993
  error("\uC778\uC99D \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.");
906
994
  process.exit(1);
907
995
  }
908
- async function init() {
996
+ async function init(config = {}, opts = {}) {
909
997
  log(`
910
998
  ${colors.cyan}Connect Base \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654${colors.reset}
911
999
  `);
@@ -916,49 +1004,104 @@ ${colors.cyan}Connect Base \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654${colors.r
916
1004
  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
917
1005
  `);
918
1006
  }
1007
+ const nonInteractive = opts.yes === true;
1008
+ if (!process.stdin.isTTY && !nonInteractive) {
1009
+ 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");
1010
+ log(`${colors.dim}\uC608: npx connectbase init --yes --secret-key cb_sk_... [--app <\uC571ID>] [--storage <\uC2A4\uD1A0\uB9AC\uC9C0ID>] [--deploy-dir ./dist]${colors.reset}`);
1011
+ 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}`);
1012
+ process.exit(1);
1013
+ }
919
1014
  const rcPath = path2.join(cwd, ".connectbaserc");
1015
+ let existingRc = {};
920
1016
  if (fs2.existsSync(rcPath)) {
921
- 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): `);
922
- if (overwrite.toLowerCase() !== "y") {
923
- info("\uCD08\uAE30\uD654\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
924
- return;
1017
+ try {
1018
+ existingRc = JSON.parse(fs2.readFileSync(rcPath, "utf-8"));
1019
+ } catch {
1020
+ }
1021
+ if (nonInteractive) {
1022
+ if (!opts.force) {
1023
+ error(".connectbaserc\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uB36E\uC5B4\uC4F0\uB824\uBA74 --force\uB97C \uD568\uAED8 \uC0AC\uC6A9\uD558\uC138\uC694");
1024
+ process.exit(1);
1025
+ }
1026
+ } else {
1027
+ 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): `);
1028
+ if (overwrite.toLowerCase() !== "y") {
1029
+ info("\uCD08\uAE30\uD654\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
1030
+ return;
1031
+ }
925
1032
  }
926
1033
  }
927
- log(`
928
- ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${colors.reset}`);
929
- log(` ${colors.cyan}1${colors.reset}) Secret Key \uC9C1\uC811 \uC785\uB825 (cb_sk_...)`);
930
- log(` ${colors.cyan}2${colors.reset}) \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC73C\uB85C \uC790\uB3D9 \uBC1C\uAE09
931
- `);
932
- const authChoice = await prompt(`${colors.blue}?${colors.reset} \uC120\uD0DD (1/2): `);
933
- let secretKey = "";
934
- if (authChoice === "2") {
935
- try {
936
- secretKey = await browserAuthFlow();
937
- } catch (err) {
938
- error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
939
- 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");
1034
+ let secretKey = config.secretKey || "";
1035
+ if (secretKey) {
1036
+ if (!secretKey.startsWith("cb_sk_")) {
1037
+ 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");
940
1038
  process.exit(1);
941
1039
  }
1040
+ info("Secret Key \uD655\uC778\uB428 (\uD50C\uB798\uADF8/\uD658\uACBD\uBCC0\uC218/.connectbaserc)");
1041
+ } else if (nonInteractive) {
1042
+ error("Secret Key\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4: --secret-key cb_sk_... \uB610\uB294 CONNECTBASE_SECRET_KEY \uD658\uACBD\uBCC0\uC218");
1043
+ process.exit(1);
942
1044
  } else {
943
- log(`${colors.dim}Secret Key (cb_sk_): \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uBC1C\uAE09${colors.reset}
1045
+ log(`
1046
+ ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${colors.reset}`);
1047
+ log(` ${colors.cyan}1${colors.reset}) Secret Key \uC9C1\uC811 \uC785\uB825 (cb_sk_...)`);
1048
+ log(` ${colors.cyan}2${colors.reset}) \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC73C\uB85C \uC790\uB3D9 \uBC1C\uAE09
944
1049
  `);
945
- secretKey = await promptSecret(`${colors.blue}?${colors.reset} Secret Key: `);
946
- if (!secretKey) {
947
- error("Secret Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
948
- process.exit(1);
949
- }
950
- if (secretKey.startsWith("cb_pk_")) {
951
- error("Public Key\uAC00 \uC544\uB2CC Secret Key(cb_sk_)\uB97C \uC785\uB825\uD558\uC138\uC694");
952
- info("Secret Key\uB294 \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uC0DD\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
953
- process.exit(1);
954
- }
955
- if (!secretKey.startsWith("cb_sk_")) {
956
- 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");
957
- process.exit(1);
1050
+ const authChoice = await prompt(`${colors.blue}?${colors.reset} \uC120\uD0DD (1/2): `);
1051
+ if (authChoice === "2") {
1052
+ try {
1053
+ secretKey = await browserAuthFlow();
1054
+ } catch (err) {
1055
+ error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
1056
+ 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");
1057
+ process.exit(1);
1058
+ }
1059
+ } else {
1060
+ log(`${colors.dim}Secret Key (cb_sk_): \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uBC1C\uAE09${colors.reset}
1061
+ `);
1062
+ secretKey = await promptSecret(`${colors.blue}?${colors.reset} Secret Key: `);
1063
+ if (!secretKey) {
1064
+ error("Secret Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
1065
+ process.exit(1);
1066
+ }
1067
+ if (secretKey.startsWith("cb_pk_")) {
1068
+ error("Public Key\uAC00 \uC544\uB2CC Secret Key(cb_sk_)\uB97C \uC785\uB825\uD558\uC138\uC694");
1069
+ info("Secret Key\uB294 \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uC0DD\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
1070
+ process.exit(1);
1071
+ }
1072
+ if (!secretKey.startsWith("cb_sk_")) {
1073
+ 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");
1074
+ process.exit(1);
1075
+ }
958
1076
  }
959
1077
  }
960
1078
  let appId = "";
961
1079
  let publicKey = "";
1080
+ const projectName = path2.basename(cwd);
1081
+ const reuseKeyIfSameApp = (selectedAppId) => {
1082
+ if (existingRc.appId === selectedAppId && existingRc.publicKey?.startsWith("cb_pk_")) {
1083
+ publicKey = existingRc.publicKey;
1084
+ success("\uAE30\uC874 Public Key \uC7AC\uC0AC\uC6A9 (.connectbaserc)");
1085
+ return true;
1086
+ }
1087
+ return false;
1088
+ };
1089
+ const issuePublicKey = async (targetAppId) => {
1090
+ info("Public Key \uC0DD\uC131 \uC911...");
1091
+ const newKeyRes = await makeRequest(
1092
+ `${DEFAULT_BASE_URL}/v1/public/cli/apps/${targetAppId}/public-keys`,
1093
+ "POST",
1094
+ { "X-Public-Key": secretKey },
1095
+ JSON.stringify({})
1096
+ );
1097
+ if (newKeyRes.status === 201) {
1098
+ const keyData = newKeyRes.data;
1099
+ publicKey = keyData.key;
1100
+ success("Public Key \uBC1C\uAE09 \uC644\uB8CC");
1101
+ } else {
1102
+ warn("Public Key \uC790\uB3D9 \uC0DD\uC131 \uC2E4\uD328. \uC218\uB3D9\uC73C\uB85C \uC0DD\uC131\uD558\uC138\uC694");
1103
+ }
1104
+ };
962
1105
  try {
963
1106
  info("\uC571 \uBAA9\uB85D \uC870\uD68C \uC911...");
964
1107
  const appsRes = await makeRequest(
@@ -975,7 +1118,23 @@ ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${co
975
1118
  }
976
1119
  const appsData = appsRes.data;
977
1120
  const apps = appsData.apps || [];
978
- if (apps.length > 0) {
1121
+ if (opts.appId) {
1122
+ const found = apps.find((a) => a.id === opts.appId);
1123
+ if (!found) {
1124
+ error(`\uC9C0\uC815\uD55C \uC571\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.appId}`);
1125
+ process.exit(1);
1126
+ }
1127
+ appId = found.id;
1128
+ success(`\uC120\uD0DD\uB428: ${found.name}`);
1129
+ if (!reuseKeyIfSameApp(appId)) await issuePublicKey(appId);
1130
+ } else if (nonInteractive) {
1131
+ const found = apps.find((a) => a.name === projectName);
1132
+ if (found) {
1133
+ appId = found.id;
1134
+ success(`\uAE30\uC874 \uC571 \uC7AC\uC0AC\uC6A9: ${found.name}`);
1135
+ if (!reuseKeyIfSameApp(appId)) await issuePublicKey(appId);
1136
+ }
1137
+ } else if (apps.length > 0) {
979
1138
  log(`
980
1139
  ${colors.dim}\uB0B4 \uC571 \uBAA9\uB85D:${colors.reset}`);
981
1140
  apps.forEach((a, i) => {
@@ -989,25 +1148,11 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
989
1148
  if (num > 0 && num <= apps.length) {
990
1149
  appId = apps[num - 1].id;
991
1150
  success(`\uC120\uD0DD\uB428: ${apps[num - 1].name}`);
992
- info("Public Key \uC0DD\uC131 \uC911...");
993
- const newKeyRes = await makeRequest(
994
- `${DEFAULT_BASE_URL}/v1/public/cli/apps/${appId}/public-keys`,
995
- "POST",
996
- { "X-Public-Key": secretKey },
997
- JSON.stringify({})
998
- );
999
- if (newKeyRes.status === 201) {
1000
- const keyData = newKeyRes.data;
1001
- publicKey = keyData.key;
1002
- success("Public Key \uBC1C\uAE09 \uC644\uB8CC");
1003
- } else {
1004
- warn("Public Key \uC790\uB3D9 \uC0DD\uC131 \uC2E4\uD328. \uC218\uB3D9\uC73C\uB85C \uC0DD\uC131\uD558\uC138\uC694");
1005
- }
1151
+ if (!reuseKeyIfSameApp(appId)) await issuePublicKey(appId);
1006
1152
  }
1007
1153
  }
1008
1154
  if (!appId) {
1009
- const projectName = path2.basename(cwd);
1010
- const appName = await prompt(`${colors.blue}?${colors.reset} \uC571 \uC774\uB984 (${projectName}): `) || projectName;
1155
+ const appName = nonInteractive ? projectName : await prompt(`${colors.blue}?${colors.reset} \uC571 \uC774\uB984 (${projectName}): `) || projectName;
1011
1156
  info("\uC571 \uC0DD\uC131 \uC911...");
1012
1157
  const createRes = await makeRequest(
1013
1158
  `${DEFAULT_BASE_URL}/v1/public/cli/apps`,
@@ -1034,6 +1179,10 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
1034
1179
  }
1035
1180
  } catch (err) {
1036
1181
  error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
1182
+ if (nonInteractive) {
1183
+ 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");
1184
+ process.exit(1);
1185
+ }
1037
1186
  log(`${colors.dim}\uC218\uB3D9\uC73C\uB85C \uC785\uB825\uD558\uC138\uC694${colors.reset}`);
1038
1187
  appId = await prompt(`${colors.blue}?${colors.reset} App ID: `);
1039
1188
  publicKey = await prompt(`${colors.blue}?${colors.reset} Public Key (cb_pk_): `);
@@ -1048,77 +1197,92 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
1048
1197
  }
1049
1198
  const publicKeyForSdk = publicKey;
1050
1199
  let storageId = "";
1051
- try {
1052
- info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC870\uD68C \uC911...");
1053
- const listRes = await makeRequest(
1054
- `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
1055
- "GET",
1056
- { "X-Public-Key": publicKeyForSdk }
1057
- );
1058
- if (listRes.status === 200) {
1059
- const listData = listRes.data;
1060
- const storages = listData.storages || [];
1061
- if (storages.length > 0) {
1062
- log(`
1200
+ if (opts.storageId) {
1201
+ storageId = opts.storageId;
1202
+ success(`\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC9C0\uC815\uB428: ${storageId}`);
1203
+ } else {
1204
+ try {
1205
+ info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC870\uD68C \uC911...");
1206
+ const listRes = await makeRequest(
1207
+ `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
1208
+ "GET",
1209
+ { "X-Public-Key": publicKeyForSdk }
1210
+ );
1211
+ if (listRes.status === 200) {
1212
+ const listData = listRes.data;
1213
+ const storages = listData.storages || [];
1214
+ if (nonInteractive) {
1215
+ const found = storages.find((s) => s.name === projectName);
1216
+ if (found) {
1217
+ storageId = found.id;
1218
+ success(`\uAE30\uC874 \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC7AC\uC0AC\uC6A9: ${found.name}`);
1219
+ }
1220
+ } else if (storages.length > 0) {
1221
+ log(`
1063
1222
  ${colors.dim}\uAE30\uC874 \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0:${colors.reset}`);
1064
- storages.forEach((s, i) => {
1065
- log(` ${colors.cyan}${i + 1}${colors.reset}) ${s.name} (${colors.dim}${s.id}${colors.reset})`);
1066
- });
1067
- log(` ${colors.cyan}0${colors.reset}) \uC0C8\uB85C \uC0DD\uC131`);
1068
- const choice = await prompt(`
1223
+ storages.forEach((s, i) => {
1224
+ log(` ${colors.cyan}${i + 1}${colors.reset}) ${s.name} (${colors.dim}${s.id}${colors.reset})`);
1225
+ });
1226
+ log(` ${colors.cyan}0${colors.reset}) \uC0C8\uB85C \uC0DD\uC131`);
1227
+ const choice = await prompt(`
1069
1228
  ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `);
1070
- const num = parseInt(choice, 10);
1071
- if (num > 0 && num <= storages.length) {
1072
- storageId = storages[num - 1].id;
1073
- success(`\uC120\uD0DD\uB428: ${storages[num - 1].name}`);
1229
+ const num = parseInt(choice, 10);
1230
+ if (num > 0 && num <= storages.length) {
1231
+ storageId = storages[num - 1].id;
1232
+ success(`\uC120\uD0DD\uB428: ${storages[num - 1].name}`);
1233
+ }
1074
1234
  }
1235
+ if (!storageId) {
1236
+ const name = nonInteractive ? projectName : await prompt(`${colors.blue}?${colors.reset} \uC2A4\uD1A0\uB9AC\uC9C0 \uC774\uB984 (${projectName}): `) || projectName;
1237
+ info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC911...");
1238
+ const createRes = await makeRequest(
1239
+ `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
1240
+ "POST",
1241
+ { "X-Public-Key": publicKeyForSdk },
1242
+ JSON.stringify({ name })
1243
+ );
1244
+ if (createRes.status !== 200) {
1245
+ const data = createRes.data;
1246
+ error(`\uC0DD\uC131 \uC2E4\uD328: ${data?.error || data?.message || `HTTP ${createRes.status}`}`);
1247
+ process.exit(1);
1248
+ }
1249
+ const createData = createRes.data;
1250
+ storageId = createData.storage_web_id;
1251
+ success(`\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC644\uB8CC: ${createData.name}`);
1252
+ }
1253
+ } else {
1254
+ throw new Error(`HTTP ${listRes.status}`);
1075
1255
  }
1256
+ } catch (err) {
1257
+ error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
1258
+ if (nonInteractive) {
1259
+ 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");
1260
+ process.exit(1);
1261
+ }
1262
+ log(`${colors.dim}\uC218\uB3D9\uC73C\uB85C Storage ID\uB97C \uC785\uB825\uD558\uC138\uC694${colors.reset}`);
1263
+ storageId = await prompt(`${colors.blue}?${colors.reset} Storage ID: `);
1076
1264
  if (!storageId) {
1077
- const projectName = path2.basename(cwd);
1078
- const name = await prompt(`${colors.blue}?${colors.reset} \uC2A4\uD1A0\uB9AC\uC9C0 \uC774\uB984 (${projectName}): `) || projectName;
1079
- info("\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC911...");
1080
- const createRes = await makeRequest(
1081
- `${DEFAULT_BASE_URL}/v1/public/storages/webs`,
1082
- "POST",
1083
- { "X-Public-Key": publicKeyForSdk },
1084
- JSON.stringify({ name })
1085
- );
1086
- if (createRes.status !== 200) {
1087
- const data = createRes.data;
1088
- error(`\uC0DD\uC131 \uC2E4\uD328: ${data?.error || data?.message || `HTTP ${createRes.status}`}`);
1089
- process.exit(1);
1090
- }
1091
- const createData = createRes.data;
1092
- storageId = createData.storage_web_id;
1093
- success(`\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uC0DD\uC131 \uC644\uB8CC: ${createData.name}`);
1265
+ error("Storage ID\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
1266
+ process.exit(1);
1094
1267
  }
1095
- } else {
1096
- throw new Error(`HTTP ${listRes.status}`);
1097
- }
1098
- } catch (err) {
1099
- error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
1100
- log(`${colors.dim}\uC218\uB3D9\uC73C\uB85C Storage ID\uB97C \uC785\uB825\uD558\uC138\uC694${colors.reset}`);
1101
- storageId = await prompt(`${colors.blue}?${colors.reset} Storage ID: `);
1102
- if (!storageId) {
1103
- error("Storage ID\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
1104
- process.exit(1);
1105
1268
  }
1106
1269
  }
1107
- const defaultDir = detectBuildDir();
1108
- const deployDir = await prompt(`${colors.blue}?${colors.reset} \uBC30\uD3EC \uB514\uB809\uD1A0\uB9AC (${defaultDir}): `) || defaultDir;
1109
- const config = {
1270
+ const defaultDir = opts.deployDir || detectBuildDir();
1271
+ const deployDir = nonInteractive ? defaultDir : await prompt(`${colors.blue}?${colors.reset} \uBC30\uD3EC \uB514\uB809\uD1A0\uB9AC (${defaultDir}): `) || defaultDir;
1272
+ const rcData = {
1273
+ appId,
1110
1274
  publicKey: publicKey || "",
1111
1275
  storageId,
1112
1276
  deployDir
1113
1277
  };
1114
1278
  if (secretKey) {
1115
- config.secretKey = secretKey;
1279
+ rcData.secretKey = secretKey;
1116
1280
  }
1117
- fs2.writeFileSync(rcPath, JSON.stringify(config, null, 2) + "\n");
1281
+ fs2.writeFileSync(rcPath, JSON.stringify(rcData, null, 2) + "\n");
1118
1282
  success(".connectbaserc \uC0DD\uC131 \uC644\uB8CC");
1119
1283
  addToGitignore(".connectbaserc");
1120
1284
  addDeployScript(deployDir);
1121
- await setupClaudeCode(publicKeyForSdk, secretKey, projectRoot);
1285
+ await setupClaudeCode(secretKey, projectRoot);
1122
1286
  log(`
1123
1287
  ${colors.green}\uCD08\uAE30\uD654 \uC644\uB8CC!${colors.reset}
1124
1288
  `);
@@ -1132,9 +1296,20 @@ function detectBuildDir() {
1132
1296
  if (fs2.existsSync(path2.join(process.cwd(), "dist"))) return "./dist";
1133
1297
  if (fs2.existsSync(path2.join(process.cwd(), "build"))) return "./build";
1134
1298
  if (fs2.existsSync(path2.join(process.cwd(), "out"))) return "./out";
1135
- if (fs2.existsSync(path2.join(process.cwd(), ".next"))) return "./out";
1299
+ const nextConfig = ["next.config.js", "next.config.mjs", "next.config.ts"].map((f) => path2.join(process.cwd(), f)).find((f) => fs2.existsSync(f));
1300
+ if (nextConfig || fs2.existsSync(path2.join(process.cwd(), ".next"))) {
1301
+ if (nextConfig) {
1302
+ try {
1303
+ const src = fs2.readFileSync(nextConfig, "utf-8");
1304
+ if (!/output\s*:\s*['"]export['"]/.test(src)) {
1305
+ 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`);
1306
+ }
1307
+ } catch {
1308
+ }
1309
+ }
1310
+ return "./out";
1311
+ }
1136
1312
  if (fs2.existsSync(path2.join(process.cwd(), "vite.config.ts")) || fs2.existsSync(path2.join(process.cwd(), "vite.config.js"))) return "./dist";
1137
- if (fs2.existsSync(path2.join(process.cwd(), "next.config.js")) || fs2.existsSync(path2.join(process.cwd(), "next.config.mjs"))) return "./out";
1138
1313
  return "./dist";
1139
1314
  }
1140
1315
  function addToGitignore(entry, basePath) {
@@ -1142,7 +1317,8 @@ function addToGitignore(entry, basePath) {
1142
1317
  const gitignorePath = path2.join(dir, ".gitignore");
1143
1318
  if (fs2.existsSync(gitignorePath)) {
1144
1319
  const content = fs2.readFileSync(gitignorePath, "utf-8");
1145
- if (!content.includes(entry)) {
1320
+ const lines = content.split(/\r?\n/).map((l) => l.trim());
1321
+ if (!lines.includes(entry)) {
1146
1322
  fs2.appendFileSync(gitignorePath, `
1147
1323
  # Connect Base
1148
1324
  ${entry}
@@ -1223,13 +1399,66 @@ function detectMonorepo(gitRoot) {
1223
1399
  }
1224
1400
  return result;
1225
1401
  }
1226
- async function downloadDocs(publicKey, templates, baseDir) {
1402
+ var DOCS_MANIFEST_FILE = ".connectbase-manifest.json";
1403
+ function readDocsManifest(docsDir) {
1404
+ try {
1405
+ const parsed = JSON.parse(fs2.readFileSync(path2.join(docsDir, DOCS_MANIFEST_FILE), "utf-8"));
1406
+ if (parsed && Array.isArray(parsed.files)) return parsed;
1407
+ } catch {
1408
+ }
1409
+ return null;
1410
+ }
1411
+ var LEGACY_DOC_FILES = [
1412
+ "intro.md",
1413
+ "mcp-workflow.md",
1414
+ "sdk-basic.md",
1415
+ "sdk-database.md",
1416
+ "sdk-storage.md",
1417
+ "sdk-realtime.md",
1418
+ "sdk-auth.md",
1419
+ "sdk-payment.md",
1420
+ "example.md",
1421
+ "mcp-config.md",
1422
+ "mcp-tools.md",
1423
+ "resources.md",
1424
+ "sdk-game.md",
1425
+ "sdk-push.md",
1426
+ "sdk-video.md",
1427
+ "sdk-ads.md",
1428
+ "sdk-ai.md",
1429
+ "sdk-error-tracker.md",
1430
+ "sdk-webrtc.md",
1431
+ "sdk-subscription.md",
1432
+ "sdk-functions.md",
1433
+ "sdk-webhooks.md",
1434
+ "deploy-guide.md",
1435
+ "app-packaging.md",
1436
+ "best-practices.md",
1437
+ "code-analysis.md",
1438
+ "sdk-queue.md",
1439
+ "sdk-knowledge.md",
1440
+ "sdk-workflow.md",
1441
+ "sdk-tunnel.md",
1442
+ "sdk-native.md",
1443
+ "sdk-analytics.md",
1444
+ "sdk-domain.md",
1445
+ "sdk-public-key.md",
1446
+ "sdk-invoice.md",
1447
+ "web-performance.md",
1448
+ "sdk-analytics-advanced.md",
1449
+ "sdk-endpoint.md",
1450
+ "sdk-web-search.md",
1451
+ "sdk-oauth-provider.md",
1452
+ "sdk-oauth-integration.md"
1453
+ ];
1454
+ async function downloadDocs(baseDir, opts = {}) {
1455
+ const quiet = opts.quiet === true;
1227
1456
  if (!baseDir) {
1228
1457
  baseDir = getProjectRoot();
1229
1458
  }
1230
1459
  const gitRoot = getGitRoot() || baseDir;
1231
1460
  const monorepo = detectMonorepo(gitRoot);
1232
- if (monorepo.type !== "none") {
1461
+ if (!quiet && monorepo.type !== "none") {
1233
1462
  info(`\uBAA8\uB178\uB808\uD3EC \uAC10\uC9C0: ${monorepo.type} (\uB8E8\uD2B8: ${gitRoot})`);
1234
1463
  if (monorepo.isSubPackage) {
1235
1464
  info(`\uD604\uC7AC \uC11C\uBE0C \uD328\uD0A4\uC9C0: ${monorepo.subPackageName || path2.basename(process.cwd())}`);
@@ -1240,14 +1469,10 @@ async function downloadDocs(publicKey, templates, baseDir) {
1240
1469
  if (!fs2.existsSync(docsDir)) {
1241
1470
  fs2.mkdirSync(docsDir, { recursive: true });
1242
1471
  }
1243
- if (!templates) {
1244
- templates = ["rules"];
1245
- }
1246
- info("SDK \uAC1C\uBC1C \uAC00\uC774\uB4DC \uB2E4\uC6B4\uB85C\uB4DC \uC911...");
1472
+ if (!quiet) info("SDK \uAC1C\uBC1C \uAC00\uC774\uB4DC \uB2E4\uC6B4\uB85C\uB4DC \uC911...");
1247
1473
  try {
1248
- const templateParam = templates.join(",");
1249
1474
  const res = await makeRequest(
1250
- `${DEFAULT_BASE_URL}/v1/storages/webs/claude-md?template=${encodeURIComponent(templateParam)}&format=sections&public_key=${encodeURIComponent(publicKey)}`,
1475
+ `${DEFAULT_BASE_URL}/v1/storages/webs/claude-md?template=rules&format=sections`,
1251
1476
  "GET",
1252
1477
  {}
1253
1478
  );
@@ -1256,18 +1481,59 @@ async function downloadDocs(publicKey, templates, baseDir) {
1256
1481
  }
1257
1482
  const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
1258
1483
  const sections = data.sections || [];
1484
+ const hasher = crypto.createHash("sha256");
1485
+ const newFiles = [];
1259
1486
  for (const section of sections) {
1260
- const filePath = path2.join(docsDir, section.filename);
1261
- fs2.writeFileSync(filePath, section.content);
1487
+ const safeName = path2.basename(section.filename);
1488
+ newFiles.push(safeName);
1489
+ hasher.update(safeName).update("\0").update(section.content).update("\0");
1490
+ }
1491
+ const contentHash = hasher.digest("hex");
1492
+ const prevManifest = readDocsManifest(docsDir);
1493
+ const allPresent = newFiles.every((f) => fs2.existsSync(path2.join(docsDir, f)));
1494
+ if (prevManifest && prevManifest.contentHash === contentHash && allPresent) {
1495
+ if (!quiet) success("SDK \uBB38\uC11C\uAC00 \uC774\uBBF8 \uCD5C\uC2E0\uC785\uB2C8\uB2E4");
1496
+ updateRootClaudeMd(rootClaudeMd, quiet);
1497
+ if (monorepo.isSubPackage) {
1498
+ createSubPackageClaudeMd(path2.join(process.cwd(), "CLAUDE.md"), gitRoot, quiet);
1499
+ }
1500
+ return "unchanged";
1262
1501
  }
1263
- success(`${gitRoot}/.claude/docs/ \uC5D0 ${sections.length}\uAC1C \uBB38\uC11C \uC800\uC7A5 \uC644\uB8CC`);
1264
- updateRootClaudeMd(rootClaudeMd);
1502
+ 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 -->
1503
+
1504
+ `;
1505
+ for (const section of sections) {
1506
+ const safeName = path2.basename(section.filename);
1507
+ fs2.writeFileSync(path2.join(docsDir, safeName), stamp + section.content);
1508
+ }
1509
+ const staleCandidates = prevManifest ? prevManifest.files.map((f) => path2.basename(f)) : LEGACY_DOC_FILES;
1510
+ for (const old of staleCandidates) {
1511
+ if (newFiles.includes(old)) continue;
1512
+ const oldPath = path2.join(docsDir, old);
1513
+ if (fs2.existsSync(oldPath)) {
1514
+ try {
1515
+ fs2.unlinkSync(oldPath);
1516
+ if (!quiet) info(`\uC2A4\uD14C\uC77C \uBB38\uC11C \uC0AD\uC81C: ${old}`);
1517
+ } catch {
1518
+ }
1519
+ }
1520
+ }
1521
+ const manifest = {
1522
+ cliVersion: VERSION,
1523
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1524
+ contentHash,
1525
+ files: newFiles
1526
+ };
1527
+ fs2.writeFileSync(path2.join(docsDir, DOCS_MANIFEST_FILE), JSON.stringify(manifest, null, 2) + "\n");
1528
+ if (!quiet) success(`${gitRoot}/.claude/docs/ \uC5D0 ${sections.length}\uAC1C \uBB38\uC11C \uC800\uC7A5 \uC644\uB8CC`);
1529
+ updateRootClaudeMd(rootClaudeMd, quiet);
1265
1530
  if (monorepo.isSubPackage) {
1266
- const subClaudeMd = path2.join(process.cwd(), "CLAUDE.md");
1267
- createSubPackageClaudeMd(subClaudeMd, gitRoot);
1531
+ createSubPackageClaudeMd(path2.join(process.cwd(), "CLAUDE.md"), gitRoot, quiet);
1268
1532
  }
1269
- log(`${colors.dim}\u203B SDK \uBB38\uC11C\uB294 MCP search_sdk_docs\uB85C \uAC80\uC0C9\uD558\uC138\uC694${colors.reset}`);
1533
+ if (!quiet) log(`${colors.dim}\u203B SDK \uBB38\uC11C\uB294 MCP search_sdk_docs\uB85C \uAC80\uC0C9\uD558\uC138\uC694${colors.reset}`);
1534
+ return "updated";
1270
1535
  } catch {
1536
+ if (quiet) return "failed";
1271
1537
  warn("SDK \uAC00\uC774\uB4DC \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328, \uAE30\uBCF8 \uBB38\uC11C\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4");
1272
1538
  const fallbackContent = `# ConnectBase \uD504\uB85C\uC81D\uD2B8
1273
1539
 
@@ -1282,9 +1548,10 @@ async function downloadDocs(publicKey, templates, baseDir) {
1282
1548
  createSubPackageClaudeMd(subClaudeMd, gitRoot);
1283
1549
  }
1284
1550
  success(`.claude/docs/ \uC0DD\uC131 \uC644\uB8CC (\uAE30\uBCF8)`);
1551
+ return "failed";
1285
1552
  }
1286
1553
  }
1287
- function updateRootClaudeMd(claudeMdPath) {
1554
+ function updateRootClaudeMd(claudeMdPath, quiet = false) {
1288
1555
  const startMarker = "<!-- CONNECTBASE_START -->";
1289
1556
  const endMarker = "<!-- CONNECTBASE_END -->";
1290
1557
  const sdkBlock = `${startMarker}
@@ -1292,11 +1559,13 @@ function updateRootClaudeMd(claudeMdPath) {
1292
1559
 
1293
1560
  \uC774 \uD504\uB85C\uC81D\uD2B8\uB294 **ConnectBase**\uB97C \uBC31\uC5D4\uB4DC\uB85C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.
1294
1561
 
1295
- ### AI \uD544\uB3C5 \u2014 \uCF54\uB529 \uC804 \uBC18\uB4DC\uC2DC \uC218\uD589
1562
+ @.claude/docs/project-rules.md
1296
1563
 
1297
- 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)
1298
- 2. \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
1299
- 3. \uAC80\uC0C9 \uACB0\uACFC\uC758 \uCF54\uB4DC \uD328\uD134\uC744 \uB530\uB77C \uAD6C\uD604\uD558\uC138\uC694
1564
+ ### AI \uD544\uC218 \uC6CC\uD06C\uD50C\uB85C\uC6B0
1565
+
1566
+ 1. \uC704\uC5D0 import\uB41C project-rules.md\uC758 \uAE30\uB2A5 \uB9E4\uD551\uD45C\xB7\uBCF4\uC548 \uADDC\uCE59\uC744 \uB530\uB974\uC138\uC694
1567
+ 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
1568
+ 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)
1300
1569
  ${endMarker}`;
1301
1570
  if (fs2.existsSync(claudeMdPath)) {
1302
1571
  let content = fs2.readFileSync(claudeMdPath, "utf-8");
@@ -1308,16 +1577,16 @@ ${endMarker}`;
1308
1577
  content = sdkBlock + "\n\n" + content.trimStart();
1309
1578
  }
1310
1579
  fs2.writeFileSync(claudeMdPath, content);
1311
- info("CLAUDE.md\uC5D0 ConnectBase \uCC38\uC870 \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC");
1580
+ if (!quiet) info("CLAUDE.md\uC5D0 ConnectBase \uCC38\uC870 \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC");
1312
1581
  } else {
1313
1582
  fs2.writeFileSync(claudeMdPath, `# \uD504\uB85C\uC81D\uD2B8
1314
1583
 
1315
1584
  ${sdkBlock}
1316
1585
  `);
1317
- success("CLAUDE.md \uC0DD\uC131 \uC644\uB8CC");
1586
+ if (!quiet) success("CLAUDE.md \uC0DD\uC131 \uC644\uB8CC");
1318
1587
  }
1319
1588
  }
1320
- function createSubPackageClaudeMd(subClaudeMdPath, gitRoot) {
1589
+ function createSubPackageClaudeMd(subClaudeMdPath, gitRoot, quiet = false) {
1321
1590
  const startMarker = "<!-- CONNECTBASE_SUB_START -->";
1322
1591
  const endMarker = "<!-- CONNECTBASE_SUB_END -->";
1323
1592
  const relPath = path2.relative(path2.dirname(subClaudeMdPath), gitRoot);
@@ -1328,9 +1597,9 @@ function createSubPackageClaudeMd(subClaudeMdPath, gitRoot) {
1328
1597
 
1329
1598
  ### AI \uD544\uB3C5
1330
1599
 
1331
- 1. \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC758 \`${relPath}/.claude/docs/project-rules.md\`\uB97C **Read tool\uB85C \uC77D\uC73C\uC138\uC694**
1332
- 2. \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C SDK \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
1333
- 3. \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC758 \`${relPath}/CLAUDE.md\`\uB3C4 \uCC38\uACE0\uD558\uC138\uC694
1600
+ 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
1601
+ 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
1602
+ 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
1334
1603
  ${endMarker}`;
1335
1604
  if (fs2.existsSync(subClaudeMdPath)) {
1336
1605
  let content = fs2.readFileSync(subClaudeMdPath, "utf-8");
@@ -1350,7 +1619,7 @@ ${endMarker}`;
1350
1619
  ${subBlock}
1351
1620
  `);
1352
1621
  }
1353
- success(`\uC11C\uBE0C \uD328\uD0A4\uC9C0 CLAUDE.md \uC0DD\uC131 \uC644\uB8CC: ${subClaudeMdPath}`);
1622
+ if (!quiet) success(`\uC11C\uBE0C \uD328\uD0A4\uC9C0 CLAUDE.md \uC0DD\uC131 \uC644\uB8CC: ${subClaudeMdPath}`);
1354
1623
  }
1355
1624
  async function setupMcp(secretKey) {
1356
1625
  const gitRoot = getGitRoot();
@@ -1364,6 +1633,13 @@ async function setupMcp(secretKey) {
1364
1633
  }
1365
1634
  const mcpConfigPath = path2.join(root, ".mcp.json");
1366
1635
  if (!secretKey) {
1636
+ if (!process.stdin.isTTY) {
1637
+ error("Secret Key\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4 (\uBE44\uB300\uD654\uD615 \uD658\uACBD)");
1638
+ log(`${colors.dim}\uB2E4\uC74C \uC911 \uD558\uB098\uB85C \uD0A4\uB97C \uC804\uB2EC\uD558\uC138\uC694:${colors.reset}`);
1639
+ log(` ${colors.cyan}npx connectbase mcp --secret-key cb_sk_...${colors.reset}`);
1640
+ log(` ${colors.cyan}CONNECTBASE_SECRET_KEY=cb_sk_... npx connectbase mcp${colors.reset}`);
1641
+ process.exit(1);
1642
+ }
1367
1643
  log(`
1368
1644
  ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${colors.reset}`);
1369
1645
  log(` ${colors.cyan}1${colors.reset}) Secret Key \uC9C1\uC811 \uC785\uB825 (cb_sk_...)`);
@@ -1427,9 +1703,9 @@ ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${co
1427
1703
  log(`
1428
1704
  ${colors.dim}Claude Code\uC5D0\uC11C ConnectBase MCP \uB3C4\uAD6C\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.${colors.reset}`);
1429
1705
  }
1430
- async function setupClaudeCode(publicKey, secretKey, projectRoot) {
1706
+ async function setupClaudeCode(secretKey, projectRoot) {
1431
1707
  const root = projectRoot || getProjectRoot();
1432
- await downloadDocs(publicKey, void 0, root);
1708
+ await downloadDocs(root);
1433
1709
  await setupMcp(secretKey);
1434
1710
  }
1435
1711
  async function fetchLatestVersion(packageName) {
@@ -1448,13 +1724,18 @@ async function fetchLatestVersion(packageName) {
1448
1724
  return null;
1449
1725
  }
1450
1726
  function compareVersions(a, b) {
1451
- const pa = a.split(".").map(Number);
1452
- const pb = b.split(".").map(Number);
1727
+ const [aMain, aPre] = a.split("-", 2);
1728
+ const [bMain, bPre] = b.split("-", 2);
1729
+ const pa = aMain.split(".").map(Number);
1730
+ const pb = bMain.split(".").map(Number);
1453
1731
  for (let i = 0; i < 3; i++) {
1454
1732
  const diff = (pa[i] || 0) - (pb[i] || 0);
1455
1733
  if (diff !== 0) return diff > 0 ? 1 : -1;
1456
1734
  }
1457
- return 0;
1735
+ if (aPre === bPre) return 0;
1736
+ if (!aPre) return 1;
1737
+ if (!bPre) return -1;
1738
+ return aPre < bPre ? -1 : aPre > bPre ? 1 : 0;
1458
1739
  }
1459
1740
  function findSubPackagesWithConfig(gitRoot) {
1460
1741
  const results = [];
@@ -1526,36 +1807,7 @@ ${colors.cyan}ConnectBase Update${colors.reset}
1526
1807
  if (!opts.skipDocs) {
1527
1808
  log("");
1528
1809
  info("SDK \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uC911...");
1529
- let docsKey = config.publicKey || config.secretKey;
1530
- if (!docsKey) {
1531
- const rootRcPath = path2.join(projectRoot, ".connectbaserc");
1532
- if (fs2.existsSync(rootRcPath)) {
1533
- try {
1534
- const rc = JSON.parse(fs2.readFileSync(rootRcPath, "utf-8"));
1535
- docsKey = rc.publicKey || rc.secretKey;
1536
- } catch {
1537
- }
1538
- }
1539
- }
1540
- if (!docsKey && monorepo.type !== "none") {
1541
- const subPackages = findSubPackagesWithConfig(projectRoot);
1542
- for (const sp of subPackages) {
1543
- try {
1544
- const rc = JSON.parse(fs2.readFileSync(path2.join(sp, ".connectbaserc"), "utf-8"));
1545
- if (rc.publicKey || rc.secretKey) {
1546
- docsKey = rc.publicKey || rc.secretKey;
1547
- break;
1548
- }
1549
- } catch {
1550
- }
1551
- }
1552
- }
1553
- if (docsKey) {
1554
- await downloadDocs(docsKey, void 0, projectRoot);
1555
- } else {
1556
- warn("Public Key\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC5B4 \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4");
1557
- info(".connectbaserc \uD30C\uC77C\uC774\uB098 --public-key \uC635\uC158\uC73C\uB85C \uD0A4\uB97C \uC9C0\uC815\uD558\uC138\uC694");
1558
- }
1810
+ await downloadDocs(projectRoot);
1559
1811
  }
1560
1812
  if (!opts.skipMcp) {
1561
1813
  log("");
@@ -2620,6 +2872,9 @@ ${colors.yellow}\uBA85\uB839\uC5B4:${colors.reset}
2620
2872
  docs SDK \uBB38\uC11C \uB2E4\uC6B4\uB85C\uB4DC/\uC5C5\uB370\uC774\uD2B8 (\uBAA8\uB178\uB808\uD3EC \uC790\uB3D9 \uAC10\uC9C0)
2621
2873
  mcp MCP \uC11C\uBC84 \uC124\uC815 (.mcp.json \uC0DD\uC131/\uC5C5\uB370\uC774\uD2B8, \uBAA8\uB178\uB808\uD3EC \uC790\uB3D9 \uAC10\uC9C0)
2622
2874
  deploy <directory> \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0\uC5D0 \uD30C\uC77C \uBC30\uD3EC (--dev: Dev \uD658\uACBD)
2875
+ promote_mode=manual \uC774\uBA74 QA \uD658\uACBD\uC5D0\uB9CC \uBC30\uD3EC\uB429\uB2C8\uB2E4
2876
+ promote [releaseId] QA \uAC80\uC99D \uD6C4 production \uBC18\uC601 (\uBBF8\uC9C0\uC815: \uCD5C\uC2E0 \uBC30\uD3EC, \uACFC\uAC70 id: \uB864\uBC31)
2877
+ releases \uBC30\uD3EC \uC774\uB825 \uC870\uD68C (release \uBC88\uD638/\uC0C1\uD0DC/production\xB7QA \uD3EC\uC778\uD130)
2623
2878
  tunnel <port> \uB85C\uCEEC \uC11C\uBE44\uC2A4\uB97C \uC778\uD130\uB137\uC5D0 \uB178\uCD9C
2624
2879
 
2625
2880
  ${colors.yellow}\uCF54\uB4DC \uBD84\uC11D (MCP \uD1B5\uD574 \uC0AC\uC6A9):${colors.reset}
@@ -2642,8 +2897,12 @@ ${colors.yellow}\uC635\uC158:${colors.reset}
2642
2897
  --label <name> Endpoint binding \uC790\uB3D9 \uB4F1\uB85D (tunnel \uC804\uC6A9) \u2014 SDK \uC758 cb.endpoint.call(label) \uD638\uCD9C \uAC00\uB2A5
2643
2898
  --description <text> Endpoint binding \uC124\uBA85 (--label \uB3D9\uBC18 \uC2DC\uB9CC)
2644
2899
  -d, --dev Dev \uD658\uACBD\uC5D0 \uBC30\uD3EC (deploy \uC804\uC6A9)
2900
+ -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)
2901
+ -a, --app <id> \uAE30\uC874 \uC571 ID \uC9C0\uC815 (init \uC804\uC6A9)
2902
+ --secret-key <key> Secret Key (cb_sk_) \uC9C0\uC815 (init/mcp)
2903
+ --deploy-dir <dir> \uBC30\uD3EC \uB514\uB809\uD1A0\uB9AC \uC9C0\uC815 (init \uC804\uC6A9, \uAE30\uBCF8: \uC790\uB3D9 \uAC10\uC9C0)
2645
2904
  --check \uBC84\uC804\uB9CC \uD655\uC778 (update \uC804\uC6A9)
2646
- --skip-docs \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update \uC804\uC6A9)
2905
+ --skip-docs \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update/deploy)
2647
2906
  --skip-mcp MCP \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update \uC804\uC6A9)
2648
2907
  --setup-root \uBAA8\uB178\uB808\uD3EC \uB8E8\uD2B8\uC5D0 \uD3B8\uC758 \uC2A4\uD06C\uB9BD\uD2B8 \uCD94\uAC00 (update \uC804\uC6A9)
2649
2908
  (docs, mcp, update\uB294 \uBAA8\uB178\uB808\uD3EC\uB97C \uC790\uB3D9 \uAC10\uC9C0\uD558\uC5EC \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC5D0 \uC0DD\uC131)
@@ -2654,6 +2913,9 @@ ${colors.yellow}\uBE60\uB978 \uC2DC\uC791:${colors.reset}
2654
2913
  ${colors.dim}# 1. \uCD08\uAE30\uD654 (\uCD5C\uCD08 1\uD68C)${colors.reset}
2655
2914
  npx connectbase init
2656
2915
 
2916
+ ${colors.dim}# 1-1. \uBE44\uB300\uD654\uD615 \uCD08\uAE30\uD654 (CI/AI \uC5D0\uC774\uC804\uD2B8)${colors.reset}
2917
+ npx connectbase init --yes --secret-key cb_sk_...
2918
+
2657
2919
  ${colors.dim}# 2. SDK \uC804\uCCB4 \uC5C5\uB370\uC774\uD2B8 (\uBC84\uC804 \uCCB4\uD06C + \uBB38\uC11C + MCP)${colors.reset}
2658
2920
  npx connectbase update
2659
2921
 
@@ -2669,7 +2931,12 @@ ${colors.yellow}\uBE60\uB978 \uC2DC\uC791:${colors.reset}
2669
2931
  ${colors.dim}# 4. Prod \uBC30\uD3EC${colors.reset}
2670
2932
  npm run deploy
2671
2933
 
2672
- ${colors.dim}# 4. Dev \uD658\uACBD \uBC30\uD3EC (\uB0B4\uBD80 QA\uC6A9)${colors.reset}
2934
+ ${colors.dim}# 4-1. QA \uC6CC\uD06C\uD50C\uB85C\uC6B0 (\uCF58\uC194\uC5D0\uC11C promote_mode=manual \uC124\uC815 \uC2DC)${colors.reset}
2935
+ npx connectbase deploy ./dist ${colors.dim}# QA \uD658\uACBD\uC5D0\uB9CC \uBC30\uD3EC${colors.reset}
2936
+ npx connectbase releases ${colors.dim}# \uBC30\uD3EC \uC774\uB825 \uD655\uC778${colors.reset}
2937
+ npx connectbase promote ${colors.dim}# QA \uAC80\uC99D \uD6C4 production \uBC18\uC601${colors.reset}
2938
+
2939
+ ${colors.dim}# 4-2. Dev \uD658\uACBD \uBC30\uD3EC (\uB0B4\uBD80 \uD655\uC778\uC6A9)${colors.reset}
2673
2940
  npx connectbase deploy ./dist --dev
2674
2941
 
2675
2942
  ${colors.dim}# 5. \uD130\uB110 (\uAE30\uBCF8)${colors.reset}
@@ -2724,6 +2991,10 @@ function parseArgs(args) {
2724
2991
  result.options.appId = args[++i];
2725
2992
  } else if (arg === "--force") {
2726
2993
  result.options.force = "true";
2994
+ } else if (arg === "-y" || arg === "--yes") {
2995
+ result.options.yes = "true";
2996
+ } else if (arg === "--deploy-dir") {
2997
+ result.options.deployDir = args[++i];
2727
2998
  } else if (arg === "--public") {
2728
2999
  result.options.public = "true";
2729
3000
  } else if (arg === "--show-token") {
@@ -2771,7 +3042,13 @@ async function main() {
2771
3042
  baseUrl: parsed.options.baseUrl || fileConfig.baseUrl || DEFAULT_BASE_URL
2772
3043
  };
2773
3044
  if (parsed.command === "init") {
2774
- await init();
3045
+ await init(config, {
3046
+ yes: parsed.options.yes === "true",
3047
+ force: parsed.options.force === "true",
3048
+ appId: parsed.options.appId,
3049
+ storageId: parsed.options.storageId,
3050
+ deployDir: parsed.options.deployDir
3051
+ });
2775
3052
  } else if (parsed.command === "update") {
2776
3053
  await update(config, {
2777
3054
  checkOnly: parsed.options.check === "true",
@@ -2780,9 +3057,9 @@ async function main() {
2780
3057
  setupRoot: parsed.options.setupRoot === "true"
2781
3058
  });
2782
3059
  } else if (parsed.command === "docs") {
2783
- await downloadDocs(config.publicKey || "");
3060
+ await downloadDocs();
2784
3061
  } else if (parsed.command === "mcp") {
2785
- await setupMcp();
3062
+ await setupMcp(config.secretKey);
2786
3063
  } else if (parsed.command === "deploy") {
2787
3064
  const directory = parsed.args[0] || fileConfig.deployDir || ".";
2788
3065
  if (!config.publicKey) {
@@ -2801,6 +3078,32 @@ async function main() {
2801
3078
  else warn(`--timeout \uAC12\uC774 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC544 \uBB34\uC2DC\uB429\uB2C8\uB2E4: ${parsed.options.timeout}`);
2802
3079
  }
2803
3080
  await deploy(directory, config, isDev, deployOpts);
3081
+ if (parsed.options.skipDocs !== "true") {
3082
+ try {
3083
+ const docsRoot = getGitRoot() || getProjectRoot();
3084
+ if (fs2.existsSync(path2.join(docsRoot, ".claude", "docs", DOCS_MANIFEST_FILE))) {
3085
+ const refreshed = await downloadDocs(docsRoot, { quiet: true });
3086
+ if (refreshed === "updated") {
3087
+ info("SDK \uBB38\uC11C\uAC00 \uC0C8 \uBC84\uC804\uC73C\uB85C \uAC31\uC2E0\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (.claude/docs/)");
3088
+ }
3089
+ }
3090
+ } catch {
3091
+ }
3092
+ }
3093
+ } else if (parsed.command === "promote" || parsed.command === "releases") {
3094
+ if (!config.publicKey) {
3095
+ 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');
3096
+ process.exit(1);
3097
+ }
3098
+ if (!config.storageId) {
3099
+ 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');
3100
+ process.exit(1);
3101
+ }
3102
+ if (parsed.command === "promote") {
3103
+ await promote(config, parsed.args[0]);
3104
+ } else {
3105
+ await listReleases(config);
3106
+ }
2804
3107
  } else if (parsed.command === "tunnel") {
2805
3108
  const portStr = parsed.args[0];
2806
3109
  if (!portStr) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "3.43.0",
3
+ "version": "3.45.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",