@bifos/nhncloud-cli 0.3.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/dist/index.js CHANGED
@@ -24,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/index.ts
27
- var import_commander11 = require("commander");
28
- var import_chalk4 = __toESM(require("chalk"));
27
+ var import_commander38 = require("commander");
28
+ var import_chalk12 = __toESM(require("chalk"));
29
29
 
30
30
  // src/utils/spinner.ts
31
31
  var import_ora = __toESM(require("ora"));
@@ -317,7 +317,7 @@ function iaasCachePath(profile, region) {
317
317
  function isIaasTokenCache(val) {
318
318
  if (typeof val !== "object" || val === null) return false;
319
319
  const obj = val;
320
- return typeof obj["tokenId"] === "string" && typeof obj["expiresAt"] === "string" && typeof obj["computeEndpoint"] === "string";
320
+ return typeof obj["tokenId"] === "string" && typeof obj["expiresAt"] === "string" && typeof obj["computeEndpoint"] === "string" && typeof obj["imageEndpoint"] === "string" && typeof obj["networkEndpoint"] === "string" && typeof obj["blockStorageEndpoint"] === "string";
321
321
  }
322
322
  async function readIaasToken(profile, region) {
323
323
  const filePath = iaasCachePath(profile, region);
@@ -331,7 +331,10 @@ async function readIaasToken(profile, region) {
331
331
  return {
332
332
  tokenId: parsed.tokenId,
333
333
  expiresAt: parsed.expiresAt,
334
- computeEndpoint: parsed.computeEndpoint
334
+ computeEndpoint: parsed.computeEndpoint,
335
+ imageEndpoint: parsed.imageEndpoint,
336
+ networkEndpoint: parsed.networkEndpoint,
337
+ blockStorageEndpoint: parsed.blockStorageEndpoint
335
338
  };
336
339
  } catch {
337
340
  return null;
@@ -343,7 +346,10 @@ async function writeIaasToken(profile, region, data) {
343
346
  const cache = {
344
347
  tokenId: data.tokenId,
345
348
  expiresAt: data.expiresAt,
346
- computeEndpoint: data.computeEndpoint
349
+ computeEndpoint: data.computeEndpoint,
350
+ imageEndpoint: data.imageEndpoint,
351
+ networkEndpoint: data.networkEndpoint,
352
+ blockStorageEndpoint: data.blockStorageEndpoint
347
353
  };
348
354
  const tmp = filePath + "." + (0, import_node_crypto.randomBytes)(4).toString("hex") + ".tmp";
349
355
  await (0, import_promises2.writeFile)(tmp, JSON.stringify(cache, null, 2), { encoding: "utf-8", mode: 384 });
@@ -448,6 +454,7 @@ var import_ky3 = __toESM(require("ky"));
448
454
  // src/api/endpoints.ts
449
455
  var ENDPOINTS = {
450
456
  logncrash: "https://api-lncs-search.nhncloudservice.com",
457
+ "logncrash-collector": "https://api-logncrash.nhncloudservice.com",
451
458
  deploy: "https://api-deploy.nhncloudservice.com"
452
459
  };
453
460
  function endpointFor(service) {
@@ -469,11 +476,75 @@ var INSTANCE_HOST = {
469
476
  kr3: "kr3-api-instance-infrastructure.nhncloudservice.com",
470
477
  jp1: "jp1-api-instance-infrastructure.nhncloudservice.com"
471
478
  };
479
+ var IMAGE_HOST = {
480
+ kr1: "kr1-api-image-infrastructure.nhncloudservice.com",
481
+ kr2: "kr2-api-image-infrastructure.nhncloudservice.com",
482
+ kr3: "kr3-api-image-infrastructure.nhncloudservice.com",
483
+ jp1: "jp1-api-image-infrastructure.nhncloudservice.com"
484
+ };
485
+ var NETWORK_HOST = {
486
+ kr1: "kr1-api-network-infrastructure.nhncloudservice.com",
487
+ kr2: "kr2-api-network-infrastructure.nhncloudservice.com",
488
+ kr3: "kr3-api-network-infrastructure.nhncloudservice.com",
489
+ jp1: "jp1-api-network-infrastructure.nhncloudservice.com"
490
+ };
491
+ var BLOCKSTORAGE_HOST = {
492
+ kr1: "kr1-api-block-storage-infrastructure.nhncloudservice.com",
493
+ kr2: "kr2-api-block-storage-infrastructure.nhncloudservice.com",
494
+ kr3: "kr3-api-block-storage-infrastructure.nhncloudservice.com",
495
+ jp1: "jp1-api-block-storage-infrastructure.nhncloudservice.com"
496
+ };
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
+ }
472
513
  function instanceHost(region) {
473
514
  const host = INSTANCE_HOST[region];
474
515
  if (!host) {
475
516
  throw new NhnCloudCliError(
476
- `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${Object.keys(INSTANCE_HOST).join(", ")}`,
517
+ `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
518
+ EXIT_PARAM_ERROR
519
+ );
520
+ }
521
+ return host;
522
+ }
523
+ function imageHost(region) {
524
+ const host = IMAGE_HOST[region];
525
+ if (!host) {
526
+ throw new NhnCloudCliError(
527
+ `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
528
+ EXIT_PARAM_ERROR
529
+ );
530
+ }
531
+ return host;
532
+ }
533
+ function networkHost(region) {
534
+ const host = NETWORK_HOST[region];
535
+ if (!host) {
536
+ throw new NhnCloudCliError(
537
+ `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
538
+ EXIT_PARAM_ERROR
539
+ );
540
+ }
541
+ return host;
542
+ }
543
+ function blockStorageHost(region) {
544
+ const host = BLOCKSTORAGE_HOST[region];
545
+ if (!host) {
546
+ throw new NhnCloudCliError(
547
+ `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
477
548
  EXIT_PARAM_ERROR
478
549
  );
479
550
  }
@@ -494,11 +565,20 @@ async function getIaasToken(profile, iaas, forceRefresh = false) {
494
565
  if (!forceRefresh) {
495
566
  const cached = await readIaasToken(profile, iaas.region);
496
567
  if (cached !== null) {
497
- return { tokenId: cached.tokenId, computeEndpoint: cached.computeEndpoint };
568
+ return {
569
+ tokenId: cached.tokenId,
570
+ computeEndpoint: cached.computeEndpoint,
571
+ imageEndpoint: cached.imageEndpoint,
572
+ networkEndpoint: cached.networkEndpoint,
573
+ blockStorageEndpoint: cached.blockStorageEndpoint
574
+ };
498
575
  }
499
576
  }
500
577
  const host = instanceHost(iaas.region);
501
578
  const computeEndpoint = `https://${host}/v2/${encodeURIComponent(iaas.tenantId)}`;
579
+ const imageEndpoint = `https://${imageHost(iaas.region)}/v2`;
580
+ const networkEndpoint = `https://${networkHost(iaas.region)}/v2.0`;
581
+ const blockStorageEndpoint = `https://${blockStorageHost(iaas.region)}/v2/${encodeURIComponent(iaas.tenantId)}`;
502
582
  let raw;
503
583
  try {
504
584
  raw = await import_ky3.default.post(keystoneIdentityUrl(), {
@@ -525,22 +605,25 @@ async function getIaasToken(profile, iaas, forceRefresh = false) {
525
605
  const tokenId = raw.access.token.id;
526
606
  const expiresAt = raw.access.token.expires;
527
607
  if (!forceRefresh) {
528
- await writeIaasToken(profile, iaas.region, { tokenId, expiresAt, computeEndpoint });
608
+ await writeIaasToken(profile, iaas.region, { tokenId, expiresAt, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint });
529
609
  }
530
- return { tokenId, computeEndpoint };
610
+ return { tokenId, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint };
531
611
  }
532
612
 
533
613
  // src/services/logncrash/client.ts
534
614
  var import_ky4 = __toESM(require("ky"));
535
615
 
536
616
  // src/api/envelope.ts
537
- function unwrap(res) {
617
+ function unwrapHeader(res) {
538
618
  if (!res.header.isSuccessful) {
539
619
  throw new NhnCloudCliError(
540
620
  `API \uC624\uB958: ${res.header.resultMessage}`,
541
621
  EXIT_API_ERROR
542
622
  );
543
623
  }
624
+ }
625
+ function unwrap(res) {
626
+ unwrapHeader(res);
544
627
  if (res.body === void 0) {
545
628
  throw new NhnCloudCliError("API \uC751\uB2F5\uC5D0 body \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", EXIT_API_ERROR);
546
629
  }
@@ -550,12 +633,19 @@ function unwrap(res) {
550
633
  // src/services/logncrash/client.ts
551
634
  var LogncrashClient = class {
552
635
  appkey;
636
+ /** 검색(X-LNCS-SECRET)에만 필요. collector send 는 secret 을 쓰지 않으므로 옵셔널 (ADR-014). */
553
637
  secret;
554
638
  constructor(appkey, secret) {
555
639
  this.appkey = appkey;
556
640
  this.secret = secret;
557
641
  }
558
642
  async search(params) {
643
+ if (!this.secret) {
644
+ throw new NhnCloudCliError(
645
+ "logncrash search \uC5D0\uB294 secret \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. configure \uB85C logncrash secret \uC744 \uC124\uC815\uD558\uC138\uC694.",
646
+ EXIT_CONFIG_ERROR
647
+ );
648
+ }
559
649
  const endpoint = endpointFor("logncrash");
560
650
  const url = `${endpoint}/api/v2/search/${encodeURIComponent(this.appkey)}`;
561
651
  try {
@@ -577,6 +667,182 @@ var LogncrashClient = class {
577
667
  throw toNhnCloudCliError(err);
578
668
  }
579
669
  }
670
+ /**
671
+ * scroll 검색을 시작한다. POST /api/v2/search/scroll/{appkey}.
672
+ * body 는 search 와 동일(query/from/to/pageSize). 응답 scrollKey 로 scrollNext 를 이어 호출한다.
673
+ */
674
+ async scrollStart(params) {
675
+ if (!this.secret) {
676
+ throw new NhnCloudCliError(
677
+ "logncrash scroll \uC5D0\uB294 secret \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. configure \uB85C logncrash secret \uC744 \uC124\uC815\uD558\uC138\uC694.",
678
+ EXIT_CONFIG_ERROR
679
+ );
680
+ }
681
+ const endpoint = endpointFor("logncrash");
682
+ const url = `${endpoint}/api/v2/search/scroll/${encodeURIComponent(this.appkey)}`;
683
+ try {
684
+ const res = await import_ky4.default.post(url, {
685
+ headers: {
686
+ "X-LNCS-SECRET": this.secret,
687
+ "Content-Type": "application/json"
688
+ },
689
+ json: {
690
+ query: params.query,
691
+ from: params.from,
692
+ to: params.to,
693
+ pageSize: params.pageSize ?? 100
694
+ }
695
+ }).json();
696
+ return unwrap(res);
697
+ } catch (err) {
698
+ throw toNhnCloudCliError(err);
699
+ }
700
+ }
701
+ /**
702
+ * scroll 다음 페이지를 가져온다. POST /api/v2/search/scroll/{appkey}/{scrollKey}.
703
+ * body 는 보내지 않는다(scrollKey 가 좌표). scrollKey 만료 시 API 가 실패 봉투를 주며,
704
+ * unwrap 이 EXIT_API_ERROR 로 변환한다 — 호출부에서 만료 안내 메시지로 감싼다.
705
+ */
706
+ async scrollNext(scrollKey) {
707
+ if (!this.secret) {
708
+ throw new NhnCloudCliError(
709
+ "logncrash scroll \uC5D0\uB294 secret \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. configure \uB85C logncrash secret \uC744 \uC124\uC815\uD558\uC138\uC694.",
710
+ EXIT_CONFIG_ERROR
711
+ );
712
+ }
713
+ const endpoint = endpointFor("logncrash");
714
+ const url = `${endpoint}/api/v2/search/scroll/${encodeURIComponent(this.appkey)}/${encodeURIComponent(scrollKey)}`;
715
+ try {
716
+ const res = await import_ky4.default.post(url, {
717
+ headers: {
718
+ "X-LNCS-SECRET": this.secret,
719
+ "Content-Type": "application/json"
720
+ }
721
+ }).json();
722
+ return unwrap(res);
723
+ } catch (err) {
724
+ throw toNhnCloudCliError(err);
725
+ }
726
+ }
727
+ /**
728
+ * 로그 한 건을 Log & Crash collector 로 전송한다 (ADR-014).
729
+ * - host: api-logncrash (검색의 api-lncs-search 와 별도)
730
+ * - 인증: 헤더 없음 — body 의 projectName=appkey 로 식별 (secret 불요)
731
+ * - logVersion 은 "v2" 고정. logSource/logType 미지정 시 collector 기본값("http"/"log") 적용.
732
+ */
733
+ async send(params) {
734
+ const endpoint = endpointFor("logncrash-collector");
735
+ const url = `${endpoint}/v2/log`;
736
+ const payload = {
737
+ projectName: this.appkey,
738
+ projectVersion: params.projectVersion,
739
+ logVersion: "v2",
740
+ body: params.body
741
+ };
742
+ if (params.logLevel !== void 0) payload["logLevel"] = params.logLevel;
743
+ if (params.logSource !== void 0) payload["logSource"] = params.logSource;
744
+ if (params.logType !== void 0) payload["logType"] = params.logType;
745
+ if (params.host !== void 0) payload["host"] = params.host;
746
+ try {
747
+ const res = await import_ky4.default.post(url, {
748
+ headers: { "Content-Type": "application/json" },
749
+ json: payload
750
+ }).json();
751
+ unwrapHeader(res);
752
+ } catch (err) {
753
+ throw toNhnCloudCliError(err);
754
+ }
755
+ }
756
+ };
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
+ }
580
846
  };
581
847
 
582
848
  // src/commands/configure-verify.ts
@@ -602,6 +868,19 @@ async function verifyIaas(iaas) {
602
868
  throw err;
603
869
  }
604
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
+ }
605
884
  async function verifyLogncrash(cred) {
606
885
  if (!cred.appkey || !cred.secret) return false;
607
886
  const client = new LogncrashClient(cred.appkey, cred.secret);
@@ -625,7 +904,7 @@ async function verifyLogncrash(cred) {
625
904
  }
626
905
 
627
906
  // src/commands/configure.ts
628
- async function saveAndVerify(profileName, uak, logncrash, iaas, doVerify) {
907
+ async function saveAndVerify(profileName, uak, logncrash, iaas, ncr, doVerify) {
629
908
  if (doVerify) {
630
909
  if (uak) {
631
910
  const ok = await verifyUserAccessKey(uak);
@@ -660,6 +939,26 @@ async function saveAndVerify(profileName, uak, logncrash, iaas, doVerify) {
660
939
  );
661
940
  }
662
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
+ }
663
962
  }
664
963
  if (uak) {
665
964
  await setUserAccessKey(profileName, uak);
@@ -670,6 +969,9 @@ async function saveAndVerify(profileName, uak, logncrash, iaas, doVerify) {
670
969
  if (iaas) {
671
970
  await setIaasCredential(profileName, iaas);
672
971
  }
972
+ if (ncr) {
973
+ await setServiceCredential(profileName, "ncr", ncr);
974
+ }
673
975
  process.stderr.write(import_chalk.default.green(`
674
976
  \u2713 profile "${profileName}" \uC124\uC815\uC774 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
675
977
  `));
@@ -729,12 +1031,25 @@ async function runInteractive(opts) {
729
1031
  });
730
1032
  iaas = { tenantId, username: iaasUsername, password: iaasPassword, region };
731
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
+ }
732
1047
  if (opts.verify) {
733
1048
  process.stderr.write(import_chalk.default.gray("\n\u2014 \uC5F0\uACB0 \uD14C\uC2A4\uD2B8 \uC911\u2026 \u2014\n"));
734
1049
  }
735
1050
  if (opts.verify) {
736
1051
  try {
737
- await saveAndVerify(profileName, uak, logncrash, iaas, true);
1052
+ await saveAndVerify(profileName, uak, logncrash, iaas, ncr, true);
738
1053
  } catch (err) {
739
1054
  if (err instanceof NhnCloudCliError && err.exitCode === EXIT_AUTH_ERROR) {
740
1055
  process.stderr.write(import_chalk.default.red(` \u2717 ${err.message}
@@ -747,13 +1062,13 @@ async function runInteractive(opts) {
747
1062
  process.stderr.write(import_chalk.default.yellow("\uC800\uC7A5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
748
1063
  return;
749
1064
  }
750
- await saveAndVerify(profileName, uak, logncrash, iaas, false);
1065
+ await saveAndVerify(profileName, uak, logncrash, iaas, ncr, false);
751
1066
  } else {
752
1067
  throw err;
753
1068
  }
754
1069
  }
755
1070
  } else {
756
- await saveAndVerify(profileName, uak, logncrash, iaas, false);
1071
+ await saveAndVerify(profileName, uak, logncrash, iaas, ncr, false);
757
1072
  }
758
1073
  }
759
1074
  async function runNonInteractive(opts) {
@@ -769,22 +1084,23 @@ async function runNonInteractive(opts) {
769
1084
  password: iaasPassword,
770
1085
  region: opts.iaasRegion ?? "kr1"
771
1086
  } : void 0;
772
- if (!uak && !logncrash && !iaas) {
1087
+ const ncr = opts.ncrAppkey?.trim() ? { appkey: opts.ncrAppkey.trim() } : void 0;
1088
+ if (!uak && !logncrash && !iaas && !ncr) {
773
1089
  throw new NhnCloudCliError(
774
- "\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.",
775
1091
  EXIT_PARAM_ERROR
776
1092
  );
777
1093
  }
778
1094
  if (opts.verify) {
779
1095
  process.stderr.write(import_chalk.default.gray("\uC5F0\uACB0 \uD14C\uC2A4\uD2B8 \uC911\u2026\n"));
780
1096
  }
781
- await saveAndVerify(profileName, uak, logncrash, iaas, opts.verify);
1097
+ await saveAndVerify(profileName, uak, logncrash, iaas, ncr, opts.verify);
782
1098
  }
783
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(
784
1100
  "--iaas-password <pass>",
785
1101
  "iaas API \uBE44\uBC00\uBC88\uD638 (\uBE44\uB300\uD654\uD615, \uB178\uCD9C \uBC29\uC9C0\uB85C env NHNCLOUD_IAAS_PASSWORD \uAD8C\uC7A5)"
786
- ).option("--iaas-region <region>", "iaas region (\uAE30\uBCF8: kr1)", "kr1").option("--no-verify", "\uC5F0\uACB0 \uD14C\uC2A4\uD2B8 \uC0DD\uB7B5").action(async (opts) => {
787
- 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;
788
1104
  try {
789
1105
  if (hasFlag) {
790
1106
  await runNonInteractive(opts);
@@ -985,106 +1301,469 @@ credentials.json \uC5D0 "secret": "<secretkey>" \uB97C \uCD94\uAC00\uD558\uC138\
985
1301
  });
986
1302
  });
987
1303
 
988
- // src/commands/deploy/run.ts
1304
+ // src/commands/logncrash/send.ts
989
1305
  var import_commander3 = require("commander");
990
-
991
- // src/services/deploy/client.ts
992
- var import_ky5 = __toESM(require("ky"));
993
- var SYNC_TIMEOUT_MS = 6e5;
994
- var DEFAULT_TIMEOUT_MS = 3e4;
995
- var DeployClient = class {
996
- accessToken;
997
- baseUrl;
998
- constructor(accessToken) {
999
- this.accessToken = accessToken;
1000
- this.baseUrl = endpointFor("deploy");
1001
- }
1002
- authHeaders() {
1003
- return {
1004
- "X-NHN-AUTHORIZATION": `Bearer ${this.accessToken}`
1005
- };
1006
- }
1007
- /**
1008
- * 배포를 실행한다.
1009
- * - targetHosts 가 비어있으면 payload 에서 targetServerHostnames 를 제외한다 (서버그룹 전체 배포).
1010
- * - async=false(기본) 일 때 서버가 완료까지 응답을 보류하므로 ky timeout 을 600s 로 설정한다.
1011
- */
1012
- async run(params) {
1013
- const url = `${this.baseUrl}/api/v2.1/projects/${params.appKey}/artifacts/${params.artifactId}/server-group/${params.serverGroupId}/deploy`;
1014
- const isAsync = params.async ?? false;
1015
- const payload = {
1016
- concurrentNum: params.concurrentNum ?? 1,
1017
- nextWhenFail: params.nextWhenFail ?? false,
1018
- scenarioIds: params.scenarioIds,
1019
- deployNote: params.deployNote ?? `CLI deploy ${(/* @__PURE__ */ new Date()).toISOString()}`,
1020
- async: isAsync
1021
- };
1022
- if (params.targetHosts) {
1023
- payload["targetServerHostnames"] = params.targetHosts;
1024
- }
1306
+ var import_node_fs = require("fs");
1307
+ var MAX_LOG_BYTES = 8 * 1024 * 1024;
1308
+ var VALID_LEVELS = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"];
1309
+ function resolveBody(opts) {
1310
+ if (opts.body !== void 0) return opts.body;
1311
+ if (opts.file !== void 0) {
1312
+ let stat;
1025
1313
  try {
1026
- const res = await import_ky5.default.post(url, {
1027
- headers: {
1028
- ...this.authHeaders(),
1029
- "Content-Type": "application/json"
1030
- },
1031
- json: payload,
1032
- retry: 0,
1033
- timeout: isAsync ? DEFAULT_TIMEOUT_MS : SYNC_TIMEOUT_MS
1034
- }).json();
1035
- return unwrap(res);
1036
- } catch (err) {
1037
- throw toNhnCloudCliError(err);
1314
+ stat = (0, import_node_fs.statSync)(opts.file);
1315
+ } catch (e) {
1316
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
1317
+ throw new NhnCloudCliError(`\uB85C\uADF8 \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.file} (${reason})`, EXIT_PARAM_ERROR);
1038
1318
  }
1039
- }
1040
- /**
1041
- * 아티팩트 목록을 조회한다.
1042
- */
1043
- async artifacts(appKey) {
1044
- const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts`;
1045
- try {
1046
- const res = await import_ky5.default.get(url, {
1047
- headers: this.authHeaders(),
1048
- retry: 0,
1049
- timeout: DEFAULT_TIMEOUT_MS
1050
- }).json();
1051
- return unwrap(res);
1052
- } catch (err) {
1053
- throw toNhnCloudCliError(err);
1319
+ if (!stat.isFile()) {
1320
+ throw new NhnCloudCliError(`\uB85C\uADF8 \uD30C\uC77C\uC774 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${opts.file}`, EXIT_PARAM_ERROR);
1054
1321
  }
1055
- }
1056
- /**
1057
- * 서버그룹 목록을 조회한다.
1058
- */
1059
- async serverGroups(appKey, artifactId) {
1060
- const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts/${artifactId}/server-groups`;
1061
- try {
1062
- const res = await import_ky5.default.get(url, {
1063
- headers: this.authHeaders(),
1064
- retry: 0,
1065
- timeout: DEFAULT_TIMEOUT_MS
1066
- }).json();
1067
- return unwrap(res);
1068
- } catch (err) {
1069
- throw toNhnCloudCliError(err);
1322
+ if (stat.size > MAX_LOG_BYTES) {
1323
+ throw new NhnCloudCliError(
1324
+ `\uB85C\uADF8 \uD30C\uC77C\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${stat.size} \uBC14\uC774\uD2B8 (\uD55C\uB3C4 ${MAX_LOG_BYTES} \uBC14\uC774\uD2B8).`,
1325
+ EXIT_PARAM_ERROR
1326
+ );
1070
1327
  }
1328
+ return (0, import_node_fs.readFileSync)(opts.file, "utf-8");
1071
1329
  }
1072
- /**
1073
- * 배포 이력을 조회한다.
1074
- */
1075
- async histories(appKey, artifactId) {
1076
- const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts/${artifactId}/deploy-histories`;
1077
- try {
1078
- const res = await import_ky5.default.get(url, {
1079
- headers: this.authHeaders(),
1080
- retry: 0,
1081
- timeout: DEFAULT_TIMEOUT_MS
1082
- }).json();
1330
+ if (!process.stdin.isTTY) {
1331
+ return (0, import_node_fs.readFileSync)(0, "utf-8");
1332
+ }
1333
+ throw new NhnCloudCliError(
1334
+ "\uB85C\uADF8 \uBCF8\uBB38\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. --body <text> \uB610\uB294 --file <path> \uB610\uB294 \uD45C\uC900\uC785\uB825(\uD30C\uC774\uD504)\uC73C\uB85C \uC804\uB2EC\uD558\uC138\uC694.",
1335
+ EXIT_PARAM_ERROR
1336
+ );
1337
+ }
1338
+ var sendCommand = new import_commander3.Command("send").description("\uB85C\uADF8 \uD55C \uAC74\uC744 Log & Crash \uB85C \uC804\uC1A1\uD55C\uB2E4 (--body / --file / stdin)").option("--body <text>", "\uB85C\uADF8 \uBA54\uC2DC\uC9C0 \uBCF8\uBB38 (\uBBF8\uC9C0\uC815 \uC2DC --file \uB610\uB294 stdin)").option("--file <path>", "\uB85C\uADF8 \uBCF8\uBB38\uC744 \uC77D\uC744 \uD30C\uC77C \uACBD\uB85C").option("--level <level>", "\uB85C\uADF8 \uB808\uBCA8 (DEBUG/INFO/WARN/ERROR/FATAL)").option("--app-version <ver>", "projectVersion (\uBBF8\uC9C0\uC815 \uC2DC \uAE30\uBCF8 '1.0.0')").option("--source <source>", "logSource (collector \uAE30\uBCF8 'http')").option("--type <type>", "logType (collector \uAE30\uBCF8 'log')").option("--host <host>", "\uB85C\uADF8\uB97C \uBCF4\uB0B8 host \uC2DD\uBCC4").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
1339
+ const opts = cmd.optsWithGlobals();
1340
+ const body = resolveBody(opts);
1341
+ const bytes = Buffer.byteLength(body, "utf-8");
1342
+ if (bytes === 0) {
1343
+ throw new NhnCloudCliError("\uB85C\uADF8 \uBCF8\uBB38\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
1344
+ }
1345
+ if (bytes > MAX_LOG_BYTES) {
1346
+ throw new NhnCloudCliError(
1347
+ `\uB85C\uADF8 \uBCF8\uBB38\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${bytes} \uBC14\uC774\uD2B8 (\uD55C\uB3C4 ${MAX_LOG_BYTES} \uBC14\uC774\uD2B8).`,
1348
+ EXIT_PARAM_ERROR
1349
+ );
1350
+ }
1351
+ let logLevel;
1352
+ if (opts.level !== void 0) {
1353
+ const upper = opts.level.toUpperCase();
1354
+ if (!VALID_LEVELS.includes(upper)) {
1355
+ throw new NhnCloudCliError(
1356
+ `--level \uC740 ${VALID_LEVELS.join("/")} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${opts.level}).`,
1357
+ EXIT_PARAM_ERROR
1358
+ );
1359
+ }
1360
+ logLevel = upper;
1361
+ }
1362
+ const profileName = await resolveProfileName(opts.profile);
1363
+ const cred = await getServiceCredential("logncrash", profileName);
1364
+ if (!cred.appkey) {
1365
+ throw new NhnCloudCliError(
1366
+ `profile "${profileName}" \uC758 logncrash \uC790\uACA9\uC99D\uBA85\uC5D0 appkey \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
1367
+ credentials.json \uC5D0 "appkey": "<appkey>" \uB97C \uCD94\uAC00\uD558\uC138\uC694.`,
1368
+ EXIT_CONFIG_ERROR
1369
+ );
1370
+ }
1371
+ const client = new LogncrashClient(cred.appkey);
1372
+ startSpinner("\uB85C\uADF8 \uC804\uC1A1 \uC911...");
1373
+ try {
1374
+ await client.send({
1375
+ body,
1376
+ projectVersion: opts.appVersion ?? "1.0.0",
1377
+ logLevel,
1378
+ logSource: opts.source,
1379
+ logType: opts.type,
1380
+ host: opts.host
1381
+ });
1382
+ } catch (err) {
1383
+ stopSpinner(false);
1384
+ throw err;
1385
+ }
1386
+ stopSpinner(true, "\uB85C\uADF8\uB97C \uC804\uC1A1\uD588\uC2B5\uB2C8\uB2E4.");
1387
+ if (opts.json) {
1388
+ process.stdout.write(JSON.stringify({ ok: true, bytes }) + "\n");
1389
+ }
1390
+ });
1391
+
1392
+ // src/commands/logncrash/export.ts
1393
+ var import_commander4 = require("commander");
1394
+ var import_promises3 = require("fs/promises");
1395
+ var import_node_fs2 = require("fs");
1396
+ var import_node_crypto2 = require("crypto");
1397
+ var MAX_TOTAL = 1e5;
1398
+ function assertWritable(path, force) {
1399
+ if (force) return;
1400
+ try {
1401
+ (0, import_node_fs2.statSync)(path);
1402
+ } catch (e) {
1403
+ if (e.code === "ENOENT") return;
1404
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
1405
+ throw new NhnCloudCliError(`--output \uACBD\uB85C\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${path} (${reason})`, EXIT_PARAM_ERROR);
1406
+ }
1407
+ throw new NhnCloudCliError(
1408
+ `--output \uB300\uC0C1\uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: ${path}. \uB36E\uC5B4\uC4F0\uB824\uBA74 --force \uB97C \uC4F0\uC138\uC694.`,
1409
+ EXIT_PARAM_ERROR
1410
+ );
1411
+ }
1412
+ var exportCommand = new import_commander4.Command("export").description("Log & Crash \uB85C\uADF8\uB97C scroll \uB85C \uC804\uCCB4 \uCD94\uCD9C\uD574 \uD30C\uC77C\uB85C \uC800\uC7A5 (\uB300\uB7C9 \uCD94\uCD9C)").option("--query <lucene>", "Lucene \uC9C8\uC758 \uBB38\uC790\uC5F4 (\uD544\uC218)").option("--from <time>", "\uAC80\uC0C9 \uC2DC\uC791: ISO8601 \uB610\uB294 \uC0C1\uB300\uC2DC\uAC04 (1h/30m/2d/now) (\uD544\uC218)").option("--to <time>", "\uAC80\uC0C9 \uB05D: ISO8601 \uB610\uB294 \uC0C1\uB300\uC2DC\uAC04 (\uD544\uC218)").option("--output <file>", "\uCD9C\uB825 \uD30C\uC77C \uACBD\uB85C (\uD544\uC218)").option("--format <fmt>", "\uCD9C\uB825 \uD615\uC2DD: jsonl(\uAE30\uBCF8, \uD55C \uC904\uB2F9 \uD55C \uB85C\uADF8) \uB610\uB294 json(\uBC30\uC5F4)", "jsonl").option("--size <n>", "scroll \uD398\uC774\uC9C0 \uD06C\uAE30 (docs \uBC94\uC704 10~100, \uAE30\uBCF8 100)", "100").option("--force", "\uCD9C\uB825 \uD30C\uC77C\uC774 \uC788\uC73C\uBA74 \uB36E\uC5B4\uC4F4\uB2E4").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
1413
+ const opts = cmd.optsWithGlobals();
1414
+ if (!opts.query) {
1415
+ throw new NhnCloudCliError(`--query \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --query 'logType:"NORMAL"'`, EXIT_PARAM_ERROR);
1416
+ }
1417
+ if (!opts.from) {
1418
+ throw new NhnCloudCliError("--from \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --from 1h", EXIT_PARAM_ERROR);
1419
+ }
1420
+ if (!opts.to) {
1421
+ throw new NhnCloudCliError("--to \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --to now", EXIT_PARAM_ERROR);
1422
+ }
1423
+ if (!opts.output) {
1424
+ throw new NhnCloudCliError("--output \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --output logs.jsonl", EXIT_PARAM_ERROR);
1425
+ }
1426
+ const format = opts.format ?? "jsonl";
1427
+ if (format !== "jsonl" && format !== "json") {
1428
+ throw new NhnCloudCliError("--format \uC740 jsonl \uB610\uB294 json \uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
1429
+ }
1430
+ const sizeRaw = opts.size ?? "100";
1431
+ if (!/^[1-9]\d*$/.test(sizeRaw)) {
1432
+ throw new NhnCloudCliError("--size \uB294 \uC591\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (docs \uBC94\uC704 10~100).", EXIT_PARAM_ERROR);
1433
+ }
1434
+ const size = parseInt(sizeRaw, 10);
1435
+ if (size < 10 || size > 100) {
1436
+ throw new NhnCloudCliError("--size \uB294 10~100 \uC0AC\uC774\uC5EC\uC57C \uD569\uB2C8\uB2E4 (Log & Crash scroll pageSize \uD55C\uB3C4).", EXIT_PARAM_ERROR);
1437
+ }
1438
+ const fromIso = resolveTime(opts.from);
1439
+ const toIso = resolveTime(opts.to);
1440
+ assertSearchRange(fromIso, toIso);
1441
+ assertWritable(opts.output, opts.force ?? false);
1442
+ const profileName = await resolveProfileName(opts.profile);
1443
+ const cred = await getServiceCredential("logncrash", profileName);
1444
+ if (!cred.appkey) {
1445
+ throw new NhnCloudCliError(
1446
+ `profile "${profileName}" \uC758 logncrash \uC790\uACA9\uC99D\uBA85\uC5D0 appkey \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
1447
+ credentials.json \uC5D0 "appkey": "<appkey>" \uB97C \uCD94\uAC00\uD558\uC138\uC694.`,
1448
+ EXIT_CONFIG_ERROR
1449
+ );
1450
+ }
1451
+ if (!cred.secret) {
1452
+ throw new NhnCloudCliError(
1453
+ `profile "${profileName}" \uC758 logncrash \uC790\uACA9\uC99D\uBA85\uC5D0 secret \uC774 \uC5C6\uC2B5\uB2C8\uB2E4.
1454
+ credentials.json \uC5D0 "secret": "<secret>" \uB97C \uCD94\uAC00\uD558\uC138\uC694.`,
1455
+ EXIT_CONFIG_ERROR
1456
+ );
1457
+ }
1458
+ const client = new LogncrashClient(cred.appkey, cred.secret);
1459
+ const tmp = opts.output + "." + (0, import_node_crypto2.randomBytes)(4).toString("hex") + ".tmp";
1460
+ const stream = (0, import_node_fs2.createWriteStream)(tmp, { encoding: "utf-8" });
1461
+ const spinner = startSpinner("\uB85C\uADF8 \uCD94\uCD9C \uC911...");
1462
+ let count = 0;
1463
+ let total = 0;
1464
+ let first = true;
1465
+ const writePage = (data) => {
1466
+ for (const log of data) {
1467
+ if (count >= MAX_TOTAL) break;
1468
+ const json = JSON.stringify(log);
1469
+ stream.write(format === "json" ? first ? json : "," + json : json + "\n");
1470
+ first = false;
1471
+ count++;
1472
+ }
1473
+ };
1474
+ try {
1475
+ if (format === "json") stream.write("[");
1476
+ let res = await client.scrollStart({ query: opts.query, from: fromIso, to: toIso, pageSize: size });
1477
+ total = res.totalItems;
1478
+ writePage(res.data);
1479
+ spinner.text = `\uB85C\uADF8 \uCD94\uCD9C \uC911... ${count}/${total}`;
1480
+ while (res.data.length > 0 && res.scrollKey && count < Math.min(total, MAX_TOTAL)) {
1481
+ res = await scrollNextOrExpire(client, res.scrollKey);
1482
+ writePage(res.data);
1483
+ spinner.text = `\uB85C\uADF8 \uCD94\uCD9C \uC911... ${count}/${total}`;
1484
+ }
1485
+ if (format === "json") stream.write("]\n");
1486
+ await new Promise((resolve, reject) => {
1487
+ stream.once("error", reject);
1488
+ stream.end(resolve);
1489
+ });
1490
+ } catch (err) {
1491
+ stopSpinner(false);
1492
+ stream.destroy();
1493
+ await (0, import_promises3.rm)(tmp, { force: true }).catch(() => {
1494
+ });
1495
+ throw err;
1496
+ }
1497
+ stopSpinner(true, `${count}\uAC74 \uCD94\uCD9C \uC644\uB8CC`);
1498
+ try {
1499
+ await (0, import_promises3.rename)(tmp, opts.output);
1500
+ } catch (err) {
1501
+ await (0, import_promises3.rm)(tmp, { force: true }).catch(() => {
1502
+ });
1503
+ const reason = err.code ?? (err instanceof Error ? err.message : String(err));
1504
+ throw new NhnCloudCliError(`\uCD9C\uB825 \uD30C\uC77C\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.output} (${reason})`, EXIT_PARAM_ERROR);
1505
+ }
1506
+ if (count >= MAX_TOTAL && total > MAX_TOTAL) {
1507
+ process.stderr.write(
1508
+ `\uACBD\uACE0: \uC804\uCCB4 ${total}\uAC74 \uC911 \uC0C1\uD55C ${MAX_TOTAL}\uAC74\uAE4C\uC9C0\uB9CC \uCD94\uCD9C\uD588\uC2B5\uB2C8\uB2E4. \uAC80\uC0C9 \uBC94\uC704\uB97C \uC881\uD600 \uB098\uB220 \uCD94\uCD9C\uD558\uC138\uC694.
1509
+ `
1510
+ );
1511
+ }
1512
+ process.stderr.write(`${opts.output} \uC5D0 ${count}\uAC74 \uC800\uC7A5
1513
+ `);
1514
+ });
1515
+ async function scrollNextOrExpire(client, scrollKey) {
1516
+ try {
1517
+ return await client.scrollNext(scrollKey);
1518
+ } catch (err) {
1519
+ if (err instanceof NhnCloudCliError && err.exitCode === EXIT_API_ERROR) {
1520
+ throw new NhnCloudCliError(
1521
+ `scroll \uB2E4\uC74C \uD398\uC774\uC9C0 \uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (\uC6D0\uC778: ${err.message}). scrollKey \uB9CC\uB8CC(\uC720\uD6A8 1\uBD84)\uC77C \uC218 \uC788\uC73C\uB2C8, \uB9CC\uB8CC\uB77C\uBA74 \uAC80\uC0C9 \uBC94\uC704\uB97C \uC881\uD788\uAC70\uB098 --size \uB97C \uD0A4\uC6CC \uD398\uC774\uC9C0 \uC218\uB97C \uC904\uC778 \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`,
1522
+ EXIT_API_ERROR
1523
+ );
1524
+ }
1525
+ throw err;
1526
+ }
1527
+ }
1528
+
1529
+ // src/commands/deploy/run.ts
1530
+ var import_commander5 = require("commander");
1531
+
1532
+ // src/services/deploy/client.ts
1533
+ var import_ky6 = __toESM(require("ky"));
1534
+ function isBinaryGroup(val) {
1535
+ if (typeof val !== "object" || val === null) return false;
1536
+ const obj = val;
1537
+ const keyType = typeof obj["key"];
1538
+ return (keyType === "number" || keyType === "string") && typeof obj["name"] === "string";
1539
+ }
1540
+ function isBinary(val) {
1541
+ if (typeof val !== "object" || val === null) return false;
1542
+ const obj = val;
1543
+ const binaryKeyType = typeof obj["binaryKey"];
1544
+ const binarySizeType = typeof obj["binarySize"];
1545
+ return (binaryKeyType === "number" || binaryKeyType === "string") && (binarySizeType === "number" || binarySizeType === "string");
1546
+ }
1547
+ var SYNC_TIMEOUT_MS = 6e5;
1548
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
1549
+ var DeployClient = class {
1550
+ accessToken;
1551
+ baseUrl;
1552
+ constructor(accessToken) {
1553
+ this.accessToken = accessToken;
1554
+ this.baseUrl = endpointFor("deploy");
1555
+ }
1556
+ authHeaders() {
1557
+ return {
1558
+ "X-NHN-AUTHORIZATION": `Bearer ${this.accessToken}`
1559
+ };
1560
+ }
1561
+ /**
1562
+ * 배포를 실행한다.
1563
+ * - targetHosts 가 비어있으면 payload 에서 targetServerHostnames 를 제외한다 (서버그룹 전체 배포).
1564
+ * - async=false(기본) 일 때 서버가 완료까지 응답을 보류하므로 ky timeout 을 600s 로 설정한다.
1565
+ */
1566
+ async run(params) {
1567
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(params.appKey)}/artifacts/${encodeURIComponent(params.artifactId)}/server-group/${encodeURIComponent(params.serverGroupId)}/deploy`;
1568
+ const isAsync = params.async ?? false;
1569
+ const payload = {
1570
+ concurrentNum: params.concurrentNum ?? 1,
1571
+ nextWhenFail: params.nextWhenFail ?? false,
1572
+ scenarioIds: params.scenarioIds,
1573
+ deployNote: params.deployNote ?? `CLI deploy ${(/* @__PURE__ */ new Date()).toISOString()}`,
1574
+ async: isAsync
1575
+ };
1576
+ if (params.targetHosts) {
1577
+ payload["targetServerHostnames"] = params.targetHosts;
1578
+ }
1579
+ try {
1580
+ const res = await import_ky6.default.post(url, {
1581
+ headers: {
1582
+ ...this.authHeaders(),
1583
+ "Content-Type": "application/json"
1584
+ },
1585
+ json: payload,
1586
+ retry: 0,
1587
+ timeout: isAsync ? DEFAULT_TIMEOUT_MS2 : SYNC_TIMEOUT_MS
1588
+ }).json();
1589
+ return unwrap(res);
1590
+ } catch (err) {
1591
+ throw toNhnCloudCliError(err);
1592
+ }
1593
+ }
1594
+ /**
1595
+ * 아티팩트 목록을 조회한다.
1596
+ */
1597
+ async artifacts(appKey) {
1598
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts`;
1599
+ try {
1600
+ const res = await import_ky6.default.get(url, {
1601
+ headers: this.authHeaders(),
1602
+ retry: 0,
1603
+ timeout: DEFAULT_TIMEOUT_MS2
1604
+ }).json();
1605
+ return unwrap(res);
1606
+ } catch (err) {
1607
+ throw toNhnCloudCliError(err);
1608
+ }
1609
+ }
1610
+ /**
1611
+ * 서버그룹 목록을 조회한다.
1612
+ */
1613
+ async serverGroups(appKey, artifactId) {
1614
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/server-groups`;
1615
+ try {
1616
+ const res = await import_ky6.default.get(url, {
1617
+ headers: this.authHeaders(),
1618
+ retry: 0,
1619
+ timeout: DEFAULT_TIMEOUT_MS2
1620
+ }).json();
1621
+ return unwrap(res);
1622
+ } catch (err) {
1623
+ throw toNhnCloudCliError(err);
1624
+ }
1625
+ }
1626
+ /**
1627
+ * 배포 이력을 조회한다.
1628
+ */
1629
+ async histories(appKey, artifactId) {
1630
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/deploy-histories`;
1631
+ try {
1632
+ const res = await import_ky6.default.get(url, {
1633
+ headers: this.authHeaders(),
1634
+ retry: 0,
1635
+ timeout: DEFAULT_TIMEOUT_MS2
1636
+ }).json();
1083
1637
  return unwrap(res);
1084
1638
  } catch (err) {
1085
1639
  throw toNhnCloudCliError(err);
1086
1640
  }
1087
1641
  }
1642
+ /**
1643
+ * 바이너리 그룹 목록을 조회한다.
1644
+ */
1645
+ async binaryGroups(appKey, artifactId) {
1646
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/binary-groups`;
1647
+ try {
1648
+ const res = await import_ky6.default.get(url, {
1649
+ headers: this.authHeaders(),
1650
+ retry: 0,
1651
+ timeout: DEFAULT_TIMEOUT_MS2
1652
+ }).json();
1653
+ const body = unwrap(res);
1654
+ const list = body.binaryGroups;
1655
+ if (!Array.isArray(list) || !list.every(isBinaryGroup)) {
1656
+ throw new NhnCloudCliError(
1657
+ "binary-groups \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 binaryGroups \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
1658
+ EXIT_API_ERROR
1659
+ );
1660
+ }
1661
+ return list;
1662
+ } catch (err) {
1663
+ throw toNhnCloudCliError(err);
1664
+ }
1665
+ }
1666
+ /**
1667
+ * 특정 바이너리 그룹의 바이너리 목록을 조회한다.
1668
+ * pageNum/pageSize/sortKey/sortDirection 은 NHN docs 의 쿼리 파라미터로 그대로 전달한다.
1669
+ */
1670
+ async binaries(appKey, artifactId, binaryGroupKey, params = {}) {
1671
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/binary-groups/${binaryGroupKey}/binaries`;
1672
+ const searchParams = {};
1673
+ if (params.pageNum !== void 0) searchParams["pageNum"] = params.pageNum;
1674
+ if (params.pageSize !== void 0) searchParams["pageSize"] = params.pageSize;
1675
+ if (params.sortKey !== void 0) searchParams["sortKey"] = params.sortKey;
1676
+ if (params.sortDirection !== void 0) searchParams["sortDirection"] = params.sortDirection;
1677
+ try {
1678
+ const res = await import_ky6.default.get(url, {
1679
+ headers: this.authHeaders(),
1680
+ searchParams,
1681
+ retry: 0,
1682
+ timeout: DEFAULT_TIMEOUT_MS2
1683
+ }).json();
1684
+ const body = unwrap(res);
1685
+ const list = body.binaries;
1686
+ if (!Array.isArray(list) || !list.every(isBinary)) {
1687
+ throw new NhnCloudCliError(
1688
+ "binaries \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 binaries \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
1689
+ EXIT_API_ERROR
1690
+ );
1691
+ }
1692
+ const tc = body.totalCount;
1693
+ const totalCount = typeof tc === "number" ? tc : typeof tc === "string" && /^\d+$/.test(tc) ? Number(tc) : list.length;
1694
+ return { totalCount, binaries: list };
1695
+ } catch (err) {
1696
+ throw toNhnCloudCliError(err);
1697
+ }
1698
+ }
1699
+ /**
1700
+ * 바이너리를 multipart/form-data 로 업로드한다.
1701
+ *
1702
+ * 신규 전송 경로 — 기존 메서드는 ky `json:`(JSON body) 만 쓴다 (ADR-015).
1703
+ * - 파일 파트(binaryFile)는 command 에서 statSync 가드 후 읽은 Buffer 를 Blob 으로 감싼다.
1704
+ * - Content-Type 은 수동으로 박지 않는다 — ky 가 FormData 에서 multipart boundary 를 자동 설정한다.
1705
+ *
1706
+ * ⚠️ 실측 pending — 수동 QA 로 확정 필요:
1707
+ * - endpoint 경로 세그먼트 단/복수(`binary-group` vs `binary-groups`) — 404 시 복수형으로 교체.
1708
+ * - 응답 binaryKey 타입(number|string) — 코드는 둘 다 수용 후 Number() 정규화.
1709
+ */
1710
+ async uploadBinary(params) {
1711
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(params.appKey)}/artifacts/${encodeURIComponent(params.artifactId)}/binary-group/${params.binaryGroupKey}`;
1712
+ const form = new FormData();
1713
+ const blob = new Blob([new Uint8Array(params.fileBuffer)]);
1714
+ form.append("binaryFile", blob, params.fileName);
1715
+ form.append("applicationType", params.applicationType);
1716
+ if (params.description !== void 0) {
1717
+ form.append("description", params.description);
1718
+ }
1719
+ try {
1720
+ const res = await import_ky6.default.post(url, {
1721
+ headers: this.authHeaders(),
1722
+ // 인증 헤더만 — multipart boundary 는 ky 가 자동 설정
1723
+ body: form,
1724
+ retry: 0,
1725
+ timeout: SYNC_TIMEOUT_MS
1726
+ // 업로드는 파일 크기에 따라 길 수 있어 긴 timeout
1727
+ }).json();
1728
+ const body = unwrap(res);
1729
+ const normalizedKey = Number(body.binaryKey);
1730
+ if (typeof body.downloadUrl !== "string" || !Number.isFinite(normalizedKey)) {
1731
+ throw new NhnCloudCliError(
1732
+ "upload \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 downloadUrl/binaryKey \uB204\uB77D \uB610\uB294 \uBE44\uC22B\uC790.",
1733
+ EXIT_API_ERROR
1734
+ );
1735
+ }
1736
+ return { downloadUrl: body.downloadUrl, binaryKey: normalizedKey };
1737
+ } catch (err) {
1738
+ throw toNhnCloudCliError(err);
1739
+ }
1740
+ }
1741
+ /**
1742
+ * 바이너리를 다운로드해 내용(Buffer)을 반환한다.
1743
+ *
1744
+ * 신규 수신 경로 — 응답이 봉투 JSON 이 아니라 파일 바이너리 스트림이다 (ADR-015).
1745
+ * 다른 메서드처럼 .json()/unwrap 을 쓰면 바이너리를 JSON 으로 파싱하다 깨진다 —
1746
+ * 반드시 .arrayBuffer() 로 받는다. 성공/실패는 HTTP status(ky throwHttpErrors)로만 판정.
1747
+ * 파일 쓰기는 command 가 담당한다 (client 는 내용만 반환 — 테스트 용이).
1748
+ *
1749
+ * ⚠️ 실측 pending — 수동 QA round-trip 으로 확정 필요:
1750
+ * - endpoint 단/복수(`binary-group` vs `binary-groups`) — 404 시 복수형으로 교체.
1751
+ * - 응답이 raw 바이너리인지 downloadUrl JSON 인지 — QA step 5 diff 로 확인.
1752
+ */
1753
+ async downloadBinary(appKey, artifactId, binaryGroupKey, binaryKey) {
1754
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/binary-group/${binaryGroupKey}/binaries/${binaryKey}`;
1755
+ try {
1756
+ const ab = await import_ky6.default.get(url, {
1757
+ headers: this.authHeaders(),
1758
+ retry: 0,
1759
+ timeout: SYNC_TIMEOUT_MS
1760
+ // 큰 파일 다운로드 — 긴 timeout
1761
+ }).arrayBuffer();
1762
+ return Buffer.from(ab);
1763
+ } catch (err) {
1764
+ throw toNhnCloudCliError(err);
1765
+ }
1766
+ }
1088
1767
  };
1089
1768
 
1090
1769
  // src/commands/deploy/helpers.ts
@@ -1096,7 +1775,7 @@ async function createDeployClient(profileOpt) {
1096
1775
  }
1097
1776
 
1098
1777
  // src/commands/deploy/run.ts
1099
- var runCommand = new import_commander3.Command("run").description("\uBC30\uD3EC\uB97C \uC2E4\uD589\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--server-group-id <id>", "target \uC758 serverGroupId override").option("--scenario-ids <csv>", "target \uC758 scenarioIds override").option("--target-hosts <csv>", "\uB300\uC0C1 \uD638\uC2A4\uD2B8 (\uC0DD\uB7B5 \uC2DC \uC11C\uBC84\uADF8\uB8F9 \uC804\uCCB4)").option("--concurrent <n>", "\uBCD1\uB82C \uBC30\uD3EC \uC218 (\uAE30\uBCF8 1)", "1").option("--next-when-fail", "\uC2DC\uB098\uB9AC\uC624 \uC2E4\uD328 \uC2DC\uC5D0\uB3C4 \uC9C4\uD589").option("--note <s>", "\uBC30\uD3EC \uBA54\uBAA8").option("--async", "\uC989\uC2DC \uBC18\uD658 (\uAE30\uBCF8\uC740 \uC644\uB8CC \uB300\uAE30)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
1778
+ var runCommand = new import_commander5.Command("run").description("\uBC30\uD3EC\uB97C \uC2E4\uD589\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--server-group-id <id>", "target \uC758 serverGroupId override").option("--scenario-ids <csv>", "target \uC758 scenarioIds override").option("--target-hosts <csv>", "\uB300\uC0C1 \uD638\uC2A4\uD2B8 (\uC0DD\uB7B5 \uC2DC \uC11C\uBC84\uADF8\uB8F9 \uC804\uCCB4)").option("--concurrent <n>", "\uBCD1\uB82C \uBC30\uD3EC \uC218 (\uAE30\uBCF8 1)", "1").option("--next-when-fail", "\uC2DC\uB098\uB9AC\uC624 \uC2E4\uD328 \uC2DC\uC5D0\uB3C4 \uC9C4\uD589").option("--note <s>", "\uBC30\uD3EC \uBA54\uBAA8").option("--async", "\uC989\uC2DC \uBC18\uD658 (\uAE30\uBCF8\uC740 \uC644\uB8CC \uB300\uAE30)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
1100
1779
  const opts = cmd.optsWithGlobals();
1101
1780
  const target = await getDeployTarget(targetName);
1102
1781
  const appKey = opts.appKey ?? target.appKey;
@@ -1132,8 +1811,8 @@ var runCommand = new import_commander3.Command("run").description("\uBC30\uD3EC\
1132
1811
  });
1133
1812
 
1134
1813
  // src/commands/deploy/artifacts.ts
1135
- var import_commander4 = require("commander");
1136
- var artifactsCommand = new import_commander4.Command("artifacts").description("\uC544\uD2F0\uD329\uD2B8 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("[target]", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984 (--app-key \uB85C \uB300\uCCB4 \uAC00\uB2A5)").option("--app-key <k>", "appKey \uC9C1\uC811 \uC9C0\uC815 (target \uC5C6\uC774 \uC0AC\uC6A9 \uAC00\uB2A5)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
1814
+ var import_commander6 = require("commander");
1815
+ var artifactsCommand = new import_commander6.Command("artifacts").description("\uC544\uD2F0\uD329\uD2B8 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("[target]", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984 (--app-key \uB85C \uB300\uCCB4 \uAC00\uB2A5)").option("--app-key <k>", "appKey \uC9C1\uC811 \uC9C0\uC815 (target \uC5C6\uC774 \uC0AC\uC6A9 \uAC00\uB2A5)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
1137
1816
  const opts = cmd.optsWithGlobals();
1138
1817
  let appKey;
1139
1818
  if (opts.appKey) {
@@ -1173,8 +1852,8 @@ var artifactsCommand = new import_commander4.Command("artifacts").description("\
1173
1852
  });
1174
1853
 
1175
1854
  // src/commands/deploy/server-groups.ts
1176
- var import_commander5 = require("commander");
1177
- var serverGroupsCommand = new import_commander5.Command("server-groups").description("\uC11C\uBC84\uADF8\uB8F9 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
1855
+ var import_commander7 = require("commander");
1856
+ var serverGroupsCommand = new import_commander7.Command("server-groups").description("\uC11C\uBC84\uADF8\uB8F9 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
1178
1857
  const opts = cmd.optsWithGlobals();
1179
1858
  const target = await getDeployTarget(targetName);
1180
1859
  const appKey = opts.appKey ?? target.appKey;
@@ -1198,8 +1877,8 @@ var serverGroupsCommand = new import_commander5.Command("server-groups").descrip
1198
1877
  });
1199
1878
 
1200
1879
  // src/commands/deploy/histories.ts
1201
- var import_commander6 = require("commander");
1202
- var historiesCommand = new import_commander6.Command("histories").description("\uBC30\uD3EC \uC774\uB825\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
1880
+ var import_commander8 = require("commander");
1881
+ var historiesCommand = new import_commander8.Command("histories").description("\uBC30\uD3EC \uC774\uB825\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
1203
1882
  const opts = cmd.optsWithGlobals();
1204
1883
  const target = await getDeployTarget(targetName);
1205
1884
  const appKey = opts.appKey ?? target.appKey;
@@ -1222,12 +1901,236 @@ var historiesCommand = new import_commander6.Command("histories").description("\
1222
1901
  });
1223
1902
  });
1224
1903
 
1904
+ // src/commands/deploy/binary-groups.ts
1905
+ var import_commander9 = require("commander");
1906
+ var binaryGroupsCommand = new import_commander9.Command("binary-groups").description("\uBC14\uC774\uB108\uB9AC \uADF8\uB8F9 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
1907
+ const opts = cmd.optsWithGlobals();
1908
+ const target = await getDeployTarget(targetName);
1909
+ const appKey = opts.appKey ?? target.appKey;
1910
+ const artifactId = opts.artifactId ?? target.artifactId;
1911
+ const { client } = await createDeployClient(opts.profile);
1912
+ startSpinner("\uBC14\uC774\uB108\uB9AC \uADF8\uB8F9 \uBAA9\uB85D \uC870\uD68C \uC911...");
1913
+ let groups;
1914
+ try {
1915
+ groups = await client.binaryGroups(appKey, artifactId);
1916
+ } catch (err) {
1917
+ stopSpinner(false);
1918
+ throw err;
1919
+ }
1920
+ stopSpinner(true);
1921
+ output(opts, {
1922
+ headers: ["key", "name", "regionCode", "createDate", "description"],
1923
+ // 가드는 key·name 만 검증 — 나머지 필드는 누락 시 "undefined" 가 박히지 않게 ?? "" 방어.
1924
+ rows: groups.map((g) => [
1925
+ String(g.key),
1926
+ g.name ?? "",
1927
+ g.regionCode ?? "",
1928
+ g.createDate ?? "",
1929
+ g.description ?? ""
1930
+ ]),
1931
+ raw: groups,
1932
+ // ids 에 key 를 넣어 --quiet 시 그룹 key 만 출력 → binaries --binary-group 에 파이프 가능
1933
+ ids: groups.map((g) => String(g.key))
1934
+ });
1935
+ });
1936
+
1937
+ // src/commands/deploy/binaries.ts
1938
+ var import_commander10 = require("commander");
1939
+ function parsePositiveInt(value, flag) {
1940
+ if (value === void 0) return void 0;
1941
+ if (!/^[1-9]\d*$/.test(value)) {
1942
+ throw new NhnCloudCliError(
1943
+ `${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${JSON.stringify(value)}).`,
1944
+ EXIT_PARAM_ERROR
1945
+ );
1946
+ }
1947
+ return Number(value);
1948
+ }
1949
+ var binariesCommand = new import_commander10.Command("binaries").description("\uD2B9\uC815 \uBC14\uC774\uB108\uB9AC \uADF8\uB8F9\uC758 \uBC14\uC774\uB108\uB9AC \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").requiredOption("--binary-group <key>", "\uC870\uD68C\uD560 \uBC14\uC774\uB108\uB9AC \uADF8\uB8F9 key (binary-groups \uB85C \uD655\uC778)").option("--page-num <n>", "\uD398\uC774\uC9C0 \uBC88\uD638 (1 \uC774\uC0C1)").option("--page-size <n>", "\uD398\uC774\uC9C0 \uD06C\uAE30 (1 \uC774\uC0C1)").option("--sort-key <k>", "\uC815\uB82C \uAE30\uC900 (\uC608: UPLOAD_DATE)").option("--sort-direction <d>", "\uC815\uB82C \uBC29\uD5A5 (\uC608: DESC)").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
1950
+ const opts = cmd.optsWithGlobals();
1951
+ const binaryGroupKey = parsePositiveInt(opts.binaryGroup, "--binary-group");
1952
+ if (binaryGroupKey === void 0) {
1953
+ throw new NhnCloudCliError("--binary-group \uC774 \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
1954
+ }
1955
+ const pageNum = parsePositiveInt(opts.pageNum, "--page-num");
1956
+ const pageSize = parsePositiveInt(opts.pageSize, "--page-size");
1957
+ const target = await getDeployTarget(targetName);
1958
+ const appKey = opts.appKey ?? target.appKey;
1959
+ const artifactId = opts.artifactId ?? target.artifactId;
1960
+ const { client } = await createDeployClient(opts.profile);
1961
+ startSpinner("\uBC14\uC774\uB108\uB9AC \uBAA9\uB85D \uC870\uD68C \uC911...");
1962
+ let result;
1963
+ try {
1964
+ result = await client.binaries(appKey, artifactId, binaryGroupKey, {
1965
+ pageNum,
1966
+ pageSize,
1967
+ sortKey: opts.sortKey,
1968
+ sortDirection: opts.sortDirection
1969
+ });
1970
+ } catch (err) {
1971
+ stopSpinner(false);
1972
+ throw err;
1973
+ }
1974
+ stopSpinner(true);
1975
+ output(opts, {
1976
+ headers: ["binaryKey", "version", "binaryName", "size(bytes)", "uploadDate", "uploader"],
1977
+ // 가드는 binaryKey·binarySize 만 검증 — 나머지 필드는 응답에서 누락 시 "undefined" 가
1978
+ // 표에 박히지 않게 ?? "" 로 방어한다 (타입 정합성 실측은 후속 이슈).
1979
+ rows: result.binaries.map((b) => [
1980
+ String(b.binaryKey),
1981
+ b.version ?? "",
1982
+ b.binaryName ?? "",
1983
+ String(b.binarySize),
1984
+ b.uploadDate ?? "",
1985
+ b.uploader ?? ""
1986
+ ]),
1987
+ // raw 에 totalCount 포함 → --json 으로 페이지 정보 확인 가능
1988
+ raw: result,
1989
+ ids: result.binaries.map((b) => String(b.binaryKey))
1990
+ });
1991
+ });
1992
+
1993
+ // src/commands/deploy/upload.ts
1994
+ var import_commander11 = require("commander");
1995
+ var import_node_fs3 = require("fs");
1996
+ var import_node_path3 = require("path");
1997
+ var MAX_UPLOAD_BYTES = 512 * 1024 * 1024;
1998
+ function parsePositiveInt2(value, flag) {
1999
+ if (value === void 0) return void 0;
2000
+ if (!/^[1-9]\d*$/.test(value)) {
2001
+ throw new NhnCloudCliError(
2002
+ `${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${JSON.stringify(value)}).`,
2003
+ EXIT_PARAM_ERROR
2004
+ );
2005
+ }
2006
+ return Number(value);
2007
+ }
2008
+ var uploadCommand = new import_commander11.Command("upload").description("\uB85C\uCEEC \uD30C\uC77C\uC744 \uBC14\uC774\uB108\uB9AC \uADF8\uB8F9\uC5D0 \uC5C5\uB85C\uB4DC\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").requiredOption("--file <path>", "\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uACBD\uB85C").requiredOption("--binary-group <key>", "\uC5C5\uB85C\uB4DC \uB300\uC0C1 \uBC14\uC774\uB108\uB9AC \uADF8\uB8F9 key (binary-groups \uB85C \uD655\uC778)").option("--application-type <type>", "applicationType (\uC608: server)", "server").option("--description <text>", "\uBC14\uC774\uB108\uB9AC \uC124\uBA85").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
2009
+ const opts = cmd.optsWithGlobals();
2010
+ const binaryGroupKey = parsePositiveInt2(opts.binaryGroup, "--binary-group");
2011
+ if (binaryGroupKey === void 0) {
2012
+ throw new NhnCloudCliError("--binary-group \uC774 \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
2013
+ }
2014
+ const filePath = opts.file;
2015
+ let stat;
2016
+ try {
2017
+ stat = (0, import_node_fs3.statSync)(filePath);
2018
+ } catch (e) {
2019
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
2020
+ throw new NhnCloudCliError(
2021
+ `--file \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath} (${reason})`,
2022
+ EXIT_PARAM_ERROR
2023
+ );
2024
+ }
2025
+ if (!stat.isFile()) {
2026
+ throw new NhnCloudCliError(`--file \uC774 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${filePath}`, EXIT_PARAM_ERROR);
2027
+ }
2028
+ if (stat.size > MAX_UPLOAD_BYTES) {
2029
+ throw new NhnCloudCliError(
2030
+ `--file \uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${stat.size} \uBC14\uC774\uD2B8). \uC5C5\uB85C\uB4DC \uD55C\uB3C4 ${MAX_UPLOAD_BYTES} \uBC14\uC774\uD2B8.`,
2031
+ EXIT_PARAM_ERROR
2032
+ );
2033
+ }
2034
+ const fileBuffer = (0, import_node_fs3.readFileSync)(filePath);
2035
+ const fileName = (0, import_node_path3.basename)(filePath);
2036
+ const target = await getDeployTarget(targetName);
2037
+ const appKey = opts.appKey ?? target.appKey;
2038
+ const artifactId = opts.artifactId ?? target.artifactId;
2039
+ const { client } = await createDeployClient(opts.profile);
2040
+ startSpinner("\uBC14\uC774\uB108\uB9AC \uC5C5\uB85C\uB4DC \uC911...");
2041
+ let result;
2042
+ try {
2043
+ result = await client.uploadBinary({
2044
+ appKey,
2045
+ artifactId,
2046
+ binaryGroupKey,
2047
+ fileBuffer,
2048
+ fileName,
2049
+ applicationType: opts.applicationType,
2050
+ // Commander 옵션 기본값 "server" 가 SSOT — dead fallback 제거
2051
+ description: opts.description
2052
+ });
2053
+ } catch (err) {
2054
+ stopSpinner(false);
2055
+ throw err;
2056
+ }
2057
+ stopSpinner(true);
2058
+ output(opts, {
2059
+ headers: ["field", "value"],
2060
+ rows: [
2061
+ ["binaryKey", String(result.binaryKey)],
2062
+ ["downloadUrl", result.downloadUrl]
2063
+ ],
2064
+ raw: result,
2065
+ ids: [String(result.binaryKey)]
2066
+ });
2067
+ });
2068
+
2069
+ // src/commands/deploy/download.ts
2070
+ var import_commander12 = require("commander");
2071
+ var import_node_fs4 = require("fs");
2072
+ var import_chalk2 = __toESM(require("chalk"));
2073
+ function parsePositiveInt3(value, flag) {
2074
+ if (value === void 0) return void 0;
2075
+ if (!/^[1-9]\d*$/.test(value)) {
2076
+ throw new NhnCloudCliError(
2077
+ `${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${JSON.stringify(value)}).`,
2078
+ EXIT_PARAM_ERROR
2079
+ );
2080
+ }
2081
+ return Number(value);
2082
+ }
2083
+ function assertWritable2(path, force) {
2084
+ if (force) return;
2085
+ try {
2086
+ (0, import_node_fs4.statSync)(path);
2087
+ } catch (e) {
2088
+ if (e.code === "ENOENT") return;
2089
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
2090
+ throw new NhnCloudCliError(
2091
+ `-o \uACBD\uB85C\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${path} (${reason})`,
2092
+ EXIT_PARAM_ERROR
2093
+ );
2094
+ }
2095
+ throw new NhnCloudCliError(
2096
+ `-o \uB300\uC0C1\uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: ${path}. \uB36E\uC5B4\uC4F0\uB824\uBA74 --force \uB97C \uC4F0\uC138\uC694.`,
2097
+ EXIT_PARAM_ERROR
2098
+ );
2099
+ }
2100
+ var downloadCommand = new import_commander12.Command("download").description("\uBC14\uC774\uB108\uB9AC\uB97C \uB85C\uCEEC \uD30C\uC77C\uB85C \uB2E4\uC6B4\uB85C\uB4DC\uD55C\uB2E4").argument("<target>", "config.json \uC5D0 \uC815\uC758\uB41C deploy target \uC774\uB984").requiredOption("--binary-group <key>", "\uBC14\uC774\uB108\uB9AC \uADF8\uB8F9 key (binary-groups \uB85C \uD655\uC778)").requiredOption("--binary-key <key>", "\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uBC14\uC774\uB108\uB9AC key (binaries \uB610\uB294 upload \uB85C \uD655\uC778)").requiredOption("-o, --output <file>", "\uC800\uC7A5\uD560 \uD30C\uC77C \uACBD\uB85C").option("--force", "\uB300\uC0C1 \uD30C\uC77C\uC774 \uC788\uC73C\uBA74 \uB36E\uC5B4\uC4F4\uB2E4").option("--app-key <k>", "target \uC758 appKey override").option("--artifact-id <id>", "target \uC758 artifactId override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (targetName, _opts, cmd) => {
2101
+ const opts = cmd.optsWithGlobals();
2102
+ const binaryGroupKey = parsePositiveInt3(opts.binaryGroup, "--binary-group");
2103
+ const binaryKey = parsePositiveInt3(opts.binaryKey, "--binary-key");
2104
+ if (binaryGroupKey === void 0 || binaryKey === void 0) {
2105
+ throw new NhnCloudCliError("--binary-group / --binary-key \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
2106
+ }
2107
+ const outPath = opts.output;
2108
+ assertWritable2(outPath, opts.force ?? false);
2109
+ const target = await getDeployTarget(targetName);
2110
+ const appKey = opts.appKey ?? target.appKey;
2111
+ const artifactId = opts.artifactId ?? target.artifactId;
2112
+ const { client } = await createDeployClient(opts.profile);
2113
+ startSpinner("\uBC14\uC774\uB108\uB9AC \uB2E4\uC6B4\uB85C\uB4DC \uC911...");
2114
+ try {
2115
+ const buffer = await client.downloadBinary(appKey, artifactId, binaryGroupKey, binaryKey);
2116
+ (0, import_node_fs4.writeFileSync)(outPath, buffer);
2117
+ } catch (err) {
2118
+ stopSpinner(false);
2119
+ throw err;
2120
+ }
2121
+ stopSpinner(true);
2122
+ if (!opts.quiet) {
2123
+ process.stderr.write(import_chalk2.default.green(` \uC800\uC7A5\uB428: ${outPath}
2124
+ `));
2125
+ }
2126
+ });
2127
+
1225
2128
  // src/commands/instance/list.ts
1226
- var import_commander7 = require("commander");
2129
+ var import_commander13 = require("commander");
1227
2130
 
1228
2131
  // src/services/instance/client.ts
1229
- var import_ky6 = __toESM(require("ky"));
1230
- var DEFAULT_TIMEOUT_MS2 = 3e4;
2132
+ var import_ky7 = __toESM(require("ky"));
2133
+ var DEFAULT_TIMEOUT_MS3 = 3e4;
1231
2134
  var DEFAULT_POLL_INTERVAL_MS = 5e3;
1232
2135
  function isServer(val) {
1233
2136
  if (typeof val !== "object" || val === null) return false;
@@ -1244,21 +2147,108 @@ function isServersResponse(val) {
1244
2147
  const obj = val;
1245
2148
  return Array.isArray(obj["servers"]);
1246
2149
  }
2150
+ function isFlavor(val) {
2151
+ if (typeof val !== "object" || val === null) return false;
2152
+ const obj = val;
2153
+ return typeof obj["id"] === "string" && typeof obj["name"] === "string";
2154
+ }
2155
+ function isImage(val) {
2156
+ if (typeof val !== "object" || val === null) return false;
2157
+ const obj = val;
2158
+ return typeof obj["id"] === "string" && // Glance v2 스펙상 name 은 nullable — null 인 private 이미지 하나가 페이지 전체를 거부하지 않게 허용.
2159
+ (typeof obj["name"] === "string" || obj["name"] === null) && typeof obj["status"] === "string" && typeof obj["visibility"] === "string";
2160
+ }
2161
+ function isImagesResponse(val) {
2162
+ if (typeof val !== "object" || val === null) return false;
2163
+ const obj = val;
2164
+ const nextOk = obj["next"] === void 0 || typeof obj["next"] === "string";
2165
+ return Array.isArray(obj["images"]) && obj["images"].every(isImage) && nextOk;
2166
+ }
2167
+ function isFlavorsResponse(val) {
2168
+ if (typeof val !== "object" || val === null) return false;
2169
+ const obj = val;
2170
+ return Array.isArray(obj["flavors"]) && obj["flavors"].every(isFlavor);
2171
+ }
2172
+ function isFlavorDetail(val) {
2173
+ if (typeof val !== "object" || val === null) return false;
2174
+ const obj = val;
2175
+ return typeof obj["id"] === "string" && typeof obj["name"] === "string" && typeof obj["vcpus"] === "number" && typeof obj["ram"] === "number" && typeof obj["disk"] === "number";
2176
+ }
2177
+ function isFlavorDetailsResponse(val) {
2178
+ if (typeof val !== "object" || val === null) return false;
2179
+ const obj = val;
2180
+ return Array.isArray(obj["flavors"]) && obj["flavors"].every(isFlavorDetail);
2181
+ }
1247
2182
  function isCreateResponse(val) {
1248
2183
  if (typeof val !== "object" || val === null) return false;
1249
2184
  const server = val["server"];
1250
2185
  if (typeof server !== "object" || server === null) return false;
1251
2186
  return typeof server["id"] === "string";
1252
2187
  }
2188
+ function isKeypair(val) {
2189
+ if (typeof val !== "object" || val === null) return false;
2190
+ const obj = val;
2191
+ return typeof obj["name"] === "string" && typeof obj["public_key"] === "string" && typeof obj["fingerprint"] === "string";
2192
+ }
2193
+ function isKeypairsResponse(val) {
2194
+ if (typeof val !== "object" || val === null) return false;
2195
+ const obj = val;
2196
+ return Array.isArray(obj["keypairs"]) && obj["keypairs"].every((e) => {
2197
+ if (typeof e !== "object" || e === null) return false;
2198
+ return isKeypair(e["keypair"]);
2199
+ });
2200
+ }
2201
+ function isCreateKeypairResponse(val) {
2202
+ if (typeof val !== "object" || val === null) return false;
2203
+ const obj = val;
2204
+ return isKeypair(obj["keypair"]);
2205
+ }
2206
+ function isKeypairDetail(val) {
2207
+ if (typeof val !== "object" || val === null) return false;
2208
+ const obj = val;
2209
+ return typeof obj["name"] === "string" && typeof obj["public_key"] === "string" && typeof obj["fingerprint"] === "string" && typeof obj["user_id"] === "string" && typeof obj["id"] === "string" && typeof obj["created_at"] === "string";
2210
+ }
2211
+ function isKeypairDetailResponse(val) {
2212
+ if (typeof val !== "object" || val === null) return false;
2213
+ return isKeypairDetail(val["keypair"]);
2214
+ }
2215
+ function isAvailabilityZone(val) {
2216
+ if (typeof val !== "object" || val === null) return false;
2217
+ const obj = val;
2218
+ const state = obj["zoneState"];
2219
+ if (typeof state !== "object" || state === null) return false;
2220
+ return typeof obj["zoneName"] === "string" && typeof state["available"] === "boolean";
2221
+ }
2222
+ function isAvailabilityZonesResponse(val) {
2223
+ if (typeof val !== "object" || val === null) return false;
2224
+ const obj = val;
2225
+ return Array.isArray(obj["availabilityZoneInfo"]) && obj["availabilityZoneInfo"].every(isAvailabilityZone);
2226
+ }
2227
+ function isServerVolumeAttachment(val) {
2228
+ if (typeof val !== "object" || val === null) return false;
2229
+ const o = val;
2230
+ return typeof o["id"] === "string" && typeof o["volumeId"] === "string" && typeof o["serverId"] === "string" && typeof o["device"] === "string";
2231
+ }
2232
+ function isVolumeAttachmentsResponse(val) {
2233
+ if (typeof val !== "object" || val === null) return false;
2234
+ const arr = val["volumeAttachments"];
2235
+ return Array.isArray(arr) && arr.every(isServerVolumeAttachment);
2236
+ }
2237
+ function isVolumeAttachmentResponse(val) {
2238
+ if (typeof val !== "object" || val === null) return false;
2239
+ return isServerVolumeAttachment(val["volumeAttachment"]);
2240
+ }
1253
2241
  function hasIpAddress(server) {
1254
2242
  return Object.values(server.addresses).some((list) => list.length > 0);
1255
2243
  }
1256
2244
  var InstanceClient = class {
1257
2245
  tokenId;
1258
2246
  computeEndpoint;
1259
- constructor(tokenId, computeEndpoint) {
2247
+ imageEndpoint;
2248
+ constructor(tokenId, computeEndpoint, imageEndpoint) {
1260
2249
  this.tokenId = tokenId;
1261
2250
  this.computeEndpoint = computeEndpoint;
2251
+ this.imageEndpoint = imageEndpoint;
1262
2252
  }
1263
2253
  authHeaders() {
1264
2254
  return { "X-Auth-Token": this.tokenId };
@@ -1269,10 +2259,10 @@ var InstanceClient = class {
1269
2259
  async list() {
1270
2260
  const url = `${this.computeEndpoint}/servers/detail`;
1271
2261
  try {
1272
- const raw = await import_ky6.default.get(url, {
2262
+ const raw = await import_ky7.default.get(url, {
1273
2263
  headers: this.authHeaders(),
1274
2264
  retry: 0,
1275
- timeout: DEFAULT_TIMEOUT_MS2
2265
+ timeout: DEFAULT_TIMEOUT_MS3
1276
2266
  }).json();
1277
2267
  if (!isServersResponse(raw)) {
1278
2268
  throw new NhnCloudCliError(
@@ -1291,10 +2281,10 @@ var InstanceClient = class {
1291
2281
  async get(id) {
1292
2282
  const url = `${this.computeEndpoint}/servers/${encodeURIComponent(id)}`;
1293
2283
  try {
1294
- const raw = await import_ky6.default.get(url, {
2284
+ const raw = await import_ky7.default.get(url, {
1295
2285
  headers: this.authHeaders(),
1296
2286
  retry: 0,
1297
- timeout: DEFAULT_TIMEOUT_MS2
2287
+ timeout: DEFAULT_TIMEOUT_MS3
1298
2288
  }).json();
1299
2289
  if (!isServerResponse(raw)) {
1300
2290
  throw new NhnCloudCliError(
@@ -1350,11 +2340,11 @@ var InstanceClient = class {
1350
2340
  }
1351
2341
  let raw;
1352
2342
  try {
1353
- raw = await import_ky6.default.post(url, {
2343
+ raw = await import_ky7.default.post(url, {
1354
2344
  headers: this.authHeaders(),
1355
2345
  json: { server: serverBody },
1356
2346
  retry: 0,
1357
- timeout: DEFAULT_TIMEOUT_MS2
2347
+ timeout: DEFAULT_TIMEOUT_MS3
1358
2348
  }).json();
1359
2349
  } catch (err) {
1360
2350
  throw toNhnCloudCliError(err);
@@ -1373,109 +2363,1023 @@ var InstanceClient = class {
1373
2363
  async delete(id) {
1374
2364
  const url = `${this.computeEndpoint}/servers/${encodeURIComponent(id)}`;
1375
2365
  try {
1376
- await import_ky6.default.delete(url, {
2366
+ await import_ky7.default.delete(url, {
1377
2367
  headers: this.authHeaders(),
1378
2368
  retry: 0,
1379
- timeout: DEFAULT_TIMEOUT_MS2
2369
+ timeout: DEFAULT_TIMEOUT_MS3
1380
2370
  });
1381
2371
  } catch (err) {
1382
2372
  throw toNhnCloudCliError(err);
1383
2373
  }
1384
2374
  }
1385
2375
  /**
1386
- * 인스턴스가 ACTIVE 상태 + IP 1개 이상이 될 때까지 폴링한다.
1387
- *
1388
- * - status === "ACTIVE" + addresses IP 1개 이상: 즉시 반환
1389
- * - timeout 초과: 마지막 status 포함한 NhnCloudCliError(EXIT_API_ERROR)
2376
+ * 서버 action 실행한다 (POST /servers/{id}/action, 202 무본문).
2377
+ * NHN Cloud(OpenStack Nova)의 모든 전원·라이프사이클 action 의 공용 경로다.
2378
+ * payload 호출자가 action 별로 구성한다 (예: { "os-start": null }).
2379
+ * start/stop/reboot helper재사용하며, resize/shelve 등 향후 action 도 동일.
1390
2380
  */
1391
- async waitForActive(id, opts) {
1392
- const intervalMs = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1393
- const deadline = Date.now() + opts.timeoutMs;
1394
- let lastServer = null;
1395
- while (Date.now() < deadline) {
1396
- const server = await this.get(id);
1397
- lastServer = server;
1398
- if (server.status === "ACTIVE" && hasIpAddress(server)) {
1399
- return server;
1400
- }
1401
- const remaining = deadline - Date.now();
1402
- if (remaining <= 0) break;
1403
- await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remaining)));
2381
+ async serverAction(id, payload) {
2382
+ const url = `${this.computeEndpoint}/servers/${encodeURIComponent(id)}/action`;
2383
+ try {
2384
+ await import_ky7.default.post(url, {
2385
+ headers: this.authHeaders(),
2386
+ json: payload,
2387
+ retry: 0,
2388
+ timeout: DEFAULT_TIMEOUT_MS3
2389
+ });
2390
+ } catch (err) {
2391
+ throw toNhnCloudCliError(err);
1404
2392
  }
1405
- const lastStatus = lastServer ? lastServer.status : "unknown";
1406
- throw new NhnCloudCliError(
1407
- `\uC778\uC2A4\uD134\uC2A4 ${id} \uAC00 ACTIVE \uAC00 \uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 (\uB9C8\uC9C0\uB9C9 \uC0C1\uD0DC: ${lastStatus}). --wait \uD0C0\uC784\uC544\uC6C3(${Math.round(opts.timeoutMs / 1e3)}\uCD08) \uCD08\uACFC.`,
1408
- EXIT_API_ERROR
1409
- );
1410
2393
  }
1411
- };
1412
-
1413
- // src/commands/instance/helpers.ts
1414
- async function resolveInstanceClient(opts) {
1415
- const profileName = await resolveProfileName(opts.profile);
1416
- const iaas = await getIaasCredential(profileName);
1417
- const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
1418
- const { tokenId, computeEndpoint } = await getIaasToken(profileName, effectiveIaas);
1419
- return { client: new InstanceClient(tokenId, computeEndpoint), profileName };
1420
- }
1421
-
1422
- // src/commands/instance/list.ts
1423
- function getIps(server) {
1424
- return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
1425
- }
1426
- var listCommand = new import_commander7.Command("list").description("\uC778\uC2A4\uD134\uC2A4 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
1427
- const opts = cmd.optsWithGlobals();
1428
- const { client } = await resolveInstanceClient(opts);
1429
- startSpinner("\uC778\uC2A4\uD134\uC2A4 \uBAA9\uB85D \uC870\uD68C \uC911...");
1430
- let servers;
1431
- try {
1432
- servers = await client.list();
1433
- } catch (err) {
1434
- stopSpinner(false);
1435
- throw err;
2394
+ /** 인스턴스를 시작한다 (SHUTOFF → ACTIVE). */
2395
+ async start(id) {
2396
+ return this.serverAction(id, { "os-start": null });
1436
2397
  }
1437
- stopSpinner(true);
1438
- output(opts, {
1439
- headers: ["id", "name", "status", "IPs", "flavor"],
1440
- rows: servers.map((s) => [s.id, s.name, s.status, getIps(s), s.flavor.id]),
1441
- raw: servers,
1442
- ids: servers.map((s) => s.id)
1443
- });
1444
- });
1445
-
1446
- // src/commands/instance/get.ts
1447
- var import_commander8 = require("commander");
1448
- function getIps2(server) {
1449
- return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
1450
- }
1451
- function getImageId(server) {
1452
- return typeof server.image === "object" ? server.image.id : "";
1453
- }
1454
- var getCommand = new import_commander8.Command("get").description("\uB2E8\uC77C \uC778\uC2A4\uD134\uC2A4 \uC0C1\uD0DC\uB97C \uC870\uD68C\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
1455
- const opts = cmd.optsWithGlobals();
1456
- const { client } = await resolveInstanceClient(opts);
1457
- startSpinner("\uC778\uC2A4\uD134\uC2A4 \uC870\uD68C \uC911...");
1458
- let server;
1459
- try {
1460
- server = await client.get(id);
1461
- } catch (err) {
1462
- stopSpinner(false);
1463
- throw err;
2398
+ /** 인스턴스를 정지한다 (ACTIVE/ERROR → SHUTOFF). */
2399
+ async stop(id) {
2400
+ return this.serverAction(id, { "os-stop": null });
1464
2401
  }
1465
- stopSpinner(true);
1466
- const rows = [
1467
- ["id", server.id],
1468
- ["name", server.name],
1469
- ["status", server.status],
1470
- ["IPs", getIps2(server)],
1471
- ["flavor", server.flavor.id],
1472
- ["image", getImageId(server)],
1473
- ["key_name", server.key_name ?? ""],
1474
- ["created", server.created],
1475
- ["updated", server.updated]
1476
- ];
1477
- output(opts, {
1478
- headers: ["field", "value"],
2402
+ /** 인스턴스를 재부팅한다. type 기본 SOFT, HARD 는 강제 전원 cycle. */
2403
+ async reboot(id, type = "SOFT") {
2404
+ return this.serverAction(id, { reboot: { type } });
2405
+ }
2406
+ /**
2407
+ * 인스턴스 타입(flavor)을 변경한다 (resize action).
2408
+ * POST /servers/{id}/action body { "resize": { "flavorRef": "<flavor-id>" } }, 202 무본문.
2409
+ * 사전 상태는 ACTIVE 또는 SHUTOFF (ACTIVE 면 NHN 측에서 중지 후 재시작).
2410
+ */
2411
+ async resize(id, flavorRef) {
2412
+ return this.serverAction(id, { resize: { flavorRef } });
2413
+ }
2414
+ /** resize 를 확정한다 (VERIFY_RESIZE → ACTIVE, 새 flavor 로 고정). */
2415
+ async confirmResize(id) {
2416
+ return this.serverAction(id, { confirmResize: null });
2417
+ }
2418
+ /** resize 를 롤백한다 (VERIFY_RESIZE → ACTIVE, 이전 flavor 로 복귀). */
2419
+ async revertResize(id) {
2420
+ return this.serverAction(id, { revertResize: null });
2421
+ }
2422
+ async listFlavors(params = {}) {
2423
+ const path = params.detail ? "/flavors/detail" : "/flavors";
2424
+ const url = `${this.computeEndpoint}${path}`;
2425
+ const searchParams = {};
2426
+ if (params.minDisk !== void 0) searchParams["minDisk"] = params.minDisk;
2427
+ if (params.minRam !== void 0) searchParams["minRam"] = params.minRam;
2428
+ try {
2429
+ const raw = await import_ky7.default.get(url, {
2430
+ headers: this.authHeaders(),
2431
+ searchParams,
2432
+ retry: 0,
2433
+ timeout: DEFAULT_TIMEOUT_MS3
2434
+ }).json();
2435
+ if (params.detail) {
2436
+ if (!isFlavorDetailsResponse(raw)) {
2437
+ throw new NhnCloudCliError(
2438
+ "instance flavors --detail \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 vcpus\xB7ram\xB7disk \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
2439
+ EXIT_API_ERROR
2440
+ );
2441
+ }
2442
+ return raw.flavors;
2443
+ }
2444
+ if (!isFlavorsResponse(raw)) {
2445
+ throw new NhnCloudCliError(
2446
+ "instance flavors \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 flavors \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
2447
+ EXIT_API_ERROR
2448
+ );
2449
+ }
2450
+ return raw.flavors;
2451
+ } catch (err) {
2452
+ throw toNhnCloudCliError(err);
2453
+ }
2454
+ }
2455
+ /**
2456
+ * 가용성 영역(availability zone) 목록을 조회한다 (GET /os-availability-zone).
2457
+ * zoneName·가용 여부(available)를 반환하며 페이지네이션·필터 없음.
2458
+ */
2459
+ async listAvailabilityZones() {
2460
+ const url = `${this.computeEndpoint}/os-availability-zone`;
2461
+ try {
2462
+ const raw = await import_ky7.default.get(url, {
2463
+ headers: this.authHeaders(),
2464
+ retry: 0,
2465
+ timeout: DEFAULT_TIMEOUT_MS3
2466
+ }).json();
2467
+ if (!isAvailabilityZonesResponse(raw)) {
2468
+ throw new NhnCloudCliError(
2469
+ "instance availability-zones \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 availabilityZoneInfo \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
2470
+ EXIT_API_ERROR
2471
+ );
2472
+ }
2473
+ return raw.availabilityZoneInfo;
2474
+ } catch (err) {
2475
+ throw toNhnCloudCliError(err);
2476
+ }
2477
+ }
2478
+ /** 키페어 목록을 조회한다 (GET /os-keypairs). 응답 원소의 한겹(keypair)을 풀어 반환. */
2479
+ async listKeypairs() {
2480
+ const url = `${this.computeEndpoint}/os-keypairs`;
2481
+ try {
2482
+ const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
2483
+ if (!isKeypairsResponse(raw)) {
2484
+ throw new NhnCloudCliError(
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.",
2486
+ EXIT_API_ERROR
2487
+ );
2488
+ }
2489
+ return raw.keypairs.map((e) => e.keypair);
2490
+ } catch (err) {
2491
+ throw toNhnCloudCliError(err);
2492
+ }
2493
+ }
2494
+ /** 단일 키페어를 조회한다 (GET /os-keypairs/{name}). */
2495
+ async getKeypair(name) {
2496
+ const url = `${this.computeEndpoint}/os-keypairs/${encodeURIComponent(name)}`;
2497
+ try {
2498
+ const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
2499
+ if (!isKeypairDetailResponse(raw)) {
2500
+ throw new NhnCloudCliError(
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.`,
2502
+ EXIT_API_ERROR
2503
+ );
2504
+ }
2505
+ return raw.keypair;
2506
+ } catch (err) {
2507
+ throw toNhnCloudCliError(err);
2508
+ }
2509
+ }
2510
+ /**
2511
+ * 키페어를 생성한다 (POST /os-keypairs).
2512
+ * publicKey 미지정이면 NHN 이 키쌍을 생성하고 응답 keypair 에 private_key 가 1회성으로 포함된다.
2513
+ * publicKey 지정이면 기존 공개키를 등록하고 private_key 는 응답에 없다.
2514
+ */
2515
+ async createKeypair(params) {
2516
+ const url = `${this.computeEndpoint}/os-keypairs`;
2517
+ const keypairBody = { name: params.name };
2518
+ if (params.publicKey !== void 0) {
2519
+ keypairBody["public_key"] = params.publicKey;
2520
+ }
2521
+ let raw;
2522
+ try {
2523
+ raw = await import_ky7.default.post(url, {
2524
+ headers: this.authHeaders(),
2525
+ json: { keypair: keypairBody },
2526
+ retry: 0,
2527
+ timeout: DEFAULT_TIMEOUT_MS3
2528
+ }).json();
2529
+ } catch (err) {
2530
+ throw toNhnCloudCliError(err);
2531
+ }
2532
+ if (!isCreateKeypairResponse(raw)) {
2533
+ throw new NhnCloudCliError(
2534
+ "instance keypair create \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 keypair \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
2535
+ EXIT_API_ERROR
2536
+ );
2537
+ }
2538
+ const kp = raw.keypair;
2539
+ return {
2540
+ name: kp.name,
2541
+ public_key: kp.public_key,
2542
+ fingerprint: kp.fingerprint,
2543
+ user_id: kp.user_id ?? "",
2544
+ // 빈 문자열은 정의되지 않은 것과 동일 취급 — 빈 키 파일 저장/빈 줄 출력 방지.
2545
+ private_key: kp.private_key !== void 0 && kp.private_key.length > 0 ? kp.private_key : void 0
2546
+ };
2547
+ }
2548
+ /** 키페어를 삭제한다 (DELETE /os-keypairs/{name}, 202/204 무응답). */
2549
+ async deleteKeypair(name) {
2550
+ const url = `${this.computeEndpoint}/os-keypairs/${encodeURIComponent(name)}`;
2551
+ try {
2552
+ await import_ky7.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 });
2553
+ } catch (err) {
2554
+ throw toNhnCloudCliError(err);
2555
+ }
2556
+ }
2557
+ /**
2558
+ * 이미지 목록을 조회한다 (GET /v2/images, Glance v2).
2559
+ * compute 와 다른 host(imageEndpoint)지만 같은 Keystone 토큰을 쓴다.
2560
+ * 한 페이지(기본 limit 25)만 반환한다 — next 가 있으면 호출부가 marker 로 이어 받는다.
2561
+ */
2562
+ async listImages(params = {}) {
2563
+ const url = `${this.imageEndpoint}/images`;
2564
+ const searchParams = {};
2565
+ if (params.limit !== void 0) searchParams["limit"] = params.limit;
2566
+ if (params.marker !== void 0) searchParams["marker"] = params.marker;
2567
+ if (params.name !== void 0) searchParams["name"] = params.name;
2568
+ if (params.visibility !== void 0) searchParams["visibility"] = params.visibility;
2569
+ if (params.owner !== void 0) searchParams["owner"] = params.owner;
2570
+ if (params.status !== void 0) searchParams["status"] = params.status;
2571
+ try {
2572
+ const raw = await import_ky7.default.get(url, {
2573
+ headers: this.authHeaders(),
2574
+ searchParams,
2575
+ retry: 0,
2576
+ timeout: DEFAULT_TIMEOUT_MS3
2577
+ }).json();
2578
+ if (!isImagesResponse(raw)) {
2579
+ throw new NhnCloudCliError(
2580
+ "instance images \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 images \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
2581
+ EXIT_API_ERROR
2582
+ );
2583
+ }
2584
+ return { images: raw.images, next: raw.next };
2585
+ } catch (err) {
2586
+ throw toNhnCloudCliError(err);
2587
+ }
2588
+ }
2589
+ /**
2590
+ * 인스턴스에 연결된 볼륨 목록을 조회한다 (GET .../os-volume_attachments).
2591
+ * Nova 표준 확장 — NHN Instance(Nova v2 호환, ADR-010). 실측 200 확인 (2026-06-11).
2592
+ */
2593
+ async listVolumeAttachments(serverId) {
2594
+ const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments`;
2595
+ try {
2596
+ const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
2597
+ if (!isVolumeAttachmentsResponse(raw)) {
2598
+ throw new NhnCloudCliError(
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.",
2600
+ EXIT_API_ERROR
2601
+ );
2602
+ }
2603
+ return raw.volumeAttachments;
2604
+ } catch (err) {
2605
+ throw toNhnCloudCliError(err);
2606
+ }
2607
+ }
2608
+ /**
2609
+ * 볼륨을 인스턴스에 연결한다 (POST .../os-volume_attachments).
2610
+ * 요청 body: { volumeAttachment: { volumeId } }. 실제 연결은 수동 QA 확정 (1-26).
2611
+ */
2612
+ async attachVolume(serverId, volumeId) {
2613
+ const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments`;
2614
+ try {
2615
+ const res = await import_ky7.default.post(url, {
2616
+ headers: this.authHeaders(),
2617
+ json: { volumeAttachment: { volumeId } },
2618
+ retry: 0,
2619
+ timeout: DEFAULT_TIMEOUT_MS3
2620
+ });
2621
+ if (res.status === 202 || res.headers.get("content-length") === "0") {
2622
+ return { id: volumeId, volumeId, serverId, device: "" };
2623
+ }
2624
+ const raw = await res.json();
2625
+ if (!isVolumeAttachmentResponse(raw)) {
2626
+ throw new NhnCloudCliError(
2627
+ "instance volume attach \uC751\uB2F5\uC5D0 volumeAttachment \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
2628
+ EXIT_API_ERROR
2629
+ );
2630
+ }
2631
+ return raw.volumeAttachment;
2632
+ } catch (err) {
2633
+ throw toNhnCloudCliError(err);
2634
+ }
2635
+ }
2636
+ /**
2637
+ * 볼륨 연결을 해제한다 (DELETE .../os-volume_attachments/{volumeId}, 202 무응답).
2638
+ * 실제 해제는 수동 QA 확정 (1-26).
2639
+ */
2640
+ async detachVolume(serverId, volumeId) {
2641
+ const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments/${encodeURIComponent(volumeId)}`;
2642
+ try {
2643
+ await import_ky7.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 });
2644
+ } catch (err) {
2645
+ throw toNhnCloudCliError(err);
2646
+ }
2647
+ }
2648
+ /**
2649
+ * 인스턴스가 ACTIVE 상태 + IP 1개 이상이 될 때까지 폴링한다.
2650
+ *
2651
+ * - status === "ACTIVE" + addresses 에 IP 1개 이상: 즉시 반환
2652
+ * - timeout 초과: 마지막 status 를 포함한 NhnCloudCliError(EXIT_API_ERROR)
2653
+ */
2654
+ async waitForActive(id, opts) {
2655
+ const intervalMs = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
2656
+ const deadline = Date.now() + opts.timeoutMs;
2657
+ let lastServer = null;
2658
+ while (Date.now() < deadline) {
2659
+ const server = await this.get(id);
2660
+ lastServer = server;
2661
+ if (server.status === "ACTIVE" && hasIpAddress(server)) {
2662
+ return server;
2663
+ }
2664
+ const remaining = deadline - Date.now();
2665
+ if (remaining <= 0) break;
2666
+ await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remaining)));
2667
+ }
2668
+ const lastStatus = lastServer ? lastServer.status : "unknown";
2669
+ throw new NhnCloudCliError(
2670
+ `\uC778\uC2A4\uD134\uC2A4 ${id} \uAC00 ACTIVE \uAC00 \uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 (\uB9C8\uC9C0\uB9C9 \uC0C1\uD0DC: ${lastStatus}). --wait \uD0C0\uC784\uC544\uC6C3(${Math.round(opts.timeoutMs / 1e3)}\uCD08) \uCD08\uACFC.`,
2671
+ EXIT_API_ERROR
2672
+ );
2673
+ }
2674
+ };
2675
+
2676
+ // src/commands/instance/helpers.ts
2677
+ async function resolveInstanceClient(opts) {
2678
+ const profileName = await resolveProfileName(opts.profile);
2679
+ const iaas = await getIaasCredential(profileName);
2680
+ const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
2681
+ const { tokenId, computeEndpoint, imageEndpoint } = await getIaasToken(profileName, effectiveIaas);
2682
+ return { client: new InstanceClient(tokenId, computeEndpoint, imageEndpoint), profileName };
2683
+ }
2684
+
2685
+ // src/commands/instance/list.ts
2686
+ function getIps(server) {
2687
+ return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
2688
+ }
2689
+ var listCommand = new import_commander13.Command("list").description("\uC778\uC2A4\uD134\uC2A4 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
2690
+ const opts = cmd.optsWithGlobals();
2691
+ const { client } = await resolveInstanceClient(opts);
2692
+ startSpinner("\uC778\uC2A4\uD134\uC2A4 \uBAA9\uB85D \uC870\uD68C \uC911...");
2693
+ let servers;
2694
+ try {
2695
+ servers = await client.list();
2696
+ } catch (err) {
2697
+ stopSpinner(false);
2698
+ throw err;
2699
+ }
2700
+ stopSpinner(true);
2701
+ output(opts, {
2702
+ headers: ["id", "name", "status", "IPs", "flavor"],
2703
+ rows: servers.map((s) => [s.id, s.name, s.status, getIps(s), s.flavor.id]),
2704
+ raw: servers,
2705
+ ids: servers.map((s) => s.id)
2706
+ });
2707
+ });
2708
+
2709
+ // src/commands/volume/list.ts
2710
+ var import_commander14 = require("commander");
2711
+
2712
+ // src/services/blockstorage/client.ts
2713
+ var import_ky8 = __toESM(require("ky"));
2714
+ var DEFAULT_TIMEOUT_MS4 = 3e4;
2715
+ function isVolume(val) {
2716
+ if (typeof val !== "object" || val === null) return false;
2717
+ const obj = val;
2718
+ return typeof obj["id"] === "string" && // Cinder 는 --name 미지정 시 null — null 인 볼륨 하나가 list 전체를 거부하지 않게 허용 (isImage 선례).
2719
+ (typeof obj["name"] === "string" || obj["name"] === null) && typeof obj["size"] === "number" && typeof obj["status"] === "string" && Array.isArray(obj["attachments"]);
2720
+ }
2721
+ function isVolumesResponse(val) {
2722
+ if (typeof val !== "object" || val === null) return false;
2723
+ const obj = val;
2724
+ return Array.isArray(obj["volumes"]) && obj["volumes"].every(isVolume);
2725
+ }
2726
+ function isVolumeResponse(val) {
2727
+ if (typeof val !== "object" || val === null) return false;
2728
+ const obj = val;
2729
+ return isVolume(obj["volume"]);
2730
+ }
2731
+ var BlockStorageClient = class {
2732
+ tokenId;
2733
+ endpoint;
2734
+ // blockStorageEndpoint (/v2/{tenantId} 까지 포함)
2735
+ constructor(tokenId, blockStorageEndpoint) {
2736
+ this.tokenId = tokenId;
2737
+ this.endpoint = blockStorageEndpoint;
2738
+ }
2739
+ authHeaders() {
2740
+ return { "X-Auth-Token": this.tokenId };
2741
+ }
2742
+ async list(params) {
2743
+ const url = `${this.endpoint}/volumes`;
2744
+ const searchParams = {};
2745
+ if (params?.sort !== void 0) searchParams["sort"] = params.sort;
2746
+ if (params?.limit !== void 0) searchParams["limit"] = params.limit;
2747
+ if (params?.offset !== void 0) searchParams["offset"] = params.offset;
2748
+ if (params?.marker !== void 0) searchParams["marker"] = params.marker;
2749
+ try {
2750
+ const raw = await import_ky8.default.get(url, { headers: this.authHeaders(), searchParams, retry: 0, timeout: DEFAULT_TIMEOUT_MS4 }).json();
2751
+ if (!isVolumesResponse(raw)) {
2752
+ throw new NhnCloudCliError(
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.",
2754
+ EXIT_API_ERROR
2755
+ );
2756
+ }
2757
+ return raw.volumes;
2758
+ } catch (err) {
2759
+ throw toNhnCloudCliError(err);
2760
+ }
2761
+ }
2762
+ async get(id) {
2763
+ const url = `${this.endpoint}/volumes/${encodeURIComponent(id)}`;
2764
+ try {
2765
+ const raw = await import_ky8.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS4 }).json();
2766
+ if (!isVolumeResponse(raw)) {
2767
+ throw new NhnCloudCliError(
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.",
2769
+ EXIT_API_ERROR
2770
+ );
2771
+ }
2772
+ return raw.volume;
2773
+ } catch (err) {
2774
+ throw toNhnCloudCliError(err);
2775
+ }
2776
+ }
2777
+ async create(params) {
2778
+ const url = `${this.endpoint}/volumes`;
2779
+ const volumeBody = { size: params.size };
2780
+ if (params.name !== void 0) volumeBody["name"] = params.name;
2781
+ if (params.description !== void 0) volumeBody["description"] = params.description;
2782
+ if (params.volume_type !== void 0) volumeBody["volume_type"] = params.volume_type;
2783
+ if (params.snapshot_id !== void 0) volumeBody["snapshot_id"] = params.snapshot_id;
2784
+ try {
2785
+ const raw = await import_ky8.default.post(url, {
2786
+ headers: this.authHeaders(),
2787
+ json: { volume: volumeBody },
2788
+ retry: 0,
2789
+ timeout: DEFAULT_TIMEOUT_MS4
2790
+ }).json();
2791
+ if (!isVolumeResponse(raw)) {
2792
+ throw new NhnCloudCliError(
2793
+ "volume create \uC751\uB2F5\uC5D0 volume \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
2794
+ EXIT_API_ERROR
2795
+ );
2796
+ }
2797
+ return raw.volume;
2798
+ } catch (err) {
2799
+ throw toNhnCloudCliError(err);
2800
+ }
2801
+ }
2802
+ };
2803
+
2804
+ // src/commands/volume/helpers.ts
2805
+ async function resolveVolumeClient(opts) {
2806
+ const profileName = await resolveProfileName(opts.profile);
2807
+ const iaas = await getIaasCredential(profileName);
2808
+ const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
2809
+ const { tokenId, blockStorageEndpoint } = await getIaasToken(profileName, effectiveIaas);
2810
+ return { client: new BlockStorageClient(tokenId, blockStorageEndpoint), profileName };
2811
+ }
2812
+
2813
+ // src/commands/volume/list.ts
2814
+ function parsePositiveInt4(value, flag) {
2815
+ if (!/^[1-9]\d*$/.test(value)) {
2816
+ throw new NhnCloudCliError(
2817
+ `${flag} \uB294 \uC591\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${JSON.stringify(value)}`,
2818
+ EXIT_PARAM_ERROR
2819
+ );
2820
+ }
2821
+ return Number(value);
2822
+ }
2823
+ var listCommand2 = new import_commander14.Command("list").description("\uBCFC\uB968 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").option("--sort <key:dir>", "\uC815\uB82C \uAE30\uC900 (\uC608: created_at:desc)").option("--limit <n>", "\uCD5C\uB300 \uBC18\uD658 \uAC1C\uC218").option("--offset <n>", "\uD398\uC774\uC9C0 \uC2DC\uC791 \uC624\uD504\uC14B").option("--marker <id>", "\uD398\uC774\uC9C0\uB124\uC774\uC158 \uB9C8\uCEE4 \uBCFC\uB968 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
2824
+ const opts = cmd.optsWithGlobals();
2825
+ const limitNum = opts.limit !== void 0 ? parsePositiveInt4(opts.limit, "--limit") : void 0;
2826
+ const offsetNum = opts.offset !== void 0 ? parsePositiveInt4(opts.offset, "--offset") : void 0;
2827
+ const { client } = await resolveVolumeClient(opts);
2828
+ startSpinner("\uBCFC\uB968 \uBAA9\uB85D \uC870\uD68C \uC911...");
2829
+ let volumes;
2830
+ try {
2831
+ volumes = await client.list({
2832
+ sort: opts.sort,
2833
+ limit: limitNum,
2834
+ offset: offsetNum,
2835
+ marker: opts.marker
2836
+ });
2837
+ } catch (err) {
2838
+ stopSpinner(false);
2839
+ throw err;
2840
+ }
2841
+ stopSpinner(true);
2842
+ output(opts, {
2843
+ headers: ["id", "name", "size", "status", "type"],
2844
+ rows: volumes.map((v) => [
2845
+ v.id,
2846
+ v.name ?? "",
2847
+ String(v.size),
2848
+ v.status,
2849
+ v.volume_type ?? ""
2850
+ ]),
2851
+ raw: volumes,
2852
+ ids: volumes.map((v) => v.id)
2853
+ });
2854
+ });
2855
+
2856
+ // src/commands/volume/get.ts
2857
+ var import_commander15 = require("commander");
2858
+ var getCommand = new import_commander15.Command("get").description("\uB2E8\uC77C \uBCFC\uB968 \uC0C1\uD0DC\uB97C \uC870\uD68C\uD55C\uB2E4").argument("<id>", "\uBCFC\uB968 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
2859
+ const opts = cmd.optsWithGlobals();
2860
+ const { client } = await resolveVolumeClient(opts);
2861
+ startSpinner("\uBCFC\uB968 \uC870\uD68C \uC911...");
2862
+ let volume;
2863
+ try {
2864
+ volume = await client.get(id);
2865
+ } catch (err) {
2866
+ stopSpinner(false);
2867
+ throw err;
2868
+ }
2869
+ stopSpinner(true);
2870
+ const attachmentSummary = Array.isArray(volume.attachments) ? volume.attachments.filter((a) => typeof a === "object" && a !== null).map((a) => String(a.server_id)).join(", ") : "";
2871
+ const rows = [
2872
+ ["id", volume.id],
2873
+ ["name", volume.name ?? ""],
2874
+ ["size", String(volume.size)],
2875
+ ["status", volume.status],
2876
+ ["volume_type", volume.volume_type ?? ""],
2877
+ ["created_at", volume.created_at],
2878
+ ["attachments", attachmentSummary]
2879
+ ];
2880
+ output(opts, {
2881
+ headers: ["field", "value"],
2882
+ rows,
2883
+ raw: volume,
2884
+ ids: [volume.id]
2885
+ });
2886
+ });
2887
+
2888
+ // src/commands/volume/create.ts
2889
+ var import_commander16 = require("commander");
2890
+ var import_chalk3 = __toESM(require("chalk"));
2891
+ var createCommand = new import_commander16.Command("create").description("\uBCFC\uB968\uC744 \uBC1C\uAE09\uD55C\uB2E4").requiredOption("--size <gb>", "\uBCFC\uB968 \uD06C\uAE30(GB) \u2014 \uD544\uC218").option("--name <name>", "\uBCFC\uB968 \uC774\uB984").option("--description <text>", "\uBCFC\uB968 \uC124\uBA85").option("--volume-type <type>", "\uBCFC\uB968 \uD0C0\uC785").option("--snapshot-id <id>", "\uC2A4\uB0C5\uC0F7 ID (\uC2A4\uB0C5\uC0F7\uC5D0\uC11C \uBCFC\uB968 \uC0DD\uC131)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
2892
+ const opts = cmd.optsWithGlobals();
2893
+ if (!/^[1-9]\d*$/.test(opts.size)) {
2894
+ throw new NhnCloudCliError(
2895
+ `--size \uB294 \uC591\uC758 \uC815\uC218(GB)\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${JSON.stringify(opts.size)}`,
2896
+ EXIT_PARAM_ERROR
2897
+ );
2898
+ }
2899
+ const sizeNum = Number(opts.size);
2900
+ const { client } = await resolveVolumeClient(opts);
2901
+ startSpinner("\uBCFC\uB968 \uBC1C\uAE09 \uC911...");
2902
+ let volume;
2903
+ try {
2904
+ volume = await client.create({
2905
+ size: sizeNum,
2906
+ name: opts.name,
2907
+ description: opts.description,
2908
+ volume_type: opts.volumeType,
2909
+ snapshot_id: opts.snapshotId
2910
+ });
2911
+ } catch (err) {
2912
+ stopSpinner(false);
2913
+ throw err;
2914
+ }
2915
+ stopSpinner(true);
2916
+ process.stderr.write(import_chalk3.default.green(`\uBCFC\uB968 \uBC1C\uAE09 \uC694\uCCAD \uC644\uB8CC (id: ${volume.id}, status: ${volume.status})
2917
+ `));
2918
+ const rows = [
2919
+ ["id", volume.id],
2920
+ ["name", volume.name ?? ""],
2921
+ ["size", String(volume.size)],
2922
+ ["status", volume.status],
2923
+ ["volume_type", volume.volume_type ?? ""],
2924
+ ["created_at", volume.created_at]
2925
+ ];
2926
+ output(opts, {
2927
+ headers: ["field", "value"],
2928
+ rows,
2929
+ raw: volume,
2930
+ ids: [volume.id]
2931
+ });
2932
+ });
2933
+
2934
+ // src/commands/network/list.ts
2935
+ var import_commander17 = require("commander");
2936
+
2937
+ // src/services/network/client.ts
2938
+ var import_ky9 = __toESM(require("ky"));
2939
+ var DEFAULT_TIMEOUT_MS5 = 3e4;
2940
+ function isVpc(val) {
2941
+ if (typeof val !== "object" || val === null) return false;
2942
+ const obj = val;
2943
+ return typeof obj["id"] === "string" && typeof obj["name"] === "string" && typeof obj["cidrv4"] === "string" && typeof obj["state"] === "string" && typeof obj["router:external"] === "boolean";
2944
+ }
2945
+ function isVpcsResponse(val) {
2946
+ if (typeof val !== "object" || val === null) return false;
2947
+ const obj = val;
2948
+ return Array.isArray(obj["vpcs"]) && obj["vpcs"].every(isVpc);
2949
+ }
2950
+ function isSubnet(val) {
2951
+ if (typeof val !== "object" || val === null) return false;
2952
+ const obj = val;
2953
+ return typeof obj["id"] === "string" && typeof obj["cidr"] === "string" && typeof obj["vpc_id"] === "string" && typeof obj["gateway"] === "string" && typeof obj["available_ip_count"] === "number";
2954
+ }
2955
+ function isSubnetsResponse(val) {
2956
+ if (typeof val !== "object" || val === null) return false;
2957
+ const obj = val;
2958
+ return Array.isArray(obj["vpcsubnets"]) && obj["vpcsubnets"].every(isSubnet);
2959
+ }
2960
+ function isFloatingIp(val) {
2961
+ if (typeof val !== "object" || val === null) return false;
2962
+ const obj = val;
2963
+ return typeof obj["id"] === "string" && typeof obj["floating_ip_address"] === "string" && typeof obj["status"] === "string" && typeof obj["floating_network_id"] === "string" && (obj["port_id"] === null || typeof obj["port_id"] === "string") && (obj["fixed_ip_address"] === null || typeof obj["fixed_ip_address"] === "string");
2964
+ }
2965
+ function isFloatingIpsResponse(val) {
2966
+ if (typeof val !== "object" || val === null) return false;
2967
+ const obj = val;
2968
+ return Array.isArray(obj["floatingips"]) && obj["floatingips"].every(isFloatingIp);
2969
+ }
2970
+ function isFloatingIpResponse(val) {
2971
+ if (typeof val !== "object" || val === null) return false;
2972
+ const obj = val;
2973
+ return isFloatingIp(obj["floatingip"]);
2974
+ }
2975
+ var NetworkClient = class {
2976
+ tokenId;
2977
+ networkEndpoint;
2978
+ constructor(tokenId, networkEndpoint) {
2979
+ this.tokenId = tokenId;
2980
+ this.networkEndpoint = networkEndpoint;
2981
+ }
2982
+ authHeaders() {
2983
+ return { "X-Auth-Token": this.tokenId };
2984
+ }
2985
+ /**
2986
+ * VPC 목록을 조회한다 (GET /v2.0/vpcs, NHN VPC).
2987
+ * instance 와 다른 host(networkEndpoint)지만 같은 Keystone 토큰을 쓴다.
2988
+ */
2989
+ async listVpcs() {
2990
+ const url = `${this.networkEndpoint}/vpcs`;
2991
+ try {
2992
+ const raw = await import_ky9.default.get(url, {
2993
+ headers: this.authHeaders(),
2994
+ retry: 0,
2995
+ timeout: DEFAULT_TIMEOUT_MS5
2996
+ }).json();
2997
+ if (!isVpcsResponse(raw)) {
2998
+ throw new NhnCloudCliError(
2999
+ "network list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 vpcs \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
3000
+ EXIT_API_ERROR
3001
+ );
3002
+ }
3003
+ return raw.vpcs;
3004
+ } catch (err) {
3005
+ throw toNhnCloudCliError(err);
3006
+ }
3007
+ }
3008
+ /**
3009
+ * 서브넷 목록을 조회한다 (GET /v2.0/vpcsubnets, NHN VPC).
3010
+ */
3011
+ async listSubnets() {
3012
+ const url = `${this.networkEndpoint}/vpcsubnets`;
3013
+ try {
3014
+ const raw = await import_ky9.default.get(url, {
3015
+ headers: this.authHeaders(),
3016
+ retry: 0,
3017
+ timeout: DEFAULT_TIMEOUT_MS5
3018
+ }).json();
3019
+ if (!isSubnetsResponse(raw)) {
3020
+ throw new NhnCloudCliError(
3021
+ "network subnet list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 vpcsubnets \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
3022
+ EXIT_API_ERROR
3023
+ );
3024
+ }
3025
+ return raw.vpcsubnets;
3026
+ } catch (err) {
3027
+ throw toNhnCloudCliError(err);
3028
+ }
3029
+ }
3030
+ /** Floating IP 목록을 조회한다 (GET /v2.0/floatingips). */
3031
+ async listFloatingIps() {
3032
+ const url = `${this.networkEndpoint}/floatingips`;
3033
+ try {
3034
+ const raw = await import_ky9.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS5 }).json();
3035
+ if (!isFloatingIpsResponse(raw)) {
3036
+ throw new NhnCloudCliError(
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.",
3038
+ EXIT_API_ERROR
3039
+ );
3040
+ }
3041
+ return raw.floatingips;
3042
+ } catch (err) {
3043
+ throw toNhnCloudCliError(err);
3044
+ }
3045
+ }
3046
+ /** Floating IP 를 발급한다 (POST /v2.0/floatingips). */
3047
+ async createFloatingIp(params) {
3048
+ const url = `${this.networkEndpoint}/floatingips`;
3049
+ try {
3050
+ const raw = await import_ky9.default.post(url, {
3051
+ headers: this.authHeaders(),
3052
+ json: { floatingip: { floating_network_id: params.floating_network_id } },
3053
+ retry: 0,
3054
+ timeout: DEFAULT_TIMEOUT_MS5
3055
+ }).json();
3056
+ if (!isFloatingIpResponse(raw)) {
3057
+ throw new NhnCloudCliError(
3058
+ "floatingip create \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 floatingip \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
3059
+ EXIT_API_ERROR
3060
+ );
3061
+ }
3062
+ return raw.floatingip;
3063
+ } catch (err) {
3064
+ throw toNhnCloudCliError(err);
3065
+ }
3066
+ }
3067
+ /** Floating IP 를 삭제한다 (DELETE /v2.0/floatingips/{id}, 무본문). */
3068
+ async deleteFloatingIp(id) {
3069
+ const url = `${this.networkEndpoint}/floatingips/${encodeURIComponent(id)}`;
3070
+ try {
3071
+ await import_ky9.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS5 });
3072
+ } catch (err) {
3073
+ throw toNhnCloudCliError(err);
3074
+ }
3075
+ }
3076
+ /**
3077
+ * 외부(external) VPC id 를 찾는다 — create 의 floating_network_id 기본 소스.
3078
+ * `router:external` 은 콜론 포함 리터럴 키 — bracket 접근 필수.
3079
+ * external VPC 가 둘 이상이면 첫 매칭을 반환한다.
3080
+ * 사용자는 `--network <id>` 로 명시 지정 가능하므로 create 의 stderr 에 그 사실을 안내한다.
3081
+ */
3082
+ async findExternalNetworkId() {
3083
+ const url = `${this.networkEndpoint}/vpcs`;
3084
+ try {
3085
+ const raw = await import_ky9.default.get(url, {
3086
+ headers: this.authHeaders(),
3087
+ searchParams: { "router:external": "true" },
3088
+ retry: 0,
3089
+ timeout: DEFAULT_TIMEOUT_MS5
3090
+ }).json();
3091
+ if (typeof raw !== "object" || raw === null) return null;
3092
+ const vpcs = raw["vpcs"];
3093
+ if (!Array.isArray(vpcs)) return null;
3094
+ for (const v of vpcs) {
3095
+ if (typeof v !== "object" || v === null) continue;
3096
+ const obj = v;
3097
+ if (obj["router:external"] === true && typeof obj["id"] === "string") {
3098
+ return obj["id"];
3099
+ }
3100
+ }
3101
+ return null;
3102
+ } catch (err) {
3103
+ throw toNhnCloudCliError(err);
3104
+ }
3105
+ }
3106
+ };
3107
+
3108
+ // src/commands/network/helpers.ts
3109
+ async function resolveNetworkClient(opts) {
3110
+ const profileName = await resolveProfileName(opts.profile);
3111
+ const iaas = await getIaasCredential(profileName);
3112
+ const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
3113
+ const { tokenId, networkEndpoint } = await getIaasToken(profileName, effectiveIaas);
3114
+ return { client: new NetworkClient(tokenId, networkEndpoint), profileName };
3115
+ }
3116
+
3117
+ // src/commands/network/list.ts
3118
+ var listCommand3 = new import_commander17.Command("list").description("VPC \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4 (\uC804\uCCB4 \uD544\uB4DC\uB294 --json)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
3119
+ const opts = cmd.optsWithGlobals();
3120
+ const { client } = await resolveNetworkClient(opts);
3121
+ startSpinner("VPC \uBAA9\uB85D \uC870\uD68C \uC911...");
3122
+ let vpcs;
3123
+ try {
3124
+ vpcs = await client.listVpcs();
3125
+ } catch (err) {
3126
+ stopSpinner(false);
3127
+ throw err;
3128
+ }
3129
+ stopSpinner(true);
3130
+ output(opts, {
3131
+ headers: ["id", "name", "cidrv4", "state", "external"],
3132
+ rows: vpcs.map((v) => [v.id, v.name, v.cidrv4, v.state, String(v["router:external"])]),
3133
+ raw: vpcs,
3134
+ ids: vpcs.map((v) => v.id)
3135
+ });
3136
+ });
3137
+
3138
+ // src/commands/network/subnet.ts
3139
+ var import_commander18 = require("commander");
3140
+ var subnetListCommand = new import_commander18.Command("list").description("\uC11C\uBE0C\uB137 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4 (\uC804\uCCB4 \uD544\uB4DC\uB294 --json)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
3141
+ const opts = cmd.optsWithGlobals();
3142
+ const { client } = await resolveNetworkClient(opts);
3143
+ startSpinner("\uC11C\uBE0C\uB137 \uBAA9\uB85D \uC870\uD68C \uC911...");
3144
+ let subnets;
3145
+ try {
3146
+ subnets = await client.listSubnets();
3147
+ } catch (err) {
3148
+ stopSpinner(false);
3149
+ throw err;
3150
+ }
3151
+ stopSpinner(true);
3152
+ output(opts, {
3153
+ headers: ["id", "cidr", "vpc_id", "gateway", "available_ip"],
3154
+ rows: subnets.map((s) => [
3155
+ s.id,
3156
+ s.cidr,
3157
+ s.vpc_id,
3158
+ s.gateway,
3159
+ String(s.available_ip_count)
3160
+ ]),
3161
+ raw: subnets,
3162
+ ids: subnets.map((s) => s.id)
3163
+ });
3164
+ });
3165
+ var subnetCommand = new import_commander18.Command("subnet").description("\uC11C\uBE0C\uB137 \uAD00\uB828 \uBA85\uB839").addCommand(subnetListCommand);
3166
+
3167
+ // src/commands/floatingip/list.ts
3168
+ var import_commander19 = require("commander");
3169
+ var listCommand4 = new import_commander19.Command("list").description("Floating IP \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
3170
+ const opts = cmd.optsWithGlobals();
3171
+ const { client } = await resolveNetworkClient(opts);
3172
+ startSpinner("Floating IP \uBAA9\uB85D \uC870\uD68C \uC911...");
3173
+ let fips;
3174
+ try {
3175
+ fips = await client.listFloatingIps();
3176
+ } catch (err) {
3177
+ stopSpinner(false);
3178
+ throw err;
3179
+ }
3180
+ stopSpinner(true);
3181
+ output(opts, {
3182
+ headers: ["id", "floating_ip_address", "status", "port_id", "fixed_ip_address"],
3183
+ rows: fips.map((f) => [
3184
+ f.id,
3185
+ f.floating_ip_address,
3186
+ f.status,
3187
+ f.port_id ?? "-",
3188
+ f.fixed_ip_address ?? "-"
3189
+ ]),
3190
+ raw: fips,
3191
+ ids: fips.map((f) => f.id)
3192
+ });
3193
+ });
3194
+
3195
+ // src/commands/floatingip/create.ts
3196
+ var import_commander20 = require("commander");
3197
+ var import_chalk4 = __toESM(require("chalk"));
3198
+ var createCommand2 = new import_commander20.Command("create").description("Floating IP \uB97C \uBC1C\uAE09\uD55C\uB2E4 (--network \uBBF8\uC9C0\uC815 \uC2DC \uC678\uBD80 VPC \uC790\uB3D9 \uC870\uD68C)").option("--network <id>", "\uC678\uBD80 \uB124\uD2B8\uC6CC\uD06C(VPC) id (\uBBF8\uC9C0\uC815 \uC2DC router:external=true \uC790\uB3D9 \uC870\uD68C)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
3199
+ const opts = cmd.optsWithGlobals();
3200
+ const { client } = await resolveNetworkClient(opts);
3201
+ let networkId = opts.network;
3202
+ if (networkId === void 0) {
3203
+ startSpinner("\uC678\uBD80 \uB124\uD2B8\uC6CC\uD06C \uC870\uD68C \uC911...");
3204
+ try {
3205
+ const found = await client.findExternalNetworkId();
3206
+ if (found === null) {
3207
+ throw new NhnCloudCliError(
3208
+ "\uC678\uBD80 \uB124\uD2B8\uC6CC\uD06C(router:external=true)\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. --network <id> \uB85C \uC9C1\uC811 \uC9C0\uC815\uD558\uC138\uC694.",
3209
+ EXIT_PARAM_ERROR
3210
+ );
3211
+ }
3212
+ networkId = found;
3213
+ } catch (err) {
3214
+ stopSpinner(false);
3215
+ throw err;
3216
+ }
3217
+ stopSpinner(true);
3218
+ }
3219
+ startSpinner(`Floating IP \uBC1C\uAE09 \uC911... (network: ${networkId})`);
3220
+ let fip;
3221
+ try {
3222
+ fip = await client.createFloatingIp({ floating_network_id: networkId });
3223
+ } catch (err) {
3224
+ stopSpinner(false);
3225
+ throw err;
3226
+ }
3227
+ stopSpinner(true);
3228
+ process.stderr.write(
3229
+ import_chalk4.default.green(`\u2713 Floating IP "${fip.floating_ip_address}" \uB97C \uBC1C\uAE09\uD588\uC2B5\uB2C8\uB2E4 (id: ${fip.id}).
3230
+ `)
3231
+ );
3232
+ output(opts, {
3233
+ headers: ["id", "floating_ip_address", "status", "floating_network_id"],
3234
+ rows: [[fip.id, fip.floating_ip_address, fip.status, fip.floating_network_id]],
3235
+ raw: fip,
3236
+ ids: [fip.id]
3237
+ });
3238
+ });
3239
+
3240
+ // src/commands/floatingip/delete.ts
3241
+ var import_commander21 = require("commander");
3242
+ var import_chalk5 = __toESM(require("chalk"));
3243
+ var deleteCommand = new import_commander21.Command("delete").description("Floating IP \uB97C \uC0AD\uC81C\uD55C\uB2E4").argument("<id>", "Floating IP ID").option("--yes", "\uD655\uC778 \uD504\uB86C\uD504\uD2B8 \uC0DD\uB7B5 (CI/\uBE44\uB300\uD654\uD615 \uD544\uC218)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
3244
+ const opts = cmd.optsWithGlobals();
3245
+ const isTTY = process.stdin.isTTY;
3246
+ if (!isTTY && !opts.yes) {
3247
+ throw new NhnCloudCliError(
3248
+ "\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C Floating IP \uC0AD\uC81C\uB294 --yes \uD50C\uB798\uADF8\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
3249
+ EXIT_PARAM_ERROR
3250
+ );
3251
+ }
3252
+ if (isTTY && !opts.yes) {
3253
+ const { confirm } = await import("@inquirer/prompts");
3254
+ const ok = await confirm({
3255
+ message: `Floating IP "${id}" \uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`,
3256
+ default: false
3257
+ });
3258
+ if (!ok) {
3259
+ process.stderr.write(import_chalk5.default.yellow("\uC0AD\uC81C\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
3260
+ return;
3261
+ }
3262
+ }
3263
+ const { client } = await resolveNetworkClient(opts);
3264
+ startSpinner(`Floating IP \uC0AD\uC81C \uC911... (id: ${id})`);
3265
+ try {
3266
+ await client.deleteFloatingIp(id);
3267
+ } catch (err) {
3268
+ stopSpinner(false);
3269
+ throw err;
3270
+ }
3271
+ stopSpinner(true);
3272
+ process.stderr.write(import_chalk5.default.green(`\u2713 Floating IP "${id}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
3273
+ `));
3274
+ });
3275
+
3276
+ // src/commands/instance/flavors.ts
3277
+ var import_commander22 = require("commander");
3278
+ function parseNonNegInt(value, flag) {
3279
+ if (value === void 0) return void 0;
3280
+ const n = Number(value);
3281
+ if (!Number.isInteger(n) || n < 0) {
3282
+ throw new NhnCloudCliError(`${flag} \uB294 0 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${value}).`, EXIT_PARAM_ERROR);
3283
+ }
3284
+ return n;
3285
+ }
3286
+ var flavorsCommand = new import_commander22.Command("flavors").description("\uC778\uC2A4\uD134\uC2A4 \uD0C0\uC785(flavor)\uC744 \uC870\uD68C\uD55C\uB2E4 (\uAE30\uBCF8 id\xB7name, --detail \uB85C \uC2A4\uD399, \uC804\uCCB4 \uD544\uB4DC\uB294 --json)").option("--detail", "vcpus\xB7ram\xB7disk \uB4F1 \uC2A4\uD399 \uD3EC\uD568 (GET /flavors/detail)").option("--min-disk <gb>", "\uCD5C\uC18C \uBE14\uB85D \uC2A4\uD1A0\uB9AC\uC9C0 \uD06C\uAE30(GB) \uC774\uC0C1\uB9CC \uD544\uD130").option("--min-ram <mb>", "\uCD5C\uC18C RAM \uD06C\uAE30(MB) \uC774\uC0C1\uB9CC \uD544\uD130").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
3287
+ const opts = cmd.optsWithGlobals();
3288
+ const minDisk = parseNonNegInt(opts.minDisk, "--min-disk");
3289
+ const minRam = parseNonNegInt(opts.minRam, "--min-ram");
3290
+ const { client } = await resolveInstanceClient(opts);
3291
+ startSpinner("\uC778\uC2A4\uD134\uC2A4 \uD0C0\uC785 \uC870\uD68C \uC911...");
3292
+ try {
3293
+ if (opts.detail) {
3294
+ const flavors = await client.listFlavors({ detail: true, minDisk, minRam });
3295
+ stopSpinner(true);
3296
+ printFlavors(opts, flavors);
3297
+ } else {
3298
+ const flavors = await client.listFlavors({ minDisk, minRam });
3299
+ stopSpinner(true);
3300
+ printFlavors(opts, flavors);
3301
+ }
3302
+ } catch (err) {
3303
+ stopSpinner(false);
3304
+ throw err;
3305
+ }
3306
+ });
3307
+ function printFlavors(opts, flavors) {
3308
+ if (isDetailList(flavors)) {
3309
+ output(opts, {
3310
+ headers: ["id", "name", "vcpus", "ram(MB)", "disk(GB)"],
3311
+ rows: flavors.map((f) => [f.id, f.name, String(f.vcpus), String(f.ram), String(f.disk)]),
3312
+ raw: flavors,
3313
+ ids: flavors.map((f) => f.id)
3314
+ });
3315
+ } else {
3316
+ output(opts, {
3317
+ headers: ["id", "name"],
3318
+ rows: flavors.map((f) => [f.id, f.name]),
3319
+ raw: flavors,
3320
+ ids: flavors.map((f) => f.id)
3321
+ });
3322
+ }
3323
+ }
3324
+ function isDetailList(flavors) {
3325
+ return flavors.length > 0 && "vcpus" in flavors[0];
3326
+ }
3327
+
3328
+ // src/commands/instance/availability-zones.ts
3329
+ var import_commander23 = require("commander");
3330
+ var availabilityZonesCommand = new import_commander23.Command("availability-zones").description("\uAC00\uC6A9\uC131 \uC601\uC5ED(availability zone) \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4 (zoneName\xB7available)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
3331
+ const opts = cmd.optsWithGlobals();
3332
+ const { client } = await resolveInstanceClient(opts);
3333
+ startSpinner("\uAC00\uC6A9\uC131 \uC601\uC5ED \uC870\uD68C \uC911...");
3334
+ let zones;
3335
+ try {
3336
+ zones = await client.listAvailabilityZones();
3337
+ } catch (err) {
3338
+ stopSpinner(false);
3339
+ throw err;
3340
+ }
3341
+ stopSpinner(true);
3342
+ output(opts, {
3343
+ headers: ["zoneName", "available"],
3344
+ rows: zones.map((z) => [z.zoneName, String(z.zoneState.available)]),
3345
+ raw: zones,
3346
+ ids: zones.map((z) => z.zoneName)
3347
+ });
3348
+ });
3349
+
3350
+ // src/commands/instance/get.ts
3351
+ var import_commander24 = require("commander");
3352
+ function getIps2(server) {
3353
+ return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
3354
+ }
3355
+ function getImageId(server) {
3356
+ return typeof server.image === "object" ? server.image.id : "";
3357
+ }
3358
+ var getCommand2 = new import_commander24.Command("get").description("\uB2E8\uC77C \uC778\uC2A4\uD134\uC2A4 \uC0C1\uD0DC\uB97C \uC870\uD68C\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
3359
+ const opts = cmd.optsWithGlobals();
3360
+ const { client } = await resolveInstanceClient(opts);
3361
+ startSpinner("\uC778\uC2A4\uD134\uC2A4 \uC870\uD68C \uC911...");
3362
+ let server;
3363
+ try {
3364
+ server = await client.get(id);
3365
+ } catch (err) {
3366
+ stopSpinner(false);
3367
+ throw err;
3368
+ }
3369
+ stopSpinner(true);
3370
+ const rows = [
3371
+ ["id", server.id],
3372
+ ["name", server.name],
3373
+ ["status", server.status],
3374
+ ["IPs", getIps2(server)],
3375
+ ["flavor", server.flavor.id],
3376
+ ["image", getImageId(server)],
3377
+ ["key_name", server.key_name ?? ""],
3378
+ ["created", server.created],
3379
+ ["updated", server.updated]
3380
+ ];
3381
+ output(opts, {
3382
+ headers: ["field", "value"],
1479
3383
  rows,
1480
3384
  raw: server,
1481
3385
  ids: [server.id]
@@ -1483,9 +3387,9 @@ var getCommand = new import_commander8.Command("get").description("\uB2E8\uC77C
1483
3387
  });
1484
3388
 
1485
3389
  // src/commands/instance/create.ts
1486
- var import_commander9 = require("commander");
1487
- var import_node_fs = require("fs");
1488
- var import_chalk2 = __toESM(require("chalk"));
3390
+ var import_commander25 = require("commander");
3391
+ var import_node_fs5 = require("fs");
3392
+ var import_chalk6 = __toESM(require("chalk"));
1489
3393
  function getFirstIp(server) {
1490
3394
  for (const list of Object.values(server.addresses)) {
1491
3395
  for (const addr of list) {
@@ -1500,7 +3404,7 @@ function getIps3(server) {
1500
3404
  function getImageId2(server) {
1501
3405
  return typeof server.image === "object" ? server.image.id : "";
1502
3406
  }
1503
- var createCommand = new import_commander9.Command("create").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC0DD\uC131\uD55C\uB2E4").requiredOption("--name <name>", "\uC778\uC2A4\uD134\uC2A4 \uC774\uB984").requiredOption("--flavor <id>", "flavor ID").requiredOption("--image <id>", "\uC774\uBBF8\uC9C0 ID").requiredOption("--network <uuid>", "\uB124\uD2B8\uC6CC\uD06C UUID (\uC5EC\uB7EC \uAC1C: \uBC18\uBCF5 \uC9C0\uC815)", (v, prev) => [...prev, v], []).option("--boot-volume-size <gb>", "boot-from-volume root \uBCFC\uB968 \uD06C\uAE30 (GB). GPU(g2) \uB4F1 boot-from-volume \uD544\uC218 flavor \uC5D0 \uC9C0\uC815").option("--key-name <name>", "\uD0A4\uD398\uC5B4 \uC774\uB984").option("--security-group <name>", "\uBCF4\uC548 \uADF8\uB8F9 \uC774\uB984 (\uC5EC\uB7EC \uAC1C: \uBC18\uBCF5 \uC9C0\uC815)", (v, prev) => [...prev, v], []).option("--ephemeral-disk-size <gb>", "\uC784\uC2DC \uB514\uC2A4\uD06C \uD06C\uAE30 (GB, NHN \uD655\uC7A5)").option("--protect", "\uC0AD\uC81C \uBC29\uC9C0 \uC124\uC815 (NHN \uD655\uC7A5)").option("--user-data <path>", "cloud-init user-data \uD30C\uC77C \uACBD\uB85C (base64 \uC778\uCF54\uB529\uD574 \uC8FC\uC785, \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8 \uD55C\uB3C4)").option("--wait", "ACTIVE \uC0C1\uD0DC\uAC00 \uB420 \uB54C\uAE4C\uC9C0 \uB300\uAE30").option("--timeout <sec>", "wait \uD0C0\uC784\uC544\uC6C3 (\uCD08, \uAE30\uBCF8 300)", "300").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
3407
+ var createCommand3 = new import_commander25.Command("create").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC0DD\uC131\uD55C\uB2E4").requiredOption("--name <name>", "\uC778\uC2A4\uD134\uC2A4 \uC774\uB984").requiredOption("--flavor <id>", "flavor ID").requiredOption("--image <id>", "\uC774\uBBF8\uC9C0 ID").requiredOption("--network <uuid>", "\uB124\uD2B8\uC6CC\uD06C UUID (\uC5EC\uB7EC \uAC1C: \uBC18\uBCF5 \uC9C0\uC815)", (v, prev) => [...prev, v], []).option("--boot-volume-size <gb>", "boot-from-volume root \uBCFC\uB968 \uD06C\uAE30 (GB). GPU(g2) \uB4F1 boot-from-volume \uD544\uC218 flavor \uC5D0 \uC9C0\uC815").option("--key-name <name>", "\uD0A4\uD398\uC5B4 \uC774\uB984").option("--security-group <name>", "\uBCF4\uC548 \uADF8\uB8F9 \uC774\uB984 (\uC5EC\uB7EC \uAC1C: \uBC18\uBCF5 \uC9C0\uC815)", (v, prev) => [...prev, v], []).option("--ephemeral-disk-size <gb>", "\uC784\uC2DC \uB514\uC2A4\uD06C \uD06C\uAE30 (GB, NHN \uD655\uC7A5)").option("--protect", "\uC0AD\uC81C \uBC29\uC9C0 \uC124\uC815 (NHN \uD655\uC7A5)").option("--user-data <path>", "cloud-init user-data \uD30C\uC77C \uACBD\uB85C (base64 \uC778\uCF54\uB529\uD574 \uC8FC\uC785, \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8 \uD55C\uB3C4)").option("--wait", "ACTIVE \uC0C1\uD0DC\uAC00 \uB420 \uB54C\uAE4C\uC9C0 \uB300\uAE30").option("--timeout <sec>", "wait \uD0C0\uC784\uC544\uC6C3 (\uCD08, \uAE30\uBCF8 300)", "300").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
1504
3408
  const opts = cmd.optsWithGlobals();
1505
3409
  const networks = opts.network ?? [];
1506
3410
  if (networks.length === 0) {
@@ -1510,7 +3414,7 @@ var createCommand = new import_commander9.Command("create").description("\uC778\
1510
3414
  if (opts.userData !== void 0) {
1511
3415
  let stat;
1512
3416
  try {
1513
- stat = (0, import_node_fs.statSync)(opts.userData);
3417
+ stat = (0, import_node_fs5.statSync)(opts.userData);
1514
3418
  } catch (e) {
1515
3419
  const reason = e.code ?? (e instanceof Error ? e.message : String(e));
1516
3420
  throw new NhnCloudCliError(
@@ -1520,149 +3424,859 @@ var createCommand = new import_commander9.Command("create").description("\uC778\
1520
3424
  }
1521
3425
  if (!stat.isFile()) {
1522
3426
  throw new NhnCloudCliError(
1523
- `--user-data \uAC00 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${opts.userData}`,
3427
+ `--user-data \uAC00 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${opts.userData}`,
3428
+ EXIT_PARAM_ERROR
3429
+ );
3430
+ }
3431
+ if (stat.size > 49149) {
3432
+ throw new NhnCloudCliError(
3433
+ `--user-data \uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${stat.size} \uBC14\uC774\uD2B8). base64 \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8 \uD55C\uB3C4\uB77C raw 49149 \uBC14\uC774\uD2B8\uAE4C\uC9C0\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4.`,
3434
+ EXIT_PARAM_ERROR
3435
+ );
3436
+ }
3437
+ const raw = (0, import_node_fs5.readFileSync)(opts.userData);
3438
+ userDataBase64 = raw.toString("base64");
3439
+ if (userDataBase64.length > 65535) {
3440
+ throw new NhnCloudCliError(
3441
+ `--user-data \uAC00 base64 \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4 (${userDataBase64.length} \uBC14\uC774\uD2B8). cloud-init \uB0B4\uC6A9\uC744 \uC904\uC774\uC138\uC694.`,
3442
+ EXIT_PARAM_ERROR
3443
+ );
3444
+ }
3445
+ }
3446
+ const timeoutMs = parseInt(opts.timeout ?? "300", 10) * 1e3;
3447
+ const { client } = await resolveInstanceClient(opts);
3448
+ startSpinner("\uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131 \uC911...");
3449
+ let server;
3450
+ try {
3451
+ server = await client.create({
3452
+ name: opts.name,
3453
+ flavorRef: opts.flavor,
3454
+ imageRef: opts.image,
3455
+ networks,
3456
+ bootVolumeSize: opts.bootVolumeSize !== void 0 ? parseInt(opts.bootVolumeSize, 10) : void 0,
3457
+ keyName: opts.keyName,
3458
+ securityGroups: opts.securityGroup && opts.securityGroup.length > 0 ? opts.securityGroup : void 0,
3459
+ ephemeralDiskSize: opts.ephemeralDiskSize !== void 0 ? parseInt(opts.ephemeralDiskSize, 10) : void 0,
3460
+ protect: opts.protect,
3461
+ userDataBase64
3462
+ });
3463
+ } catch (err) {
3464
+ stopSpinner(false);
3465
+ throw err;
3466
+ }
3467
+ stopSpinner(true);
3468
+ if (opts.wait) {
3469
+ startSpinner(`ACTIVE \uB300\uAE30 \uC911... (id: ${server.id})`);
3470
+ try {
3471
+ server = await client.waitForActive(server.id, { timeoutMs });
3472
+ } catch (err) {
3473
+ stopSpinner(false);
3474
+ throw err;
3475
+ }
3476
+ stopSpinner(true, `ACTIVE \uD655\uC778 (id: ${server.id})`);
3477
+ }
3478
+ if (opts.quiet && opts.wait) {
3479
+ const ip = getFirstIp(server);
3480
+ if (ip) process.stdout.write(ip + "\n");
3481
+ return;
3482
+ }
3483
+ if (opts.wait) {
3484
+ process.stderr.write(import_chalk6.default.green(` IP: ${getIps3(server)}
3485
+ `));
3486
+ }
3487
+ const rows = [
3488
+ ["id", server.id],
3489
+ ["name", server.name],
3490
+ ["status", server.status],
3491
+ ["IPs", getIps3(server)],
3492
+ ["flavor", server.flavor.id],
3493
+ ["image", getImageId2(server)]
3494
+ ];
3495
+ output(opts, {
3496
+ headers: ["field", "value"],
3497
+ rows,
3498
+ raw: server,
3499
+ ids: [server.id]
3500
+ });
3501
+ });
3502
+
3503
+ // src/commands/instance/delete.ts
3504
+ var import_commander26 = require("commander");
3505
+ var import_chalk7 = __toESM(require("chalk"));
3506
+ var deleteCommand2 = new import_commander26.Command("delete").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC0AD\uC81C\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--yes", "\uD655\uC778 \uD504\uB86C\uD504\uD2B8 \uC0DD\uB7B5 (CI/\uBE44\uB300\uD654\uD615 \uD544\uC218)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
3507
+ const opts = cmd.optsWithGlobals();
3508
+ const isTTY = process.stdin.isTTY;
3509
+ if (!isTTY && !opts.yes) {
3510
+ throw new NhnCloudCliError(
3511
+ "\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C \uC778\uC2A4\uD134\uC2A4 \uC0AD\uC81C\uB294 --yes \uD50C\uB798\uADF8\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
3512
+ EXIT_PARAM_ERROR
3513
+ );
3514
+ }
3515
+ if (isTTY && !opts.yes) {
3516
+ const { confirm } = await import("@inquirer/prompts");
3517
+ const ok = await confirm({
3518
+ message: `\uC778\uC2A4\uD134\uC2A4 "${id}" \uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`,
3519
+ default: false
3520
+ });
3521
+ if (!ok) {
3522
+ process.stderr.write(import_chalk7.default.yellow("\uC0AD\uC81C\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
3523
+ return;
3524
+ }
3525
+ }
3526
+ const { client } = await resolveInstanceClient(opts);
3527
+ startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC0AD\uC81C \uC911... (id: ${id})`);
3528
+ try {
3529
+ await client.delete(id);
3530
+ } catch (err) {
3531
+ stopSpinner(false);
3532
+ throw err;
3533
+ }
3534
+ stopSpinner(true);
3535
+ process.stderr.write(import_chalk7.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
3536
+ `));
3537
+ });
3538
+
3539
+ // src/commands/instance/power.ts
3540
+ var import_commander27 = require("commander");
3541
+ var import_chalk8 = __toESM(require("chalk"));
3542
+ var startCommand = new import_commander27.Command("start").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC2DC\uC791\uD55C\uB2E4 (SHUTOFF \u2192 ACTIVE)").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
3543
+ const opts = cmd.optsWithGlobals();
3544
+ const { client } = await resolveInstanceClient(opts);
3545
+ startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC2DC\uC791 \uC911... (id: ${id})`);
3546
+ try {
3547
+ await client.start(id);
3548
+ } catch (err) {
3549
+ stopSpinner(false);
3550
+ throw err;
3551
+ }
3552
+ stopSpinner(true);
3553
+ process.stderr.write(import_chalk8.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uC2DC\uC791\uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\u2192 ACTIVE).
3554
+ `));
3555
+ });
3556
+ var stopCommand = new import_commander27.Command("stop").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC815\uC9C0\uD55C\uB2E4 (ACTIVE/ERROR \u2192 SHUTOFF)").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
3557
+ const opts = cmd.optsWithGlobals();
3558
+ const { client } = await resolveInstanceClient(opts);
3559
+ startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC815\uC9C0 \uC911... (id: ${id})`);
3560
+ try {
3561
+ await client.stop(id);
3562
+ } catch (err) {
3563
+ stopSpinner(false);
3564
+ throw err;
3565
+ }
3566
+ stopSpinner(true);
3567
+ process.stderr.write(import_chalk8.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uC815\uC9C0\uB97C \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\u2192 SHUTOFF).
3568
+ `));
3569
+ });
3570
+ var rebootCommand = new import_commander27.Command("reboot").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC7AC\uBD80\uD305\uD55C\uB2E4 (\uAE30\uBCF8 SOFT, --hard \uB85C HARD)").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--hard", "HARD \uC7AC\uBD80\uD305 (\uAC15\uC81C \uC804\uC6D0 cycle, \uAE30\uBCF8\uC740 SOFT)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
3571
+ const opts = cmd.optsWithGlobals();
3572
+ const { client } = await resolveInstanceClient(opts);
3573
+ const type = opts.hard ? "HARD" : "SOFT";
3574
+ startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC7AC\uBD80\uD305 \uC911... (id: ${id}, ${type})`);
3575
+ try {
3576
+ await client.reboot(id, type);
3577
+ } catch (err) {
3578
+ stopSpinner(false);
3579
+ throw err;
3580
+ }
3581
+ stopSpinner(true);
3582
+ process.stderr.write(import_chalk8.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" ${type} \uC7AC\uBD80\uD305\uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4.
3583
+ `));
3584
+ });
3585
+
3586
+ // src/commands/instance/resize.ts
3587
+ var import_commander28 = require("commander");
3588
+ var import_chalk9 = __toESM(require("chalk"));
3589
+ var resizeCommand = new import_commander28.Command("resize").description("\uC778\uC2A4\uD134\uC2A4 \uD0C0\uC785(flavor)\uC744 \uBCC0\uACBD\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").requiredOption("--flavor <id>", "\uBCC0\uACBD\uD560 flavor ID (instance flavors \uB85C \uD6C4\uBCF4 \uC870\uD68C)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
3590
+ const opts = cmd.optsWithGlobals();
3591
+ const { client } = await resolveInstanceClient(opts);
3592
+ startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uD0C0\uC785 \uBCC0\uACBD \uC911... (id: ${id})`);
3593
+ try {
3594
+ await client.resize(id, opts.flavor);
3595
+ } catch (err) {
3596
+ stopSpinner(false);
3597
+ throw err;
3598
+ }
3599
+ stopSpinner(true);
3600
+ process.stderr.write(
3601
+ import_chalk9.default.green(
3602
+ `\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uD0C0\uC785 \uBCC0\uACBD(flavor: ${opts.flavor}) \uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\u2192 VERIFY_RESIZE, instance get \uC73C\uB85C \uD655\uC778 \uD6C4 resize-confirm/resize-revert).
3603
+ `
3604
+ )
3605
+ );
3606
+ });
3607
+ var resizeConfirmCommand = new import_commander28.Command("resize-confirm").description("resize \uB97C \uD655\uC815\uD55C\uB2E4 (VERIFY_RESIZE \u2192 ACTIVE, \uC0C8 flavor \uB85C \uACE0\uC815)").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
3608
+ const opts = cmd.optsWithGlobals();
3609
+ const { client } = await resolveInstanceClient(opts);
3610
+ startSpinner(`resize \uD655\uC815 \uC911... (id: ${id})`);
3611
+ try {
3612
+ await client.confirmResize(id);
3613
+ } catch (err) {
3614
+ stopSpinner(false);
3615
+ throw err;
3616
+ }
3617
+ stopSpinner(true);
3618
+ process.stderr.write(import_chalk9.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" resize \uD655\uC815\uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\u2192 ACTIVE).
3619
+ `));
3620
+ });
3621
+ var resizeRevertCommand = new import_commander28.Command("resize-revert").description("resize \uB97C \uB864\uBC31\uD55C\uB2E4 (VERIFY_RESIZE \u2192 ACTIVE, \uC774\uC804 flavor \uB85C \uBCF5\uADC0)").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
3622
+ const opts = cmd.optsWithGlobals();
3623
+ const { client } = await resolveInstanceClient(opts);
3624
+ startSpinner(`resize \uB864\uBC31 \uC911... (id: ${id})`);
3625
+ try {
3626
+ await client.revertResize(id);
3627
+ } catch (err) {
3628
+ stopSpinner(false);
3629
+ throw err;
3630
+ }
3631
+ stopSpinner(true);
3632
+ process.stderr.write(
3633
+ import_chalk9.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" resize \uB864\uBC31\uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\uC774\uC804 flavor \uB85C \uBCF5\uADC0).
3634
+ `)
3635
+ );
3636
+ });
3637
+
3638
+ // src/commands/instance/images.ts
3639
+ var import_commander29 = require("commander");
3640
+ var VISIBILITY_VALUES = ["public", "private", "shared"];
3641
+ function parsePositiveInt5(value, flag) {
3642
+ if (value === void 0) return void 0;
3643
+ const n = Number(value);
3644
+ if (!Number.isInteger(n) || n < 1) {
3645
+ throw new NhnCloudCliError(`${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${value}).`, EXIT_PARAM_ERROR);
3646
+ }
3647
+ return n;
3648
+ }
3649
+ var imagesCommand = new import_commander29.Command("images").description("\uC774\uBBF8\uC9C0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4 (create --image <id> \uC18C\uC2A4, \uC804\uCCB4 \uD544\uB4DC\uB294 --json)").option("--limit <n>", "\uD55C \uD398\uC774\uC9C0 \uCD5C\uB300 \uAC1C\uC218 (\uAE30\uBCF8: \uC11C\uBC84 \uAE30\uBCF8\uAC12 25)").option("--marker <id>", "\uC774 image id \uB2E4\uC74C\uBD80\uD130 \uC870\uD68C (\uD398\uC774\uC9C0\uB124\uC774\uC158)").option("--name <name>", "\uC774\uB984\uC73C\uB85C \uD544\uD130").option("--visibility <v>", `\uB178\uCD9C \uBC94\uC704 \uD544\uD130 (${VISIBILITY_VALUES.join("|")})`).option("--owner <id>", "\uC18C\uC720\uC790(\uD504\uB85C\uC81D\uD2B8 id)\uB85C \uD544\uD130").option("--status <status>", "\uC0C1\uD0DC\uB85C \uD544\uD130 (\uC608: active)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
3650
+ const opts = cmd.optsWithGlobals();
3651
+ const limit = parsePositiveInt5(opts.limit, "--limit");
3652
+ if (opts.visibility !== void 0 && !VISIBILITY_VALUES.includes(opts.visibility)) {
3653
+ throw new NhnCloudCliError(
3654
+ `--visibility \uB294 ${VISIBILITY_VALUES.join(" | ")} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${opts.visibility}).`,
3655
+ EXIT_PARAM_ERROR
3656
+ );
3657
+ }
3658
+ const { client } = await resolveInstanceClient(opts);
3659
+ startSpinner("\uC774\uBBF8\uC9C0 \uBAA9\uB85D \uC870\uD68C \uC911...");
3660
+ let result;
3661
+ try {
3662
+ result = await client.listImages({
3663
+ limit,
3664
+ marker: opts.marker,
3665
+ name: opts.name,
3666
+ visibility: opts.visibility,
3667
+ owner: opts.owner,
3668
+ status: opts.status
3669
+ });
3670
+ } catch (err) {
3671
+ stopSpinner(false);
3672
+ throw err;
3673
+ }
3674
+ stopSpinner(true);
3675
+ output(opts, {
3676
+ headers: ["id", "name", "status", "visibility", "size"],
3677
+ rows: result.images.map((img) => [
3678
+ img.id,
3679
+ img.name ?? "-",
3680
+ img.status,
3681
+ img.visibility,
3682
+ img.size === void 0 ? "-" : String(img.size)
3683
+ ]),
3684
+ raw: result.images,
3685
+ ids: result.images.map((img) => img.id)
3686
+ });
3687
+ if (result.next && !opts.json && !opts.quiet) {
3688
+ const lastId = result.images.at(-1)?.id;
3689
+ if (lastId) {
3690
+ process.stderr.write(`\uB2E4\uC74C \uD398\uC774\uC9C0: --marker ${lastId}
3691
+ `);
3692
+ }
3693
+ }
3694
+ });
3695
+
3696
+ // src/commands/instance/keypairs.ts
3697
+ var import_commander30 = require("commander");
3698
+ var keypairsCommand = new import_commander30.Command("keypairs").description("\uD0A4\uD398\uC5B4 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4 (name\xB7fingerprint, \uC804\uCCB4 \uD544\uB4DC\uB294 --json)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
3699
+ const opts = cmd.optsWithGlobals();
3700
+ const { client } = await resolveInstanceClient(opts);
3701
+ startSpinner("\uD0A4\uD398\uC5B4 \uBAA9\uB85D \uC870\uD68C \uC911...");
3702
+ let keypairs;
3703
+ try {
3704
+ keypairs = await client.listKeypairs();
3705
+ } catch (err) {
3706
+ stopSpinner(false);
3707
+ throw err;
3708
+ }
3709
+ stopSpinner(true);
3710
+ output(opts, {
3711
+ headers: ["name", "fingerprint"],
3712
+ rows: keypairs.map((k) => [k.name, k.fingerprint]),
3713
+ raw: keypairs,
3714
+ ids: keypairs.map((k) => k.name)
3715
+ });
3716
+ });
3717
+
3718
+ // src/commands/instance/keypair.ts
3719
+ var import_commander31 = require("commander");
3720
+ var import_node_fs6 = require("fs");
3721
+ var import_node_crypto3 = require("crypto");
3722
+ var import_chalk10 = __toESM(require("chalk"));
3723
+ function resolvePublicKey(value) {
3724
+ let stat;
3725
+ try {
3726
+ stat = (0, import_node_fs6.statSync)(value);
3727
+ } catch (e) {
3728
+ const err = e;
3729
+ if (err.code && err.code !== "ENOENT") {
3730
+ throw new NhnCloudCliError(
3731
+ `--public-key \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${value} (${err.code})`,
1524
3732
  EXIT_PARAM_ERROR
1525
3733
  );
1526
3734
  }
1527
- if (stat.size > 49149) {
1528
- throw new NhnCloudCliError(
1529
- `--user-data \uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${stat.size} \uBC14\uC774\uD2B8). base64 \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8 \uD55C\uB3C4\uB77C raw 49149 \uBC14\uC774\uD2B8\uAE4C\uC9C0\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4.`,
1530
- EXIT_PARAM_ERROR
1531
- );
3735
+ return value;
3736
+ }
3737
+ if (!stat.isFile()) {
3738
+ throw new NhnCloudCliError(`--public-key \uAC00 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${value}`, EXIT_PARAM_ERROR);
3739
+ }
3740
+ return (0, import_node_fs6.readFileSync)(value, "utf-8").trim();
3741
+ }
3742
+ function savePrivateKey(filePath, privateKey) {
3743
+ const tmp = `${filePath}.${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.tmp`;
3744
+ const content = privateKey.endsWith("\n") ? privateKey : privateKey + "\n";
3745
+ try {
3746
+ (0, import_node_fs6.writeFileSync)(tmp, content, { encoding: "utf-8", mode: 384 });
3747
+ (0, import_node_fs6.renameSync)(tmp, filePath);
3748
+ } catch (e) {
3749
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
3750
+ let tmpNote = "";
3751
+ try {
3752
+ (0, import_node_fs6.unlinkSync)(tmp);
3753
+ } catch {
3754
+ tmpNote = ` \u2014 0600 \uC784\uC2DC \uD30C\uC77C\uC774 \uB0A8\uC558\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4: ${tmp}`;
1532
3755
  }
1533
- const raw = (0, import_node_fs.readFileSync)(opts.userData);
1534
- userDataBase64 = raw.toString("base64");
1535
- if (userDataBase64.length > 65535) {
1536
- throw new NhnCloudCliError(
1537
- `--user-data \uAC00 base64 \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4 (${userDataBase64.length} \uBC14\uC774\uD2B8). cloud-init \uB0B4\uC6A9\uC744 \uC904\uC774\uC138\uC694.`,
1538
- EXIT_PARAM_ERROR
3756
+ throw new NhnCloudCliError(
3757
+ `private_key \uD30C\uC77C\uC744 \uC800\uC7A5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath} (${reason})${tmpNote}`,
3758
+ EXIT_PARAM_ERROR
3759
+ );
3760
+ }
3761
+ }
3762
+ var getKeypairCmd = new import_commander31.Command("get").description("\uB2E8\uC77C \uD0A4\uD398\uC5B4\uB97C \uC870\uD68C\uD55C\uB2E4").argument("<name>", "\uD0A4\uD398\uC5B4 \uC774\uB984").option("--region <region>", "region override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (name, _opts, cmd) => {
3763
+ const opts = cmd.optsWithGlobals();
3764
+ const { client } = await resolveInstanceClient(opts);
3765
+ startSpinner("\uD0A4\uD398\uC5B4 \uC870\uD68C \uC911...");
3766
+ let kp;
3767
+ try {
3768
+ kp = await client.getKeypair(name);
3769
+ } catch (err) {
3770
+ stopSpinner(false);
3771
+ throw err;
3772
+ }
3773
+ stopSpinner(true);
3774
+ output(opts, {
3775
+ headers: ["field", "value"],
3776
+ rows: [
3777
+ ["name", kp.name],
3778
+ ["fingerprint", kp.fingerprint],
3779
+ ["user_id", kp.user_id],
3780
+ ["created_at", kp.created_at],
3781
+ ["public_key", kp.public_key]
3782
+ ],
3783
+ raw: kp,
3784
+ ids: [kp.name]
3785
+ });
3786
+ });
3787
+ var createKeypairCmd = new import_commander31.Command("create").description("\uD0A4\uD398\uC5B4\uB97C \uC0DD\uC131\uD55C\uB2E4 (--public-key \uBBF8\uC9C0\uC815 \uC2DC NHN \uC774 \uD0A4\uC30D \uC0DD\uC131 \u2014 private_key 1\uD68C\uC131)").argument("<name>", "\uD0A4\uD398\uC5B4 \uC774\uB984").option("--public-key <path|key>", "\uAE30\uC874 \uACF5\uAC1C\uD0A4 (\uD30C\uC77C \uACBD\uB85C \uB610\uB294 \uD0A4 \uBB38\uC790\uC5F4). \uC9C0\uC815 \uC2DC private_key \uBBF8\uBC18\uD658").option("-o, --output <keyfile>", "\uC0DD\uC131\uB41C private_key \uB97C \uD30C\uC77C(mode 0600)\uB85C \uC800\uC7A5").option("--region <region>", "region override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (name, _opts, cmd) => {
3788
+ const opts = cmd.optsWithGlobals();
3789
+ const publicKey = opts.publicKey !== void 0 ? resolvePublicKey(opts.publicKey) : void 0;
3790
+ if (opts.output !== void 0 && publicKey !== void 0) {
3791
+ throw new NhnCloudCliError(
3792
+ "--output \uC740 NHN \uC774 \uD0A4\uB97C \uC0DD\uC131\uD560 \uB54C\uB9CC \uC758\uBBF8\uAC00 \uC788\uC2B5\uB2C8\uB2E4. --public-key \uC640 \uD568\uAED8 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
3793
+ EXIT_PARAM_ERROR
3794
+ );
3795
+ }
3796
+ if (publicKey === void 0 && opts.quiet && opts.output === void 0) {
3797
+ throw new NhnCloudCliError(
3798
+ "NHN \uC774 \uC0DD\uC131\uD558\uB294 private_key \uB294 1\uD68C\uB9CC \uBC18\uD658\uB429\uB2C8\uB2E4. --quiet \uB85C \uC0DD\uC131\uD560 \uB54C\uB294 --output <keyfile> \uB85C \uC800\uC7A5 \uACBD\uB85C\uB97C \uC9C0\uC815\uD558\uC138\uC694 (\uBBF8\uC9C0\uC815 \uC2DC \uD0A4 \uC720\uC2E4).",
3799
+ EXIT_PARAM_ERROR
3800
+ );
3801
+ }
3802
+ const { client } = await resolveInstanceClient(opts);
3803
+ startSpinner("\uD0A4\uD398\uC5B4 \uC0DD\uC131 \uC911...");
3804
+ let result;
3805
+ try {
3806
+ result = await client.createKeypair({ name, publicKey });
3807
+ } catch (err) {
3808
+ stopSpinner(false);
3809
+ throw err;
3810
+ }
3811
+ stopSpinner(true);
3812
+ if (result.private_key !== void 0) {
3813
+ if (opts.output !== void 0) {
3814
+ try {
3815
+ savePrivateKey(opts.output, result.private_key);
3816
+ process.stderr.write(import_chalk10.default.green(` private_key \uB97C ${opts.output} \uC5D0 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4 (mode 0600).
3817
+ `));
3818
+ } catch (saveErr) {
3819
+ const reason = saveErr instanceof Error ? saveErr.message : String(saveErr);
3820
+ process.stderr.write(
3821
+ import_chalk10.default.red(` \u26A0 private_key \uD30C\uC77C \uC800\uC7A5 \uC2E4\uD328 (${reason}). \uC720\uC2E4 \uBC29\uC9C0\uB97C \uC704\uD574 \uC544\uB798\uC5D0 \uCD9C\uB825\uD569\uB2C8\uB2E4 \u2014 \uC989\uC2DC \uC548\uC804\uD55C \uACF3\uC5D0 \uBCF4\uAD00\uD558\uC138\uC694.
3822
+ `)
3823
+ );
3824
+ process.stdout.write(result.private_key + "\n");
3825
+ }
3826
+ } else {
3827
+ process.stderr.write(
3828
+ import_chalk10.default.yellow(" \u26A0 private_key \uB294 \uC9C0\uAE08 \uD55C \uBC88\uB9CC \uD45C\uC2DC\uB429\uB2C8\uB2E4. \uBD84\uC2E4 \uC2DC \uBCF5\uAD6C \uBD88\uAC00 \u2014 \uC548\uC804\uD55C \uACF3\uC5D0 \uBCF4\uAD00\uD558\uC138\uC694.\n")
1539
3829
  );
3830
+ process.stdout.write(result.private_key + "\n");
1540
3831
  }
1541
3832
  }
1542
- const timeoutMs = parseInt(opts.timeout ?? "300", 10) * 1e3;
3833
+ const { private_key, ...meta } = result;
3834
+ void private_key;
3835
+ output(opts, {
3836
+ headers: ["field", "value"],
3837
+ rows: [
3838
+ ["name", meta.name],
3839
+ ["fingerprint", meta.fingerprint],
3840
+ ["user_id", meta.user_id]
3841
+ ],
3842
+ raw: meta,
3843
+ ids: [meta.name]
3844
+ });
3845
+ });
3846
+ var deleteKeypairCmd = new import_commander31.Command("delete").description("\uD0A4\uD398\uC5B4\uB97C \uC0AD\uC81C\uD55C\uB2E4").argument("<name>", "\uD0A4\uD398\uC5B4 \uC774\uB984").option("--region <region>", "region override").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (name, _opts, cmd) => {
3847
+ const opts = cmd.optsWithGlobals();
1543
3848
  const { client } = await resolveInstanceClient(opts);
1544
- startSpinner("\uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131 \uC911...");
1545
- let server;
3849
+ startSpinner("\uD0A4\uD398\uC5B4 \uC0AD\uC81C \uC911...");
1546
3850
  try {
1547
- server = await client.create({
1548
- name: opts.name,
1549
- flavorRef: opts.flavor,
1550
- imageRef: opts.image,
1551
- networks,
1552
- bootVolumeSize: opts.bootVolumeSize !== void 0 ? parseInt(opts.bootVolumeSize, 10) : void 0,
1553
- keyName: opts.keyName,
1554
- securityGroups: opts.securityGroup && opts.securityGroup.length > 0 ? opts.securityGroup : void 0,
1555
- ephemeralDiskSize: opts.ephemeralDiskSize !== void 0 ? parseInt(opts.ephemeralDiskSize, 10) : void 0,
1556
- protect: opts.protect,
1557
- userDataBase64
1558
- });
3851
+ await client.deleteKeypair(name);
1559
3852
  } catch (err) {
1560
3853
  stopSpinner(false);
1561
3854
  throw err;
1562
3855
  }
1563
3856
  stopSpinner(true);
1564
- if (opts.wait) {
1565
- startSpinner(`ACTIVE \uB300\uAE30 \uC911... (id: ${server.id})`);
3857
+ process.stderr.write(import_chalk10.default.green(`\u2713 \uD0A4\uD398\uC5B4 "${name}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
3858
+ `));
3859
+ });
3860
+ var keypairCommand = new import_commander31.Command("keypair").description("\uD0A4\uD398\uC5B4 \uB2E8\uAC74 \uAD00\uB9AC (get / create / delete)").addCommand(getKeypairCmd).addCommand(createKeypairCmd).addCommand(deleteKeypairCmd);
3861
+
3862
+ // src/commands/instance/volume.ts
3863
+ var import_commander32 = require("commander");
3864
+ var import_chalk11 = __toESM(require("chalk"));
3865
+ var attachCommand = new import_commander32.Command("attach").description("\uBCFC\uB968\uC744 \uC778\uC2A4\uD134\uC2A4\uC5D0 \uC5F0\uACB0\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").requiredOption("--volume <volumeId>", "\uC5F0\uACB0\uD560 \uBCFC\uB968 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
3866
+ const opts = cmd.optsWithGlobals();
3867
+ const { client } = await resolveInstanceClient(opts);
3868
+ startSpinner("\uBCFC\uB968 \uC5F0\uACB0 \uC911...");
3869
+ let att;
3870
+ try {
3871
+ att = await client.attachVolume(id, opts.volume);
3872
+ } catch (err) {
3873
+ stopSpinner(false);
3874
+ throw err;
3875
+ }
3876
+ stopSpinner(true);
3877
+ process.stderr.write(
3878
+ import_chalk11.default.green(`\uBCFC\uB968 \uC5F0\uACB0 \uC644\uB8CC (volumeId: ${att.volumeId}, device: ${att.device})
3879
+ `)
3880
+ );
3881
+ output(opts, {
3882
+ headers: ["field", "value"],
3883
+ rows: [
3884
+ ["id", att.id],
3885
+ ["volumeId", att.volumeId],
3886
+ ["serverId", att.serverId],
3887
+ ["device", att.device]
3888
+ ],
3889
+ raw: att,
3890
+ ids: [att.id]
3891
+ });
3892
+ });
3893
+ var detachCommand = new import_commander32.Command("detach").description("\uBCFC\uB968 \uC5F0\uACB0\uC744 \uD574\uC81C\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").argument("<volumeId>", "\uD574\uC81C\uD560 \uBCFC\uB968 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, volumeId, _opts, cmd) => {
3894
+ const opts = cmd.optsWithGlobals();
3895
+ const { client } = await resolveInstanceClient(opts);
3896
+ startSpinner("\uBCFC\uB968 \uC5F0\uACB0 \uD574\uC81C \uC911...");
3897
+ try {
3898
+ await client.detachVolume(id, volumeId);
3899
+ } catch (err) {
3900
+ stopSpinner(false);
3901
+ throw err;
3902
+ }
3903
+ stopSpinner(true);
3904
+ process.stderr.write(
3905
+ import_chalk11.default.green(`\uBCFC\uB968 \uC5F0\uACB0 \uD574\uC81C \uC694\uCCAD \uC644\uB8CC (volumeId: ${volumeId})
3906
+ `)
3907
+ );
3908
+ });
3909
+ var volumeCommand = new import_commander32.Command("volume").description(
3910
+ "\uC778\uC2A4\uD134\uC2A4 \uBCFC\uB968 \uC5F0\uACB0/\uD574\uC81C"
3911
+ );
3912
+ volumeCommand.addCommand(attachCommand);
3913
+ volumeCommand.addCommand(detachCommand);
3914
+
3915
+ // src/commands/instance/volumes.ts
3916
+ var import_commander33 = require("commander");
3917
+ var volumesCommand = new import_commander33.Command("volumes").description("\uC778\uC2A4\uD134\uC2A4\uC5D0 \uC5F0\uACB0\uB41C \uBCFC\uB968 \uBAA9\uB85D\uC744 \uC870\uD68C\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
3918
+ const opts = cmd.optsWithGlobals();
3919
+ const { client } = await resolveInstanceClient(opts);
3920
+ startSpinner("\uBCFC\uB968 \uBAA9\uB85D \uC870\uD68C \uC911...");
3921
+ let attachments;
3922
+ try {
3923
+ attachments = await client.listVolumeAttachments(id);
3924
+ } catch (err) {
3925
+ stopSpinner(false);
3926
+ throw err;
3927
+ }
3928
+ stopSpinner(true);
3929
+ output(opts, {
3930
+ headers: ["id", "volumeId", "device"],
3931
+ rows: attachments.map((a) => [a.id, a.volumeId, a.device]),
3932
+ raw: attachments,
3933
+ ids: attachments.map((a) => a.id)
3934
+ });
3935
+ });
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;
1566
3968
  try {
1567
- server = await client.waitForActive(server.id, { timeoutMs });
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
+ }
1568
3994
  } catch (err) {
1569
- stopSpinner(false);
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) {
1570
4038
  throw err;
1571
4039
  }
1572
- stopSpinner(true, `ACTIVE \uD655\uC778 (id: ${server.id})`);
1573
4040
  }
1574
- if (opts.quiet && opts.wait) {
1575
- const ip = getFirstIp(server);
1576
- if (ip) process.stdout.write(ip + "\n");
1577
- return;
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
+ );
1578
4046
  }
1579
- if (opts.wait) {
1580
- process.stderr.write(import_chalk2.default.green(` IP: ${getIps3(server)}
1581
- `));
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
+ );
1582
4064
  }
1583
- const rows = [
1584
- ["id", server.id],
1585
- ["name", server.name],
1586
- ["status", server.status],
1587
- ["IPs", getIps3(server)],
1588
- ["flavor", server.flavor.id],
1589
- ["image", getImageId2(server)]
1590
- ];
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);
1591
4090
  output(opts, {
1592
- headers: ["field", "value"],
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"],
1593
4160
  rows,
1594
- raw: server,
1595
- ids: [server.id]
4161
+ raw: repos,
4162
+ ids
1596
4163
  });
1597
4164
  });
1598
4165
 
1599
- // src/commands/instance/delete.ts
1600
- var import_commander10 = require("commander");
1601
- var import_chalk3 = __toESM(require("chalk"));
1602
- var deleteCommand = new import_commander10.Command("delete").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC0AD\uC81C\uD55C\uB2E4").argument("<id>", "\uC778\uC2A4\uD134\uC2A4 ID").option("--yes", "\uD655\uC778 \uD504\uB86C\uD504\uD2B8 \uC0DD\uB7B5 (CI/\uBE44\uB300\uD654\uD615 \uD544\uC218)").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (id, _opts, cmd) => {
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) => {
1603
4169
  const opts = cmd.optsWithGlobals();
1604
- const isTTY = process.stdin.isTTY;
1605
- if (!isTTY && !opts.yes) {
4170
+ if (!registry.trim()) {
1606
4171
  throw new NhnCloudCliError(
1607
- "\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C \uC778\uC2A4\uD134\uC2A4 \uC0AD\uC81C\uB294 --yes \uD50C\uB798\uADF8\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
4172
+ "registry \uC778\uC218\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. \uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC774\uB984\uC744 \uC9C0\uC815\uD558\uC138\uC694.",
1608
4173
  EXIT_PARAM_ERROR
1609
4174
  );
1610
4175
  }
1611
- if (isTTY && !opts.yes) {
1612
- const { confirm } = await import("@inquirer/prompts");
1613
- const ok = await confirm({
1614
- message: `\uC778\uC2A4\uD134\uC2A4 "${id}" \uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`,
1615
- default: false
1616
- });
1617
- if (!ok) {
1618
- process.stderr.write(import_chalk3.default.yellow("\uC0AD\uC81C\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
1619
- return;
1620
- }
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
+ );
1621
4181
  }
1622
- const { client } = await resolveInstanceClient(opts);
1623
- startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC0AD\uC81C \uC911... (id: ${id})`);
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;
1624
4186
  try {
1625
- await client.delete(id);
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
+ );
1626
4195
  } catch (err) {
1627
4196
  stopSpinner(false);
1628
4197
  throw err;
1629
4198
  }
1630
4199
  stopSpinner(true);
1631
- process.stderr.write(import_chalk3.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
1632
- `));
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
+ });
1633
4206
  });
1634
4207
 
1635
4208
  // src/index.ts
1636
- var program = new import_commander11.Command();
1637
- program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.3.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");
1638
4211
  program.hook("preAction", () => {
1639
4212
  const opts = program.opts();
1640
4213
  if (!opts.color || process.env["NO_COLOR"]) {
1641
- import_chalk4.default.level = 0;
4214
+ import_chalk12.default.level = 0;
1642
4215
  }
1643
4216
  if (opts.json || opts.quiet) {
1644
4217
  setQuiet(true);
1645
4218
  }
1646
4219
  });
1647
4220
  program.addCommand(configureCommand);
1648
- var logncrashCommand = new import_commander11.Command("logncrash").description("Log & Crash \uAD00\uB828 \uBA85\uB839");
4221
+ var logncrashCommand = new import_commander38.Command("logncrash").description("Log & Crash \uAD00\uB828 \uBA85\uB839");
1649
4222
  logncrashCommand.addCommand(searchCommand);
4223
+ logncrashCommand.addCommand(sendCommand);
4224
+ logncrashCommand.addCommand(exportCommand);
1650
4225
  program.addCommand(logncrashCommand);
1651
- var deployCommand = new import_commander11.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");
1652
4227
  deployCommand.addCommand(runCommand);
1653
4228
  deployCommand.addCommand(artifactsCommand);
1654
4229
  deployCommand.addCommand(serverGroupsCommand);
1655
4230
  deployCommand.addCommand(historiesCommand);
4231
+ deployCommand.addCommand(binaryGroupsCommand);
4232
+ deployCommand.addCommand(binariesCommand);
4233
+ deployCommand.addCommand(uploadCommand);
4234
+ deployCommand.addCommand(downloadCommand);
1656
4235
  program.addCommand(deployCommand);
1657
- var instanceCommand = new import_commander11.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");
1658
4237
  instanceCommand.addCommand(listCommand);
1659
- instanceCommand.addCommand(getCommand);
1660
- instanceCommand.addCommand(createCommand);
1661
- instanceCommand.addCommand(deleteCommand);
4238
+ instanceCommand.addCommand(flavorsCommand);
4239
+ instanceCommand.addCommand(availabilityZonesCommand);
4240
+ instanceCommand.addCommand(getCommand2);
4241
+ instanceCommand.addCommand(createCommand3);
4242
+ instanceCommand.addCommand(deleteCommand2);
4243
+ instanceCommand.addCommand(startCommand);
4244
+ instanceCommand.addCommand(stopCommand);
4245
+ instanceCommand.addCommand(rebootCommand);
4246
+ instanceCommand.addCommand(resizeCommand);
4247
+ instanceCommand.addCommand(resizeConfirmCommand);
4248
+ instanceCommand.addCommand(resizeRevertCommand);
4249
+ instanceCommand.addCommand(imagesCommand);
4250
+ instanceCommand.addCommand(keypairsCommand);
4251
+ instanceCommand.addCommand(keypairCommand);
4252
+ instanceCommand.addCommand(volumeCommand);
4253
+ instanceCommand.addCommand(volumesCommand);
1662
4254
  program.addCommand(instanceCommand);
4255
+ var networkCommand = new import_commander38.Command("network").description("VPC\xB7\uC11C\uBE0C\uB137 \uC870\uD68C");
4256
+ networkCommand.addCommand(listCommand3);
4257
+ networkCommand.addCommand(subnetCommand);
4258
+ program.addCommand(networkCommand);
4259
+ var volumeCommand2 = new import_commander38.Command("volume").description("Block Storage \uBCFC\uB968 \uAD00\uB828 \uBA85\uB839");
4260
+ volumeCommand2.addCommand(listCommand2);
4261
+ volumeCommand2.addCommand(getCommand);
4262
+ volumeCommand2.addCommand(createCommand);
4263
+ program.addCommand(volumeCommand2);
4264
+ var floatingipCommand = new import_commander38.Command("floatingip").description(
4265
+ "Floating IP(\uC778\uC2A4\uD134\uC2A4 \uACF5\uC778 IP) \uAD00\uB9AC"
4266
+ );
4267
+ floatingipCommand.addCommand(listCommand4);
4268
+ floatingipCommand.addCommand(createCommand2);
4269
+ floatingipCommand.addCommand(deleteCommand);
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);
1663
4277
  program.parseAsync().catch((err) => {
1664
4278
  const message = err instanceof Error ? err.message : String(err);
1665
4279
  const exitCode = err instanceof NhnCloudCliError ? err.exitCode : 1;
1666
- process.stderr.write(import_chalk4.default.red(`\uC624\uB958: ${message}`) + "\n");
4280
+ process.stderr.write(import_chalk12.default.red(`\uC624\uB958: ${message}`) + "\n");
1667
4281
  process.exit(exitCode);
1668
4282
  });