@simplysm/sd-cli 13.0.0-beta.46 → 13.0.0-beta.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/builders/BaseBuilder.js.map +0 -1
- package/dist/builders/DtsBuilder.js.map +0 -1
- package/dist/builders/LibraryBuilder.js.map +0 -1
- package/dist/builders/index.js.map +0 -1
- package/dist/builders/types.js.map +0 -1
- package/dist/capacitor/capacitor.js.map +0 -1
- package/dist/commands/add-client.js.map +0 -1
- package/dist/commands/add-server.js.map +0 -1
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/device.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/lint.js.map +0 -1
- package/dist/commands/publish.js.map +0 -1
- package/dist/commands/typecheck.js.map +0 -1
- package/dist/commands/watch.js.map +0 -1
- package/dist/electron/electron.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infra/ResultCollector.js.map +0 -1
- package/dist/infra/SignalHandler.js.map +0 -1
- package/dist/infra/WorkerManager.js.map +0 -1
- package/dist/infra/index.js.map +0 -1
- package/dist/orchestrators/WatchOrchestrator.js.map +0 -1
- package/dist/orchestrators/index.js.map +0 -1
- package/dist/sd-cli.js.map +0 -1
- package/dist/sd-config.types.js.map +0 -1
- package/dist/utils/build-env.js.map +0 -1
- package/dist/utils/config-editor.js.map +0 -1
- package/dist/utils/copy-src.js.map +0 -1
- package/dist/utils/esbuild-config.d.ts +1 -0
- package/dist/utils/esbuild-config.d.ts.map +1 -1
- package/dist/utils/esbuild-config.js +2 -1
- package/dist/utils/esbuild-config.js.map +1 -2
- package/dist/utils/listr-manager.js.map +0 -1
- package/dist/utils/output-utils.js.map +0 -1
- package/dist/utils/package-utils.js.map +0 -1
- package/dist/utils/replace-deps.js.map +0 -1
- package/dist/utils/sd-config.js.map +0 -1
- package/dist/utils/spawn.js.map +0 -1
- package/dist/utils/tailwind-config-deps.js.map +0 -1
- package/dist/utils/template.js.map +0 -1
- package/dist/utils/tsconfig.js.map +0 -1
- package/dist/utils/typecheck-serialization.js.map +0 -1
- package/dist/utils/vite-config.js.map +0 -1
- package/dist/utils/worker-events.js.map +0 -1
- package/dist/workers/client.worker.js.map +0 -1
- package/dist/workers/dts.worker.js.map +0 -1
- package/dist/workers/library.worker.js.map +0 -1
- package/dist/workers/server-runtime.worker.js.map +0 -1
- package/dist/workers/server.worker.js.map +0 -1
- package/package.json +5 -4
- package/src/builders/BaseBuilder.ts +141 -0
- package/src/builders/DtsBuilder.ts +138 -0
- package/src/builders/LibraryBuilder.ts +161 -0
- package/src/builders/index.ts +4 -0
- package/src/builders/types.ts +55 -0
- package/src/capacitor/capacitor.ts +827 -0
- package/src/commands/add-client.ts +135 -0
- package/src/commands/add-server.ts +150 -0
- package/src/commands/build.ts +475 -0
- package/src/commands/dev.ts +602 -0
- package/src/commands/device.ts +151 -0
- package/src/commands/init.ts +104 -0
- package/src/commands/lint.ts +216 -0
- package/src/commands/publish.ts +836 -0
- package/src/commands/typecheck.ts +329 -0
- package/src/commands/watch.ts +38 -0
- package/src/electron/electron.ts +329 -0
- package/src/index.ts +1 -0
- package/src/infra/ResultCollector.ts +81 -0
- package/src/infra/SignalHandler.ts +52 -0
- package/src/infra/WorkerManager.ts +65 -0
- package/src/infra/index.ts +3 -0
- package/src/orchestrators/WatchOrchestrator.ts +211 -0
- package/src/orchestrators/index.ts +1 -0
- package/src/sd-cli.ts +307 -0
- package/src/sd-config.types.ts +271 -0
- package/src/utils/build-env.ts +12 -0
- package/src/utils/config-editor.ts +131 -0
- package/src/utils/copy-src.ts +60 -0
- package/src/utils/esbuild-config.ts +263 -0
- package/src/utils/listr-manager.ts +89 -0
- package/src/utils/output-utils.ts +61 -0
- package/src/utils/package-utils.ts +63 -0
- package/src/utils/replace-deps.ts +163 -0
- package/src/utils/sd-config.ts +44 -0
- package/src/utils/spawn.ts +79 -0
- package/src/utils/tailwind-config-deps.ts +95 -0
- package/src/utils/template.ts +51 -0
- package/src/utils/tsconfig.ts +111 -0
- package/src/utils/typecheck-serialization.ts +82 -0
- package/src/utils/vite-config.ts +184 -0
- package/src/utils/worker-events.ts +102 -0
- package/src/workers/client.worker.ts +236 -0
- package/src/workers/dts.worker.ts +416 -0
- package/src/workers/library.worker.ts +245 -0
- package/src/workers/server-runtime.worker.ts +154 -0
- package/src/workers/server.worker.ts +435 -0
- package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
- package/templates/add-server/__SERVER__/package.json.hbs +2 -2
- package/templates/init/package.json.hbs +3 -3
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import semver from "semver";
|
|
3
|
+
import { consola, LogLevels } from "consola";
|
|
4
|
+
import { Listr, type ListrTask } from "listr2";
|
|
5
|
+
import { StorageFactory } from "@simplysm/storage";
|
|
6
|
+
import { fsExists, fsRead, fsReadJson, fsWrite, fsGlob, fsCopy } from "@simplysm/core-node";
|
|
7
|
+
import { env, jsonStringify } from "@simplysm/core-common";
|
|
8
|
+
import "@simplysm/core-common";
|
|
9
|
+
import type { SdConfig, SdPublishConfig } from "../sd-config.types";
|
|
10
|
+
import { loadSdConfig } from "../utils/sd-config";
|
|
11
|
+
import { spawn } from "../utils/spawn";
|
|
12
|
+
import { runBuild } from "./build";
|
|
13
|
+
import os from "os";
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import ssh2 from "ssh2";
|
|
16
|
+
import { password as passwordPrompt } from "@inquirer/prompts";
|
|
17
|
+
|
|
18
|
+
const { Client: SshClient, utils } = ssh2;
|
|
19
|
+
|
|
20
|
+
//#region Types
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Publish 명령 옵션
|
|
24
|
+
*/
|
|
25
|
+
export interface PublishOptions {
|
|
26
|
+
/** 배포할 패키지 필터 (빈 배열이면 publish 설정이 있는 모든 패키지) */
|
|
27
|
+
targets: string[];
|
|
28
|
+
/** 빌드 없이 배포 (위험) */
|
|
29
|
+
noBuild: boolean;
|
|
30
|
+
/** 실제 배포 없이 시뮬레이션 */
|
|
31
|
+
dryRun: boolean;
|
|
32
|
+
/** sd.config.ts에 전달할 추가 옵션 */
|
|
33
|
+
options: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* package.json 타입 (필요한 필드만)
|
|
38
|
+
*/
|
|
39
|
+
interface PackageJson {
|
|
40
|
+
name: string;
|
|
41
|
+
version: string;
|
|
42
|
+
dependencies?: Record<string, string>;
|
|
43
|
+
devDependencies?: Record<string, string>;
|
|
44
|
+
peerDependencies?: Record<string, string>;
|
|
45
|
+
optionalDependencies?: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
|
|
50
|
+
//#region Utilities
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 환경변수 치환 (%VAR% 형식)
|
|
54
|
+
* @throws 치환 결과가 빈 문자열이면 에러
|
|
55
|
+
*/
|
|
56
|
+
function replaceEnvVariables(str: string, version: string, projectPath: string): string {
|
|
57
|
+
const result = str.replace(/%([^%]+)%/g, (match, envName: string) => {
|
|
58
|
+
if (envName === "VER") {
|
|
59
|
+
return version;
|
|
60
|
+
}
|
|
61
|
+
if (envName === "PROJECT") {
|
|
62
|
+
return projectPath;
|
|
63
|
+
}
|
|
64
|
+
return (env[envName] as string | undefined) ?? match;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// 치환되지 않은 환경변수가 남아있으면 에러
|
|
68
|
+
if (/%[^%]+%/.test(result)) {
|
|
69
|
+
throw new Error(`환경변수 치환 실패: ${str} → ${result}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 카운트다운 대기
|
|
77
|
+
*/
|
|
78
|
+
async function waitWithCountdown(message: string, seconds: number): Promise<void> {
|
|
79
|
+
for (let i = seconds; i > 0; i--) {
|
|
80
|
+
if (i !== seconds && process.stdout.isTTY) {
|
|
81
|
+
process.stdout.cursorTo(0);
|
|
82
|
+
}
|
|
83
|
+
process.stdout.write(`${message} ${i}`);
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (process.stdout.isTTY) {
|
|
88
|
+
process.stdout.cursorTo(0);
|
|
89
|
+
process.stdout.clearLine(0);
|
|
90
|
+
} else {
|
|
91
|
+
process.stdout.write("\n");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* SSH 키 인증 사전 확인 및 설정
|
|
97
|
+
*
|
|
98
|
+
* pass가 없는 SFTP 서버에 대해:
|
|
99
|
+
* 1. SSH 키 파일이 없으면 생성
|
|
100
|
+
* 2. 키 인증을 테스트하고, 실패하면 비밀번호로 공개키 등록
|
|
101
|
+
*/
|
|
102
|
+
async function ensureSshAuth(
|
|
103
|
+
publishPackages: Array<{ name: string; config: SdPublishConfig }>,
|
|
104
|
+
logger: ReturnType<typeof consola.withTag>,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
// pass 없는 SFTP 서버 수집 (user@host 중복 제거)
|
|
107
|
+
const sshTargets = new Map<string, { host: string; port?: number; user: string }>();
|
|
108
|
+
for (const pkg of publishPackages) {
|
|
109
|
+
if (pkg.config === "npm") continue;
|
|
110
|
+
if (pkg.config.type !== "sftp") continue;
|
|
111
|
+
if (pkg.config.pass != null) continue;
|
|
112
|
+
if (pkg.config.user == null) {
|
|
113
|
+
throw new Error(`[${pkg.name}] SFTP 설정에 user가 없습니다.`);
|
|
114
|
+
}
|
|
115
|
+
const key = `${pkg.config.user}@${pkg.config.host}`;
|
|
116
|
+
sshTargets.set(key, {
|
|
117
|
+
host: pkg.config.host,
|
|
118
|
+
port: pkg.config.port,
|
|
119
|
+
user: pkg.config.user,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (sshTargets.size === 0) return;
|
|
124
|
+
|
|
125
|
+
// SSH 키 파일 확인/생성
|
|
126
|
+
const sshDir = path.join(os.homedir(), ".ssh");
|
|
127
|
+
const keyPath = path.join(sshDir, "id_ed25519");
|
|
128
|
+
const pubKeyPath = path.join(sshDir, "id_ed25519.pub");
|
|
129
|
+
|
|
130
|
+
if (!fs.existsSync(keyPath)) {
|
|
131
|
+
logger.info("SSH 키가 없습니다. 생성합니다...");
|
|
132
|
+
|
|
133
|
+
if (!fs.existsSync(sshDir)) {
|
|
134
|
+
fs.mkdirSync(sshDir, { mode: 0o700 });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const keyPair = utils.generateKeyPairSync("ed25519");
|
|
138
|
+
fs.writeFileSync(keyPath, keyPair.private, { mode: 0o600 });
|
|
139
|
+
fs.writeFileSync(pubKeyPath, keyPair.public + "\n", { mode: 0o644 });
|
|
140
|
+
|
|
141
|
+
logger.info(`SSH 키 생성 완료: ${keyPath}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const privateKeyData = fs.readFileSync(keyPath);
|
|
145
|
+
const publicKey = fs.readFileSync(pubKeyPath, "utf-8").trim();
|
|
146
|
+
|
|
147
|
+
// privateKey가 암호화되어 있는지 확인
|
|
148
|
+
const parsed = utils.parseKey(privateKeyData);
|
|
149
|
+
const isKeyEncrypted = parsed instanceof Error;
|
|
150
|
+
const sshAgent = process.env["SSH_AUTH_SOCK"];
|
|
151
|
+
|
|
152
|
+
// 각 서버에 대해 키 인증 확인
|
|
153
|
+
for (const [label, target] of sshTargets) {
|
|
154
|
+
const canAuth = await testSshKeyAuth(target, {
|
|
155
|
+
privateKey: isKeyEncrypted ? undefined : privateKeyData,
|
|
156
|
+
agent: sshAgent,
|
|
157
|
+
});
|
|
158
|
+
if (canAuth) {
|
|
159
|
+
logger.debug(`SSH 키 인증 확인: ${label}`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 키 인증 실패 → 비밀번호로 공개키 등록
|
|
164
|
+
logger.info(`${label}: SSH 키가 서버에 등록되어 있지 않습니다.`);
|
|
165
|
+
const pass = await passwordPrompt({
|
|
166
|
+
message: `${label} 비밀번호 (공개키 등록용):`,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await registerSshPublicKey(target, pass, publicKey);
|
|
170
|
+
logger.info(`SSH 공개키 등록 완료: ${label}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* SSH 키 인증 테스트 (접속 후 즉시 종료)
|
|
176
|
+
*/
|
|
177
|
+
function testSshKeyAuth(
|
|
178
|
+
target: { host: string; port?: number; user: string },
|
|
179
|
+
auth: { privateKey?: Buffer; agent?: string },
|
|
180
|
+
): Promise<boolean> {
|
|
181
|
+
if (auth.privateKey == null && auth.agent == null) {
|
|
182
|
+
return Promise.resolve(false);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
const conn = new SshClient();
|
|
187
|
+
conn.on("ready", () => {
|
|
188
|
+
conn.end();
|
|
189
|
+
resolve(true);
|
|
190
|
+
});
|
|
191
|
+
conn.on("error", () => {
|
|
192
|
+
resolve(false);
|
|
193
|
+
});
|
|
194
|
+
conn.connect({
|
|
195
|
+
host: target.host,
|
|
196
|
+
port: target.port ?? 22,
|
|
197
|
+
username: target.user,
|
|
198
|
+
...(auth.privateKey != null ? { privateKey: auth.privateKey } : {}),
|
|
199
|
+
...(auth.agent != null ? { agent: auth.agent } : {}),
|
|
200
|
+
readyTimeout: 10_000,
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 비밀번호로 서버에 접속하여 SSH 공개키를 등록
|
|
207
|
+
*/
|
|
208
|
+
function registerSshPublicKey(
|
|
209
|
+
target: { host: string; port?: number; user: string },
|
|
210
|
+
pass: string,
|
|
211
|
+
publicKey: string,
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const conn = new SshClient();
|
|
215
|
+
conn.on("ready", () => {
|
|
216
|
+
// authorized_keys에 공개키 추가
|
|
217
|
+
const cmd = [
|
|
218
|
+
"mkdir -p ~/.ssh",
|
|
219
|
+
"chmod 700 ~/.ssh",
|
|
220
|
+
`echo '${publicKey}' >> ~/.ssh/authorized_keys`,
|
|
221
|
+
"chmod 600 ~/.ssh/authorized_keys",
|
|
222
|
+
].join(" && ");
|
|
223
|
+
|
|
224
|
+
conn.exec(cmd, (err, stream) => {
|
|
225
|
+
if (err) {
|
|
226
|
+
conn.end();
|
|
227
|
+
reject(new Error(`SSH 명령 실행 실패: ${err.message}`));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let stderr = "";
|
|
232
|
+
stream.on("data", () => {}); // stdout 소비 (미소비 시 stream 미종료)
|
|
233
|
+
stream.stderr.on("data", (data: Uint8Array) => {
|
|
234
|
+
stderr += data.toString();
|
|
235
|
+
});
|
|
236
|
+
stream.on("exit", (code: number | null) => {
|
|
237
|
+
conn.end();
|
|
238
|
+
if (code !== 0) {
|
|
239
|
+
reject(new Error(`SSH 공개키 등록 실패 (exit code: ${code}): ${stderr}`));
|
|
240
|
+
} else {
|
|
241
|
+
resolve();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
conn.on("error", (err) => {
|
|
247
|
+
reject(new Error(`SSH 접속 실패 (${target.host}): ${err.message}`));
|
|
248
|
+
});
|
|
249
|
+
conn.connect({
|
|
250
|
+
host: target.host,
|
|
251
|
+
port: target.port ?? 22,
|
|
252
|
+
username: target.user,
|
|
253
|
+
password: pass,
|
|
254
|
+
readyTimeout: 10_000,
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
//#endregion
|
|
260
|
+
|
|
261
|
+
//#region Version Upgrade
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 프로젝트 및 패키지 버전 업그레이드
|
|
265
|
+
* @param dryRun true면 파일 수정 없이 새 버전만 계산
|
|
266
|
+
*/
|
|
267
|
+
async function upgradeVersion(
|
|
268
|
+
cwd: string,
|
|
269
|
+
allPkgPaths: string[],
|
|
270
|
+
dryRun: boolean,
|
|
271
|
+
): Promise<{ version: string; changedFiles: string[] }> {
|
|
272
|
+
const changedFiles: string[] = [];
|
|
273
|
+
const projPkgPath = path.resolve(cwd, "package.json");
|
|
274
|
+
const projPkg = await fsReadJson<PackageJson>(projPkgPath);
|
|
275
|
+
|
|
276
|
+
const currentVersion = projPkg.version;
|
|
277
|
+
const prereleaseInfo = semver.prerelease(currentVersion);
|
|
278
|
+
|
|
279
|
+
// prerelease 여부에 따라 증가 방식 결정
|
|
280
|
+
const newVersion =
|
|
281
|
+
prereleaseInfo !== null ? semver.inc(currentVersion, "prerelease")! : semver.inc(currentVersion, "patch")!;
|
|
282
|
+
|
|
283
|
+
if (dryRun) {
|
|
284
|
+
// dry-run: 파일 수정 없이 새 버전만 반환
|
|
285
|
+
return { version: newVersion, changedFiles: [] };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
projPkg.version = newVersion;
|
|
289
|
+
await fsWrite(projPkgPath, jsonStringify(projPkg, { space: 2 }) + "\n");
|
|
290
|
+
changedFiles.push(projPkgPath);
|
|
291
|
+
|
|
292
|
+
// 각 패키지 package.json 버전 설정
|
|
293
|
+
for (const pkgPath of allPkgPaths) {
|
|
294
|
+
const pkgJsonPath = path.resolve(pkgPath, "package.json");
|
|
295
|
+
const pkgJson = await fsReadJson<PackageJson>(pkgJsonPath);
|
|
296
|
+
pkgJson.version = newVersion;
|
|
297
|
+
await fsWrite(pkgJsonPath, jsonStringify(pkgJson, { space: 2 }) + "\n");
|
|
298
|
+
changedFiles.push(pkgJsonPath);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 템플릿 파일의 @simplysm 패키지 버전 동기화
|
|
302
|
+
const templateFiles = await fsGlob(path.resolve(cwd, "packages/sd-cli/templates/**/*.hbs"));
|
|
303
|
+
const versionRegex = /("@simplysm\/[^"]+"\s*:\s*)"~[^"]+"/g;
|
|
304
|
+
|
|
305
|
+
for (const templatePath of templateFiles) {
|
|
306
|
+
const content = await fsRead(templatePath);
|
|
307
|
+
const newContent = content.replace(versionRegex, `$1"~${newVersion}"`);
|
|
308
|
+
|
|
309
|
+
if (content !== newContent) {
|
|
310
|
+
await fsWrite(templatePath, newContent);
|
|
311
|
+
changedFiles.push(templatePath);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { version: newVersion, changedFiles };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
//#endregion
|
|
319
|
+
|
|
320
|
+
//#region Package Publishing
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 개별 패키지 배포
|
|
324
|
+
* @param dryRun true면 실제 배포 없이 시뮬레이션
|
|
325
|
+
*/
|
|
326
|
+
async function publishPackage(
|
|
327
|
+
pkgPath: string,
|
|
328
|
+
publishConfig: SdPublishConfig,
|
|
329
|
+
version: string,
|
|
330
|
+
projectPath: string,
|
|
331
|
+
logger: ReturnType<typeof consola.withTag>,
|
|
332
|
+
dryRun: boolean,
|
|
333
|
+
): Promise<void> {
|
|
334
|
+
const pkgName = path.basename(pkgPath);
|
|
335
|
+
|
|
336
|
+
if (publishConfig === "npm") {
|
|
337
|
+
// npm publish
|
|
338
|
+
const prereleaseInfo = semver.prerelease(version);
|
|
339
|
+
const args = ["publish", "--access", "public", "--no-git-checks"];
|
|
340
|
+
|
|
341
|
+
if (prereleaseInfo !== null && typeof prereleaseInfo[0] === "string") {
|
|
342
|
+
args.push("--tag", prereleaseInfo[0]);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (dryRun) {
|
|
346
|
+
args.push("--dry-run");
|
|
347
|
+
logger.info(`[DRY-RUN] [${pkgName}] pnpm ${args.join(" ")}`);
|
|
348
|
+
} else {
|
|
349
|
+
logger.debug(`[${pkgName}] pnpm ${args.join(" ")}`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await spawn("pnpm", args, { cwd: pkgPath });
|
|
353
|
+
} else if (publishConfig.type === "local-directory") {
|
|
354
|
+
// 로컬 디렉토리 복사
|
|
355
|
+
const targetPath = replaceEnvVariables(publishConfig.path, version, projectPath);
|
|
356
|
+
const distPath = path.resolve(pkgPath, "dist");
|
|
357
|
+
|
|
358
|
+
if (dryRun) {
|
|
359
|
+
logger.info(`[DRY-RUN] [${pkgName}] 로컬 복사: ${distPath} → ${targetPath}`);
|
|
360
|
+
} else {
|
|
361
|
+
logger.debug(`[${pkgName}] 로컬 복사: ${distPath} → ${targetPath}`);
|
|
362
|
+
await fsCopy(distPath, targetPath);
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
// 스토리지 업로드
|
|
366
|
+
const distPath = path.resolve(pkgPath, "dist");
|
|
367
|
+
const remotePath = publishConfig.path ?? "/";
|
|
368
|
+
|
|
369
|
+
if (dryRun) {
|
|
370
|
+
logger.info(`[DRY-RUN] [${pkgName}] ${publishConfig.type} 업로드: ${distPath} → ${remotePath}`);
|
|
371
|
+
} else {
|
|
372
|
+
logger.debug(`[${pkgName}] ${publishConfig.type} 업로드: ${distPath} → ${remotePath}`);
|
|
373
|
+
await StorageFactory.connect(
|
|
374
|
+
publishConfig.type,
|
|
375
|
+
{
|
|
376
|
+
host: publishConfig.host,
|
|
377
|
+
port: publishConfig.port,
|
|
378
|
+
user: publishConfig.user,
|
|
379
|
+
pass: publishConfig.pass,
|
|
380
|
+
},
|
|
381
|
+
async (storage) => {
|
|
382
|
+
await storage.uploadDir(distPath, remotePath);
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
//#endregion
|
|
390
|
+
|
|
391
|
+
//#region Dependency Levels
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 배포 패키지의 의존성 레벨을 계산한다.
|
|
395
|
+
* 의존성이 없는 패키지 → Level 0, Level 0에만 의존 → Level 1, ...
|
|
396
|
+
*/
|
|
397
|
+
async function computePublishLevels(
|
|
398
|
+
publishPkgs: Array<{ name: string; path: string; config: SdPublishConfig }>,
|
|
399
|
+
): Promise<Array<Array<{ name: string; path: string; config: SdPublishConfig }>>> {
|
|
400
|
+
const pkgNames = new Set(publishPkgs.map((p) => p.name));
|
|
401
|
+
|
|
402
|
+
// 각 패키지의 워크스페이스 내 의존성 수집
|
|
403
|
+
const depsMap = new Map<string, Set<string>>();
|
|
404
|
+
for (const pkg of publishPkgs) {
|
|
405
|
+
const pkgJson = await fsReadJson<PackageJson>(path.resolve(pkg.path, "package.json"));
|
|
406
|
+
const allDeps = {
|
|
407
|
+
...pkgJson.dependencies,
|
|
408
|
+
...pkgJson.peerDependencies,
|
|
409
|
+
...pkgJson.optionalDependencies,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const workspaceDeps = new Set<string>();
|
|
413
|
+
for (const depName of Object.keys(allDeps)) {
|
|
414
|
+
const shortName = depName.replace(/^@simplysm\//, "");
|
|
415
|
+
if (shortName !== depName && pkgNames.has(shortName)) {
|
|
416
|
+
workspaceDeps.add(shortName);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
depsMap.set(pkg.name, workspaceDeps);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 위상 정렬로 레벨 분류
|
|
423
|
+
const levels: Array<Array<{ name: string; path: string; config: SdPublishConfig }>> = [];
|
|
424
|
+
const assigned = new Set<string>();
|
|
425
|
+
const remaining = new Map(publishPkgs.map((p) => [p.name, p]));
|
|
426
|
+
|
|
427
|
+
while (remaining.size > 0) {
|
|
428
|
+
const level: Array<{ name: string; path: string; config: SdPublishConfig }> = [];
|
|
429
|
+
for (const [name, pkg] of remaining) {
|
|
430
|
+
const deps = depsMap.get(name)!;
|
|
431
|
+
if ([...deps].every((d) => assigned.has(d))) {
|
|
432
|
+
level.push(pkg);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (level.length === 0) {
|
|
437
|
+
// 순환 의존성 — 남은 패키지를 모두 마지막 레벨에 배치
|
|
438
|
+
levels.push([...remaining.values()]);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const pkg of level) {
|
|
443
|
+
assigned.add(pkg.name);
|
|
444
|
+
remaining.delete(pkg.name);
|
|
445
|
+
}
|
|
446
|
+
levels.push(level);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return levels;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
//#endregion
|
|
453
|
+
|
|
454
|
+
//#region Main
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* publish 명령을 실행한다.
|
|
458
|
+
*
|
|
459
|
+
* **배포 순서 (안전성 우선):**
|
|
460
|
+
* 1. 사전 검증 (npm 인증, Git 상태)
|
|
461
|
+
* 2. 버전 업그레이드 (package.json + 템플릿)
|
|
462
|
+
* 3. 빌드
|
|
463
|
+
* 4. Git 커밋/태그/푸시 (변경된 파일만 명시적으로 staging)
|
|
464
|
+
* 5. pnpm 배포
|
|
465
|
+
* 6. postPublish (실패해도 계속)
|
|
466
|
+
*/
|
|
467
|
+
export async function runPublish(options: PublishOptions): Promise<void> {
|
|
468
|
+
const { targets, noBuild, dryRun } = options;
|
|
469
|
+
const cwd = process.cwd();
|
|
470
|
+
const logger = consola.withTag("sd:cli:publish");
|
|
471
|
+
|
|
472
|
+
if (dryRun) {
|
|
473
|
+
logger.info("[DRY-RUN] 시뮬레이션 모드 - 실제 배포 없음");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
logger.debug("배포 시작", { targets, noBuild, dryRun });
|
|
477
|
+
|
|
478
|
+
// sd.config.ts 로드
|
|
479
|
+
let sdConfig: SdConfig;
|
|
480
|
+
try {
|
|
481
|
+
sdConfig = await loadSdConfig({ cwd, dev: false, opt: options.options });
|
|
482
|
+
logger.debug("sd.config.ts 로드 완료");
|
|
483
|
+
} catch (err) {
|
|
484
|
+
consola.error(`sd.config.ts 로드 실패: ${err instanceof Error ? err.message : err}`);
|
|
485
|
+
process.exitCode = 1;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// package.json 로드
|
|
490
|
+
const projPkgPath = path.resolve(cwd, "package.json");
|
|
491
|
+
const projPkg = await fsReadJson<PackageJson>(projPkgPath);
|
|
492
|
+
|
|
493
|
+
// pnpm-workspace.yaml에서 워크스페이스 패키지 경로 수집
|
|
494
|
+
const workspaceYamlPath = path.resolve(cwd, "pnpm-workspace.yaml");
|
|
495
|
+
const workspaceGlobs: string[] = [];
|
|
496
|
+
if (await fsExists(workspaceYamlPath)) {
|
|
497
|
+
const yamlContent = await fsRead(workspaceYamlPath);
|
|
498
|
+
let inPackages = false;
|
|
499
|
+
for (const line of yamlContent.split("\n")) {
|
|
500
|
+
if (/^packages:\s*$/.test(line)) {
|
|
501
|
+
inPackages = true;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (inPackages) {
|
|
505
|
+
const match = /^\s+-\s+(.+)$/.exec(line);
|
|
506
|
+
if (match != null) {
|
|
507
|
+
workspaceGlobs.push(match[1].trim());
|
|
508
|
+
} else {
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const allPkgPaths = (await Promise.all(workspaceGlobs.map((item) => fsGlob(path.resolve(cwd, item)))))
|
|
516
|
+
.flat()
|
|
517
|
+
.filter((item) => !item.includes("."));
|
|
518
|
+
|
|
519
|
+
// publish 설정이 있는 패키지 필터링
|
|
520
|
+
const publishPackages: Array<{
|
|
521
|
+
name: string;
|
|
522
|
+
path: string;
|
|
523
|
+
config: SdPublishConfig;
|
|
524
|
+
}> = [];
|
|
525
|
+
|
|
526
|
+
for (const [name, config] of Object.entries(sdConfig.packages)) {
|
|
527
|
+
if (config == null) continue;
|
|
528
|
+
if (config.target === "scripts") continue;
|
|
529
|
+
|
|
530
|
+
const pkgConfig = config;
|
|
531
|
+
if (pkgConfig.publish == null) continue;
|
|
532
|
+
|
|
533
|
+
// targets가 지정되면 해당 패키지만 포함
|
|
534
|
+
if (targets.length > 0 && !targets.includes(name)) continue;
|
|
535
|
+
|
|
536
|
+
const pkgPath = allPkgPaths.find((p) => path.basename(p) === name);
|
|
537
|
+
if (pkgPath == null) {
|
|
538
|
+
logger.warn(`패키지를 찾을 수 없습니다: ${name}`);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
publishPackages.push({
|
|
543
|
+
name,
|
|
544
|
+
path: pkgPath,
|
|
545
|
+
config: pkgConfig.publish,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (publishPackages.length === 0) {
|
|
550
|
+
process.stdout.write("✔ 배포할 패키지가 없습니다.\n");
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
logger.debug(
|
|
555
|
+
"배포 대상 패키지",
|
|
556
|
+
publishPackages.map((p) => p.name),
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
// Git 사용 여부 확인
|
|
560
|
+
const hasGit = await fsExists(path.resolve(cwd, ".git"));
|
|
561
|
+
|
|
562
|
+
//#region Phase 1: 사전 검증
|
|
563
|
+
|
|
564
|
+
// npm 인증 확인 (npm publish 설정이 있는 경우)
|
|
565
|
+
if (publishPackages.some((p) => p.config === "npm")) {
|
|
566
|
+
logger.debug("npm 인증 확인...");
|
|
567
|
+
try {
|
|
568
|
+
const whoami = await spawn("npm", ["whoami"]);
|
|
569
|
+
if (whoami.trim() === "") {
|
|
570
|
+
throw new Error("npm 로그인 정보가 없습니다.");
|
|
571
|
+
}
|
|
572
|
+
logger.debug(`npm 로그인 확인: ${whoami.trim()}`);
|
|
573
|
+
} catch {
|
|
574
|
+
consola.error(
|
|
575
|
+
"npm 토큰이 유효하지 않거나 만료되었습니다.\n" +
|
|
576
|
+
"https://www.npmjs.com/settings/~/tokens 에서 Granular Access Token 생성 후:\n" +
|
|
577
|
+
" npm config set //registry.npmjs.org/:_authToken <토큰>",
|
|
578
|
+
);
|
|
579
|
+
process.exitCode = 1;
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// SSH 키 인증 확인 (pass 없는 SFTP publish 설정이 있는 경우)
|
|
585
|
+
try {
|
|
586
|
+
await ensureSshAuth(publishPackages, logger);
|
|
587
|
+
} catch (err) {
|
|
588
|
+
consola.error(`SSH 인증 설정 실패: ${err instanceof Error ? err.message : err}`);
|
|
589
|
+
process.exitCode = 1;
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Git 미커밋 변경사항 확인 및 자동 커밋 (noBuild가 아닌 경우)
|
|
594
|
+
if (!noBuild && hasGit) {
|
|
595
|
+
logger.debug("Git 커밋 여부 확인...");
|
|
596
|
+
try {
|
|
597
|
+
const diff = await spawn("git", ["diff", "--name-only"]);
|
|
598
|
+
const stagedDiff = await spawn("git", ["diff", "--cached", "--name-only"]);
|
|
599
|
+
|
|
600
|
+
if (diff.trim() !== "" || stagedDiff.trim() !== "") {
|
|
601
|
+
logger.info("커밋되지 않은 변경사항 감지. claude 자동 커밋 시도...");
|
|
602
|
+
try {
|
|
603
|
+
await spawn("claude", ["-p", "/sd-commit all", "--dangerously-skip-permissions", "--model", "haiku"]);
|
|
604
|
+
} catch (e) {
|
|
605
|
+
throw new Error(
|
|
606
|
+
"자동 커밋에 실패했습니다. 수동으로 커밋 후 다시 시도하세요.\n" +
|
|
607
|
+
(e instanceof Error ? e.message : String(e)),
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 커밋 후 재확인
|
|
612
|
+
const recheckDiff = await spawn("git", ["diff", "--name-only"]);
|
|
613
|
+
const recheckStaged = await spawn("git", ["diff", "--cached", "--name-only"]);
|
|
614
|
+
if (recheckDiff.trim() !== "" || recheckStaged.trim() !== "") {
|
|
615
|
+
throw new Error("자동 커밋 후에도 미커밋 변경사항이 남아있습니다.\n" + recheckDiff + recheckStaged);
|
|
616
|
+
}
|
|
617
|
+
logger.info("자동 커밋 완료.");
|
|
618
|
+
}
|
|
619
|
+
} catch (err) {
|
|
620
|
+
consola.error(err instanceof Error ? err.message : err);
|
|
621
|
+
process.exitCode = 1;
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
//#endregion
|
|
627
|
+
|
|
628
|
+
//#region Phase 2 & 3: 빌드 또는 noBuild 경고
|
|
629
|
+
|
|
630
|
+
let version = projPkg.version;
|
|
631
|
+
|
|
632
|
+
if (noBuild) {
|
|
633
|
+
// noBuild 경고
|
|
634
|
+
logger.warn("빌드하지 않고 배포하는 것은 상당히 위험합니다.");
|
|
635
|
+
await waitWithCountdown("프로세스를 중지하려면 'CTRL+C'를 누르세요.", 5);
|
|
636
|
+
} else {
|
|
637
|
+
// 버전 업그레이드
|
|
638
|
+
logger.debug("버전 업그레이드...");
|
|
639
|
+
const upgradeResult = await upgradeVersion(cwd, allPkgPaths, dryRun);
|
|
640
|
+
version = upgradeResult.version;
|
|
641
|
+
const _changedFiles = upgradeResult.changedFiles;
|
|
642
|
+
if (dryRun) {
|
|
643
|
+
logger.info(`[DRY-RUN] 버전 업그레이드: ${projPkg.version} → ${version} (파일 수정 없음)`);
|
|
644
|
+
} else {
|
|
645
|
+
logger.info(`버전 업그레이드: ${projPkg.version} → ${version}`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// 빌드 실행
|
|
649
|
+
if (dryRun) {
|
|
650
|
+
logger.info("[DRY-RUN] 빌드 시작 (검증용)...");
|
|
651
|
+
} else {
|
|
652
|
+
logger.debug("빌드 시작...");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
await runBuild({
|
|
657
|
+
targets: publishPackages.map((p) => p.name),
|
|
658
|
+
options: options.options,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// 빌드 실패 확인
|
|
662
|
+
if (process.exitCode === 1) {
|
|
663
|
+
throw new Error("빌드 실패");
|
|
664
|
+
}
|
|
665
|
+
} catch {
|
|
666
|
+
if (dryRun) {
|
|
667
|
+
logger.error("[DRY-RUN] 빌드 실패");
|
|
668
|
+
} else {
|
|
669
|
+
consola.error(
|
|
670
|
+
"빌드 실패. 수동 복구가 필요할 수 있습니다:\n" +
|
|
671
|
+
" 버전 변경을 되돌리려면:\n" +
|
|
672
|
+
" git checkout -- package.json packages/*/package.json packages/sd-cli/templates/",
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
process.exitCode = 1;
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
//#region Phase 3: Git 커밋/태그/푸시
|
|
680
|
+
|
|
681
|
+
if (hasGit) {
|
|
682
|
+
if (dryRun) {
|
|
683
|
+
logger.info("[DRY-RUN] Git 커밋/태그/푸시 시뮬레이션...");
|
|
684
|
+
logger.info(`[DRY-RUN] git add (${_changedFiles.length}개 파일)`);
|
|
685
|
+
logger.info(`[DRY-RUN] git commit -m "v${version}"`);
|
|
686
|
+
logger.info(`[DRY-RUN] git tag -a v${version} -m "v${version}"`);
|
|
687
|
+
logger.info("[DRY-RUN] git push --dry-run");
|
|
688
|
+
await spawn("git", ["push", "--dry-run"]);
|
|
689
|
+
logger.info("[DRY-RUN] git push --tags --dry-run");
|
|
690
|
+
await spawn("git", ["push", "--tags", "--dry-run"]);
|
|
691
|
+
logger.info("[DRY-RUN] Git 작업 시뮬레이션 완료");
|
|
692
|
+
} else {
|
|
693
|
+
logger.debug("Git 커밋/태그/푸시...");
|
|
694
|
+
try {
|
|
695
|
+
await spawn("git", ["add", ..._changedFiles]);
|
|
696
|
+
await spawn("git", ["commit", "-m", `v${version}`]);
|
|
697
|
+
await spawn("git", ["tag", "-a", `v${version}`, "-m", `v${version}`]);
|
|
698
|
+
await spawn("git", ["push"]);
|
|
699
|
+
await spawn("git", ["push", "--tags"]);
|
|
700
|
+
logger.debug("Git 작업 완료");
|
|
701
|
+
} catch (err) {
|
|
702
|
+
consola.error(
|
|
703
|
+
`Git 작업 실패: ${err instanceof Error ? err.message : err}\n` +
|
|
704
|
+
"수동 복구가 필요할 수 있습니다:\n" +
|
|
705
|
+
` git revert HEAD # 버전 커밋 되돌리기\n` +
|
|
706
|
+
` git tag -d v${version} # 태그 삭제`,
|
|
707
|
+
);
|
|
708
|
+
process.exitCode = 1;
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
//#endregion
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
//#endregion
|
|
718
|
+
|
|
719
|
+
//#region Phase 4: 배포 (의존성 레벨별 병렬, Listr)
|
|
720
|
+
|
|
721
|
+
const levels = await computePublishLevels(publishPackages);
|
|
722
|
+
const publishedPackages: string[] = [];
|
|
723
|
+
let publishFailed = false;
|
|
724
|
+
|
|
725
|
+
const publishListr = new Listr(
|
|
726
|
+
levels.map(
|
|
727
|
+
(levelPkgs, levelIdx): ListrTask => ({
|
|
728
|
+
title: `Level ${levelIdx + 1}/${levels.length}`,
|
|
729
|
+
skip: () => publishFailed,
|
|
730
|
+
task: (_, task) =>
|
|
731
|
+
task.newListr(
|
|
732
|
+
levelPkgs.map(
|
|
733
|
+
(pkg): ListrTask => ({
|
|
734
|
+
title: dryRun ? `[DRY-RUN] ${pkg.name}` : pkg.name,
|
|
735
|
+
task: async (_ctx, pkgTask) => {
|
|
736
|
+
const maxRetries = 3;
|
|
737
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
738
|
+
try {
|
|
739
|
+
await publishPackage(pkg.path, pkg.config, version, cwd, logger, dryRun);
|
|
740
|
+
break;
|
|
741
|
+
} catch (err) {
|
|
742
|
+
if (attempt < maxRetries) {
|
|
743
|
+
const delay = attempt * 5_000;
|
|
744
|
+
pkgTask.title = dryRun
|
|
745
|
+
? `[DRY-RUN] ${pkg.name} (재시도 ${attempt + 1}/${maxRetries})`
|
|
746
|
+
: `${pkg.name} (재시도 ${attempt + 1}/${maxRetries})`;
|
|
747
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
748
|
+
} else {
|
|
749
|
+
throw err;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
publishedPackages.push(pkg.name);
|
|
754
|
+
},
|
|
755
|
+
}),
|
|
756
|
+
),
|
|
757
|
+
{ concurrent: true, exitOnError: false },
|
|
758
|
+
),
|
|
759
|
+
}),
|
|
760
|
+
),
|
|
761
|
+
{
|
|
762
|
+
concurrent: false,
|
|
763
|
+
exitOnError: false,
|
|
764
|
+
renderer: consola.level >= LogLevels.debug ? "verbose" : "default",
|
|
765
|
+
},
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
await publishListr.run();
|
|
770
|
+
} catch {
|
|
771
|
+
// Listr 내부 에러는 아래에서 처리
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// 실패한 패키지 확인
|
|
775
|
+
const allPkgNames = publishPackages.map((p) => p.name);
|
|
776
|
+
const failedPkgNames = allPkgNames.filter((n) => !publishedPackages.includes(n));
|
|
777
|
+
|
|
778
|
+
if (failedPkgNames.length > 0) {
|
|
779
|
+
publishFailed = true;
|
|
780
|
+
|
|
781
|
+
if (publishedPackages.length > 0) {
|
|
782
|
+
consola.error(
|
|
783
|
+
"배포 중 오류가 발생했습니다.\n" +
|
|
784
|
+
"이미 배포된 패키지:\n" +
|
|
785
|
+
publishedPackages.map((n) => ` - ${n}`).join("\n") +
|
|
786
|
+
"\n\n수동 복구가 필요할 수 있습니다.\n" +
|
|
787
|
+
"npm 패키지는 72시간 내에 `npm unpublish <pkg>@<version>` 으로 삭제할 수 있습니다.",
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
for (const name of failedPkgNames) {
|
|
792
|
+
consola.error(`[${name}] 배포 실패`);
|
|
793
|
+
}
|
|
794
|
+
process.exitCode = 1;
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
//#endregion
|
|
799
|
+
|
|
800
|
+
//#region Phase 5: postPublish
|
|
801
|
+
|
|
802
|
+
if (sdConfig.postPublish != null && sdConfig.postPublish.length > 0) {
|
|
803
|
+
if (dryRun) {
|
|
804
|
+
logger.info("[DRY-RUN] postPublish 스크립트 시뮬레이션...");
|
|
805
|
+
} else {
|
|
806
|
+
logger.debug("postPublish 스크립트 실행...");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
for (const script of sdConfig.postPublish) {
|
|
810
|
+
try {
|
|
811
|
+
const cmd = replaceEnvVariables(script.cmd, version, cwd);
|
|
812
|
+
const args = script.args.map((arg) => replaceEnvVariables(arg, version, cwd));
|
|
813
|
+
|
|
814
|
+
if (dryRun) {
|
|
815
|
+
logger.info(`[DRY-RUN] 실행 예정: ${cmd} ${args.join(" ")}`);
|
|
816
|
+
} else {
|
|
817
|
+
logger.debug(`실행: ${cmd} ${args.join(" ")}`);
|
|
818
|
+
await spawn(cmd, args, { cwd });
|
|
819
|
+
}
|
|
820
|
+
} catch (err) {
|
|
821
|
+
// postPublish 실패 시 경고만 출력 (배포 롤백 불가)
|
|
822
|
+
logger.warn(`postPublish 스크립트 실패 (계속 진행): ${err instanceof Error ? err.message : err}`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
//#endregion
|
|
828
|
+
|
|
829
|
+
if (dryRun) {
|
|
830
|
+
logger.info(`[DRY-RUN] 시뮬레이션 완료. 실제 배포 시 버전: v${version}`);
|
|
831
|
+
} else {
|
|
832
|
+
logger.info(`모든 배포가 완료되었습니다. (v${version})`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
//#endregion
|