@bstage-sdk/cli 0.1.0 → 0.2.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/{chunk-S6HGR2U3.js → chunk-FER3TEK7.js} +49 -17
- package/dist/index.js +1985 -196
- package/dist/vite/preset.js +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_TRANSLATION_TIER,
|
|
4
4
|
LOGIN_PATH,
|
|
5
|
+
PORTAL_HOSTS,
|
|
6
|
+
PORTAL_HOST_ENV_KEY,
|
|
5
7
|
TRANSLATION_CDN_BASE_KEY,
|
|
6
8
|
TRANSLATION_CDN_BUCKET_PREFIX,
|
|
7
9
|
TRANSLATION_CDN_ORIGIN,
|
|
@@ -9,13 +11,14 @@ import {
|
|
|
9
11
|
bstageDevPlugin,
|
|
10
12
|
createViteConfig,
|
|
11
13
|
describePhase,
|
|
14
|
+
isPublicPhase,
|
|
12
15
|
resolvePhase,
|
|
13
16
|
resolveProjectTarget
|
|
14
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-FER3TEK7.js";
|
|
15
18
|
|
|
16
19
|
// src/index.ts
|
|
17
20
|
import { readFileSync as readFileSync2 } from "fs";
|
|
18
|
-
import { dirname as
|
|
21
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
19
22
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
20
23
|
import { Command } from "commander";
|
|
21
24
|
|
|
@@ -490,6 +493,952 @@ function formatSize(bytes) {
|
|
|
490
493
|
return `${mb.toFixed(2)} MB`;
|
|
491
494
|
}
|
|
492
495
|
|
|
496
|
+
// src/commands/deploy.ts
|
|
497
|
+
import pc5 from "picocolors";
|
|
498
|
+
|
|
499
|
+
// src/portal/applyPlacements.ts
|
|
500
|
+
import pc3 from "picocolors";
|
|
501
|
+
|
|
502
|
+
// src/portal/client.ts
|
|
503
|
+
var PortalError = class extends Error {
|
|
504
|
+
constructor(status, message, retryAfter) {
|
|
505
|
+
super(message);
|
|
506
|
+
this.status = status;
|
|
507
|
+
this.retryAfter = retryAfter;
|
|
508
|
+
this.name = "PortalError";
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
var MAX_RETRY = 3;
|
|
512
|
+
var DEFAULT_BACKOFF_MS = [5e3, 1e4, 2e4];
|
|
513
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
514
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
515
|
+
var PortalClient = class {
|
|
516
|
+
constructor(opts) {
|
|
517
|
+
this.base = opts.portalUrl.replace(/\/+$/, "") + "/api/v1";
|
|
518
|
+
this.token = opts.token;
|
|
519
|
+
this.fetchFn = opts.fetch ?? globalThis.fetch;
|
|
520
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
|
|
521
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* 429는 `Retry-After`(초)가 있으면 그만큼, 없으면 5/10/20초로 최대 3회 재시도한다.
|
|
525
|
+
* 그 외 실패(4xx/5xx)는 즉시 `PortalError`로 던진다.
|
|
526
|
+
*/
|
|
527
|
+
async request(method, path, body) {
|
|
528
|
+
for (let attempt = 0; ; attempt++) {
|
|
529
|
+
const res = await this.sendWithNetworkRetry(method, path, body);
|
|
530
|
+
if (res.status !== 429 || attempt >= MAX_RETRY) {
|
|
531
|
+
if (!res.ok) throw await toError(res);
|
|
532
|
+
return res;
|
|
533
|
+
}
|
|
534
|
+
await this.sleep(backoffMs(res.headers.get("Retry-After"), attempt));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* 네트워크 계층 실패(`fetch failed`, 예: keep-alive 소켓이 서버 쪽에서 닫힌 뒤 재사용된
|
|
539
|
+
* UND_ERR_SOCKET)는 재시도해도 안전한 요청에 한해 한 번만 즉시 다시 보낸다. sandbox 실측:
|
|
540
|
+
* 디바이스 코드 폴링이 5초 간격이라 그 사이 유휴 연결이 닫혀 첫 재사용에서 실패했다.
|
|
541
|
+
* GET과 디바이스 흐름(code·token)만 대상이다 — deploy 같은 비멱등 POST는 재시도하지 않는다.
|
|
542
|
+
*/
|
|
543
|
+
async sendWithNetworkRetry(method, path, body) {
|
|
544
|
+
try {
|
|
545
|
+
return await this.send(method, path, body);
|
|
546
|
+
} catch (err) {
|
|
547
|
+
if (!isNetworkError(err)) throw err;
|
|
548
|
+
if (!isSafeToRetry(method, path)) throw toNetworkError(err, method, path);
|
|
549
|
+
await this.sleep(NETWORK_RETRY_DELAY_MS);
|
|
550
|
+
try {
|
|
551
|
+
return await this.send(method, path, body);
|
|
552
|
+
} catch (again) {
|
|
553
|
+
throw isNetworkError(again) ? toNetworkError(again, method, path) : again;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/** fetch 한 번. 응답이 없는 실패(타임아웃·네트워크)는 `PortalError`로 바꿔 종료코드 1이 되게 한다. */
|
|
558
|
+
async send(method, path, body) {
|
|
559
|
+
try {
|
|
560
|
+
return await this.fetchFn(this.base + path, {
|
|
561
|
+
method,
|
|
562
|
+
headers: {
|
|
563
|
+
...this.token ? { Authorization: `Bearer ${this.token}` } : {},
|
|
564
|
+
Accept: "application/json, text/plain",
|
|
565
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
566
|
+
},
|
|
567
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
568
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
569
|
+
});
|
|
570
|
+
} catch (err) {
|
|
571
|
+
if (isAbortError(err)) {
|
|
572
|
+
throw new PortalError(
|
|
573
|
+
0,
|
|
574
|
+
`\uD3EC\uD138 \uC751\uB2F5\uC774 ${Math.round(this.timeoutMs / 1e3)}\uCD08 \uC548\uC5D0 \uC624\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 (${method} ${path}). \uB124\uD2B8\uC6CC\uD06C\xB7\uD3EC\uD138 \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
throw err;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async json(method, path, body) {
|
|
581
|
+
return (await this.request(method, path, body)).json();
|
|
582
|
+
}
|
|
583
|
+
stage(org, space) {
|
|
584
|
+
return `/organizations/${enc(org)}/stages/${enc(space)}`;
|
|
585
|
+
}
|
|
586
|
+
me() {
|
|
587
|
+
return this.json("GET", "/auth/me");
|
|
588
|
+
}
|
|
589
|
+
async listSpaces(org) {
|
|
590
|
+
const body = await this.json(
|
|
591
|
+
"GET",
|
|
592
|
+
`/organizations/${enc(org)}/spaces`
|
|
593
|
+
);
|
|
594
|
+
return unwrap(body, "spaces");
|
|
595
|
+
}
|
|
596
|
+
async listRepos(org, space) {
|
|
597
|
+
const body = await this.json(
|
|
598
|
+
"GET",
|
|
599
|
+
`${this.stage(org, space)}/repos`
|
|
600
|
+
);
|
|
601
|
+
return unwrap(body, "repos").map((r) => ({ ...r, spaceId: r.spaceId ?? space }));
|
|
602
|
+
}
|
|
603
|
+
async listBuilds(org, space, limit = 20) {
|
|
604
|
+
const body = await this.json(
|
|
605
|
+
"GET",
|
|
606
|
+
`${this.stage(org, space)}/builds?limit=${limit}`
|
|
607
|
+
);
|
|
608
|
+
return unwrap(body, "builds").map((b) => ({ ...b, spaceId: b.spaceId ?? space }));
|
|
609
|
+
}
|
|
610
|
+
async getBuild(org, space, buildId) {
|
|
611
|
+
const b = await this.json("GET", `${this.stage(org, space)}/builds/${enc(buildId)}`);
|
|
612
|
+
return { ...b, spaceId: b.spaceId ?? space };
|
|
613
|
+
}
|
|
614
|
+
async getBuildLog(org, space, buildId) {
|
|
615
|
+
return (await this.request("GET", `${this.stage(org, space)}/builds/${enc(buildId)}/log`)).text();
|
|
616
|
+
}
|
|
617
|
+
createBuild(org, space, repoId) {
|
|
618
|
+
return this.json("POST", `${this.stage(org, space)}/builds`, { repoId });
|
|
619
|
+
}
|
|
620
|
+
async listPlacements(org, space, surface) {
|
|
621
|
+
const body = await this.json(
|
|
622
|
+
"GET",
|
|
623
|
+
`${this.stage(org, space)}/placements?surface=${surface}`
|
|
624
|
+
);
|
|
625
|
+
return unwrap(body, "placements");
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* `expectedLiveBuildId`는 CAS(compare-and-swap) — 서버 측 live 빌드와 다르면 409.
|
|
629
|
+
* 응답은 배치 전체가 아니라 `{id, liveBuildId}`만 온다(포털 핸들러 그대로).
|
|
630
|
+
*/
|
|
631
|
+
deployPlacement(org, space, placementId, buildId, expectedLiveBuildId) {
|
|
632
|
+
return this.json(
|
|
633
|
+
"POST",
|
|
634
|
+
`${this.stage(org, space)}/placements/${enc(placementId)}/deploy`,
|
|
635
|
+
{ buildId, expectedLiveBuildId }
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
async setPlacementEnabled(org, space, placementId, enabled) {
|
|
639
|
+
const r = await this.json(
|
|
640
|
+
"PATCH",
|
|
641
|
+
`${this.stage(org, space)}/placements/${enc(placementId)}`,
|
|
642
|
+
{ enabled }
|
|
643
|
+
);
|
|
644
|
+
return r.placement;
|
|
645
|
+
}
|
|
646
|
+
// ── 디바이스 코드 흐름(RFC 8628) — 토큰 없이 호출된다 ─────────────────────
|
|
647
|
+
startDeviceAuth(deviceName, cliVersion) {
|
|
648
|
+
return this.json("POST", "/cli/device/code", { deviceName, cliVersion });
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* `authorization_pending`·`slow_down`은 정상 흐름의 결과로 돌려준다(폴링 계속).
|
|
652
|
+
* `expired_token`·`access_denied`는 더 폴링해도 회복되지 않으므로 `PortalError(400, 코드)`로 던진다.
|
|
653
|
+
*/
|
|
654
|
+
async pollDeviceToken(deviceCode) {
|
|
655
|
+
try {
|
|
656
|
+
const issued = await this.json("POST", "/cli/device/token", { deviceCode });
|
|
657
|
+
return { status: "ok", ...issued };
|
|
658
|
+
} catch (err) {
|
|
659
|
+
if (err instanceof PortalError && err.status === 400) {
|
|
660
|
+
if (err.message === "authorization_pending") return { status: "pending" };
|
|
661
|
+
if (err.message === "slow_down") {
|
|
662
|
+
return err.retryAfter === void 0 ? { status: "slow_down" } : { status: "slow_down", retryAfter: err.retryAfter };
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
throw err;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
function enc(s) {
|
|
670
|
+
return encodeURIComponent(s);
|
|
671
|
+
}
|
|
672
|
+
function backoffMs(retryAfterHeader, attempt) {
|
|
673
|
+
const seconds = Number(retryAfterHeader);
|
|
674
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
675
|
+
return Math.min(seconds * 1e3, MAX_RETRY_AFTER_MS);
|
|
676
|
+
}
|
|
677
|
+
return DEFAULT_BACKOFF_MS[attempt] ?? 2e4;
|
|
678
|
+
}
|
|
679
|
+
var NETWORK_RETRY_DELAY_MS = 500;
|
|
680
|
+
function isNetworkError(err) {
|
|
681
|
+
return err instanceof TypeError && !isAbortError(err);
|
|
682
|
+
}
|
|
683
|
+
function toNetworkError(err, method, path) {
|
|
684
|
+
const code = err.cause?.code;
|
|
685
|
+
return new PortalError(
|
|
686
|
+
0,
|
|
687
|
+
`\uD3EC\uD138\uC5D0 \uC5F0\uACB0\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (${method} ${path}${code ? `, ${code}` : ""}). \uB124\uD2B8\uC6CC\uD06C\xB7\uD3EC\uD138 \uC8FC\uC18C\uB97C \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
function isSafeToRetry(method, path) {
|
|
691
|
+
return method === "GET" || path === "/cli/device/token" || path === "/cli/device/code";
|
|
692
|
+
}
|
|
693
|
+
function isAbortError(err) {
|
|
694
|
+
return err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
695
|
+
}
|
|
696
|
+
function unwrap(body, key) {
|
|
697
|
+
if (Array.isArray(body)) return body;
|
|
698
|
+
return body[key] ?? [];
|
|
699
|
+
}
|
|
700
|
+
async function toError(res) {
|
|
701
|
+
const text2 = await res.text().catch(() => "");
|
|
702
|
+
let message = `HTTP ${res.status}`;
|
|
703
|
+
try {
|
|
704
|
+
const parsed = JSON.parse(text2);
|
|
705
|
+
message = parsed.error ?? parsed.message ?? message;
|
|
706
|
+
} catch {
|
|
707
|
+
if (text2.trim()) message = text2.trim().slice(0, 300);
|
|
708
|
+
}
|
|
709
|
+
const retryAfter = Number(res.headers.get("Retry-After"));
|
|
710
|
+
return new PortalError(res.status, message, retryAfter > 0 ? retryAfter : void 0);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/portal/output.ts
|
|
714
|
+
import pc2 from "picocolors";
|
|
715
|
+
var ExitCode = {
|
|
716
|
+
OK: 0,
|
|
717
|
+
FAILURE: 1,
|
|
718
|
+
PRECONDITION: 2,
|
|
719
|
+
AUTH: 3,
|
|
720
|
+
CONFLICT: 4,
|
|
721
|
+
RATE_LIMIT: 5
|
|
722
|
+
};
|
|
723
|
+
var CliExit = class extends Error {
|
|
724
|
+
constructor(code, message) {
|
|
725
|
+
super(message);
|
|
726
|
+
this.code = code;
|
|
727
|
+
this.name = "CliExit";
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
function fail(code, message) {
|
|
731
|
+
throw new CliExit(code, message);
|
|
732
|
+
}
|
|
733
|
+
function exitCodeFor(err) {
|
|
734
|
+
if (err instanceof CliExit) return err.code;
|
|
735
|
+
if (err instanceof PortalError) {
|
|
736
|
+
if (err.status === 401 || err.status === 403) return ExitCode.AUTH;
|
|
737
|
+
if (err.status === 409) return ExitCode.CONFLICT;
|
|
738
|
+
if (err.status === 429) return ExitCode.RATE_LIMIT;
|
|
739
|
+
}
|
|
740
|
+
return ExitCode.FAILURE;
|
|
741
|
+
}
|
|
742
|
+
function printJson(v) {
|
|
743
|
+
process.stdout.write(JSON.stringify(v, null, 2) + "\n");
|
|
744
|
+
}
|
|
745
|
+
function toEntry(i) {
|
|
746
|
+
return { id: i.id, label: i.label, from: i.from, to: i.to, message: i.message };
|
|
747
|
+
}
|
|
748
|
+
function jsonSummary(items, extra = {}) {
|
|
749
|
+
const pick2 = (status) => items.filter((i) => i.status === status).map(toEntry);
|
|
750
|
+
return {
|
|
751
|
+
...extra,
|
|
752
|
+
changed: pick2("changed"),
|
|
753
|
+
unchanged: pick2("unchanged"),
|
|
754
|
+
conflicts: pick2("conflict"),
|
|
755
|
+
failed: pick2("failed")
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function table(rows) {
|
|
759
|
+
const widths = [];
|
|
760
|
+
for (const row of rows) {
|
|
761
|
+
row.forEach((cell, i) => {
|
|
762
|
+
widths[i] = Math.max(widths[i] ?? 0, cell.length);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return rows.map(
|
|
766
|
+
(row) => row.map((cell, i) => i === row.length - 1 ? cell : cell.padEnd(widths[i])).join(" ")
|
|
767
|
+
).join("\n");
|
|
768
|
+
}
|
|
769
|
+
function shortSha(sha) {
|
|
770
|
+
return sha ? sha.slice(0, 7) : "-";
|
|
771
|
+
}
|
|
772
|
+
function scrub(message) {
|
|
773
|
+
return message.replace(/bsc_[0-9a-f]{6,}/g, "bsc_****");
|
|
774
|
+
}
|
|
775
|
+
async function runCommand(fn) {
|
|
776
|
+
try {
|
|
777
|
+
await fn();
|
|
778
|
+
} catch (err) {
|
|
779
|
+
const code = exitCodeFor(err);
|
|
780
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
781
|
+
console.error(pc2.red(`[bstage] ${scrub(message)}`));
|
|
782
|
+
if (code === ExitCode.AUTH) {
|
|
783
|
+
console.error(
|
|
784
|
+
pc2.dim(
|
|
785
|
+
" bstage login \uC73C\uB85C \uD1A0\uD070\uC744 \uB2E4\uC2DC \uB4F1\uB85D\uD558\uAC70\uB098, \uD1A0\uD070 \uB4F1\uAE09\xB7\uC2A4\uD14C\uC774\uC9C0 \uC2A4\uCF54\uD504\uB97C \uD655\uC778\uD558\uC138\uC694."
|
|
786
|
+
)
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
process.exit(code);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/portal/applyPlacements.ts
|
|
794
|
+
function publishHint(placement) {
|
|
795
|
+
return placement.enabled === false ? "\uAC8C\uC2DC \uAEBC\uC9D0 \u2014 bstage publish \uB85C \uAC8C\uC2DC\uD574\uC57C \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBCF4\uC785\uB2C8\uB2E4." : void 0;
|
|
796
|
+
}
|
|
797
|
+
async function applyPlacements(ctx, plan) {
|
|
798
|
+
const { organizationId: org, spaceId: space } = ctx.link;
|
|
799
|
+
const results = [];
|
|
800
|
+
for (const row of plan) {
|
|
801
|
+
const base = { id: row.placement.id, label: row.label, from: row.from, to: row.to };
|
|
802
|
+
if (row.unchanged) {
|
|
803
|
+
results.push({ ...base, status: "unchanged", message: "\uBCC0\uACBD \uC5C6\uC74C" });
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
try {
|
|
807
|
+
await ctx.client.deployPlacement(org, space, row.placement.id, row.to, row.from ?? "");
|
|
808
|
+
results.push({ ...base, status: "changed", message: publishHint(row.placement) });
|
|
809
|
+
} catch (err) {
|
|
810
|
+
results.push({ ...base, ...failureOf(err) });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return results;
|
|
814
|
+
}
|
|
815
|
+
function failureOf(err) {
|
|
816
|
+
if (err instanceof PortalError && err.status === 409) {
|
|
817
|
+
return {
|
|
818
|
+
status: "conflict",
|
|
819
|
+
message: `\uB2E4\uB978 \uBC30\uD3EC\uAC00 \uBA3C\uC800 \uC801\uC6A9\uB410\uC2B5\uB2C8\uB2E4(${err.message}) \u2014 \uB77C\uC774\uBE0C \uC0C1\uD0DC\uAC00 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4.`
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
return { status: "failed", message: err instanceof Error ? err.message : String(err) };
|
|
823
|
+
}
|
|
824
|
+
var MARKS = {
|
|
825
|
+
changed: pc3.green("\u2713"),
|
|
826
|
+
unchanged: pc3.dim("="),
|
|
827
|
+
conflict: pc3.yellow("!"),
|
|
828
|
+
failed: pc3.red("\u2717")
|
|
829
|
+
};
|
|
830
|
+
function printResults(results, out) {
|
|
831
|
+
for (const r of results) {
|
|
832
|
+
const suffix = r.message ? pc3.dim(` ${r.message}`) : "";
|
|
833
|
+
out(`${MARKS[r.status]} ${r.label} ${shortSha(r.from)} \u2192 ${shortSha(r.to)}${suffix}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function reportOutcome(results, options, out, extra = {}) {
|
|
837
|
+
if (options.json) printJson(jsonSummary(results, extra));
|
|
838
|
+
else printResults(results, out);
|
|
839
|
+
const conflicts = results.filter((r) => r.status === "conflict");
|
|
840
|
+
const errors = results.filter((r) => r.status === "failed");
|
|
841
|
+
if (conflicts.length > 0) {
|
|
842
|
+
const names = conflicts.map((r) => r.label).join(", ");
|
|
843
|
+
const errSuffix = errors.length > 0 ? ` (\uADF8 \uC678 \uC2E4\uD328: ${errors.map((r) => r.label).join(", ")})` : "";
|
|
844
|
+
fail(
|
|
845
|
+
ExitCode.CONFLICT,
|
|
846
|
+
`\uB2E4\uB978 \uBC30\uD3EC\uAC00 \uBA3C\uC800 \uC801\uC6A9\uB41C \uBC30\uCE58\uAC00 \uC788\uC2B5\uB2C8\uB2E4: ${names}.${errSuffix} bstage list \uB85C \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
if (errors.length > 0) {
|
|
850
|
+
fail(
|
|
851
|
+
ExitCode.FAILURE,
|
|
852
|
+
`\uC77C\uBD80 \uBC30\uCE58 \uC801\uC6A9\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4: ${errors.map((r) => r.label).join(", ")}`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/portal/confirm.ts
|
|
858
|
+
import * as p from "@clack/prompts";
|
|
859
|
+
var clackConfirm = async (message) => {
|
|
860
|
+
const v = await p.confirm({ message });
|
|
861
|
+
return !p.isCancel(v) && v === true;
|
|
862
|
+
};
|
|
863
|
+
async function askConfirm(deps, message) {
|
|
864
|
+
if (deps.confirm) return deps.confirm(message);
|
|
865
|
+
assertCanConfirm(deps);
|
|
866
|
+
return clackConfirm(message);
|
|
867
|
+
}
|
|
868
|
+
function canConfirm(deps) {
|
|
869
|
+
if (deps.confirm) return true;
|
|
870
|
+
return (deps.isTTY ?? (() => Boolean(process.stdin.isTTY)))();
|
|
871
|
+
}
|
|
872
|
+
function assertCanConfirm(deps) {
|
|
873
|
+
if (canConfirm(deps)) return;
|
|
874
|
+
fail(
|
|
875
|
+
ExitCode.PRECONDITION,
|
|
876
|
+
"\uD130\uBBF8\uB110\uC774 \uC544\uB2C8\uB77C \uD655\uC778\uC744 \uBB3C\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --yes \uB85C \uD655\uC778\uC744 \uAC74\uB108\uB6F0\uC138\uC694."
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/portal/git.ts
|
|
881
|
+
import { execFile } from "child_process";
|
|
882
|
+
import { promisify } from "util";
|
|
883
|
+
var execFileP = promisify(execFile);
|
|
884
|
+
function gitExec(cwd) {
|
|
885
|
+
return async (args) => (await execFileP("git", args, { cwd })).stdout;
|
|
886
|
+
}
|
|
887
|
+
async function tryExec(exec, args) {
|
|
888
|
+
try {
|
|
889
|
+
return (await exec(args)).trim();
|
|
890
|
+
} catch {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function readGitState(exec) {
|
|
895
|
+
const branch = (await exec(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
896
|
+
const head = (await exec(["rev-parse", "HEAD"])).trim();
|
|
897
|
+
const dirty = (await exec(["status", "--porcelain"])).trim().length > 0;
|
|
898
|
+
const upstream = await tryExec(exec, [
|
|
899
|
+
"rev-parse",
|
|
900
|
+
"--abbrev-ref",
|
|
901
|
+
"--symbolic-full-name",
|
|
902
|
+
"@{u}"
|
|
903
|
+
]);
|
|
904
|
+
let ahead = 0;
|
|
905
|
+
let behind = 0;
|
|
906
|
+
if (upstream) {
|
|
907
|
+
const counts = await tryExec(exec, ["rev-list", "--left-right", "--count", "HEAD...@{u}"]);
|
|
908
|
+
const [a, b] = (counts ?? "0 0").split(/\s+/).map((n) => parseInt(n, 10) || 0);
|
|
909
|
+
ahead = a;
|
|
910
|
+
behind = b;
|
|
911
|
+
}
|
|
912
|
+
return { branch, head, dirty, upstream, ahead, behind };
|
|
913
|
+
}
|
|
914
|
+
function assertDeployable(state, defaultBranch) {
|
|
915
|
+
if (state.dirty) {
|
|
916
|
+
fail(
|
|
917
|
+
ExitCode.PRECONDITION,
|
|
918
|
+
"\uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC740 \uBCC0\uACBD\uC774 \uC788\uC2B5\uB2C8\uB2E4. \uD3EC\uD138\uC740 push\uB41C \uCEE4\uBC0B\uB9CC \uBE4C\uB4DC\uD569\uB2C8\uB2E4 \u2014 \uCEE4\uBC0B\xB7push \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694. (--skip-git-check \uB85C \uBB34\uC2DC \uAC00\uB2A5)"
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
if (state.branch !== defaultBranch) {
|
|
922
|
+
fail(
|
|
923
|
+
ExitCode.PRECONDITION,
|
|
924
|
+
`\uD3EC\uD138\uC740 ${defaultBranch} \uBE0C\uB79C\uCE58 HEAD\uB97C \uBE4C\uB4DC\uD569\uB2C8\uB2E4. \uD604\uC7AC \uBE0C\uB79C\uCE58\uB294 ${state.branch} \uC785\uB2C8\uB2E4 \u2014 ${defaultBranch} \uC5D0 \uBA38\uC9C0\xB7push \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.`
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
if (!state.upstream || state.ahead > 0) {
|
|
928
|
+
fail(
|
|
929
|
+
ExitCode.PRECONDITION,
|
|
930
|
+
"push\uB418\uC9C0 \uC54A\uC740 \uCEE4\uBC0B\uC774 \uC788\uC2B5\uB2C8\uB2E4. git push \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694. (--skip-git-check \uB85C \uBB34\uC2DC \uAC00\uB2A5)"
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/portal/config.ts
|
|
936
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
937
|
+
import { dirname, join as join2 } from "path";
|
|
938
|
+
import { loadEnv as loadEnv2 } from "vite";
|
|
939
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
940
|
+
function normalizePortalUrl(url) {
|
|
941
|
+
const u = new URL(url);
|
|
942
|
+
if (u.protocol !== "https:" && !(u.protocol === "http:" && LOCAL_HOSTS.has(u.hostname))) {
|
|
943
|
+
fail(
|
|
944
|
+
ExitCode.PRECONDITION,
|
|
945
|
+
`\uD3EC\uD138 \uC8FC\uC18C\uB294 https \uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uBC1B\uC740 \uAC12: ${url}). http\uB294 localhost\xB7127.0.0.1 \uC5D0\uC11C\uB9CC \uD5C8\uC6A9\uD569\uB2C8\uB2E4.`
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
const path = u.pathname.replace(/\/+$/, "");
|
|
949
|
+
return u.origin + path;
|
|
950
|
+
}
|
|
951
|
+
function linkPath(cwd) {
|
|
952
|
+
return join2(cwd, ".bstage", "project.json");
|
|
953
|
+
}
|
|
954
|
+
async function saveLink(cwd, link) {
|
|
955
|
+
await mkdir2(dirname(linkPath(cwd)), { recursive: true });
|
|
956
|
+
await writeFile(linkPath(cwd), JSON.stringify(link, null, 2) + "\n");
|
|
957
|
+
}
|
|
958
|
+
function linkFromEnv(env2) {
|
|
959
|
+
const { BSTAGE_PORTAL_URL, BSTAGE_ORG, BSTAGE_SPACE, BSTAGE_REPO, BSTAGE_SURFACE } = env2;
|
|
960
|
+
if (!BSTAGE_PORTAL_URL || !BSTAGE_ORG || !BSTAGE_SPACE || !BSTAGE_REPO || !BSTAGE_SURFACE) {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
if (BSTAGE_SURFACE !== "USER" && BSTAGE_SURFACE !== "ADMIN") return null;
|
|
964
|
+
return {
|
|
965
|
+
portalUrl: BSTAGE_PORTAL_URL,
|
|
966
|
+
organizationId: BSTAGE_ORG,
|
|
967
|
+
spaceId: BSTAGE_SPACE,
|
|
968
|
+
repoId: BSTAGE_REPO,
|
|
969
|
+
surface: BSTAGE_SURFACE
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
async function loadLink(cwd, env2 = process.env) {
|
|
973
|
+
const fromEnv = linkFromEnv(env2);
|
|
974
|
+
if (fromEnv) return fromEnv;
|
|
975
|
+
try {
|
|
976
|
+
return JSON.parse(await readFile2(linkPath(cwd), "utf-8"));
|
|
977
|
+
} catch {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function portalUrlForPhase(phase, cwd, env2 = process.env) {
|
|
982
|
+
if (isPublicPhase(phase) && PORTAL_HOSTS[phase]) return `https://${PORTAL_HOSTS[phase]}`;
|
|
983
|
+
const host = internalPortalHost(cwd, env2);
|
|
984
|
+
if (!host) {
|
|
985
|
+
throw new Error(
|
|
986
|
+
`${phase}\uB294 \uC0AC\uB0B4 \uC804\uC6A9 \uD658\uACBD\uC774\uB77C \uD3EC\uD138 \uC8FC\uC18C\uAC00 SDK\uC5D0 \uB4E4\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
|
|
987
|
+
\uC124\uC815\uB418\uC9C0 \uC54A\uC740 \uAC12: ${PORTAL_HOST_ENV_KEY}
|
|
988
|
+
\xB7 \uC0AC\uB0B4: \uC704 \uAC12\uC744 .env\uC5D0 \uC124\uC815\uD558\uC138\uC694 (\uD638\uC2A4\uD2B8 \uAC12\uC740 \uC0AC\uB0B4 \uBB38\uC11C\uB97C \uCC38\uACE0\uD558\uC138\uC694)
|
|
989
|
+
\xB7 \uADF8 \uC678: --phase sandbox \uB610\uB294 --portal <url> \uC744 \uC9C0\uC815\uD558\uC138\uC694`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
return normalizePortalUrl(host);
|
|
993
|
+
}
|
|
994
|
+
function allowedPortalOrigins(cwd, env2 = process.env, explicit) {
|
|
995
|
+
const origins = new Set(Object.values(PORTAL_HOSTS).map((host) => `https://${host}`));
|
|
996
|
+
if (explicit) origins.add(originOf(explicit));
|
|
997
|
+
if (env2.BSTAGE_PORTAL_URL) origins.add(originOf(env2.BSTAGE_PORTAL_URL));
|
|
998
|
+
if (!env2.BSTAGE_TOKEN) {
|
|
999
|
+
const internal = internalPortalHost(cwd, env2);
|
|
1000
|
+
if (internal) origins.add(originOf(internal));
|
|
1001
|
+
}
|
|
1002
|
+
return origins;
|
|
1003
|
+
}
|
|
1004
|
+
function internalPortalHost(cwd, env2) {
|
|
1005
|
+
const fileEnv = loadEnv2("development", cwd, "");
|
|
1006
|
+
const host = (env2[PORTAL_HOST_ENV_KEY] || fileEnv[PORTAL_HOST_ENV_KEY] || "").trim();
|
|
1007
|
+
if (!host) return null;
|
|
1008
|
+
return host.startsWith("http") ? host : `https://${host}`;
|
|
1009
|
+
}
|
|
1010
|
+
function originOf(url) {
|
|
1011
|
+
return new URL(normalizePortalUrl(url)).origin;
|
|
1012
|
+
}
|
|
1013
|
+
var TOKEN_ENV_HINT = "BSTAGE_TOKEN \uC774 \uC124\uC815\uB41C \uD658\uACBD\uC5D0\uC11C\uB294 \uD3EC\uD138 \uC8FC\uC18C\uB97C --portal, BSTAGE_PORTAL_URL, \uB610\uB294 \uACF5\uAC1C phase \uB9E4\uD551\uC73C\uB85C\uB9CC \uC815\uD569\uB2C8\uB2E4.";
|
|
1014
|
+
function portalUrlFromLink(raw, cwd, env2) {
|
|
1015
|
+
const url = normalizePortalUrl(raw);
|
|
1016
|
+
if (env2.BSTAGE_TOKEN) {
|
|
1017
|
+
fail(
|
|
1018
|
+
ExitCode.PRECONDITION,
|
|
1019
|
+
`${TOKEN_ENV_HINT} .bstage/project.json \uC758 \uC8FC\uC18C\uB294 \uC4F0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 --portal \uB610\uB294 BSTAGE_PORTAL_URL \uB85C \uBA85\uC2DC\uD558\uC138\uC694.`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
const origin = new URL(url).origin;
|
|
1023
|
+
if (!allowedPortalOrigins(cwd, env2).has(origin)) {
|
|
1024
|
+
fail(
|
|
1025
|
+
ExitCode.PRECONDITION,
|
|
1026
|
+
`.bstage/project.json \uC758 \uD3EC\uD138 \uC8FC\uC18C(${origin})\uAC00 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4. --portal \uB610\uB294 BSTAGE_PORTAL_URL \uB85C \uBA85\uC2DC\uD558\uAC70\uB098 bstage link \uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
return url;
|
|
1030
|
+
}
|
|
1031
|
+
function portalUrlFromPhase(phaseFlag, cwd, env2) {
|
|
1032
|
+
const { phase } = resolvePhase(phaseFlag, cwd, env2);
|
|
1033
|
+
const url = portalUrlForPhase(phase, cwd, env2);
|
|
1034
|
+
if (env2.BSTAGE_TOKEN && !allowedPortalOrigins(cwd, env2).has(new URL(url).origin)) {
|
|
1035
|
+
fail(
|
|
1036
|
+
ExitCode.PRECONDITION,
|
|
1037
|
+
`${TOKEN_ENV_HINT} .env \uC758 ${PORTAL_HOST_ENV_KEY}(${phase})\uB294 \uC4F0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 --portal \uB610\uB294 BSTAGE_PORTAL_URL \uB85C \uBA85\uC2DC\uD558\uC138\uC694.`
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
return url;
|
|
1041
|
+
}
|
|
1042
|
+
async function resolvePortalUrl(explicit, cwd, env2 = process.env, phaseFlag, options = {}) {
|
|
1043
|
+
if (explicit) return normalizePortalUrl(explicit);
|
|
1044
|
+
if (env2.BSTAGE_PORTAL_URL) return normalizePortalUrl(env2.BSTAGE_PORTAL_URL);
|
|
1045
|
+
const link = options.allowLink === false ? null : await loadLink(cwd, env2);
|
|
1046
|
+
if (link) return portalUrlFromLink(link.portalUrl, cwd, env2);
|
|
1047
|
+
return portalUrlFromPhase(phaseFlag, cwd, env2);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// src/portal/credentials.ts
|
|
1051
|
+
import { chmod, mkdir as mkdir3, readFile as readFile3, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
1052
|
+
import { homedir } from "os";
|
|
1053
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1054
|
+
var UNKNOWN_ORG = "*";
|
|
1055
|
+
function credentialsPath(env2 = process.env) {
|
|
1056
|
+
const base = env2.XDG_CONFIG_HOME || join3(homedir(), ".config");
|
|
1057
|
+
return join3(base, "bstage", "credentials.json");
|
|
1058
|
+
}
|
|
1059
|
+
function hasEntry(bucket, org) {
|
|
1060
|
+
return Object.prototype.hasOwnProperty.call(bucket, org);
|
|
1061
|
+
}
|
|
1062
|
+
function isEntry(v) {
|
|
1063
|
+
if (!v || typeof v !== "object") return false;
|
|
1064
|
+
const token = v.token;
|
|
1065
|
+
return typeof token === "string" && token.length > 0;
|
|
1066
|
+
}
|
|
1067
|
+
function sanitize(tokens) {
|
|
1068
|
+
const buckets = Object.entries(tokens).map(([portal, bucket]) => {
|
|
1069
|
+
if (!bucket || typeof bucket !== "object") return [portal, {}];
|
|
1070
|
+
const kept = Object.entries(bucket).filter(([, v]) => isEntry(v));
|
|
1071
|
+
return [portal, Object.fromEntries(kept)];
|
|
1072
|
+
});
|
|
1073
|
+
return Object.fromEntries(buckets.filter(([, bucket]) => Object.keys(bucket).length > 0));
|
|
1074
|
+
}
|
|
1075
|
+
function migrate(raw) {
|
|
1076
|
+
if (!raw || typeof raw !== "object") return { version: 2, tokens: {} };
|
|
1077
|
+
const obj = raw;
|
|
1078
|
+
if (obj.version === 2 && obj.tokens && typeof obj.tokens === "object") {
|
|
1079
|
+
return { version: 2, tokens: sanitize(obj.tokens) };
|
|
1080
|
+
}
|
|
1081
|
+
const tokens = Object.entries(obj).filter(([, v]) => isEntry(v)).map(([portal, v]) => [portal, { [UNKNOWN_ORG]: v }]);
|
|
1082
|
+
return { version: 2, tokens: Object.fromEntries(tokens) };
|
|
1083
|
+
}
|
|
1084
|
+
async function readCredentialFile(env2) {
|
|
1085
|
+
try {
|
|
1086
|
+
return migrate(JSON.parse(await readFile3(credentialsPath(env2), "utf-8")));
|
|
1087
|
+
} catch {
|
|
1088
|
+
return { version: 2, tokens: {} };
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
async function writeCredentialFile(env2, data) {
|
|
1092
|
+
const p4 = credentialsPath(env2);
|
|
1093
|
+
await mkdir3(dirname2(p4), { recursive: true, mode: 448 });
|
|
1094
|
+
await chmod(dirname2(p4), 448);
|
|
1095
|
+
const tmp = `${p4}.${process.pid}.${Date.now()}.tmp`;
|
|
1096
|
+
await writeFile2(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
|
|
1097
|
+
await chmod(tmp, 384);
|
|
1098
|
+
await rename2(tmp, p4);
|
|
1099
|
+
}
|
|
1100
|
+
function entryOf(c) {
|
|
1101
|
+
return {
|
|
1102
|
+
token: c.token,
|
|
1103
|
+
savedAt: c.savedAt,
|
|
1104
|
+
permission: c.permission,
|
|
1105
|
+
allSpaces: c.allSpaces,
|
|
1106
|
+
spaceIds: c.spaceIds,
|
|
1107
|
+
expiresAt: c.expiresAt
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
async function saveCredential(c, env2 = process.env) {
|
|
1111
|
+
const key = normalizePortalUrl(c.portalUrl);
|
|
1112
|
+
const org = c.organizationId || UNKNOWN_ORG;
|
|
1113
|
+
const data = await readCredentialFile(env2);
|
|
1114
|
+
const kept = Object.entries(data.tokens[key] ?? {}).filter(
|
|
1115
|
+
([o]) => org === UNKNOWN_ORG || o !== UNKNOWN_ORG
|
|
1116
|
+
);
|
|
1117
|
+
const bucket = { ...Object.fromEntries(kept), [org]: entryOf(c) };
|
|
1118
|
+
await writeCredentialFile(env2, { version: 2, tokens: { ...data.tokens, [key]: bucket } });
|
|
1119
|
+
}
|
|
1120
|
+
async function bucketOf(portalUrl, env2) {
|
|
1121
|
+
return (await readCredentialFile(env2)).tokens[normalizePortalUrl(portalUrl)] ?? {};
|
|
1122
|
+
}
|
|
1123
|
+
function pickOrg(bucket, organizationId) {
|
|
1124
|
+
const orgs = Object.keys(bucket);
|
|
1125
|
+
if (organizationId) {
|
|
1126
|
+
if (hasEntry(bucket, organizationId)) return organizationId;
|
|
1127
|
+
return orgs.length === 1 && orgs[0] === UNKNOWN_ORG ? UNKNOWN_ORG : null;
|
|
1128
|
+
}
|
|
1129
|
+
if (hasEntry(bucket, UNKNOWN_ORG)) return UNKNOWN_ORG;
|
|
1130
|
+
return orgs.length === 1 ? orgs[0] : null;
|
|
1131
|
+
}
|
|
1132
|
+
async function loadCredential(portalUrl, organizationId, env2 = process.env) {
|
|
1133
|
+
const bucket = await bucketOf(portalUrl, env2);
|
|
1134
|
+
const picked = pickOrg(bucket, organizationId);
|
|
1135
|
+
if (!picked) return null;
|
|
1136
|
+
return { portalUrl: normalizePortalUrl(portalUrl), organizationId: picked, ...bucket[picked] };
|
|
1137
|
+
}
|
|
1138
|
+
async function listCredentialOrgs(portalUrl, env2 = process.env) {
|
|
1139
|
+
return Object.keys(await bucketOf(portalUrl, env2)).sort();
|
|
1140
|
+
}
|
|
1141
|
+
async function listCredentials(portalUrl, env2 = process.env) {
|
|
1142
|
+
const bucket = await bucketOf(portalUrl, env2);
|
|
1143
|
+
const key = normalizePortalUrl(portalUrl);
|
|
1144
|
+
return Object.keys(bucket).sort().map((organizationId) => ({ portalUrl: key, organizationId, ...bucket[organizationId] }));
|
|
1145
|
+
}
|
|
1146
|
+
async function deleteCredential(portalUrl, organizationId, env2 = process.env) {
|
|
1147
|
+
const data = await readCredentialFile(env2);
|
|
1148
|
+
const key = normalizePortalUrl(portalUrl);
|
|
1149
|
+
const bucket = data.tokens[key];
|
|
1150
|
+
if (!bucket) return [];
|
|
1151
|
+
const resolved = organizationId ? pickOrg(bucket, organizationId) : null;
|
|
1152
|
+
const legacy = resolved && hasEntry(bucket, UNKNOWN_ORG) ? [UNKNOWN_ORG] : [];
|
|
1153
|
+
const removed = organizationId ? resolved ? [.../* @__PURE__ */ new Set([resolved, ...legacy])] : [] : Object.keys(bucket).sort();
|
|
1154
|
+
if (removed.length === 0) return [];
|
|
1155
|
+
const rest = Object.entries(bucket).filter(([org]) => !removed.includes(org));
|
|
1156
|
+
const tokens = Object.fromEntries(
|
|
1157
|
+
Object.entries(data.tokens).filter(([portal]) => portal !== key).concat(rest.length > 0 ? [[key, Object.fromEntries(rest)]] : [])
|
|
1158
|
+
);
|
|
1159
|
+
if (Object.keys(tokens).length === 0) {
|
|
1160
|
+
await rm2(credentialsPath(env2), { force: true });
|
|
1161
|
+
} else {
|
|
1162
|
+
await writeCredentialFile(env2, { version: 2, tokens });
|
|
1163
|
+
}
|
|
1164
|
+
return removed;
|
|
1165
|
+
}
|
|
1166
|
+
async function resolveToken(portalUrl, env2 = process.env, organizationId) {
|
|
1167
|
+
if (env2.BSTAGE_TOKEN) return env2.BSTAGE_TOKEN;
|
|
1168
|
+
return (await loadCredential(portalUrl, organizationId, env2))?.token ?? null;
|
|
1169
|
+
}
|
|
1170
|
+
function scopeLabel(allSpaces, spaceIds = []) {
|
|
1171
|
+
if (allSpaces) return "\uC774 \uC870\uC9C1\uC758 \uBAA8\uB4E0 \uC2A4\uD14C\uC774\uC9C0";
|
|
1172
|
+
if (spaceIds.length === 0) return "\uC2A4\uD14C\uC774\uC9C0 \uC5C6\uC74C";
|
|
1173
|
+
return `\uC2A4\uD14C\uC774\uC9C0 ${spaceIds.length}\uAC1C (${spaceIds.join(", ")})`;
|
|
1174
|
+
}
|
|
1175
|
+
function orgLabel(organizationId) {
|
|
1176
|
+
return organizationId === UNKNOWN_ORG ? "\uC870\uC9C1 \uBBF8\uC0C1" : organizationId;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/portal/resolveContext.ts
|
|
1180
|
+
function failNoToken(portalUrl, organizationId, storedOrgs) {
|
|
1181
|
+
const named = storedOrgs.filter((o) => o !== UNKNOWN_ORG);
|
|
1182
|
+
const listed = named.length > 0 ? `
|
|
1183
|
+
\uC800\uC7A5\uB41C \uC870\uC9C1: ${named.join(", ")}` : "";
|
|
1184
|
+
if (organizationId) {
|
|
1185
|
+
fail(
|
|
1186
|
+
ExitCode.AUTH,
|
|
1187
|
+
`${portalUrl} \uC758 \uC870\uC9C1 ${organizationId} \uD1A0\uD070\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774 \uC870\uC9C1\uC73C\uB85C bstage login \uD558\uC138\uC694.${listed}`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
if (named.length > 1) {
|
|
1191
|
+
fail(
|
|
1192
|
+
ExitCode.AUTH,
|
|
1193
|
+
`${portalUrl} \uC5D0 \uC5EC\uB7EC \uC870\uC9C1\uC758 \uD1A0\uD070\uC774 \uC800\uC7A5\uB3FC \uC788\uC5B4 \uD558\uB098\uB97C \uACE0\uB97C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --org <id> \uB610\uB294 BSTAGE_ORG \uB85C \uC870\uC9C1\uC744 \uC9C0\uC815\uD558\uAC70\uB098, \uC4F0\uB824\uB294 \uC870\uC9C1\uC73C\uB85C bstage login \uD558\uC138\uC694.${listed}`
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
fail(
|
|
1197
|
+
ExitCode.AUTH,
|
|
1198
|
+
`${portalUrl} \uC5D0 \uB85C\uADF8\uC778\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. bstage login --portal ${portalUrl} \uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.`
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
async function resolveClientOnly(opts = {}) {
|
|
1202
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1203
|
+
const env2 = opts.env ?? process.env;
|
|
1204
|
+
const portalUrl = await resolvePortalUrl(opts.portal, cwd, env2, opts.phase);
|
|
1205
|
+
if (!portalUrl) {
|
|
1206
|
+
fail(
|
|
1207
|
+
ExitCode.PRECONDITION,
|
|
1208
|
+
"\uD3EC\uD138 \uC8FC\uC18C\uB97C \uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --portal <url> \uB610\uB294 --phase \uB97C \uC9C0\uC815\uD558\uC138\uC694."
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
const organizationId = opts.organizationId ?? env2.BSTAGE_ORG ?? (await loadLink(cwd, env2))?.organizationId;
|
|
1212
|
+
const token = await resolveToken(portalUrl, env2, organizationId);
|
|
1213
|
+
if (!token) failNoToken(portalUrl, organizationId, await listCredentialOrgs(portalUrl, env2));
|
|
1214
|
+
return {
|
|
1215
|
+
client: new PortalClient({ portalUrl, token, fetch: opts.fetch }),
|
|
1216
|
+
portalUrl,
|
|
1217
|
+
organizationId
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
async function resolveContext(opts = {}) {
|
|
1221
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1222
|
+
const env2 = opts.env ?? process.env;
|
|
1223
|
+
const link = await loadLink(cwd, env2);
|
|
1224
|
+
if (!link) {
|
|
1225
|
+
fail(
|
|
1226
|
+
ExitCode.PRECONDITION,
|
|
1227
|
+
"\uC774 \uB514\uB809\uD130\uB9AC\uB294 \uD3EC\uD138 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uC5F0\uACB0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. bstage link \uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
const portalUrl = await resolvePortalUrl(opts.portal, cwd, env2);
|
|
1231
|
+
if (!portalUrl) {
|
|
1232
|
+
fail(
|
|
1233
|
+
ExitCode.PRECONDITION,
|
|
1234
|
+
"\uD3EC\uD138 \uC8FC\uC18C\uB97C \uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --portal <url> \uB610\uB294 BSTAGE_PORTAL_URL \uC744 \uC9C0\uC815\uD558\uC138\uC694."
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const organizationId = opts.organizationId ?? link.organizationId;
|
|
1238
|
+
const token = await resolveToken(portalUrl, env2, organizationId);
|
|
1239
|
+
if (!token) failNoToken(portalUrl, organizationId, await listCredentialOrgs(portalUrl, env2));
|
|
1240
|
+
return {
|
|
1241
|
+
client: new PortalClient({ portalUrl, token, fetch: opts.fetch }),
|
|
1242
|
+
link: { ...link, portalUrl },
|
|
1243
|
+
organizationId
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// src/portal/types.ts
|
|
1248
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
1249
|
+
"SUCCEEDED",
|
|
1250
|
+
"FAILED",
|
|
1251
|
+
"TIMED_OUT",
|
|
1252
|
+
"CANCELED"
|
|
1253
|
+
]);
|
|
1254
|
+
function placementLabel(p4) {
|
|
1255
|
+
return p4.kind === "SLOT" ? p4.slotId ?? p4.artifact ?? p4.id : p4.path ?? p4.artifact ?? p4.id;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/portal/selectPlacements.ts
|
|
1259
|
+
function selectPlacements(all, repoId, filter) {
|
|
1260
|
+
const mine = all.filter((p4) => p4.stageRepoId === repoId);
|
|
1261
|
+
if (!filter) return mine;
|
|
1262
|
+
return mine.filter((p4) => p4.id === filter || p4.path === filter || p4.slotId === filter);
|
|
1263
|
+
}
|
|
1264
|
+
function applyPlan(placements, buildId) {
|
|
1265
|
+
return placements.map((p4) => ({
|
|
1266
|
+
placement: p4,
|
|
1267
|
+
label: placementLabel(p4),
|
|
1268
|
+
from: p4.liveBuildId || void 0,
|
|
1269
|
+
to: buildId,
|
|
1270
|
+
unchanged: p4.liveBuildId === buildId
|
|
1271
|
+
}));
|
|
1272
|
+
}
|
|
1273
|
+
function requireTargets(all, repoId, filter, buildIdHint) {
|
|
1274
|
+
const mine = selectPlacements(all, repoId, filter);
|
|
1275
|
+
if (mine.length > 0) return mine;
|
|
1276
|
+
const base = filter ? `\uBC30\uCE58 '${filter}' \uC744(\uB97C) \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. bstage list \uB85C \uD655\uC778\uD558\uC138\uC694.` : "\uC774 \uB808\uD3EC\uC5D0 \uC5F0\uACB0\uB41C \uBC30\uCE58\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uD3EC\uD138 > \uC2A4\uD14C\uC774\uC9C0 > \uD398\uC774\uC9C0\uC5D0\uC11C \uBC30\uCE58\uB97C \uBA3C\uC800 \uB9CC\uB4DC\uC138\uC694.";
|
|
1277
|
+
const hint = buildIdHint ? ` \uBE4C\uB4DC ${buildIdHint} \uB294 \uB0A8\uC544 \uC788\uC73C\uB2C8 \uBC30\uCE58\uB97C \uD655\uC778\uD55C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC801\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.` : "";
|
|
1278
|
+
return fail(ExitCode.PRECONDITION, base + hint);
|
|
1279
|
+
}
|
|
1280
|
+
async function fetchTargets(ctx, filter, buildIdHint) {
|
|
1281
|
+
const all = await ctx.client.listPlacements(
|
|
1282
|
+
ctx.link.organizationId,
|
|
1283
|
+
ctx.link.spaceId,
|
|
1284
|
+
ctx.link.surface
|
|
1285
|
+
);
|
|
1286
|
+
return requireTargets(all, ctx.link.repoId, filter, buildIdHint);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/portal/waitBuild.ts
|
|
1290
|
+
import pc4 from "picocolors";
|
|
1291
|
+
var DEFAULT_INTERVAL = 3e3;
|
|
1292
|
+
var DEFAULT_TIMEOUT = 20 * 60 * 1e3;
|
|
1293
|
+
async function waitForBuild(client, org, space, buildId, o = {}) {
|
|
1294
|
+
const interval = o.intervalMs ?? DEFAULT_INTERVAL;
|
|
1295
|
+
const timeout = o.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
1296
|
+
const sleep = o.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1297
|
+
const started = Date.now();
|
|
1298
|
+
let seen = 0;
|
|
1299
|
+
let logFailing = false;
|
|
1300
|
+
const flushLog = async () => {
|
|
1301
|
+
if (!o.onLog) return;
|
|
1302
|
+
let log2;
|
|
1303
|
+
try {
|
|
1304
|
+
log2 = await client.getBuildLog(org, space, buildId);
|
|
1305
|
+
} catch {
|
|
1306
|
+
if (!logFailing) {
|
|
1307
|
+
console.error(pc4.dim("[bstage] \uB85C\uADF8 \uC870\uD68C \uC2E4\uD328, \uB2E4\uC74C \uD3F4\uB9C1\uC5D0\uC11C \uC7AC\uC2DC\uB3C4\uD569\uB2C8\uB2E4."));
|
|
1308
|
+
logFailing = true;
|
|
1309
|
+
}
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
logFailing = false;
|
|
1313
|
+
if (log2.length < seen) {
|
|
1314
|
+
o.onLog("\n[bstage] \uB85C\uADF8\uAC00 \uC7AC\uC2DC\uC791\uB41C \uAC83\uC73C\uB85C \uBCF4\uC785\uB2C8\uB2E4 \u2014 \uCC98\uC74C\uBD80\uD130 \uB2E4\uC2DC \uCD9C\uB825\uD569\uB2C8\uB2E4.\n");
|
|
1315
|
+
seen = 0;
|
|
1316
|
+
}
|
|
1317
|
+
if (log2.length > seen) {
|
|
1318
|
+
o.onLog(log2.slice(seen));
|
|
1319
|
+
seen = log2.length;
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
for (; ; ) {
|
|
1323
|
+
const build = await client.getBuild(org, space, buildId);
|
|
1324
|
+
await flushLog();
|
|
1325
|
+
if (TERMINAL_STATUSES.has(build.status)) return build;
|
|
1326
|
+
if (Date.now() - started > timeout) {
|
|
1327
|
+
fail(
|
|
1328
|
+
ExitCode.FAILURE,
|
|
1329
|
+
`\uBE4C\uB4DC ${buildId} \uAC00 ${Math.round(timeout / 6e4)}\uBD84 \uC548\uC5D0 \uB05D\uB098\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uD3EC\uD138\uC5D0\uC11C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694.`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
await sleep(interval);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/commands/deploy.ts
|
|
1337
|
+
async function repoDefaultBranch(ctx) {
|
|
1338
|
+
const repos = await ctx.client.listRepos(ctx.link.organizationId, ctx.link.spaceId);
|
|
1339
|
+
const repo = repos.find((r) => r.id === ctx.link.repoId);
|
|
1340
|
+
if (!repo) {
|
|
1341
|
+
fail(
|
|
1342
|
+
ExitCode.PRECONDITION,
|
|
1343
|
+
`\uC5F0\uACB0\uB41C \uB808\uD3EC ${ctx.link.repoId} \uB97C \uD3EC\uD138\uC5D0\uC11C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. bstage link \uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.`
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
return repo.defaultBranch;
|
|
1347
|
+
}
|
|
1348
|
+
async function precheckGit(deps, defaultBranch) {
|
|
1349
|
+
const exec = deps.exec ?? gitExec(deps.cwd ?? process.cwd());
|
|
1350
|
+
let state;
|
|
1351
|
+
try {
|
|
1352
|
+
state = await readGitState(exec);
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
fail(
|
|
1355
|
+
ExitCode.PRECONDITION,
|
|
1356
|
+
`git \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${err instanceof Error ? err.message : String(err)}. git\uC774 \uC124\uCE58\uB3FC \uC788\uACE0 \uC774 \uB514\uB809\uD130\uB9AC\uAC00 git \uC800\uC7A5\uC18C\uC778\uC9C0 \uD655\uC778\uD558\uAC70\uB098 --skip-git-check \uB85C \uAC74\uB108\uB6F0\uC138\uC694.`
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
assertDeployable(state, defaultBranch);
|
|
1360
|
+
}
|
|
1361
|
+
async function createAndAnnounceBuild(ctx, options) {
|
|
1362
|
+
const created = await ctx.client.createBuild(
|
|
1363
|
+
ctx.link.organizationId,
|
|
1364
|
+
ctx.link.spaceId,
|
|
1365
|
+
ctx.link.repoId
|
|
1366
|
+
);
|
|
1367
|
+
if (!options.json) {
|
|
1368
|
+
console.error(pc5.dim(`[bstage] \uBE4C\uB4DC ${created.id} \uC2DC\uC791 (${shortSha(created.commitSha)})`));
|
|
1369
|
+
}
|
|
1370
|
+
return created;
|
|
1371
|
+
}
|
|
1372
|
+
function reportNoWait(build, options, out) {
|
|
1373
|
+
if (options.json) {
|
|
1374
|
+
printJson(jsonSummary([], { build }));
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
out(
|
|
1378
|
+
`\uBE4C\uB4DC ${build.id} \uB97C \uC2DC\uC791\uD588\uC2B5\uB2C8\uB2E4. bstage logs ${build.id} --follow \uB85C \uC9C4\uD589 \uC0C1\uD669\uC744 \uD655\uC778\uD558\uC138\uC694.`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
async function waitForCompletedBuild(ctx, deps, build, json) {
|
|
1382
|
+
const { organizationId: org, spaceId: space } = ctx.link;
|
|
1383
|
+
const done = await waitForBuild(ctx.client, org, space, build.id, {
|
|
1384
|
+
sleep: deps.sleep,
|
|
1385
|
+
onLog: json ? void 0 : (delta) => process.stderr.write(pc5.dim(delta))
|
|
1386
|
+
});
|
|
1387
|
+
if (done.status !== "SUCCEEDED") {
|
|
1388
|
+
fail(
|
|
1389
|
+
ExitCode.FAILURE,
|
|
1390
|
+
`\uBE4C\uB4DC ${done.id} \uC2E4\uD328: ${done.status}${done.failureReason ? ` (${done.failureReason})` : ""}. bstage logs ${done.id} \uB85C \uB85C\uADF8\uB97C \uD655\uC778\uD558\uC138\uC694.`
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
return done;
|
|
1394
|
+
}
|
|
1395
|
+
function renderPlanTable(plan, build) {
|
|
1396
|
+
const header = `\uBE4C\uB4DC ${build.id} (\uCEE4\uBC0B ${shortSha(build.commitSha)})`;
|
|
1397
|
+
const rows = table([
|
|
1398
|
+
["\uC790\uB9AC", "\uD604\uC7AC", "\u2192 \uC801\uC6A9"],
|
|
1399
|
+
...plan.map((row) => {
|
|
1400
|
+
const to = shortSha(build.commitSha) + (row.unchanged ? " (\uBCC0\uACBD \uC5C6\uC74C)" : "");
|
|
1401
|
+
return [row.label, shortSha(row.placement.liveCommit), to];
|
|
1402
|
+
})
|
|
1403
|
+
]);
|
|
1404
|
+
return `${header}
|
|
1405
|
+
${rows}`;
|
|
1406
|
+
}
|
|
1407
|
+
async function confirmPlan(plan, build, options, deps) {
|
|
1408
|
+
if (options.yes) return;
|
|
1409
|
+
const out = deps.out ?? console.log;
|
|
1410
|
+
out(renderPlanTable(plan, build));
|
|
1411
|
+
const ok = await askConfirm(
|
|
1412
|
+
deps,
|
|
1413
|
+
`${plan.length}\uAC1C \uBC30\uCE58\uB97C \uBE4C\uB4DC ${build.id} \uB85C \uB77C\uC774\uBE0C \uC804\uD658\uD569\uB2C8\uB2E4. \uC9C4\uD589\uD560\uAE4C\uC694?`
|
|
1414
|
+
);
|
|
1415
|
+
if (!ok) {
|
|
1416
|
+
fail(ExitCode.PRECONDITION, "\uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4. \uBE4C\uB4DC\uB294 \uB0A8\uC544 \uC788\uC73C\uB2C8 \uD544\uC694\uD558\uBA74 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC801\uC6A9\uD558\uC138\uC694.");
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
async function deployCommand(options, deps = {}) {
|
|
1420
|
+
if (options.json && !options.yes) {
|
|
1421
|
+
fail(ExitCode.PRECONDITION, "--json \uC740 --yes \uC640 \uD568\uAED8 \uC4F0\uC138\uC694.");
|
|
1422
|
+
}
|
|
1423
|
+
if (!options.yes) assertCanConfirm(deps);
|
|
1424
|
+
const out = deps.out ?? console.log;
|
|
1425
|
+
const ctx = await resolveContext({ cwd: deps.cwd, env: deps.env, fetch: deps.fetch });
|
|
1426
|
+
const defaultBranch = await repoDefaultBranch(ctx);
|
|
1427
|
+
if (!options.skipGitCheck) await precheckGit(deps, defaultBranch);
|
|
1428
|
+
await fetchTargets(ctx, options.placement);
|
|
1429
|
+
const created = await createAndAnnounceBuild(ctx, options);
|
|
1430
|
+
if (options.noWait) {
|
|
1431
|
+
reportNoWait(created, options, out);
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
const build = await waitForCompletedBuild(ctx, deps, created, options.json);
|
|
1435
|
+
const fresh = await fetchTargets(ctx, options.placement, build.id);
|
|
1436
|
+
const plan = applyPlan(fresh, build.id);
|
|
1437
|
+
await confirmPlan(plan, build, options, deps);
|
|
1438
|
+
const results = await applyPlacements(ctx, plan);
|
|
1439
|
+
reportOutcome(results, options, out, { build });
|
|
1440
|
+
}
|
|
1441
|
+
|
|
493
1442
|
// src/commands/dev.ts
|
|
494
1443
|
import { createServer as createViteServer, searchForWorkspaceRoot } from "vite";
|
|
495
1444
|
import react from "@vitejs/plugin-react";
|
|
@@ -535,11 +1484,11 @@ async function devCommand(options) {
|
|
|
535
1484
|
}
|
|
536
1485
|
|
|
537
1486
|
// src/commands/init.ts
|
|
538
|
-
import * as
|
|
539
|
-
import
|
|
1487
|
+
import * as p2 from "@clack/prompts";
|
|
1488
|
+
import pc6 from "picocolors";
|
|
540
1489
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
541
1490
|
import { execSync } from "child_process";
|
|
542
|
-
import { resolve as resolve2, join as
|
|
1491
|
+
import { resolve as resolve2, join as join4 } from "path";
|
|
543
1492
|
|
|
544
1493
|
// src/templates/agentsMdRegion.ts
|
|
545
1494
|
var AGENTS_MANAGED_VERSION = 10;
|
|
@@ -1709,7 +2658,7 @@ npx bstage docs # SLOT_CATALOG_V2.md
|
|
|
1709
2658
|
var PHASES = ["dev", "qa", "real", "sandbox"];
|
|
1710
2659
|
var TARGETS = ["user", "admin"];
|
|
1711
2660
|
async function initCommand(options) {
|
|
1712
|
-
|
|
2661
|
+
p2.intro(`${pc6.cyan(pc6.bold("bstage init"))} ${pc6.dim("\u2014 b.stage Template Project Setup")}`);
|
|
1713
2662
|
let space;
|
|
1714
2663
|
let templateName;
|
|
1715
2664
|
let packageManager;
|
|
@@ -1721,44 +2670,44 @@ async function initCommand(options) {
|
|
|
1721
2670
|
if (options.yes) {
|
|
1722
2671
|
const targetOpt = options.target ?? "user";
|
|
1723
2672
|
if (!TARGETS.includes(targetOpt)) {
|
|
1724
|
-
|
|
1725
|
-
|
|
2673
|
+
p2.log.error(`--target \uAC12\uC740 ${TARGETS.join(" | ")} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
|
|
2674
|
+
p2.outro(pc6.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
1726
2675
|
process.exit(1);
|
|
1727
2676
|
}
|
|
1728
2677
|
target = targetOpt;
|
|
1729
2678
|
if (!options.space) {
|
|
1730
|
-
|
|
1731
|
-
|
|
2679
|
+
p2.log.error("--yes \uBAA8\uB4DC\uC5D0\uC11C\uB294 --space\uAC00 \uD544\uC218\uC785\uB2C8\uB2E4.");
|
|
2680
|
+
p2.outro(pc6.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
1732
2681
|
process.exit(1);
|
|
1733
2682
|
}
|
|
1734
2683
|
const slugPattern = /^[a-zA-Z][a-zA-Z0-9-]*$/;
|
|
1735
2684
|
if (!slugPattern.test(options.space)) {
|
|
1736
|
-
|
|
1737
|
-
`${
|
|
2685
|
+
p2.log.error(
|
|
2686
|
+
`${pc6.red("--space")} \uAC12\uC740 \uC601\uBB38\uC790\uB85C \uC2DC\uC791\uD574\uC57C \uD558\uBA70, \uC601\uBB38, \uC22B\uC790, \uD558\uC774\uD508\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
|
|
1738
2687
|
);
|
|
1739
|
-
|
|
2688
|
+
p2.outro(pc6.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
1740
2689
|
process.exit(1);
|
|
1741
2690
|
}
|
|
1742
2691
|
space = options.space;
|
|
1743
2692
|
const templateOpt = resolveTemplateName(space, options.template);
|
|
1744
2693
|
if (!ELEMENT_NAME_RE2.test(templateOpt)) {
|
|
1745
|
-
|
|
1746
|
-
`${
|
|
2694
|
+
p2.log.error(
|
|
2695
|
+
`${pc6.red("--template")} \uAC12\uC740 Custom Element \uC2A4\uD399\uC744 \uB530\uB77C\uC57C \uD569\uB2C8\uB2E4: \uC18C\uBB38\uC790 \uC2DC\uC791, \uD558\uC774\uD508 1\uAC1C \uC774\uC0C1, \uC18C\uBB38\uC790\xB7\uC22B\uC790\xB7\uD558\uC774\uD508\uB9CC.`
|
|
1747
2696
|
);
|
|
1748
|
-
|
|
2697
|
+
p2.outro(pc6.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
1749
2698
|
process.exit(1);
|
|
1750
2699
|
}
|
|
1751
2700
|
templateName = templateOpt;
|
|
1752
2701
|
packageManager = options.pm ?? "npm";
|
|
1753
2702
|
const phaseOpt = options.phase ?? "sandbox";
|
|
1754
2703
|
if (!PHASES.includes(phaseOpt)) {
|
|
1755
|
-
|
|
1756
|
-
|
|
2704
|
+
p2.log.error(`--phase \uAC12\uC740 ${PHASES.join(" | ")} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
|
|
2705
|
+
p2.outro(pc6.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
1757
2706
|
process.exit(1);
|
|
1758
2707
|
}
|
|
1759
2708
|
phase = phaseOpt;
|
|
1760
2709
|
} else {
|
|
1761
|
-
target = await
|
|
2710
|
+
target = await p2.select({
|
|
1762
2711
|
message: "\uCCAB \uBC88\uC9F8 \uD15C\uD50C\uB9BF\uC740 \uC5B4\uB290 \uD50C\uB7AB\uD3FC\uC6A9\uC778\uAC00\uC694? (\uB808\uD3EC\uC5D0 \uB450 \uC885\uB958\uB97C \uC11E\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4)",
|
|
1763
2712
|
options: [
|
|
1764
2713
|
{ value: "user", label: "user (b.stage \uC720\uC800 \uD50C\uB7AB\uD3FC)" },
|
|
@@ -1766,13 +2715,13 @@ async function initCommand(options) {
|
|
|
1766
2715
|
],
|
|
1767
2716
|
initialValue: options.target === "admin" ? "admin" : "user"
|
|
1768
2717
|
});
|
|
1769
|
-
if (
|
|
1770
|
-
space = await
|
|
2718
|
+
if (p2.isCancel(target)) return cancelled();
|
|
2719
|
+
space = await p2.text({
|
|
1771
2720
|
message: "\uD15C\uD50C\uB9BF\uC744 \uC801\uC6A9\uD560 Space ID\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
|
|
1772
2721
|
validate: validateSlug("Space ID\uB294 \uD544\uC218\uC785\uB2C8\uB2E4.")
|
|
1773
2722
|
});
|
|
1774
|
-
if (
|
|
1775
|
-
phase = await
|
|
2723
|
+
if (p2.isCancel(space)) return cancelled();
|
|
2724
|
+
phase = await p2.select({
|
|
1776
2725
|
message: "\uD15C\uD50C\uB9BF\uC744 \uBC30\uD3EC\uD560 \uD658\uACBD\uC744 \uC120\uD0DD\uD574 \uC8FC\uC138\uC694.",
|
|
1777
2726
|
options: [
|
|
1778
2727
|
{ value: "sandbox", label: "sandbox", hint: "\uAE30\uBCF8" },
|
|
@@ -1782,14 +2731,14 @@ async function initCommand(options) {
|
|
|
1782
2731
|
],
|
|
1783
2732
|
initialValue: "sandbox"
|
|
1784
2733
|
});
|
|
1785
|
-
if (
|
|
1786
|
-
templateName = await
|
|
1787
|
-
message: `\uCCAB \uBC88\uC9F8 \uD15C\uD50C\uB9BF \uC774\uB984\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694. ${
|
|
2734
|
+
if (p2.isCancel(phase)) return cancelled();
|
|
2735
|
+
templateName = await p2.text({
|
|
2736
|
+
message: `\uCCAB \uBC88\uC9F8 \uD15C\uD50C\uB9BF \uC774\uB984\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694. ${pc6.dim(`(\uC608: ${space}-hello \u2014 \uD558\uC774\uD508 \uD544\uC218)`)}`,
|
|
1788
2737
|
placeholder: resolveTemplateName(space, void 0),
|
|
1789
2738
|
validate: validateElementName
|
|
1790
2739
|
});
|
|
1791
|
-
if (
|
|
1792
|
-
packageManager = await
|
|
2740
|
+
if (p2.isCancel(templateName)) return cancelled();
|
|
2741
|
+
packageManager = await p2.select({
|
|
1793
2742
|
message: "\uC0AC\uC6A9\uD560 \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800\uB97C \uC120\uD0DD\uD574 \uC8FC\uC138\uC694.",
|
|
1794
2743
|
options: [
|
|
1795
2744
|
{ value: "npm", label: "npm" },
|
|
@@ -1797,22 +2746,22 @@ async function initCommand(options) {
|
|
|
1797
2746
|
],
|
|
1798
2747
|
initialValue: "npm"
|
|
1799
2748
|
});
|
|
1800
|
-
if (
|
|
1801
|
-
const configureAuth = await
|
|
2749
|
+
if (p2.isCancel(packageManager)) return cancelled();
|
|
2750
|
+
const configureAuth = await p2.confirm({
|
|
1802
2751
|
message: "b.stage \uD30C\uD2B8\uB108\uC2A4\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 API \uD0A4\uAC00 \uC788\uC73C\uC2E0\uAC00\uC694? (\uC5C6\uC5B4\uB3C4 \uC2DC\uC791\uD560 \uC218 \uC788\uC5B4\uC694)",
|
|
1803
2752
|
initialValue: false
|
|
1804
2753
|
});
|
|
1805
|
-
if (
|
|
2754
|
+
if (p2.isCancel(configureAuth)) return cancelled();
|
|
1806
2755
|
if (configureAuth) {
|
|
1807
|
-
|
|
1808
|
-
appId = await
|
|
2756
|
+
p2.log.info(pc6.dim("API \uC778\uC99D \uC124\uC815 (\uC120\uD0DD)"));
|
|
2757
|
+
appId = await p2.text({
|
|
1809
2758
|
message: "\uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 APP-ID\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694."
|
|
1810
2759
|
}) ?? "";
|
|
1811
|
-
if (
|
|
1812
|
-
appSecret = await
|
|
2760
|
+
if (p2.isCancel(appId)) return cancelled();
|
|
2761
|
+
appSecret = await p2.text({
|
|
1813
2762
|
message: "\uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 Secret \uAC12\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."
|
|
1814
2763
|
}) ?? "";
|
|
1815
|
-
if (
|
|
2764
|
+
if (p2.isCancel(appSecret)) return cancelled();
|
|
1816
2765
|
warnIfCredentialsSwapped(appId, appSecret);
|
|
1817
2766
|
tenantId = space;
|
|
1818
2767
|
}
|
|
@@ -1820,13 +2769,13 @@ async function initCommand(options) {
|
|
|
1820
2769
|
const projectName = `${space}-custom-templates-${phase}`;
|
|
1821
2770
|
const projectDir = resolve2(process.cwd(), projectName);
|
|
1822
2771
|
if (existsSync(projectDir)) {
|
|
1823
|
-
|
|
1824
|
-
`${
|
|
2772
|
+
p2.log.error(
|
|
2773
|
+
`${pc6.red(projectDir)} \uB514\uB809\uD1A0\uB9AC\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uD574\uB2F9 \uB514\uB809\uD1A0\uB9AC\uB97C \uC815\uB9AC\uD55C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.`
|
|
1825
2774
|
);
|
|
1826
|
-
|
|
2775
|
+
p2.outro(pc6.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
1827
2776
|
process.exit(1);
|
|
1828
2777
|
}
|
|
1829
|
-
const s =
|
|
2778
|
+
const s = p2.spinner();
|
|
1830
2779
|
s.start("SDK \uCD5C\uC2E0 \uBC84\uC804 \uC870\uD68C \uC911...");
|
|
1831
2780
|
let sdkVersions;
|
|
1832
2781
|
try {
|
|
@@ -1839,12 +2788,12 @@ async function initCommand(options) {
|
|
|
1839
2788
|
sdkVersions = { cli, core, react: react2, design };
|
|
1840
2789
|
s.stop(`SDK \uBC84\uC804 \uD655\uC815 \u2014 cli@${cli}, core@${core}, react@${react2}, design@${design}`);
|
|
1841
2790
|
} catch (err) {
|
|
1842
|
-
s.stop(
|
|
1843
|
-
|
|
2791
|
+
s.stop(pc6.red("SDK \uCD5C\uC2E0 \uBC84\uC804 \uC870\uD68C \uC2E4\uD328"));
|
|
2792
|
+
p2.log.error(
|
|
1844
2793
|
(err instanceof Error ? err.message : String(err)) + `
|
|
1845
|
-
${
|
|
2794
|
+
${pc6.dim("\uB124\uD2B8\uC6CC\uD06C \uC5F0\uACB0\uACFC registry.npmjs.org \uC811\uADFC\uC774 \uAC00\uB2A5\uD55C\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.")}`
|
|
1846
2795
|
);
|
|
1847
|
-
|
|
2796
|
+
p2.outro(pc6.red("\uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
1848
2797
|
process.exit(1);
|
|
1849
2798
|
}
|
|
1850
2799
|
s.start("\uD504\uB85C\uC81D\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC911...");
|
|
@@ -1858,7 +2807,7 @@ async function initCommand(options) {
|
|
|
1858
2807
|
...BSTAGE_SKILLS.map((s2) => `.claude/skills/${s2.name}`)
|
|
1859
2808
|
];
|
|
1860
2809
|
for (const dir of dirs) {
|
|
1861
|
-
mkdirSync(
|
|
2810
|
+
mkdirSync(join4(projectDir, dir), { recursive: true });
|
|
1862
2811
|
}
|
|
1863
2812
|
const envFiles = [
|
|
1864
2813
|
// .env(실제 값)는 커밋되지 않음(.gitignore). .env.example은 커밋되어 팀원 세팅 안내.
|
|
@@ -1912,7 +2861,7 @@ async function initCommand(options) {
|
|
|
1912
2861
|
)
|
|
1913
2862
|
];
|
|
1914
2863
|
for (const [filePath, content] of files) {
|
|
1915
|
-
writeFileSync(
|
|
2864
|
+
writeFileSync(join4(projectDir, filePath), content, "utf-8");
|
|
1916
2865
|
}
|
|
1917
2866
|
s.stop("\uD504\uB85C\uC81D\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC644\uB8CC");
|
|
1918
2867
|
s.start("Git \uCD08\uAE30\uD654 \uC911...");
|
|
@@ -1925,7 +2874,7 @@ async function initCommand(options) {
|
|
|
1925
2874
|
});
|
|
1926
2875
|
s.stop("Git \uCD08\uAE30\uD654 \uC644\uB8CC");
|
|
1927
2876
|
} catch {
|
|
1928
|
-
s.stop(
|
|
2877
|
+
s.stop(pc6.yellow("Git \uCD08\uAE30\uD654 \uAC74\uB108\uB700 (git\uC774 \uC124\uCE58\uB418\uC9C0 \uC54A\uC558\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4)"));
|
|
1929
2878
|
}
|
|
1930
2879
|
s.start(`\uC758\uC874\uC131 \uC124\uCE58 \uC911... (${packageManager} install)`);
|
|
1931
2880
|
try {
|
|
@@ -1933,16 +2882,16 @@ async function initCommand(options) {
|
|
|
1933
2882
|
execSync(installCmd, { cwd: projectDir, stdio: "ignore", env: { ...process.env } });
|
|
1934
2883
|
s.stop("\uC758\uC874\uC131 \uC124\uCE58 \uC644\uB8CC");
|
|
1935
2884
|
} catch {
|
|
1936
|
-
s.stop(
|
|
2885
|
+
s.stop(pc6.yellow("\uC758\uC874\uC131 \uC124\uCE58 \uC2E4\uD328 \u2014 \uC9C1\uC811 \uC124\uCE58\uD574 \uC8FC\uC138\uC694"));
|
|
1937
2886
|
}
|
|
1938
2887
|
const runCmd = packageManager === "pnpm" ? "pnpm dev" : "npm run dev";
|
|
1939
2888
|
const label = target === "admin" ? "\uC5B4\uB4DC\uBBFC \uD15C\uD50C\uB9BF \uD504\uB85C\uC81D\uD2B8" : "\uD504\uB85C\uC81D\uD2B8";
|
|
1940
|
-
|
|
2889
|
+
p2.outro(`${pc6.green(`${label}\uAC00 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4!`)}
|
|
1941
2890
|
|
|
1942
2891
|
\uB2E4\uC74C \uB2E8\uACC4:
|
|
1943
|
-
${
|
|
1944
|
-
${
|
|
1945
|
-
${
|
|
2892
|
+
${pc6.cyan(`cd ${projectName}`)}
|
|
2893
|
+
${pc6.cyan(runCmd)}
|
|
2894
|
+
${pc6.dim("# \uBE0C\uB77C\uC6B0\uC800: http://localhost:5173")}`);
|
|
1946
2895
|
}
|
|
1947
2896
|
async function fetchLatestVersion(packageName) {
|
|
1948
2897
|
const res = await fetch(`https://registry.npmjs.org/${packageName}`, {
|
|
@@ -1979,21 +2928,21 @@ var APP_ID_PREFIX2 = "bsa_";
|
|
|
1979
2928
|
var APP_SECRET_PREFIX2 = "bsp_";
|
|
1980
2929
|
function warnIfCredentialsSwapped(appId, appSecret) {
|
|
1981
2930
|
if (!appId.startsWith(APP_SECRET_PREFIX2) && !appSecret.startsWith(APP_ID_PREFIX2)) return;
|
|
1982
|
-
|
|
2931
|
+
p2.log.warn(
|
|
1983
2932
|
`APP-ID\uC640 Secret\uC774 \uC11C\uB85C \uBC14\uB010 \uAC83 \uAC19\uC2B5\uB2C8\uB2E4.
|
|
1984
2933
|
APP-ID\uB294 "${APP_ID_PREFIX2}", Secret\uC740 "${APP_SECRET_PREFIX2}"\uB85C \uC2DC\uC791\uD569\uB2C8\uB2E4.
|
|
1985
2934
|
\uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC740 \uAC12\uC744 \uB2E4\uC2DC \uD655\uC778\uD574 \uC8FC\uC138\uC694.`
|
|
1986
2935
|
);
|
|
1987
2936
|
}
|
|
1988
2937
|
function cancelled() {
|
|
1989
|
-
|
|
2938
|
+
p2.outro(pc6.red("\uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
1990
2939
|
process.exit(0);
|
|
1991
2940
|
}
|
|
1992
2941
|
|
|
1993
2942
|
// src/commands/i18n.ts
|
|
1994
2943
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1995
|
-
import { dirname, join as
|
|
1996
|
-
import
|
|
2944
|
+
import { dirname as dirname3, join as join5, resolve as resolve3 } from "path";
|
|
2945
|
+
import pc7 from "picocolors";
|
|
1997
2946
|
function latestKey(phase, tier, target) {
|
|
1998
2947
|
const segments = tier === DEFAULT_TRANSLATION_TIER ? [TRANSLATION_CDN_BASE_KEY, phase, target] : [TRANSLATION_CDN_BASE_KEY, phase, target, tier];
|
|
1999
2948
|
return [...segments, "latest.json"].join("/");
|
|
@@ -2040,87 +2989,87 @@ async function i18nPullCommand(options) {
|
|
|
2040
2989
|
const tier = options.tier || DEFAULT_TRANSLATION_TIER;
|
|
2041
2990
|
const target = options.target || "user";
|
|
2042
2991
|
const latestUrl = `${TRANSLATION_CDN_ORIGIN}/${latestKey(options.phase, tier, target)}?date=${cacheStamp(/* @__PURE__ */ new Date())}`;
|
|
2043
|
-
console.log(
|
|
2992
|
+
console.log(pc7.cyan(`[bstage] \uBC88\uC5ED pull: ${options.phase}/${target} (tier=${tier})`));
|
|
2044
2993
|
let latest;
|
|
2045
2994
|
try {
|
|
2046
2995
|
latest = await fetchJson(latestUrl);
|
|
2047
2996
|
} catch (err) {
|
|
2048
2997
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2049
|
-
console.error(
|
|
2998
|
+
console.error(pc7.red(`[bstage] latest.json \uC870\uD68C \uC2E4\uD328: ${msg}
|
|
2050
2999
|
${latestUrl}`));
|
|
2051
3000
|
process.exit(1);
|
|
2052
3001
|
}
|
|
2053
3002
|
const versionBase = `${TRANSLATION_CDN_ORIGIN}/${latest.cdnPath.replace(TRANSLATION_CDN_BUCKET_PREFIX, "")}`;
|
|
2054
|
-
console.log(
|
|
2055
|
-
const cacheDir =
|
|
3003
|
+
console.log(pc7.dim(` version: ${latest.version}`));
|
|
3004
|
+
const cacheDir = join5(root, ".bstage", "i18n", target);
|
|
2056
3005
|
mkdirSync2(cacheDir, { recursive: true });
|
|
2057
3006
|
let refDict = null;
|
|
2058
3007
|
const fetched = [];
|
|
2059
3008
|
for (const locale of TRANSLATION_LANGUAGES) {
|
|
2060
3009
|
try {
|
|
2061
3010
|
const dict = await fetchJson(`${versionBase}/${locale}/translation.json`);
|
|
2062
|
-
writeFileSync2(
|
|
3011
|
+
writeFileSync2(join5(cacheDir, `${locale}.json`), JSON.stringify(dict, null, 2));
|
|
2063
3012
|
fetched.push(locale);
|
|
2064
3013
|
if (locale === options.ref) refDict = dict;
|
|
2065
|
-
console.log(
|
|
3014
|
+
console.log(pc7.green(` \u2713 ${locale} (${Object.keys(dict).length})`));
|
|
2066
3015
|
} catch (err) {
|
|
2067
3016
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2068
|
-
console.warn(
|
|
3017
|
+
console.warn(pc7.yellow(` - ${locale}: ${msg} \u2014 \uAC74\uB108\uB700`));
|
|
2069
3018
|
}
|
|
2070
3019
|
}
|
|
2071
3020
|
if (fetched.length === 0) {
|
|
2072
|
-
console.error(
|
|
3021
|
+
console.error(pc7.red("[bstage] \uBC1B\uC740 \uBC88\uC5ED\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
2073
3022
|
process.exit(1);
|
|
2074
3023
|
}
|
|
2075
3024
|
if (!refDict) {
|
|
2076
3025
|
console.warn(
|
|
2077
|
-
|
|
3026
|
+
pc7.yellow(`[bstage] ref \uB85C\uCF00\uC77C(${options.ref}) \uC0AC\uC804\uC774 \uC5C6\uC5B4 \uD0C0\uC785 \uC0DD\uC131\uC744 \uAC74\uB108\uB701\uB2C8\uB2E4.`)
|
|
2078
3027
|
);
|
|
2079
3028
|
return;
|
|
2080
3029
|
}
|
|
2081
3030
|
const outPath = resolve3(root, options.out);
|
|
2082
|
-
mkdirSync2(
|
|
3031
|
+
mkdirSync2(dirname3(outPath), { recursive: true });
|
|
2083
3032
|
writeFileSync2(outPath, generateTypeModule(refDict, { version: latest.version, ref: options.ref }));
|
|
2084
|
-
console.log(
|
|
3033
|
+
console.log(pc7.green(`[bstage] \uD0C0\uC785 \uC0DD\uC131: ${options.out} (${Object.keys(refDict).length} \uD0A4)`));
|
|
2085
3034
|
console.log(
|
|
2086
|
-
|
|
3035
|
+
pc7.dim(
|
|
2087
3036
|
` \uCE90\uC2DC: .bstage/i18n/${target}/*.json \u2014 \uD0C0\uC785 \uC0DD\uC131 \uC785\uB825 + \uC6D0\uBB38 \uAC80\uC0C9\uC6A9(\uB7F0\uD0C0\uC784\uC740 CDN\uC5D0\uC11C \uC9C1\uC811 \uBC1B\uB294\uB2E4)`
|
|
2088
3037
|
)
|
|
2089
3038
|
);
|
|
2090
|
-
console.log(
|
|
3039
|
+
console.log(pc7.dim(" \uC0DD\uC131 \uD30C\uC77C\uC744 \uD55C \uBC88 import\uD558\uBA74 useBstageTranslations().t() \uD0A4\uAC00 \uC881\uD600\uC9D1\uB2C8\uB2E4."));
|
|
2091
3040
|
}
|
|
2092
3041
|
|
|
2093
3042
|
// src/commands/doctor.ts
|
|
2094
3043
|
import { existsSync as existsSync3 } from "fs";
|
|
2095
|
-
import { readFile as
|
|
2096
|
-
import { dirname as
|
|
3044
|
+
import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
|
|
3045
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
2097
3046
|
import { fileURLToPath } from "url";
|
|
2098
|
-
import
|
|
3047
|
+
import pc9 from "picocolors";
|
|
2099
3048
|
|
|
2100
3049
|
// src/commands/skills.ts
|
|
2101
3050
|
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync3 } from "fs";
|
|
2102
|
-
import { join as
|
|
2103
|
-
import
|
|
3051
|
+
import { join as join6 } from "path";
|
|
3052
|
+
import pc8 from "picocolors";
|
|
2104
3053
|
async function skillsInstallCommand(options) {
|
|
2105
3054
|
const root = process.cwd();
|
|
2106
3055
|
const baseDir = options.dir || ".claude/skills";
|
|
2107
3056
|
const target = resolveProjectTarget(root);
|
|
2108
|
-
console.log(
|
|
3057
|
+
console.log(pc8.cyan(`[bstage] \uC2A4\uD0AC \uC124\uCE58: ${baseDir}/`) + pc8.dim(` (\uB514\uC790\uC778 \uD0C0\uAE43: ${target})`));
|
|
2109
3058
|
console.log(
|
|
2110
|
-
|
|
3059
|
+
pc8.dim(
|
|
2111
3060
|
" SDK\uAC00 \uC18C\uC720\uD558\uB294 \uC790\uC0B0\uC785\uB2C8\uB2E4. \uAC19\uC740 \uC774\uB984\uC758 \uAE30\uC874 SKILL.md\uB294 \uB36E\uC5B4\uC4F0\uC5EC \uCD5C\uC2E0\uC73C\uB85C \uB3D9\uAE30\uD654\uB429\uB2C8\uB2E4."
|
|
2112
3061
|
)
|
|
2113
3062
|
);
|
|
2114
3063
|
for (const skill of BSTAGE_SKILLS) {
|
|
2115
|
-
const dir =
|
|
3064
|
+
const dir = join6(root, baseDir, skill.name);
|
|
2116
3065
|
mkdirSync3(dir, { recursive: true });
|
|
2117
|
-
const file =
|
|
3066
|
+
const file = join6(dir, "SKILL.md");
|
|
2118
3067
|
const action = existsSync2(file) ? "\uAC31\uC2E0" : "\uC0DD\uC131";
|
|
2119
3068
|
writeFileSync3(file, skill.content(target), "utf-8");
|
|
2120
|
-
console.log(
|
|
3069
|
+
console.log(pc8.green(` \u2713 ${skill.name} (${action})`));
|
|
2121
3070
|
}
|
|
2122
3071
|
console.log(
|
|
2123
|
-
|
|
3072
|
+
pc8.dim(
|
|
2124
3073
|
` ${BSTAGE_SKILLS.length}\uAC1C \uC2A4\uD0AC \uC124\uCE58 \uC644\uB8CC. Claude Code\uAC00 ${baseDir}/ \uC758 \uC2A4\uD0AC\uC744 \uC790\uB3D9 \uC778\uC2DD\uD569\uB2C8\uB2E4.`
|
|
2125
3074
|
)
|
|
2126
3075
|
);
|
|
@@ -2128,16 +3077,16 @@ async function skillsInstallCommand(options) {
|
|
|
2128
3077
|
installGuardHook(root);
|
|
2129
3078
|
}
|
|
2130
3079
|
function syncAgentsMd(root, target) {
|
|
2131
|
-
const file =
|
|
3080
|
+
const file = join6(root, "AGENTS.md");
|
|
2132
3081
|
if (!existsSync2(file)) {
|
|
2133
|
-
console.log(
|
|
3082
|
+
console.log(pc8.dim(" \xB7 AGENTS.md \uC5C6\uC74C \u2014 `bstage init` \uB610\uB294 \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC73C\uB85C \uC0DD\uC131\uD558\uC138\uC694."));
|
|
2134
3083
|
return;
|
|
2135
3084
|
}
|
|
2136
3085
|
const content = readFileSync(file, "utf-8");
|
|
2137
3086
|
const status = agentsMdStatus(content);
|
|
2138
3087
|
if (status === "legacy") {
|
|
2139
3088
|
console.log(
|
|
2140
|
-
|
|
3089
|
+
pc8.yellow(" ! AGENTS.md\uAC00 \uB808\uAC70\uC2DC \uD3EC\uB9F7\uC785\uB2C8\uB2E4(\uAD00\uB9AC \uC601\uC5ED \uB9C8\uCEE4 \uC5C6\uC74C).") + pc8.dim(" `bstage-migrate` \uC2A4\uD0AC\uB85C reconcile\uD558\uC138\uC694 \u2014 skills install\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")
|
|
2141
3090
|
);
|
|
2142
3091
|
return;
|
|
2143
3092
|
}
|
|
@@ -2146,20 +3095,20 @@ function syncAgentsMd(root, target) {
|
|
|
2146
3095
|
const space = parsed.space ?? projectName.split("-custom-templates-")[0] ?? projectName;
|
|
2147
3096
|
const swapped = swapManagedBlock(content, renderManagedBlock({ space, projectName, target }));
|
|
2148
3097
|
if (swapped === null) {
|
|
2149
|
-
console.log(
|
|
3098
|
+
console.log(pc8.dim(" \xB7 AGENTS.md \uAD00\uB9AC \uC601\uC5ED\uC744 \uCC3E\uC9C0 \uBABB\uD574 \uAC74\uB108\uB701\uB2C8\uB2E4."));
|
|
2150
3099
|
return;
|
|
2151
3100
|
}
|
|
2152
3101
|
if (swapped === content) {
|
|
2153
|
-
console.log(
|
|
3102
|
+
console.log(pc8.green(" \u2713 AGENTS.md (\uAD00\uB9AC \uC601\uC5ED \uCD5C\uC2E0)"));
|
|
2154
3103
|
return;
|
|
2155
3104
|
}
|
|
2156
3105
|
writeFileSync3(file, swapped, "utf-8");
|
|
2157
|
-
console.log(
|
|
3106
|
+
console.log(pc8.green(" \u2713 AGENTS.md (\uAD00\uB9AC \uC601\uC5ED \uAC31\uC2E0, \uC790\uC720 \uC601\uC5ED \uBCF4\uC874)"));
|
|
2158
3107
|
}
|
|
2159
3108
|
function installGuardHook(root) {
|
|
2160
|
-
const pkgPath =
|
|
3109
|
+
const pkgPath = join6(root, "package.json");
|
|
2161
3110
|
if (!existsSync2(pkgPath)) {
|
|
2162
|
-
console.log(
|
|
3111
|
+
console.log(pc8.dim(" \xB7 package.json \uC5C6\uC74C \u2014 pre-commit \uAC00\uB4DC \uC124\uCE58\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4."));
|
|
2163
3112
|
return;
|
|
2164
3113
|
}
|
|
2165
3114
|
let pkgChanged = false;
|
|
@@ -2182,39 +3131,39 @@ function installGuardHook(root) {
|
|
|
2182
3131
|
writeFileSync3(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
2183
3132
|
}
|
|
2184
3133
|
} catch {
|
|
2185
|
-
console.log(
|
|
3134
|
+
console.log(pc8.yellow(" ! package.json \uD30C\uC2F1 \uC2E4\uD328 \u2014 pre-commit \uAC00\uB4DC \uC124\uCE58\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4."));
|
|
2186
3135
|
return;
|
|
2187
3136
|
}
|
|
2188
|
-
const huskyDir =
|
|
3137
|
+
const huskyDir = join6(root, ".husky");
|
|
2189
3138
|
mkdirSync3(huskyDir, { recursive: true });
|
|
2190
|
-
const scriptFile =
|
|
3139
|
+
const scriptFile = join6(huskyDir, "check-secrets.mjs");
|
|
2191
3140
|
const scriptExisted = existsSync2(scriptFile);
|
|
2192
3141
|
writeFileSync3(scriptFile, checkSecretsScript(), "utf-8");
|
|
2193
|
-
console.log(
|
|
2194
|
-
const hookFile =
|
|
3142
|
+
console.log(pc8.green(` \u2713 .husky/check-secrets.mjs (${scriptExisted ? "\uAC31\uC2E0" : "\uC0DD\uC131"})`));
|
|
3143
|
+
const hookFile = join6(huskyDir, "pre-commit");
|
|
2195
3144
|
const body = huskyPreCommit();
|
|
2196
3145
|
if (!existsSync2(hookFile)) {
|
|
2197
3146
|
writeFileSync3(hookFile, body, "utf-8");
|
|
2198
|
-
console.log(
|
|
3147
|
+
console.log(pc8.green(" \u2713 .husky/pre-commit (\uC0DD\uC131)"));
|
|
2199
3148
|
} else {
|
|
2200
3149
|
const cur = readFileSync(hookFile, "utf-8");
|
|
2201
3150
|
if (cur.includes("check-secrets.mjs")) {
|
|
2202
|
-
console.log(
|
|
3151
|
+
console.log(pc8.green(" \u2713 .husky/pre-commit (\uAC00\uB4DC \uC874\uC7AC)"));
|
|
2203
3152
|
} else {
|
|
2204
3153
|
const sep = cur.endsWith("\n") ? "" : "\n";
|
|
2205
3154
|
writeFileSync3(hookFile, cur + sep + body, "utf-8");
|
|
2206
|
-
console.log(
|
|
3155
|
+
console.log(pc8.green(" \u2713 .husky/pre-commit (\uAC00\uB4DC \uB77C\uC778 \uCD94\uAC00)"));
|
|
2207
3156
|
}
|
|
2208
3157
|
}
|
|
2209
3158
|
if (pkgChanged) {
|
|
2210
3159
|
console.log(
|
|
2211
|
-
|
|
3160
|
+
pc8.dim(" \xB7 husky\uB97C package.json\uC5D0 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4 \u2014 install \uD6C4 pre-commit \uD6C5\uC774 \uD65C\uC131\uD654\uB429\uB2C8\uB2E4.")
|
|
2212
3161
|
);
|
|
2213
3162
|
}
|
|
2214
3163
|
}
|
|
2215
3164
|
function readPkgName(root) {
|
|
2216
3165
|
try {
|
|
2217
|
-
return JSON.parse(readFileSync(
|
|
3166
|
+
return JSON.parse(readFileSync(join6(root, "package.json"), "utf-8")).name ?? null;
|
|
2218
3167
|
} catch {
|
|
2219
3168
|
return null;
|
|
2220
3169
|
}
|
|
@@ -2255,23 +3204,23 @@ var EXPECTED_FILES = [
|
|
|
2255
3204
|
];
|
|
2256
3205
|
async function doctorCommand(options) {
|
|
2257
3206
|
const root = process.cwd();
|
|
2258
|
-
const
|
|
3207
|
+
const report2 = await diagnose(root);
|
|
2259
3208
|
if (options.json) {
|
|
2260
|
-
console.log(JSON.stringify(
|
|
3209
|
+
console.log(JSON.stringify(report2, null, 2));
|
|
2261
3210
|
return;
|
|
2262
3211
|
}
|
|
2263
|
-
printReport(
|
|
2264
|
-
await autoSyncSkills(
|
|
3212
|
+
printReport(report2);
|
|
3213
|
+
await autoSyncSkills(report2);
|
|
2265
3214
|
}
|
|
2266
|
-
async function autoSyncSkills(
|
|
2267
|
-
if (!
|
|
2268
|
-
if (!
|
|
3215
|
+
async function autoSyncSkills(report2) {
|
|
3216
|
+
if (!report2.isBstageProject) return;
|
|
3217
|
+
if (!report2.skills.some((s) => s.status !== "ok")) return;
|
|
2269
3218
|
await skillsInstallCommand({ dir: ".claude/skills" });
|
|
2270
3219
|
}
|
|
2271
3220
|
async function diagnose(root) {
|
|
2272
3221
|
let pkgJson = null;
|
|
2273
3222
|
try {
|
|
2274
|
-
pkgJson = JSON.parse(await
|
|
3223
|
+
pkgJson = JSON.parse(await readFile4(join7(root, "package.json"), "utf-8"));
|
|
2275
3224
|
} catch {
|
|
2276
3225
|
}
|
|
2277
3226
|
const deps = {
|
|
@@ -2280,7 +3229,7 @@ async function diagnose(root) {
|
|
|
2280
3229
|
...pkgJson?.peerDependencies,
|
|
2281
3230
|
...pkgJson?.optionalDependencies
|
|
2282
3231
|
};
|
|
2283
|
-
const isBstageProject = SDK_PACKAGES.some((
|
|
3232
|
+
const isBstageProject = SDK_PACKAGES.some((p4) => p4 in deps);
|
|
2284
3233
|
const installed = /* @__PURE__ */ new Map();
|
|
2285
3234
|
for (const pkg of SDK_PACKAGES) {
|
|
2286
3235
|
installed.set(pkg, await readInstalledVersion(root, pkg) ?? cleanRange(deps[pkg]));
|
|
@@ -2309,9 +3258,9 @@ async function diagnose(root) {
|
|
|
2309
3258
|
const files = EXPECTED_FILES.map((f) => ({
|
|
2310
3259
|
path: f.path,
|
|
2311
3260
|
note: f.note,
|
|
2312
|
-
present: existsSync3(
|
|
3261
|
+
present: existsSync3(join7(root, f.path))
|
|
2313
3262
|
}));
|
|
2314
|
-
const isPnpmProject = existsSync3(
|
|
3263
|
+
const isPnpmProject = existsSync3(join7(root, "pnpm-lock.yaml"));
|
|
2315
3264
|
let pnpmWorkspace2 = {
|
|
2316
3265
|
relevant: false,
|
|
2317
3266
|
allowBuildsDeclared: false,
|
|
@@ -2321,7 +3270,7 @@ async function diagnose(root) {
|
|
|
2321
3270
|
if (isPnpmProject) {
|
|
2322
3271
|
let yaml = null;
|
|
2323
3272
|
try {
|
|
2324
|
-
yaml = await
|
|
3273
|
+
yaml = await readFile4(join7(root, "pnpm-workspace.yaml"), "utf-8");
|
|
2325
3274
|
} catch {
|
|
2326
3275
|
}
|
|
2327
3276
|
pnpmWorkspace2 = {
|
|
@@ -2367,7 +3316,7 @@ async function diagnose(root) {
|
|
|
2367
3316
|
};
|
|
2368
3317
|
}
|
|
2369
3318
|
async function diagnoseLayout(root) {
|
|
2370
|
-
const legacyRoot =
|
|
3319
|
+
const legacyRoot = join7(root, "src", "templates");
|
|
2371
3320
|
let legacyTemplates = [];
|
|
2372
3321
|
let legacyPresent = false;
|
|
2373
3322
|
try {
|
|
@@ -2379,8 +3328,8 @@ async function diagnoseLayout(root) {
|
|
|
2379
3328
|
return {
|
|
2380
3329
|
legacyPresent,
|
|
2381
3330
|
legacyTemplates,
|
|
2382
|
-
pagesPresent: existsSync3(
|
|
2383
|
-
slotsPresent: existsSync3(
|
|
3331
|
+
pagesPresent: existsSync3(join7(root, "src", "pages")),
|
|
3332
|
+
slotsPresent: existsSync3(join7(root, "src", "slots"))
|
|
2384
3333
|
};
|
|
2385
3334
|
}
|
|
2386
3335
|
async function diagnoseSkills(root) {
|
|
@@ -2389,7 +3338,7 @@ async function diagnoseSkills(root) {
|
|
|
2389
3338
|
BSTAGE_SKILLS.map(async (skill) => {
|
|
2390
3339
|
let installed;
|
|
2391
3340
|
try {
|
|
2392
|
-
installed = await
|
|
3341
|
+
installed = await readFile4(join7(root, ".claude", "skills", skill.name, "SKILL.md"), "utf-8");
|
|
2393
3342
|
} catch {
|
|
2394
3343
|
return { name: skill.name, status: "missing" };
|
|
2395
3344
|
}
|
|
@@ -2400,7 +3349,7 @@ async function diagnoseSkills(root) {
|
|
|
2400
3349
|
async function diagnoseAgentsMd(root, pkgName) {
|
|
2401
3350
|
let content;
|
|
2402
3351
|
try {
|
|
2403
|
-
content = await
|
|
3352
|
+
content = await readFile4(join7(root, "AGENTS.md"), "utf-8");
|
|
2404
3353
|
} catch {
|
|
2405
3354
|
return { status: "missing", managedBlock: null };
|
|
2406
3355
|
}
|
|
@@ -2414,7 +3363,7 @@ async function diagnoseAgentsMd(root, pkgName) {
|
|
|
2414
3363
|
}
|
|
2415
3364
|
async function readInstalledVersion(root, pkg) {
|
|
2416
3365
|
try {
|
|
2417
|
-
const raw = await
|
|
3366
|
+
const raw = await readFile4(join7(root, "node_modules", pkg, "package.json"), "utf-8");
|
|
2418
3367
|
const parsed = JSON.parse(raw);
|
|
2419
3368
|
return parsed.version ?? null;
|
|
2420
3369
|
} catch {
|
|
@@ -2489,18 +3438,18 @@ function normalizeMigrationPkg(short) {
|
|
|
2489
3438
|
return short.startsWith("@") ? short : `@bstage-sdk/${short}`;
|
|
2490
3439
|
}
|
|
2491
3440
|
async function readBundledMigrationDoc() {
|
|
2492
|
-
const here =
|
|
3441
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
2493
3442
|
const candidates = [
|
|
2494
|
-
|
|
3443
|
+
join7(here, "MIGRATION.md"),
|
|
2495
3444
|
// 배포물: dist/ 옆 (cli/dist/MIGRATION.md)
|
|
2496
|
-
|
|
3445
|
+
join7(here, "..", "..", "..", "docs", "MIGRATION.md"),
|
|
2497
3446
|
// dev(dist): cli/dist → repo/docs
|
|
2498
|
-
|
|
3447
|
+
join7(here, "..", "..", "..", "..", "docs", "MIGRATION.md")
|
|
2499
3448
|
// dev(src): cli/src/commands → repo/docs
|
|
2500
3449
|
];
|
|
2501
3450
|
for (const path of candidates) {
|
|
2502
3451
|
try {
|
|
2503
|
-
return await
|
|
3452
|
+
return await readFile4(path, "utf-8");
|
|
2504
3453
|
} catch {
|
|
2505
3454
|
}
|
|
2506
3455
|
}
|
|
@@ -2522,172 +3471,172 @@ function compareSemver(a, b) {
|
|
|
2522
3471
|
}
|
|
2523
3472
|
function printReport(r) {
|
|
2524
3473
|
console.log("");
|
|
2525
|
-
console.log(`${
|
|
3474
|
+
console.log(`${pc9.cyan(pc9.bold("bstage doctor"))} ${pc9.dim(`\u2014 ${r.project ?? "(\uC774\uB984 \uC5C6\uC74C)"}`)}`);
|
|
2526
3475
|
console.log("");
|
|
2527
3476
|
if (!r.isBstageProject) {
|
|
2528
|
-
console.log(
|
|
2529
|
-
console.log(
|
|
3477
|
+
console.log(pc9.yellow("\uC774 \uB514\uB809\uD1A0\uB9AC\uB294 bstage \uD15C\uD50C\uB9BF \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2CC \uAC83 \uAC19\uC2B5\uB2C8\uB2E4."));
|
|
3478
|
+
console.log(pc9.dim(" package.json\uC5D0 @bstage-sdk/* \uC758\uC874\uC131\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
2530
3479
|
return;
|
|
2531
3480
|
}
|
|
2532
|
-
console.log(
|
|
3481
|
+
console.log(pc9.bold("\u{1F4E6} SDK \uBC84\uC804"));
|
|
2533
3482
|
if (!r.latestChecked) {
|
|
2534
|
-
console.log(
|
|
3483
|
+
console.log(pc9.dim(" \uB808\uC9C0\uC2A4\uD2B8\uB9AC \uC870\uD68C\uB97C \uAC74\uB108\uB6F0\uC5B4 \uC124\uCE58 \uBC84\uC804\uB9CC \uD45C\uC2DC\uD569\uB2C8\uB2E4."));
|
|
2535
3484
|
}
|
|
2536
3485
|
const nameWidth = Math.max(12, ...r.versions.map((v) => v.pkg.replace("@bstage-sdk/", "").length));
|
|
2537
3486
|
for (const v of r.versions) {
|
|
2538
3487
|
const name = v.pkg.replace("@bstage-sdk/", "").padEnd(nameWidth);
|
|
2539
3488
|
const inst = v.installed ?? "?";
|
|
2540
3489
|
if (v.latest && v.installed) {
|
|
2541
|
-
const arrow = v.outdated ?
|
|
3490
|
+
const arrow = v.outdated ? pc9.yellow(`\u2192 ${v.latest} (\uB4A4\uCC98\uC9D0)`) : pc9.green("(\uCD5C\uC2E0)");
|
|
2542
3491
|
console.log(` ${name} ${inst.padEnd(8)} ${arrow}`);
|
|
2543
3492
|
} else {
|
|
2544
3493
|
console.log(` ${name} ${inst}`);
|
|
2545
3494
|
}
|
|
2546
3495
|
}
|
|
2547
3496
|
console.log("");
|
|
2548
|
-
console.log(
|
|
3497
|
+
console.log(pc9.bold("\u{1F4C1} init \uAE30\uBCF8 \uD30C\uC77C (\uCC38\uACE0)"));
|
|
2549
3498
|
const absent = r.files.filter((f) => !f.present);
|
|
2550
3499
|
if (absent.length === 0) {
|
|
2551
|
-
console.log(
|
|
3500
|
+
console.log(pc9.green(" \u2713 init \uAE30\uBCF8 \uAD6C\uC131\uACFC \uB3D9\uC77C"));
|
|
2552
3501
|
} else {
|
|
2553
|
-
console.log(
|
|
3502
|
+
console.log(pc9.dim(" init \uAE30\uBCF8\uAC12\uACFC\uC758 \uCC28\uC774\uC77C \uBFD0 \u2014 \uD504\uB85C\uC81D\uD2B8\uAC00 \uB2E4\uB974\uAC8C \uCC98\uB9AC \uC911\uC774\uBA74 \uBB34\uC2DC\uD558\uC138\uC694."));
|
|
2554
3503
|
for (const f of r.files) {
|
|
2555
|
-
if (f.present) console.log(` ${
|
|
2556
|
-
else console.log(` ${
|
|
3504
|
+
if (f.present) console.log(` ${pc9.green("\u2713")} ${f.path}`);
|
|
3505
|
+
else console.log(` ${pc9.dim("\xB7")} ${f.path.padEnd(20)} ${pc9.dim(`\uC5C6\uC74C (${f.note})`)}`);
|
|
2557
3506
|
}
|
|
2558
3507
|
}
|
|
2559
3508
|
console.log("");
|
|
2560
|
-
console.log(
|
|
3509
|
+
console.log(pc9.bold("\u{1F5C2} \uD15C\uD50C\uB9BF \uB514\uB809\uD1A0\uB9AC"));
|
|
2561
3510
|
if (r.layout.legacyPresent) {
|
|
2562
|
-
console.log(` ${
|
|
2563
|
-
console.log(
|
|
2564
|
-
console.log(
|
|
3511
|
+
console.log(` ${pc9.yellow("\u2192")} ${pc9.yellow("src/templates/ \uAC00 \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4 (\uC61B \uAD6C\uC870)")}`);
|
|
3512
|
+
console.log(pc9.dim(" \uBE4C\uB4DC \uC0B0\uCD9C\uBB3C \uACBD\uB85C\uAC00 \uD3F4\uB354 \uAD6C\uC870\uB97C \uB530\uB77C\uAC00\uB3C4\uB85D \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
3513
|
+
console.log(pc9.dim(" \uD398\uC774\uC9C0 \u2192 src/pages/{\uACBD\uB85C}/ \xB7 \uC704\uC82F \u2192 src/slots/{\uC544\uBB34\uC774\uB984}/"));
|
|
2565
3514
|
if (r.layout.legacyTemplates.length > 0) {
|
|
2566
|
-
console.log(
|
|
3515
|
+
console.log(pc9.dim(` \uC62E\uACA8\uC57C \uD560 \uD15C\uD50C\uB9BF: ${r.layout.legacyTemplates.join(", ")}`));
|
|
2567
3516
|
}
|
|
2568
|
-
console.log(
|
|
3517
|
+
console.log(pc9.dim(' \uBCC0\uD658: \uC5D0\uC774\uC804\uD2B8\uC5D0\uAC8C "bstage \uB9C8\uC774\uADF8\uB808\uC774\uC158" \uC694\uCCAD'));
|
|
2569
3518
|
} else if (!r.layout.pagesPresent && !r.layout.slotsPresent) {
|
|
2570
|
-
console.log(` ${
|
|
3519
|
+
console.log(` ${pc9.dim("\xB7")} src/pages/ \xB7 src/slots/ \uB458 \uB2E4 \uC5C6\uC74C`);
|
|
2571
3520
|
} else {
|
|
2572
|
-
console.log(
|
|
3521
|
+
console.log(pc9.green(" \u2713 src/pages/ \xB7 src/slots/ \uAD6C\uC870"));
|
|
2573
3522
|
}
|
|
2574
3523
|
console.log("");
|
|
2575
3524
|
if (r.pnpmWorkspace.relevant) {
|
|
2576
|
-
console.log(
|
|
3525
|
+
console.log(pc9.bold("\u{1F4E6} pnpm workspace \uC124\uC815"));
|
|
2577
3526
|
if (r.pnpmWorkspace.allowBuildsDeclared) {
|
|
2578
|
-
console.log(
|
|
3527
|
+
console.log(pc9.green(" \u2713 pnpm-workspace.yaml\uC5D0 allowBuilds \uC120\uC5B8\uB428"));
|
|
2579
3528
|
} else {
|
|
2580
|
-
console.log(` ${
|
|
3529
|
+
console.log(` ${pc9.yellow("\u2192")} ${pc9.yellow("pnpm-workspace.yaml\uC5D0 allowBuilds \uC5C6\uC74C")}`);
|
|
2581
3530
|
console.log(
|
|
2582
|
-
|
|
3531
|
+
pc9.dim(" pnpm\uC740 \uC758\uC874\uC131 build script\uB97C \uAE30\uBCF8 \uCC28\uB2E8\uD558\uACE0, pnpm 11\uC740 \uADF8\uAC83\uC744 \uC5D0\uB7EC\uB85C \uB0B8\uB2E4")
|
|
2583
3532
|
);
|
|
2584
3533
|
console.log(
|
|
2585
|
-
|
|
3534
|
+
pc9.dim(" \u2192 `pnpm install`\uC774 ERR_PNPM_IGNORED_BUILDS\uB85C \uC2E4\uD328\uD558\uACE0 `pnpm dev`\uB3C4 \uC8FD\uB294\uB2E4")
|
|
2586
3535
|
);
|
|
2587
|
-
console.log(
|
|
2588
|
-
console.log(
|
|
3536
|
+
console.log(pc9.dim(" \uCD94\uAC00: pnpm-workspace.yaml\uC5D0 `allowBuilds:` + `esbuild: true`"));
|
|
3537
|
+
console.log(pc9.dim(" \uB610\uB294 `pnpm approve-builds`\uB85C \uB300\uD654\uC2DD \uC120\uD0DD"));
|
|
2589
3538
|
}
|
|
2590
3539
|
if (r.pnpmWorkspace.filePresent && !r.pnpmWorkspace.packagesDeclared) {
|
|
2591
|
-
console.log(` ${
|
|
3540
|
+
console.log(` ${pc9.yellow("\u2192")} ${pc9.yellow("pnpm-workspace.yaml\uC5D0 packages \uC5C6\uC74C")}`);
|
|
2592
3541
|
console.log(
|
|
2593
|
-
|
|
3542
|
+
pc9.dim(" pnpm 9\uB294 \uC774 \uD30C\uC77C\uC774 \uC788\uC73C\uBA74 workspace \uB8E8\uD2B8\uB85C \uBCF4\uACE0 packages\uB97C \uC694\uAD6C\uD55C\uB2E4")
|
|
2594
3543
|
);
|
|
2595
|
-
console.log(
|
|
2596
|
-
console.log(
|
|
3544
|
+
console.log(pc9.dim(" \u2192 pnpm 9\uC5D0\uC11C `ERROR packages field missing or empty`\uB85C \uC8FD\uB294\uB2E4"));
|
|
3545
|
+
console.log(pc9.dim(" \uCD94\uAC00: pnpm-workspace.yaml \uB9E8 \uC704\uC5D0 `packages:` + `- '.'`"));
|
|
2597
3546
|
} else if (r.pnpmWorkspace.packagesDeclared) {
|
|
2598
|
-
console.log(
|
|
3547
|
+
console.log(pc9.green(" \u2713 pnpm-workspace.yaml\uC5D0 packages \uC120\uC5B8\uB428"));
|
|
2599
3548
|
}
|
|
2600
3549
|
console.log("");
|
|
2601
3550
|
}
|
|
2602
|
-
console.log(
|
|
3551
|
+
console.log(pc9.bold("\u{1F6E0} package.json \uC2A4\uD06C\uB9BD\uD2B8"));
|
|
2603
3552
|
const scriptIssues = r.scripts.filter((sc) => sc.actual !== sc.expected);
|
|
2604
3553
|
if (scriptIssues.length === 0) {
|
|
2605
|
-
console.log(
|
|
3554
|
+
console.log(pc9.green(" \u2713 init \uAE30\uBCF8 \uAD6C\uC131\uACFC \uB3D9\uC77C"));
|
|
2606
3555
|
} else {
|
|
2607
3556
|
for (const sc of r.scripts) {
|
|
2608
3557
|
if (sc.actual === sc.expected) {
|
|
2609
|
-
console.log(` ${
|
|
3558
|
+
console.log(` ${pc9.green("\u2713")} ${sc.name}`);
|
|
2610
3559
|
continue;
|
|
2611
3560
|
}
|
|
2612
3561
|
if (sc.actual === null) {
|
|
2613
|
-
console.log(` ${
|
|
3562
|
+
console.log(` ${pc9.yellow("\u2192")} ${sc.name} ${pc9.yellow("\uC5C6\uC74C")}`);
|
|
2614
3563
|
} else {
|
|
2615
3564
|
console.log(
|
|
2616
|
-
` ${
|
|
3565
|
+
` ${pc9.yellow("\u2192")} ${sc.name} ${pc9.yellow("\uB2E4\uB984")} ${pc9.dim(`\u2014 \uD604\uC7AC \`${sc.actual}\``)}`
|
|
2617
3566
|
);
|
|
2618
3567
|
if (sc.actual.includes("--noEmit") && !sc.actual.includes("-p ")) {
|
|
2619
3568
|
console.log(
|
|
2620
|
-
|
|
3569
|
+
pc9.dim(
|
|
2621
3570
|
" \uC8FC\uC758: solution-style tsconfig\uC5D0\uC11C `tsc --noEmit`\uC740 \uC544\uBB34 \uD30C\uC77C\uB3C4 \uAC80\uC0AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
|
|
2622
3571
|
)
|
|
2623
3572
|
);
|
|
2624
3573
|
}
|
|
2625
3574
|
}
|
|
2626
|
-
console.log(
|
|
2627
|
-
console.log(
|
|
3575
|
+
console.log(pc9.dim(` ${sc.why}`));
|
|
3576
|
+
console.log(pc9.dim(` \uAD8C\uC7A5: "${sc.name}": "${sc.expected}"`));
|
|
2628
3577
|
}
|
|
2629
3578
|
}
|
|
2630
3579
|
console.log("");
|
|
2631
|
-
console.log(
|
|
3580
|
+
console.log(pc9.bold("\u{1F9F1} \uAD8C\uC7A5 \uC758\uC874\uC131"));
|
|
2632
3581
|
if (r.recommendedDeps.every((d) => d.declared)) {
|
|
2633
|
-
console.log(
|
|
3582
|
+
console.log(pc9.green(" \u2713 \uBAA8\uB450 \uC120\uC5B8\uB428"));
|
|
2634
3583
|
} else {
|
|
2635
3584
|
for (const d of r.recommendedDeps) {
|
|
2636
3585
|
if (d.declared) {
|
|
2637
|
-
console.log(` ${
|
|
3586
|
+
console.log(` ${pc9.green("\u2713")} ${d.pkg}`);
|
|
2638
3587
|
continue;
|
|
2639
3588
|
}
|
|
2640
|
-
console.log(` ${
|
|
2641
|
-
console.log(
|
|
2642
|
-
console.log(
|
|
3589
|
+
console.log(` ${pc9.yellow("\u2192")} ${d.pkg} ${pc9.yellow("\uBBF8\uC120\uC5B8")} ${pc9.dim(`\u2014 ${d.note}`)}`);
|
|
3590
|
+
console.log(pc9.dim(` ${d.why}`));
|
|
3591
|
+
console.log(pc9.dim(` \uCD94\uAC00: npm i ${d.pkg} (\uD504\uB85C\uC81D\uD2B8 \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800\uB85C)`));
|
|
2643
3592
|
}
|
|
2644
3593
|
}
|
|
2645
3594
|
console.log("");
|
|
2646
|
-
console.log(
|
|
3595
|
+
console.log(pc9.bold("\u{1F4D6} \uC801\uC6A9 \uAC00\uB2A5\uD55C \uB9C8\uC774\uADF8\uB808\uC774\uC158"));
|
|
2647
3596
|
if (r.migrations.length === 0) {
|
|
2648
|
-
console.log(
|
|
3597
|
+
console.log(pc9.dim(" \uD574\uB2F9 \uD56D\uBAA9 \uC5C6\uC74C."));
|
|
2649
3598
|
} else {
|
|
2650
3599
|
for (const m of r.migrations) {
|
|
2651
|
-
console.log(` ${
|
|
3600
|
+
console.log(` ${pc9.cyan(`\u2192 ${m.label}`)}`);
|
|
2652
3601
|
for (const item of m.items) console.log(` \u2022 ${item}`);
|
|
2653
3602
|
}
|
|
2654
3603
|
}
|
|
2655
3604
|
console.log("");
|
|
2656
|
-
console.log(
|
|
3605
|
+
console.log(pc9.bold("\u{1F4DD} AGENTS.md"));
|
|
2657
3606
|
switch (r.agentsMd.status) {
|
|
2658
3607
|
case "ok":
|
|
2659
|
-
console.log(
|
|
3608
|
+
console.log(pc9.green(" \u2713 \uAD00\uB9AC \uC601\uC5ED \uCD5C\uC2E0"));
|
|
2660
3609
|
break;
|
|
2661
3610
|
case "missing":
|
|
2662
|
-
console.log(
|
|
3611
|
+
console.log(pc9.dim(" \xB7 \uD30C\uC77C \uC5C6\uC74C \u2014 bstage \uD504\uB85C\uC81D\uD2B8\uB77C\uBA74 init/\uB9C8\uC774\uADF8\uB808\uC774\uC158\uC73C\uB85C \uC0DD\uC131\uD558\uC138\uC694."));
|
|
2663
3612
|
break;
|
|
2664
3613
|
case "stale":
|
|
2665
3614
|
console.log(
|
|
2666
|
-
|
|
3615
|
+
pc9.yellow(" \u2192 \uAD00\uB9AC \uC601\uC5ED\uC774 \uC624\uB798\uB428") + pc9.dim(" \u2014 `bstage skills install`\uB85C \uAC31\uC2E0(\uC790\uC720 \uC601\uC5ED\uC740 \uBCF4\uC874).")
|
|
2667
3616
|
);
|
|
2668
3617
|
break;
|
|
2669
3618
|
case "legacy":
|
|
2670
3619
|
console.log(
|
|
2671
|
-
|
|
3620
|
+
pc9.yellow(" \u2192 \uB808\uAC70\uC2DC \uD3EC\uB9F7(\uAD00\uB9AC \uC601\uC5ED \uB9C8\uCEE4 \uC5C6\uC74C)") + pc9.dim(
|
|
2672
3621
|
" \u2014 `bstage skills install`\uB85C \uC2A4\uD0AC\uC744 \uBA3C\uC800 \uC124\uCE58\uD55C \uB4A4, `bstage-migrate` \uC2A4\uD0AC\uB85C reconcile(SDK \uC601\uC5ED \uAD50\uCCB4 + \uC0AC\uC6A9\uC790 \uB0B4\uC6A9 \uBCF4\uC874)."
|
|
2673
3622
|
)
|
|
2674
3623
|
);
|
|
2675
3624
|
break;
|
|
2676
3625
|
}
|
|
2677
3626
|
console.log("");
|
|
2678
|
-
console.log(
|
|
3627
|
+
console.log(pc9.bold("\u{1F9E9} \uC5D0\uC774\uC804\uD2B8 \uC2A4\uD0AC (.claude/skills/)"));
|
|
2679
3628
|
const skillsNeedWork = r.skills.filter((s) => s.status !== "ok");
|
|
2680
3629
|
if (skillsNeedWork.length === 0) {
|
|
2681
|
-
console.log(
|
|
3630
|
+
console.log(pc9.green(" \u2713 \uBAA8\uB4E0 \uC2A4\uD0AC \uCD5C\uC2E0"));
|
|
2682
3631
|
} else {
|
|
2683
3632
|
for (const s of r.skills) {
|
|
2684
|
-
if (s.status === "ok") console.log(` ${
|
|
3633
|
+
if (s.status === "ok") console.log(` ${pc9.green("\u2713")} ${s.name}`);
|
|
2685
3634
|
else if (s.status === "missing")
|
|
2686
|
-
console.log(` ${
|
|
2687
|
-
else console.log(` ${
|
|
3635
|
+
console.log(` ${pc9.yellow("\u2192")} ${s.name.padEnd(16)} ${pc9.yellow("\uBBF8\uC124\uCE58")}`);
|
|
3636
|
+
else console.log(` ${pc9.yellow("\u2192")} ${s.name.padEnd(16)} ${pc9.yellow("\uAD6C\uBC84\uC804/\uC218\uC815\uB428")}`);
|
|
2688
3637
|
}
|
|
2689
3638
|
console.log(
|
|
2690
|
-
|
|
3639
|
+
pc9.dim(
|
|
2691
3640
|
" \uC544\uB798\uC5D0\uC11C \uC790\uB3D9 \uB3D9\uAE30\uD654\uD569\uB2C8\uB2E4 \u2014 bstage-* \uC2A4\uD0AC\uB9CC \uB36E\uC5B4\uC4F0\uBA70, \uADF8 \uC678 \uC0AC\uC6A9\uC790 \uC2A4\uD0AC\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
|
|
2692
3641
|
)
|
|
2693
3642
|
);
|
|
@@ -2697,26 +3646,741 @@ function printReport(r) {
|
|
|
2697
3646
|
const hasWork = r.versions.some((v) => v.outdated) || r.migrations.length > 0 || agentsNeedsWork;
|
|
2698
3647
|
if (hasWork) {
|
|
2699
3648
|
console.log(
|
|
2700
|
-
|
|
3649
|
+
pc9.dim(
|
|
2701
3650
|
"\uB2E4\uC74C \uB2E8\uACC4: \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC2A4\uD0AC\uC744 \uC2E4\uD589\uD574 \uC704 \uD56D\uBAA9\uC744 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uB9DE\uCDB0 \uC801\uC6A9\uD558\uC138\uC694.\n \uC0C1\uC138 \uAC00\uC774\uB4DC: SDK \uBB38\uC11C\uC758 MIGRATION.md (\uC704 \uD56D\uBAA9\uBCC4 \uC801\uC6A9 \uC808\uCC28)"
|
|
2702
3651
|
)
|
|
2703
3652
|
);
|
|
2704
3653
|
} else {
|
|
2705
|
-
console.log(
|
|
3654
|
+
console.log(pc9.green("\u2713 \uCD5C\uC2E0 \uC0C1\uD0DC\uC785\uB2C8\uB2E4. \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC774 \uD544\uC694 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
2706
3655
|
if (absent.length > 0) {
|
|
2707
|
-
console.log(
|
|
3656
|
+
console.log(pc9.dim(" (\uC704 init \uAE30\uBCF8 \uD30C\uC77C \uCC28\uC774\uB294 \uC120\uD0DD \uC0AC\uD56D\uC785\uB2C8\uB2E4.)"));
|
|
2708
3657
|
}
|
|
2709
3658
|
if (r.recommendedDeps.some((d) => !d.declared)) {
|
|
2710
|
-
console.log(
|
|
3659
|
+
console.log(pc9.dim(" (\uC704 \uAD8C\uC7A5 \uC758\uC874\uC131\uC740 \uBC84\uC804\uACFC \uBB34\uAD00\uD569\uB2C8\uB2E4 \u2014 \uC4F8 \uAC70\uBA74 \uB530\uB85C \uCD94\uAC00\uD558\uC138\uC694.)"));
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
console.log("");
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
// src/commands/link.ts
|
|
3666
|
+
import * as p3 from "@clack/prompts";
|
|
3667
|
+
import pc10 from "picocolors";
|
|
3668
|
+
var clackSelect = async (message, items) => {
|
|
3669
|
+
const options = items.map((i) => ({
|
|
3670
|
+
value: i.value,
|
|
3671
|
+
label: i.label
|
|
3672
|
+
}));
|
|
3673
|
+
const v = await p3.select({ message, options });
|
|
3674
|
+
if (p3.isCancel(v)) fail(ExitCode.PRECONDITION, "\uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4.");
|
|
3675
|
+
return v;
|
|
3676
|
+
};
|
|
3677
|
+
async function originOwnerRepo(exec) {
|
|
3678
|
+
try {
|
|
3679
|
+
const stdout = await exec(["remote", "get-url", "origin"]);
|
|
3680
|
+
const m = stdout.trim().match(/[/:]([^/:]+)\/([^/]+?)(?:\.git)?$/);
|
|
3681
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
3682
|
+
} catch {
|
|
3683
|
+
return null;
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
function findUniqueOriginMatch(repos, origin) {
|
|
3687
|
+
const lower = origin.toLowerCase();
|
|
3688
|
+
const matches = repos.filter((r) => `${r.owner}/${r.repo}`.toLowerCase() === lower);
|
|
3689
|
+
return matches.length === 1 ? matches[0] : null;
|
|
3690
|
+
}
|
|
3691
|
+
async function pick(label, flag, items, explicit, yes, interactive, select3) {
|
|
3692
|
+
if (items.length === 0)
|
|
3693
|
+
fail(ExitCode.PRECONDITION, `\uC120\uD0DD\uD560 ${label} \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uD3EC\uD138\uC5D0\uC11C \uBA3C\uC800 \uC5F0\uACB0\uD558\uC138\uC694.`);
|
|
3694
|
+
if (explicit) {
|
|
3695
|
+
const hit = items.find((i) => i.match.includes(explicit));
|
|
3696
|
+
if (!hit) {
|
|
3697
|
+
fail(
|
|
3698
|
+
ExitCode.PRECONDITION,
|
|
3699
|
+
`${label} '${explicit}' \uC744(\uB97C) \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD6C4\uBCF4: ${items.map((i) => i.label).join(", ")}`
|
|
3700
|
+
);
|
|
3701
|
+
}
|
|
3702
|
+
return hit.value;
|
|
3703
|
+
}
|
|
3704
|
+
if (items.length === 1) return items[0].value;
|
|
3705
|
+
if (yes || !interactive) {
|
|
3706
|
+
fail(
|
|
3707
|
+
ExitCode.PRECONDITION,
|
|
3708
|
+
`${label} \uD6C4\uBCF4\uAC00 \uC5EC\uB7EC \uAC1C\uC785\uB2C8\uB2E4. --${flag} \uB85C \uC9C0\uC815\uD558\uC138\uC694: ${items.map((i) => i.label).join(", ")}`
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
return select3(`${label}\uC744(\uB97C) \uC120\uD0DD\uD558\uC138\uC694`, items);
|
|
3712
|
+
}
|
|
3713
|
+
async function resolveRepoSelection(repos, ctx) {
|
|
3714
|
+
const explicit = ctx.options.repo ?? ctx.env.BSTAGE_REPO;
|
|
3715
|
+
if (!explicit && repos.length > 1) {
|
|
3716
|
+
const origin = await originOwnerRepo(ctx.exec);
|
|
3717
|
+
const match = origin ? findUniqueOriginMatch(repos, origin) : null;
|
|
3718
|
+
if (match) return { repo: match, fromOrigin: true };
|
|
3719
|
+
}
|
|
3720
|
+
const items = repos.map((r) => ({
|
|
3721
|
+
value: r,
|
|
3722
|
+
label: `${r.owner}/${r.repo} [${r.surface}] (${r.defaultBranch})`,
|
|
3723
|
+
match: [r.id, `${r.owner}/${r.repo}`]
|
|
3724
|
+
}));
|
|
3725
|
+
const repo = await pick(
|
|
3726
|
+
"\uB808\uD3EC",
|
|
3727
|
+
"repo",
|
|
3728
|
+
items,
|
|
3729
|
+
explicit,
|
|
3730
|
+
ctx.options.yes,
|
|
3731
|
+
ctx.interactive,
|
|
3732
|
+
ctx.select
|
|
3733
|
+
);
|
|
3734
|
+
return { repo, fromOrigin: false };
|
|
3735
|
+
}
|
|
3736
|
+
async function resolveSelections(ctx) {
|
|
3737
|
+
const { client, options, env: env2, interactive, select: select3 } = ctx;
|
|
3738
|
+
const me = await client.me();
|
|
3739
|
+
const organizationId = await pick(
|
|
3740
|
+
"\uC870\uC9C1",
|
|
3741
|
+
"org",
|
|
3742
|
+
me.memberships.map((m) => ({
|
|
3743
|
+
value: m.organizationId,
|
|
3744
|
+
label: m.organizationId,
|
|
3745
|
+
match: [m.organizationId]
|
|
3746
|
+
})),
|
|
3747
|
+
options.org ?? env2.BSTAGE_ORG,
|
|
3748
|
+
options.yes,
|
|
3749
|
+
interactive,
|
|
3750
|
+
select3
|
|
3751
|
+
);
|
|
3752
|
+
const spaces = await client.listSpaces(organizationId);
|
|
3753
|
+
const spaceId = await pick(
|
|
3754
|
+
"\uC2A4\uD14C\uC774\uC9C0",
|
|
3755
|
+
"space",
|
|
3756
|
+
spaces.map((s) => ({
|
|
3757
|
+
value: s.spaceId,
|
|
3758
|
+
label: `${s.spaceId} (${s.tier})`,
|
|
3759
|
+
match: [s.spaceId]
|
|
3760
|
+
})),
|
|
3761
|
+
options.space ?? env2.BSTAGE_SPACE,
|
|
3762
|
+
options.yes,
|
|
3763
|
+
interactive,
|
|
3764
|
+
select3
|
|
3765
|
+
);
|
|
3766
|
+
const repos = await client.listRepos(organizationId, spaceId);
|
|
3767
|
+
const { repo, fromOrigin } = await resolveRepoSelection(repos, ctx);
|
|
3768
|
+
return { organizationId, spaceId, repo, repoFromOrigin: fromOrigin };
|
|
3769
|
+
}
|
|
3770
|
+
function printLinked(link, repoLabel, repoFromOrigin, json) {
|
|
3771
|
+
if (json) {
|
|
3772
|
+
printJson(link);
|
|
3773
|
+
return;
|
|
3774
|
+
}
|
|
3775
|
+
if (repoFromOrigin) {
|
|
3776
|
+
console.log(pc10.dim(` git origin(${repoLabel})\uACFC \uC77C\uCE58\uD558\uB294 \uB808\uD3EC\uB97C \uAE30\uBCF8\uAC12\uC73C\uB85C \uC120\uD0DD\uD588\uC2B5\uB2C8\uB2E4.`));
|
|
3777
|
+
}
|
|
3778
|
+
console.log(
|
|
3779
|
+
pc10.green(
|
|
3780
|
+
`\u2713 \uC5F0\uACB0\uD588\uC2B5\uB2C8\uB2E4: ${link.organizationId} / ${link.spaceId} / ${repoLabel} [${link.surface}]`
|
|
3781
|
+
)
|
|
3782
|
+
);
|
|
3783
|
+
console.log(
|
|
3784
|
+
pc10.dim(
|
|
3785
|
+
" .bstage/project.json \uC5D0 \uC800\uC7A5 (gitignore \uB300\uC0C1). CI\uC5D0\uC11C\uB294 BSTAGE_ORG\xB7BSTAGE_SPACE\xB7BSTAGE_REPO\xB7BSTAGE_SURFACE\xB7BSTAGE_PORTAL_URL \uD658\uACBD\uBCC0\uC218\uB97C \uC4F0\uC138\uC694."
|
|
3786
|
+
)
|
|
3787
|
+
);
|
|
3788
|
+
}
|
|
3789
|
+
async function linkCommand(options, deps = {}) {
|
|
3790
|
+
if (options.json && !options.yes) {
|
|
3791
|
+
fail(ExitCode.PRECONDITION, "--json \uC740 --yes \uC640 \uD568\uAED8 \uC4F0\uC138\uC694.");
|
|
3792
|
+
}
|
|
3793
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
3794
|
+
const env2 = deps.env ?? process.env;
|
|
3795
|
+
const interactive = deps.select !== void 0 || p3.isTTY(process.stdout);
|
|
3796
|
+
const select3 = deps.select ?? clackSelect;
|
|
3797
|
+
const exec = deps.exec ?? gitExec(cwd);
|
|
3798
|
+
const { client, portalUrl } = await resolveClientOnly({
|
|
3799
|
+
portal: options.portal,
|
|
3800
|
+
phase: options.phase,
|
|
3801
|
+
// 링크 파일이 아직 없을 수 있으니 조직은 사람이 준 값으로 정한다 — 여러 조직 토큰이
|
|
3802
|
+
// 저장돼 있을 때 --org 안내가 실제로 통하게 하는 자리다.
|
|
3803
|
+
organizationId: options.org ?? env2.BSTAGE_ORG,
|
|
3804
|
+
cwd,
|
|
3805
|
+
env: env2,
|
|
3806
|
+
fetch: deps.fetch
|
|
3807
|
+
});
|
|
3808
|
+
const { organizationId, spaceId, repo, repoFromOrigin } = await resolveSelections({
|
|
3809
|
+
client,
|
|
3810
|
+
exec,
|
|
3811
|
+
env: env2,
|
|
3812
|
+
options,
|
|
3813
|
+
interactive,
|
|
3814
|
+
select: select3
|
|
3815
|
+
});
|
|
3816
|
+
const link = {
|
|
3817
|
+
portalUrl,
|
|
3818
|
+
organizationId,
|
|
3819
|
+
spaceId,
|
|
3820
|
+
repoId: repo.id,
|
|
3821
|
+
surface: repo.surface
|
|
3822
|
+
};
|
|
3823
|
+
await saveLink(cwd, link);
|
|
3824
|
+
printLinked(link, `${repo.owner}/${repo.repo}`, repoFromOrigin, options.json);
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
// src/commands/list.ts
|
|
3828
|
+
import pc11 from "picocolors";
|
|
3829
|
+
|
|
3830
|
+
// src/portal/buildRepo.ts
|
|
3831
|
+
function buildMatchesRepo(b, repo) {
|
|
3832
|
+
if (b.stageRepoId) return b.stageRepoId === repo.id;
|
|
3833
|
+
if (!b.owner || !b.repo) return false;
|
|
3834
|
+
if (b.owner.toLowerCase() !== repo.owner.toLowerCase()) return false;
|
|
3835
|
+
if (b.repo.toLowerCase() !== repo.repo.toLowerCase()) return false;
|
|
3836
|
+
if (!b.surface || b.surface !== repo.surface) return false;
|
|
3837
|
+
if (!b.spaceId || b.spaceId !== repo.spaceId) return false;
|
|
3838
|
+
return true;
|
|
3839
|
+
}
|
|
3840
|
+
async function resolveLinkedRepo(ctx) {
|
|
3841
|
+
const repos = await ctx.client.listRepos(ctx.link.organizationId, ctx.link.spaceId);
|
|
3842
|
+
const repo = repos.find((r) => r.id === ctx.link.repoId);
|
|
3843
|
+
if (!repo) {
|
|
3844
|
+
fail(
|
|
3845
|
+
ExitCode.PRECONDITION,
|
|
3846
|
+
"\uC5F0\uACB0\uB41C \uB808\uD3EC\uB97C \uC2A4\uD14C\uC774\uC9C0\uC5D0\uC11C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. bstage link \uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694."
|
|
3847
|
+
);
|
|
3848
|
+
}
|
|
3849
|
+
return repo;
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3852
|
+
// src/commands/list.ts
|
|
3853
|
+
function renderList(placements, builds, repo) {
|
|
3854
|
+
const mine = placements.filter((p4) => p4.stageRepoId === repo.id);
|
|
3855
|
+
const lines = [];
|
|
3856
|
+
lines.push(pc11.bold("\uBC30\uCE58"));
|
|
3857
|
+
if (mine.length === 0) {
|
|
3858
|
+
lines.push(
|
|
3859
|
+
pc11.yellow(
|
|
3860
|
+
" \uC774 \uB808\uD3EC\uC5D0 \uC5F0\uACB0\uB41C \uBC30\uCE58\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uD3EC\uD138 > \uC2A4\uD14C\uC774\uC9C0 > \uD398\uC774\uC9C0\uC5D0\uC11C \uBC30\uCE58\uB97C \uBA3C\uC800 \uB9CC\uB4DC\uC138\uC694."
|
|
3861
|
+
)
|
|
3862
|
+
);
|
|
3863
|
+
} else {
|
|
3864
|
+
lines.push(
|
|
3865
|
+
table([
|
|
3866
|
+
["\uC885\uB958", "\uC790\uB9AC", "\uC0C1\uD0DC", "\uB77C\uC774\uBE0C \uCEE4\uBC0B", "\uAC8C\uC2DC"],
|
|
3867
|
+
...mine.map((p4) => [
|
|
3868
|
+
p4.kind,
|
|
3869
|
+
placementLabel(p4),
|
|
3870
|
+
p4.status,
|
|
3871
|
+
shortSha(p4.liveCommit),
|
|
3872
|
+
p4.enabled === false ? "off" : "on"
|
|
3873
|
+
])
|
|
3874
|
+
])
|
|
3875
|
+
);
|
|
3876
|
+
}
|
|
3877
|
+
lines.push("");
|
|
3878
|
+
lines.push(pc11.bold("\uCD5C\uADFC \uBE4C\uB4DC"));
|
|
3879
|
+
const recent = builds.filter((b) => buildMatchesRepo(b, repo));
|
|
3880
|
+
lines.push(
|
|
3881
|
+
recent.length === 0 ? pc11.dim(" \uBE4C\uB4DC \uC774\uB825\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.") : table([
|
|
3882
|
+
["\uBE4C\uB4DC", "\uC0C1\uD0DC", "\uCEE4\uBC0B", "\uC694\uCCAD \uC2DC\uAC01"],
|
|
3883
|
+
...recent.map((b) => [b.id, b.status, shortSha(b.commitSha), b.requestedAt])
|
|
3884
|
+
])
|
|
3885
|
+
);
|
|
3886
|
+
return lines.join("\n");
|
|
3887
|
+
}
|
|
3888
|
+
async function listCommand(options, deps = {}) {
|
|
3889
|
+
const out = deps.out ?? console.log;
|
|
3890
|
+
const { client, link } = await resolveContext({
|
|
3891
|
+
cwd: deps.cwd,
|
|
3892
|
+
env: deps.env,
|
|
3893
|
+
fetch: deps.fetch
|
|
3894
|
+
});
|
|
3895
|
+
const limit = parseInt(options.builds ?? "5", 10) || 5;
|
|
3896
|
+
const [placements, builds, repo] = await Promise.all([
|
|
3897
|
+
client.listPlacements(link.organizationId, link.spaceId, link.surface),
|
|
3898
|
+
client.listBuilds(link.organizationId, link.spaceId, limit),
|
|
3899
|
+
resolveLinkedRepo({ client, link })
|
|
3900
|
+
]);
|
|
3901
|
+
const mine = placements.filter((p4) => p4.stageRepoId === repo.id);
|
|
3902
|
+
const myBuilds = builds.filter((b) => buildMatchesRepo(b, repo));
|
|
3903
|
+
if (options.json) {
|
|
3904
|
+
printJson({ link, placements: mine, builds: myBuilds });
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
out(pc11.dim(`${link.organizationId} / ${link.spaceId} [${link.surface}]`));
|
|
3908
|
+
out(renderList(placements, builds, repo));
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
// src/commands/logs.ts
|
|
3912
|
+
import pc12 from "picocolors";
|
|
3913
|
+
function statusLine(build) {
|
|
3914
|
+
return `[bstage] \uBE4C\uB4DC ${build.id} \xB7 ${build.status}${build.failureReason ? ` \xB7 ${build.failureReason}` : ""}`;
|
|
3915
|
+
}
|
|
3916
|
+
async function latestBuildId(ctx) {
|
|
3917
|
+
const { organizationId: org, spaceId: space } = ctx.link;
|
|
3918
|
+
const repo = await resolveLinkedRepo(ctx);
|
|
3919
|
+
const latest = (await ctx.client.listBuilds(org, space, 20)).find(
|
|
3920
|
+
(b) => buildMatchesRepo(b, repo)
|
|
3921
|
+
);
|
|
3922
|
+
if (!latest) fail(ExitCode.PRECONDITION, "\uC774 \uB808\uD3EC\uC758 \uBE4C\uB4DC \uC774\uB825\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
3923
|
+
return latest.id;
|
|
3924
|
+
}
|
|
3925
|
+
function logSink(json) {
|
|
3926
|
+
return json ? (delta) => void process.stderr.write(pc12.dim(delta)) : (delta) => void process.stdout.write(delta);
|
|
3927
|
+
}
|
|
3928
|
+
async function logsCommand(buildId, options, deps = {}) {
|
|
3929
|
+
const out = deps.out ?? ((s) => void process.stdout.write(s));
|
|
3930
|
+
const ctx = await resolveContext({ cwd: deps.cwd, env: deps.env, fetch: deps.fetch });
|
|
3931
|
+
const { organizationId: org, spaceId: space } = ctx.link;
|
|
3932
|
+
const id = buildId ?? await latestBuildId(ctx);
|
|
3933
|
+
const initial = await ctx.client.getBuild(org, space, id);
|
|
3934
|
+
const following = Boolean(options.follow) && !TERMINAL_STATUSES.has(initial.status);
|
|
3935
|
+
const build = following ? await waitForBuild(ctx.client, org, space, id, {
|
|
3936
|
+
sleep: deps.sleep,
|
|
3937
|
+
onLog: logSink(options.json)
|
|
3938
|
+
}) : initial;
|
|
3939
|
+
await report(ctx.client, org, space, id, build, options, out, following);
|
|
3940
|
+
if (TERMINAL_STATUSES.has(build.status) && build.status !== "SUCCEEDED") {
|
|
3941
|
+
process.exitCode = ExitCode.FAILURE;
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
async function report(client, org, space, id, build, options, out, following) {
|
|
3945
|
+
if (options.json) {
|
|
3946
|
+
printJson({ build, log: await client.getBuildLog(org, space, id) });
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
if (following) {
|
|
3950
|
+
console.error(pc12.dim(`
|
|
3951
|
+
${statusLine(build)}`));
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
const log2 = await client.getBuildLog(org, space, id);
|
|
3955
|
+
out(log2.endsWith("\n") ? log2 : log2 + "\n");
|
|
3956
|
+
console.error(pc12.dim(statusLine(build)));
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
// src/commands/login.ts
|
|
3960
|
+
import { hostname as osHostname } from "os";
|
|
3961
|
+
import pc13 from "picocolors";
|
|
3962
|
+
|
|
3963
|
+
// src/portal/openBrowser.ts
|
|
3964
|
+
import { execFile as execFile2 } from "child_process";
|
|
3965
|
+
var SHELL_METACHARS = /[&|^<>%!"'`\\]/;
|
|
3966
|
+
function isWebUrl(url) {
|
|
3967
|
+
try {
|
|
3968
|
+
const u = new URL(url);
|
|
3969
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
3970
|
+
} catch {
|
|
3971
|
+
return false;
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
function openBrowser(url) {
|
|
3975
|
+
if (!isWebUrl(url) || SHELL_METACHARS.test(url)) return Promise.resolve(false);
|
|
3976
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
3977
|
+
return new Promise((resolve4) => {
|
|
3978
|
+
try {
|
|
3979
|
+
execFile2(cmd, args, { timeout: 5e3 }, (err) => resolve4(!err));
|
|
3980
|
+
} catch {
|
|
3981
|
+
resolve4(false);
|
|
2711
3982
|
}
|
|
3983
|
+
});
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
// src/commands/login.ts
|
|
3987
|
+
var SLOW_DOWN_STEP_MS = 5e3;
|
|
3988
|
+
async function verifyToken(client) {
|
|
3989
|
+
try {
|
|
3990
|
+
return await client.me();
|
|
3991
|
+
} catch (err) {
|
|
3992
|
+
if (err instanceof PortalError && (err.status === 401 || err.status === 403)) {
|
|
3993
|
+
fail(
|
|
3994
|
+
ExitCode.AUTH,
|
|
3995
|
+
"\uD1A0\uD070\uC774 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uD3EC\uD138 \uC124\uC815 \u203A CLI \uD1A0\uD070\uC5D0\uC11C \uC0C8 \uD1A0\uD070\uC744 \uBC1C\uAE09\uD558\uC138\uC694."
|
|
3996
|
+
);
|
|
3997
|
+
}
|
|
3998
|
+
throw err;
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
async function loginWithToken(portalUrl, token, options, deps, env2) {
|
|
4002
|
+
if (!token.startsWith("bsc_")) {
|
|
4003
|
+
fail(
|
|
4004
|
+
ExitCode.PRECONDITION,
|
|
4005
|
+
"CLI \uD1A0\uD070\uC740 bsc_ \uB85C \uC2DC\uC791\uD569\uB2C8\uB2E4. \uD3EC\uD138 \uC124\uC815 \u203A CLI \uD1A0\uD070\uC5D0\uC11C \uBC1C\uAE09\uD55C \uAC12\uC778\uC9C0 \uD655\uC778\uD558\uC138\uC694."
|
|
4006
|
+
);
|
|
4007
|
+
}
|
|
4008
|
+
const organizationId = options.org ?? env2.BSTAGE_ORG;
|
|
4009
|
+
if (!organizationId) {
|
|
4010
|
+
fail(
|
|
4011
|
+
ExitCode.PRECONDITION,
|
|
4012
|
+
"\uBD99\uC5EC \uB123\uC740 \uD1A0\uD070\uC774 \uC5B4\uB290 \uC870\uC9C1 \uAC83\uC778\uC9C0 \uC54C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --org <id> \uB610\uB294 BSTAGE_ORG \uB85C \uC870\uC9C1\uC744 \uC9C0\uC815\uD558\uC138\uC694."
|
|
4013
|
+
);
|
|
2712
4014
|
}
|
|
4015
|
+
const client = new PortalClient({ portalUrl, token, fetch: deps.fetch });
|
|
4016
|
+
const me = await verifyToken(client);
|
|
4017
|
+
await saveCredential({ portalUrl, organizationId, token, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, env2);
|
|
4018
|
+
if (options.json) {
|
|
4019
|
+
printJson({
|
|
4020
|
+
portalUrl,
|
|
4021
|
+
method: "token",
|
|
4022
|
+
organizationId,
|
|
4023
|
+
email: me.user.email,
|
|
4024
|
+
organizations: me.memberships.map((m) => m.organizationId)
|
|
4025
|
+
});
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4028
|
+
console.log(
|
|
4029
|
+
pc13.green(`\u2713 ${portalUrl} \uC5D0 ${me.user.email} \uB85C \uB85C\uADF8\uC778\uD588\uC2B5\uB2C8\uB2E4 (\uC870\uC9C1 ${organizationId}).`)
|
|
4030
|
+
);
|
|
4031
|
+
}
|
|
4032
|
+
function nextIntervalOnSlowDown(interval, retryAfterSec) {
|
|
4033
|
+
const bumped = interval + SLOW_DOWN_STEP_MS;
|
|
4034
|
+
return retryAfterSec !== void 0 ? Math.max(bumped, retryAfterSec * 1e3) : bumped;
|
|
4035
|
+
}
|
|
4036
|
+
function printDeviceCodePrompt(userCode, verificationUriComplete, expiresIn) {
|
|
2713
4037
|
console.log("");
|
|
4038
|
+
console.log(` \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC544\uB798 \uCF54\uB4DC\uB97C \uD655\uC778\uD558\uACE0 \uC2B9\uC778\uD558\uC138\uC694.`);
|
|
4039
|
+
console.log("");
|
|
4040
|
+
console.log(` \uCF54\uB4DC ${pc13.bold(userCode)}`);
|
|
4041
|
+
console.log(` \uC8FC\uC18C ${pc13.underline(verificationUriComplete)}`);
|
|
4042
|
+
console.log("");
|
|
4043
|
+
console.log(
|
|
4044
|
+
pc13.dim(
|
|
4045
|
+
` \uD130\uBBF8\uB110\uC758 \uCF54\uB4DC\uC640 \uBE0C\uB77C\uC6B0\uC800\uC758 \uCF54\uB4DC\uAC00 \uAC19\uC740\uC9C0 \uAF2D \uD655\uC778\uD558\uC138\uC694. ${Math.round(expiresIn / 60)}\uBD84 \uC548\uC5D0 \uC2B9\uC778\uB418\uC9C0 \uC54A\uC73C\uBA74 \uB9CC\uB8CC\uB429\uB2C8\uB2E4.`
|
|
4046
|
+
)
|
|
4047
|
+
);
|
|
4048
|
+
}
|
|
4049
|
+
async function pollUntilIssued(client, deviceCode, startInterval, expiresIn, sleep, now) {
|
|
4050
|
+
let interval = startInterval;
|
|
4051
|
+
const deadline = now() + expiresIn * 1e3;
|
|
4052
|
+
for (; ; ) {
|
|
4053
|
+
const remaining = deadline - now();
|
|
4054
|
+
if (remaining <= 0) {
|
|
4055
|
+
fail(ExitCode.PRECONDITION, "\uC2B9\uC778 \uC2DC\uAC04\uC774 \uC9C0\uB0AC\uC2B5\uB2C8\uB2E4. bstage login \uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694");
|
|
4056
|
+
}
|
|
4057
|
+
await sleep(Math.min(interval, remaining));
|
|
4058
|
+
let result;
|
|
4059
|
+
try {
|
|
4060
|
+
result = await client.pollDeviceToken(deviceCode);
|
|
4061
|
+
} catch (err) {
|
|
4062
|
+
if (err instanceof PortalError && err.message === "access_denied") {
|
|
4063
|
+
fail(ExitCode.AUTH, "\uD3EC\uD138\uC5D0\uC11C \uC774 \uB85C\uADF8\uC778 \uC694\uCCAD\uC744 \uAC70\uBD80\uD588\uC2B5\uB2C8\uB2E4.");
|
|
4064
|
+
}
|
|
4065
|
+
if (err instanceof PortalError && err.message === "expired_token") {
|
|
4066
|
+
fail(
|
|
4067
|
+
ExitCode.PRECONDITION,
|
|
4068
|
+
"10\uBD84 \uC548\uC5D0 \uC2B9\uC778\uB418\uC9C0 \uC54A\uC544 \uCF54\uB4DC\uAC00 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. bstage login \uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694."
|
|
4069
|
+
);
|
|
4070
|
+
}
|
|
4071
|
+
throw err;
|
|
4072
|
+
}
|
|
4073
|
+
if (result.status === "slow_down") {
|
|
4074
|
+
interval = nextIntervalOnSlowDown(interval, result.retryAfter);
|
|
4075
|
+
continue;
|
|
4076
|
+
}
|
|
4077
|
+
if (result.status === "pending") continue;
|
|
4078
|
+
return result;
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
async function openVerificationUri(portalUrl, verificationUriComplete, options, deps) {
|
|
4082
|
+
if (!sameOrigin(portalUrl, verificationUriComplete)) {
|
|
4083
|
+
if (!options.json) {
|
|
4084
|
+
console.log(
|
|
4085
|
+
pc13.yellow(
|
|
4086
|
+
" \uC2B9\uC778 \uC8FC\uC18C\uAC00 \uD3EC\uD138\uACFC \uB2E4\uB978 \uC0AC\uC774\uD2B8\uC785\uB2C8\uB2E4. \uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 \uC8FC\uC18C\uB97C \uD655\uC778\uD55C \uB4A4 \uC9C1\uC811 \uC5F4\uC5B4 \uC8FC\uC138\uC694."
|
|
4087
|
+
)
|
|
4088
|
+
);
|
|
4089
|
+
}
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
const opened = await (deps.openBrowser ?? openBrowser)(verificationUriComplete);
|
|
4093
|
+
if (!opened && !options.json) {
|
|
4094
|
+
console.log(pc13.dim(" \uBE0C\uB77C\uC6B0\uC800\uB97C \uC790\uB3D9\uC73C\uB85C \uC5F4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uC704 \uC8FC\uC18C\uB97C \uC9C1\uC811 \uC5F4\uC5B4 \uC8FC\uC138\uC694."));
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
function sameOrigin(a, b) {
|
|
4098
|
+
try {
|
|
4099
|
+
return new URL(a).origin === new URL(b).origin;
|
|
4100
|
+
} catch {
|
|
4101
|
+
return false;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
async function loginWithDevice(portalUrl, options, deps, env2) {
|
|
4105
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
4106
|
+
const anon = new PortalClient({ portalUrl, token: "", fetch: deps.fetch });
|
|
4107
|
+
const code = await anon.startDeviceAuth(
|
|
4108
|
+
(deps.hostname ?? osHostname)(),
|
|
4109
|
+
deps.cliVersion ?? "unknown"
|
|
4110
|
+
);
|
|
4111
|
+
if (!options.json)
|
|
4112
|
+
printDeviceCodePrompt(code.userCode, code.verificationUriComplete, code.expiresIn);
|
|
4113
|
+
if (!options.noBrowser) {
|
|
4114
|
+
await openVerificationUri(portalUrl, code.verificationUriComplete, options, deps);
|
|
4115
|
+
}
|
|
4116
|
+
const now = deps.now ?? Date.now;
|
|
4117
|
+
const issued = await pollUntilIssued(
|
|
4118
|
+
anon,
|
|
4119
|
+
code.deviceCode,
|
|
4120
|
+
code.interval * 1e3,
|
|
4121
|
+
code.expiresIn,
|
|
4122
|
+
sleep,
|
|
4123
|
+
now
|
|
4124
|
+
);
|
|
4125
|
+
const organizationId = issued.organizationId ?? options.org ?? env2.BSTAGE_ORG;
|
|
4126
|
+
await storeIssued(portalUrl, issued, organizationId, env2);
|
|
4127
|
+
reportIssued(portalUrl, issued, organizationId, options.json);
|
|
4128
|
+
}
|
|
4129
|
+
async function storeIssued(portalUrl, issued, organizationId, env2) {
|
|
4130
|
+
await saveCredential(
|
|
4131
|
+
{
|
|
4132
|
+
portalUrl,
|
|
4133
|
+
organizationId,
|
|
4134
|
+
token: issued.token,
|
|
4135
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4136
|
+
permission: issued.permission,
|
|
4137
|
+
allSpaces: issued.allSpaces,
|
|
4138
|
+
spaceIds: issued.spaceIds,
|
|
4139
|
+
expiresAt: issued.expiresAt
|
|
4140
|
+
},
|
|
4141
|
+
env2
|
|
4142
|
+
);
|
|
4143
|
+
}
|
|
4144
|
+
function reportIssued(portalUrl, issued, organizationId, json) {
|
|
4145
|
+
if (json) {
|
|
4146
|
+
printJson({
|
|
4147
|
+
portalUrl,
|
|
4148
|
+
method: "device",
|
|
4149
|
+
tokenId: issued.id,
|
|
4150
|
+
prefix: issued.prefix,
|
|
4151
|
+
organizationId: organizationId ?? null,
|
|
4152
|
+
permission: issued.permission,
|
|
4153
|
+
allSpaces: issued.allSpaces ?? false,
|
|
4154
|
+
spaceIds: issued.spaceIds,
|
|
4155
|
+
expiresAt: issued.expiresAt
|
|
4156
|
+
});
|
|
4157
|
+
return;
|
|
4158
|
+
}
|
|
4159
|
+
console.log(pc13.green(`\u2713 ${portalUrl} \uC5D0 \uB85C\uADF8\uC778\uD588\uC2B5\uB2C8\uB2E4.`));
|
|
4160
|
+
console.log(
|
|
4161
|
+
pc13.dim(
|
|
4162
|
+
` \uC870\uC9C1 ${orgLabel(organizationId ?? UNKNOWN_ORG)} \xB7 \uD1A0\uD070 ${issued.prefix}\u2026 \xB7 \uB4F1\uAE09 ${issued.permission} \xB7 ${scopeLabel(issued.allSpaces, issued.spaceIds)} \xB7 \uB9CC\uB8CC ${issued.expiresAt.slice(0, 10)}`
|
|
4163
|
+
)
|
|
4164
|
+
);
|
|
4165
|
+
console.log(pc13.dim(" \uB2E4\uC74C: \uD504\uB85C\uC81D\uD2B8 \uB514\uB809\uD130\uB9AC\uC5D0\uC11C bstage link"));
|
|
4166
|
+
}
|
|
4167
|
+
async function loginCommand(options, deps = {}) {
|
|
4168
|
+
const env2 = deps.env ?? process.env;
|
|
4169
|
+
const portalUrl = await resolvePortalUrl(
|
|
4170
|
+
options.portal,
|
|
4171
|
+
deps.cwd ?? process.cwd(),
|
|
4172
|
+
env2,
|
|
4173
|
+
options.phase,
|
|
4174
|
+
{ allowLink: false }
|
|
4175
|
+
);
|
|
4176
|
+
const pasted = (options.token ?? env2.BSTAGE_TOKEN ?? "").trim();
|
|
4177
|
+
if (pasted) return loginWithToken(portalUrl, pasted, options, deps, env2);
|
|
4178
|
+
return loginWithDevice(portalUrl, options, deps, env2);
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
// src/commands/logout.ts
|
|
4182
|
+
import pc14 from "picocolors";
|
|
4183
|
+
async function logoutCommand(options, deps = {}) {
|
|
4184
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
4185
|
+
const env2 = deps.env ?? process.env;
|
|
4186
|
+
const portalUrl = await resolvePortalUrl(options.portal, cwd, env2, options.phase);
|
|
4187
|
+
if (!portalUrl) {
|
|
4188
|
+
fail(
|
|
4189
|
+
ExitCode.PRECONDITION,
|
|
4190
|
+
"\uD3EC\uD138 \uC8FC\uC18C\uB97C \uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: bstage logout --phase <phase> \uB610\uB294 --portal <url>"
|
|
4191
|
+
);
|
|
4192
|
+
}
|
|
4193
|
+
const removed = await deleteCredential(portalUrl, options.org, env2);
|
|
4194
|
+
if (removed.length === 0) {
|
|
4195
|
+
const scope = options.org ? `\uC870\uC9C1 ${options.org} \uC758 ` : "";
|
|
4196
|
+
console.log(pc14.yellow(`${portalUrl} \uC5D0 \uC800\uC7A5\uB41C ${scope}\uC790\uACA9\uC99D\uBA85\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`));
|
|
4197
|
+
} else {
|
|
4198
|
+
const orgs = removed.map(orgLabel).join(", ");
|
|
4199
|
+
console.log(pc14.green(`\u2713 ${portalUrl} \uC790\uACA9\uC99D\uBA85\uC744 \uC0AD\uC81C\uD588\uC2B5\uB2C8\uB2E4 \u2014 \uC870\uC9C1 ${orgs}`));
|
|
4200
|
+
}
|
|
4201
|
+
console.log(pc14.dim(" \uD3EC\uD138 \uD654\uBA74\uC5D0\uC11C \uD1A0\uD070 \uC790\uCCB4\uB97C \uD3D0\uAE30\uD558\uBA74 \uB2E4\uB978 \uAE30\uAE30\uC5D0\uC11C\uB3C4 \uBB34\uD6A8\uAC00 \uB429\uB2C8\uB2E4."));
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
// src/commands/publish.ts
|
|
4205
|
+
import pc15 from "picocolors";
|
|
4206
|
+
function parseOnOff(v) {
|
|
4207
|
+
const s = v.toLowerCase();
|
|
4208
|
+
if (s === "on") return true;
|
|
4209
|
+
if (s === "off") return false;
|
|
4210
|
+
return fail(ExitCode.PRECONDITION, `publish \uC778\uC790\uB294 on \uB610\uB294 off \uC5EC\uC57C \uD569\uB2C8\uB2E4 (\uBC1B\uC740 \uAC12: ${v})`);
|
|
4211
|
+
}
|
|
4212
|
+
function isPublished(p4) {
|
|
4213
|
+
return p4.enabled !== false;
|
|
4214
|
+
}
|
|
4215
|
+
async function confirmToggle(toChange, enabled, options, deps) {
|
|
4216
|
+
if (options.yes) return;
|
|
4217
|
+
const names = toChange.map(placementLabel).join(", ");
|
|
4218
|
+
const ok = await askConfirm(deps, `${names} \uAC8C\uC2DC\uB97C ${enabled ? "\uCF2D\uB2C8\uB2E4" : "\uB055\uB2C8\uB2E4"}. \uC9C4\uD589\uD560\uAE4C\uC694?`);
|
|
4219
|
+
if (!ok) fail(ExitCode.PRECONDITION, "\uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4.");
|
|
4220
|
+
}
|
|
4221
|
+
async function togglePlacement(ctx, p4, enabled) {
|
|
4222
|
+
const base = { id: p4.id, label: placementLabel(p4) };
|
|
4223
|
+
if (isPublished(p4) === enabled) {
|
|
4224
|
+
return { ...base, enabled, status: "unchanged", message: "\uBCC0\uACBD \uC5C6\uC74C" };
|
|
4225
|
+
}
|
|
4226
|
+
try {
|
|
4227
|
+
const { organizationId: org, spaceId: space } = ctx.link;
|
|
4228
|
+
const updated = await ctx.client.setPlacementEnabled(org, space, p4.id, enabled);
|
|
4229
|
+
return { ...base, enabled: updated.enabled !== false, status: "changed" };
|
|
4230
|
+
} catch (err) {
|
|
4231
|
+
if (err instanceof PortalError && err.status === 409) {
|
|
4232
|
+
return { ...base, enabled: isPublished(p4), status: "conflict", message: err.message };
|
|
4233
|
+
}
|
|
4234
|
+
return {
|
|
4235
|
+
...base,
|
|
4236
|
+
enabled: isPublished(p4),
|
|
4237
|
+
status: "failed",
|
|
4238
|
+
message: err instanceof Error ? err.message : String(err)
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
function printResults2(results, enabled, out) {
|
|
4243
|
+
for (const r of results) {
|
|
4244
|
+
const mark = r.status === "failed" ? pc15.red("\u2717") : r.status === "conflict" ? pc15.yellow("!") : pc15.green("\u2713");
|
|
4245
|
+
const stateLabel = r.status === "unchanged" ? "\uBCC0\uACBD \uC5C6\uC74C" : `\uAC8C\uC2DC ${r.enabled ? "on" : "off"}`;
|
|
4246
|
+
const suffix = r.message && r.status !== "unchanged" ? pc15.dim(` ${r.message}`) : "";
|
|
4247
|
+
out(`${mark} ${r.label} ${stateLabel}${suffix}`);
|
|
4248
|
+
}
|
|
4249
|
+
if (!enabled) {
|
|
4250
|
+
out(pc15.dim(" \uB77C\uC774\uBE0C \uBC84\uC804 \uD3EC\uC778\uD130\uB294 \uC720\uC9C0\uB429\uB2C8\uB2E4 \u2014 \uB2E4\uC2DC \uCF1C\uBA74 \uAC19\uC740 \uBE4C\uB4DC\uAC00 \uC989\uC2DC \uC11C\uBE59\uB429\uB2C8\uB2E4."));
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
function reportOutcome2(results, options, enabled, out) {
|
|
4254
|
+
if (options.json) printJson(jsonSummary(results));
|
|
4255
|
+
else printResults2(results, enabled, out);
|
|
4256
|
+
const conflicts = results.filter((r) => r.status === "conflict");
|
|
4257
|
+
const errors = results.filter((r) => r.status === "failed");
|
|
4258
|
+
if (conflicts.length > 0) {
|
|
4259
|
+
fail(
|
|
4260
|
+
ExitCode.CONFLICT,
|
|
4261
|
+
`\uB2E4\uB978 \uBCC0\uACBD\uC774 \uBA3C\uC800 \uC801\uC6A9\uB41C \uBC30\uCE58\uAC00 \uC788\uC2B5\uB2C8\uB2E4: ${conflicts.map((r) => r.label).join(", ")}. bstage list \uB85C \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.`
|
|
4262
|
+
);
|
|
4263
|
+
}
|
|
4264
|
+
if (errors.length > 0) {
|
|
4265
|
+
fail(
|
|
4266
|
+
ExitCode.FAILURE,
|
|
4267
|
+
`\uC77C\uBD80 \uBC30\uCE58 \uC801\uC6A9\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4: ${errors.map((r) => r.label).join(", ")}`
|
|
4268
|
+
);
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
async function publishCommand(state, options, deps = {}) {
|
|
4272
|
+
if (options.json && !options.yes) {
|
|
4273
|
+
fail(ExitCode.PRECONDITION, "--json \uC740 --yes \uC640 \uD568\uAED8 \uC4F0\uC138\uC694.");
|
|
4274
|
+
}
|
|
4275
|
+
const enabled = parseOnOff(state);
|
|
4276
|
+
if (!options.yes) assertCanConfirm(deps);
|
|
4277
|
+
const out = deps.out ?? console.log;
|
|
4278
|
+
const ctx = await resolveContext({ cwd: deps.cwd, env: deps.env, fetch: deps.fetch });
|
|
4279
|
+
const targets = await fetchTargets(ctx, options.placement);
|
|
4280
|
+
const toChange = targets.filter((p4) => isPublished(p4) !== enabled);
|
|
4281
|
+
if (toChange.length > 0) await confirmToggle(toChange, enabled, options, deps);
|
|
4282
|
+
const results = [];
|
|
4283
|
+
for (const p4 of targets) {
|
|
4284
|
+
results.push(await togglePlacement(ctx, p4, enabled));
|
|
4285
|
+
}
|
|
4286
|
+
reportOutcome2(results, options, enabled, out);
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
// src/commands/rollback.ts
|
|
4290
|
+
import pc16 from "picocolors";
|
|
4291
|
+
function pickRollbackTarget(builds, repo, currentLive) {
|
|
4292
|
+
const ok = builds.filter((b) => buildMatchesRepo(b, repo) && b.status === "SUCCEEDED").sort((a, b) => a.requestedAt < b.requestedAt ? 1 : -1);
|
|
4293
|
+
if (!currentLive) return ok[0] ?? null;
|
|
4294
|
+
const idx = ok.findIndex((b) => b.id === currentLive);
|
|
4295
|
+
if (idx === -1) return ok[0] ?? null;
|
|
4296
|
+
return ok[idx + 1] ?? null;
|
|
4297
|
+
}
|
|
4298
|
+
async function resolveExplicitTarget(ctx, builds, repo, buildId) {
|
|
4299
|
+
const { organizationId: org, spaceId: space } = ctx.link;
|
|
4300
|
+
let target = builds.find((b) => b.id === buildId);
|
|
4301
|
+
if (!target) {
|
|
4302
|
+
try {
|
|
4303
|
+
target = await ctx.client.getBuild(org, space, buildId);
|
|
4304
|
+
} catch (err) {
|
|
4305
|
+
if (err instanceof PortalError && err.status === 404) {
|
|
4306
|
+
fail(
|
|
4307
|
+
ExitCode.PRECONDITION,
|
|
4308
|
+
`\uBE4C\uB4DC ${buildId} \uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. bstage list \uB85C \uCD5C\uADFC \uBE4C\uB4DC\uB97C \uD655\uC778\uD558\uC138\uC694.`
|
|
4309
|
+
);
|
|
4310
|
+
}
|
|
4311
|
+
throw err;
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
if (target.status !== "SUCCEEDED" || !buildMatchesRepo(target, repo)) {
|
|
4315
|
+
fail(ExitCode.PRECONDITION, `\uBE4C\uB4DC ${buildId} \uB294 \uC774 \uB808\uD3EC\uC758 \uC131\uACF5\uD55C \uBE4C\uB4DC\uAC00 \uC544\uB2D9\uB2C8\uB2E4.`);
|
|
4316
|
+
}
|
|
4317
|
+
return target;
|
|
4318
|
+
}
|
|
4319
|
+
function pickOrFail(builds, repo, currentLive, label) {
|
|
4320
|
+
const target = pickRollbackTarget(builds, repo, currentLive);
|
|
4321
|
+
if (!target) fail(ExitCode.PRECONDITION, `${label}: \uB418\uB3CC\uB9B4 \uC774\uC804 \uC131\uACF5 \uBE4C\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`);
|
|
4322
|
+
return target;
|
|
4323
|
+
}
|
|
4324
|
+
async function buildRollbackPlan(ctx, placements, builds, repo, buildId) {
|
|
4325
|
+
const sharedTarget = buildId ? await resolveExplicitTarget(ctx, builds, repo, buildId) : void 0;
|
|
4326
|
+
return placements.map((pl) => {
|
|
4327
|
+
const target = sharedTarget ?? pickOrFail(builds, repo, pl.liveBuildId, placementLabel(pl));
|
|
4328
|
+
return {
|
|
4329
|
+
placement: pl,
|
|
4330
|
+
label: placementLabel(pl),
|
|
4331
|
+
from: pl.liveBuildId || void 0,
|
|
4332
|
+
to: target.id,
|
|
4333
|
+
unchanged: pl.liveBuildId === target.id,
|
|
4334
|
+
targetCommit: target.commitSha
|
|
4335
|
+
};
|
|
4336
|
+
});
|
|
4337
|
+
}
|
|
4338
|
+
function renderConfirmTable(changing) {
|
|
4339
|
+
const header = `\uBE4C\uB4DC ${changing[0].to} (\uCEE4\uBC0B ${shortSha(changing[0].targetCommit)})`;
|
|
4340
|
+
const rows = table([
|
|
4341
|
+
["\uC790\uB9AC", "\uD604\uC7AC", "\u2192 \uB418\uB3CC\uB9B4 \uBE4C\uB4DC"],
|
|
4342
|
+
...changing.map((r) => [r.label, shortSha(r.placement.liveCommit), shortSha(r.targetCommit)])
|
|
4343
|
+
]);
|
|
4344
|
+
return `${header}
|
|
4345
|
+
${rows}`;
|
|
4346
|
+
}
|
|
4347
|
+
async function confirmRollback(changing, out, deps) {
|
|
4348
|
+
out(renderConfirmTable(changing));
|
|
4349
|
+
const ok = await askConfirm(deps, `${changing.length}\uAC1C \uBC30\uCE58\uB97C \uB418\uB3CC\uB9BD\uB2C8\uB2E4. \uC9C4\uD589\uD560\uAE4C\uC694?`);
|
|
4350
|
+
if (!ok) fail(ExitCode.PRECONDITION, "\uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4.");
|
|
4351
|
+
}
|
|
4352
|
+
async function rollbackCommand(buildId, options, deps = {}) {
|
|
4353
|
+
if (options.json && !options.yes) {
|
|
4354
|
+
fail(ExitCode.PRECONDITION, "--json \uC740 --yes \uC640 \uD568\uAED8 \uC4F0\uC138\uC694.");
|
|
4355
|
+
}
|
|
4356
|
+
if (!options.yes) assertCanConfirm(deps);
|
|
4357
|
+
const out = deps.out ?? console.log;
|
|
4358
|
+
const ctx = await resolveContext({ cwd: deps.cwd, env: deps.env, fetch: deps.fetch });
|
|
4359
|
+
const { organizationId: org, spaceId: space } = ctx.link;
|
|
4360
|
+
const [placements, builds, repo] = await Promise.all([
|
|
4361
|
+
fetchTargets(ctx, options.placement),
|
|
4362
|
+
ctx.client.listBuilds(org, space, 50),
|
|
4363
|
+
resolveLinkedRepo(ctx)
|
|
4364
|
+
]);
|
|
4365
|
+
const plan = await buildRollbackPlan(ctx, placements, builds, repo, buildId);
|
|
4366
|
+
const changing = plan.filter((r) => !r.unchanged);
|
|
4367
|
+
if (changing.length === 0) {
|
|
4368
|
+
fail(ExitCode.PRECONDITION, "\uC774\uBBF8 \uD574\uB2F9 \uBE4C\uB4DC\uAC00 \uB77C\uC774\uBE0C\uC785\uB2C8\uB2E4.");
|
|
4369
|
+
}
|
|
4370
|
+
if (!options.yes) await confirmRollback(changing, out, deps);
|
|
4371
|
+
const results = await applyPlacements(ctx, plan);
|
|
4372
|
+
reportOutcome(results, options, out);
|
|
4373
|
+
if (!options.json) {
|
|
4374
|
+
out(
|
|
4375
|
+
pc16.dim(" \uB418\uB3CC\uB9B0 \uB4A4\uC5D0\uB3C4 \uCD5C\uC2E0 \uBE4C\uB4DC\uB294 \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4 \u2014 bstage deploy \uB85C \uB2E4\uC2DC \uC62C\uB9B4 \uC218 \uC788\uC2B5\uB2C8\uB2E4.")
|
|
4376
|
+
);
|
|
4377
|
+
}
|
|
2714
4378
|
}
|
|
2715
4379
|
|
|
2716
4380
|
// src/commands/docs.ts
|
|
2717
4381
|
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
2718
|
-
import { join as
|
|
2719
|
-
import
|
|
4382
|
+
import { join as join8 } from "path";
|
|
4383
|
+
import pc17 from "picocolors";
|
|
2720
4384
|
var DOC_ORDER = [
|
|
2721
4385
|
["GETTING_STARTED.md", "\uBE60\uB978 \uC2DC\uC791 \uAC00\uC774\uB4DC"],
|
|
2722
4386
|
["API_REFERENCE.md", "createTemplate, hooks, BstageClient, PlatformBridge API"],
|
|
@@ -2733,10 +4397,10 @@ var DOC_ORDER = [
|
|
|
2733
4397
|
["SDK_ARCHITECTURE.md", "\uD328\uD0A4\uC9C0 \uAD6C\uC870, \uC124\uACC4 \uACB0\uC815"]
|
|
2734
4398
|
];
|
|
2735
4399
|
async function docsCommand() {
|
|
2736
|
-
const docsDir =
|
|
4400
|
+
const docsDir = join8(process.cwd(), "node_modules", "@bstage-sdk", "core", "docs");
|
|
2737
4401
|
if (!existsSync4(docsDir)) {
|
|
2738
|
-
console.log(
|
|
2739
|
-
console.log(
|
|
4402
|
+
console.log(pc17.yellow("SDK \uBB38\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4403
|
+
console.log(pc17.dim(" @bstage-sdk/core\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694 (npm install \uD6C4 \uC7AC\uC2DC\uB3C4)."));
|
|
2740
4404
|
return;
|
|
2741
4405
|
}
|
|
2742
4406
|
const present = new Set(readdirSync(docsDir).filter((f) => f.endsWith(".md")));
|
|
@@ -2744,27 +4408,119 @@ async function docsCommand() {
|
|
|
2744
4408
|
const known = new Set(DOC_ORDER.map(([f]) => f));
|
|
2745
4409
|
const extras = [...present].filter((f) => !known.has(f)).sort();
|
|
2746
4410
|
console.log("");
|
|
2747
|
-
console.log(
|
|
2748
|
-
console.log(
|
|
4411
|
+
console.log(pc17.bold("bstage SDK \uBB38\uC11C"));
|
|
4412
|
+
console.log(pc17.dim(` ${docsDir}`));
|
|
2749
4413
|
console.log("");
|
|
2750
4414
|
for (const [file, desc] of ordered) {
|
|
2751
|
-
console.log(` ${
|
|
4415
|
+
console.log(` ${pc17.cyan(file.padEnd(22))} ${pc17.dim(desc)}`);
|
|
2752
4416
|
}
|
|
2753
4417
|
for (const file of extras) {
|
|
2754
|
-
console.log(` ${
|
|
4418
|
+
console.log(` ${pc17.cyan(file.padEnd(22))}`);
|
|
2755
4419
|
}
|
|
2756
4420
|
console.log("");
|
|
2757
4421
|
console.log(
|
|
2758
|
-
|
|
4422
|
+
pc17.dim("\uD30C\uC77C\uC744 \uC5F4\uC5B4 \uC0C1\uC138 \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uC138\uC694. \uCEE4\uB9E8\uB4DC \uB3C4\uC6C0\uB9D0\uC740 `bstage <command> --help`.")
|
|
2759
4423
|
);
|
|
2760
4424
|
console.log("");
|
|
2761
4425
|
}
|
|
2762
4426
|
|
|
4427
|
+
// src/commands/whoami.ts
|
|
4428
|
+
import pc18 from "picocolors";
|
|
4429
|
+
function jsonEntry(c, active) {
|
|
4430
|
+
return {
|
|
4431
|
+
organizationId: c.organizationId,
|
|
4432
|
+
permission: c.permission ?? null,
|
|
4433
|
+
allSpaces: c.allSpaces ?? false,
|
|
4434
|
+
spaceIds: c.spaceIds ?? [],
|
|
4435
|
+
expiresAt: c.expiresAt ?? null,
|
|
4436
|
+
savedAt: c.savedAt,
|
|
4437
|
+
active
|
|
4438
|
+
};
|
|
4439
|
+
}
|
|
4440
|
+
function printCredentials(stored, activeOrg) {
|
|
4441
|
+
if (stored.length === 0) return;
|
|
4442
|
+
console.log(pc18.dim("\uC800\uC7A5\uB41C \uD1A0\uD070"));
|
|
4443
|
+
const rows = stored.map((c) => [
|
|
4444
|
+
` \uC870\uC9C1 ${orgLabel(c.organizationId)}`,
|
|
4445
|
+
`\uB4F1\uAE09 ${c.permission ?? "-"}`,
|
|
4446
|
+
scopeLabel(c.allSpaces, c.spaceIds),
|
|
4447
|
+
`\uB9CC\uB8CC ${c.expiresAt ? c.expiresAt.slice(0, 10) : "-"}`,
|
|
4448
|
+
c.organizationId === activeOrg ? pc18.dim("(\uC0AC\uC6A9 \uC911)") : ""
|
|
4449
|
+
]);
|
|
4450
|
+
console.log(table(rows));
|
|
4451
|
+
}
|
|
4452
|
+
function printMe(me, portalUrl) {
|
|
4453
|
+
console.log(`${pc18.bold(me.user.email)} ${pc18.dim(portalUrl)}`);
|
|
4454
|
+
for (const m of me.memberships) {
|
|
4455
|
+
const stages = Object.entries(m.stageRoles ?? {}).map(([s, r]) => `${s}:${r}`).join(", ");
|
|
4456
|
+
console.log(` \uC870\uC9C1 ${m.organizationId} ${m.organizationRole ?? ""} ${pc18.dim(stages)}`);
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
async function whoamiCommand(options, deps = {}) {
|
|
4460
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
4461
|
+
const env2 = deps.env ?? process.env;
|
|
4462
|
+
const portalUrl = await resolvePortalUrl(options.portal, cwd, env2, options.phase);
|
|
4463
|
+
if (!portalUrl) {
|
|
4464
|
+
fail(
|
|
4465
|
+
ExitCode.PRECONDITION,
|
|
4466
|
+
"\uD3EC\uD138 \uC8FC\uC18C\uB97C \uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --portal <url> \uB610\uB294 --phase \uB97C \uC9C0\uC815\uD558\uC138\uC694."
|
|
4467
|
+
);
|
|
4468
|
+
}
|
|
4469
|
+
const stored = await listCredentials(portalUrl, env2);
|
|
4470
|
+
const organizationId = options.org ?? env2.BSTAGE_ORG;
|
|
4471
|
+
const picked = await loadCredential(portalUrl, organizationId, env2);
|
|
4472
|
+
const envToken = env2.BSTAGE_TOKEN || void 0;
|
|
4473
|
+
const viaEnv = Boolean(envToken);
|
|
4474
|
+
const token = envToken ?? picked?.token;
|
|
4475
|
+
const activeOrg = viaEnv ? void 0 : picked?.organizationId;
|
|
4476
|
+
if (!token) {
|
|
4477
|
+
if (stored.length === 0) {
|
|
4478
|
+
fail(
|
|
4479
|
+
ExitCode.AUTH,
|
|
4480
|
+
`${portalUrl} \uC5D0 \uB85C\uADF8\uC778\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. bstage login --portal ${portalUrl} \uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.`
|
|
4481
|
+
);
|
|
4482
|
+
}
|
|
4483
|
+
return reportStoredOnly(portalUrl, stored, options.json);
|
|
4484
|
+
}
|
|
4485
|
+
const me = await new PortalClient({ portalUrl, token, fetch: deps.fetch }).me();
|
|
4486
|
+
if (options.json) {
|
|
4487
|
+
printJson({
|
|
4488
|
+
portalUrl,
|
|
4489
|
+
tokenSource: viaEnv ? "env" : "stored",
|
|
4490
|
+
organizationId: activeOrg ?? null,
|
|
4491
|
+
user: me.user,
|
|
4492
|
+
memberships: me.memberships,
|
|
4493
|
+
credentials: stored.map((c) => jsonEntry(c, !viaEnv && c.organizationId === activeOrg))
|
|
4494
|
+
});
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
printMe(me, portalUrl);
|
|
4498
|
+
if (viaEnv) console.error(" \uD1A0\uD070 \uCD9C\uCC98: BSTAGE_TOKEN \uD658\uACBD\uBCC0\uC218 (\uC800\uC7A5\uB41C \uC790\uACA9\uC99D\uBA85\uC740 \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uC74C)");
|
|
4499
|
+
printCredentials(stored, activeOrg);
|
|
4500
|
+
}
|
|
4501
|
+
function reportStoredOnly(portalUrl, stored, json) {
|
|
4502
|
+
if (json) {
|
|
4503
|
+
printJson({
|
|
4504
|
+
portalUrl,
|
|
4505
|
+
organizationId: null,
|
|
4506
|
+
user: null,
|
|
4507
|
+
memberships: [],
|
|
4508
|
+
credentials: stored.map((c) => jsonEntry(c, false))
|
|
4509
|
+
});
|
|
4510
|
+
return;
|
|
4511
|
+
}
|
|
4512
|
+
console.log(pc18.dim(portalUrl));
|
|
4513
|
+
printCredentials(stored);
|
|
4514
|
+
console.log(
|
|
4515
|
+
pc18.yellow(" \uC5EC\uB7EC \uC870\uC9C1\uC758 \uD1A0\uD070\uC774 \uC788\uC2B5\uB2C8\uB2E4 \u2014 --org <id> \uB85C \uC870\uC9C1\uC744 \uC9C0\uC815\uD558\uBA74 \uACC4\uC815 \uC815\uBCF4\uB3C4 \uBD05\uB2C8\uB2E4.")
|
|
4516
|
+
);
|
|
4517
|
+
}
|
|
4518
|
+
|
|
2763
4519
|
// src/index.ts
|
|
2764
4520
|
function readVersion() {
|
|
2765
4521
|
try {
|
|
2766
|
-
const here =
|
|
2767
|
-
return JSON.parse(readFileSync2(
|
|
4522
|
+
const here = dirname5(fileURLToPath2(import.meta.url));
|
|
4523
|
+
return JSON.parse(readFileSync2(join9(here, "..", "package.json"), "utf-8")).version || "0.0.0";
|
|
2768
4524
|
} catch {
|
|
2769
4525
|
return "0.0.0";
|
|
2770
4526
|
}
|
|
@@ -2782,6 +4538,28 @@ var skills = program.command("skills").description("\uC5D0\uC774\uC804\uD2B8\uC6
|
|
|
2782
4538
|
skills.command("install").description("\uC5D0\uC774\uC804\uD2B8\uC6A9 \uC2A4\uD0AC\uC744 .claude/skills/\uC5D0 \uC124\uCE58\xB7\uB3D9\uAE30\uD654 (\uAE30\uC874 \uD504\uB85C\uC81D\uD2B8\uC6A9)").option("--dir <dir>", "\uC124\uCE58 \uC704\uCE58", ".claude/skills").action(skillsInstallCommand);
|
|
2783
4539
|
var i18n = program.command("i18n").description("\uB2E4\uAD6D\uC5B4(\uBC88\uC5ED) \uC720\uD2F8");
|
|
2784
4540
|
i18n.command("pull").description("\uD50C\uB7AB\uD3FC \uBC88\uC5ED \uC0AC\uC804\uC744 CDN\uC5D0\uC11C \uBC1B\uC544 \uD0C0\uC785 \uC0DD\uC131 + \uB85C\uCEEC \uCE90\uC2DC").option("--phase <phase>", "\uB300\uC0C1 phase (real, qa, dev)", "real").option("--target <target>", "\uBC88\uC5ED \uC2DC\uC2A4\uD15C (user, admin)", "user").option("--tier <tier>", "\uBC88\uC5ED tier (inhouse, mnetplus, hiand)", "inhouse").option("--ref <locale>", "\uD0C0\uC785 \uC6D0\uBB38 \uAE30\uC900 \uB85C\uCF00\uC77C", "ko").option("--out <file>", "\uC0DD\uC131 \uD0C0\uC785 \uD30C\uC77C \uACBD\uB85C", "src/bstage-i18n.ts").action(i18nPullCommand);
|
|
4541
|
+
program.command("login").description("\uD3EC\uD138 CLI \uD1A0\uD070 \uB4F1\uB85D (\uD3EC\uD138 \uC124\uC815 > CLI \uD1A0\uD070\uC5D0\uC11C \uBC1C\uAE09)").option(
|
|
4542
|
+
"--phase <phase>",
|
|
4543
|
+
"\uD3EC\uD138 \uD658\uACBD (real, sandbox, dev, qa). \uC0DD\uB7B5 \uC2DC .env\uC758 VITE_BSTAGE_PHASE, \uADF8\uAC83\uB3C4 \uC5C6\uC73C\uBA74 sandbox"
|
|
4544
|
+
).option(
|
|
4545
|
+
"--portal <url>",
|
|
4546
|
+
"\uD3EC\uD138 \uC8FC\uC18C \uC9C1\uC811 \uC9C0\uC815 (phase \uB9E4\uD551\uBCF4\uB2E4 \uC6B0\uC120, BSTAGE_PORTAL_URL). \uB9C1\uD06C \uD30C\uC77C\xB7.env \uC758 \uC8FC\uC18C\uB294 \uD5C8\uC6A9 \uBAA9\uB85D \uC548\uC77C \uB54C\uB9CC \uC4F0\uC774\uBBC0\uB85C \uBAA9\uB85D \uBC16 \uD3EC\uD138\uC740 \uC774 \uC635\uC158\uC73C\uB85C \uC9C0\uC815\uD55C\uB2E4"
|
|
4547
|
+
).option(
|
|
4548
|
+
"--token <token>",
|
|
4549
|
+
"\uBD99\uC5EC \uB123\uAE30 \uACBD\uB85C \u2014 CI\uC6A9 (BSTAGE_TOKEN). \uC0DD\uB7B5 \uC2DC \uBE0C\uB77C\uC6B0\uC800 \uC2B9\uC778(\uB514\uBC14\uC774\uC2A4 \uCF54\uB4DC)"
|
|
4550
|
+
).option("--org <id>", "\uBD99\uC5EC \uB123\uC740 \uD1A0\uD070\uC758 \uC870\uC9C1 (BSTAGE_ORG). \uB514\uBC14\uC774\uC2A4 \uC2B9\uC778 \uACBD\uB85C\uC5D0\uC11C\uB294 \uBD88\uD544\uC694").option("--no-browser", "\uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4\uC9C0 \uC54A\uACE0 \uCF54\uB4DC\xB7\uC8FC\uC18C\uB9CC \uCD9C\uB825").option("--json", "\uAD6C\uC870\uD654 \uCD9C\uB825").action(
|
|
4551
|
+
(o) => runCommand(
|
|
4552
|
+
() => loginCommand({ ...o, noBrowser: o.browser === false }, { cliVersion: readVersion() })
|
|
4553
|
+
)
|
|
4554
|
+
);
|
|
4555
|
+
program.command("logout").description("\uC800\uC7A5\uB41C \uD3EC\uD138 \uC790\uACA9\uC99D\uBA85 \uC0AD\uC81C (\uAE30\uBCF8: \uADF8 \uD3EC\uD138\uC758 \uBAA8\uB4E0 \uC870\uC9C1)").option("--phase <phase>", "\uD3EC\uD138 \uD658\uACBD").option("--portal <url>", "\uD3EC\uD138 \uC8FC\uC18C \uC9C1\uC811 \uC9C0\uC815 (\uD5C8\uC6A9 \uBAA9\uB85D \uBC16 \uD3EC\uD138\uC740 \uC774 \uC635\uC158\uC73C\uB85C)").option("--org <id>", "\uC774 \uC870\uC9C1 \uD1A0\uD070\uB9CC \uC0AD\uC81C").action((o) => runCommand(() => logoutCommand(o)));
|
|
4556
|
+
program.command("link").description("\uC774 \uB514\uB809\uD130\uB9AC\uB97C \uD3EC\uD138 \uC870\uC9C1\xB7\uC2A4\uD14C\uC774\uC9C0\xB7\uB808\uD3EC\uC5D0 \uC5F0\uACB0 (.bstage/project.json)").option("--phase <phase>", "\uD3EC\uD138 \uD658\uACBD (\uC0DD\uB7B5 \uC2DC .env\uC758 VITE_BSTAGE_PHASE \u2192 sandbox)").option("--portal <url>", "\uD3EC\uD138 \uC8FC\uC18C \uC9C1\uC811 \uC9C0\uC815 (\uD5C8\uC6A9 \uBAA9\uB85D \uBC16 \uD3EC\uD138\uC740 \uC774 \uC635\uC158\uC73C\uB85C)").option("--org <id>", "\uC870\uC9C1 ID").option("--space <id>", "\uC2A4\uD14C\uC774\uC9C0(\uC2A4\uD398\uC774\uC2A4) ID").option("--repo <id|owner/repo>", "\uC5F0\uACB0 \uB808\uD3EC").option("-y, --yes", "\uD504\uB86C\uD504\uD2B8 \uC5C6\uC774 (\uD6C4\uBCF4\uAC00 \uC720\uC77C\uD558\uAC70\uB098 \uC635\uC158\uC73C\uB85C \uC9C0\uC815\uB3FC\uC57C \uD568)").option("--json", "\uAD6C\uC870\uD654 \uCD9C\uB825 (--yes \uC640 \uD568\uAED8)").action((o) => runCommand(() => linkCommand(o)));
|
|
4557
|
+
program.command("list").alias("ls").description("\uC774 \uB808\uD3EC\uC758 \uBC30\uCE58(\uB77C\uC774\uBE0C \uBC84\uC804\xB7\uAC8C\uC2DC \uC0C1\uD0DC)\uC640 \uCD5C\uADFC \uBE4C\uB4DC").option("--builds <n>", "\uD45C\uC2DC\uD560 \uBE4C\uB4DC \uC218", "5").option("--json", "\uAD6C\uC870\uD654 \uCD9C\uB825").action((o) => runCommand(() => listCommand(o)));
|
|
4558
|
+
program.command("deploy").description("push\uB41C \uCEE4\uBC0B\uC744 \uD3EC\uD138\uC5D0\uC11C \uBE4C\uB4DC\uD558\uACE0 \uC774 \uB808\uD3EC\uC758 \uBC30\uCE58\uC5D0 \uB77C\uC774\uBE0C \uC801\uC6A9").option("-y, --yes", "\uD655\uC778 \uC5C6\uC774 \uC801\uC6A9").option("--placement <id|path|slotId>", "\uD2B9\uC815 \uBC30\uCE58\uB9CC").option("--skip-git-check", "\uB354\uD2F0 \uD2B8\uB9AC\xB7\uBE0C\uB79C\uCE58\xB7push \uC810\uAC80 \uC0DD\uB7B5").option("--no-wait", "\uBE4C\uB4DC\uB9CC \uC2DC\uC791\uD558\uACE0 \uC885\uB8CC").option("--json", "\uAD6C\uC870\uD654 \uCD9C\uB825 (--yes \uC640 \uD568\uAED8)").action((o) => runCommand(() => deployCommand({ ...o, noWait: o.wait === false })));
|
|
4559
|
+
program.command("rollback [buildId]").description("\uC774\uC804 \uC131\uACF5 \uBE4C\uB4DC(\uB610\uB294 \uC9C0\uC815\uD55C \uBE4C\uB4DC)\uB97C \uB77C\uC774\uBE0C\uB85C \uC7AC\uC801\uC6A9").option("-y, --yes", "\uD655\uC778 \uC5C6\uC774 \uC801\uC6A9").option("--placement <id|path|slotId>", "\uD2B9\uC815 \uBC30\uCE58\uB9CC").option("--json", "\uAD6C\uC870\uD654 \uCD9C\uB825 (--yes \uC640 \uD568\uAED8)").action((id, o) => runCommand(() => rollbackCommand(id, o)));
|
|
4560
|
+
program.command("publish <on|off>").description("\uBC30\uCE58 \uAC8C\uC2DC \uCF1C\uAE30/\uB044\uAE30 (\uB77C\uC774\uBE0C \uBC84\uC804\uC740 \uC720\uC9C0)").option("--placement <id|path|slotId>", "\uD2B9\uC815 \uBC30\uCE58\uB9CC").option("-y, --yes", "\uD655\uC778 \uC5C6\uC774").option("--json", "\uAD6C\uC870\uD654 \uCD9C\uB825 (--yes \uC640 \uD568\uAED8)").action((state, o) => runCommand(() => publishCommand(state, o)));
|
|
4561
|
+
program.command("logs [buildId]").description("\uBE4C\uB4DC \uB85C\uADF8 (\uC0DD\uB7B5 \uC2DC \uCD5C\uADFC \uBE4C\uB4DC)").option("-f, --follow", "\uC9C4\uD589 \uC911\uC774\uBA74 \uB05D\uB0A0 \uB54C\uAE4C\uC9C0 \uC774\uC5B4\uC11C \uCD9C\uB825").option("--json", "\uAD6C\uC870\uD654 \uCD9C\uB825").action((id, o) => runCommand(() => logsCommand(id, o)));
|
|
4562
|
+
program.command("whoami").description("\uD604\uC7AC \uB85C\uADF8\uC778 \uC0AC\uC6A9\uC790\xB7\uC870\uC9C1\xB7\uC2A4\uD14C\uC774\uC9C0 \uC5ED\uD560\uACFC \uC800\uC7A5\uB41C \uC870\uC9C1\uBCC4 \uD1A0\uD070").option("--phase <phase>", "\uD3EC\uD138 \uD658\uACBD").option("--portal <url>", "\uD3EC\uD138 \uC8FC\uC18C \uC9C1\uC811 \uC9C0\uC815 (\uD5C8\uC6A9 \uBAA9\uB85D \uBC16 \uD3EC\uD138\uC740 \uC774 \uC635\uC158\uC73C\uB85C)").option("--org <id>", "\uC774 \uC870\uC9C1 \uD1A0\uD070\uC73C\uB85C \uC870\uD68C (BSTAGE_ORG)").option("--json", "\uAD6C\uC870\uD654 \uCD9C\uB825").action((o) => runCommand(() => whoamiCommand(o)));
|
|
2785
4563
|
program.addHelpText(
|
|
2786
4564
|
"after",
|
|
2787
4565
|
`
|
|
@@ -2792,6 +4570,17 @@ program.addHelpText(
|
|
|
2792
4570
|
$ bstage doctor SDK \uBC84\uC804\xB7\uB4DC\uB9AC\uD504\uD2B8 \uC9C4\uB2E8
|
|
2793
4571
|
$ bstage skills install \uC5D0\uC774\uC804\uD2B8 \uC2A4\uD0AC \uC124\uCE58/\uB3D9\uAE30\uD654
|
|
2794
4572
|
$ bstage i18n pull \uD50C\uB7AB\uD3FC \uBC88\uC5ED \uBC1B\uAE30 + \uD0C0\uC785 \uC0DD\uC131
|
|
4573
|
+
$ bstage login \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC2B9\uC778\uD574 \uB85C\uADF8\uC778 (CI: --token \uB610\uB294 BSTAGE_TOKEN)
|
|
4574
|
+
$ bstage link \uB514\uB809\uD130\uB9AC\uB97C \uD3EC\uD138 \uC870\uC9C1\xB7\uC2A4\uD14C\uC774\uC9C0\xB7\uB808\uD3EC\uC5D0 \uC5F0\uACB0
|
|
4575
|
+
$ bstage list \uC774 \uB808\uD3EC\uC758 \uBC30\uCE58\xB7\uCD5C\uADFC \uBE4C\uB4DC \uD655\uC778
|
|
4576
|
+
$ bstage deploy push\uB41C \uCEE4\uBC0B\uC744 \uBE4C\uB4DC\uD574 \uC774 \uB808\uD3EC\uC758 \uBC30\uCE58\uC5D0 \uB77C\uC774\uBE0C \uC801\uC6A9
|
|
4577
|
+
$ bstage rollback \uC774\uC804 \uC131\uACF5 \uBE4C\uB4DC\uB85C \uB77C\uC774\uBE0C\uB97C \uB418\uB3CC\uB9BC
|
|
4578
|
+
$ bstage publish off \uAC8C\uC2DC \uB044\uAE30 (\uB77C\uC774\uBE0C \uBC84\uC804 \uC720\uC9C0)
|
|
4579
|
+
$ bstage logs -f \uCD5C\uADFC \uBE4C\uB4DC \uB85C\uADF8\uB97C \uB05D\uB0A0 \uB54C\uAE4C\uC9C0 \uC774\uC5B4\uC11C \uCD9C\uB825
|
|
4580
|
+
$ bstage whoami \uD604\uC7AC \uB85C\uADF8\uC778 \uC0AC\uC6A9\uC790 \uD655\uC778
|
|
4581
|
+
$ bstage logout \uC800\uC7A5\uB41C \uC790\uACA9\uC99D\uBA85 \uC0AD\uC81C
|
|
4582
|
+
|
|
4583
|
+
\uC885\uB8CC \uCF54\uB4DC: 0 \uC131\uACF5 \xB7 1 \uC2E4\uD328 \xB7 2 \uC0AC\uC804\uC870\uAC74 \xB7 3 \uC778\uC99D \xB7 4 \uCDA9\uB3CC \xB7 5 \uC694\uCCAD \uC81C\uD55C
|
|
2795
4584
|
|
|
2796
4585
|
\uBB38\uC11C:
|
|
2797
4586
|
$ bstage docs \uC124\uCE58\uB41C SDK \uBB38\uC11C \uBAA9\uB85D\xB7\uACBD\uB85C
|