@bifos/nhncloud-cli 0.2.0 → 0.4.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_commander34 = 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,60 @@ 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(", ");
472
498
  function instanceHost(region) {
473
499
  const host = INSTANCE_HOST[region];
474
500
  if (!host) {
475
501
  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(", ")}`,
502
+ `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
503
+ EXIT_PARAM_ERROR
504
+ );
505
+ }
506
+ return host;
507
+ }
508
+ function imageHost(region) {
509
+ const host = IMAGE_HOST[region];
510
+ if (!host) {
511
+ throw new NhnCloudCliError(
512
+ `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
513
+ EXIT_PARAM_ERROR
514
+ );
515
+ }
516
+ return host;
517
+ }
518
+ function networkHost(region) {
519
+ const host = NETWORK_HOST[region];
520
+ if (!host) {
521
+ throw new NhnCloudCliError(
522
+ `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
523
+ EXIT_PARAM_ERROR
524
+ );
525
+ }
526
+ return host;
527
+ }
528
+ function blockStorageHost(region) {
529
+ const host = BLOCKSTORAGE_HOST[region];
530
+ if (!host) {
531
+ throw new NhnCloudCliError(
532
+ `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 region \uC785\uB2C8\uB2E4: "${region}". \uC0AC\uC6A9 \uAC00\uB2A5\uD55C region: ${IAAS_REGIONS}`,
477
533
  EXIT_PARAM_ERROR
478
534
  );
479
535
  }
@@ -494,11 +550,20 @@ async function getIaasToken(profile, iaas, forceRefresh = false) {
494
550
  if (!forceRefresh) {
495
551
  const cached = await readIaasToken(profile, iaas.region);
496
552
  if (cached !== null) {
497
- return { tokenId: cached.tokenId, computeEndpoint: cached.computeEndpoint };
553
+ return {
554
+ tokenId: cached.tokenId,
555
+ computeEndpoint: cached.computeEndpoint,
556
+ imageEndpoint: cached.imageEndpoint,
557
+ networkEndpoint: cached.networkEndpoint,
558
+ blockStorageEndpoint: cached.blockStorageEndpoint
559
+ };
498
560
  }
499
561
  }
500
562
  const host = instanceHost(iaas.region);
501
563
  const computeEndpoint = `https://${host}/v2/${encodeURIComponent(iaas.tenantId)}`;
564
+ const imageEndpoint = `https://${imageHost(iaas.region)}/v2`;
565
+ const networkEndpoint = `https://${networkHost(iaas.region)}/v2.0`;
566
+ const blockStorageEndpoint = `https://${blockStorageHost(iaas.region)}/v2/${encodeURIComponent(iaas.tenantId)}`;
502
567
  let raw;
503
568
  try {
504
569
  raw = await import_ky3.default.post(keystoneIdentityUrl(), {
@@ -525,22 +590,25 @@ async function getIaasToken(profile, iaas, forceRefresh = false) {
525
590
  const tokenId = raw.access.token.id;
526
591
  const expiresAt = raw.access.token.expires;
527
592
  if (!forceRefresh) {
528
- await writeIaasToken(profile, iaas.region, { tokenId, expiresAt, computeEndpoint });
593
+ await writeIaasToken(profile, iaas.region, { tokenId, expiresAt, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint });
529
594
  }
530
- return { tokenId, computeEndpoint };
595
+ return { tokenId, computeEndpoint, imageEndpoint, networkEndpoint, blockStorageEndpoint };
531
596
  }
532
597
 
533
598
  // src/services/logncrash/client.ts
534
599
  var import_ky4 = __toESM(require("ky"));
535
600
 
536
601
  // src/api/envelope.ts
537
- function unwrap(res) {
602
+ function unwrapHeader(res) {
538
603
  if (!res.header.isSuccessful) {
539
604
  throw new NhnCloudCliError(
540
605
  `API \uC624\uB958: ${res.header.resultMessage}`,
541
606
  EXIT_API_ERROR
542
607
  );
543
608
  }
609
+ }
610
+ function unwrap(res) {
611
+ unwrapHeader(res);
544
612
  if (res.body === void 0) {
545
613
  throw new NhnCloudCliError("API \uC751\uB2F5\uC5D0 body \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", EXIT_API_ERROR);
546
614
  }
@@ -550,12 +618,19 @@ function unwrap(res) {
550
618
  // src/services/logncrash/client.ts
551
619
  var LogncrashClient = class {
552
620
  appkey;
621
+ /** 검색(X-LNCS-SECRET)에만 필요. collector send 는 secret 을 쓰지 않으므로 옵셔널 (ADR-014). */
553
622
  secret;
554
623
  constructor(appkey, secret) {
555
624
  this.appkey = appkey;
556
625
  this.secret = secret;
557
626
  }
558
627
  async search(params) {
628
+ if (!this.secret) {
629
+ throw new NhnCloudCliError(
630
+ "logncrash search \uC5D0\uB294 secret \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. configure \uB85C logncrash secret \uC744 \uC124\uC815\uD558\uC138\uC694.",
631
+ EXIT_CONFIG_ERROR
632
+ );
633
+ }
559
634
  const endpoint = endpointFor("logncrash");
560
635
  const url = `${endpoint}/api/v2/search/${encodeURIComponent(this.appkey)}`;
561
636
  try {
@@ -577,6 +652,92 @@ var LogncrashClient = class {
577
652
  throw toNhnCloudCliError(err);
578
653
  }
579
654
  }
655
+ /**
656
+ * scroll 검색을 시작한다. POST /api/v2/search/scroll/{appkey}.
657
+ * body 는 search 와 동일(query/from/to/pageSize). 응답 scrollKey 로 scrollNext 를 이어 호출한다.
658
+ */
659
+ async scrollStart(params) {
660
+ if (!this.secret) {
661
+ throw new NhnCloudCliError(
662
+ "logncrash scroll \uC5D0\uB294 secret \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. configure \uB85C logncrash secret \uC744 \uC124\uC815\uD558\uC138\uC694.",
663
+ EXIT_CONFIG_ERROR
664
+ );
665
+ }
666
+ const endpoint = endpointFor("logncrash");
667
+ const url = `${endpoint}/api/v2/search/scroll/${encodeURIComponent(this.appkey)}`;
668
+ try {
669
+ const res = await import_ky4.default.post(url, {
670
+ headers: {
671
+ "X-LNCS-SECRET": this.secret,
672
+ "Content-Type": "application/json"
673
+ },
674
+ json: {
675
+ query: params.query,
676
+ from: params.from,
677
+ to: params.to,
678
+ pageSize: params.pageSize ?? 100
679
+ }
680
+ }).json();
681
+ return unwrap(res);
682
+ } catch (err) {
683
+ throw toNhnCloudCliError(err);
684
+ }
685
+ }
686
+ /**
687
+ * scroll 다음 페이지를 가져온다. POST /api/v2/search/scroll/{appkey}/{scrollKey}.
688
+ * body 는 보내지 않는다(scrollKey 가 좌표). scrollKey 만료 시 API 가 실패 봉투를 주며,
689
+ * unwrap 이 EXIT_API_ERROR 로 변환한다 — 호출부에서 만료 안내 메시지로 감싼다.
690
+ */
691
+ async scrollNext(scrollKey) {
692
+ if (!this.secret) {
693
+ throw new NhnCloudCliError(
694
+ "logncrash scroll \uC5D0\uB294 secret \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. configure \uB85C logncrash secret \uC744 \uC124\uC815\uD558\uC138\uC694.",
695
+ EXIT_CONFIG_ERROR
696
+ );
697
+ }
698
+ const endpoint = endpointFor("logncrash");
699
+ const url = `${endpoint}/api/v2/search/scroll/${encodeURIComponent(this.appkey)}/${encodeURIComponent(scrollKey)}`;
700
+ try {
701
+ const res = await import_ky4.default.post(url, {
702
+ headers: {
703
+ "X-LNCS-SECRET": this.secret,
704
+ "Content-Type": "application/json"
705
+ }
706
+ }).json();
707
+ return unwrap(res);
708
+ } catch (err) {
709
+ throw toNhnCloudCliError(err);
710
+ }
711
+ }
712
+ /**
713
+ * 로그 한 건을 Log & Crash collector 로 전송한다 (ADR-014).
714
+ * - host: api-logncrash (검색의 api-lncs-search 와 별도)
715
+ * - 인증: 헤더 없음 — body 의 projectName=appkey 로 식별 (secret 불요)
716
+ * - logVersion 은 "v2" 고정. logSource/logType 미지정 시 collector 기본값("http"/"log") 적용.
717
+ */
718
+ async send(params) {
719
+ const endpoint = endpointFor("logncrash-collector");
720
+ const url = `${endpoint}/v2/log`;
721
+ const payload = {
722
+ projectName: this.appkey,
723
+ projectVersion: params.projectVersion,
724
+ logVersion: "v2",
725
+ body: params.body
726
+ };
727
+ if (params.logLevel !== void 0) payload["logLevel"] = params.logLevel;
728
+ if (params.logSource !== void 0) payload["logSource"] = params.logSource;
729
+ if (params.logType !== void 0) payload["logType"] = params.logType;
730
+ if (params.host !== void 0) payload["host"] = params.host;
731
+ try {
732
+ const res = await import_ky4.default.post(url, {
733
+ headers: { "Content-Type": "application/json" },
734
+ json: payload
735
+ }).json();
736
+ unwrapHeader(res);
737
+ } catch (err) {
738
+ throw toNhnCloudCliError(err);
739
+ }
740
+ }
580
741
  };
581
742
 
582
743
  // src/commands/configure-verify.ts
@@ -985,11 +1146,249 @@ credentials.json \uC5D0 "secret": "<secretkey>" \uB97C \uCD94\uAC00\uD558\uC138\
985
1146
  });
986
1147
  });
987
1148
 
988
- // src/commands/deploy/run.ts
1149
+ // src/commands/logncrash/send.ts
989
1150
  var import_commander3 = require("commander");
1151
+ var import_node_fs = require("fs");
1152
+ var MAX_LOG_BYTES = 8 * 1024 * 1024;
1153
+ var VALID_LEVELS = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"];
1154
+ function resolveBody(opts) {
1155
+ if (opts.body !== void 0) return opts.body;
1156
+ if (opts.file !== void 0) {
1157
+ let stat;
1158
+ try {
1159
+ stat = (0, import_node_fs.statSync)(opts.file);
1160
+ } catch (e) {
1161
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
1162
+ throw new NhnCloudCliError(`\uB85C\uADF8 \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.file} (${reason})`, EXIT_PARAM_ERROR);
1163
+ }
1164
+ if (!stat.isFile()) {
1165
+ throw new NhnCloudCliError(`\uB85C\uADF8 \uD30C\uC77C\uC774 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${opts.file}`, EXIT_PARAM_ERROR);
1166
+ }
1167
+ if (stat.size > MAX_LOG_BYTES) {
1168
+ throw new NhnCloudCliError(
1169
+ `\uB85C\uADF8 \uD30C\uC77C\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${stat.size} \uBC14\uC774\uD2B8 (\uD55C\uB3C4 ${MAX_LOG_BYTES} \uBC14\uC774\uD2B8).`,
1170
+ EXIT_PARAM_ERROR
1171
+ );
1172
+ }
1173
+ return (0, import_node_fs.readFileSync)(opts.file, "utf-8");
1174
+ }
1175
+ if (!process.stdin.isTTY) {
1176
+ return (0, import_node_fs.readFileSync)(0, "utf-8");
1177
+ }
1178
+ throw new NhnCloudCliError(
1179
+ "\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.",
1180
+ EXIT_PARAM_ERROR
1181
+ );
1182
+ }
1183
+ 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) => {
1184
+ const opts = cmd.optsWithGlobals();
1185
+ const body = resolveBody(opts);
1186
+ const bytes = Buffer.byteLength(body, "utf-8");
1187
+ if (bytes === 0) {
1188
+ throw new NhnCloudCliError("\uB85C\uADF8 \uBCF8\uBB38\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
1189
+ }
1190
+ if (bytes > MAX_LOG_BYTES) {
1191
+ throw new NhnCloudCliError(
1192
+ `\uB85C\uADF8 \uBCF8\uBB38\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${bytes} \uBC14\uC774\uD2B8 (\uD55C\uB3C4 ${MAX_LOG_BYTES} \uBC14\uC774\uD2B8).`,
1193
+ EXIT_PARAM_ERROR
1194
+ );
1195
+ }
1196
+ let logLevel;
1197
+ if (opts.level !== void 0) {
1198
+ const upper = opts.level.toUpperCase();
1199
+ if (!VALID_LEVELS.includes(upper)) {
1200
+ throw new NhnCloudCliError(
1201
+ `--level \uC740 ${VALID_LEVELS.join("/")} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${opts.level}).`,
1202
+ EXIT_PARAM_ERROR
1203
+ );
1204
+ }
1205
+ logLevel = upper;
1206
+ }
1207
+ const profileName = await resolveProfileName(opts.profile);
1208
+ const cred = await getServiceCredential("logncrash", profileName);
1209
+ if (!cred.appkey) {
1210
+ throw new NhnCloudCliError(
1211
+ `profile "${profileName}" \uC758 logncrash \uC790\uACA9\uC99D\uBA85\uC5D0 appkey \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
1212
+ credentials.json \uC5D0 "appkey": "<appkey>" \uB97C \uCD94\uAC00\uD558\uC138\uC694.`,
1213
+ EXIT_CONFIG_ERROR
1214
+ );
1215
+ }
1216
+ const client = new LogncrashClient(cred.appkey);
1217
+ startSpinner("\uB85C\uADF8 \uC804\uC1A1 \uC911...");
1218
+ try {
1219
+ await client.send({
1220
+ body,
1221
+ projectVersion: opts.appVersion ?? "1.0.0",
1222
+ logLevel,
1223
+ logSource: opts.source,
1224
+ logType: opts.type,
1225
+ host: opts.host
1226
+ });
1227
+ } catch (err) {
1228
+ stopSpinner(false);
1229
+ throw err;
1230
+ }
1231
+ stopSpinner(true, "\uB85C\uADF8\uB97C \uC804\uC1A1\uD588\uC2B5\uB2C8\uB2E4.");
1232
+ if (opts.json) {
1233
+ process.stdout.write(JSON.stringify({ ok: true, bytes }) + "\n");
1234
+ }
1235
+ });
1236
+
1237
+ // src/commands/logncrash/export.ts
1238
+ var import_commander4 = require("commander");
1239
+ var import_promises3 = require("fs/promises");
1240
+ var import_node_fs2 = require("fs");
1241
+ var import_node_crypto2 = require("crypto");
1242
+ var MAX_TOTAL = 1e5;
1243
+ function assertWritable(path, force) {
1244
+ if (force) return;
1245
+ try {
1246
+ (0, import_node_fs2.statSync)(path);
1247
+ } catch (e) {
1248
+ if (e.code === "ENOENT") return;
1249
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
1250
+ throw new NhnCloudCliError(`--output \uACBD\uB85C\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${path} (${reason})`, EXIT_PARAM_ERROR);
1251
+ }
1252
+ throw new NhnCloudCliError(
1253
+ `--output \uB300\uC0C1\uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: ${path}. \uB36E\uC5B4\uC4F0\uB824\uBA74 --force \uB97C \uC4F0\uC138\uC694.`,
1254
+ EXIT_PARAM_ERROR
1255
+ );
1256
+ }
1257
+ 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) => {
1258
+ const opts = cmd.optsWithGlobals();
1259
+ if (!opts.query) {
1260
+ throw new NhnCloudCliError(`--query \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --query 'logType:"NORMAL"'`, EXIT_PARAM_ERROR);
1261
+ }
1262
+ if (!opts.from) {
1263
+ throw new NhnCloudCliError("--from \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --from 1h", EXIT_PARAM_ERROR);
1264
+ }
1265
+ if (!opts.to) {
1266
+ throw new NhnCloudCliError("--to \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --to now", EXIT_PARAM_ERROR);
1267
+ }
1268
+ if (!opts.output) {
1269
+ throw new NhnCloudCliError("--output \uC635\uC158\uC740 \uD544\uC218\uC785\uB2C8\uB2E4. \uC608: --output logs.jsonl", EXIT_PARAM_ERROR);
1270
+ }
1271
+ const format = opts.format ?? "jsonl";
1272
+ if (format !== "jsonl" && format !== "json") {
1273
+ throw new NhnCloudCliError("--format \uC740 jsonl \uB610\uB294 json \uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
1274
+ }
1275
+ const sizeRaw = opts.size ?? "100";
1276
+ if (!/^[1-9]\d*$/.test(sizeRaw)) {
1277
+ throw new NhnCloudCliError("--size \uB294 \uC591\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (docs \uBC94\uC704 10~100).", EXIT_PARAM_ERROR);
1278
+ }
1279
+ const size = parseInt(sizeRaw, 10);
1280
+ if (size < 10 || size > 100) {
1281
+ throw new NhnCloudCliError("--size \uB294 10~100 \uC0AC\uC774\uC5EC\uC57C \uD569\uB2C8\uB2E4 (Log & Crash scroll pageSize \uD55C\uB3C4).", EXIT_PARAM_ERROR);
1282
+ }
1283
+ const fromIso = resolveTime(opts.from);
1284
+ const toIso = resolveTime(opts.to);
1285
+ assertSearchRange(fromIso, toIso);
1286
+ assertWritable(opts.output, opts.force ?? false);
1287
+ const profileName = await resolveProfileName(opts.profile);
1288
+ const cred = await getServiceCredential("logncrash", profileName);
1289
+ if (!cred.appkey) {
1290
+ throw new NhnCloudCliError(
1291
+ `profile "${profileName}" \uC758 logncrash \uC790\uACA9\uC99D\uBA85\uC5D0 appkey \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
1292
+ credentials.json \uC5D0 "appkey": "<appkey>" \uB97C \uCD94\uAC00\uD558\uC138\uC694.`,
1293
+ EXIT_CONFIG_ERROR
1294
+ );
1295
+ }
1296
+ if (!cred.secret) {
1297
+ throw new NhnCloudCliError(
1298
+ `profile "${profileName}" \uC758 logncrash \uC790\uACA9\uC99D\uBA85\uC5D0 secret \uC774 \uC5C6\uC2B5\uB2C8\uB2E4.
1299
+ credentials.json \uC5D0 "secret": "<secret>" \uB97C \uCD94\uAC00\uD558\uC138\uC694.`,
1300
+ EXIT_CONFIG_ERROR
1301
+ );
1302
+ }
1303
+ const client = new LogncrashClient(cred.appkey, cred.secret);
1304
+ const tmp = opts.output + "." + (0, import_node_crypto2.randomBytes)(4).toString("hex") + ".tmp";
1305
+ const stream = (0, import_node_fs2.createWriteStream)(tmp, { encoding: "utf-8" });
1306
+ const spinner = startSpinner("\uB85C\uADF8 \uCD94\uCD9C \uC911...");
1307
+ let count = 0;
1308
+ let total = 0;
1309
+ let first = true;
1310
+ const writePage = (data) => {
1311
+ for (const log of data) {
1312
+ if (count >= MAX_TOTAL) break;
1313
+ const json = JSON.stringify(log);
1314
+ stream.write(format === "json" ? first ? json : "," + json : json + "\n");
1315
+ first = false;
1316
+ count++;
1317
+ }
1318
+ };
1319
+ try {
1320
+ if (format === "json") stream.write("[");
1321
+ let res = await client.scrollStart({ query: opts.query, from: fromIso, to: toIso, pageSize: size });
1322
+ total = res.totalItems;
1323
+ writePage(res.data);
1324
+ spinner.text = `\uB85C\uADF8 \uCD94\uCD9C \uC911... ${count}/${total}`;
1325
+ while (res.data.length > 0 && res.scrollKey && count < Math.min(total, MAX_TOTAL)) {
1326
+ res = await scrollNextOrExpire(client, res.scrollKey);
1327
+ writePage(res.data);
1328
+ spinner.text = `\uB85C\uADF8 \uCD94\uCD9C \uC911... ${count}/${total}`;
1329
+ }
1330
+ if (format === "json") stream.write("]\n");
1331
+ await new Promise((resolve, reject) => {
1332
+ stream.once("error", reject);
1333
+ stream.end(resolve);
1334
+ });
1335
+ } catch (err) {
1336
+ stopSpinner(false);
1337
+ stream.destroy();
1338
+ await (0, import_promises3.rm)(tmp, { force: true }).catch(() => {
1339
+ });
1340
+ throw err;
1341
+ }
1342
+ stopSpinner(true, `${count}\uAC74 \uCD94\uCD9C \uC644\uB8CC`);
1343
+ try {
1344
+ await (0, import_promises3.rename)(tmp, opts.output);
1345
+ } catch (err) {
1346
+ await (0, import_promises3.rm)(tmp, { force: true }).catch(() => {
1347
+ });
1348
+ const reason = err.code ?? (err instanceof Error ? err.message : String(err));
1349
+ throw new NhnCloudCliError(`\uCD9C\uB825 \uD30C\uC77C\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.output} (${reason})`, EXIT_PARAM_ERROR);
1350
+ }
1351
+ if (count >= MAX_TOTAL && total > MAX_TOTAL) {
1352
+ process.stderr.write(
1353
+ `\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.
1354
+ `
1355
+ );
1356
+ }
1357
+ process.stderr.write(`${opts.output} \uC5D0 ${count}\uAC74 \uC800\uC7A5
1358
+ `);
1359
+ });
1360
+ async function scrollNextOrExpire(client, scrollKey) {
1361
+ try {
1362
+ return await client.scrollNext(scrollKey);
1363
+ } catch (err) {
1364
+ if (err instanceof NhnCloudCliError && err.exitCode === EXIT_API_ERROR) {
1365
+ throw new NhnCloudCliError(
1366
+ `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.`,
1367
+ EXIT_API_ERROR
1368
+ );
1369
+ }
1370
+ throw err;
1371
+ }
1372
+ }
1373
+
1374
+ // src/commands/deploy/run.ts
1375
+ var import_commander5 = require("commander");
990
1376
 
991
1377
  // src/services/deploy/client.ts
992
1378
  var import_ky5 = __toESM(require("ky"));
1379
+ function isBinaryGroup(val) {
1380
+ if (typeof val !== "object" || val === null) return false;
1381
+ const obj = val;
1382
+ const keyType = typeof obj["key"];
1383
+ return (keyType === "number" || keyType === "string") && typeof obj["name"] === "string";
1384
+ }
1385
+ function isBinary(val) {
1386
+ if (typeof val !== "object" || val === null) return false;
1387
+ const obj = val;
1388
+ const binaryKeyType = typeof obj["binaryKey"];
1389
+ const binarySizeType = typeof obj["binarySize"];
1390
+ return (binaryKeyType === "number" || binaryKeyType === "string") && (binarySizeType === "number" || binarySizeType === "string");
1391
+ }
993
1392
  var SYNC_TIMEOUT_MS = 6e5;
994
1393
  var DEFAULT_TIMEOUT_MS = 3e4;
995
1394
  var DeployClient = class {
@@ -1010,7 +1409,7 @@ var DeployClient = class {
1010
1409
  * - async=false(기본) 일 때 서버가 완료까지 응답을 보류하므로 ky timeout 을 600s 로 설정한다.
1011
1410
  */
1012
1411
  async run(params) {
1013
- const url = `${this.baseUrl}/api/v2.1/projects/${params.appKey}/artifacts/${params.artifactId}/server-group/${params.serverGroupId}/deploy`;
1412
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(params.appKey)}/artifacts/${encodeURIComponent(params.artifactId)}/server-group/${encodeURIComponent(params.serverGroupId)}/deploy`;
1014
1413
  const isAsync = params.async ?? false;
1015
1414
  const payload = {
1016
1415
  concurrentNum: params.concurrentNum ?? 1,
@@ -1041,7 +1440,7 @@ var DeployClient = class {
1041
1440
  * 아티팩트 목록을 조회한다.
1042
1441
  */
1043
1442
  async artifacts(appKey) {
1044
- const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts`;
1443
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts`;
1045
1444
  try {
1046
1445
  const res = await import_ky5.default.get(url, {
1047
1446
  headers: this.authHeaders(),
@@ -1057,7 +1456,7 @@ var DeployClient = class {
1057
1456
  * 서버그룹 목록을 조회한다.
1058
1457
  */
1059
1458
  async serverGroups(appKey, artifactId) {
1060
- const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts/${artifactId}/server-groups`;
1459
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/server-groups`;
1061
1460
  try {
1062
1461
  const res = await import_ky5.default.get(url, {
1063
1462
  headers: this.authHeaders(),
@@ -1073,7 +1472,7 @@ var DeployClient = class {
1073
1472
  * 배포 이력을 조회한다.
1074
1473
  */
1075
1474
  async histories(appKey, artifactId) {
1076
- const url = `${this.baseUrl}/api/v2.1/projects/${appKey}/artifacts/${artifactId}/deploy-histories`;
1475
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/deploy-histories`;
1077
1476
  try {
1078
1477
  const res = await import_ky5.default.get(url, {
1079
1478
  headers: this.authHeaders(),
@@ -1085,44 +1484,169 @@ var DeployClient = class {
1085
1484
  throw toNhnCloudCliError(err);
1086
1485
  }
1087
1486
  }
1088
- };
1089
-
1090
- // src/commands/deploy/helpers.ts
1091
- async function createDeployClient(profileOpt) {
1092
- const profileName = await resolveProfileName(profileOpt);
1093
- const uak = await getUserAccessKey(profileName);
1094
- const accessToken = await getAccessToken(profileName, uak.id, uak.secret);
1095
- return { client: new DeployClient(accessToken), profileName };
1096
- }
1097
-
1098
- // 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) => {
1100
- const opts = cmd.optsWithGlobals();
1101
- const target = await getDeployTarget(targetName);
1102
- const appKey = opts.appKey ?? target.appKey;
1103
- const artifactId = opts.artifactId ?? target.artifactId;
1104
- const serverGroupId = opts.serverGroupId ?? target.serverGroupId;
1105
- const scenarioIds = opts.scenarioIds ?? target.scenarioIds;
1106
- const { client } = await createDeployClient(opts.profile);
1107
- startSpinner("\uBC30\uD3EC \uC2E4\uD589 \uC911...");
1108
- let result;
1109
- try {
1110
- result = await client.run({
1111
- appKey,
1112
- artifactId,
1113
- serverGroupId,
1114
- scenarioIds,
1115
- targetHosts: opts.targetHosts,
1116
- concurrentNum: parseInt(opts.concurrent ?? "1", 10),
1117
- nextWhenFail: opts.nextWhenFail ?? false,
1118
- deployNote: opts.note,
1119
- async: opts.async ?? false
1120
- });
1121
- } catch (err) {
1122
- stopSpinner(false);
1123
- throw err;
1487
+ /**
1488
+ * 바이너리 그룹 목록을 조회한다.
1489
+ */
1490
+ async binaryGroups(appKey, artifactId) {
1491
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/binary-groups`;
1492
+ try {
1493
+ const res = await import_ky5.default.get(url, {
1494
+ headers: this.authHeaders(),
1495
+ retry: 0,
1496
+ timeout: DEFAULT_TIMEOUT_MS
1497
+ }).json();
1498
+ const body = unwrap(res);
1499
+ const list = body.binaryGroups;
1500
+ if (!Array.isArray(list) || !list.every(isBinaryGroup)) {
1501
+ throw new NhnCloudCliError(
1502
+ "binary-groups \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 binaryGroups \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
1503
+ EXIT_API_ERROR
1504
+ );
1505
+ }
1506
+ return list;
1507
+ } catch (err) {
1508
+ throw toNhnCloudCliError(err);
1509
+ }
1124
1510
  }
1125
- stopSpinner(true);
1511
+ /**
1512
+ * 특정 바이너리 그룹의 바이너리 목록을 조회한다.
1513
+ * pageNum/pageSize/sortKey/sortDirection 은 NHN docs 의 쿼리 파라미터로 그대로 전달한다.
1514
+ */
1515
+ async binaries(appKey, artifactId, binaryGroupKey, params = {}) {
1516
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/binary-groups/${binaryGroupKey}/binaries`;
1517
+ const searchParams = {};
1518
+ if (params.pageNum !== void 0) searchParams["pageNum"] = params.pageNum;
1519
+ if (params.pageSize !== void 0) searchParams["pageSize"] = params.pageSize;
1520
+ if (params.sortKey !== void 0) searchParams["sortKey"] = params.sortKey;
1521
+ if (params.sortDirection !== void 0) searchParams["sortDirection"] = params.sortDirection;
1522
+ try {
1523
+ const res = await import_ky5.default.get(url, {
1524
+ headers: this.authHeaders(),
1525
+ searchParams,
1526
+ retry: 0,
1527
+ timeout: DEFAULT_TIMEOUT_MS
1528
+ }).json();
1529
+ const body = unwrap(res);
1530
+ const list = body.binaries;
1531
+ if (!Array.isArray(list) || !list.every(isBinary)) {
1532
+ throw new NhnCloudCliError(
1533
+ "binaries \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 binaries \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
1534
+ EXIT_API_ERROR
1535
+ );
1536
+ }
1537
+ const tc = body.totalCount;
1538
+ const totalCount = typeof tc === "number" ? tc : typeof tc === "string" && /^\d+$/.test(tc) ? Number(tc) : list.length;
1539
+ return { totalCount, binaries: list };
1540
+ } catch (err) {
1541
+ throw toNhnCloudCliError(err);
1542
+ }
1543
+ }
1544
+ /**
1545
+ * 바이너리를 multipart/form-data 로 업로드한다.
1546
+ *
1547
+ * 신규 전송 경로 — 기존 메서드는 ky `json:`(JSON body) 만 쓴다 (ADR-015).
1548
+ * - 파일 파트(binaryFile)는 command 에서 statSync 가드 후 읽은 Buffer 를 Blob 으로 감싼다.
1549
+ * - Content-Type 은 수동으로 박지 않는다 — ky 가 FormData 에서 multipart boundary 를 자동 설정한다.
1550
+ *
1551
+ * ⚠️ 실측 pending — 수동 QA 로 확정 필요:
1552
+ * - endpoint 경로 세그먼트 단/복수(`binary-group` vs `binary-groups`) — 404 시 복수형으로 교체.
1553
+ * - 응답 binaryKey 타입(number|string) — 코드는 둘 다 수용 후 Number() 정규화.
1554
+ */
1555
+ async uploadBinary(params) {
1556
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(params.appKey)}/artifacts/${encodeURIComponent(params.artifactId)}/binary-group/${params.binaryGroupKey}`;
1557
+ const form = new FormData();
1558
+ const blob = new Blob([new Uint8Array(params.fileBuffer)]);
1559
+ form.append("binaryFile", blob, params.fileName);
1560
+ form.append("applicationType", params.applicationType);
1561
+ if (params.description !== void 0) {
1562
+ form.append("description", params.description);
1563
+ }
1564
+ try {
1565
+ const res = await import_ky5.default.post(url, {
1566
+ headers: this.authHeaders(),
1567
+ // 인증 헤더만 — multipart boundary 는 ky 가 자동 설정
1568
+ body: form,
1569
+ retry: 0,
1570
+ timeout: SYNC_TIMEOUT_MS
1571
+ // 업로드는 파일 크기에 따라 길 수 있어 긴 timeout
1572
+ }).json();
1573
+ const body = unwrap(res);
1574
+ const normalizedKey = Number(body.binaryKey);
1575
+ if (typeof body.downloadUrl !== "string" || !Number.isFinite(normalizedKey)) {
1576
+ throw new NhnCloudCliError(
1577
+ "upload \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 downloadUrl/binaryKey \uB204\uB77D \uB610\uB294 \uBE44\uC22B\uC790.",
1578
+ EXIT_API_ERROR
1579
+ );
1580
+ }
1581
+ return { downloadUrl: body.downloadUrl, binaryKey: normalizedKey };
1582
+ } catch (err) {
1583
+ throw toNhnCloudCliError(err);
1584
+ }
1585
+ }
1586
+ /**
1587
+ * 바이너리를 다운로드해 내용(Buffer)을 반환한다.
1588
+ *
1589
+ * 신규 수신 경로 — 응답이 봉투 JSON 이 아니라 파일 바이너리 스트림이다 (ADR-015).
1590
+ * 다른 메서드처럼 .json()/unwrap 을 쓰면 바이너리를 JSON 으로 파싱하다 깨진다 —
1591
+ * 반드시 .arrayBuffer() 로 받는다. 성공/실패는 HTTP status(ky throwHttpErrors)로만 판정.
1592
+ * 파일 쓰기는 command 가 담당한다 (client 는 내용만 반환 — 테스트 용이).
1593
+ *
1594
+ * ⚠️ 실측 pending — 수동 QA round-trip 으로 확정 필요:
1595
+ * - endpoint 단/복수(`binary-group` vs `binary-groups`) — 404 시 복수형으로 교체.
1596
+ * - 응답이 raw 바이너리인지 downloadUrl JSON 인지 — QA step 5 diff 로 확인.
1597
+ */
1598
+ async downloadBinary(appKey, artifactId, binaryGroupKey, binaryKey) {
1599
+ const url = `${this.baseUrl}/api/v2.1/projects/${encodeURIComponent(appKey)}/artifacts/${encodeURIComponent(artifactId)}/binary-group/${binaryGroupKey}/binaries/${binaryKey}`;
1600
+ try {
1601
+ const ab = await import_ky5.default.get(url, {
1602
+ headers: this.authHeaders(),
1603
+ retry: 0,
1604
+ timeout: SYNC_TIMEOUT_MS
1605
+ // 큰 파일 다운로드 — 긴 timeout
1606
+ }).arrayBuffer();
1607
+ return Buffer.from(ab);
1608
+ } catch (err) {
1609
+ throw toNhnCloudCliError(err);
1610
+ }
1611
+ }
1612
+ };
1613
+
1614
+ // src/commands/deploy/helpers.ts
1615
+ async function createDeployClient(profileOpt) {
1616
+ const profileName = await resolveProfileName(profileOpt);
1617
+ const uak = await getUserAccessKey(profileName);
1618
+ const accessToken = await getAccessToken(profileName, uak.id, uak.secret);
1619
+ return { client: new DeployClient(accessToken), profileName };
1620
+ }
1621
+
1622
+ // src/commands/deploy/run.ts
1623
+ 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) => {
1624
+ const opts = cmd.optsWithGlobals();
1625
+ const target = await getDeployTarget(targetName);
1626
+ const appKey = opts.appKey ?? target.appKey;
1627
+ const artifactId = opts.artifactId ?? target.artifactId;
1628
+ const serverGroupId = opts.serverGroupId ?? target.serverGroupId;
1629
+ const scenarioIds = opts.scenarioIds ?? target.scenarioIds;
1630
+ const { client } = await createDeployClient(opts.profile);
1631
+ startSpinner("\uBC30\uD3EC \uC2E4\uD589 \uC911...");
1632
+ let result;
1633
+ try {
1634
+ result = await client.run({
1635
+ appKey,
1636
+ artifactId,
1637
+ serverGroupId,
1638
+ scenarioIds,
1639
+ targetHosts: opts.targetHosts,
1640
+ concurrentNum: parseInt(opts.concurrent ?? "1", 10),
1641
+ nextWhenFail: opts.nextWhenFail ?? false,
1642
+ deployNote: opts.note,
1643
+ async: opts.async ?? false
1644
+ });
1645
+ } catch (err) {
1646
+ stopSpinner(false);
1647
+ throw err;
1648
+ }
1649
+ stopSpinner(true);
1126
1650
  output(opts, {
1127
1651
  headers: ["key", "value"],
1128
1652
  rows: Object.entries(result).map(([k, v]) => [k, String(v ?? "")]),
@@ -1132,8 +1656,8 @@ var runCommand = new import_commander3.Command("run").description("\uBC30\uD3EC\
1132
1656
  });
1133
1657
 
1134
1658
  // 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) => {
1659
+ var import_commander6 = require("commander");
1660
+ 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
1661
  const opts = cmd.optsWithGlobals();
1138
1662
  let appKey;
1139
1663
  if (opts.appKey) {
@@ -1173,8 +1697,8 @@ var artifactsCommand = new import_commander4.Command("artifacts").description("\
1173
1697
  });
1174
1698
 
1175
1699
  // 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) => {
1700
+ var import_commander7 = require("commander");
1701
+ 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
1702
  const opts = cmd.optsWithGlobals();
1179
1703
  const target = await getDeployTarget(targetName);
1180
1704
  const appKey = opts.appKey ?? target.appKey;
@@ -1198,8 +1722,8 @@ var serverGroupsCommand = new import_commander5.Command("server-groups").descrip
1198
1722
  });
1199
1723
 
1200
1724
  // 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) => {
1725
+ var import_commander8 = require("commander");
1726
+ 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
1727
  const opts = cmd.optsWithGlobals();
1204
1728
  const target = await getDeployTarget(targetName);
1205
1729
  const appKey = opts.appKey ?? target.appKey;
@@ -1222,8 +1746,232 @@ var historiesCommand = new import_commander6.Command("histories").description("\
1222
1746
  });
1223
1747
  });
1224
1748
 
1749
+ // src/commands/deploy/binary-groups.ts
1750
+ var import_commander9 = require("commander");
1751
+ 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) => {
1752
+ const opts = cmd.optsWithGlobals();
1753
+ const target = await getDeployTarget(targetName);
1754
+ const appKey = opts.appKey ?? target.appKey;
1755
+ const artifactId = opts.artifactId ?? target.artifactId;
1756
+ const { client } = await createDeployClient(opts.profile);
1757
+ startSpinner("\uBC14\uC774\uB108\uB9AC \uADF8\uB8F9 \uBAA9\uB85D \uC870\uD68C \uC911...");
1758
+ let groups;
1759
+ try {
1760
+ groups = await client.binaryGroups(appKey, artifactId);
1761
+ } catch (err) {
1762
+ stopSpinner(false);
1763
+ throw err;
1764
+ }
1765
+ stopSpinner(true);
1766
+ output(opts, {
1767
+ headers: ["key", "name", "regionCode", "createDate", "description"],
1768
+ // 가드는 key·name 만 검증 — 나머지 필드는 누락 시 "undefined" 가 박히지 않게 ?? "" 방어.
1769
+ rows: groups.map((g) => [
1770
+ String(g.key),
1771
+ g.name ?? "",
1772
+ g.regionCode ?? "",
1773
+ g.createDate ?? "",
1774
+ g.description ?? ""
1775
+ ]),
1776
+ raw: groups,
1777
+ // ids 에 key 를 넣어 --quiet 시 그룹 key 만 출력 → binaries --binary-group 에 파이프 가능
1778
+ ids: groups.map((g) => String(g.key))
1779
+ });
1780
+ });
1781
+
1782
+ // src/commands/deploy/binaries.ts
1783
+ var import_commander10 = require("commander");
1784
+ function parsePositiveInt(value, flag) {
1785
+ if (value === void 0) return void 0;
1786
+ if (!/^[1-9]\d*$/.test(value)) {
1787
+ throw new NhnCloudCliError(
1788
+ `${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${JSON.stringify(value)}).`,
1789
+ EXIT_PARAM_ERROR
1790
+ );
1791
+ }
1792
+ return Number(value);
1793
+ }
1794
+ 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) => {
1795
+ const opts = cmd.optsWithGlobals();
1796
+ const binaryGroupKey = parsePositiveInt(opts.binaryGroup, "--binary-group");
1797
+ if (binaryGroupKey === void 0) {
1798
+ throw new NhnCloudCliError("--binary-group \uC774 \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
1799
+ }
1800
+ const pageNum = parsePositiveInt(opts.pageNum, "--page-num");
1801
+ const pageSize = parsePositiveInt(opts.pageSize, "--page-size");
1802
+ const target = await getDeployTarget(targetName);
1803
+ const appKey = opts.appKey ?? target.appKey;
1804
+ const artifactId = opts.artifactId ?? target.artifactId;
1805
+ const { client } = await createDeployClient(opts.profile);
1806
+ startSpinner("\uBC14\uC774\uB108\uB9AC \uBAA9\uB85D \uC870\uD68C \uC911...");
1807
+ let result;
1808
+ try {
1809
+ result = await client.binaries(appKey, artifactId, binaryGroupKey, {
1810
+ pageNum,
1811
+ pageSize,
1812
+ sortKey: opts.sortKey,
1813
+ sortDirection: opts.sortDirection
1814
+ });
1815
+ } catch (err) {
1816
+ stopSpinner(false);
1817
+ throw err;
1818
+ }
1819
+ stopSpinner(true);
1820
+ output(opts, {
1821
+ headers: ["binaryKey", "version", "binaryName", "size(bytes)", "uploadDate", "uploader"],
1822
+ // 가드는 binaryKey·binarySize 만 검증 — 나머지 필드는 응답에서 누락 시 "undefined" 가
1823
+ // 표에 박히지 않게 ?? "" 로 방어한다 (타입 정합성 실측은 후속 이슈).
1824
+ rows: result.binaries.map((b) => [
1825
+ String(b.binaryKey),
1826
+ b.version ?? "",
1827
+ b.binaryName ?? "",
1828
+ String(b.binarySize),
1829
+ b.uploadDate ?? "",
1830
+ b.uploader ?? ""
1831
+ ]),
1832
+ // raw 에 totalCount 포함 → --json 으로 페이지 정보 확인 가능
1833
+ raw: result,
1834
+ ids: result.binaries.map((b) => String(b.binaryKey))
1835
+ });
1836
+ });
1837
+
1838
+ // src/commands/deploy/upload.ts
1839
+ var import_commander11 = require("commander");
1840
+ var import_node_fs3 = require("fs");
1841
+ var import_node_path3 = require("path");
1842
+ var MAX_UPLOAD_BYTES = 512 * 1024 * 1024;
1843
+ function parsePositiveInt2(value, flag) {
1844
+ if (value === void 0) return void 0;
1845
+ if (!/^[1-9]\d*$/.test(value)) {
1846
+ throw new NhnCloudCliError(
1847
+ `${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${JSON.stringify(value)}).`,
1848
+ EXIT_PARAM_ERROR
1849
+ );
1850
+ }
1851
+ return Number(value);
1852
+ }
1853
+ 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) => {
1854
+ const opts = cmd.optsWithGlobals();
1855
+ const binaryGroupKey = parsePositiveInt2(opts.binaryGroup, "--binary-group");
1856
+ if (binaryGroupKey === void 0) {
1857
+ throw new NhnCloudCliError("--binary-group \uC774 \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
1858
+ }
1859
+ const filePath = opts.file;
1860
+ let stat;
1861
+ try {
1862
+ stat = (0, import_node_fs3.statSync)(filePath);
1863
+ } catch (e) {
1864
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
1865
+ throw new NhnCloudCliError(
1866
+ `--file \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath} (${reason})`,
1867
+ EXIT_PARAM_ERROR
1868
+ );
1869
+ }
1870
+ if (!stat.isFile()) {
1871
+ throw new NhnCloudCliError(`--file \uC774 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${filePath}`, EXIT_PARAM_ERROR);
1872
+ }
1873
+ if (stat.size > MAX_UPLOAD_BYTES) {
1874
+ throw new NhnCloudCliError(
1875
+ `--file \uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${stat.size} \uBC14\uC774\uD2B8). \uC5C5\uB85C\uB4DC \uD55C\uB3C4 ${MAX_UPLOAD_BYTES} \uBC14\uC774\uD2B8.`,
1876
+ EXIT_PARAM_ERROR
1877
+ );
1878
+ }
1879
+ const fileBuffer = (0, import_node_fs3.readFileSync)(filePath);
1880
+ const fileName = (0, import_node_path3.basename)(filePath);
1881
+ const target = await getDeployTarget(targetName);
1882
+ const appKey = opts.appKey ?? target.appKey;
1883
+ const artifactId = opts.artifactId ?? target.artifactId;
1884
+ const { client } = await createDeployClient(opts.profile);
1885
+ startSpinner("\uBC14\uC774\uB108\uB9AC \uC5C5\uB85C\uB4DC \uC911...");
1886
+ let result;
1887
+ try {
1888
+ result = await client.uploadBinary({
1889
+ appKey,
1890
+ artifactId,
1891
+ binaryGroupKey,
1892
+ fileBuffer,
1893
+ fileName,
1894
+ applicationType: opts.applicationType,
1895
+ // Commander 옵션 기본값 "server" 가 SSOT — dead fallback 제거
1896
+ description: opts.description
1897
+ });
1898
+ } catch (err) {
1899
+ stopSpinner(false);
1900
+ throw err;
1901
+ }
1902
+ stopSpinner(true);
1903
+ output(opts, {
1904
+ headers: ["field", "value"],
1905
+ rows: [
1906
+ ["binaryKey", String(result.binaryKey)],
1907
+ ["downloadUrl", result.downloadUrl]
1908
+ ],
1909
+ raw: result,
1910
+ ids: [String(result.binaryKey)]
1911
+ });
1912
+ });
1913
+
1914
+ // src/commands/deploy/download.ts
1915
+ var import_commander12 = require("commander");
1916
+ var import_node_fs4 = require("fs");
1917
+ var import_chalk2 = __toESM(require("chalk"));
1918
+ function parsePositiveInt3(value, flag) {
1919
+ if (value === void 0) return void 0;
1920
+ if (!/^[1-9]\d*$/.test(value)) {
1921
+ throw new NhnCloudCliError(
1922
+ `${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${JSON.stringify(value)}).`,
1923
+ EXIT_PARAM_ERROR
1924
+ );
1925
+ }
1926
+ return Number(value);
1927
+ }
1928
+ function assertWritable2(path, force) {
1929
+ if (force) return;
1930
+ try {
1931
+ (0, import_node_fs4.statSync)(path);
1932
+ } catch (e) {
1933
+ if (e.code === "ENOENT") return;
1934
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
1935
+ throw new NhnCloudCliError(
1936
+ `-o \uACBD\uB85C\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${path} (${reason})`,
1937
+ EXIT_PARAM_ERROR
1938
+ );
1939
+ }
1940
+ throw new NhnCloudCliError(
1941
+ `-o \uB300\uC0C1\uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: ${path}. \uB36E\uC5B4\uC4F0\uB824\uBA74 --force \uB97C \uC4F0\uC138\uC694.`,
1942
+ EXIT_PARAM_ERROR
1943
+ );
1944
+ }
1945
+ 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) => {
1946
+ const opts = cmd.optsWithGlobals();
1947
+ const binaryGroupKey = parsePositiveInt3(opts.binaryGroup, "--binary-group");
1948
+ const binaryKey = parsePositiveInt3(opts.binaryKey, "--binary-key");
1949
+ if (binaryGroupKey === void 0 || binaryKey === void 0) {
1950
+ throw new NhnCloudCliError("--binary-group / --binary-key \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
1951
+ }
1952
+ const outPath = opts.output;
1953
+ assertWritable2(outPath, opts.force ?? false);
1954
+ const target = await getDeployTarget(targetName);
1955
+ const appKey = opts.appKey ?? target.appKey;
1956
+ const artifactId = opts.artifactId ?? target.artifactId;
1957
+ const { client } = await createDeployClient(opts.profile);
1958
+ startSpinner("\uBC14\uC774\uB108\uB9AC \uB2E4\uC6B4\uB85C\uB4DC \uC911...");
1959
+ try {
1960
+ const buffer = await client.downloadBinary(appKey, artifactId, binaryGroupKey, binaryKey);
1961
+ (0, import_node_fs4.writeFileSync)(outPath, buffer);
1962
+ } catch (err) {
1963
+ stopSpinner(false);
1964
+ throw err;
1965
+ }
1966
+ stopSpinner(true);
1967
+ if (!opts.quiet) {
1968
+ process.stderr.write(import_chalk2.default.green(` \uC800\uC7A5\uB428: ${outPath}
1969
+ `));
1970
+ }
1971
+ });
1972
+
1225
1973
  // src/commands/instance/list.ts
1226
- var import_commander7 = require("commander");
1974
+ var import_commander13 = require("commander");
1227
1975
 
1228
1976
  // src/services/instance/client.ts
1229
1977
  var import_ky6 = __toESM(require("ky"));
@@ -1244,21 +1992,108 @@ function isServersResponse(val) {
1244
1992
  const obj = val;
1245
1993
  return Array.isArray(obj["servers"]);
1246
1994
  }
1995
+ function isFlavor(val) {
1996
+ if (typeof val !== "object" || val === null) return false;
1997
+ const obj = val;
1998
+ return typeof obj["id"] === "string" && typeof obj["name"] === "string";
1999
+ }
2000
+ function isImage(val) {
2001
+ if (typeof val !== "object" || val === null) return false;
2002
+ const obj = val;
2003
+ return typeof obj["id"] === "string" && // Glance v2 스펙상 name 은 nullable — null 인 private 이미지 하나가 페이지 전체를 거부하지 않게 허용.
2004
+ (typeof obj["name"] === "string" || obj["name"] === null) && typeof obj["status"] === "string" && typeof obj["visibility"] === "string";
2005
+ }
2006
+ function isImagesResponse(val) {
2007
+ if (typeof val !== "object" || val === null) return false;
2008
+ const obj = val;
2009
+ const nextOk = obj["next"] === void 0 || typeof obj["next"] === "string";
2010
+ return Array.isArray(obj["images"]) && obj["images"].every(isImage) && nextOk;
2011
+ }
2012
+ function isFlavorsResponse(val) {
2013
+ if (typeof val !== "object" || val === null) return false;
2014
+ const obj = val;
2015
+ return Array.isArray(obj["flavors"]) && obj["flavors"].every(isFlavor);
2016
+ }
2017
+ function isFlavorDetail(val) {
2018
+ if (typeof val !== "object" || val === null) return false;
2019
+ const obj = val;
2020
+ return typeof obj["id"] === "string" && typeof obj["name"] === "string" && typeof obj["vcpus"] === "number" && typeof obj["ram"] === "number" && typeof obj["disk"] === "number";
2021
+ }
2022
+ function isFlavorDetailsResponse(val) {
2023
+ if (typeof val !== "object" || val === null) return false;
2024
+ const obj = val;
2025
+ return Array.isArray(obj["flavors"]) && obj["flavors"].every(isFlavorDetail);
2026
+ }
1247
2027
  function isCreateResponse(val) {
1248
2028
  if (typeof val !== "object" || val === null) return false;
1249
2029
  const server = val["server"];
1250
2030
  if (typeof server !== "object" || server === null) return false;
1251
2031
  return typeof server["id"] === "string";
1252
2032
  }
2033
+ function isKeypair(val) {
2034
+ if (typeof val !== "object" || val === null) return false;
2035
+ const obj = val;
2036
+ return typeof obj["name"] === "string" && typeof obj["public_key"] === "string" && typeof obj["fingerprint"] === "string";
2037
+ }
2038
+ function isKeypairsResponse(val) {
2039
+ if (typeof val !== "object" || val === null) return false;
2040
+ const obj = val;
2041
+ return Array.isArray(obj["keypairs"]) && obj["keypairs"].every((e) => {
2042
+ if (typeof e !== "object" || e === null) return false;
2043
+ return isKeypair(e["keypair"]);
2044
+ });
2045
+ }
2046
+ function isCreateKeypairResponse(val) {
2047
+ if (typeof val !== "object" || val === null) return false;
2048
+ const obj = val;
2049
+ return isKeypair(obj["keypair"]);
2050
+ }
2051
+ function isKeypairDetail(val) {
2052
+ if (typeof val !== "object" || val === null) return false;
2053
+ const obj = val;
2054
+ 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";
2055
+ }
2056
+ function isKeypairDetailResponse(val) {
2057
+ if (typeof val !== "object" || val === null) return false;
2058
+ return isKeypairDetail(val["keypair"]);
2059
+ }
2060
+ function isAvailabilityZone(val) {
2061
+ if (typeof val !== "object" || val === null) return false;
2062
+ const obj = val;
2063
+ const state = obj["zoneState"];
2064
+ if (typeof state !== "object" || state === null) return false;
2065
+ return typeof obj["zoneName"] === "string" && typeof state["available"] === "boolean";
2066
+ }
2067
+ function isAvailabilityZonesResponse(val) {
2068
+ if (typeof val !== "object" || val === null) return false;
2069
+ const obj = val;
2070
+ return Array.isArray(obj["availabilityZoneInfo"]) && obj["availabilityZoneInfo"].every(isAvailabilityZone);
2071
+ }
2072
+ function isServerVolumeAttachment(val) {
2073
+ if (typeof val !== "object" || val === null) return false;
2074
+ const o = val;
2075
+ return typeof o["id"] === "string" && typeof o["volumeId"] === "string" && typeof o["serverId"] === "string" && typeof o["device"] === "string";
2076
+ }
2077
+ function isVolumeAttachmentsResponse(val) {
2078
+ if (typeof val !== "object" || val === null) return false;
2079
+ const arr = val["volumeAttachments"];
2080
+ return Array.isArray(arr) && arr.every(isServerVolumeAttachment);
2081
+ }
2082
+ function isVolumeAttachmentResponse(val) {
2083
+ if (typeof val !== "object" || val === null) return false;
2084
+ return isServerVolumeAttachment(val["volumeAttachment"]);
2085
+ }
1253
2086
  function hasIpAddress(server) {
1254
2087
  return Object.values(server.addresses).some((list) => list.length > 0);
1255
2088
  }
1256
2089
  var InstanceClient = class {
1257
2090
  tokenId;
1258
2091
  computeEndpoint;
1259
- constructor(tokenId, computeEndpoint) {
2092
+ imageEndpoint;
2093
+ constructor(tokenId, computeEndpoint, imageEndpoint) {
1260
2094
  this.tokenId = tokenId;
1261
2095
  this.computeEndpoint = computeEndpoint;
2096
+ this.imageEndpoint = imageEndpoint;
1262
2097
  }
1263
2098
  authHeaders() {
1264
2099
  return { "X-Auth-Token": this.tokenId };
@@ -1310,6 +2145,7 @@ var InstanceClient = class {
1310
2145
  /**
1311
2146
  * 인스턴스를 생성한다 (POST /servers).
1312
2147
  * NHN 확장 필드(ephemeralDiskSize / protect)는 정의됐을 때만 payload 에 포함한다.
2148
+ * userDataBase64 도 정의됐을 때만 `user_data` 로 포함한다 (인코딩은 command 단에서 완료).
1313
2149
  */
1314
2150
  async create(params) {
1315
2151
  const url = `${this.computeEndpoint}/servers`;
@@ -1344,6 +2180,9 @@ var InstanceClient = class {
1344
2180
  if (params.protect !== void 0) {
1345
2181
  serverBody["NHN-EXT-ATTR:protect"] = params.protect;
1346
2182
  }
2183
+ if (params.userDataBase64 !== void 0) {
2184
+ serverBody["user_data"] = params.userDataBase64;
2185
+ }
1347
2186
  let raw;
1348
2187
  try {
1349
2188
  raw = await import_ky6.default.post(url, {
@@ -1379,18 +2218,291 @@ var InstanceClient = class {
1379
2218
  }
1380
2219
  }
1381
2220
  /**
1382
- * 인스턴스가 ACTIVE 상태 + IP 1개 이상이 될 때까지 폴링한다.
1383
- *
1384
- * - status === "ACTIVE" + addresses IP 1개 이상: 즉시 반환
1385
- * - timeout 초과: 마지막 status 포함한 NhnCloudCliError(EXIT_API_ERROR)
2221
+ * 서버 action 실행한다 (POST /servers/{id}/action, 202 무본문).
2222
+ * NHN Cloud(OpenStack Nova)의 모든 전원·라이프사이클 action 의 공용 경로다.
2223
+ * payload 호출자가 action 별로 구성한다 (예: { "os-start": null }).
2224
+ * start/stop/reboot helper재사용하며, resize/shelve 등 향후 action 도 동일.
1386
2225
  */
1387
- async waitForActive(id, opts) {
1388
- const intervalMs = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1389
- const deadline = Date.now() + opts.timeoutMs;
1390
- let lastServer = null;
1391
- while (Date.now() < deadline) {
1392
- const server = await this.get(id);
1393
- lastServer = server;
2226
+ async serverAction(id, payload) {
2227
+ const url = `${this.computeEndpoint}/servers/${encodeURIComponent(id)}/action`;
2228
+ try {
2229
+ await import_ky6.default.post(url, {
2230
+ headers: this.authHeaders(),
2231
+ json: payload,
2232
+ retry: 0,
2233
+ timeout: DEFAULT_TIMEOUT_MS2
2234
+ });
2235
+ } catch (err) {
2236
+ throw toNhnCloudCliError(err);
2237
+ }
2238
+ }
2239
+ /** 인스턴스를 시작한다 (SHUTOFF → ACTIVE). */
2240
+ async start(id) {
2241
+ return this.serverAction(id, { "os-start": null });
2242
+ }
2243
+ /** 인스턴스를 정지한다 (ACTIVE/ERROR → SHUTOFF). */
2244
+ async stop(id) {
2245
+ return this.serverAction(id, { "os-stop": null });
2246
+ }
2247
+ /** 인스턴스를 재부팅한다. type 기본 SOFT, HARD 는 강제 전원 cycle. */
2248
+ async reboot(id, type = "SOFT") {
2249
+ return this.serverAction(id, { reboot: { type } });
2250
+ }
2251
+ /**
2252
+ * 인스턴스 타입(flavor)을 변경한다 (resize action).
2253
+ * POST /servers/{id}/action body { "resize": { "flavorRef": "<flavor-id>" } }, 202 무본문.
2254
+ * 사전 상태는 ACTIVE 또는 SHUTOFF (ACTIVE 면 NHN 측에서 중지 후 재시작).
2255
+ */
2256
+ async resize(id, flavorRef) {
2257
+ return this.serverAction(id, { resize: { flavorRef } });
2258
+ }
2259
+ /** resize 를 확정한다 (VERIFY_RESIZE → ACTIVE, 새 flavor 로 고정). */
2260
+ async confirmResize(id) {
2261
+ return this.serverAction(id, { confirmResize: null });
2262
+ }
2263
+ /** resize 를 롤백한다 (VERIFY_RESIZE → ACTIVE, 이전 flavor 로 복귀). */
2264
+ async revertResize(id) {
2265
+ return this.serverAction(id, { revertResize: null });
2266
+ }
2267
+ async listFlavors(params = {}) {
2268
+ const path = params.detail ? "/flavors/detail" : "/flavors";
2269
+ const url = `${this.computeEndpoint}${path}`;
2270
+ const searchParams = {};
2271
+ if (params.minDisk !== void 0) searchParams["minDisk"] = params.minDisk;
2272
+ if (params.minRam !== void 0) searchParams["minRam"] = params.minRam;
2273
+ try {
2274
+ const raw = await import_ky6.default.get(url, {
2275
+ headers: this.authHeaders(),
2276
+ searchParams,
2277
+ retry: 0,
2278
+ timeout: DEFAULT_TIMEOUT_MS2
2279
+ }).json();
2280
+ if (params.detail) {
2281
+ if (!isFlavorDetailsResponse(raw)) {
2282
+ throw new NhnCloudCliError(
2283
+ "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.",
2284
+ EXIT_API_ERROR
2285
+ );
2286
+ }
2287
+ return raw.flavors;
2288
+ }
2289
+ if (!isFlavorsResponse(raw)) {
2290
+ throw new NhnCloudCliError(
2291
+ "instance flavors \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 flavors \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
2292
+ EXIT_API_ERROR
2293
+ );
2294
+ }
2295
+ return raw.flavors;
2296
+ } catch (err) {
2297
+ throw toNhnCloudCliError(err);
2298
+ }
2299
+ }
2300
+ /**
2301
+ * 가용성 영역(availability zone) 목록을 조회한다 (GET /os-availability-zone).
2302
+ * zoneName·가용 여부(available)를 반환하며 페이지네이션·필터 없음.
2303
+ */
2304
+ async listAvailabilityZones() {
2305
+ const url = `${this.computeEndpoint}/os-availability-zone`;
2306
+ try {
2307
+ const raw = await import_ky6.default.get(url, {
2308
+ headers: this.authHeaders(),
2309
+ retry: 0,
2310
+ timeout: DEFAULT_TIMEOUT_MS2
2311
+ }).json();
2312
+ if (!isAvailabilityZonesResponse(raw)) {
2313
+ throw new NhnCloudCliError(
2314
+ "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.",
2315
+ EXIT_API_ERROR
2316
+ );
2317
+ }
2318
+ return raw.availabilityZoneInfo;
2319
+ } catch (err) {
2320
+ throw toNhnCloudCliError(err);
2321
+ }
2322
+ }
2323
+ /** 키페어 목록을 조회한다 (GET /os-keypairs). 응답 원소의 한겹(keypair)을 풀어 반환. */
2324
+ async listKeypairs() {
2325
+ const url = `${this.computeEndpoint}/os-keypairs`;
2326
+ try {
2327
+ const raw = await import_ky6.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 }).json();
2328
+ if (!isKeypairsResponse(raw)) {
2329
+ throw new NhnCloudCliError(
2330
+ "instance keypairs \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 keypairs \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
2331
+ EXIT_API_ERROR
2332
+ );
2333
+ }
2334
+ return raw.keypairs.map((e) => e.keypair);
2335
+ } catch (err) {
2336
+ throw toNhnCloudCliError(err);
2337
+ }
2338
+ }
2339
+ /** 단일 키페어를 조회한다 (GET /os-keypairs/{name}). */
2340
+ async getKeypair(name) {
2341
+ const url = `${this.computeEndpoint}/os-keypairs/${encodeURIComponent(name)}`;
2342
+ try {
2343
+ const raw = await import_ky6.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 }).json();
2344
+ if (!isKeypairDetailResponse(raw)) {
2345
+ throw new NhnCloudCliError(
2346
+ `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.`,
2347
+ EXIT_API_ERROR
2348
+ );
2349
+ }
2350
+ return raw.keypair;
2351
+ } catch (err) {
2352
+ throw toNhnCloudCliError(err);
2353
+ }
2354
+ }
2355
+ /**
2356
+ * 키페어를 생성한다 (POST /os-keypairs).
2357
+ * publicKey 미지정이면 NHN 이 키쌍을 생성하고 응답 keypair 에 private_key 가 1회성으로 포함된다.
2358
+ * publicKey 지정이면 기존 공개키를 등록하고 private_key 는 응답에 없다.
2359
+ */
2360
+ async createKeypair(params) {
2361
+ const url = `${this.computeEndpoint}/os-keypairs`;
2362
+ const keypairBody = { name: params.name };
2363
+ if (params.publicKey !== void 0) {
2364
+ keypairBody["public_key"] = params.publicKey;
2365
+ }
2366
+ let raw;
2367
+ try {
2368
+ raw = await import_ky6.default.post(url, {
2369
+ headers: this.authHeaders(),
2370
+ json: { keypair: keypairBody },
2371
+ retry: 0,
2372
+ timeout: DEFAULT_TIMEOUT_MS2
2373
+ }).json();
2374
+ } catch (err) {
2375
+ throw toNhnCloudCliError(err);
2376
+ }
2377
+ if (!isCreateKeypairResponse(raw)) {
2378
+ throw new NhnCloudCliError(
2379
+ "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.",
2380
+ EXIT_API_ERROR
2381
+ );
2382
+ }
2383
+ const kp = raw.keypair;
2384
+ return {
2385
+ name: kp.name,
2386
+ public_key: kp.public_key,
2387
+ fingerprint: kp.fingerprint,
2388
+ user_id: kp.user_id ?? "",
2389
+ // 빈 문자열은 정의되지 않은 것과 동일 취급 — 빈 키 파일 저장/빈 줄 출력 방지.
2390
+ private_key: kp.private_key !== void 0 && kp.private_key.length > 0 ? kp.private_key : void 0
2391
+ };
2392
+ }
2393
+ /** 키페어를 삭제한다 (DELETE /os-keypairs/{name}, 202/204 무응답). */
2394
+ async deleteKeypair(name) {
2395
+ const url = `${this.computeEndpoint}/os-keypairs/${encodeURIComponent(name)}`;
2396
+ try {
2397
+ await import_ky6.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 });
2398
+ } catch (err) {
2399
+ throw toNhnCloudCliError(err);
2400
+ }
2401
+ }
2402
+ /**
2403
+ * 이미지 목록을 조회한다 (GET /v2/images, Glance v2).
2404
+ * compute 와 다른 host(imageEndpoint)지만 같은 Keystone 토큰을 쓴다.
2405
+ * 한 페이지(기본 limit 25)만 반환한다 — next 가 있으면 호출부가 marker 로 이어 받는다.
2406
+ */
2407
+ async listImages(params = {}) {
2408
+ const url = `${this.imageEndpoint}/images`;
2409
+ const searchParams = {};
2410
+ if (params.limit !== void 0) searchParams["limit"] = params.limit;
2411
+ if (params.marker !== void 0) searchParams["marker"] = params.marker;
2412
+ if (params.name !== void 0) searchParams["name"] = params.name;
2413
+ if (params.visibility !== void 0) searchParams["visibility"] = params.visibility;
2414
+ if (params.owner !== void 0) searchParams["owner"] = params.owner;
2415
+ if (params.status !== void 0) searchParams["status"] = params.status;
2416
+ try {
2417
+ const raw = await import_ky6.default.get(url, {
2418
+ headers: this.authHeaders(),
2419
+ searchParams,
2420
+ retry: 0,
2421
+ timeout: DEFAULT_TIMEOUT_MS2
2422
+ }).json();
2423
+ if (!isImagesResponse(raw)) {
2424
+ throw new NhnCloudCliError(
2425
+ "instance images \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 images \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
2426
+ EXIT_API_ERROR
2427
+ );
2428
+ }
2429
+ return { images: raw.images, next: raw.next };
2430
+ } catch (err) {
2431
+ throw toNhnCloudCliError(err);
2432
+ }
2433
+ }
2434
+ /**
2435
+ * 인스턴스에 연결된 볼륨 목록을 조회한다 (GET .../os-volume_attachments).
2436
+ * Nova 표준 확장 — NHN Instance(Nova v2 호환, ADR-010). 실측 200 확인 (2026-06-11).
2437
+ */
2438
+ async listVolumeAttachments(serverId) {
2439
+ const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments`;
2440
+ try {
2441
+ const raw = await import_ky6.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 }).json();
2442
+ if (!isVolumeAttachmentsResponse(raw)) {
2443
+ throw new NhnCloudCliError(
2444
+ "instance volumes \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volumeAttachments \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
2445
+ EXIT_API_ERROR
2446
+ );
2447
+ }
2448
+ return raw.volumeAttachments;
2449
+ } catch (err) {
2450
+ throw toNhnCloudCliError(err);
2451
+ }
2452
+ }
2453
+ /**
2454
+ * 볼륨을 인스턴스에 연결한다 (POST .../os-volume_attachments).
2455
+ * 요청 body: { volumeAttachment: { volumeId } }. 실제 연결은 수동 QA 확정 (1-26).
2456
+ */
2457
+ async attachVolume(serverId, volumeId) {
2458
+ const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments`;
2459
+ try {
2460
+ const res = await import_ky6.default.post(url, {
2461
+ headers: this.authHeaders(),
2462
+ json: { volumeAttachment: { volumeId } },
2463
+ retry: 0,
2464
+ timeout: DEFAULT_TIMEOUT_MS2
2465
+ });
2466
+ if (res.status === 202 || res.headers.get("content-length") === "0") {
2467
+ return { id: volumeId, volumeId, serverId, device: "" };
2468
+ }
2469
+ const raw = await res.json();
2470
+ if (!isVolumeAttachmentResponse(raw)) {
2471
+ throw new NhnCloudCliError(
2472
+ "instance volume attach \uC751\uB2F5\uC5D0 volumeAttachment \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
2473
+ EXIT_API_ERROR
2474
+ );
2475
+ }
2476
+ return raw.volumeAttachment;
2477
+ } catch (err) {
2478
+ throw toNhnCloudCliError(err);
2479
+ }
2480
+ }
2481
+ /**
2482
+ * 볼륨 연결을 해제한다 (DELETE .../os-volume_attachments/{volumeId}, 202 무응답).
2483
+ * 실제 해제는 수동 QA 확정 (1-26).
2484
+ */
2485
+ async detachVolume(serverId, volumeId) {
2486
+ const url = `${this.computeEndpoint}/servers/${encodeURIComponent(serverId)}/os-volume_attachments/${encodeURIComponent(volumeId)}`;
2487
+ try {
2488
+ await import_ky6.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS2 });
2489
+ } catch (err) {
2490
+ throw toNhnCloudCliError(err);
2491
+ }
2492
+ }
2493
+ /**
2494
+ * 인스턴스가 ACTIVE 상태 + IP 1개 이상이 될 때까지 폴링한다.
2495
+ *
2496
+ * - status === "ACTIVE" + addresses 에 IP 1개 이상: 즉시 반환
2497
+ * - timeout 초과: 마지막 status 를 포함한 NhnCloudCliError(EXIT_API_ERROR)
2498
+ */
2499
+ async waitForActive(id, opts) {
2500
+ const intervalMs = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
2501
+ const deadline = Date.now() + opts.timeoutMs;
2502
+ let lastServer = null;
2503
+ while (Date.now() < deadline) {
2504
+ const server = await this.get(id);
2505
+ lastServer = server;
1394
2506
  if (server.status === "ACTIVE" && hasIpAddress(server)) {
1395
2507
  return server;
1396
2508
  }
@@ -1404,50 +2516,691 @@ var InstanceClient = class {
1404
2516
  EXIT_API_ERROR
1405
2517
  );
1406
2518
  }
1407
- };
2519
+ };
2520
+
2521
+ // src/commands/instance/helpers.ts
2522
+ async function resolveInstanceClient(opts) {
2523
+ const profileName = await resolveProfileName(opts.profile);
2524
+ const iaas = await getIaasCredential(profileName);
2525
+ const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
2526
+ const { tokenId, computeEndpoint, imageEndpoint } = await getIaasToken(profileName, effectiveIaas);
2527
+ return { client: new InstanceClient(tokenId, computeEndpoint, imageEndpoint), profileName };
2528
+ }
2529
+
2530
+ // src/commands/instance/list.ts
2531
+ function getIps(server) {
2532
+ return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
2533
+ }
2534
+ 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) => {
2535
+ const opts = cmd.optsWithGlobals();
2536
+ const { client } = await resolveInstanceClient(opts);
2537
+ startSpinner("\uC778\uC2A4\uD134\uC2A4 \uBAA9\uB85D \uC870\uD68C \uC911...");
2538
+ let servers;
2539
+ try {
2540
+ servers = await client.list();
2541
+ } catch (err) {
2542
+ stopSpinner(false);
2543
+ throw err;
2544
+ }
2545
+ stopSpinner(true);
2546
+ output(opts, {
2547
+ headers: ["id", "name", "status", "IPs", "flavor"],
2548
+ rows: servers.map((s) => [s.id, s.name, s.status, getIps(s), s.flavor.id]),
2549
+ raw: servers,
2550
+ ids: servers.map((s) => s.id)
2551
+ });
2552
+ });
2553
+
2554
+ // src/commands/volume/list.ts
2555
+ var import_commander14 = require("commander");
2556
+
2557
+ // src/services/blockstorage/client.ts
2558
+ var import_ky7 = __toESM(require("ky"));
2559
+ var DEFAULT_TIMEOUT_MS3 = 3e4;
2560
+ function isVolume(val) {
2561
+ if (typeof val !== "object" || val === null) return false;
2562
+ const obj = val;
2563
+ return typeof obj["id"] === "string" && // Cinder 는 --name 미지정 시 null — null 인 볼륨 하나가 list 전체를 거부하지 않게 허용 (isImage 선례).
2564
+ (typeof obj["name"] === "string" || obj["name"] === null) && typeof obj["size"] === "number" && typeof obj["status"] === "string" && Array.isArray(obj["attachments"]);
2565
+ }
2566
+ function isVolumesResponse(val) {
2567
+ if (typeof val !== "object" || val === null) return false;
2568
+ const obj = val;
2569
+ return Array.isArray(obj["volumes"]) && obj["volumes"].every(isVolume);
2570
+ }
2571
+ function isVolumeResponse(val) {
2572
+ if (typeof val !== "object" || val === null) return false;
2573
+ const obj = val;
2574
+ return isVolume(obj["volume"]);
2575
+ }
2576
+ var BlockStorageClient = class {
2577
+ tokenId;
2578
+ endpoint;
2579
+ // blockStorageEndpoint (/v2/{tenantId} 까지 포함)
2580
+ constructor(tokenId, blockStorageEndpoint) {
2581
+ this.tokenId = tokenId;
2582
+ this.endpoint = blockStorageEndpoint;
2583
+ }
2584
+ authHeaders() {
2585
+ return { "X-Auth-Token": this.tokenId };
2586
+ }
2587
+ async list(params) {
2588
+ const url = `${this.endpoint}/volumes`;
2589
+ const searchParams = {};
2590
+ if (params?.sort !== void 0) searchParams["sort"] = params.sort;
2591
+ if (params?.limit !== void 0) searchParams["limit"] = params.limit;
2592
+ if (params?.offset !== void 0) searchParams["offset"] = params.offset;
2593
+ if (params?.marker !== void 0) searchParams["marker"] = params.marker;
2594
+ try {
2595
+ const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), searchParams, retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
2596
+ if (!isVolumesResponse(raw)) {
2597
+ throw new NhnCloudCliError(
2598
+ "volume list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volumes \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
2599
+ EXIT_API_ERROR
2600
+ );
2601
+ }
2602
+ return raw.volumes;
2603
+ } catch (err) {
2604
+ throw toNhnCloudCliError(err);
2605
+ }
2606
+ }
2607
+ async get(id) {
2608
+ const url = `${this.endpoint}/volumes/${encodeURIComponent(id)}`;
2609
+ try {
2610
+ const raw = await import_ky7.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS3 }).json();
2611
+ if (!isVolumeResponse(raw)) {
2612
+ throw new NhnCloudCliError(
2613
+ "volume get \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 volume \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
2614
+ EXIT_API_ERROR
2615
+ );
2616
+ }
2617
+ return raw.volume;
2618
+ } catch (err) {
2619
+ throw toNhnCloudCliError(err);
2620
+ }
2621
+ }
2622
+ async create(params) {
2623
+ const url = `${this.endpoint}/volumes`;
2624
+ const volumeBody = { size: params.size };
2625
+ if (params.name !== void 0) volumeBody["name"] = params.name;
2626
+ if (params.description !== void 0) volumeBody["description"] = params.description;
2627
+ if (params.volume_type !== void 0) volumeBody["volume_type"] = params.volume_type;
2628
+ if (params.snapshot_id !== void 0) volumeBody["snapshot_id"] = params.snapshot_id;
2629
+ try {
2630
+ const raw = await import_ky7.default.post(url, {
2631
+ headers: this.authHeaders(),
2632
+ json: { volume: volumeBody },
2633
+ retry: 0,
2634
+ timeout: DEFAULT_TIMEOUT_MS3
2635
+ }).json();
2636
+ if (!isVolumeResponse(raw)) {
2637
+ throw new NhnCloudCliError(
2638
+ "volume create \uC751\uB2F5\uC5D0 volume \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
2639
+ EXIT_API_ERROR
2640
+ );
2641
+ }
2642
+ return raw.volume;
2643
+ } catch (err) {
2644
+ throw toNhnCloudCliError(err);
2645
+ }
2646
+ }
2647
+ };
2648
+
2649
+ // src/commands/volume/helpers.ts
2650
+ async function resolveVolumeClient(opts) {
2651
+ const profileName = await resolveProfileName(opts.profile);
2652
+ const iaas = await getIaasCredential(profileName);
2653
+ const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
2654
+ const { tokenId, blockStorageEndpoint } = await getIaasToken(profileName, effectiveIaas);
2655
+ return { client: new BlockStorageClient(tokenId, blockStorageEndpoint), profileName };
2656
+ }
2657
+
2658
+ // src/commands/volume/list.ts
2659
+ function parsePositiveInt4(value, flag) {
2660
+ if (!/^[1-9]\d*$/.test(value)) {
2661
+ throw new NhnCloudCliError(
2662
+ `${flag} \uB294 \uC591\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${JSON.stringify(value)}`,
2663
+ EXIT_PARAM_ERROR
2664
+ );
2665
+ }
2666
+ return Number(value);
2667
+ }
2668
+ 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) => {
2669
+ const opts = cmd.optsWithGlobals();
2670
+ const limitNum = opts.limit !== void 0 ? parsePositiveInt4(opts.limit, "--limit") : void 0;
2671
+ const offsetNum = opts.offset !== void 0 ? parsePositiveInt4(opts.offset, "--offset") : void 0;
2672
+ const { client } = await resolveVolumeClient(opts);
2673
+ startSpinner("\uBCFC\uB968 \uBAA9\uB85D \uC870\uD68C \uC911...");
2674
+ let volumes;
2675
+ try {
2676
+ volumes = await client.list({
2677
+ sort: opts.sort,
2678
+ limit: limitNum,
2679
+ offset: offsetNum,
2680
+ marker: opts.marker
2681
+ });
2682
+ } catch (err) {
2683
+ stopSpinner(false);
2684
+ throw err;
2685
+ }
2686
+ stopSpinner(true);
2687
+ output(opts, {
2688
+ headers: ["id", "name", "size", "status", "type"],
2689
+ rows: volumes.map((v) => [
2690
+ v.id,
2691
+ v.name ?? "",
2692
+ String(v.size),
2693
+ v.status,
2694
+ v.volume_type ?? ""
2695
+ ]),
2696
+ raw: volumes,
2697
+ ids: volumes.map((v) => v.id)
2698
+ });
2699
+ });
2700
+
2701
+ // src/commands/volume/get.ts
2702
+ var import_commander15 = require("commander");
2703
+ 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) => {
2704
+ const opts = cmd.optsWithGlobals();
2705
+ const { client } = await resolveVolumeClient(opts);
2706
+ startSpinner("\uBCFC\uB968 \uC870\uD68C \uC911...");
2707
+ let volume;
2708
+ try {
2709
+ volume = await client.get(id);
2710
+ } catch (err) {
2711
+ stopSpinner(false);
2712
+ throw err;
2713
+ }
2714
+ stopSpinner(true);
2715
+ const attachmentSummary = Array.isArray(volume.attachments) ? volume.attachments.filter((a) => typeof a === "object" && a !== null).map((a) => String(a.server_id)).join(", ") : "";
2716
+ const rows = [
2717
+ ["id", volume.id],
2718
+ ["name", volume.name ?? ""],
2719
+ ["size", String(volume.size)],
2720
+ ["status", volume.status],
2721
+ ["volume_type", volume.volume_type ?? ""],
2722
+ ["created_at", volume.created_at],
2723
+ ["attachments", attachmentSummary]
2724
+ ];
2725
+ output(opts, {
2726
+ headers: ["field", "value"],
2727
+ rows,
2728
+ raw: volume,
2729
+ ids: [volume.id]
2730
+ });
2731
+ });
2732
+
2733
+ // src/commands/volume/create.ts
2734
+ var import_commander16 = require("commander");
2735
+ var import_chalk3 = __toESM(require("chalk"));
2736
+ 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) => {
2737
+ const opts = cmd.optsWithGlobals();
2738
+ if (!/^[1-9]\d*$/.test(opts.size)) {
2739
+ throw new NhnCloudCliError(
2740
+ `--size \uB294 \uC591\uC758 \uC815\uC218(GB)\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${JSON.stringify(opts.size)}`,
2741
+ EXIT_PARAM_ERROR
2742
+ );
2743
+ }
2744
+ const sizeNum = Number(opts.size);
2745
+ const { client } = await resolveVolumeClient(opts);
2746
+ startSpinner("\uBCFC\uB968 \uBC1C\uAE09 \uC911...");
2747
+ let volume;
2748
+ try {
2749
+ volume = await client.create({
2750
+ size: sizeNum,
2751
+ name: opts.name,
2752
+ description: opts.description,
2753
+ volume_type: opts.volumeType,
2754
+ snapshot_id: opts.snapshotId
2755
+ });
2756
+ } catch (err) {
2757
+ stopSpinner(false);
2758
+ throw err;
2759
+ }
2760
+ stopSpinner(true);
2761
+ process.stderr.write(import_chalk3.default.green(`\uBCFC\uB968 \uBC1C\uAE09 \uC694\uCCAD \uC644\uB8CC (id: ${volume.id}, status: ${volume.status})
2762
+ `));
2763
+ const rows = [
2764
+ ["id", volume.id],
2765
+ ["name", volume.name ?? ""],
2766
+ ["size", String(volume.size)],
2767
+ ["status", volume.status],
2768
+ ["volume_type", volume.volume_type ?? ""],
2769
+ ["created_at", volume.created_at]
2770
+ ];
2771
+ output(opts, {
2772
+ headers: ["field", "value"],
2773
+ rows,
2774
+ raw: volume,
2775
+ ids: [volume.id]
2776
+ });
2777
+ });
2778
+
2779
+ // src/commands/network/list.ts
2780
+ var import_commander17 = require("commander");
2781
+
2782
+ // src/services/network/client.ts
2783
+ var import_ky8 = __toESM(require("ky"));
2784
+ var DEFAULT_TIMEOUT_MS4 = 3e4;
2785
+ function isVpc(val) {
2786
+ if (typeof val !== "object" || val === null) return false;
2787
+ const obj = val;
2788
+ return typeof obj["id"] === "string" && typeof obj["name"] === "string" && typeof obj["cidrv4"] === "string" && typeof obj["state"] === "string" && typeof obj["router:external"] === "boolean";
2789
+ }
2790
+ function isVpcsResponse(val) {
2791
+ if (typeof val !== "object" || val === null) return false;
2792
+ const obj = val;
2793
+ return Array.isArray(obj["vpcs"]) && obj["vpcs"].every(isVpc);
2794
+ }
2795
+ function isSubnet(val) {
2796
+ if (typeof val !== "object" || val === null) return false;
2797
+ const obj = val;
2798
+ return typeof obj["id"] === "string" && typeof obj["cidr"] === "string" && typeof obj["vpc_id"] === "string" && typeof obj["gateway"] === "string" && typeof obj["available_ip_count"] === "number";
2799
+ }
2800
+ function isSubnetsResponse(val) {
2801
+ if (typeof val !== "object" || val === null) return false;
2802
+ const obj = val;
2803
+ return Array.isArray(obj["vpcsubnets"]) && obj["vpcsubnets"].every(isSubnet);
2804
+ }
2805
+ function isFloatingIp(val) {
2806
+ if (typeof val !== "object" || val === null) return false;
2807
+ const obj = val;
2808
+ 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");
2809
+ }
2810
+ function isFloatingIpsResponse(val) {
2811
+ if (typeof val !== "object" || val === null) return false;
2812
+ const obj = val;
2813
+ return Array.isArray(obj["floatingips"]) && obj["floatingips"].every(isFloatingIp);
2814
+ }
2815
+ function isFloatingIpResponse(val) {
2816
+ if (typeof val !== "object" || val === null) return false;
2817
+ const obj = val;
2818
+ return isFloatingIp(obj["floatingip"]);
2819
+ }
2820
+ var NetworkClient = class {
2821
+ tokenId;
2822
+ networkEndpoint;
2823
+ constructor(tokenId, networkEndpoint) {
2824
+ this.tokenId = tokenId;
2825
+ this.networkEndpoint = networkEndpoint;
2826
+ }
2827
+ authHeaders() {
2828
+ return { "X-Auth-Token": this.tokenId };
2829
+ }
2830
+ /**
2831
+ * VPC 목록을 조회한다 (GET /v2.0/vpcs, NHN VPC).
2832
+ * instance 와 다른 host(networkEndpoint)지만 같은 Keystone 토큰을 쓴다.
2833
+ */
2834
+ async listVpcs() {
2835
+ const url = `${this.networkEndpoint}/vpcs`;
2836
+ try {
2837
+ const raw = await import_ky8.default.get(url, {
2838
+ headers: this.authHeaders(),
2839
+ retry: 0,
2840
+ timeout: DEFAULT_TIMEOUT_MS4
2841
+ }).json();
2842
+ if (!isVpcsResponse(raw)) {
2843
+ throw new NhnCloudCliError(
2844
+ "network list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 vpcs \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
2845
+ EXIT_API_ERROR
2846
+ );
2847
+ }
2848
+ return raw.vpcs;
2849
+ } catch (err) {
2850
+ throw toNhnCloudCliError(err);
2851
+ }
2852
+ }
2853
+ /**
2854
+ * 서브넷 목록을 조회한다 (GET /v2.0/vpcsubnets, NHN VPC).
2855
+ */
2856
+ async listSubnets() {
2857
+ const url = `${this.networkEndpoint}/vpcsubnets`;
2858
+ try {
2859
+ const raw = await import_ky8.default.get(url, {
2860
+ headers: this.authHeaders(),
2861
+ retry: 0,
2862
+ timeout: DEFAULT_TIMEOUT_MS4
2863
+ }).json();
2864
+ if (!isSubnetsResponse(raw)) {
2865
+ throw new NhnCloudCliError(
2866
+ "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.",
2867
+ EXIT_API_ERROR
2868
+ );
2869
+ }
2870
+ return raw.vpcsubnets;
2871
+ } catch (err) {
2872
+ throw toNhnCloudCliError(err);
2873
+ }
2874
+ }
2875
+ /** Floating IP 목록을 조회한다 (GET /v2.0/floatingips). */
2876
+ async listFloatingIps() {
2877
+ const url = `${this.networkEndpoint}/floatingips`;
2878
+ try {
2879
+ const raw = await import_ky8.default.get(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS4 }).json();
2880
+ if (!isFloatingIpsResponse(raw)) {
2881
+ throw new NhnCloudCliError(
2882
+ "floatingip list \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 floatingips \uBC30\uC5F4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
2883
+ EXIT_API_ERROR
2884
+ );
2885
+ }
2886
+ return raw.floatingips;
2887
+ } catch (err) {
2888
+ throw toNhnCloudCliError(err);
2889
+ }
2890
+ }
2891
+ /** Floating IP 를 발급한다 (POST /v2.0/floatingips). */
2892
+ async createFloatingIp(params) {
2893
+ const url = `${this.networkEndpoint}/floatingips`;
2894
+ try {
2895
+ const raw = await import_ky8.default.post(url, {
2896
+ headers: this.authHeaders(),
2897
+ json: { floatingip: { floating_network_id: params.floating_network_id } },
2898
+ retry: 0,
2899
+ timeout: DEFAULT_TIMEOUT_MS4
2900
+ }).json();
2901
+ if (!isFloatingIpResponse(raw)) {
2902
+ throw new NhnCloudCliError(
2903
+ "floatingip create \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 floatingip \uAC1D\uCCB4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
2904
+ EXIT_API_ERROR
2905
+ );
2906
+ }
2907
+ return raw.floatingip;
2908
+ } catch (err) {
2909
+ throw toNhnCloudCliError(err);
2910
+ }
2911
+ }
2912
+ /** Floating IP 를 삭제한다 (DELETE /v2.0/floatingips/{id}, 무본문). */
2913
+ async deleteFloatingIp(id) {
2914
+ const url = `${this.networkEndpoint}/floatingips/${encodeURIComponent(id)}`;
2915
+ try {
2916
+ await import_ky8.default.delete(url, { headers: this.authHeaders(), retry: 0, timeout: DEFAULT_TIMEOUT_MS4 });
2917
+ } catch (err) {
2918
+ throw toNhnCloudCliError(err);
2919
+ }
2920
+ }
2921
+ /**
2922
+ * 외부(external) VPC id 를 찾는다 — create 의 floating_network_id 기본 소스.
2923
+ * `router:external` 은 콜론 포함 리터럴 키 — bracket 접근 필수.
2924
+ * external VPC 가 둘 이상이면 첫 매칭을 반환한다.
2925
+ * 사용자는 `--network <id>` 로 명시 지정 가능하므로 create 의 stderr 에 그 사실을 안내한다.
2926
+ */
2927
+ async findExternalNetworkId() {
2928
+ const url = `${this.networkEndpoint}/vpcs`;
2929
+ try {
2930
+ const raw = await import_ky8.default.get(url, {
2931
+ headers: this.authHeaders(),
2932
+ searchParams: { "router:external": "true" },
2933
+ retry: 0,
2934
+ timeout: DEFAULT_TIMEOUT_MS4
2935
+ }).json();
2936
+ if (typeof raw !== "object" || raw === null) return null;
2937
+ const vpcs = raw["vpcs"];
2938
+ if (!Array.isArray(vpcs)) return null;
2939
+ for (const v of vpcs) {
2940
+ if (typeof v !== "object" || v === null) continue;
2941
+ const obj = v;
2942
+ if (obj["router:external"] === true && typeof obj["id"] === "string") {
2943
+ return obj["id"];
2944
+ }
2945
+ }
2946
+ return null;
2947
+ } catch (err) {
2948
+ throw toNhnCloudCliError(err);
2949
+ }
2950
+ }
2951
+ };
2952
+
2953
+ // src/commands/network/helpers.ts
2954
+ async function resolveNetworkClient(opts) {
2955
+ const profileName = await resolveProfileName(opts.profile);
2956
+ const iaas = await getIaasCredential(profileName);
2957
+ const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
2958
+ const { tokenId, networkEndpoint } = await getIaasToken(profileName, effectiveIaas);
2959
+ return { client: new NetworkClient(tokenId, networkEndpoint), profileName };
2960
+ }
2961
+
2962
+ // src/commands/network/list.ts
2963
+ 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) => {
2964
+ const opts = cmd.optsWithGlobals();
2965
+ const { client } = await resolveNetworkClient(opts);
2966
+ startSpinner("VPC \uBAA9\uB85D \uC870\uD68C \uC911...");
2967
+ let vpcs;
2968
+ try {
2969
+ vpcs = await client.listVpcs();
2970
+ } catch (err) {
2971
+ stopSpinner(false);
2972
+ throw err;
2973
+ }
2974
+ stopSpinner(true);
2975
+ output(opts, {
2976
+ headers: ["id", "name", "cidrv4", "state", "external"],
2977
+ rows: vpcs.map((v) => [v.id, v.name, v.cidrv4, v.state, String(v["router:external"])]),
2978
+ raw: vpcs,
2979
+ ids: vpcs.map((v) => v.id)
2980
+ });
2981
+ });
2982
+
2983
+ // src/commands/network/subnet.ts
2984
+ var import_commander18 = require("commander");
2985
+ 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) => {
2986
+ const opts = cmd.optsWithGlobals();
2987
+ const { client } = await resolveNetworkClient(opts);
2988
+ startSpinner("\uC11C\uBE0C\uB137 \uBAA9\uB85D \uC870\uD68C \uC911...");
2989
+ let subnets;
2990
+ try {
2991
+ subnets = await client.listSubnets();
2992
+ } catch (err) {
2993
+ stopSpinner(false);
2994
+ throw err;
2995
+ }
2996
+ stopSpinner(true);
2997
+ output(opts, {
2998
+ headers: ["id", "cidr", "vpc_id", "gateway", "available_ip"],
2999
+ rows: subnets.map((s) => [
3000
+ s.id,
3001
+ s.cidr,
3002
+ s.vpc_id,
3003
+ s.gateway,
3004
+ String(s.available_ip_count)
3005
+ ]),
3006
+ raw: subnets,
3007
+ ids: subnets.map((s) => s.id)
3008
+ });
3009
+ });
3010
+ var subnetCommand = new import_commander18.Command("subnet").description("\uC11C\uBE0C\uB137 \uAD00\uB828 \uBA85\uB839").addCommand(subnetListCommand);
3011
+
3012
+ // src/commands/floatingip/list.ts
3013
+ var import_commander19 = require("commander");
3014
+ 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) => {
3015
+ const opts = cmd.optsWithGlobals();
3016
+ const { client } = await resolveNetworkClient(opts);
3017
+ startSpinner("Floating IP \uBAA9\uB85D \uC870\uD68C \uC911...");
3018
+ let fips;
3019
+ try {
3020
+ fips = await client.listFloatingIps();
3021
+ } catch (err) {
3022
+ stopSpinner(false);
3023
+ throw err;
3024
+ }
3025
+ stopSpinner(true);
3026
+ output(opts, {
3027
+ headers: ["id", "floating_ip_address", "status", "port_id", "fixed_ip_address"],
3028
+ rows: fips.map((f) => [
3029
+ f.id,
3030
+ f.floating_ip_address,
3031
+ f.status,
3032
+ f.port_id ?? "-",
3033
+ f.fixed_ip_address ?? "-"
3034
+ ]),
3035
+ raw: fips,
3036
+ ids: fips.map((f) => f.id)
3037
+ });
3038
+ });
3039
+
3040
+ // src/commands/floatingip/create.ts
3041
+ var import_commander20 = require("commander");
3042
+ var import_chalk4 = __toESM(require("chalk"));
3043
+ 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) => {
3044
+ const opts = cmd.optsWithGlobals();
3045
+ const { client } = await resolveNetworkClient(opts);
3046
+ let networkId = opts.network;
3047
+ if (networkId === void 0) {
3048
+ startSpinner("\uC678\uBD80 \uB124\uD2B8\uC6CC\uD06C \uC870\uD68C \uC911...");
3049
+ try {
3050
+ const found = await client.findExternalNetworkId();
3051
+ if (found === null) {
3052
+ throw new NhnCloudCliError(
3053
+ "\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.",
3054
+ EXIT_PARAM_ERROR
3055
+ );
3056
+ }
3057
+ networkId = found;
3058
+ } catch (err) {
3059
+ stopSpinner(false);
3060
+ throw err;
3061
+ }
3062
+ stopSpinner(true);
3063
+ }
3064
+ startSpinner(`Floating IP \uBC1C\uAE09 \uC911... (network: ${networkId})`);
3065
+ let fip;
3066
+ try {
3067
+ fip = await client.createFloatingIp({ floating_network_id: networkId });
3068
+ } catch (err) {
3069
+ stopSpinner(false);
3070
+ throw err;
3071
+ }
3072
+ stopSpinner(true);
3073
+ process.stderr.write(
3074
+ import_chalk4.default.green(`\u2713 Floating IP "${fip.floating_ip_address}" \uB97C \uBC1C\uAE09\uD588\uC2B5\uB2C8\uB2E4 (id: ${fip.id}).
3075
+ `)
3076
+ );
3077
+ output(opts, {
3078
+ headers: ["id", "floating_ip_address", "status", "floating_network_id"],
3079
+ rows: [[fip.id, fip.floating_ip_address, fip.status, fip.floating_network_id]],
3080
+ raw: fip,
3081
+ ids: [fip.id]
3082
+ });
3083
+ });
3084
+
3085
+ // src/commands/floatingip/delete.ts
3086
+ var import_commander21 = require("commander");
3087
+ var import_chalk5 = __toESM(require("chalk"));
3088
+ 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) => {
3089
+ const opts = cmd.optsWithGlobals();
3090
+ const isTTY = process.stdin.isTTY;
3091
+ if (!isTTY && !opts.yes) {
3092
+ throw new NhnCloudCliError(
3093
+ "\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C Floating IP \uC0AD\uC81C\uB294 --yes \uD50C\uB798\uADF8\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
3094
+ EXIT_PARAM_ERROR
3095
+ );
3096
+ }
3097
+ if (isTTY && !opts.yes) {
3098
+ const { confirm } = await import("@inquirer/prompts");
3099
+ const ok = await confirm({
3100
+ message: `Floating IP "${id}" \uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`,
3101
+ default: false
3102
+ });
3103
+ if (!ok) {
3104
+ process.stderr.write(import_chalk5.default.yellow("\uC0AD\uC81C\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
3105
+ return;
3106
+ }
3107
+ }
3108
+ const { client } = await resolveNetworkClient(opts);
3109
+ startSpinner(`Floating IP \uC0AD\uC81C \uC911... (id: ${id})`);
3110
+ try {
3111
+ await client.deleteFloatingIp(id);
3112
+ } catch (err) {
3113
+ stopSpinner(false);
3114
+ throw err;
3115
+ }
3116
+ stopSpinner(true);
3117
+ process.stderr.write(import_chalk5.default.green(`\u2713 Floating IP "${id}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
3118
+ `));
3119
+ });
1408
3120
 
1409
- // src/commands/instance/helpers.ts
1410
- async function resolveInstanceClient(opts) {
1411
- const profileName = await resolveProfileName(opts.profile);
1412
- const iaas = await getIaasCredential(profileName);
1413
- const effectiveIaas = opts.region ? { ...iaas, region: opts.region } : iaas;
1414
- const { tokenId, computeEndpoint } = await getIaasToken(profileName, effectiveIaas);
1415
- return { client: new InstanceClient(tokenId, computeEndpoint), profileName };
3121
+ // src/commands/instance/flavors.ts
3122
+ var import_commander22 = require("commander");
3123
+ function parseNonNegInt(value, flag) {
3124
+ if (value === void 0) return void 0;
3125
+ const n = Number(value);
3126
+ if (!Number.isInteger(n) || n < 0) {
3127
+ throw new NhnCloudCliError(`${flag} \uB294 0 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${value}).`, EXIT_PARAM_ERROR);
3128
+ }
3129
+ return n;
1416
3130
  }
1417
-
1418
- // src/commands/instance/list.ts
1419
- function getIps(server) {
1420
- return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
3131
+ 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) => {
3132
+ const opts = cmd.optsWithGlobals();
3133
+ const minDisk = parseNonNegInt(opts.minDisk, "--min-disk");
3134
+ const minRam = parseNonNegInt(opts.minRam, "--min-ram");
3135
+ const { client } = await resolveInstanceClient(opts);
3136
+ startSpinner("\uC778\uC2A4\uD134\uC2A4 \uD0C0\uC785 \uC870\uD68C \uC911...");
3137
+ try {
3138
+ if (opts.detail) {
3139
+ const flavors = await client.listFlavors({ detail: true, minDisk, minRam });
3140
+ stopSpinner(true);
3141
+ printFlavors(opts, flavors);
3142
+ } else {
3143
+ const flavors = await client.listFlavors({ minDisk, minRam });
3144
+ stopSpinner(true);
3145
+ printFlavors(opts, flavors);
3146
+ }
3147
+ } catch (err) {
3148
+ stopSpinner(false);
3149
+ throw err;
3150
+ }
3151
+ });
3152
+ function printFlavors(opts, flavors) {
3153
+ if (isDetailList(flavors)) {
3154
+ output(opts, {
3155
+ headers: ["id", "name", "vcpus", "ram(MB)", "disk(GB)"],
3156
+ rows: flavors.map((f) => [f.id, f.name, String(f.vcpus), String(f.ram), String(f.disk)]),
3157
+ raw: flavors,
3158
+ ids: flavors.map((f) => f.id)
3159
+ });
3160
+ } else {
3161
+ output(opts, {
3162
+ headers: ["id", "name"],
3163
+ rows: flavors.map((f) => [f.id, f.name]),
3164
+ raw: flavors,
3165
+ ids: flavors.map((f) => f.id)
3166
+ });
3167
+ }
1421
3168
  }
1422
- 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) => {
3169
+ function isDetailList(flavors) {
3170
+ return flavors.length > 0 && "vcpus" in flavors[0];
3171
+ }
3172
+
3173
+ // src/commands/instance/availability-zones.ts
3174
+ var import_commander23 = require("commander");
3175
+ 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) => {
1423
3176
  const opts = cmd.optsWithGlobals();
1424
3177
  const { client } = await resolveInstanceClient(opts);
1425
- startSpinner("\uC778\uC2A4\uD134\uC2A4 \uBAA9\uB85D \uC870\uD68C \uC911...");
1426
- let servers;
3178
+ startSpinner("\uAC00\uC6A9\uC131 \uC601\uC5ED \uC870\uD68C \uC911...");
3179
+ let zones;
1427
3180
  try {
1428
- servers = await client.list();
3181
+ zones = await client.listAvailabilityZones();
1429
3182
  } catch (err) {
1430
3183
  stopSpinner(false);
1431
3184
  throw err;
1432
3185
  }
1433
3186
  stopSpinner(true);
1434
3187
  output(opts, {
1435
- headers: ["id", "name", "status", "IPs", "flavor"],
1436
- rows: servers.map((s) => [s.id, s.name, s.status, getIps(s), s.flavor.id]),
1437
- raw: servers,
1438
- ids: servers.map((s) => s.id)
3188
+ headers: ["zoneName", "available"],
3189
+ rows: zones.map((z) => [z.zoneName, String(z.zoneState.available)]),
3190
+ raw: zones,
3191
+ ids: zones.map((z) => z.zoneName)
1439
3192
  });
1440
3193
  });
1441
3194
 
1442
3195
  // src/commands/instance/get.ts
1443
- var import_commander8 = require("commander");
3196
+ var import_commander24 = require("commander");
1444
3197
  function getIps2(server) {
1445
3198
  return Object.values(server.addresses).flat().map((a) => a.addr).join(", ");
1446
3199
  }
1447
3200
  function getImageId(server) {
1448
3201
  return typeof server.image === "object" ? server.image.id : "";
1449
3202
  }
1450
- 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) => {
3203
+ 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) => {
1451
3204
  const opts = cmd.optsWithGlobals();
1452
3205
  const { client } = await resolveInstanceClient(opts);
1453
3206
  startSpinner("\uC778\uC2A4\uD134\uC2A4 \uC870\uD68C \uC911...");
@@ -1479,8 +3232,9 @@ var getCommand = new import_commander8.Command("get").description("\uB2E8\uC77C
1479
3232
  });
1480
3233
 
1481
3234
  // src/commands/instance/create.ts
1482
- var import_commander9 = require("commander");
1483
- var import_chalk2 = __toESM(require("chalk"));
3235
+ var import_commander25 = require("commander");
3236
+ var import_node_fs5 = require("fs");
3237
+ var import_chalk6 = __toESM(require("chalk"));
1484
3238
  function getFirstIp(server) {
1485
3239
  for (const list of Object.values(server.addresses)) {
1486
3240
  for (const addr of list) {
@@ -1495,12 +3249,45 @@ function getIps3(server) {
1495
3249
  function getImageId2(server) {
1496
3250
  return typeof server.image === "object" ? server.image.id : "";
1497
3251
  }
1498
- 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("--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) => {
3252
+ 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) => {
1499
3253
  const opts = cmd.optsWithGlobals();
1500
3254
  const networks = opts.network ?? [];
1501
3255
  if (networks.length === 0) {
1502
3256
  throw new NhnCloudCliError("--network \uB294 \uCD5C\uC18C 1\uAC1C \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
1503
3257
  }
3258
+ let userDataBase64;
3259
+ if (opts.userData !== void 0) {
3260
+ let stat;
3261
+ try {
3262
+ stat = (0, import_node_fs5.statSync)(opts.userData);
3263
+ } catch (e) {
3264
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
3265
+ throw new NhnCloudCliError(
3266
+ `--user-data \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.userData} (${reason})`,
3267
+ EXIT_PARAM_ERROR
3268
+ );
3269
+ }
3270
+ if (!stat.isFile()) {
3271
+ throw new NhnCloudCliError(
3272
+ `--user-data \uAC00 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${opts.userData}`,
3273
+ EXIT_PARAM_ERROR
3274
+ );
3275
+ }
3276
+ if (stat.size > 49149) {
3277
+ throw new NhnCloudCliError(
3278
+ `--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.`,
3279
+ EXIT_PARAM_ERROR
3280
+ );
3281
+ }
3282
+ const raw = (0, import_node_fs5.readFileSync)(opts.userData);
3283
+ userDataBase64 = raw.toString("base64");
3284
+ if (userDataBase64.length > 65535) {
3285
+ throw new NhnCloudCliError(
3286
+ `--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.`,
3287
+ EXIT_PARAM_ERROR
3288
+ );
3289
+ }
3290
+ }
1504
3291
  const timeoutMs = parseInt(opts.timeout ?? "300", 10) * 1e3;
1505
3292
  const { client } = await resolveInstanceClient(opts);
1506
3293
  startSpinner("\uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131 \uC911...");
@@ -1515,7 +3302,8 @@ var createCommand = new import_commander9.Command("create").description("\uC778\
1515
3302
  keyName: opts.keyName,
1516
3303
  securityGroups: opts.securityGroup && opts.securityGroup.length > 0 ? opts.securityGroup : void 0,
1517
3304
  ephemeralDiskSize: opts.ephemeralDiskSize !== void 0 ? parseInt(opts.ephemeralDiskSize, 10) : void 0,
1518
- protect: opts.protect
3305
+ protect: opts.protect,
3306
+ userDataBase64
1519
3307
  });
1520
3308
  } catch (err) {
1521
3309
  stopSpinner(false);
@@ -1538,7 +3326,7 @@ var createCommand = new import_commander9.Command("create").description("\uC778\
1538
3326
  return;
1539
3327
  }
1540
3328
  if (opts.wait) {
1541
- process.stderr.write(import_chalk2.default.green(` IP: ${getIps3(server)}
3329
+ process.stderr.write(import_chalk6.default.green(` IP: ${getIps3(server)}
1542
3330
  `));
1543
3331
  }
1544
3332
  const rows = [
@@ -1558,9 +3346,9 @@ var createCommand = new import_commander9.Command("create").description("\uC778\
1558
3346
  });
1559
3347
 
1560
3348
  // src/commands/instance/delete.ts
1561
- var import_commander10 = require("commander");
1562
- var import_chalk3 = __toESM(require("chalk"));
1563
- 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) => {
3349
+ var import_commander26 = require("commander");
3350
+ var import_chalk7 = __toESM(require("chalk"));
3351
+ 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) => {
1564
3352
  const opts = cmd.optsWithGlobals();
1565
3353
  const isTTY = process.stdin.isTTY;
1566
3354
  if (!isTTY && !opts.yes) {
@@ -1576,7 +3364,7 @@ var deleteCommand = new import_commander10.Command("delete").description("\uC778
1576
3364
  default: false
1577
3365
  });
1578
3366
  if (!ok) {
1579
- process.stderr.write(import_chalk3.default.yellow("\uC0AD\uC81C\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
3367
+ process.stderr.write(import_chalk7.default.yellow("\uC0AD\uC81C\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n"));
1580
3368
  return;
1581
3369
  }
1582
3370
  }
@@ -1589,41 +3377,474 @@ var deleteCommand = new import_commander10.Command("delete").description("\uC778
1589
3377
  throw err;
1590
3378
  }
1591
3379
  stopSpinner(true);
1592
- process.stderr.write(import_chalk3.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
3380
+ process.stderr.write(import_chalk7.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
3381
+ `));
3382
+ });
3383
+
3384
+ // src/commands/instance/power.ts
3385
+ var import_commander27 = require("commander");
3386
+ var import_chalk8 = __toESM(require("chalk"));
3387
+ 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) => {
3388
+ const opts = cmd.optsWithGlobals();
3389
+ const { client } = await resolveInstanceClient(opts);
3390
+ startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC2DC\uC791 \uC911... (id: ${id})`);
3391
+ try {
3392
+ await client.start(id);
3393
+ } catch (err) {
3394
+ stopSpinner(false);
3395
+ throw err;
3396
+ }
3397
+ stopSpinner(true);
3398
+ process.stderr.write(import_chalk8.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uC2DC\uC791\uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\u2192 ACTIVE).
3399
+ `));
3400
+ });
3401
+ 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) => {
3402
+ const opts = cmd.optsWithGlobals();
3403
+ const { client } = await resolveInstanceClient(opts);
3404
+ startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC815\uC9C0 \uC911... (id: ${id})`);
3405
+ try {
3406
+ await client.stop(id);
3407
+ } catch (err) {
3408
+ stopSpinner(false);
3409
+ throw err;
3410
+ }
3411
+ stopSpinner(true);
3412
+ process.stderr.write(import_chalk8.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" \uC815\uC9C0\uB97C \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4 (\u2192 SHUTOFF).
3413
+ `));
3414
+ });
3415
+ 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) => {
3416
+ const opts = cmd.optsWithGlobals();
3417
+ const { client } = await resolveInstanceClient(opts);
3418
+ const type = opts.hard ? "HARD" : "SOFT";
3419
+ startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uC7AC\uBD80\uD305 \uC911... (id: ${id}, ${type})`);
3420
+ try {
3421
+ await client.reboot(id, type);
3422
+ } catch (err) {
3423
+ stopSpinner(false);
3424
+ throw err;
3425
+ }
3426
+ stopSpinner(true);
3427
+ process.stderr.write(import_chalk8.default.green(`\u2713 \uC778\uC2A4\uD134\uC2A4 "${id}" ${type} \uC7AC\uBD80\uD305\uC744 \uC694\uCCAD\uD588\uC2B5\uB2C8\uB2E4.
3428
+ `));
3429
+ });
3430
+
3431
+ // src/commands/instance/resize.ts
3432
+ var import_commander28 = require("commander");
3433
+ var import_chalk9 = __toESM(require("chalk"));
3434
+ 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) => {
3435
+ const opts = cmd.optsWithGlobals();
3436
+ const { client } = await resolveInstanceClient(opts);
3437
+ startSpinner(`\uC778\uC2A4\uD134\uC2A4 \uD0C0\uC785 \uBCC0\uACBD \uC911... (id: ${id})`);
3438
+ try {
3439
+ await client.resize(id, opts.flavor);
3440
+ } catch (err) {
3441
+ stopSpinner(false);
3442
+ throw err;
3443
+ }
3444
+ stopSpinner(true);
3445
+ process.stderr.write(
3446
+ import_chalk9.default.green(
3447
+ `\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).
3448
+ `
3449
+ )
3450
+ );
3451
+ });
3452
+ 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) => {
3453
+ const opts = cmd.optsWithGlobals();
3454
+ const { client } = await resolveInstanceClient(opts);
3455
+ startSpinner(`resize \uD655\uC815 \uC911... (id: ${id})`);
3456
+ try {
3457
+ await client.confirmResize(id);
3458
+ } catch (err) {
3459
+ stopSpinner(false);
3460
+ throw err;
3461
+ }
3462
+ stopSpinner(true);
3463
+ 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).
3464
+ `));
3465
+ });
3466
+ 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) => {
3467
+ const opts = cmd.optsWithGlobals();
3468
+ const { client } = await resolveInstanceClient(opts);
3469
+ startSpinner(`resize \uB864\uBC31 \uC911... (id: ${id})`);
3470
+ try {
3471
+ await client.revertResize(id);
3472
+ } catch (err) {
3473
+ stopSpinner(false);
3474
+ throw err;
3475
+ }
3476
+ stopSpinner(true);
3477
+ process.stderr.write(
3478
+ 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).
3479
+ `)
3480
+ );
3481
+ });
3482
+
3483
+ // src/commands/instance/images.ts
3484
+ var import_commander29 = require("commander");
3485
+ var VISIBILITY_VALUES = ["public", "private", "shared"];
3486
+ function parsePositiveInt5(value, flag) {
3487
+ if (value === void 0) return void 0;
3488
+ const n = Number(value);
3489
+ if (!Number.isInteger(n) || n < 1) {
3490
+ throw new NhnCloudCliError(`${flag} \uB294 1 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${value}).`, EXIT_PARAM_ERROR);
3491
+ }
3492
+ return n;
3493
+ }
3494
+ 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) => {
3495
+ const opts = cmd.optsWithGlobals();
3496
+ const limit = parsePositiveInt5(opts.limit, "--limit");
3497
+ if (opts.visibility !== void 0 && !VISIBILITY_VALUES.includes(opts.visibility)) {
3498
+ throw new NhnCloudCliError(
3499
+ `--visibility \uB294 ${VISIBILITY_VALUES.join(" | ")} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uC785\uB825: ${opts.visibility}).`,
3500
+ EXIT_PARAM_ERROR
3501
+ );
3502
+ }
3503
+ const { client } = await resolveInstanceClient(opts);
3504
+ startSpinner("\uC774\uBBF8\uC9C0 \uBAA9\uB85D \uC870\uD68C \uC911...");
3505
+ let result;
3506
+ try {
3507
+ result = await client.listImages({
3508
+ limit,
3509
+ marker: opts.marker,
3510
+ name: opts.name,
3511
+ visibility: opts.visibility,
3512
+ owner: opts.owner,
3513
+ status: opts.status
3514
+ });
3515
+ } catch (err) {
3516
+ stopSpinner(false);
3517
+ throw err;
3518
+ }
3519
+ stopSpinner(true);
3520
+ output(opts, {
3521
+ headers: ["id", "name", "status", "visibility", "size"],
3522
+ rows: result.images.map((img) => [
3523
+ img.id,
3524
+ img.name ?? "-",
3525
+ img.status,
3526
+ img.visibility,
3527
+ img.size === void 0 ? "-" : String(img.size)
3528
+ ]),
3529
+ raw: result.images,
3530
+ ids: result.images.map((img) => img.id)
3531
+ });
3532
+ if (result.next && !opts.json && !opts.quiet) {
3533
+ const lastId = result.images.at(-1)?.id;
3534
+ if (lastId) {
3535
+ process.stderr.write(`\uB2E4\uC74C \uD398\uC774\uC9C0: --marker ${lastId}
3536
+ `);
3537
+ }
3538
+ }
3539
+ });
3540
+
3541
+ // src/commands/instance/keypairs.ts
3542
+ var import_commander30 = require("commander");
3543
+ 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) => {
3544
+ const opts = cmd.optsWithGlobals();
3545
+ const { client } = await resolveInstanceClient(opts);
3546
+ startSpinner("\uD0A4\uD398\uC5B4 \uBAA9\uB85D \uC870\uD68C \uC911...");
3547
+ let keypairs;
3548
+ try {
3549
+ keypairs = await client.listKeypairs();
3550
+ } catch (err) {
3551
+ stopSpinner(false);
3552
+ throw err;
3553
+ }
3554
+ stopSpinner(true);
3555
+ output(opts, {
3556
+ headers: ["name", "fingerprint"],
3557
+ rows: keypairs.map((k) => [k.name, k.fingerprint]),
3558
+ raw: keypairs,
3559
+ ids: keypairs.map((k) => k.name)
3560
+ });
3561
+ });
3562
+
3563
+ // src/commands/instance/keypair.ts
3564
+ var import_commander31 = require("commander");
3565
+ var import_node_fs6 = require("fs");
3566
+ var import_node_crypto3 = require("crypto");
3567
+ var import_chalk10 = __toESM(require("chalk"));
3568
+ function resolvePublicKey(value) {
3569
+ let stat;
3570
+ try {
3571
+ stat = (0, import_node_fs6.statSync)(value);
3572
+ } catch (e) {
3573
+ const err = e;
3574
+ if (err.code && err.code !== "ENOENT") {
3575
+ throw new NhnCloudCliError(
3576
+ `--public-key \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${value} (${err.code})`,
3577
+ EXIT_PARAM_ERROR
3578
+ );
3579
+ }
3580
+ return value;
3581
+ }
3582
+ if (!stat.isFile()) {
3583
+ throw new NhnCloudCliError(`--public-key \uAC00 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${value}`, EXIT_PARAM_ERROR);
3584
+ }
3585
+ return (0, import_node_fs6.readFileSync)(value, "utf-8").trim();
3586
+ }
3587
+ function savePrivateKey(filePath, privateKey) {
3588
+ const tmp = `${filePath}.${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.tmp`;
3589
+ const content = privateKey.endsWith("\n") ? privateKey : privateKey + "\n";
3590
+ try {
3591
+ (0, import_node_fs6.writeFileSync)(tmp, content, { encoding: "utf-8", mode: 384 });
3592
+ (0, import_node_fs6.renameSync)(tmp, filePath);
3593
+ } catch (e) {
3594
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
3595
+ let tmpNote = "";
3596
+ try {
3597
+ (0, import_node_fs6.unlinkSync)(tmp);
3598
+ } catch {
3599
+ tmpNote = ` \u2014 0600 \uC784\uC2DC \uD30C\uC77C\uC774 \uB0A8\uC558\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4: ${tmp}`;
3600
+ }
3601
+ throw new NhnCloudCliError(
3602
+ `private_key \uD30C\uC77C\uC744 \uC800\uC7A5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath} (${reason})${tmpNote}`,
3603
+ EXIT_PARAM_ERROR
3604
+ );
3605
+ }
3606
+ }
3607
+ 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) => {
3608
+ const opts = cmd.optsWithGlobals();
3609
+ const { client } = await resolveInstanceClient(opts);
3610
+ startSpinner("\uD0A4\uD398\uC5B4 \uC870\uD68C \uC911...");
3611
+ let kp;
3612
+ try {
3613
+ kp = await client.getKeypair(name);
3614
+ } catch (err) {
3615
+ stopSpinner(false);
3616
+ throw err;
3617
+ }
3618
+ stopSpinner(true);
3619
+ output(opts, {
3620
+ headers: ["field", "value"],
3621
+ rows: [
3622
+ ["name", kp.name],
3623
+ ["fingerprint", kp.fingerprint],
3624
+ ["user_id", kp.user_id],
3625
+ ["created_at", kp.created_at],
3626
+ ["public_key", kp.public_key]
3627
+ ],
3628
+ raw: kp,
3629
+ ids: [kp.name]
3630
+ });
3631
+ });
3632
+ 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) => {
3633
+ const opts = cmd.optsWithGlobals();
3634
+ const publicKey = opts.publicKey !== void 0 ? resolvePublicKey(opts.publicKey) : void 0;
3635
+ if (opts.output !== void 0 && publicKey !== void 0) {
3636
+ throw new NhnCloudCliError(
3637
+ "--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.",
3638
+ EXIT_PARAM_ERROR
3639
+ );
3640
+ }
3641
+ if (publicKey === void 0 && opts.quiet && opts.output === void 0) {
3642
+ throw new NhnCloudCliError(
3643
+ "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).",
3644
+ EXIT_PARAM_ERROR
3645
+ );
3646
+ }
3647
+ const { client } = await resolveInstanceClient(opts);
3648
+ startSpinner("\uD0A4\uD398\uC5B4 \uC0DD\uC131 \uC911...");
3649
+ let result;
3650
+ try {
3651
+ result = await client.createKeypair({ name, publicKey });
3652
+ } catch (err) {
3653
+ stopSpinner(false);
3654
+ throw err;
3655
+ }
3656
+ stopSpinner(true);
3657
+ if (result.private_key !== void 0) {
3658
+ if (opts.output !== void 0) {
3659
+ try {
3660
+ savePrivateKey(opts.output, result.private_key);
3661
+ process.stderr.write(import_chalk10.default.green(` private_key \uB97C ${opts.output} \uC5D0 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4 (mode 0600).
3662
+ `));
3663
+ } catch (saveErr) {
3664
+ const reason = saveErr instanceof Error ? saveErr.message : String(saveErr);
3665
+ process.stderr.write(
3666
+ 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.
3667
+ `)
3668
+ );
3669
+ process.stdout.write(result.private_key + "\n");
3670
+ }
3671
+ } else {
3672
+ process.stderr.write(
3673
+ 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")
3674
+ );
3675
+ process.stdout.write(result.private_key + "\n");
3676
+ }
3677
+ }
3678
+ const { private_key, ...meta } = result;
3679
+ void private_key;
3680
+ output(opts, {
3681
+ headers: ["field", "value"],
3682
+ rows: [
3683
+ ["name", meta.name],
3684
+ ["fingerprint", meta.fingerprint],
3685
+ ["user_id", meta.user_id]
3686
+ ],
3687
+ raw: meta,
3688
+ ids: [meta.name]
3689
+ });
3690
+ });
3691
+ 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) => {
3692
+ const opts = cmd.optsWithGlobals();
3693
+ const { client } = await resolveInstanceClient(opts);
3694
+ startSpinner("\uD0A4\uD398\uC5B4 \uC0AD\uC81C \uC911...");
3695
+ try {
3696
+ await client.deleteKeypair(name);
3697
+ } catch (err) {
3698
+ stopSpinner(false);
3699
+ throw err;
3700
+ }
3701
+ stopSpinner(true);
3702
+ process.stderr.write(import_chalk10.default.green(`\u2713 \uD0A4\uD398\uC5B4 "${name}" \uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
1593
3703
  `));
1594
3704
  });
3705
+ var keypairCommand = new import_commander31.Command("keypair").description("\uD0A4\uD398\uC5B4 \uB2E8\uAC74 \uAD00\uB9AC (get / create / delete)").addCommand(getKeypairCmd).addCommand(createKeypairCmd).addCommand(deleteKeypairCmd);
3706
+
3707
+ // src/commands/instance/volume.ts
3708
+ var import_commander32 = require("commander");
3709
+ var import_chalk11 = __toESM(require("chalk"));
3710
+ 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) => {
3711
+ const opts = cmd.optsWithGlobals();
3712
+ const { client } = await resolveInstanceClient(opts);
3713
+ startSpinner("\uBCFC\uB968 \uC5F0\uACB0 \uC911...");
3714
+ let att;
3715
+ try {
3716
+ att = await client.attachVolume(id, opts.volume);
3717
+ } catch (err) {
3718
+ stopSpinner(false);
3719
+ throw err;
3720
+ }
3721
+ stopSpinner(true);
3722
+ process.stderr.write(
3723
+ import_chalk11.default.green(`\uBCFC\uB968 \uC5F0\uACB0 \uC644\uB8CC (volumeId: ${att.volumeId}, device: ${att.device})
3724
+ `)
3725
+ );
3726
+ output(opts, {
3727
+ headers: ["field", "value"],
3728
+ rows: [
3729
+ ["id", att.id],
3730
+ ["volumeId", att.volumeId],
3731
+ ["serverId", att.serverId],
3732
+ ["device", att.device]
3733
+ ],
3734
+ raw: att,
3735
+ ids: [att.id]
3736
+ });
3737
+ });
3738
+ 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) => {
3739
+ const opts = cmd.optsWithGlobals();
3740
+ const { client } = await resolveInstanceClient(opts);
3741
+ startSpinner("\uBCFC\uB968 \uC5F0\uACB0 \uD574\uC81C \uC911...");
3742
+ try {
3743
+ await client.detachVolume(id, volumeId);
3744
+ } catch (err) {
3745
+ stopSpinner(false);
3746
+ throw err;
3747
+ }
3748
+ stopSpinner(true);
3749
+ process.stderr.write(
3750
+ import_chalk11.default.green(`\uBCFC\uB968 \uC5F0\uACB0 \uD574\uC81C \uC694\uCCAD \uC644\uB8CC (volumeId: ${volumeId})
3751
+ `)
3752
+ );
3753
+ });
3754
+ var volumeCommand = new import_commander32.Command("volume").description(
3755
+ "\uC778\uC2A4\uD134\uC2A4 \uBCFC\uB968 \uC5F0\uACB0/\uD574\uC81C"
3756
+ );
3757
+ volumeCommand.addCommand(attachCommand);
3758
+ volumeCommand.addCommand(detachCommand);
3759
+
3760
+ // src/commands/instance/volumes.ts
3761
+ var import_commander33 = require("commander");
3762
+ 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) => {
3763
+ const opts = cmd.optsWithGlobals();
3764
+ const { client } = await resolveInstanceClient(opts);
3765
+ startSpinner("\uBCFC\uB968 \uBAA9\uB85D \uC870\uD68C \uC911...");
3766
+ let attachments;
3767
+ try {
3768
+ attachments = await client.listVolumeAttachments(id);
3769
+ } catch (err) {
3770
+ stopSpinner(false);
3771
+ throw err;
3772
+ }
3773
+ stopSpinner(true);
3774
+ output(opts, {
3775
+ headers: ["id", "volumeId", "device"],
3776
+ rows: attachments.map((a) => [a.id, a.volumeId, a.device]),
3777
+ raw: attachments,
3778
+ ids: attachments.map((a) => a.id)
3779
+ });
3780
+ });
1595
3781
 
1596
3782
  // src/index.ts
1597
- var program = new import_commander11.Command();
1598
- program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.2.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");
3783
+ var program = new import_commander34.Command();
3784
+ program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.4.0").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "\uCD5C\uC18C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9)").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654");
1599
3785
  program.hook("preAction", () => {
1600
3786
  const opts = program.opts();
1601
3787
  if (!opts.color || process.env["NO_COLOR"]) {
1602
- import_chalk4.default.level = 0;
3788
+ import_chalk12.default.level = 0;
1603
3789
  }
1604
3790
  if (opts.json || opts.quiet) {
1605
3791
  setQuiet(true);
1606
3792
  }
1607
3793
  });
1608
3794
  program.addCommand(configureCommand);
1609
- var logncrashCommand = new import_commander11.Command("logncrash").description("Log & Crash \uAD00\uB828 \uBA85\uB839");
3795
+ var logncrashCommand = new import_commander34.Command("logncrash").description("Log & Crash \uAD00\uB828 \uBA85\uB839");
1610
3796
  logncrashCommand.addCommand(searchCommand);
3797
+ logncrashCommand.addCommand(sendCommand);
3798
+ logncrashCommand.addCommand(exportCommand);
1611
3799
  program.addCommand(logncrashCommand);
1612
- var deployCommand = new import_commander11.Command("deploy").description("NHN Cloud Deploy \uAD00\uB828 \uBA85\uB839");
3800
+ var deployCommand = new import_commander34.Command("deploy").description("NHN Cloud Deploy \uAD00\uB828 \uBA85\uB839");
1613
3801
  deployCommand.addCommand(runCommand);
1614
3802
  deployCommand.addCommand(artifactsCommand);
1615
3803
  deployCommand.addCommand(serverGroupsCommand);
1616
3804
  deployCommand.addCommand(historiesCommand);
3805
+ deployCommand.addCommand(binaryGroupsCommand);
3806
+ deployCommand.addCommand(binariesCommand);
3807
+ deployCommand.addCommand(uploadCommand);
3808
+ deployCommand.addCommand(downloadCommand);
1617
3809
  program.addCommand(deployCommand);
1618
- var instanceCommand = new import_commander11.Command("instance").description("Compute \uC778\uC2A4\uD134\uC2A4 \uAD00\uB828 \uBA85\uB839");
3810
+ var instanceCommand = new import_commander34.Command("instance").description("Compute \uC778\uC2A4\uD134\uC2A4 \uAD00\uB828 \uBA85\uB839");
1619
3811
  instanceCommand.addCommand(listCommand);
1620
- instanceCommand.addCommand(getCommand);
1621
- instanceCommand.addCommand(createCommand);
1622
- instanceCommand.addCommand(deleteCommand);
3812
+ instanceCommand.addCommand(flavorsCommand);
3813
+ instanceCommand.addCommand(availabilityZonesCommand);
3814
+ instanceCommand.addCommand(getCommand2);
3815
+ instanceCommand.addCommand(createCommand3);
3816
+ instanceCommand.addCommand(deleteCommand2);
3817
+ instanceCommand.addCommand(startCommand);
3818
+ instanceCommand.addCommand(stopCommand);
3819
+ instanceCommand.addCommand(rebootCommand);
3820
+ instanceCommand.addCommand(resizeCommand);
3821
+ instanceCommand.addCommand(resizeConfirmCommand);
3822
+ instanceCommand.addCommand(resizeRevertCommand);
3823
+ instanceCommand.addCommand(imagesCommand);
3824
+ instanceCommand.addCommand(keypairsCommand);
3825
+ instanceCommand.addCommand(keypairCommand);
3826
+ instanceCommand.addCommand(volumeCommand);
3827
+ instanceCommand.addCommand(volumesCommand);
1623
3828
  program.addCommand(instanceCommand);
3829
+ var networkCommand = new import_commander34.Command("network").description("VPC\xB7\uC11C\uBE0C\uB137 \uC870\uD68C");
3830
+ networkCommand.addCommand(listCommand3);
3831
+ networkCommand.addCommand(subnetCommand);
3832
+ program.addCommand(networkCommand);
3833
+ var volumeCommand2 = new import_commander34.Command("volume").description("Block Storage \uBCFC\uB968 \uAD00\uB828 \uBA85\uB839");
3834
+ volumeCommand2.addCommand(listCommand2);
3835
+ volumeCommand2.addCommand(getCommand);
3836
+ volumeCommand2.addCommand(createCommand);
3837
+ program.addCommand(volumeCommand2);
3838
+ var floatingipCommand = new import_commander34.Command("floatingip").description(
3839
+ "Floating IP(\uC778\uC2A4\uD134\uC2A4 \uACF5\uC778 IP) \uAD00\uB9AC"
3840
+ );
3841
+ floatingipCommand.addCommand(listCommand4);
3842
+ floatingipCommand.addCommand(createCommand2);
3843
+ floatingipCommand.addCommand(deleteCommand);
3844
+ program.addCommand(floatingipCommand);
1624
3845
  program.parseAsync().catch((err) => {
1625
3846
  const message = err instanceof Error ? err.message : String(err);
1626
3847
  const exitCode = err instanceof NhnCloudCliError ? err.exitCode : 1;
1627
- process.stderr.write(import_chalk4.default.red(`\uC624\uB958: ${message}`) + "\n");
3848
+ process.stderr.write(import_chalk12.default.red(`\uC624\uB958: ${message}`) + "\n");
1628
3849
  process.exit(exitCode);
1629
3850
  });