@simplysm/sd-cli 14.0.11 → 14.0.13

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.
Files changed (265) hide show
  1. package/README.md +58 -253
  2. package/dist/angular/client-transform-stylesheet.js +1 -1
  3. package/dist/angular/client-transform-stylesheet.js.map +1 -1
  4. package/dist/angular/vite-angular-plugin.d.ts +4 -2
  5. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  6. package/dist/angular/vite-angular-plugin.js +73 -36
  7. package/dist/angular/vite-angular-plugin.js.map +1 -1
  8. package/dist/angular/vite-postcss-inline-plugin.d.ts +1 -1
  9. package/dist/angular/vite-postcss-inline-plugin.js +1 -1
  10. package/dist/capacitor/capacitor.d.ts +20 -2
  11. package/dist/capacitor/capacitor.d.ts.map +1 -1
  12. package/dist/capacitor/capacitor.js +155 -28
  13. package/dist/capacitor/capacitor.js.map +1 -1
  14. package/dist/commands/build.d.ts +3 -10
  15. package/dist/commands/build.d.ts.map +1 -1
  16. package/dist/commands/build.js +3 -10
  17. package/dist/commands/build.js.map +1 -1
  18. package/dist/commands/check.js +3 -3
  19. package/dist/commands/check.js.map +1 -1
  20. package/dist/commands/dev.d.ts +3 -9
  21. package/dist/commands/dev.d.ts.map +1 -1
  22. package/dist/commands/dev.js +3 -9
  23. package/dist/commands/dev.js.map +1 -1
  24. package/dist/commands/device.d.ts +3 -3
  25. package/dist/commands/device.js +5 -5
  26. package/dist/commands/device.js.map +1 -1
  27. package/dist/commands/publish.d.ts +1 -1
  28. package/dist/commands/publish.d.ts.map +1 -1
  29. package/dist/commands/publish.js +24 -26
  30. package/dist/commands/publish.js.map +1 -1
  31. package/dist/commands/replace-deps.d.ts +3 -3
  32. package/dist/commands/replace-deps.d.ts.map +1 -1
  33. package/dist/commands/replace-deps.js +1 -1
  34. package/dist/commands/typecheck.d.ts +4 -3
  35. package/dist/commands/typecheck.d.ts.map +1 -1
  36. package/dist/commands/typecheck.js +5 -11
  37. package/dist/commands/typecheck.js.map +1 -1
  38. package/dist/commands/watch.d.ts +9 -9
  39. package/dist/commands/watch.js +9 -9
  40. package/dist/electron/electron.d.ts.map +1 -1
  41. package/dist/electron/electron.js +42 -3
  42. package/dist/electron/electron.js.map +1 -1
  43. package/dist/engines/BaseEngine.d.ts +1 -1
  44. package/dist/engines/BaseEngine.d.ts.map +1 -1
  45. package/dist/engines/BaseEngine.js +3 -1
  46. package/dist/engines/BaseEngine.js.map +1 -1
  47. package/dist/engines/NgtscEngine.d.ts +7 -7
  48. package/dist/engines/NgtscEngine.d.ts.map +1 -1
  49. package/dist/engines/NgtscEngine.js +3 -3
  50. package/dist/engines/ServerEsbuildEngine.d.ts +7 -7
  51. package/dist/engines/ServerEsbuildEngine.d.ts.map +1 -1
  52. package/dist/engines/ServerEsbuildEngine.js +3 -3
  53. package/dist/engines/TscEngine.d.ts +7 -7
  54. package/dist/engines/TscEngine.d.ts.map +1 -1
  55. package/dist/engines/TscEngine.js +3 -3
  56. package/dist/engines/ViteEngine.d.ts +7 -1
  57. package/dist/engines/ViteEngine.d.ts.map +1 -1
  58. package/dist/engines/ViteEngine.js +13 -12
  59. package/dist/engines/ViteEngine.js.map +1 -1
  60. package/dist/engines/index.d.ts +9 -5
  61. package/dist/engines/index.d.ts.map +1 -1
  62. package/dist/engines/index.js +7 -5
  63. package/dist/engines/index.js.map +1 -1
  64. package/dist/engines/types.d.ts +20 -20
  65. package/dist/engines/types.d.ts.map +1 -1
  66. package/dist/infra/ResultCollector.d.ts +9 -9
  67. package/dist/infra/ResultCollector.js +8 -8
  68. package/dist/infra/SignalHandler.d.ts +7 -7
  69. package/dist/infra/SignalHandler.js +7 -7
  70. package/dist/infra/WorkerManager.d.ts +14 -14
  71. package/dist/infra/WorkerManager.js +14 -14
  72. package/dist/orchestrators/BuildOrchestrator.d.ts +25 -25
  73. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  74. package/dist/orchestrators/BuildOrchestrator.js +34 -30
  75. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  76. package/dist/orchestrators/DevWatchOrchestrator.d.ts +7 -7
  77. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  78. package/dist/orchestrators/DevWatchOrchestrator.js +34 -34
  79. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  80. package/dist/sd-cli-entry.d.ts +2 -2
  81. package/dist/sd-cli-entry.d.ts.map +1 -1
  82. package/dist/sd-cli-entry.js +15 -8
  83. package/dist/sd-cli-entry.js.map +1 -1
  84. package/dist/sd-cli.d.ts +3 -3
  85. package/dist/sd-cli.js +16 -16
  86. package/dist/sd-cli.js.map +1 -1
  87. package/dist/sd-config.types.d.ts +105 -105
  88. package/dist/sd-config.types.d.ts.map +1 -1
  89. package/dist/utils/angular-compiler.js +5 -5
  90. package/dist/utils/angular-compiler.js.map +1 -1
  91. package/dist/utils/build-env.d.ts +1 -1
  92. package/dist/utils/build-env.js +1 -1
  93. package/dist/utils/concurrency.d.ts +7 -7
  94. package/dist/utils/concurrency.js +7 -7
  95. package/dist/utils/copy-public.d.ts +9 -9
  96. package/dist/utils/copy-public.js +17 -17
  97. package/dist/utils/copy-public.js.map +1 -1
  98. package/dist/utils/copy-src.d.ts +9 -9
  99. package/dist/utils/copy-src.js +11 -11
  100. package/dist/utils/copy-src.js.map +1 -1
  101. package/dist/utils/engine-stop.d.ts +8 -9
  102. package/dist/utils/engine-stop.d.ts.map +1 -1
  103. package/dist/utils/engine-stop.js +9 -10
  104. package/dist/utils/engine-stop.js.map +1 -1
  105. package/dist/utils/esbuild-config.d.ts +23 -23
  106. package/dist/utils/esbuild-config.d.ts.map +1 -1
  107. package/dist/utils/esbuild-config.js +25 -25
  108. package/dist/utils/esbuild-config.js.map +1 -1
  109. package/dist/utils/lint-with-program.d.ts +15 -15
  110. package/dist/utils/lint-with-program.d.ts.map +1 -1
  111. package/dist/utils/lint-with-program.js +29 -29
  112. package/dist/utils/lint-with-program.js.map +1 -1
  113. package/dist/utils/ngtsc-build-core.d.ts +8 -8
  114. package/dist/utils/ngtsc-build-core.d.ts.map +1 -1
  115. package/dist/utils/ngtsc-build-core.js +14 -14
  116. package/dist/utils/ngtsc-build-core.js.map +1 -1
  117. package/dist/utils/output-path-rewriter.d.ts +14 -14
  118. package/dist/utils/output-path-rewriter.js +18 -18
  119. package/dist/utils/output-path-rewriter.js.map +1 -1
  120. package/dist/utils/output-utils.d.ts +6 -6
  121. package/dist/utils/output-utils.js +11 -11
  122. package/dist/utils/output-utils.js.map +1 -1
  123. package/dist/utils/package-utils.d.ts +21 -21
  124. package/dist/utils/package-utils.d.ts.map +1 -1
  125. package/dist/utils/package-utils.js +56 -45
  126. package/dist/utils/package-utils.js.map +1 -1
  127. package/dist/utils/replace-deps.d.ts +25 -25
  128. package/dist/utils/replace-deps.d.ts.map +1 -1
  129. package/dist/utils/replace-deps.js +84 -65
  130. package/dist/utils/replace-deps.js.map +1 -1
  131. package/dist/utils/sd-config.d.ts +3 -3
  132. package/dist/utils/sd-config.js +3 -3
  133. package/dist/utils/tsc-build.d.ts +13 -13
  134. package/dist/utils/tsc-build.d.ts.map +1 -1
  135. package/dist/utils/tsc-build.js +9 -9
  136. package/dist/utils/tsc-build.js.map +1 -1
  137. package/dist/utils/tsconfig.d.ts +11 -9
  138. package/dist/utils/tsconfig.d.ts.map +1 -1
  139. package/dist/utils/tsconfig.js +11 -9
  140. package/dist/utils/tsconfig.js.map +1 -1
  141. package/dist/utils/typecheck-non-package.d.ts +5 -6
  142. package/dist/utils/typecheck-non-package.d.ts.map +1 -1
  143. package/dist/utils/typecheck-non-package.js +7 -8
  144. package/dist/utils/typecheck-non-package.js.map +1 -1
  145. package/dist/utils/typecheck-serialization.d.ts +8 -8
  146. package/dist/utils/typecheck-serialization.d.ts.map +1 -1
  147. package/dist/utils/typecheck-serialization.js +12 -16
  148. package/dist/utils/typecheck-serialization.js.map +1 -1
  149. package/dist/utils/vite-config.d.ts +12 -5
  150. package/dist/utils/vite-config.d.ts.map +1 -1
  151. package/dist/utils/vite-config.js +95 -41
  152. package/dist/utils/vite-config.js.map +1 -1
  153. package/dist/utils/vite-scope-watch-plugin.d.ts.map +1 -1
  154. package/dist/utils/vite-scope-watch-plugin.js +1 -1
  155. package/dist/utils/vite-scope-watch-plugin.js.map +1 -1
  156. package/dist/utils/worker-events.d.ts +12 -12
  157. package/dist/utils/worker-events.d.ts.map +1 -1
  158. package/dist/utils/worker-events.js +10 -10
  159. package/dist/utils/worker-events.js.map +1 -1
  160. package/dist/utils/worker-utils.d.ts +12 -13
  161. package/dist/utils/worker-utils.d.ts.map +1 -1
  162. package/dist/utils/worker-utils.js +12 -13
  163. package/dist/utils/worker-utils.js.map +1 -1
  164. package/dist/vitest-plugin.d.ts.map +1 -1
  165. package/dist/vitest-plugin.js +5 -7
  166. package/dist/vitest-plugin.js.map +1 -1
  167. package/dist/workers/client.worker.d.ts +8 -2
  168. package/dist/workers/client.worker.d.ts.map +1 -1
  169. package/dist/workers/client.worker.js +215 -6
  170. package/dist/workers/client.worker.js.map +1 -1
  171. package/dist/workers/library-build.worker.d.ts +1 -1
  172. package/dist/workers/library-build.worker.d.ts.map +1 -1
  173. package/dist/workers/library-build.worker.js +7 -7
  174. package/dist/workers/library-build.worker.js.map +1 -1
  175. package/dist/workers/lint.worker.d.ts +2 -2
  176. package/dist/workers/lint.worker.js +2 -2
  177. package/dist/workers/ngtsc-build.worker.js +30 -30
  178. package/dist/workers/ngtsc-build.worker.js.map +1 -1
  179. package/dist/workers/server-build.worker.d.ts +17 -17
  180. package/dist/workers/server-build.worker.d.ts.map +1 -1
  181. package/dist/workers/server-build.worker.js +46 -46
  182. package/dist/workers/server-build.worker.js.map +1 -1
  183. package/dist/workers/server-runtime.worker.d.ts +7 -7
  184. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  185. package/dist/workers/server-runtime.worker.js +17 -17
  186. package/dist/workers/server-runtime.worker.js.map +1 -1
  187. package/docs/config.md +340 -0
  188. package/docs/publish-configuration-types.md +87 -0
  189. package/docs/pwa-configuration-types.md +55 -0
  190. package/docs/vitest-plugin.md +47 -0
  191. package/package.json +9 -7
  192. package/src/angular/client-transform-stylesheet.ts +1 -1
  193. package/src/angular/vite-angular-plugin.ts +89 -39
  194. package/src/angular/vite-postcss-inline-plugin.ts +1 -1
  195. package/src/capacitor/capacitor.ts +185 -38
  196. package/src/commands/build.ts +3 -10
  197. package/src/commands/check.ts +3 -3
  198. package/src/commands/dev.ts +3 -9
  199. package/src/commands/device.ts +5 -5
  200. package/src/commands/publish.ts +30 -26
  201. package/src/commands/replace-deps.ts +3 -3
  202. package/src/commands/typecheck.ts +7 -13
  203. package/src/commands/watch.ts +9 -9
  204. package/src/electron/electron.ts +49 -4
  205. package/src/engines/BaseEngine.ts +4 -1
  206. package/src/engines/NgtscEngine.ts +7 -7
  207. package/src/engines/ServerEsbuildEngine.ts +7 -7
  208. package/src/engines/TscEngine.ts +7 -7
  209. package/src/engines/ViteEngine.ts +18 -13
  210. package/src/engines/index.ts +11 -5
  211. package/src/engines/types.ts +20 -20
  212. package/src/infra/ResultCollector.ts +9 -9
  213. package/src/infra/SignalHandler.ts +7 -7
  214. package/src/infra/WorkerManager.ts +14 -14
  215. package/src/orchestrators/BuildOrchestrator.ts +42 -38
  216. package/src/orchestrators/DevWatchOrchestrator.ts +36 -36
  217. package/src/sd-cli-entry.ts +15 -8
  218. package/src/sd-cli.ts +16 -16
  219. package/src/sd-config.types.ts +107 -107
  220. package/src/utils/angular-compiler.ts +5 -5
  221. package/src/utils/build-env.ts +1 -1
  222. package/src/utils/concurrency.ts +7 -7
  223. package/src/utils/copy-public.ts +17 -17
  224. package/src/utils/copy-src.ts +11 -11
  225. package/src/utils/engine-stop.ts +9 -10
  226. package/src/utils/esbuild-config.ts +29 -29
  227. package/src/utils/lint-with-program.ts +34 -34
  228. package/src/utils/ngtsc-build-core.ts +17 -17
  229. package/src/utils/output-path-rewriter.ts +18 -18
  230. package/src/utils/output-utils.ts +11 -11
  231. package/src/utils/package-utils.ts +57 -45
  232. package/src/utils/replace-deps.ts +92 -67
  233. package/src/utils/sd-config.ts +3 -3
  234. package/src/utils/tsc-build.ts +18 -18
  235. package/src/utils/tsconfig.ts +11 -9
  236. package/src/utils/typecheck-non-package.ts +7 -8
  237. package/src/utils/typecheck-serialization.ts +13 -15
  238. package/src/utils/vite-config.ts +108 -46
  239. package/src/utils/vite-scope-watch-plugin.ts +6 -1
  240. package/src/utils/worker-events.ts +16 -16
  241. package/src/utils/worker-utils.ts +12 -13
  242. package/src/vitest-plugin.ts +5 -8
  243. package/src/workers/client.worker.ts +246 -7
  244. package/src/workers/library-build.worker.ts +8 -8
  245. package/src/workers/lint.worker.ts +2 -2
  246. package/src/workers/ngtsc-build.worker.ts +31 -31
  247. package/src/workers/server-build.worker.ts +60 -60
  248. package/src/workers/server-runtime.worker.ts +22 -22
  249. package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +1 -0
  250. package/tests/angular/vite-angular-plugin-hmr.spec.ts +78 -0
  251. package/tests/angular/vite-angular-plugin.spec.ts +67 -0
  252. package/tests/capacitor/capacitor-build.spec.ts +93 -11
  253. package/tests/capacitor/capacitor-icon.spec.ts +7 -5
  254. package/tests/capacitor/capacitor-init.spec.ts +124 -10
  255. package/tests/capacitor/capacitor-run.spec.ts +14 -17
  256. package/tests/capacitor/capacitor-workspace.spec.ts +5 -3
  257. package/tests/commands/check.spec.ts +2 -2
  258. package/tests/commands/publish.spec.ts +2 -2
  259. package/tests/commands/typecheck.spec.ts +8 -0
  260. package/tests/electron/electron.spec.ts +12 -10
  261. package/tests/engines/base-engine.spec.ts +37 -0
  262. package/tests/engines/vite-engine.spec.ts +115 -3
  263. package/tests/utils/vite-config.spec.ts +162 -90
  264. package/tests/workers/client-worker.spec.ts +690 -0
  265. package/tests/workers/server-build-worker.spec.ts +3 -3
@@ -5,7 +5,7 @@ import { symlink } from "fs/promises";
5
5
  import { createRequire } from "module";
6
6
  import { cpx, fsx, pathx } from "@simplysm/core-node";
7
7
  import { env } from "@simplysm/core-common";
8
- import { consola } from "consola";
8
+ import { consola, LogLevels } from "consola";
9
9
  import type { SdCapacitorConfig } from "../sd-config.types.js";
10
10
 
11
11
  /**
@@ -91,9 +91,7 @@ export class Capacitor {
91
91
  const platforms = Object.keys(config.platform);
92
92
  for (const p of platforms) {
93
93
  if (p !== "android") {
94
- throw new CapacitorConfigError(
95
- `지원하지 않는 플랫폼입니다: ${p} (현재 android만 지원)`,
96
- );
94
+ throw new CapacitorConfigError(`지원하지 않는 플랫폼입니다: ${p} (현재 android만 지원)`);
97
95
  }
98
96
  }
99
97
  }
@@ -109,47 +107,65 @@ export class Capacitor {
109
107
  * 5. cap sync 또는 cap copy 실행
110
108
  */
111
109
  async initialize(): Promise<void> {
110
+ Capacitor._logger.debug("initialize 시작");
112
111
  await this._acquireLock();
113
112
 
114
113
  try {
115
114
  // 외부 도구 검증
115
+ Capacitor._logger.debug("외부 도구 검증 시작");
116
116
  await this._validateTools();
117
+ Capacitor._logger.debug("외부 도구 검증 완료");
117
118
 
118
119
  // 1. Capacitor 프로젝트 초기화
120
+ Capacitor._logger.debug("Capacitor 프로젝트 초기화 시작");
119
121
  const changed = await this._initCap();
122
+ Capacitor._logger.debug(`Capacitor 프로젝트 초기화 완료 (changed: ${changed})`);
120
123
 
121
124
  // 2. Capacitor 설정 파일 생성
125
+ Capacitor._logger.debug("Capacitor 설정 파일 생성 시작");
122
126
  await this._writeCapConf();
127
+ Capacitor._logger.debug("Capacitor 설정 파일 생성 완료");
123
128
 
124
129
  // 3. 플랫폼 관리 (멱등성: 이미 존재하면 스킵)
130
+ Capacitor._logger.debug("플랫폼 추가 시작");
125
131
  await this._addPlatforms();
132
+ Capacitor._logger.debug("플랫폼 추가 완료");
126
133
 
127
134
  // 4. 아이콘 처리
135
+ Capacitor._logger.debug("아이콘 처리 시작");
128
136
  await this._setupIcon();
137
+ Capacitor._logger.debug("아이콘 처리 완료");
129
138
 
130
139
  // 5. Android 네이티브 설정 구성
131
140
  if (this._platforms.includes("android")) {
141
+ Capacitor._logger.debug("Android 네이티브 설정 시작");
132
142
  await this._configureAndroid();
143
+ Capacitor._logger.debug("Android 네이티브 설정 완료");
133
144
  }
134
145
 
135
146
  // 6. 웹 에셋 동기화
136
147
  if (changed) {
148
+ Capacitor._logger.debug("cap sync 시작 (의존성 변경됨)");
137
149
  await this._execCap(["sync"]);
150
+ Capacitor._logger.debug("cap sync 완료");
138
151
  } else {
152
+ Capacitor._logger.debug("cap copy 시작");
139
153
  await this._execCap(["copy"]);
154
+ Capacitor._logger.debug("cap copy 완료");
140
155
  }
141
156
  } finally {
142
157
  await this._releaseLock();
158
+ Capacitor._logger.debug("initialize 완료");
143
159
  }
144
160
  }
145
161
 
146
162
  //#region Private - 명령어 실행
147
163
 
148
164
  /**
149
- * Capacitor CLI 명령어를 npx로 실행
165
+ * Capacitor CLI 명령어를 pnpm exec로 실행
150
166
  */
151
167
  private async _execCap(args: string[]): Promise<string> {
152
- return this._exec("npx", ["cap", ...args], this._capPath);
168
+ return this._exec("pnpm", ["exec", "cap", ...args], this._capPath);
153
169
  }
154
170
 
155
171
  /**
@@ -157,7 +173,11 @@ export class Capacitor {
157
173
  */
158
174
  private async _exec(command: string, args: string[], cwd: string): Promise<string> {
159
175
  Capacitor._logger.debug(`명령어 실행: ${command} ${args.join(" ")}`);
160
- const { stdout } = await cpx.exec(command, args, { cwd });
176
+ const isDebug = consola.level >= LogLevels.debug;
177
+ const { stdout } = await cpx.spawn(command, args, {
178
+ cwd,
179
+ ...(isDebug ? { stdio: ["ignore", "inherit", "inherit"] } : {}),
180
+ });
161
181
  Capacitor._logger.debug(`실행 결과: ${stdout}`);
162
182
  return stdout;
163
183
  }
@@ -215,6 +235,7 @@ export class Capacitor {
215
235
  "2. ANDROID_HOME 또는 ANDROID_SDK_ROOT 환경 변수를 설정하세요.",
216
236
  );
217
237
  }
238
+ Capacitor._logger.debug(`Android SDK 경로: ${sdkPath}`);
218
239
 
219
240
  // Java 확인 (android 플랫폼인 경우에만)
220
241
  if (this._platforms.includes("android")) {
@@ -223,6 +244,8 @@ export class Capacitor {
223
244
  Capacitor._logger.warn(
224
245
  "Java 21을 찾을 수 없습니다. Gradle이 내장 JDK를 사용하거나 빌드가 실패할 수 있습니다.",
225
246
  );
247
+ } else {
248
+ Capacitor._logger.debug(`Java 21 경로: ${javaPath}`);
226
249
  }
227
250
  }
228
251
  }
@@ -235,36 +258,47 @@ export class Capacitor {
235
258
  * Capacitor 프로젝트 기본 초기화 (package.json, npm install, cap init)
236
259
  */
237
260
  private async _initCap(): Promise<boolean> {
261
+ Capacitor._logger.debug("package.json 설정 시작");
238
262
  const { depChanged, workspacePlugins } = await this._setupNpmConf();
239
263
  const nodeModulesExists = await fsx.exists(pathx.posixResolve(this._capPath, "node_modules"));
264
+ Capacitor._logger.debug(`depChanged: ${depChanged}, nodeModulesExists: ${nodeModulesExists}`);
240
265
 
241
266
  if (!depChanged && nodeModulesExists) {
242
267
  // 의존성 미변경이어도 workspace 플러그인 symlink는 항상 갱신
268
+ Capacitor._logger.debug("의존성 변경 없음, workspace 플러그인 symlink만 갱신");
243
269
  await this._linkWorkspacePlugins(workspacePlugins);
244
270
  return false;
245
271
  }
246
272
 
247
- // npm install
248
- const installResult = await this._exec("npm", ["install"], this._capPath);
249
- Capacitor._logger.debug(`npm install 완료: ${installResult}`);
273
+ // pnpm-workspace.yaml 생성 (상위 workspace 탐색 차단)
274
+ const workspaceYamlPath = pathx.posixResolve(this._capPath, "pnpm-workspace.yaml");
275
+ if (!(await fsx.exists(workspaceYamlPath))) {
276
+ await fsx.write(workspaceYamlPath, "");
277
+ }
278
+ const lockfilePath = pathx.posixResolve(this._capPath, "pnpm-lock.yaml");
279
+ if (!(await fsx.exists(lockfilePath))) {
280
+ await fsx.write(lockfilePath, "");
281
+ }
282
+
283
+ // pnpm install + 빌드 스크립트 승인
284
+ Capacitor._logger.debug("pnpm install 시작");
285
+ await this._exec("pnpm", ["install"], this._capPath);
286
+ await this._exec("pnpm", ["approve-builds", "--all"], this._capPath);
287
+ Capacitor._logger.debug("pnpm install 완료");
250
288
 
251
289
  // workspace 플러그인 symlink
290
+ Capacitor._logger.debug("workspace 플러그인 symlink 시작");
252
291
  await this._linkWorkspacePlugins(workspacePlugins);
292
+ Capacitor._logger.debug("workspace 플러그인 symlink 완료");
253
293
 
254
294
  // 멱등성: capacitor.config.ts가 없을 때만 cap init 실행
255
295
  const configPath = pathx.posixResolve(this._capPath, "capacitor.config.ts");
256
296
  if (!(await fsx.exists(configPath))) {
297
+ Capacitor._logger.debug("cap init 시작");
257
298
  await this._execCap(["init", this._config.appId, this._config.appId]);
299
+ Capacitor._logger.debug("cap init 완료");
258
300
  }
259
301
 
260
- // 기본 www/index.html 생성
261
- const wwwPath = pathx.posixResolve(this._capPath, "www");
262
- await fsx.mkdir(wwwPath);
263
- await fsx.write(
264
- pathx.posixResolve(wwwPath, "index.html"),
265
- "<!DOCTYPE html><html><head></head><body></body></html>",
266
- );
267
-
268
302
  return true;
269
303
  }
270
304
 
@@ -465,8 +499,9 @@ export default config;
465
499
 
466
500
  // capacitor-assets로 모든 해상도 아이콘/스플래시 생성
467
501
  await this._exec(
468
- "npx",
502
+ "pnpm",
469
503
  [
504
+ "exec",
470
505
  "capacitor-assets",
471
506
  "generate",
472
507
  "--iconBackgroundColor",
@@ -500,10 +535,29 @@ export default config;
500
535
  throw new Error(`Android 프로젝트 디렉토리를 찾을 수 없습니다: ${androidPath}`);
501
536
  }
502
537
 
538
+ Capacitor._logger.debug("JAVA_HOME 설정 시작");
503
539
  await this._configureAndroidJavaHomePath(androidPath);
540
+ Capacitor._logger.debug("JAVA_HOME 설정 완료");
541
+
542
+ Capacitor._logger.debug("Android SDK 경로 설정 시작");
504
543
  await this._configureAndroidSdkPath(androidPath);
544
+ Capacitor._logger.debug("Android SDK 경로 설정 완료");
545
+
546
+ Capacitor._logger.debug("AndroidManifest.xml 설정 시작");
505
547
  await this._configureAndroidManifest(androidPath);
548
+ Capacitor._logger.debug("AndroidManifest.xml 설정 완료");
549
+
550
+ Capacitor._logger.debug("루트 build.gradle Kotlin 플러그인 설정 시작");
551
+ await this._configureAndroidRootBuildGradle(androidPath);
552
+ Capacitor._logger.debug("루트 build.gradle Kotlin 플러그인 설정 완료");
553
+
554
+ Capacitor._logger.debug("build.gradle 설정 시작");
506
555
  await this._configureAndroidBuildGradle(androidPath);
556
+ Capacitor._logger.debug("build.gradle 설정 완료");
557
+
558
+ Capacitor._logger.debug("styles.xml 설정 시작");
559
+ await this._configureAndroidStyles(androidPath);
560
+ Capacitor._logger.debug("styles.xml 설정 완료");
507
561
  }
508
562
 
509
563
  /**
@@ -574,7 +628,9 @@ export default config;
574
628
  * Android SDK 경로 탐색
575
629
  */
576
630
  private async _findAndroidSdk(): Promise<string | undefined> {
577
- const androidHome = (env["ANDROID_HOME"] as string | undefined) ?? (env["ANDROID_SDK_ROOT"] as string | undefined);
631
+ const androidHome =
632
+ (env["ANDROID_HOME"] as string | undefined) ??
633
+ (env["ANDROID_SDK_ROOT"] as string | undefined);
578
634
  if (androidHome != null && (await fsx.exists(androidHome))) {
579
635
  return androidHome;
580
636
  }
@@ -670,6 +726,28 @@ export default config;
670
726
  await fsx.write(manifestPath, content);
671
727
  }
672
728
 
729
+ /**
730
+ * 루트 build.gradle에 Kotlin Gradle 플러그인 classpath 추가
731
+ */
732
+ private async _configureAndroidRootBuildGradle(androidPath: string): Promise<void> {
733
+ const rootBuildGradlePath = pathx.posixResolve(androidPath, "build.gradle");
734
+
735
+ if (!(await fsx.exists(rootBuildGradlePath))) {
736
+ Capacitor._logger.warn(`루트 build.gradle 파일을 찾을 수 없습니다: ${rootBuildGradlePath}`);
737
+ return;
738
+ }
739
+
740
+ let content = await fsx.read(rootBuildGradlePath);
741
+
742
+ if (!content.includes("kotlin-gradle-plugin")) {
743
+ content = content.replace(
744
+ /classpath 'com\.android\.tools\.build:gradle:[^']+'/,
745
+ `$&\n classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20'`,
746
+ );
747
+ await fsx.write(rootBuildGradlePath, content);
748
+ }
749
+ }
750
+
673
751
  /**
674
752
  * build.gradle 수정 (서명 설정 제외)
675
753
  */
@@ -710,6 +788,51 @@ export default config;
710
788
  await fsx.write(buildGradlePath, content);
711
789
  }
712
790
 
791
+ /**
792
+ * styles.xml의 스플래시 테마 수정
793
+ *
794
+ * 1. Theme.SplashScreen parent → Theme.AppCompat.DayNight.NoActionBar
795
+ * Theme.SplashScreen은 android:windowBackground에 compat_splash_screen을 설정하여
796
+ * android:background(@drawable/splash)와 이중 표시를 발생시킨다.
797
+ * installSplashScreen()을 호출하지 않으므로 Theme.SplashScreen 기능이 불필요하다.
798
+ *
799
+ * 2. android:background → android:windowBackground
800
+ * android:background는 View 레벨 속성으로 AppCompat 뷰 계층의 여러 View에 상속되어
801
+ * 동일한 splash 로고가 다중 레이어에 중복 렌더링된다.
802
+ * android:windowBackground는 Window의 DecorView에만 적용되어 단일 렌더링을 보장한다.
803
+ */
804
+ private async _configureAndroidStyles(androidPath: string): Promise<void> {
805
+ const stylesPath = pathx.posixResolve(androidPath, "app/src/main/res/values/styles.xml");
806
+
807
+ if (!(await fsx.exists(stylesPath))) {
808
+ Capacitor._logger.warn(`styles.xml 파일을 찾을 수 없습니다: ${stylesPath}`);
809
+ return;
810
+ }
811
+
812
+ let content = await fsx.read(stylesPath);
813
+ let changed = false;
814
+
815
+ if (content.includes('parent="Theme.SplashScreen"')) {
816
+ content = content.replace(
817
+ 'parent="Theme.SplashScreen"',
818
+ 'parent="Theme.AppCompat.DayNight.NoActionBar"',
819
+ );
820
+ changed = true;
821
+ }
822
+
823
+ if (content.includes('"android:background">@drawable/splash')) {
824
+ content = content.replace(
825
+ '"android:background">@drawable/splash',
826
+ '"android:windowBackground">@drawable/splash',
827
+ );
828
+ changed = true;
829
+ }
830
+
831
+ if (changed) {
832
+ await fsx.write(stylesPath, content);
833
+ }
834
+ }
835
+
713
836
  //#endregion
714
837
 
715
838
  //#region Public — 기기 실행
@@ -719,28 +842,35 @@ export default config;
719
842
  *
720
843
  * 1. capacitor.config.ts에 server.url 설정 (Hot Reload용)
721
844
  * 2. cap copy — 웹 에셋 동기화
722
- * 3. cap run — 기기에서 앱 실행 (실패 시 adb kill-server 후 1회 재시도)
845
+ * 3. cap run — 기기에서 앱 실행
723
846
  */
724
847
  async run(url: string): Promise<void> {
848
+ Capacitor._logger.debug(`server.url 설정: ${url}`);
725
849
  await this._updateServerUrl(url);
726
850
 
727
851
  for (const platform of this._platforms) {
852
+ Capacitor._logger.debug(`[${platform}] cap copy 시작`);
728
853
  await this._execCap(["copy", platform]);
854
+ Capacitor._logger.debug(`[${platform}] cap copy 완료`);
729
855
 
730
856
  try {
857
+ Capacitor._logger.debug(`[${platform}] cap run 시작`);
731
858
  await this._execCap(["run", platform]);
859
+ Capacitor._logger.debug(`[${platform}] cap run 완료`);
732
860
  } catch (err) {
733
861
  if (platform === "android") {
734
- Capacitor._logger.warn("cap run 실패. adb kill-server 후 재시도합니다.");
862
+ Capacitor._logger.debug(`[${platform}] adb kill-server 시작`);
735
863
  try {
736
864
  await this._exec("adb", ["kill-server"], this._capPath);
737
- } catch {
738
- // adb kill-server 실패는 무시
865
+ Capacitor._logger.debug(`[${platform}] adb kill-server 완료`);
866
+ } catch (adbErr) {
867
+ const adbErrMsg = adbErr instanceof Error ? adbErr.message : String(adbErr);
868
+ Capacitor._logger.debug(
869
+ `[${platform}] adb kill-server 실패 (무시): ${adbErrMsg}`,
870
+ );
739
871
  }
740
- await this._execCap(["run", platform]);
741
- } else {
742
- throw err;
743
872
  }
873
+ throw err;
744
874
  }
745
875
  }
746
876
  }
@@ -779,30 +909,40 @@ export default config;
779
909
  * 4. 빌드 산출물 복사
780
910
  */
781
911
  async build(outPath: string): Promise<void> {
912
+ Capacitor._logger.debug("build 시작");
913
+
782
914
  // 1. 웹 에셋 동기화
915
+ Capacitor._logger.debug("cap copy 시작");
783
916
  await this._execCap(["copy"]);
917
+ Capacitor._logger.debug("cap copy 완료");
784
918
 
785
919
  // 2. 빌드 타입 결정
786
920
  const isDebug = this._config.debug === true;
787
921
  const isBundle = this._config.platform?.android?.bundle === true;
788
922
  const buildType = isDebug ? "debug" : "release";
923
+ Capacitor._logger.debug(`빌드 타입: ${buildType}, bundle: ${isBundle}`);
789
924
 
790
925
  // 3. 서명 설정
791
926
  const signConfig = this._config.platform?.android?.sign;
792
927
  if (!isDebug && signConfig != null) {
793
- await this._configureSigningConfig(
794
- pathx.posixResolve(this._capPath, "android"),
795
- signConfig,
796
- );
928
+ Capacitor._logger.debug("서명 설정 시작");
929
+ await this._configureSigningConfig(pathx.posixResolve(this._capPath, "android"), signConfig);
930
+ Capacitor._logger.debug("서명 설정 완료");
797
931
  } else if (!isDebug) {
798
932
  Capacitor._logger.warn("서명 설정이 없어 unsigned 빌드가 생성됩니다.");
799
933
  }
800
934
 
801
935
  // 4. Gradle 빌드
936
+ Capacitor._logger.debug("Gradle 빌드 시작");
802
937
  await this._buildAndroid(buildType, isBundle);
938
+ Capacitor._logger.debug("Gradle 빌드 완료");
803
939
 
804
940
  // 5. 빌드 산출물 복사
941
+ Capacitor._logger.debug("빌드 산출물 복사 시작");
805
942
  await this._copyBuildOutput(outPath, buildType, isBundle);
943
+ Capacitor._logger.debug("빌드 산출물 복사 완료");
944
+
945
+ Capacitor._logger.debug("build 완료");
806
946
  }
807
947
 
808
948
  //#endregion
@@ -833,8 +973,9 @@ export default config;
833
973
  if (content.includes("signingConfigs")) return;
834
974
 
835
975
  const storeType = sign.keystoreType ?? "jks";
836
- const escapedStorePassword = sign.storePassword.replace(/'/g, "\\'");
837
- const escapedKeyPassword = sign.password.replace(/'/g, "\\'");
976
+ const escapeGroovy = (s: string) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
977
+ const escapedStorePassword = escapeGroovy(sign.storePassword);
978
+ const escapedKeyPassword = escapeGroovy(sign.password);
838
979
 
839
980
  const signingBlock = ` signingConfigs {
840
981
  release {
@@ -848,7 +989,7 @@ export default config;
848
989
  `;
849
990
 
850
991
  // signingConfigs 블록을 buildTypes 앞에 삽입
851
- content = content.replace(/(\s*buildTypes\s*\{)/, `${signingBlock}$1`);
992
+ content = content.replace(/(\s*buildTypes\s*\{)/, (match) => `\n${signingBlock}${match}`);
852
993
 
853
994
  // buildTypes.release에 signingConfig 추가
854
995
  content = content.replace(
@@ -874,11 +1015,15 @@ export default config;
874
1015
 
875
1016
  const androidPath = pathx.posixResolve(this._capPath, "android");
876
1017
  const isWindows = process.platform === "win32";
877
- const gradlew = isWindows
878
- ? pathx.posixResolve(androidPath, "gradlew.bat")
879
- : pathx.posixResolve(androidPath, "gradlew");
880
1018
 
881
- await this._exec(gradlew, [gradleTask, "--no-daemon"], androidPath);
1019
+ if (isWindows) {
1020
+ Capacitor._logger.debug(`Gradle 실행: cmd /c gradlew.bat ${gradleTask}`);
1021
+ await this._exec("cmd", ["/c", "gradlew.bat", gradleTask, "--no-daemon"], androidPath);
1022
+ } else {
1023
+ const gradlew = pathx.posixResolve(androidPath, "gradlew");
1024
+ Capacitor._logger.debug(`Gradle 실행: ${gradlew} ${gradleTask}`);
1025
+ await this._exec(gradlew, [gradleTask, "--no-daemon"], androidPath);
1026
+ }
882
1027
  }
883
1028
 
884
1029
  /**
@@ -899,11 +1044,13 @@ export default config;
899
1044
  );
900
1045
 
901
1046
  // 빌드 산출물 찾기
1047
+ Capacitor._logger.debug(`빌드 산출물 탐색: ${androidBuildPath}`);
902
1048
  const candidates = await fsx.glob(pathx.posixResolve(androidBuildPath, `app-*.${ext}`));
903
1049
  if (candidates.length === 0) {
904
1050
  throw new Error(`빌드 산출물을 찾을 수 없습니다: ${androidBuildPath}`);
905
1051
  }
906
1052
  const builtFile = candidates[0];
1053
+ Capacitor._logger.debug(`빌드 산출물: ${builtFile}`);
907
1054
  const isUnsigned = builtFile.includes("unsigned");
908
1055
 
909
1056
  // 출력 디렉토리 생성
@@ -4,17 +4,10 @@ import {
4
4
  } from "../orchestrators/BuildOrchestrator";
5
5
 
6
6
  /**
7
- * Run production build.
7
+ * BuildOrchestrator를 통해 프로덕션 빌드를 실행한다.
8
8
  *
9
- * - Load `sd.config.ts` to check build target info per package (required)
10
- * - Run lint
11
- * - Clean dist folder (clean build)
12
- * - `node`/`browser`/`neutral` target: esbuild JS build + dts generation (with type check)
13
- * - `client` target: Vite production build + typecheck (dts not needed)
14
- * - Set `process.exitCode = 1` if any step fails
15
- *
16
- * @param options - build execution options
17
- * @returns resolves on completion
9
+ * @param options - 빌드 실행 옵션
10
+ * @returns 완료 시 resolve
18
11
  */
19
12
  export async function runBuild(options: BuildOrchestratorOptions): Promise<void> {
20
13
  const orchestrator = new BuildOrchestrator(options);
@@ -34,7 +34,7 @@ async function spawnVitest(targets: string[]): Promise<CheckResult> {
34
34
  const args = ["vitest", ...targets, "--run"];
35
35
  logger.debug("vitest 실행", { args });
36
36
  logger.start("테스트 실행 중...");
37
- const result = await cpx.exec("pnpm", args, { cwd: process.cwd(), reject: false });
37
+ const result = await cpx.spawn("pnpm", args, { cwd: process.cwd(), reject: false });
38
38
  const output = result.stdout + result.stderr;
39
39
  const code = result.exitCode;
40
40
 
@@ -188,7 +188,7 @@ export async function runCheck(options: CheckOptions): Promise<void> {
188
188
  const results = await Promise.allSettled(tasks);
189
189
  logger.success("체크 실행 완료");
190
190
 
191
- // 결과 수집 (flatten arrays from executeTypecheck)
191
+ // 결과 수집 (executeTypecheck 배열 평탄화)
192
192
  const checkResults: CheckResult[] = results.flatMap((r) => {
193
193
  if (r.status === "fulfilled") {
194
194
  const value = r.value;
@@ -211,7 +211,7 @@ export async function runCheck(options: CheckOptions): Promise<void> {
211
211
  process.stdout.write(formatSection(result));
212
212
  }
213
213
 
214
- // SUMMARY
214
+ // 요약
215
215
  const failed = checkResults.filter((r) => !r.success);
216
216
  const totalErrors = checkResults.reduce((sum, r) => sum + r.errorCount, 0);
217
217
  const totalWarnings = checkResults.reduce((sum, r) => sum + r.warningCount, 0);
@@ -4,16 +4,10 @@ import {
4
4
  } from "../orchestrators/DevWatchOrchestrator";
5
5
 
6
6
  /**
7
- * Run Server packages in development mode.
7
+ * DevWatchOrchestrator를 통해 서버 패키지를 개발 모드로 실행한다.
8
8
  *
9
- * - Load `sd.config.ts` to check build target info per package (required)
10
- * - `server` target: Server Build Worker + Server Runtime Worker
11
- * - `client` target: recognized but skipped (BuildEngine not yet implemented)
12
- * - Library packages excluded from dev mode
13
- * - Terminate with SIGINT/SIGTERM signals
14
- *
15
- * @param options - dev execution options (targets, options)
16
- * @returns resolves on termination signal
9
+ * @param options - 개발 모드 실행 옵션 (targets, options)
10
+ * @returns 종료 시그널 수신 resolve
17
11
  */
18
12
  export async function runDev(options: Omit<DevWatchOrchestratorOptions, "mode">): Promise<void> {
19
13
  const orchestrator = new DevWatchOrchestrator({ mode: "dev", ...options });
@@ -14,10 +14,10 @@ export interface DeviceOptions {
14
14
  }
15
15
 
16
16
  /**
17
- * Run native app on device/desktop.
17
+ * 네이티브 앱을 디바이스/데스크톱에서 실행한다.
18
18
  *
19
- * - Electron config takes priority over Capacitor when both are present.
20
- * - If --url is not provided, auto-generates from sd.config.ts server port.
19
+ * - Electron 설정이 Capacitor보다 우선한다.
20
+ * - --url 제공되지 않으면 sd.config.ts 서버 포트로 자동 생성한다.
21
21
  */
22
22
  export async function runDevice(options: DeviceOptions): Promise<void> {
23
23
  const cwd = process.cwd();
@@ -34,7 +34,7 @@ export async function runDevice(options: DeviceOptions): Promise<void> {
34
34
  const clientConfig = pkgConfig;
35
35
  const pkgDir = pathx.posixResolve(cwd, "packages", options.package);
36
36
 
37
- // Determine server URL
37
+ // 서버 URL 결정
38
38
  let serverUrl = options.url;
39
39
  if (serverUrl == null) {
40
40
  if (typeof clientConfig.server === "number") {
@@ -46,7 +46,7 @@ export async function runDevice(options: DeviceOptions): Promise<void> {
46
46
  }
47
47
  }
48
48
 
49
- // Electron takes priority over Capacitor (v13 behavior)
49
+ // Electron Capacitor보다 우선
50
50
  if (clientConfig.electron != null) {
51
51
  logger.start(`${options.package} (electron) 실행 중...`);
52
52
  const electron = await Electron.create(pkgDir, clientConfig.electron, clientConfig.exclude);
@@ -144,7 +144,7 @@ async function ensureSshAuth(
144
144
  const privateKeyData = fs.readFileSync(keyPath);
145
145
  const publicKey = fs.readFileSync(pubKeyPath, "utf-8").trim();
146
146
 
147
- // 개인키가 암호화되어 있는지 확인
147
+ // 개인키 파싱 시도 (암호화 또는 형식 오류 시 Error 반환)
148
148
  const parsed = utils.parseKey(privateKeyData);
149
149
  const isKeyEncrypted = parsed instanceof Error;
150
150
  const sshAgent = process.env["SSH_AUTH_SOCK"];
@@ -352,7 +352,7 @@ async function publishPackage(
352
352
  logger.debug(`[${pkgName}] pnpm ${args.join(" ")}`);
353
353
  }
354
354
 
355
- await cpx.exec("pnpm", args, { cwd: pkgPath });
355
+ await cpx.spawn("pnpm", args, { cwd: pkgPath });
356
356
  } else if (publishConfig.type === "local-directory") {
357
357
  // 로컬 디렉토리에 복사
358
358
  const targetPath = replaceEnvVariables(publishConfig.path, version, projectPath);
@@ -466,7 +466,7 @@ async function computePublishLevels(
466
466
  * 2. 버전 업그레이드 (package.json + 템플릿)
467
467
  * 3. 빌드
468
468
  * 4. Git commit/tag/push (변경된 파일만 명시적으로 스테이징)
469
- * 5. pnpm 배포
469
+ * 5. 패키지 배포 (npm/로컬 디렉토리/스토리지)
470
470
  * 6. postPublish (실패해도 계속 진행)
471
471
  */
472
472
  export async function runPublish(options: PublishOptions): Promise<void> {
@@ -559,18 +559,18 @@ export async function runPublish(options: PublishOptions): Promise<void> {
559
559
  if (publishPackages.some((p) => p.config.type === "npm")) {
560
560
  logger.debug("npm 인증 검증 중...");
561
561
  try {
562
- const { stdout: whoami } = await cpx.exec("npm", ["whoami"]);
562
+ const { stdout: whoami } = await cpx.spawn("npm", ["whoami"]);
563
563
  if (whoami.trim() === "") {
564
564
  throw new Error("npm 로그인 정보를 찾을 수 없습니다.");
565
565
  }
566
566
  logger.debug(`npm 로그인 확인됨: ${whoami.trim()}`);
567
- } catch (err) {
568
- logger.error(`npm whoami 실패:`, err);
569
- /*logger.error(
570
- "npm token is invalid or expired.\n" +
571
- "Create a Granular Access Token at https://www.npmjs.com/settings/~/tokens, then:\n" +
572
- " npm config set //registry.npmjs.org/:_authToken <token>",
573
- );*/
567
+ } catch {
568
+ logger.error(
569
+ "npm 인증 실패. 로그인 상태를 확인해주세요.\n" +
570
+ " npm whoami # 현재 로그인 확인\n" +
571
+ " npm login # 로그인\n" +
572
+ " npm config set //registry.npmjs.org/:_authToken <token> # 토큰 직접 설정",
573
+ );
574
574
  process.exitCode = 1;
575
575
  return;
576
576
  }
@@ -589,19 +589,23 @@ export async function runPublish(options: PublishOptions): Promise<void> {
589
589
  if (!noBuild && hasGit) {
590
590
  logger.debug("git 커밋 상태 확인 중...");
591
591
  try {
592
- const { stdout: diff } = await cpx.exec("git", ["diff", "--name-only"]);
593
- const { stdout: stagedDiff } = await cpx.exec("git", ["diff", "--cached", "--name-only"]);
592
+ const { stdout: diff } = await cpx.spawn("git", ["diff", "--name-only"]);
593
+ const { stdout: stagedDiff } = await cpx.spawn("git", ["diff", "--cached", "--name-only"]);
594
594
 
595
595
  if (diff.trim() !== "" || stagedDiff.trim() !== "") {
596
596
  logger.info("커밋되지 않은 변경사항 감지. claude로 자동 커밋 시도 중...");
597
597
  try {
598
- await cpx.exec("claude", [
599
- "-p",
600
- "/sd-commit all",
601
- "--dangerously-skip-permissions",
602
- "--model",
603
- "haiku",
604
- ], { stdio: "inherit" });
598
+ await cpx.spawn(
599
+ "claude",
600
+ ["-p", "/sd-commit", "--dangerously-skip-permissions", "--model", "haiku"],
601
+ {
602
+ stdio: "inherit",
603
+ env: {
604
+ ...process.env,
605
+ MCP_CONNECTION_NONBLOCKING: "true",
606
+ },
607
+ },
608
+ );
605
609
  } catch (e) {
606
610
  throw new Error(
607
611
  "자동 커밋에 실패했습니다. 수동으로 커밋 후 다시 시도해주세요.\n" +
@@ -683,11 +687,11 @@ export async function runPublish(options: PublishOptions): Promise<void> {
683
687
  } else {
684
688
  logger.debug("Git commit/tag/push...");
685
689
  try {
686
- await cpx.exec("git", ["add", ..._changedFiles]);
687
- await cpx.exec("git", ["commit", "-m", `v${version}`]);
688
- await cpx.exec("git", ["tag", "-a", `v${version}`, "-m", `v${version}`]);
689
- await cpx.exec("git", ["push"]);
690
- await cpx.exec("git", ["push", "--tags"]);
690
+ await cpx.spawn("git", ["add", ..._changedFiles]);
691
+ await cpx.spawn("git", ["commit", "-m", `v${version}`]);
692
+ await cpx.spawn("git", ["tag", "-a", `v${version}`, "-m", `v${version}`]);
693
+ await cpx.spawn("git", ["push"]);
694
+ await cpx.spawn("git", ["push", "--tags"]);
691
695
  logger.debug("Git 작업 완료");
692
696
  } catch (err) {
693
697
  logger.error(
@@ -806,7 +810,7 @@ export async function runPublish(options: PublishOptions): Promise<void> {
806
810
  logger.info(`[DRY-RUN] 실행 예정: ${cmd} ${args.join(" ")}`);
807
811
  } else {
808
812
  logger.debug(`실행 중: ${cmd} ${args.join(" ")}`);
809
- await cpx.exec(cmd, args, { cwd });
813
+ await cpx.spawn(cmd, args, { cwd });
810
814
  }
811
815
  } catch (err) {
812
816
  // postPublish 실패 시 경고만 출력 (배포 롤백 불가)