@simplysm/sd-cli 12.16.4 → 12.16.5

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.
@@ -15,20 +15,24 @@ export declare class SdCliCapacitor {
15
15
  private static _execAsync;
16
16
  initializeAsync(): Promise<void>;
17
17
  private _initializeCapacitorProjectAsync;
18
- private _syncVersion;
19
- private _createCapacitorConfig;
18
+ private _syncVersionAsync;
19
+ private _createCapacitorConfigAsync;
20
20
  private _managePlatformsAsync;
21
21
  private _managePluginsAsync;
22
- private _setupAndroidSign;
22
+ private _setupAndroidSignAsync;
23
23
  private _setupIconAndSplashScreenAsync;
24
- private _configureAndroidNative;
25
- private _configureAndroidManifest;
26
- private _configureAndroidBuildGradle;
27
- private _configureAndroidStrings;
24
+ private _configureAndroidNativeAsync;
25
+ private _configureAndroidGradlePropertiesAsync;
26
+ private _findJava21;
27
+ private _configureSdkPathAsync;
28
+ private _configureAndroidManifestAsync;
29
+ private _configureAndroidBuildGradleAsync;
30
+ private _configureAndroidStringsAsync;
28
31
  buildAsync(outPath: string): Promise<void>;
29
32
  private _buildPlatformAsync;
30
33
  private _buildAndroidAsync;
31
- private _copyAndroidBuildOutput;
34
+ private _copyAndroidBuildOutputAsync;
35
+ private _findAndroidSdk;
32
36
  static runWebviewOnDeviceAsync(opt: {
33
37
  platform: string;
34
38
  package: string;
@@ -9,7 +9,7 @@ export class SdCliCapacitor {
9
9
  this._CONFIG_FILE_NAME = "capacitor.config.ts";
10
10
  this._KEYSTORE_FILE_NAME = "android.keystore";
11
11
  this._ICON_DIR_PATH = "resources";
12
- this._platforms = Object.keys(this._opt.config.platform ?? { android: {} });
12
+ this._platforms = Object.keys(this._opt.config.platform ?? {});
13
13
  this._npmConfig = FsUtils.readJson(path.resolve(this._opt.pkgPath, "package.json"));
14
14
  }
15
15
  static { this._logger = SdLogger.get(["simplysm", "sd-cli", "SdCliCapacitor"]); }
@@ -23,18 +23,18 @@ export class SdCliCapacitor {
23
23
  // 1. Capacitor 프로젝트 초기화
24
24
  await this._initializeCapacitorProjectAsync(capacitorPath);
25
25
  // 2. Capacitor 설정 파일 생성
26
- this._createCapacitorConfig(capacitorPath);
26
+ await this._createCapacitorConfigAsync(capacitorPath);
27
27
  // 3. 플랫폼 관리
28
28
  await this._managePlatformsAsync(capacitorPath);
29
29
  // 4. 플러그인 관리
30
30
  await this._managePluginsAsync(capacitorPath);
31
31
  // 5. 안드로이드 서명 설정
32
- this._setupAndroidSign(capacitorPath);
32
+ await this._setupAndroidSignAsync(capacitorPath);
33
33
  // 6. 아이콘 및 스플래시 스크린 설정
34
34
  await this._setupIconAndSplashScreenAsync(capacitorPath);
35
35
  // 7. Android 네이티브 설정 (AndroidManifest.xml, build.gradle 등)
36
36
  if (this._platforms.includes("android")) {
37
- this._configureAndroidNative(capacitorPath);
37
+ await this._configureAndroidNativeAsync(capacitorPath);
38
38
  }
39
39
  // 8. 웹 자산 동기화
40
40
  await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
@@ -44,44 +44,56 @@ export class SdCliCapacitor {
44
44
  if (FsUtils.exists(capacitorPath)) {
45
45
  SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
46
46
  // 버전 동기화
47
- this._syncVersion(capacitorPath);
47
+ await this._syncVersionAsync(capacitorPath);
48
48
  }
49
49
  else {
50
- FsUtils.mkdirs(capacitorPath);
50
+ await FsUtils.mkdirsAsync(capacitorPath);
51
51
  // package.json 생성
52
+ const projNpmConfig = await FsUtils.readJsonAsync(path.resolve(this._opt.pkgPath, "../../package.json"));
52
53
  const pkgJson = {
53
54
  name: this._opt.config.appId,
54
55
  version: this._npmConfig.version,
55
56
  private: true,
57
+ volta: projNpmConfig.volta,
56
58
  dependencies: {
57
59
  "@capacitor/core": "^7.0.0",
58
60
  },
59
61
  devDependencies: {
60
62
  "@capacitor/cli": "^7.0.0",
61
63
  "@capacitor/assets": "^3.0.0",
64
+ ...this._platforms.toObject((item) => `@capacitor/${item}`, () => "^7.0.0"),
62
65
  },
63
66
  };
64
- FsUtils.writeJson(path.resolve(capacitorPath, "package.json"), pkgJson, { space: 2 });
67
+ await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
68
+ space: 2,
69
+ });
70
+ // .yarnrc.yml 작성
71
+ await FsUtils.writeFileAsync(path.resolve(capacitorPath, ".yarnrc.yml"), "nodeLinker: node-modules");
72
+ // yarn.lock 작성
73
+ await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
65
74
  // yarn install
66
75
  await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
67
76
  // capacitor init
68
77
  await SdCliCapacitor._execAsync("npx", ["cap", "init", this._opt.config.appName, this._opt.config.appId], capacitorPath);
69
78
  }
79
+ // www/index.html 생성
80
+ const wwwPath = path.resolve(capacitorPath, "www");
81
+ await FsUtils.writeFileAsync(path.resolve(wwwPath, "index.html"), "<!DOCTYPE html><html><head></head><body></body></html>");
70
82
  }
71
83
  // 버전 동기화
72
- _syncVersion(capacitorPath) {
84
+ async _syncVersionAsync(capacitorPath) {
73
85
  const pkgJsonPath = path.resolve(capacitorPath, "package.json");
74
86
  if (FsUtils.exists(pkgJsonPath)) {
75
- const pkgJson = FsUtils.readJson(pkgJsonPath);
87
+ const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath));
76
88
  if (pkgJson.version !== this._npmConfig.version) {
77
89
  pkgJson.version = this._npmConfig.version;
78
- FsUtils.writeJson(pkgJsonPath, pkgJson, { space: 2 });
90
+ await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
79
91
  SdCliCapacitor._logger.log(`버전 동기화: ${this._npmConfig.version}`);
80
92
  }
81
93
  }
82
94
  }
83
95
  // 2. Capacitor 설정 파일 생성
84
- _createCapacitorConfig(capacitorPath) {
96
+ async _createCapacitorConfigAsync(capacitorPath) {
85
97
  const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
86
98
  // 플러그인 옵션 생성
87
99
  const pluginOptions = {};
@@ -114,7 +126,7 @@ export class SdCliCapacitor {
114
126
 
115
127
  export default config;
116
128
  `;
117
- FsUtils.writeFile(configFilePath, configContent);
129
+ await FsUtils.writeFileAsync(configFilePath, configContent);
118
130
  }
119
131
  // 3. 플랫폼 관리
120
132
  async _managePlatformsAsync(capacitorPath) {
@@ -127,7 +139,7 @@ export class SdCliCapacitor {
127
139
  // 4. 플러그인 관리
128
140
  async _managePluginsAsync(capacitorPath) {
129
141
  const pkgJsonPath = path.resolve(capacitorPath, "package.json");
130
- const pkgJson = FsUtils.readJson(pkgJsonPath);
142
+ const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath));
131
143
  const currentDeps = Object.keys(pkgJson.dependencies ?? {});
132
144
  const usePlugins = Object.keys(this._opt.config.plugins ?? {});
133
145
  // 사용하지 않는 플러그인 제거
@@ -154,24 +166,23 @@ export class SdCliCapacitor {
154
166
  // 새 플러그인 설치
155
167
  for (const plugin of usePlugins) {
156
168
  if (!currentDeps.includes(plugin)) {
157
- try {
158
- await SdCliCapacitor._execAsync("yarn", ["add", plugin], capacitorPath);
159
- SdCliCapacitor._logger.log(`플러그인 설치: ${plugin}`);
160
- }
161
- catch {
162
- SdCliCapacitor._logger.warn(`플러그인 설치 실패: ${plugin}`);
163
- }
169
+ await SdCliCapacitor._execAsync("yarn", ["add", plugin], capacitorPath);
170
+ SdCliCapacitor._logger.log(`플러그인 설치: ${plugin}`);
171
+ /*try {
172
+ } catch {
173
+ SdCliCapacitor._logger.warn(`플러그인 설치 실패: ${plugin}`);
174
+ }*/
164
175
  }
165
176
  }
166
177
  }
167
178
  // 5. 안드로이드 서명 설정
168
- _setupAndroidSign(capacitorPath) {
179
+ async _setupAndroidSignAsync(capacitorPath) {
169
180
  const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
170
181
  if (this._opt.config.platform?.android?.sign) {
171
- FsUtils.copy(path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore), keystorePath);
182
+ await FsUtils.copyAsync(path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore), keystorePath);
172
183
  }
173
184
  else {
174
- FsUtils.remove(keystorePath);
185
+ await FsUtils.removeAsync(keystorePath);
175
186
  }
176
187
  }
177
188
  // 6. 아이콘 및 스플래시 스크린 설정
@@ -179,11 +190,11 @@ export class SdCliCapacitor {
179
190
  const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
180
191
  // ICON 파일 복사
181
192
  if (this._opt.config.icon != null) {
182
- FsUtils.mkdirs(iconDirPath);
193
+ await FsUtils.mkdirsAsync(iconDirPath);
183
194
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
184
195
  // icon.png, splash.png 둘 다 같은 파일 사용
185
- FsUtils.copy(iconSource, path.resolve(iconDirPath, "icon.png"));
186
- FsUtils.copy(iconSource, path.resolve(iconDirPath, "splash.png"));
196
+ await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "icon.png"));
197
+ await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
187
198
  // @capacitor/assets로 아이콘/스플래시 리사이징
188
199
  try {
189
200
  await SdCliCapacitor._execAsync("npx", ["@capacitor/assets", "generate", "--android"], capacitorPath);
@@ -193,28 +204,73 @@ export class SdCliCapacitor {
193
204
  }
194
205
  }
195
206
  else {
196
- FsUtils.remove(iconDirPath);
207
+ await FsUtils.removeAsync(iconDirPath);
197
208
  }
198
209
  }
199
210
  // 7. Android 네이티브 설정
200
- _configureAndroidNative(capacitorPath) {
211
+ async _configureAndroidNativeAsync(capacitorPath) {
201
212
  const androidPath = path.resolve(capacitorPath, "android");
202
213
  if (!FsUtils.exists(androidPath)) {
203
214
  return;
204
215
  }
216
+ // JAVA_HOME 찾기
217
+ await this._configureAndroidGradlePropertiesAsync(androidPath);
218
+ // local.properties 생성
219
+ await this._configureSdkPathAsync(androidPath);
205
220
  // AndroidManifest.xml 수정
206
- this._configureAndroidManifest(androidPath);
221
+ await this._configureAndroidManifestAsync(androidPath);
207
222
  // build.gradle 수정 (필요시)
208
- this._configureAndroidBuildGradle(androidPath);
223
+ await this._configureAndroidBuildGradleAsync(androidPath);
209
224
  // strings.xml 앱 이름 수정
210
- this._configureAndroidStrings(androidPath);
225
+ await this._configureAndroidStringsAsync(androidPath);
226
+ }
227
+ async _configureAndroidGradlePropertiesAsync(androidPath) {
228
+ const gradlePropsPath = path.resolve(androidPath, "gradle.properties");
229
+ if (!FsUtils.exists(gradlePropsPath)) {
230
+ return;
231
+ }
232
+ let content = await FsUtils.readFileAsync(gradlePropsPath);
233
+ // Java 21 경로 자동 탐색
234
+ const java21Path = this._findJava21();
235
+ if (java21Path != null && !content.includes("org.gradle.java.home")) {
236
+ content += `\norg.gradle.java.home=${java21Path.replace(/\\/g, "\\\\")}\n`;
237
+ FsUtils.writeFile(gradlePropsPath, content);
238
+ }
211
239
  }
212
- _configureAndroidManifest(androidPath) {
240
+ _findJava21() {
241
+ const patterns = [
242
+ "C:/Program Files/Amazon Corretto/jdk21*",
243
+ "C:/Program Files/Eclipse Adoptium/jdk-21*",
244
+ "C:/Program Files/Java/jdk-21*",
245
+ "C:/Program Files/Microsoft/jdk-21*",
246
+ ];
247
+ for (const pattern of patterns) {
248
+ const matches = FsUtils.glob(pattern);
249
+ if (matches.length > 0) {
250
+ // 가장 최신 버전 선택 (정렬 후 마지막)
251
+ return matches.sort().at(-1);
252
+ }
253
+ }
254
+ return undefined;
255
+ }
256
+ async _configureSdkPathAsync(androidPath) {
257
+ // local.properties 생성
258
+ const localPropsPath = path.resolve(androidPath, "local.properties");
259
+ if (FsUtils.exists(localPropsPath)) {
260
+ return;
261
+ }
262
+ // SDK 경로 탐색 (Cordova 방식과 유사)
263
+ const sdkPath = this._findAndroidSdk();
264
+ if (sdkPath != null) {
265
+ await FsUtils.writeFileAsync(localPropsPath, `sdk.dir=${sdkPath.replace(/\\/g, "/")}\n`);
266
+ }
267
+ }
268
+ async _configureAndroidManifestAsync(androidPath) {
213
269
  const manifestPath = path.resolve(androidPath, "app/src/main/AndroidManifest.xml");
214
270
  if (!FsUtils.exists(manifestPath)) {
215
271
  return;
216
272
  }
217
- let manifestContent = FsUtils.readFile(manifestPath);
273
+ let manifestContent = await FsUtils.readFileAsync(manifestPath);
218
274
  // usesCleartextTraffic 설정
219
275
  if (!manifestContent.includes("android:usesCleartextTraffic")) {
220
276
  manifestContent = manifestContent.replace("<application", '<application android:usesCleartextTraffic="true"');
@@ -244,14 +300,14 @@ export class SdCliCapacitor {
244
300
  }
245
301
  }
246
302
  }
247
- FsUtils.writeFile(manifestPath, manifestContent);
303
+ await FsUtils.writeFileAsync(manifestPath, manifestContent);
248
304
  }
249
- _configureAndroidBuildGradle(androidPath) {
305
+ async _configureAndroidBuildGradleAsync(androidPath) {
250
306
  const buildGradlePath = path.resolve(androidPath, "app/build.gradle");
251
307
  if (!FsUtils.exists(buildGradlePath)) {
252
308
  return;
253
309
  }
254
- let gradleContent = FsUtils.readFile(buildGradlePath);
310
+ let gradleContent = await FsUtils.readFileAsync(buildGradlePath);
255
311
  // versionName, versionCode 설정
256
312
  const version = this._npmConfig.version;
257
313
  const versionParts = version.split(".");
@@ -292,19 +348,19 @@ export class SdCliCapacitor {
292
348
  gradleContent = gradleContent.replace(/(buildTypes\s*\{[\s\S]*?release\s*\{)/, `$1\n signingConfig signingConfigs.release`);
293
349
  }
294
350
  }
295
- FsUtils.writeFile(buildGradlePath, gradleContent);
351
+ await FsUtils.writeFileAsync(buildGradlePath, gradleContent);
296
352
  }
297
- _configureAndroidStrings(androidPath) {
353
+ async _configureAndroidStringsAsync(androidPath) {
298
354
  const stringsPath = path.resolve(androidPath, "app/src/main/res/values/strings.xml");
299
355
  if (!FsUtils.exists(stringsPath)) {
300
356
  return;
301
357
  }
302
- let stringsContent = FsUtils.readFile(stringsPath);
358
+ let stringsContent = await FsUtils.readFileAsync(stringsPath);
303
359
  stringsContent = stringsContent.replace(/<string name="app_name">[^<]+<\/string>/, `<string name="app_name">${this._opt.config.appName}</string>`);
304
360
  stringsContent = stringsContent.replace(/<string name="title_activity_main">[^<]+<\/string>/, `<string name="title_activity_main">${this._opt.config.appName}</string>`);
305
361
  stringsContent = stringsContent.replace(/<string name="package_name">[^<]+<\/string>/, `<string name="package_name">${this._opt.config.appId}</string>`);
306
362
  stringsContent = stringsContent.replace(/<string name="custom_url_scheme">[^<]+<\/string>/, `<string name="custom_url_scheme">${this._opt.config.appId}</string>`);
307
- FsUtils.writeFile(stringsPath, stringsContent);
363
+ await FsUtils.writeFileAsync(stringsPath, stringsContent);
308
364
  }
309
365
  async buildAsync(outPath) {
310
366
  const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
@@ -340,9 +396,9 @@ export class SdCliCapacitor {
340
396
  const gradleCmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
341
397
  await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
342
398
  // 빌드 결과물 복사
343
- this._copyAndroidBuildOutput(androidPath, targetOutPath, buildType);
399
+ await this._copyAndroidBuildOutputAsync(androidPath, targetOutPath, buildType);
344
400
  }
345
- _copyAndroidBuildOutput(androidPath, targetOutPath, buildType) {
401
+ async _copyAndroidBuildOutputAsync(androidPath, targetOutPath, buildType) {
346
402
  const isBundle = this._opt.config.platform?.android?.bundle;
347
403
  const isSigned = !!this._opt.config.platform?.android?.sign;
348
404
  const ext = isBundle ? "aab" : "apk";
@@ -357,21 +413,41 @@ export class SdCliCapacitor {
357
413
  return;
358
414
  }
359
415
  const outputFileName = `${this._opt.config.appName}${isSigned ? "" : "-unsigned"}-latest.${ext}`;
360
- FsUtils.mkdirs(targetOutPath);
361
- FsUtils.copy(actualPath, path.resolve(targetOutPath, outputFileName));
416
+ await FsUtils.mkdirsAsync(targetOutPath);
417
+ await FsUtils.copyAsync(actualPath, path.resolve(targetOutPath, outputFileName));
362
418
  const updatesPath = path.resolve(targetOutPath, "updates");
363
- FsUtils.mkdirs(updatesPath);
364
- FsUtils.copy(actualPath, path.resolve(updatesPath, `${this._npmConfig.version}.${ext}`));
419
+ await FsUtils.mkdirsAsync(updatesPath);
420
+ await FsUtils.copyAsync(actualPath, path.resolve(updatesPath, `${this._npmConfig.version}.${ext}`));
421
+ }
422
+ _findAndroidSdk() {
423
+ // 1. 환경변수 확인
424
+ const fromEnv = process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"];
425
+ if (fromEnv != null && FsUtils.exists(fromEnv)) {
426
+ return fromEnv;
427
+ }
428
+ // 2. 일반적인 설치 경로 탐색
429
+ const candidates = [
430
+ path.resolve(process.env["LOCALAPPDATA"] ?? "", "Android/Sdk"),
431
+ path.resolve(process.env["HOME"] ?? "", "Android/Sdk"),
432
+ "C:/Program Files/Android/Sdk",
433
+ "C:/Android/Sdk",
434
+ ];
435
+ for (const candidate of candidates) {
436
+ if (FsUtils.exists(candidate)) {
437
+ return candidate;
438
+ }
439
+ }
440
+ return undefined;
365
441
  }
366
442
  static async runWebviewOnDeviceAsync(opt) {
367
- const projNpmConf = FsUtils.readJson(path.resolve(process.cwd(), "package.json"));
368
- const allPkgPaths = projNpmConf.workspaces.mapMany((item) => FsUtils.glob(PathUtils.posix(process.cwd(), item)));
443
+ const projNpmConf = (await FsUtils.readJsonAsync(path.resolve(process.cwd(), "package.json")));
444
+ const allPkgPaths = await projNpmConf.workspaces.mapManyAsync(async (item) => await FsUtils.globAsync(PathUtils.posix(process.cwd(), item)));
369
445
  const capacitorPath = path.resolve(allPkgPaths.single((item) => item.endsWith(opt.package)), ".capacitor");
370
446
  if (opt.url !== undefined) {
371
447
  // capacitor.config.ts의 server.url 설정 업데이트
372
448
  const configPath = path.resolve(capacitorPath, "capacitor.config.ts");
373
449
  if (FsUtils.exists(configPath)) {
374
- let configContent = FsUtils.readFile(configPath);
450
+ let configContent = await FsUtils.readFileAsync(configPath);
375
451
  const serverUrl = `${opt.url.replace(/\/$/, "")}/${opt.package}/capacitor/`;
376
452
  // 기존 url 설정이 있으면 교체, 없으면 server 블록 첫 줄에 추가
377
453
  if (configContent.includes("url:")) {
@@ -380,11 +456,16 @@ export class SdCliCapacitor {
380
456
  else if (configContent.includes("server:")) {
381
457
  configContent = configContent.replace(/server:\s*\{/, `server: {\n url: "${serverUrl}",`);
382
458
  }
383
- FsUtils.writeFile(configPath, configContent);
459
+ await FsUtils.writeFileAsync(configPath, configContent);
384
460
  }
385
461
  }
386
462
  // cap sync 후 run
387
463
  await this._execAsync("npx", ["cap", "sync", opt.platform], capacitorPath);
388
- await this._execAsync("npx", ["cap", "run", opt.platform, "--target", "device"], capacitorPath);
464
+ try {
465
+ await this._execAsync("npx", ["cap", "run", opt.platform], capacitorPath);
466
+ }
467
+ catch {
468
+ await SdProcess.spawnAsync("adb", ["kill-server"]);
469
+ }
389
470
  }
390
471
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/sd-cli",
3
- "version": "12.16.4",
3
+ "version": "12.16.5",
4
4
  "description": "심플리즘 패키지 - CLI",
5
5
  "author": "김석래",
6
6
  "repository": {
@@ -17,10 +17,10 @@
17
17
  "@angular/compiler-cli": "^20.3.15",
18
18
  "@anthropic-ai/sdk": "^0.71.2",
19
19
  "@electron/rebuild": "^4.0.2",
20
- "@simplysm/sd-core-common": "12.16.4",
21
- "@simplysm/sd-core-node": "12.16.4",
22
- "@simplysm/sd-service-server": "12.16.4",
23
- "@simplysm/sd-storage": "12.16.4",
20
+ "@simplysm/sd-core-common": "12.16.5",
21
+ "@simplysm/sd-core-node": "12.16.5",
22
+ "@simplysm/sd-service-server": "12.16.5",
23
+ "@simplysm/sd-storage": "12.16.5",
24
24
  "browserslist": "^4.28.1",
25
25
  "cordova": "^13.0.0",
26
26
  "electron": "^33.4.11",
@@ -18,7 +18,7 @@ export class SdCliCapacitor {
18
18
  private readonly _npmConfig: INpmConfig;
19
19
 
20
20
  constructor(private readonly _opt: { pkgPath: string; config: ISdClientBuilderCapacitorConfig }) {
21
- this._platforms = Object.keys(this._opt.config.platform ?? { android: {} });
21
+ this._platforms = Object.keys(this._opt.config.platform ?? {});
22
22
  this._npmConfig = FsUtils.readJson(path.resolve(this._opt.pkgPath, "package.json"));
23
23
  }
24
24
 
@@ -37,7 +37,7 @@ export class SdCliCapacitor {
37
37
  await this._initializeCapacitorProjectAsync(capacitorPath);
38
38
 
39
39
  // 2. Capacitor 설정 파일 생성
40
- this._createCapacitorConfig(capacitorPath);
40
+ await this._createCapacitorConfigAsync(capacitorPath);
41
41
 
42
42
  // 3. 플랫폼 관리
43
43
  await this._managePlatformsAsync(capacitorPath);
@@ -46,14 +46,14 @@ export class SdCliCapacitor {
46
46
  await this._managePluginsAsync(capacitorPath);
47
47
 
48
48
  // 5. 안드로이드 서명 설정
49
- this._setupAndroidSign(capacitorPath);
49
+ await this._setupAndroidSignAsync(capacitorPath);
50
50
 
51
51
  // 6. 아이콘 및 스플래시 스크린 설정
52
52
  await this._setupIconAndSplashScreenAsync(capacitorPath);
53
53
 
54
54
  // 7. Android 네이티브 설정 (AndroidManifest.xml, build.gradle 등)
55
55
  if (this._platforms.includes("android")) {
56
- this._configureAndroidNative(capacitorPath);
56
+ await this._configureAndroidNativeAsync(capacitorPath);
57
57
  }
58
58
 
59
59
  // 8. 웹 자산 동기화
@@ -66,24 +66,43 @@ export class SdCliCapacitor {
66
66
  SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
67
67
 
68
68
  // 버전 동기화
69
- this._syncVersion(capacitorPath);
69
+ await this._syncVersionAsync(capacitorPath);
70
70
  } else {
71
- FsUtils.mkdirs(capacitorPath);
71
+ await FsUtils.mkdirsAsync(capacitorPath);
72
72
 
73
73
  // package.json 생성
74
+ const projNpmConfig = await FsUtils.readJsonAsync(
75
+ path.resolve(this._opt.pkgPath, "../../package.json"),
76
+ );
74
77
  const pkgJson = {
75
78
  name: this._opt.config.appId,
76
79
  version: this._npmConfig.version,
77
80
  private: true,
81
+ volta: projNpmConfig.volta,
78
82
  dependencies: {
79
83
  "@capacitor/core": "^7.0.0",
80
84
  },
81
85
  devDependencies: {
82
86
  "@capacitor/cli": "^7.0.0",
83
87
  "@capacitor/assets": "^3.0.0",
88
+ ...this._platforms.toObject(
89
+ (item) => `@capacitor/${item}`,
90
+ () => "^7.0.0",
91
+ ),
84
92
  },
85
93
  };
86
- FsUtils.writeJson(path.resolve(capacitorPath, "package.json"), pkgJson, { space: 2 });
94
+ await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
95
+ space: 2,
96
+ });
97
+
98
+ // .yarnrc.yml 작성
99
+ await FsUtils.writeFileAsync(
100
+ path.resolve(capacitorPath, ".yarnrc.yml"),
101
+ "nodeLinker: node-modules",
102
+ );
103
+
104
+ // yarn.lock 작성
105
+ await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
87
106
 
88
107
  // yarn install
89
108
  await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
@@ -95,25 +114,32 @@ export class SdCliCapacitor {
95
114
  capacitorPath,
96
115
  );
97
116
  }
117
+
118
+ // www/index.html 생성
119
+ const wwwPath = path.resolve(capacitorPath, "www");
120
+ await FsUtils.writeFileAsync(
121
+ path.resolve(wwwPath, "index.html"),
122
+ "<!DOCTYPE html><html><head></head><body></body></html>",
123
+ );
98
124
  }
99
125
 
100
126
  // 버전 동기화
101
- private _syncVersion(capacitorPath: string): void {
127
+ private async _syncVersionAsync(capacitorPath: string) {
102
128
  const pkgJsonPath = path.resolve(capacitorPath, "package.json");
103
129
 
104
130
  if (FsUtils.exists(pkgJsonPath)) {
105
- const pkgJson = FsUtils.readJson(pkgJsonPath) as INpmConfig;
131
+ const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
106
132
 
107
133
  if (pkgJson.version !== this._npmConfig.version) {
108
134
  pkgJson.version = this._npmConfig.version;
109
- FsUtils.writeJson(pkgJsonPath, pkgJson, { space: 2 });
135
+ await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
110
136
  SdCliCapacitor._logger.log(`버전 동기화: ${this._npmConfig.version}`);
111
137
  }
112
138
  }
113
139
  }
114
140
 
115
141
  // 2. Capacitor 설정 파일 생성
116
- private _createCapacitorConfig(capacitorPath: string): void {
142
+ private async _createCapacitorConfigAsync(capacitorPath: string) {
117
143
  const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
118
144
 
119
145
  // 플러그인 옵션 생성
@@ -151,7 +177,7 @@ export class SdCliCapacitor {
151
177
  export default config;
152
178
  `;
153
179
 
154
- FsUtils.writeFile(configFilePath, configContent);
180
+ await FsUtils.writeFileAsync(configFilePath, configContent);
155
181
  }
156
182
 
157
183
  // 3. 플랫폼 관리
@@ -166,7 +192,7 @@ export class SdCliCapacitor {
166
192
  // 4. 플러그인 관리
167
193
  private async _managePluginsAsync(capacitorPath: string): Promise<void> {
168
194
  const pkgJsonPath = path.resolve(capacitorPath, "package.json");
169
- const pkgJson = FsUtils.readJson(pkgJsonPath) as INpmConfig;
195
+ const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
170
196
  const currentDeps = Object.keys(pkgJson.dependencies ?? {});
171
197
 
172
198
  const usePlugins = Object.keys(this._opt.config.plugins ?? {});
@@ -198,27 +224,27 @@ export class SdCliCapacitor {
198
224
  // 새 플러그인 설치
199
225
  for (const plugin of usePlugins) {
200
226
  if (!currentDeps.includes(plugin)) {
201
- try {
202
- await SdCliCapacitor._execAsync("yarn", ["add", plugin], capacitorPath);
203
- SdCliCapacitor._logger.log(`플러그인 설치: ${plugin}`);
227
+ await SdCliCapacitor._execAsync("yarn", ["add", plugin], capacitorPath);
228
+ SdCliCapacitor._logger.log(`플러그인 설치: ${plugin}`);
229
+ /*try {
204
230
  } catch {
205
231
  SdCliCapacitor._logger.warn(`플러그인 설치 실패: ${plugin}`);
206
- }
232
+ }*/
207
233
  }
208
234
  }
209
235
  }
210
236
 
211
237
  // 5. 안드로이드 서명 설정
212
- private _setupAndroidSign(capacitorPath: string): void {
238
+ private async _setupAndroidSignAsync(capacitorPath: string) {
213
239
  const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
214
240
 
215
241
  if (this._opt.config.platform?.android?.sign) {
216
- FsUtils.copy(
242
+ await FsUtils.copyAsync(
217
243
  path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore),
218
244
  keystorePath,
219
245
  );
220
246
  } else {
221
- FsUtils.remove(keystorePath);
247
+ await FsUtils.removeAsync(keystorePath);
222
248
  }
223
249
  }
224
250
 
@@ -228,13 +254,13 @@ export class SdCliCapacitor {
228
254
 
229
255
  // ICON 파일 복사
230
256
  if (this._opt.config.icon != null) {
231
- FsUtils.mkdirs(iconDirPath);
257
+ await FsUtils.mkdirsAsync(iconDirPath);
232
258
 
233
259
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
234
260
 
235
261
  // icon.png, splash.png 둘 다 같은 파일 사용
236
- FsUtils.copy(iconSource, path.resolve(iconDirPath, "icon.png"));
237
- FsUtils.copy(iconSource, path.resolve(iconDirPath, "splash.png"));
262
+ await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "icon.png"));
263
+ await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
238
264
 
239
265
  // @capacitor/assets로 아이콘/스플래시 리사이징
240
266
  try {
@@ -247,36 +273,93 @@ export class SdCliCapacitor {
247
273
  SdCliCapacitor._logger.warn("아이콘 리사이징 실패, 기본 아이콘 사용");
248
274
  }
249
275
  } else {
250
- FsUtils.remove(iconDirPath);
276
+ await FsUtils.removeAsync(iconDirPath);
251
277
  }
252
278
  }
253
279
 
254
280
  // 7. Android 네이티브 설정
255
- private _configureAndroidNative(capacitorPath: string): void {
281
+ private async _configureAndroidNativeAsync(capacitorPath: string) {
256
282
  const androidPath = path.resolve(capacitorPath, "android");
257
283
 
258
284
  if (!FsUtils.exists(androidPath)) {
259
285
  return;
260
286
  }
261
287
 
288
+ // JAVA_HOME 찾기
289
+ await this._configureAndroidGradlePropertiesAsync(androidPath);
290
+
291
+ // local.properties 생성
292
+ await this._configureSdkPathAsync(androidPath);
293
+
262
294
  // AndroidManifest.xml 수정
263
- this._configureAndroidManifest(androidPath);
295
+ await this._configureAndroidManifestAsync(androidPath);
264
296
 
265
297
  // build.gradle 수정 (필요시)
266
- this._configureAndroidBuildGradle(androidPath);
298
+ await this._configureAndroidBuildGradleAsync(androidPath);
267
299
 
268
300
  // strings.xml 앱 이름 수정
269
- this._configureAndroidStrings(androidPath);
301
+ await this._configureAndroidStringsAsync(androidPath);
302
+ }
303
+
304
+ private async _configureAndroidGradlePropertiesAsync(androidPath: string) {
305
+ const gradlePropsPath = path.resolve(androidPath, "gradle.properties");
306
+
307
+ if (!FsUtils.exists(gradlePropsPath)) {
308
+ return;
309
+ }
310
+
311
+ let content = await FsUtils.readFileAsync(gradlePropsPath);
312
+
313
+ // Java 21 경로 자동 탐색
314
+ const java21Path = this._findJava21();
315
+ if (java21Path != null && !content.includes("org.gradle.java.home")) {
316
+ content += `\norg.gradle.java.home=${java21Path.replace(/\\/g, "\\\\")}\n`;
317
+ FsUtils.writeFile(gradlePropsPath, content);
318
+ }
319
+ }
320
+
321
+ private _findJava21(): string | undefined {
322
+ const patterns = [
323
+ "C:/Program Files/Amazon Corretto/jdk21*",
324
+ "C:/Program Files/Eclipse Adoptium/jdk-21*",
325
+ "C:/Program Files/Java/jdk-21*",
326
+ "C:/Program Files/Microsoft/jdk-21*",
327
+ ];
328
+
329
+ for (const pattern of patterns) {
330
+ const matches = FsUtils.glob(pattern);
331
+ if (matches.length > 0) {
332
+ // 가장 최신 버전 선택 (정렬 후 마지막)
333
+ return matches.sort().at(-1);
334
+ }
335
+ }
336
+
337
+ return undefined;
338
+ }
339
+
340
+ private async _configureSdkPathAsync(androidPath: string) {
341
+ // local.properties 생성
342
+ const localPropsPath = path.resolve(androidPath, "local.properties");
343
+
344
+ if (FsUtils.exists(localPropsPath)) {
345
+ return;
346
+ }
347
+
348
+ // SDK 경로 탐색 (Cordova 방식과 유사)
349
+ const sdkPath = this._findAndroidSdk();
350
+ if (sdkPath != null) {
351
+ await FsUtils.writeFileAsync(localPropsPath, `sdk.dir=${sdkPath.replace(/\\/g, "/")}\n`);
352
+ }
270
353
  }
271
354
 
272
- private _configureAndroidManifest(androidPath: string): void {
355
+ private async _configureAndroidManifestAsync(androidPath: string) {
273
356
  const manifestPath = path.resolve(androidPath, "app/src/main/AndroidManifest.xml");
274
357
 
275
358
  if (!FsUtils.exists(manifestPath)) {
276
359
  return;
277
360
  }
278
361
 
279
- let manifestContent = FsUtils.readFile(manifestPath);
362
+ let manifestContent = await FsUtils.readFileAsync(manifestPath);
280
363
 
281
364
  // usesCleartextTraffic 설정
282
365
  if (!manifestContent.includes("android:usesCleartextTraffic")) {
@@ -319,17 +402,17 @@ export class SdCliCapacitor {
319
402
  }
320
403
  }
321
404
 
322
- FsUtils.writeFile(manifestPath, manifestContent);
405
+ await FsUtils.writeFileAsync(manifestPath, manifestContent);
323
406
  }
324
407
 
325
- private _configureAndroidBuildGradle(androidPath: string): void {
408
+ private async _configureAndroidBuildGradleAsync(androidPath: string) {
326
409
  const buildGradlePath = path.resolve(androidPath, "app/build.gradle");
327
410
 
328
411
  if (!FsUtils.exists(buildGradlePath)) {
329
412
  return;
330
413
  }
331
414
 
332
- let gradleContent = FsUtils.readFile(buildGradlePath);
415
+ let gradleContent = await FsUtils.readFileAsync(buildGradlePath);
333
416
 
334
417
  // versionName, versionCode 설정
335
418
  const version = this._npmConfig.version;
@@ -384,17 +467,17 @@ export class SdCliCapacitor {
384
467
  }
385
468
  }
386
469
 
387
- FsUtils.writeFile(buildGradlePath, gradleContent);
470
+ await FsUtils.writeFileAsync(buildGradlePath, gradleContent);
388
471
  }
389
472
 
390
- private _configureAndroidStrings(androidPath: string): void {
473
+ private async _configureAndroidStringsAsync(androidPath: string) {
391
474
  const stringsPath = path.resolve(androidPath, "app/src/main/res/values/strings.xml");
392
475
 
393
476
  if (!FsUtils.exists(stringsPath)) {
394
477
  return;
395
478
  }
396
479
 
397
- let stringsContent = FsUtils.readFile(stringsPath);
480
+ let stringsContent = await FsUtils.readFileAsync(stringsPath);
398
481
  stringsContent = stringsContent.replace(
399
482
  /<string name="app_name">[^<]+<\/string>/,
400
483
  `<string name="app_name">${this._opt.config.appName}</string>`,
@@ -412,7 +495,7 @@ export class SdCliCapacitor {
412
495
  `<string name="custom_url_scheme">${this._opt.config.appId}</string>`,
413
496
  );
414
497
 
415
- FsUtils.writeFile(stringsPath, stringsContent);
498
+ await FsUtils.writeFileAsync(stringsPath, stringsContent);
416
499
  }
417
500
 
418
501
  async buildAsync(outPath: string): Promise<void> {
@@ -470,14 +553,14 @@ export class SdCliCapacitor {
470
553
  await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
471
554
 
472
555
  // 빌드 결과물 복사
473
- this._copyAndroidBuildOutput(androidPath, targetOutPath, buildType);
556
+ await this._copyAndroidBuildOutputAsync(androidPath, targetOutPath, buildType);
474
557
  }
475
558
 
476
- private _copyAndroidBuildOutput(
559
+ private async _copyAndroidBuildOutputAsync(
477
560
  androidPath: string,
478
561
  targetOutPath: string,
479
562
  buildType: string,
480
- ): void {
563
+ ) {
481
564
  const isBundle = this._opt.config.platform?.android?.bundle;
482
565
  const isSigned = !!this._opt.config.platform?.android?.sign;
483
566
 
@@ -510,12 +593,39 @@ export class SdCliCapacitor {
510
593
 
511
594
  const outputFileName = `${this._opt.config.appName}${isSigned ? "" : "-unsigned"}-latest.${ext}`;
512
595
 
513
- FsUtils.mkdirs(targetOutPath);
514
- FsUtils.copy(actualPath, path.resolve(targetOutPath, outputFileName));
596
+ await FsUtils.mkdirsAsync(targetOutPath);
597
+ await FsUtils.copyAsync(actualPath, path.resolve(targetOutPath, outputFileName));
515
598
 
516
599
  const updatesPath = path.resolve(targetOutPath, "updates");
517
- FsUtils.mkdirs(updatesPath);
518
- FsUtils.copy(actualPath, path.resolve(updatesPath, `${this._npmConfig.version}.${ext}`));
600
+ await FsUtils.mkdirsAsync(updatesPath);
601
+ await FsUtils.copyAsync(
602
+ actualPath,
603
+ path.resolve(updatesPath, `${this._npmConfig.version}.${ext}`),
604
+ );
605
+ }
606
+
607
+ private _findAndroidSdk(): string | undefined {
608
+ // 1. 환경변수 확인
609
+ const fromEnv = process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"];
610
+ if (fromEnv != null && FsUtils.exists(fromEnv)) {
611
+ return fromEnv;
612
+ }
613
+
614
+ // 2. 일반적인 설치 경로 탐색
615
+ const candidates = [
616
+ path.resolve(process.env["LOCALAPPDATA"] ?? "", "Android/Sdk"),
617
+ path.resolve(process.env["HOME"] ?? "", "Android/Sdk"),
618
+ "C:/Program Files/Android/Sdk",
619
+ "C:/Android/Sdk",
620
+ ];
621
+
622
+ for (const candidate of candidates) {
623
+ if (FsUtils.exists(candidate)) {
624
+ return candidate;
625
+ }
626
+ }
627
+
628
+ return undefined;
519
629
  }
520
630
 
521
631
  static async runWebviewOnDeviceAsync(opt: {
@@ -523,9 +633,11 @@ export class SdCliCapacitor {
523
633
  package: string;
524
634
  url?: string;
525
635
  }): Promise<void> {
526
- const projNpmConf = FsUtils.readJson(path.resolve(process.cwd(), "package.json")) as INpmConfig;
527
- const allPkgPaths = projNpmConf.workspaces!.mapMany((item) =>
528
- FsUtils.glob(PathUtils.posix(process.cwd(), item)),
636
+ const projNpmConf = (await FsUtils.readJsonAsync(
637
+ path.resolve(process.cwd(), "package.json"),
638
+ )) as INpmConfig;
639
+ const allPkgPaths = await projNpmConf.workspaces!.mapManyAsync(
640
+ async (item) => await FsUtils.globAsync(PathUtils.posix(process.cwd(), item)),
529
641
  );
530
642
 
531
643
  const capacitorPath = path.resolve(
@@ -537,7 +649,7 @@ export class SdCliCapacitor {
537
649
  // capacitor.config.ts의 server.url 설정 업데이트
538
650
  const configPath = path.resolve(capacitorPath, "capacitor.config.ts");
539
651
  if (FsUtils.exists(configPath)) {
540
- let configContent = FsUtils.readFile(configPath);
652
+ let configContent = await FsUtils.readFileAsync(configPath);
541
653
  const serverUrl = `${opt.url.replace(/\/$/, "")}/${opt.package}/capacitor/`;
542
654
 
543
655
  // 기존 url 설정이 있으면 교체, 없으면 server 블록 첫 줄에 추가
@@ -549,12 +661,17 @@ export class SdCliCapacitor {
549
661
  `server: {\n url: "${serverUrl}",`,
550
662
  );
551
663
  }
552
- FsUtils.writeFile(configPath, configContent);
664
+ await FsUtils.writeFileAsync(configPath, configContent);
553
665
  }
554
666
  }
555
667
 
556
668
  // cap sync 후 run
557
669
  await this._execAsync("npx", ["cap", "sync", opt.platform], capacitorPath);
558
- await this._execAsync("npx", ["cap", "run", opt.platform, "--target", "device"], capacitorPath);
670
+
671
+ try {
672
+ await this._execAsync("npx", ["cap", "run", opt.platform], capacitorPath);
673
+ } catch {
674
+ await SdProcess.spawnAsync("adb", ["kill-server"]);
675
+ }
559
676
  }
560
677
  }