@bifos/nhncloud-cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # nhncloud-cli
2
2
 
3
3
  NHN Cloud 서비스를 AWS CLI 방식으로 호출하는 통합 CLI.
4
- 현재 `configure`, `logncrash search/send` (Log & Crash 로그 검색·전송), `deploy` (배포·바이너리 조회·업로드·다운로드), `instance` (Compute 인스턴스 목록·발급·전원 제어·타입 변경·키페어 관리·이미지·가용성 영역 조회·볼륨 연결 포함), `network` (VPC·서브넷 목록 조회), `volume` (Block Storage 볼륨 목록·조회·생성) 명령을 지원한다.
4
+ 현재 `configure`, `logncrash search/send` (Log & Crash 로그 검색·전송), `deploy` (배포·바이너리 조회·업로드·다운로드), `instance` (Compute 인스턴스 목록·발급·전원 제어·타입 변경·키페어 관리·이미지·가용성 영역 조회·볼륨 연결 포함), `network` (VPC·서브넷 목록 조회), `volume` (Block Storage 볼륨 목록·조회·생성), `ncr` (NHN Container Registry 레지스트리 목록·조회·이미지 목록·태그 목록) 명령을 지원한다.
5
5
 
6
6
  ## 설치
7
7
 
@@ -17,7 +17,7 @@ npm install -g @bifos/nhncloud-cli
17
17
  nhncloud configure
18
18
  ```
19
19
 
20
- - profile → UAK(id/secret) → logncrash appkey/secret → iaas 자격증명 순으로 입력한다.
20
+ - profile → UAK(id/secret) → logncrash appkey/secret → iaas 자격증명 → ncr appkey 순으로 입력한다.
21
21
  - 저장 전 연결 테스트를 자동으로 수행한다 (`--no-verify` 로 생략 가능).
22
22
  - CI/자동화는 flag 로 비대화형 설정이 가능하다.
23
23
 
@@ -437,6 +437,58 @@ nhncloud floatingip delete <floatingip-id> --yes
437
437
  > **associate**: `floatingip associate <floatingip-id> <instance-id>` 는 instance→port_id 매핑 경로 미확정으로 보류 중.
438
438
  > 실측으로 경로가 확정되면 후속 task 에서 추가한다.
439
439
 
440
+ ### NHN Container Registry (NCR)
441
+
442
+ NCR Management API 로 레지스트리를 조회한다.
443
+ 인증은 공통 UAK(`X-TC-AUTHENTICATION-ID/SECRET` 정적 헤더)를 재사용하며 OAuth 토큰 교환이 없다.
444
+ appKey 는 NHN Cloud 콘솔 → Container Registry 서비스에서 확인한다.
445
+
446
+ ```bash
447
+ # ncr appkey 설정 (비대화형)
448
+ nhncloud configure --ncr-appkey <appkey>
449
+
450
+ # 레지스트리 목록 조회
451
+ nhncloud ncr list --app-key <appkey>
452
+
453
+ # region 지정 (기본: kr1)
454
+ nhncloud ncr list --region kr2 --app-key <appkey>
455
+
456
+ # JSON 출력
457
+ nhncloud ncr list --app-key <appkey> --json
458
+
459
+ # 단일 레지스트리 조회 (이름 또는 ID)
460
+ nhncloud ncr get <registry-name> --app-key <appkey>
461
+ ```
462
+
463
+ appKey 를 `nhncloud configure --ncr-appkey <appkey>` 로 저장해 두면 `--app-key` 없이도 호출할 수 있다.
464
+
465
+ ```bash
466
+ # configure 로 저장 후 --app-key 생략
467
+ nhncloud configure --ncr-appkey <appkey>
468
+ nhncloud ncr list
469
+ nhncloud ncr get <registry-name>
470
+ ```
471
+
472
+ 이미지(repository)·태그는 레지스트리 데이터플레인 Harbor REST API 를 직접 호출한다.
473
+ UAK 를 Basic Auth 로 사용하며 추가 설정은 없다.
474
+
475
+ ```bash
476
+ # 이미지(repository) 목록 조회
477
+ nhncloud ncr images <registry>
478
+
479
+ # region 지정
480
+ nhncloud ncr images <registry> --region kr2
481
+
482
+ # JSON 출력
483
+ nhncloud ncr images <registry> --json
484
+
485
+ # 특정 이미지의 태그 목록 조회
486
+ nhncloud ncr tags <registry> <repository>
487
+
488
+ # JSON 출력 (태그·push_time·size)
489
+ nhncloud ncr tags <registry> <repository> --json
490
+ ```
491
+
440
492
  ## 개발
441
493
 
442
494
  ```bash
@@ -445,4 +497,5 @@ pnpm run build # tsup 단일 번들 (dist/index.js)
445
497
  pnpm tsc --noEmit # 타입 체크
446
498
  node dist/index.js instance --help
447
499
  node dist/index.js logncrash search --help
500
+ node dist/index.js ncr --help
448
501
  ```
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/index.ts
27
- var import_commander34 = require("commander");
27
+ var import_commander38 = require("commander");
28
28
  var import_chalk12 = __toESM(require("chalk"));
29
29
 
30
30
  // src/utils/spinner.ts
@@ -495,6 +495,21 @@ var BLOCKSTORAGE_HOST = {
495
495
  jp1: "jp1-api-block-storage-infrastructure.nhncloudservice.com"
496
496
  };
497
497
  var IAAS_REGIONS = Object.keys(INSTANCE_HOST).join(", ");
498
+ var NCR_HOST = {
499
+ kr1: "kr1-ncr.api.nhncloudservice.com",
500
+ kr2: "kr2-ncr.api.nhncloudservice.com",
501
+ kr3: "kr3-ncr.api.nhncloudservice.com"
502
+ };
503
+ function ncrHost(region) {
504
+ const host = NCR_HOST[region];
505
+ if (!host) {
506
+ throw new NhnCloudCliError(
507
+ `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 NCR region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${Object.keys(NCR_HOST).join(", ")}`,
508
+ EXIT_PARAM_ERROR
509
+ );
510
+ }
511
+ return host;
512
+ }
498
513
  function instanceHost(region) {
499
514
  const host = INSTANCE_HOST[region];
500
515
  if (!host) {
@@ -740,6 +755,96 @@ var LogncrashClient = class {
740
755
  }
741
756
  };
742
757
 
758
+ // src/services/ncr/client.ts
759
+ var import_ky5 = __toESM(require("ky"));
760
+
761
+ // src/services/ncr/types.ts
762
+ function isRegistry(val) {
763
+ if (typeof val !== "object" || val === null) return false;
764
+ const obj = val;
765
+ return typeof obj["name"] === "string";
766
+ }
767
+ function isRepository(val) {
768
+ if (typeof val !== "object" || val === null) return false;
769
+ const obj = val;
770
+ return typeof obj["name"] === "string";
771
+ }
772
+ function isArtifact(val) {
773
+ return typeof val === "object" && val !== null;
774
+ }
775
+
776
+ // src/services/ncr/client.ts
777
+ var DEFAULT_TIMEOUT_MS = 3e4;
778
+ var NcrClient = class {
779
+ uakId;
780
+ uakSecret;
781
+ baseUrl;
782
+ constructor(uakId, uakSecret, region) {
783
+ this.uakId = uakId;
784
+ this.uakSecret = uakSecret;
785
+ this.baseUrl = `https://${ncrHost(region)}`;
786
+ }
787
+ authHeaders() {
788
+ return {
789
+ "X-TC-AUTHENTICATION-ID": this.uakId,
790
+ "X-TC-AUTHENTICATION-SECRET": this.uakSecret
791
+ };
792
+ }
793
+ /**
794
+ * 레지스트리 목록을 반환한다.
795
+ * GET /ncr/v2.0/appkeys/{appKey}/registries
796
+ * 응답: { header, registries: [...] } — body 가 아니라 named 필드.
797
+ */
798
+ async listRegistries(appKey) {
799
+ const url = `${this.baseUrl}/ncr/v2.0/appkeys/${encodeURIComponent(appKey)}/registries`;
800
+ try {
801
+ const res = await import_ky5.default.get(url, {
802
+ headers: this.authHeaders(),
803
+ retry: 0,
804
+ timeout: DEFAULT_TIMEOUT_MS
805
+ }).json();
806
+ unwrapHeader(res);
807
+ if (!Array.isArray(res.registries)) {
808
+ if (res.registries === void 0) return [];
809
+ throw new NhnCloudCliError(
810
+ "NCR API \uC751\uB2F5 \uD615\uC2DD \uC624\uB958: registries \uAC00 \uBC30\uC5F4\uC774 \uC544\uB2D9\uB2C8\uB2E4.",
811
+ EXIT_API_ERROR
812
+ );
813
+ }
814
+ return res.registries.filter(isRegistry);
815
+ } catch (err) {
816
+ if (err instanceof NhnCloudCliError) throw err;
817
+ throw toNhnCloudCliError(err);
818
+ }
819
+ }
820
+ /**
821
+ * 단일 레지스트리를 반환한다.
822
+ * GET /ncr/v2.0/appkeys/{appKey}/registries/{registryNameOrId}
823
+ * 응답: { header, registry: {...} } — body 가 아니라 named 필드.
824
+ */
825
+ async getRegistry(appKey, registry) {
826
+ const url = `${this.baseUrl}/ncr/v2.0/appkeys/${encodeURIComponent(appKey)}/registries/${encodeURIComponent(registry)}`;
827
+ try {
828
+ const res = await import_ky5.default.get(url, {
829
+ headers: this.authHeaders(),
830
+ retry: 0,
831
+ timeout: DEFAULT_TIMEOUT_MS
832
+ }).json();
833
+ unwrapHeader(res);
834
+ if (res.registry && isRegistry(res.registry)) {
835
+ return res.registry;
836
+ }
837
+ throw new NhnCloudCliError(
838
+ "NCR API \uC751\uB2F5 \uD615\uC2DD \uC624\uB958: registry \uAC1D\uCCB4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
839
+ EXIT_API_ERROR
840
+ );
841
+ } catch (err) {
842
+ if (err instanceof NhnCloudCliError) throw err;
843
+ throw toNhnCloudCliError(err);
844
+ }
845
+ }
846
+ };
847
+
743
848
  // src/commands/configure-verify.ts
744
849
  async function verifyUserAccessKey(uak) {
745
850
  try {
@@ -763,6 +868,19 @@ async function verifyIaas(iaas) {
763
868
  throw err;
764
869
  }
765
870
  }
871
+ async function verifyNcr(uak, appkey) {
872
+ if (!appkey) return false;
873
+ const client = new NcrClient(uak.id, uak.secret, "kr1");
874
+ try {
875
+ await client.listRegistries(appkey);
876
+ return true;
877
+ } catch (err) {
878
+ if (err instanceof NhnCloudCliError && err.exitCode === EXIT_AUTH_ERROR) {
879
+ return false;
880
+ }
881
+ throw err;
882
+ }
883
+ }
766
884
  async function verifyLogncrash(cred) {
767
885
  if (!cred.appkey || !cred.secret) return false;
768
886
  const client = new LogncrashClient(cred.appkey, cred.secret);
@@ -786,7 +904,7 @@ async function verifyLogncrash(cred) {
786
904
  }
787
905
 
788
906
  // src/commands/configure.ts
789
- async function saveAndVerify(profileName, uak, logncrash, iaas, doVerify) {
907
+ async function saveAndVerify(profileName, uak, logncrash, iaas, ncr, doVerify) {
790
908
  if (doVerify) {
791
909
  if (uak) {
792
910
  const ok = await verifyUserAccessKey(uak);
@@ -821,6 +939,26 @@ async function saveAndVerify(profileName, uak, logncrash, iaas, doVerify) {
821
939
  );
822
940
  }
823
941
  }
942
+ if (ncr) {
943
+ if (uak) {
944
+ const appkey = ncr.appkey ?? "";
945
+ const ok = await verifyNcr(uak, appkey);
946
+ if (ok) {
947
+ process.stderr.write(import_chalk.default.green(" \u2713 ncr \uC5F0\uACB0 \uC131\uACF5 (kr1)\n"));
948
+ } else {
949
+ throw new NhnCloudCliError(
950
+ "ncr \uC778\uC99D \uC2E4\uD328 \u2014 appkey \uB610\uB294 UAK \uB97C \uD655\uC778\uD558\uC138\uC694.",
951
+ EXIT_AUTH_ERROR
952
+ );
953
+ }
954
+ } else {
955
+ process.stderr.write(
956
+ import_chalk.default.yellow(
957
+ " \u26A0 ncr verify \uAC74\uB108\uB700 \u2014 \uC774\uBC88 \uC124\uC815\uC5D0 UAK \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. ncr \uBA85\uB839\uC740 \uACF5\uD1B5 UAK \uAC00 \uD544\uC694\uD558\uB2C8 \uBA3C\uC800 \uC124\uC815\uD558\uC138\uC694.\n"
958
+ )
959
+ );
960
+ }
961
+ }
824
962
  }
825
963
  if (uak) {
826
964
  await setUserAccessKey(profileName, uak);
@@ -831,6 +969,9 @@ async function saveAndVerify(profileName, uak, logncrash, iaas, doVerify) {
831
969
  if (iaas) {
832
970
  await setIaasCredential(profileName, iaas);
833
971
  }
972
+ if (ncr) {
973
+ await setServiceCredential(profileName, "ncr", ncr);
974
+ }
834
975
  process.stderr.write(import_chalk.default.green(`
835
976
  \u2713 profile "${profileName}" \uC124\uC815\uC774 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
836
977
  `));
@@ -890,12 +1031,25 @@ async function runInteractive(opts) {
890
1031
  });
891
1032
  iaas = { tenantId, username: iaasUsername, password: iaasPassword, region };
892
1033
  }
1034
+ let ncr;
1035
+ const setupNcr = await confirm({
1036
+ message: "ncr \uC790\uACA9\uC99D\uBA85\uB3C4 \uC124\uC815\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
1037
+ default: false
1038
+ });
1039
+ if (setupNcr) {
1040
+ process.stderr.write(import_chalk.default.gray("\n\u2014 ncr (Container Registry) \uC790\uACA9\uC99D\uBA85 \u2014\n"));
1041
+ const ncrAppkey = await input({
1042
+ message: "ncr appkey",
1043
+ validate: (v) => v.trim().length > 0 || "ncr appkey \uB97C \uC785\uB825\uD558\uC138\uC694"
1044
+ });
1045
+ ncr = { appkey: ncrAppkey.trim() };
1046
+ }
893
1047
  if (opts.verify) {
894
1048
  process.stderr.write(import_chalk.default.gray("\n\u2014 \uC5F0\uACB0 \uD14C\uC2A4\uD2B8 \uC911\u2026 \u2014\n"));
895
1049
  }
896
1050
  if (opts.verify) {
897
1051
  try {
898
- await saveAndVerify(profileName, uak, logncrash, iaas, true);
1052
+ await saveAndVerify(profileName, uak, logncrash, iaas, ncr, true);
899
1053
  } catch (err) {
900
1054
  if (err instanceof NhnCloudCliError && err.exitCode === EXIT_AUTH_ERROR) {
901
1055
  process.stderr.write(import_chalk.default.red(` \u2717 ${err.message}
@@ -908,13 +1062,13 @@ async function runInteractive(opts) {
908
1062
  process.stderr.write(import_chalk.default.yellow("\uC800\uC7A5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
909
1063
  return;
910
1064
  }
911
- await saveAndVerify(profileName, uak, logncrash, iaas, false);
1065
+ await saveAndVerify(profileName, uak, logncrash, iaas, ncr, false);
912
1066
  } else {
913
1067
  throw err;
914
1068
  }
915
1069
  }
916
1070
  } else {
917
- await saveAndVerify(profileName, uak, logncrash, iaas, false);
1071
+ await saveAndVerify(profileName, uak, logncrash, iaas, ncr, false);
918
1072
  }
919
1073
  }
920
1074
  async function runNonInteractive(opts) {
@@ -930,22 +1084,23 @@ async function runNonInteractive(opts) {
930
1084
  password: iaasPassword,
931
1085
  region: opts.iaasRegion ?? "kr1"
932
1086
  } : void 0;
933
- if (!uak && !logncrash && !iaas) {
1087
+ const ncr = opts.ncrAppkey?.trim() ? { appkey: opts.ncrAppkey.trim() } : void 0;
1088
+ if (!uak && !logncrash && !iaas && !ncr) {
934
1089
  throw new NhnCloudCliError(
935
- "\uBE44\uB300\uD654\uD615 \uBAA8\uB4DC: --uak-id + UAK secret, --logncrash-appkey + logncrash secret,\n\uB610\uB294 --iaas-tenant-id + --iaas-username + iaas password \uC911 \uD558\uB098\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.\nsecret/password \uB294 \uB178\uCD9C \uBC29\uC9C0\uB97C \uC704\uD574 \uD658\uACBD\uBCC0\uC218 \uAD8C\uC7A5:\nNHNCLOUD_UAK_SECRET / NHNCLOUD_LOGNCRASH_SECRET / NHNCLOUD_IAAS_PASSWORD.",
1090
+ "\uBE44\uB300\uD654\uD615 \uBAA8\uB4DC: --uak-id + UAK secret, --logncrash-appkey + logncrash secret,\n--iaas-tenant-id + --iaas-username + iaas password,\n\uB610\uB294 --ncr-appkey \uC911 \uD558\uB098\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.\nsecret/password \uB294 \uB178\uCD9C \uBC29\uC9C0\uB97C \uC704\uD574 \uD658\uACBD\uBCC0\uC218 \uAD8C\uC7A5:\nNHNCLOUD_UAK_SECRET / NHNCLOUD_LOGNCRASH_SECRET / NHNCLOUD_IAAS_PASSWORD.",
936
1091
  EXIT_PARAM_ERROR
937
1092
  );
938
1093
  }
939
1094
  if (opts.verify) {
940
1095
  process.stderr.write(import_chalk.default.gray("\uC5F0\uACB0 \uD14C\uC2A4\uD2B8 \uC911\u2026\n"));
941
1096
  }
942
- await saveAndVerify(profileName, uak, logncrash, iaas, opts.verify);
1097
+ await saveAndVerify(profileName, uak, logncrash, iaas, ncr, opts.verify);
943
1098
  }
944
1099
  var configureCommand = new import_commander.Command("configure").description("\uC790\uACA9\uC99D\uBA85 \uC124\uC815 \uB9C8\uBC95\uC0AC (\uB300\uD654\uD615 + flag)").option("--profile <name>", "\uB300\uC0C1 profile \uC774\uB984 (\uAE30\uBCF8: default)").option("--uak-id <id>", "\uAC1C\uC778 UAK ID (\uBE44\uB300\uD654\uD615)").option("--uak-secret <secret>", "\uAC1C\uC778 UAK Secret (\uBE44\uB300\uD654\uD615, \uB178\uCD9C \uBC29\uC9C0\uB85C env NHNCLOUD_UAK_SECRET \uAD8C\uC7A5)").option("--logncrash-appkey <key>", "logncrash appkey (\uBE44\uB300\uD654\uD615)").option("--logncrash-secret <secret>", "logncrash secret (\uBE44\uB300\uD654\uD615, env NHNCLOUD_LOGNCRASH_SECRET \uAD8C\uC7A5)").option("--iaas-tenant-id <id>", "iaas tenantId / \uD504\uB85C\uC81D\uD2B8 ID (\uBE44\uB300\uD654\uD615)").option("--iaas-username <user>", "iaas IAM username (\uBE44\uB300\uD654\uD615)").option(
945
1100
  "--iaas-password <pass>",
946
1101
  "iaas API \uBE44\uBC00\uBC88\uD638 (\uBE44\uB300\uD654\uD615, \uB178\uCD9C \uBC29\uC9C0\uB85C env NHNCLOUD_IAAS_PASSWORD \uAD8C\uC7A5)"
947
- ).option("--iaas-region <region>", "iaas region (\uAE30\uBCF8: kr1)", "kr1").option("--no-verify", "\uC5F0\uACB0 \uD14C\uC2A4\uD2B8 \uC0DD\uB7B5").action(async (opts) => {
948
- const hasFlag = opts.uakId || opts.uakSecret || opts.logncrashAppkey || opts.logncrashSecret || opts.iaasTenantId || opts.iaasUsername || opts.iaasPassword;
1102
+ ).option("--iaas-region <region>", "iaas region (\uAE30\uBCF8: kr1)", "kr1").option("--ncr-appkey <key>", "ncr appkey (\uBE44\uB300\uD654\uD615)").option("--no-verify", "\uC5F0\uACB0 \uD14C\uC2A4\uD2B8 \uC0DD\uB7B5").action(async (opts) => {
1103
+ const hasFlag = opts.uakId || opts.uakSecret || opts.logncrashAppkey || opts.logncrashSecret || opts.iaasTenantId || opts.iaasUsername || opts.iaasPassword || opts.ncrAppkey;
949
1104
  try {
950
1105
  if (hasFlag) {
951
1106
  await runNonInteractive(opts);
@@ -1375,7 +1530,7 @@ async function scrollNextOrExpire(client, scrollKey) {
1375
1530
  var import_commander5 = require("commander");
1376
1531
 
1377
1532
  // src/services/deploy/client.ts
1378
- var import_ky5 = __toESM(require("ky"));
1533
+ var import_ky6 = __toESM(require("ky"));
1379
1534
  function isBinaryGroup(val) {
1380
1535
  if (typeof val !== "object" || val === null) return false;
1381
1536
  const obj = val;
@@ -1390,7 +1545,7 @@ function isBinary(val) {
1390
1545
  return (binaryKeyType === "number" || binaryKeyType === "string") && (binarySizeType === "number" || binarySizeType === "string");
1391
1546
  }
1392
1547
  var SYNC_TIMEOUT_MS = 6e5;
1393
- var DEFAULT_TIMEOUT_MS = 3e4;
1548
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
1394
1549
  var DeployClient = class {
1395
1550
  accessToken;
1396
1551
  baseUrl;
@@ -1422,14 +1577,14 @@ var DeployClient = class {
1422
1577
  payload["targetServerHostnames"] = params.targetHosts;
1423
1578
  }
1424
1579
  try {
1425
- const res = await import_ky5.default.post(url, {
1580
+ const res = await import_ky6.default.post(url, {
1426
1581
  headers: {
1427
1582
  ...this.authHeaders(),
1428
1583
  "Content-Type": "application/json"
1429
1584
  },
1430
1585
  json: payload,
1431
1586
  retry: 0,
1432
- timeout: isAsync ? DEFAULT_TIMEOUT_MS : SYNC_TIMEOUT_MS
1587
+ timeout: isAsync ? DEFAULT_TIMEOUT_MS2 : SYNC_TIMEOUT_MS
1433
1588
  }).json();
1434
1589
  return unwrap(res);
1435
1590
  } catch (err) {
@@ -1442,10 +1597,10 @@ var DeployClient = class {
1442
1597
  async artifacts(appKey) {
1443
1598
  const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts`;
1444
1599
  try {
1445
- const res = await import_ky5.default.get(url, {
1600
+ const res = await import_ky6.default.get(url, {
1446
1601
  headers: this.authHeaders(),
1447
1602
  retry: 0,
1448
- timeout: DEFAULT_TIMEOUT_MS
1603
+ timeout: DEFAULT_TIMEOUT_MS2
1449
1604
  }).json();
1450
1605
  return unwrap(res);
1451
1606
  } catch (err) {
@@ -1458,10 +1613,10 @@ var DeployClient = class {
1458
1613
  async serverGroups(appKey, artifactId) {
1459
1614
  const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/server-groups`;
1460
1615
  try {
1461
- const res = await import_ky5.default.get(url, {
1616
+ const res = await import_ky6.default.get(url, {
1462
1617
  headers: this.authHeaders(),
1463
1618
  retry: 0,
1464
- timeout: DEFAULT_TIMEOUT_MS
1619
+ timeout: DEFAULT_TIMEOUT_MS2
1465
1620
  }).json();
1466
1621
  return unwrap(res);
1467
1622
  } catch (err) {
@@ -1474,10 +1629,10 @@ var DeployClient = class {
1474
1629
  async histories(appKey, artifactId) {
1475
1630
  const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/deploy-histories`;
1476
1631
  try {
1477
- const res = await import_ky5.default.get(url, {
1632
+ const res = await import_ky6.default.get(url, {
1478
1633
  headers: this.authHeaders(),
1479
1634
  retry: 0,
1480
- timeout: DEFAULT_TIMEOUT_MS
1635
+ timeout: DEFAULT_TIMEOUT_MS2
1481
1636
  }).json();
1482
1637
  return unwrap(res);
1483
1638
  } catch (err) {
@@ -1490,10 +1645,10 @@ var DeployClient = class {
1490
1645
  async binaryGroups(appKey, artifactId) {
1491
1646
  const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/binary-groups`;
1492
1647
  try {
1493
- const res = await import_ky5.default.get(url, {
1648
+ const res = await import_ky6.default.get(url, {
1494
1649
  headers: this.authHeaders(),
1495
1650
  retry: 0,
1496
- timeout: DEFAULT_TIMEOUT_MS
1651
+ timeout: DEFAULT_TIMEOUT_MS2
1497
1652
  }).json();
1498
1653
  const body = unwrap(res);
1499
1654
  const list = body.binaryGroups;
@@ -1520,11 +1675,11 @@ var DeployClient = class {
1520
1675
  if (params.sortKey !== void 0) searchParams["sortKey"] = params.sortKey;
1521
1676
  if (params.sortDirection !== void 0) searchParams["sortDirection"] = params.sortDirection;
1522
1677
  try {
1523
- const res = await import_ky5.default.get(url, {
1678
+ const res = await import_ky6.default.get(url, {
1524
1679
  headers: this.authHeaders(),
1525
1680
  searchParams,
1526
1681
  retry: 0,
1527
- timeout: DEFAULT_TIMEOUT_MS
1682
+ timeout: DEFAULT_TIMEOUT_MS2
1528
1683
  }).json();
1529
1684
  const body = unwrap(res);
1530
1685
  const list = body.binaries;
@@ -1562,7 +1717,7 @@ var DeployClient = class {
1562
1717
  form.append("description", params.description);
1563
1718
  }
1564
1719
  try {
1565
- const res = await import_ky5.default.post(url, {
1720
+ const res = await import_ky6.default.post(url, {
1566
1721
  headers: this.authHeaders(),
1567
1722
  // 인증 헤더만 — multipart boundary 는 ky 가 자동 설정
1568
1723
  body: form,
@@ -1598,7 +1753,7 @@ var DeployClient = class {
1598
1753
  async downloadBinary(appKey, artifactId, binaryGroupKey, binaryKey) {
1599
1754
  const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/binary-group/${binaryGroupKey}/binaries/${binaryKey}`;
1600
1755
  try {
1601
- const ab = await import_ky5.default.get(url, {
1756
+ const ab = await import_ky6.default.get(url, {
1602
1757
  headers: this.authHeaders(),
1603
1758
  retry: 0,
1604
1759
  timeout: SYNC_TIMEOUT_MS
@@ -1974,8 +2129,8 @@ var downloadCommand = new import_commander12.Command("download").description("\u
1974
2129
  var import_commander13 = require("commander");
1975
2130
 
1976
2131
  // src/services/instance/client.ts
1977
- var import_ky6 = __toESM(require("ky"));
1978
- var DEFAULT_TIMEOUT_MS2 = 3e4;
2132
+ var import_ky7 = __toESM(require("ky"));
2133
+ var DEFAULT_TIMEOUT_MS3 = 3e4;
1979
2134
  var DEFAULT_POLL_INTERVAL_MS = 5e3;
1980
2135
  function isServer(val) {
1981
2136
  if (typeof val !== "object" || val === null) return false;
@@ -2104,10 +2259,10 @@ var InstanceClient = class {
2104
2259
  async list() {
2105
2260
  const url = `${this.computeEndpoint}/servers/detail`;
2106
2261
  try {
2107
- const raw = await import_ky6.default.get(url, {
2262
+ const raw = await import_ky7.default.get(url, {
2108
2263
  headers: this.authHeaders(),
2109
2264
  retry: 0,
2110
- timeout: DEFAULT_TIMEOUT_MS2
2265
+ timeout: DEFAULT_TIMEOUT_MS3
2111
2266
  }).json();
2112
2267
  if (!isServersResponse(raw)) {
2113
2268
  throw new NhnCloudCliError(
@@ -2126,10 +2281,10 @@ var InstanceClient = class {
2126
2281
  async get(id) {
2127
2282
  const url = `${this.computeEndpoint}/servers/${encodeURIComponent(id)}`;
2128
2283
  try {
2129
- const raw = await import_ky6.default.get(url, {
2284
+ const raw = await import_ky7.default.get(url, {
2130
2285
  headers: this.authHeaders(),
2131
2286
  retry: 0,
2132
- timeout: DEFAULT_TIMEOUT_MS2
2287
+ timeout: DEFAULT_TIMEOUT_MS3
2133
2288
  }).json();
2134
2289
  if (!isServerResponse(raw)) {
2135
2290
  throw new NhnCloudCliError(
@@ -2185,11 +2340,11 @@ var InstanceClient = class {
2185
2340
  }
2186
2341
  let raw;
2187
2342
  try {
2188
- raw = await import_ky6.default.post(url, {
2343
+ raw = await import_ky7.default.post(url, {
2189
2344
  headers: this.authHeaders(),
2190
2345
  json: { server: serverBody },
2191
2346
  retry: 0,
2192
- timeout: DEFAULT_TIMEOUT_MS2
2347
+ timeout: DEFAULT_TIMEOUT_MS3
2193
2348
  }).json();
2194
2349
  } catch (err) {
2195
2350
  throw toNhnCloudCliError(err);
@@ -2208,10 +2363,10 @@ var InstanceClient = class {
2208
2363
  async delete(id) {
2209
2364
  const url = `${this.computeEndpoint}/servers/${encodeURIComponent(id)}`;
2210
2365
  try {
2211
- await import_ky6.default.delete(url, {
2366
+ await import_ky7.default.delete(url, {
2212
2367
  headers: this.authHeaders(),
2213
2368
  retry: 0,
2214
- timeout: DEFAULT_TIMEOUT_MS2
2369
+ timeout: DEFAULT_TIMEOUT_MS3
2215
2370
  });
2216
2371
  } catch (err) {
2217
2372
  throw toNhnCloudCliError(err);
@@ -2226,11 +2381,11 @@ var InstanceClient = class {
2226
2381
  async serverAction(id, payload) {
2227
2382
  const url = `${this.computeEndpoint}/servers/${encodeURIComponent(id)}/action`;
2228
2383
  try {
2229
- await import_ky6.default.post(url, {
2384
+ await import_ky7.default.post(url, {
2230
2385
  headers: this.authHeaders(),
2231
2386
  json: payload,
2232
2387
  retry: 0,
2233
- timeout: DEFAULT_TIMEOUT_MS2
2388
+ timeout: DEFAULT_TIMEOUT_MS3
2234
2389
  });
2235
2390
  } catch (err) {
2236
2391
  throw toNhnCloudCliError(err);
@@ -2271,11 +2426,11 @@ var InstanceClient = class {
2271
2426
  if (params.minDisk !== void 0) searchParams["minDisk"] = params.minDisk;
2272
2427
  if (params.minRam !== void 0) searchParams["minRam"] = params.minRam;
2273
2428
  try {
2274
- const raw = await import_ky6.default.get(url, {
2429
+ const raw = await import_ky7.default.get(url, {
2275
2430
  headers: this.authHeaders(),
2276
2431
  searchParams,
2277
2432
  retry: 0,
2278
- timeout: DEFAULT_TIMEOUT_MS2
2433
+ timeout: DEFAULT_TIMEOUT_MS3
2279
2434
  }).json();
2280
2435
  if (params.detail) {
2281
2436
  if (!isFlavorDetailsResponse(raw)) {
@@ -2304,10 +2459,10 @@ var InstanceClient = class {
2304
2459
  async listAvailabilityZones() {
2305
2460
  const url = `${this.computeEndpoint}/os-availability-zone`;
2306
2461
  try {
2307
- const raw = await import_ky6.default.get(url, {
2462
+ const raw = await import_ky7.default.get(url, {
2308
2463
  headers: this.authHeaders(),
2309
2464
  retry: 0,
2310
- timeout: DEFAULT_TIMEOUT_MS2
2465
+ timeout: DEFAULT_TIMEOUT_MS3
2311
2466
  }).json();
2312
2467
  if (!isAvailabilityZonesResponse(raw)) {
2313
2468
  throw new NhnCloudCliError(
@@ -2324,7 +2479,7 @@ var InstanceClient = class {
2324
2479
  async listKeypairs() {
2325
2480
  const url = `${this.computeEndpoint}/os-keypairs`;
2326
2481
  try {
2327
- const raw = await import_ky6.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 }).json();
2482
+ const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
2328
2483
  if (!isKeypairsResponse(raw)) {
2329
2484
  throw new NhnCloudCliError(
2330
2485
  "instance keypairs \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 keypairs \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
@@ -2340,7 +2495,7 @@ var InstanceClient = class {
2340
2495
  async getKeypair(name) {
2341
2496
  const url = `${this.computeEndpoint}/os-keypairs/${encodeURIComponent(name)}`;
2342
2497
  try {
2343
- const raw = await import_ky6.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 }).json();
2498
+ const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
2344
2499
  if (!isKeypairDetailResponse(raw)) {
2345
2500
  throw new NhnCloudCliError(
2346
2501
  `instance keypair get(${name}) \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 keypair \uC0C1\uC138 \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`,
@@ -2365,11 +2520,11 @@ var InstanceClient = class {
2365
2520
  }
2366
2521
  let raw;
2367
2522
  try {
2368
- raw = await import_ky6.default.post(url, {
2523
+ raw = await import_ky7.default.post(url, {
2369
2524
  headers: this.authHeaders(),
2370
2525
  json: { keypair: keypairBody },
2371
2526
  retry: 0,
2372
- timeout: DEFAULT_TIMEOUT_MS2
2527
+ timeout: DEFAULT_TIMEOUT_MS3
2373
2528
  }).json();
2374
2529
  } catch (err) {
2375
2530
  throw toNhnCloudCliError(err);
@@ -2394,7 +2549,7 @@ var InstanceClient = class {
2394
2549
  async deleteKeypair(name) {
2395
2550
  const url = `${this.computeEndpoint}/os-keypairs/${encodeURIComponent(name)}`;
2396
2551
  try {
2397
- await import_ky6.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 });
2552
+ await import_ky7.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 });
2398
2553
  } catch (err) {
2399
2554
  throw toNhnCloudCliError(err);
2400
2555
  }
@@ -2414,11 +2569,11 @@ var InstanceClient = class {
2414
2569
  if (params.owner !== void 0) searchParams["owner"] = params.owner;
2415
2570
  if (params.status !== void 0) searchParams["status"] = params.status;
2416
2571
  try {
2417
- const raw = await import_ky6.default.get(url, {
2572
+ const raw = await import_ky7.default.get(url, {
2418
2573
  headers: this.authHeaders(),
2419
2574
  searchParams,
2420
2575
  retry: 0,
2421
- timeout: DEFAULT_TIMEOUT_MS2
2576
+ timeout: DEFAULT_TIMEOUT_MS3
2422
2577
  }).json();
2423
2578
  if (!isImagesResponse(raw)) {
2424
2579
  throw new NhnCloudCliError(
@@ -2438,7 +2593,7 @@ var InstanceClient = class {
2438
2593
  async listVolumeAttachments(serverId) {
2439
2594
  const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments`;
2440
2595
  try {
2441
- const raw = await import_ky6.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 }).json();
2596
+ const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
2442
2597
  if (!isVolumeAttachmentsResponse(raw)) {
2443
2598
  throw new NhnCloudCliError(
2444
2599
  "instance volumes \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volumeAttachments \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
@@ -2457,11 +2612,11 @@ var InstanceClient = class {
2457
2612
  async attachVolume(serverId, volumeId) {
2458
2613
  const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments`;
2459
2614
  try {
2460
- const res = await import_ky6.default.post(url, {
2615
+ const res = await import_ky7.default.post(url, {
2461
2616
  headers: this.authHeaders(),
2462
2617
  json: { volumeAttachment: { volumeId } },
2463
2618
  retry: 0,
2464
- timeout: DEFAULT_TIMEOUT_MS2
2619
+ timeout: DEFAULT_TIMEOUT_MS3
2465
2620
  });
2466
2621
  if (res.status === 202 || res.headers.get("content-length") === "0") {
2467
2622
  return { id: volumeId, volumeId, serverId, device: "" };
@@ -2485,7 +2640,7 @@ var InstanceClient = class {
2485
2640
  async detachVolume(serverId, volumeId) {
2486
2641
  const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments/${encodeURIComponent(volumeId)}`;
2487
2642
  try {
2488
- await import_ky6.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 });
2643
+ await import_ky7.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 });
2489
2644
  } catch (err) {
2490
2645
  throw toNhnCloudCliError(err);
2491
2646
  }
@@ -2555,8 +2710,8 @@ var listCommand = new import_commander13.Command("list").description("\uC778\uC2
2555
2710
  var import_commander14 = require("commander");
2556
2711
 
2557
2712
  // src/services/blockstorage/client.ts
2558
- var import_ky7 = __toESM(require("ky"));
2559
- var DEFAULT_TIMEOUT_MS3 = 3e4;
2713
+ var import_ky8 = __toESM(require("ky"));
2714
+ var DEFAULT_TIMEOUT_MS4 = 3e4;
2560
2715
  function isVolume(val) {
2561
2716
  if (typeof val !== "object" || val === null) return false;
2562
2717
  const obj = val;
@@ -2592,7 +2747,7 @@ var BlockStorageClient = class {
2592
2747
  if (params?.offset !== void 0) searchParams["offset"] = params.offset;
2593
2748
  if (params?.marker !== void 0) searchParams["marker"] = params.marker;
2594
2749
  try {
2595
- const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), searchParams, retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
2750
+ const raw = await import_ky8.default.get(url, { headers: this.authHeaders(), searchParams, retry: 0, timeout: DEFAULT_TIMEOUT_MS4 }).json();
2596
2751
  if (!isVolumesResponse(raw)) {
2597
2752
  throw new NhnCloudCliError(
2598
2753
  "volume list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volumes \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
@@ -2607,7 +2762,7 @@ var BlockStorageClient = class {
2607
2762
  async get(id) {
2608
2763
  const url = `${this.endpoint}/volumes/${encodeURIComponent(id)}`;
2609
2764
  try {
2610
- const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
2765
+ const raw = await import_ky8.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS4 }).json();
2611
2766
  if (!isVolumeResponse(raw)) {
2612
2767
  throw new NhnCloudCliError(
2613
2768
  "volume get \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volume \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
@@ -2627,11 +2782,11 @@ var BlockStorageClient = class {
2627
2782
  if (params.volume_type !== void 0) volumeBody["volume_type"] = params.volume_type;
2628
2783
  if (params.snapshot_id !== void 0) volumeBody["snapshot_id"] = params.snapshot_id;
2629
2784
  try {
2630
- const raw = await import_ky7.default.post(url, {
2785
+ const raw = await import_ky8.default.post(url, {
2631
2786
  headers: this.authHeaders(),
2632
2787
  json: { volume: volumeBody },
2633
2788
  retry: 0,
2634
- timeout: DEFAULT_TIMEOUT_MS3
2789
+ timeout: DEFAULT_TIMEOUT_MS4
2635
2790
  }).json();
2636
2791
  if (!isVolumeResponse(raw)) {
2637
2792
  throw new NhnCloudCliError(
@@ -2780,8 +2935,8 @@ var createCommand = new import_commander16.Command("create").description("\uBCFC
2780
2935
  var import_commander17 = require("commander");
2781
2936
 
2782
2937
  // src/services/network/client.ts
2783
- var import_ky8 = __toESM(require("ky"));
2784
- var DEFAULT_TIMEOUT_MS4 = 3e4;
2938
+ var import_ky9 = __toESM(require("ky"));
2939
+ var DEFAULT_TIMEOUT_MS5 = 3e4;
2785
2940
  function isVpc(val) {
2786
2941
  if (typeof val !== "object" || val === null) return false;
2787
2942
  const obj = val;
@@ -2834,10 +2989,10 @@ var NetworkClient = class {
2834
2989
  async listVpcs() {
2835
2990
  const url = `${this.networkEndpoint}/vpcs`;
2836
2991
  try {
2837
- const raw = await import_ky8.default.get(url, {
2992
+ const raw = await import_ky9.default.get(url, {
2838
2993
  headers: this.authHeaders(),
2839
2994
  retry: 0,
2840
- timeout: DEFAULT_TIMEOUT_MS4
2995
+ timeout: DEFAULT_TIMEOUT_MS5
2841
2996
  }).json();
2842
2997
  if (!isVpcsResponse(raw)) {
2843
2998
  throw new NhnCloudCliError(
@@ -2856,10 +3011,10 @@ var NetworkClient = class {
2856
3011
  async listSubnets() {
2857
3012
  const url = `${this.networkEndpoint}/vpcsubnets`;
2858
3013
  try {
2859
- const raw = await import_ky8.default.get(url, {
3014
+ const raw = await import_ky9.default.get(url, {
2860
3015
  headers: this.authHeaders(),
2861
3016
  retry: 0,
2862
- timeout: DEFAULT_TIMEOUT_MS4
3017
+ timeout: DEFAULT_TIMEOUT_MS5
2863
3018
  }).json();
2864
3019
  if (!isSubnetsResponse(raw)) {
2865
3020
  throw new NhnCloudCliError(
@@ -2876,7 +3031,7 @@ var NetworkClient = class {
2876
3031
  async listFloatingIps() {
2877
3032
  const url = `${this.networkEndpoint}/floatingips`;
2878
3033
  try {
2879
- const raw = await import_ky8.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS4 }).json();
3034
+ const raw = await import_ky9.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS5 }).json();
2880
3035
  if (!isFloatingIpsResponse(raw)) {
2881
3036
  throw new NhnCloudCliError(
2882
3037
  "floatingip list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 floatingips \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
@@ -2892,11 +3047,11 @@ var NetworkClient = class {
2892
3047
  async createFloatingIp(params) {
2893
3048
  const url = `${this.networkEndpoint}/floatingips`;
2894
3049
  try {
2895
- const raw = await import_ky8.default.post(url, {
3050
+ const raw = await import_ky9.default.post(url, {
2896
3051
  headers: this.authHeaders(),
2897
3052
  json: { floatingip: { floating_network_id: params.floating_network_id } },
2898
3053
  retry: 0,
2899
- timeout: DEFAULT_TIMEOUT_MS4
3054
+ timeout: DEFAULT_TIMEOUT_MS5
2900
3055
  }).json();
2901
3056
  if (!isFloatingIpResponse(raw)) {
2902
3057
  throw new NhnCloudCliError(
@@ -2913,7 +3068,7 @@ var NetworkClient = class {
2913
3068
  async deleteFloatingIp(id) {
2914
3069
  const url = `${this.networkEndpoint}/floatingips/${encodeURIComponent(id)}`;
2915
3070
  try {
2916
- await import_ky8.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS4 });
3071
+ await import_ky9.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS5 });
2917
3072
  } catch (err) {
2918
3073
  throw toNhnCloudCliError(err);
2919
3074
  }
@@ -2927,11 +3082,11 @@ var NetworkClient = class {
2927
3082
  async findExternalNetworkId() {
2928
3083
  const url = `${this.networkEndpoint}/vpcs`;
2929
3084
  try {
2930
- const raw = await import_ky8.default.get(url, {
3085
+ const raw = await import_ky9.default.get(url, {
2931
3086
  headers: this.authHeaders(),
2932
3087
  searchParams: { "router:external": "true" },
2933
3088
  retry: 0,
2934
- timeout: DEFAULT_TIMEOUT_MS4
3089
+ timeout: DEFAULT_TIMEOUT_MS5
2935
3090
  }).json();
2936
3091
  if (typeof raw !== "object" || raw === null) return null;
2937
3092
  const vpcs = raw["vpcs"];
@@ -3779,9 +3934,280 @@ var volumesCommand = new import_commander33.Command("volumes").description("\uC7
3779
3934
  });
3780
3935
  });
3781
3936
 
3937
+ // src/commands/ncr/list.ts
3938
+ var import_commander34 = require("commander");
3939
+
3940
+ // src/services/ncr/harbor-client.ts
3941
+ var import_ky10 = __toESM(require("ky"));
3942
+ var DEFAULT_TIMEOUT_MS6 = 3e4;
3943
+ var PAGE_SIZE = 100;
3944
+ var MAX_PAGES = 1e3;
3945
+ var HarborClient = class {
3946
+ uakId;
3947
+ uakSecret;
3948
+ host;
3949
+ constructor(uakId, uakSecret, host) {
3950
+ this.uakId = uakId;
3951
+ this.uakSecret = uakSecret;
3952
+ this.host = host;
3953
+ }
3954
+ basicAuthHeaders() {
3955
+ const token = Buffer.from(`${this.uakId}:${this.uakSecret}`).toString("base64");
3956
+ return { Authorization: `Basic ${token}` };
3957
+ }
3958
+ /**
3959
+ * Harbor REST 페이지네이션 전수 수집 (ADR-017 — silent truncation 방지).
3960
+ *
3961
+ * Harbor 응답 Link: <...?page=N+1...>; rel="next" 헤더가 없으면 종료.
3962
+ * ky.get() 이 Response 를 반환하므로 .json() 과 .headers.get("link") 를 함께 사용한다
3963
+ * (체이닝하면 헤더를 못 본다 — 기존 NCR client 의 .json<T>() 체이닝 패턴과 다름).
3964
+ */
3965
+ async getAllPages(path) {
3966
+ const acc = [];
3967
+ let page = 1;
3968
+ try {
3969
+ for (; ; ) {
3970
+ const url = `https://${this.host}${path}?page=${page}&page_size=${PAGE_SIZE}`;
3971
+ const res = await import_ky10.default.get(url, {
3972
+ headers: this.basicAuthHeaders(),
3973
+ retry: 0,
3974
+ timeout: DEFAULT_TIMEOUT_MS6
3975
+ });
3976
+ const data = await res.json();
3977
+ if (!Array.isArray(data)) {
3978
+ throw new NhnCloudCliError(
3979
+ "Harbor REST \uC751\uB2F5 \uD615\uC2DD \uC624\uB958: \uBC30\uC5F4\uC774 \uC544\uB2D9\uB2C8\uB2E4.",
3980
+ EXIT_API_ERROR
3981
+ );
3982
+ }
3983
+ acc.push(...data);
3984
+ const link = res.headers.get("link");
3985
+ if (!link || !link.includes('rel="next"')) break;
3986
+ page++;
3987
+ if (page > MAX_PAGES) {
3988
+ throw new NhnCloudCliError(
3989
+ `Harbor pagination \uCD5C\uB300 \uD398\uC774\uC9C0(${MAX_PAGES}) \uCD08\uACFC \u2014 \uBE44\uC815\uC0C1 \uC751\uB2F5\uC73C\uB85C \uC911\uB2E8\uD569\uB2C8\uB2E4.`,
3990
+ EXIT_API_ERROR
3991
+ );
3992
+ }
3993
+ }
3994
+ } catch (err) {
3995
+ if (err instanceof NhnCloudCliError) throw err;
3996
+ throw toNhnCloudCliError(err);
3997
+ }
3998
+ return acc;
3999
+ }
4000
+ /**
4001
+ * 프로젝트(레지스트리)의 repository(이미지) 목록을 반환한다.
4002
+ * GET /api/v2.0/projects/{project}/repositories
4003
+ */
4004
+ async listRepositories(project) {
4005
+ const enc = encodeURIComponent(project);
4006
+ const data = await this.getAllPages(`/api/v2.0/projects/${enc}/repositories`);
4007
+ return data.filter(isRepository);
4008
+ }
4009
+ /**
4010
+ * repository 의 artifact 목록을 반환한다.
4011
+ * GET /api/v2.0/projects/{project}/repositories/{repository}/artifacts
4012
+ * repository 의 '/' 는 %2F 로 인코딩(path-traversal 방지).
4013
+ */
4014
+ async listArtifacts(project, repository) {
4015
+ const encProject = encodeURIComponent(project);
4016
+ const encRepo = encodeURIComponent(repository);
4017
+ const data = await this.getAllPages(
4018
+ `/api/v2.0/projects/${encProject}/repositories/${encRepo}/artifacts`
4019
+ );
4020
+ return data.filter(isArtifact);
4021
+ }
4022
+ };
4023
+
4024
+ // src/commands/ncr/helpers.ts
4025
+ async function createNcrClient(opts) {
4026
+ const profileName = await resolveProfileName(opts.profile);
4027
+ const uak = await getUserAccessKey(profileName);
4028
+ const region = opts.region ?? "kr1";
4029
+ return { client: new NcrClient(uak.id, uak.secret, region), profileName };
4030
+ }
4031
+ async function resolveAppKey(profileName, appKeyOpt) {
4032
+ if (appKeyOpt) return appKeyOpt;
4033
+ let cred;
4034
+ try {
4035
+ cred = await getServiceCredential("ncr", profileName);
4036
+ } catch (err) {
4037
+ if (!(err instanceof NhnCloudCliError) || err.exitCode !== EXIT_CONFIG_ERROR) {
4038
+ throw err;
4039
+ }
4040
+ }
4041
+ if (!cred?.appkey) {
4042
+ throw new NhnCloudCliError(
4043
+ "NCR appKey \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. --app-key \uC635\uC158\uC73C\uB85C \uC9C0\uC815\uD558\uAC70\uB098\nnhncloud configure --ncr-appkey <key> \uB97C \uC2E4\uD589\uD574 \uC124\uC815\uD558\uC138\uC694.",
4044
+ EXIT_CONFIG_ERROR
4045
+ );
4046
+ }
4047
+ return cred.appkey;
4048
+ }
4049
+ async function createHarborClient(opts, registryArg) {
4050
+ const { client: ncrClient, profileName } = await createNcrClient(opts);
4051
+ const appKey = await resolveAppKey(profileName, opts.appKey);
4052
+ const uak = await getUserAccessKey(profileName);
4053
+ const reg = await ncrClient.getRegistry(appKey, registryArg);
4054
+ const host = parseHarborHost(reg.uri);
4055
+ const project = typeof reg.name === "string" ? reg.name : registryArg;
4056
+ return { harbor: new HarborClient(uak.id, uak.secret, host), project };
4057
+ }
4058
+ function parseHarborHost(uri) {
4059
+ if (!uri) {
4060
+ throw new NhnCloudCliError(
4061
+ "\uB808\uC9C0\uC2A4\uD2B8\uB9AC uri \uAC00 \uC5C6\uC5B4 \uC774\uBBF8\uC9C0 host \uB97C \uD574\uC11D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
4062
+ EXIT_API_ERROR
4063
+ );
4064
+ }
4065
+ const noScheme = uri.replace(/^https?:\/\//, "");
4066
+ const host = noScheme.split("/")[0];
4067
+ if (!host) {
4068
+ throw new NhnCloudCliError(
4069
+ "\uB808\uC9C0\uC2A4\uD2B8\uB9AC uri \uD615\uC2DD \uC624\uB958 \u2014 host \uCD94\uCD9C \uC2E4\uD328.",
4070
+ EXIT_API_ERROR
4071
+ );
4072
+ }
4073
+ return host;
4074
+ }
4075
+
4076
+ // src/commands/ncr/list.ts
4077
+ var listCommand5 = new import_commander34.Command("list").description("NCR \uB808\uC9C0\uC2A4\uD2B8\uB9AC \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").option("--region <region>", "NCR region (\uAE30\uBCF8: kr1)", "kr1").option("--app-key <key>", "NCR appKey (profile \uC758 ncr.appkey \uBCF4\uB2E4 \uC6B0\uC120)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
4078
+ const opts = cmd.optsWithGlobals();
4079
+ const { client, profileName } = await createNcrClient(opts);
4080
+ const appKey = await resolveAppKey(profileName, opts.appKey);
4081
+ startSpinner("\uB808\uC9C0\uC2A4\uD2B8\uB9AC \uBAA9\uB85D \uC870\uD68C \uC911...");
4082
+ let registries;
4083
+ try {
4084
+ registries = await client.listRegistries(appKey);
4085
+ } catch (err) {
4086
+ stopSpinner(false);
4087
+ throw err;
4088
+ }
4089
+ stopSpinner(true);
4090
+ output(opts, {
4091
+ headers: ["name", "repo_count", "uri"],
4092
+ rows: registries.map((r) => [
4093
+ r.name,
4094
+ String(r.repo_count ?? ""),
4095
+ r.uri ?? ""
4096
+ ]),
4097
+ raw: registries,
4098
+ ids: registries.map((r) => r.name)
4099
+ });
4100
+ });
4101
+
4102
+ // src/commands/ncr/get.ts
4103
+ var import_commander35 = require("commander");
4104
+ var getCommand3 = new import_commander35.Command("get").description("\uB2E8\uC77C NCR \uB808\uC9C0\uC2A4\uD2B8\uB9AC\uB97C \uC870\uD68C\uD55C\uB2E4").argument("<registry>", "\uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC774\uB984 \uB610\uB294 ID").option("--region <region>", "NCR region (\uAE30\uBCF8: kr1)", "kr1").option("--app-key <key>", "NCR appKey (profile \uC758 ncr.appkey \uBCF4\uB2E4 \uC6B0\uC120)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (registry, _opts, cmd) => {
4105
+ const opts = cmd.optsWithGlobals();
4106
+ if (!registry.trim()) {
4107
+ throw new NhnCloudCliError(
4108
+ "registry \uC778\uC218\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. \uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC774\uB984 \uB610\uB294 ID \uB97C \uC9C0\uC815\uD558\uC138\uC694.",
4109
+ EXIT_PARAM_ERROR
4110
+ );
4111
+ }
4112
+ const { client, profileName } = await createNcrClient(opts);
4113
+ const appKey = await resolveAppKey(profileName, opts.appKey);
4114
+ startSpinner(`\uB808\uC9C0\uC2A4\uD2B8\uB9AC "${registry}" \uC870\uD68C \uC911...`);
4115
+ let reg;
4116
+ try {
4117
+ reg = await client.getRegistry(appKey, registry);
4118
+ } catch (err) {
4119
+ stopSpinner(false);
4120
+ throw err;
4121
+ }
4122
+ stopSpinner(true);
4123
+ output(opts, {
4124
+ headers: ["name", "repo_count", "uri", "private_uri"],
4125
+ rows: [[reg.name, String(reg.repo_count ?? ""), reg.uri ?? "", reg.private_uri ?? ""]],
4126
+ raw: reg,
4127
+ ids: [reg.name]
4128
+ });
4129
+ });
4130
+
4131
+ // src/commands/ncr/images.ts
4132
+ var import_commander36 = require("commander");
4133
+ var imagesCommand2 = new import_commander36.Command("images").description("\uB808\uC9C0\uC2A4\uD2B8\uB9AC\uC758 \uC774\uBBF8\uC9C0(repository) \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<registry>", "\uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC774\uB984").option("--region <region>", "NCR region (\uAE30\uBCF8: kr1)", "kr1").option("--app-key <key>", "NCR appKey (profile \uC758 ncr.appkey \uBCF4\uB2E4 \uC6B0\uC120)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (registry, _opts, cmd) => {
4134
+ const opts = cmd.optsWithGlobals();
4135
+ if (!registry.trim()) {
4136
+ throw new NhnCloudCliError(
4137
+ "registry \uC778\uC218\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. \uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC774\uB984\uC744 \uC9C0\uC815\uD558\uC138\uC694.",
4138
+ EXIT_PARAM_ERROR
4139
+ );
4140
+ }
4141
+ const { harbor, project } = await createHarborClient(opts, registry);
4142
+ startSpinner(`"${registry}" \uC774\uBBF8\uC9C0 \uBAA9\uB85D \uC870\uD68C \uC911...`);
4143
+ let repos;
4144
+ try {
4145
+ repos = await harbor.listRepositories(project);
4146
+ } catch (err) {
4147
+ stopSpinner(false);
4148
+ throw err;
4149
+ }
4150
+ stopSpinner(true);
4151
+ const rows = repos.map((r) => {
4152
+ const short = r.name.startsWith(project + "/") ? r.name.slice(project.length + 1) : r.name;
4153
+ return [short, String(r.artifact_count ?? ""), String(r.pull_count ?? "")];
4154
+ });
4155
+ const ids = repos.map(
4156
+ (r) => r.name.startsWith(project + "/") ? r.name.slice(project.length + 1) : r.name
4157
+ );
4158
+ output(opts, {
4159
+ headers: ["repository", "artifact_count", "pull_count"],
4160
+ rows,
4161
+ raw: repos,
4162
+ ids
4163
+ });
4164
+ });
4165
+
4166
+ // src/commands/ncr/tags.ts
4167
+ var import_commander37 = require("commander");
4168
+ var tagsCommand = new import_commander37.Command("tags").description("\uD2B9\uC815 \uC774\uBBF8\uC9C0\uC758 \uD0DC\uADF8 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<registry>", "\uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC774\uB984").argument("<repository>", "\uC774\uBBF8\uC9C0(repository) \uC774\uB984 (\uC9E7\uC740 \uC774\uB984 \uB610\uB294 {project}/{repo})").option("--region <region>", "NCR region (\uAE30\uBCF8: kr1)", "kr1").option("--app-key <key>", "NCR appKey (profile \uC758 ncr.appkey \uBCF4\uB2E4 \uC6B0\uC120)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (registry, repository, _opts, cmd) => {
4169
+ const opts = cmd.optsWithGlobals();
4170
+ if (!registry.trim()) {
4171
+ throw new NhnCloudCliError(
4172
+ "registry \uC778\uC218\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. \uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC774\uB984\uC744 \uC9C0\uC815\uD558\uC138\uC694.",
4173
+ EXIT_PARAM_ERROR
4174
+ );
4175
+ }
4176
+ if (!repository.trim()) {
4177
+ throw new NhnCloudCliError(
4178
+ "repository \uC778\uC218\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. \uC774\uBBF8\uC9C0 \uC774\uB984\uC744 \uC9C0\uC815\uD558\uC138\uC694.",
4179
+ EXIT_PARAM_ERROR
4180
+ );
4181
+ }
4182
+ const { harbor, project } = await createHarborClient(opts, registry);
4183
+ const repo = repository.startsWith(project + "/") ? repository.slice(project.length + 1) : repository;
4184
+ startSpinner(`"${registry}/${repo}" \uD0DC\uADF8 \uBAA9\uB85D \uC870\uD68C \uC911...`);
4185
+ let tagRows;
4186
+ try {
4187
+ const artifacts = await harbor.listArtifacts(project, repo);
4188
+ tagRows = artifacts.flatMap(
4189
+ (a) => (a.tags ?? []).map((t) => ({
4190
+ tag: t.name,
4191
+ push_time: t.push_time ?? a.push_time,
4192
+ size: String(a.size ?? "")
4193
+ }))
4194
+ );
4195
+ } catch (err) {
4196
+ stopSpinner(false);
4197
+ throw err;
4198
+ }
4199
+ stopSpinner(true);
4200
+ output(opts, {
4201
+ headers: ["tag", "push_time", "size"],
4202
+ rows: tagRows.map((r) => [r.tag, r.push_time ?? "", r.size]),
4203
+ raw: tagRows,
4204
+ ids: tagRows.map((r) => r.tag)
4205
+ });
4206
+ });
4207
+
3782
4208
  // src/index.ts
3783
- var program = new import_commander34.Command();
3784
- program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.4.0").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "\uCD5C\uC18C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9)").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654");
4209
+ var program = new import_commander38.Command();
4210
+ program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.5.0").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "\uCD5C\uC18C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9)").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654");
3785
4211
  program.hook("preAction", () => {
3786
4212
  const opts = program.opts();
3787
4213
  if (!opts.color || process.env["NO_COLOR"]) {
@@ -3792,12 +4218,12 @@ program.hook("preAction", () => {
3792
4218
  }
3793
4219
  });
3794
4220
  program.addCommand(configureCommand);
3795
- var logncrashCommand = new import_commander34.Command("logncrash").description("Log & Crash \uAD00\uB828 \uBA85\uB839");
4221
+ var logncrashCommand = new import_commander38.Command("logncrash").description("Log & Crash \uAD00\uB828 \uBA85\uB839");
3796
4222
  logncrashCommand.addCommand(searchCommand);
3797
4223
  logncrashCommand.addCommand(sendCommand);
3798
4224
  logncrashCommand.addCommand(exportCommand);
3799
4225
  program.addCommand(logncrashCommand);
3800
- var deployCommand = new import_commander34.Command("deploy").description("NHN Cloud Deploy \uAD00\uB828 \uBA85\uB839");
4226
+ var deployCommand = new import_commander38.Command("deploy").description("NHN Cloud Deploy \uAD00\uB828 \uBA85\uB839");
3801
4227
  deployCommand.addCommand(runCommand);
3802
4228
  deployCommand.addCommand(artifactsCommand);
3803
4229
  deployCommand.addCommand(serverGroupsCommand);
@@ -3807,7 +4233,7 @@ deployCommand.addCommand(binariesCommand);
3807
4233
  deployCommand.addCommand(uploadCommand);
3808
4234
  deployCommand.addCommand(downloadCommand);
3809
4235
  program.addCommand(deployCommand);
3810
- var instanceCommand = new import_commander34.Command("instance").description("Compute \uC778\uC2A4\uD134\uC2A4 \uAD00\uB828 \uBA85\uB839");
4236
+ var instanceCommand = new import_commander38.Command("instance").description("Compute \uC778\uC2A4\uD134\uC2A4 \uAD00\uB828 \uBA85\uB839");
3811
4237
  instanceCommand.addCommand(listCommand);
3812
4238
  instanceCommand.addCommand(flavorsCommand);
3813
4239
  instanceCommand.addCommand(availabilityZonesCommand);
@@ -3826,22 +4252,28 @@ instanceCommand.addCommand(keypairCommand);
3826
4252
  instanceCommand.addCommand(volumeCommand);
3827
4253
  instanceCommand.addCommand(volumesCommand);
3828
4254
  program.addCommand(instanceCommand);
3829
- var networkCommand = new import_commander34.Command("network").description("VPC\xB7\uC11C\uBE0C\uB137 \uC870\uD68C");
4255
+ var networkCommand = new import_commander38.Command("network").description("VPC\xB7\uC11C\uBE0C\uB137 \uC870\uD68C");
3830
4256
  networkCommand.addCommand(listCommand3);
3831
4257
  networkCommand.addCommand(subnetCommand);
3832
4258
  program.addCommand(networkCommand);
3833
- var volumeCommand2 = new import_commander34.Command("volume").description("Block Storage \uBCFC\uB968 \uAD00\uB828 \uBA85\uB839");
4259
+ var volumeCommand2 = new import_commander38.Command("volume").description("Block Storage \uBCFC\uB968 \uAD00\uB828 \uBA85\uB839");
3834
4260
  volumeCommand2.addCommand(listCommand2);
3835
4261
  volumeCommand2.addCommand(getCommand);
3836
4262
  volumeCommand2.addCommand(createCommand);
3837
4263
  program.addCommand(volumeCommand2);
3838
- var floatingipCommand = new import_commander34.Command("floatingip").description(
4264
+ var floatingipCommand = new import_commander38.Command("floatingip").description(
3839
4265
  "Floating IP(\uC778\uC2A4\uD134\uC2A4 \uACF5\uC778 IP) \uAD00\uB9AC"
3840
4266
  );
3841
4267
  floatingipCommand.addCommand(listCommand4);
3842
4268
  floatingipCommand.addCommand(createCommand2);
3843
4269
  floatingipCommand.addCommand(deleteCommand);
3844
4270
  program.addCommand(floatingipCommand);
4271
+ var ncrCommand = new import_commander38.Command("ncr").description("NHN Container Registry \uAD00\uB828 \uBA85\uB839");
4272
+ ncrCommand.addCommand(listCommand5);
4273
+ ncrCommand.addCommand(getCommand3);
4274
+ ncrCommand.addCommand(imagesCommand2);
4275
+ ncrCommand.addCommand(tagsCommand);
4276
+ program.addCommand(ncrCommand);
3845
4277
  program.parseAsync().catch((err) => {
3846
4278
  const message = err instanceof Error ? err.message : String(err);
3847
4279
  const exitCode = err instanceof NhnCloudCliError ? err.exitCode : 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bifos/nhncloud-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "CLI tool for NHN Cloud services — AI agent & terminal friendly",
5
5
  "keywords": [
6
6
  "nhncloud",
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: nhncloud-cli
3
- description: NHN Cloud 서비스 CLI. 자격증명 설정(configure), Log & Crash 로그 검색·전송(logncrash search/send), Deploy 배포 실행·바이너리 그룹·바이너리 목록 조회·업로드·다운로드(deploy upload/download), Compute 인스턴스 관리(instance — 목록·발급·전원 제어·타입 변경(resize/resize-confirm/resize-revert)·키페어·이미지·가용성 영역 조회·볼륨 연결(instance volume attach/detach, instance volumes)), VPC·서브넷 조회(network list/subnet list), Block Storage 볼륨 관리(volume list/get/create), Floating IP 관리(floatingip list/create/delete) 등 NHN Cloud PaaS API 를 터미널·AI 에이전트에서 호출한다.
3
+ description: NHN Cloud 서비스 CLI. 자격증명 설정(configure), Log & Crash 로그 검색·전송(logncrash search/send), Deploy 배포 실행·바이너리 그룹·바이너리 목록 조회·업로드·다운로드(deploy upload/download), Compute 인스턴스 관리(instance — 목록·발급·전원 제어·타입 변경(resize/resize-confirm/resize-revert)·키페어·이미지·가용성 영역 조회·볼륨 연결(instance volume attach/detach, instance volumes)), VPC·서브넷 조회(network list/subnet list), Block Storage 볼륨 관리(volume list/get/create), Floating IP 관리(floatingip list/create/delete), NHN Container Registry 레지스트리·이미지·태그 조회(ncr list/get/images/tags) 등 NHN Cloud PaaS API 를 터미널·AI 에이전트에서 호출한다.
4
4
  ---
5
5
 
6
6
  # nhncloud-cli
7
7
 
8
8
  NHN Cloud PaaS 서비스를 AWS CLI 방식으로 호출하는 TypeScript CLI.
9
- `configure`, `logncrash search/send`, `deploy`, `instance` (전원 제어·keypair·볼륨 연결 포함), `network` (VPC·서브넷 조회), `volume` (Block Storage 볼륨 목록·조회·생성), `floatingip` (공인 IP 관리) 명령을 지원한다.
9
+ `configure`, `logncrash search/send`, `deploy`, `instance` (전원 제어·keypair·볼륨 연결 포함), `network` (VPC·서브넷 조회), `volume` (Block Storage 볼륨 목록·조회·생성), `floatingip` (공인 IP 관리), `ncr` (NHN Container Registry 레지스트리·이미지·태그 조회) 명령을 지원한다.
10
10
 
11
11
  ## 설치
12
12
 
@@ -42,6 +42,7 @@ nhncloud configure \
42
42
  | `--uak-secret <secret>` | 개인 UAK Secret |
43
43
  | `--logncrash-appkey <key>` | logncrash appkey |
44
44
  | `--logncrash-secret <secret>` | logncrash secret |
45
+ | `--ncr-appkey <key>` | NCR(Container Registry) appkey |
45
46
  | `--no-verify` | 연결 테스트 생략 |
46
47
 
47
48
  저장 파일 구조 (`~/.nhncloud/credentials.json`, mode 0600):
@@ -101,6 +102,13 @@ logncrash appkey 와 secret 은 콘솔 → Log & Crash Search → 프로젝트
101
102
  | 다른 profile 사용 | `nhncloud logncrash search --query '*' --from 1h --to now --profile staging` |
102
103
  | 로그 대량 추출 (파일로) | `nhncloud logncrash export --query '<lucene>' --from 1h --to now --output logs.jsonl` |
103
104
  | Log & Crash 로그 전송 | `nhncloud logncrash send --body "<메시지>" --level INFO` |
105
+ | NCR 레지스트리 목록 조회 | `nhncloud ncr list --app-key <appkey>` |
106
+ | NCR 레지스트리 목록 (JSON 파싱용) | `nhncloud ncr list --app-key <appkey> --json` |
107
+ | NCR 단일 레지스트리 조회 | `nhncloud ncr get <registry-name> --app-key <appkey>` |
108
+ | NCR 이미지(repository) 목록 조회 | `nhncloud ncr images <registry>` |
109
+ | NCR 이미지 목록 (JSON) | `nhncloud ncr images <registry> --json` |
110
+ | NCR 태그 목록 조회 | `nhncloud ncr tags <registry> <repository>` |
111
+ | NCR 태그 목록 (JSON) | `nhncloud ncr tags <registry> <repository> --json` |
104
112
 
105
113
  ## logncrash search 옵션
106
114
 
@@ -624,3 +632,76 @@ nhncloud floatingip delete <floatingip-id> --yes
624
632
  | 외부 네트워크 미발견 (create --network 미지정) | 3 (PARAM_ERROR) |
625
633
  | 비대화형 delete --yes 누락 | 3 (PARAM_ERROR) |
626
634
  | Floating IP API 오류 | 1 (API_ERROR) |
635
+
636
+ ## ncr — NHN Container Registry 레지스트리·이미지·태그 조회
637
+
638
+ NCR Management API 로 레지스트리(Harbor 프로젝트)를 조회한다.
639
+ 이미지(repository)·태그는 레지스트리 데이터플레인 Harbor REST API 를 직접 호출한다.
640
+ 두 경로 모두 공통 UAK 를 사용하므로 추가 설정은 없다.
641
+ appKey 는 `--app-key` 옵션 또는 `nhncloud configure --ncr-appkey <appkey>` 로 profile 에 저장한 값을 자동 사용한다.
642
+
643
+ ### 의도 → 커맨드 매핑
644
+
645
+ | 의도 | 커맨드 |
646
+ |------|--------|
647
+ | 레지스트리 목록 조회 | `nhncloud ncr list --app-key <appkey>` |
648
+ | 레지스트리 목록 (JSON) | `nhncloud ncr list --app-key <appkey> --json` |
649
+ | 다른 region 조회 (기본 kr1) | `nhncloud ncr list --region kr2 --app-key <appkey>` |
650
+ | 단일 레지스트리 조회 | `nhncloud ncr get <registry> --app-key <appkey>` |
651
+ | appkey 저장 후 생략 | `nhncloud configure --ncr-appkey <appkey>` → `nhncloud ncr list` |
652
+ | 이미지(repository) 목록 조회 | `nhncloud ncr images <registry>` |
653
+ | 이미지 목록 (JSON) | `nhncloud ncr images <registry> --json` |
654
+ | 특정 이미지의 태그 목록 조회 | `nhncloud ncr tags <registry> <repository>` |
655
+ | 태그 목록 (JSON) | `nhncloud ncr tags <registry> <repository> --json` |
656
+
657
+ ### ncr list / get 옵션
658
+
659
+ | 옵션 | 설명 |
660
+ |------|------|
661
+ | `--region <region>` | NCR region (기본: `kr1`). 지원: `kr1`, `kr2`, `kr3` |
662
+ | `--app-key <key>` | NCR appKey (profile 의 `ncr.appkey` 보다 우선) |
663
+ | `--profile <name>` | 사용할 profile 이름 |
664
+
665
+ ### ncr images / ncr tags 옵션
666
+
667
+ | 옵션 | 설명 |
668
+ |------|------|
669
+ | `--region <region>` | NCR region (기본: `kr1`) |
670
+ | `--app-key <key>` | NCR appKey (profile 의 `ncr.appkey` 보다 우선) |
671
+ | `--profile <name>` | 사용할 profile 이름 |
672
+
673
+ ### 체이닝 예시
674
+
675
+ ```bash
676
+ # 레지스트리 목록 (이름만 추출)
677
+ nhncloud ncr list --app-key <appkey> --json | jq -r '.[].name'
678
+
679
+ # 레지스트리 수 확인
680
+ nhncloud ncr list --app-key <appkey> --json | jq length
681
+
682
+ # 단일 레지스트리 상세 조회
683
+ nhncloud ncr get <registry> --app-key <appkey> --json
684
+
685
+ # 이미지 목록 (이름만 추출)
686
+ nhncloud ncr images <registry> --json | jq -r '.[].repository'
687
+
688
+ # 이미지별 artifact 수 확인
689
+ nhncloud ncr images <registry> --json | jq '.[] | {repository, artifact_count}'
690
+
691
+ # 특정 이미지의 태그 목록
692
+ nhncloud ncr tags <registry> <repository> --json | jq -r '.[].tag'
693
+
694
+ # 가장 최근 push 태그 확인
695
+ nhncloud ncr tags <registry> <repository> --json | jq 'sort_by(.push_time) | last | .tag'
696
+ ```
697
+
698
+ ### ncr 에러 코드
699
+
700
+ | 상황 | exit code |
701
+ |------|-----------|
702
+ | UAK 누락·불완전 | 4 (CONFIG_ERROR) |
703
+ | NCR appkey 미설정 (`--app-key` 미지정 + configure 미설정) | 4 (CONFIG_ERROR) |
704
+ | 지원하지 않는 region | 3 (PARAM_ERROR) |
705
+ | registry / repository 인수 공백·빈값 | 3 (PARAM_ERROR) |
706
+ | UAK 인증 실패 (401/403) | 2 (AUTH_ERROR) |
707
+ | NCR API 오류 | 1 (API_ERROR) |