@simplysm/sd-cli 13.0.11 → 13.0.12

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 (154) hide show
  1. package/dist/builders/BaseBuilder.d.ts +15 -4
  2. package/dist/builders/BaseBuilder.d.ts.map +1 -1
  3. package/dist/builders/BaseBuilder.js +50 -0
  4. package/dist/builders/BaseBuilder.js.map +1 -1
  5. package/dist/builders/DtsBuilder.d.ts.map +1 -1
  6. package/dist/builders/DtsBuilder.js +2 -39
  7. package/dist/builders/DtsBuilder.js.map +1 -1
  8. package/dist/builders/LibraryBuilder.d.ts.map +1 -1
  9. package/dist/builders/LibraryBuilder.js +2 -39
  10. package/dist/builders/LibraryBuilder.js.map +1 -1
  11. package/dist/builders/types.d.ts +2 -2
  12. package/dist/builders/types.d.ts.map +1 -1
  13. package/dist/capacitor/capacitor.d.ts.map +1 -1
  14. package/dist/capacitor/capacitor.js +2 -1
  15. package/dist/capacitor/capacitor.js.map +1 -1
  16. package/dist/commands/add-client.d.ts.map +1 -1
  17. package/dist/commands/add-client.js +1 -9
  18. package/dist/commands/add-client.js.map +1 -1
  19. package/dist/commands/add-server.d.ts.map +1 -1
  20. package/dist/commands/add-server.js +1 -9
  21. package/dist/commands/add-server.js.map +1 -1
  22. package/dist/commands/build.d.ts +1 -2
  23. package/dist/commands/build.d.ts.map +1 -1
  24. package/dist/commands/build.js +12 -311
  25. package/dist/commands/build.js.map +1 -1
  26. package/dist/commands/dev.d.ts +1 -1
  27. package/dist/commands/dev.d.ts.map +1 -1
  28. package/dist/commands/dev.js +11 -432
  29. package/dist/commands/dev.js.map +1 -1
  30. package/dist/commands/device.d.ts.map +1 -1
  31. package/dist/commands/device.js +17 -32
  32. package/dist/commands/device.js.map +1 -1
  33. package/dist/commands/init.d.ts.map +1 -1
  34. package/dist/commands/init.js +1 -9
  35. package/dist/commands/init.js.map +1 -1
  36. package/dist/commands/lint.d.ts +1 -1
  37. package/dist/commands/lint.d.ts.map +1 -1
  38. package/dist/commands/lint.js +71 -118
  39. package/dist/commands/lint.js.map +2 -2
  40. package/dist/commands/publish.d.ts.map +1 -1
  41. package/dist/commands/publish.js +47 -69
  42. package/dist/commands/publish.js.map +1 -1
  43. package/dist/commands/typecheck.d.ts +1 -1
  44. package/dist/commands/typecheck.d.ts.map +1 -1
  45. package/dist/commands/typecheck.js +11 -24
  46. package/dist/commands/typecheck.js.map +1 -1
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/infra/ResultCollector.d.ts +1 -1
  52. package/dist/infra/ResultCollector.d.ts.map +1 -1
  53. package/dist/infra/ResultCollector.js +1 -1
  54. package/dist/infra/ResultCollector.js.map +1 -1
  55. package/dist/orchestrators/BuildOrchestrator.d.ts +53 -0
  56. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -0
  57. package/dist/orchestrators/BuildOrchestrator.js +338 -0
  58. package/dist/orchestrators/BuildOrchestrator.js.map +6 -0
  59. package/dist/orchestrators/DevOrchestrator.d.ts +64 -0
  60. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -0
  61. package/dist/orchestrators/DevOrchestrator.js +524 -0
  62. package/dist/orchestrators/DevOrchestrator.js.map +6 -0
  63. package/dist/orchestrators/WatchOrchestrator.d.ts +1 -1
  64. package/dist/orchestrators/WatchOrchestrator.d.ts.map +1 -1
  65. package/dist/orchestrators/WatchOrchestrator.js +30 -21
  66. package/dist/orchestrators/WatchOrchestrator.js.map +1 -1
  67. package/dist/orchestrators/index.d.ts +2 -0
  68. package/dist/orchestrators/index.d.ts.map +1 -1
  69. package/dist/orchestrators/index.js +4 -0
  70. package/dist/orchestrators/index.js.map +1 -1
  71. package/dist/sd-cli-entry.js +14 -14
  72. package/dist/sd-cli-entry.js.map +1 -1
  73. package/dist/sd-cli.js +6 -4
  74. package/dist/sd-cli.js.map +1 -1
  75. package/dist/utils/output-utils.d.ts +0 -1
  76. package/dist/utils/output-utils.d.ts.map +1 -1
  77. package/dist/utils/output-utils.js +1 -1
  78. package/dist/utils/output-utils.js.map +1 -1
  79. package/dist/utils/package-utils.d.ts +5 -1
  80. package/dist/utils/package-utils.d.ts.map +1 -1
  81. package/dist/utils/package-utils.js +12 -0
  82. package/dist/utils/package-utils.js.map +1 -1
  83. package/dist/utils/rebuild-manager.d.ts +15 -0
  84. package/dist/utils/rebuild-manager.d.ts.map +1 -0
  85. package/dist/utils/rebuild-manager.js +50 -0
  86. package/dist/utils/rebuild-manager.js.map +6 -0
  87. package/dist/utils/replace-deps.d.ts.map +1 -1
  88. package/dist/utils/replace-deps.js +7 -13
  89. package/dist/utils/replace-deps.js.map +1 -1
  90. package/dist/utils/vite-config.d.ts.map +1 -1
  91. package/dist/utils/vite-config.js +43 -5
  92. package/dist/utils/vite-config.js.map +1 -1
  93. package/dist/utils/worker-events.d.ts +5 -4
  94. package/dist/utils/worker-events.d.ts.map +1 -1
  95. package/dist/utils/worker-events.js +4 -0
  96. package/dist/utils/worker-events.js.map +1 -1
  97. package/dist/utils/worker-utils.d.ts +13 -0
  98. package/dist/utils/worker-utils.d.ts.map +1 -0
  99. package/dist/utils/worker-utils.js +15 -0
  100. package/dist/utils/worker-utils.js.map +6 -0
  101. package/dist/workers/client.worker.d.ts.map +1 -1
  102. package/dist/workers/client.worker.js +2 -14
  103. package/dist/workers/client.worker.js.map +1 -1
  104. package/dist/workers/library.worker.d.ts.map +1 -1
  105. package/dist/workers/library.worker.js +2 -14
  106. package/dist/workers/library.worker.js.map +1 -1
  107. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  108. package/dist/workers/server-runtime.worker.js +11 -11
  109. package/dist/workers/server-runtime.worker.js.map +1 -1
  110. package/dist/workers/server.worker.d.ts.map +1 -1
  111. package/dist/workers/server.worker.js +2 -14
  112. package/dist/workers/server.worker.js.map +1 -1
  113. package/package.json +4 -5
  114. package/src/builders/BaseBuilder.ts +71 -4
  115. package/src/builders/DtsBuilder.ts +2 -49
  116. package/src/builders/LibraryBuilder.ts +2 -51
  117. package/src/builders/types.ts +2 -2
  118. package/src/capacitor/capacitor.ts +2 -1
  119. package/src/commands/add-client.ts +1 -14
  120. package/src/commands/add-server.ts +1 -13
  121. package/src/commands/build.ts +13 -443
  122. package/src/commands/dev.ts +12 -582
  123. package/src/commands/device.ts +17 -34
  124. package/src/commands/init.ts +1 -13
  125. package/src/commands/lint.ts +85 -146
  126. package/src/commands/publish.ts +58 -76
  127. package/src/commands/typecheck.ts +13 -46
  128. package/src/index.ts +1 -0
  129. package/src/infra/ResultCollector.ts +2 -2
  130. package/src/orchestrators/BuildOrchestrator.ts +499 -0
  131. package/src/orchestrators/DevOrchestrator.ts +703 -0
  132. package/src/orchestrators/WatchOrchestrator.ts +42 -25
  133. package/src/orchestrators/index.ts +2 -0
  134. package/src/sd-cli-entry.ts +14 -14
  135. package/src/sd-cli.ts +6 -4
  136. package/src/utils/output-utils.ts +1 -2
  137. package/src/utils/package-utils.ts +16 -1
  138. package/src/utils/rebuild-manager.ts +65 -0
  139. package/src/utils/replace-deps.ts +25 -23
  140. package/src/utils/vite-config.ts +84 -6
  141. package/src/utils/worker-events.ts +13 -5
  142. package/src/utils/worker-utils.ts +26 -0
  143. package/src/workers/client.worker.ts +3 -19
  144. package/src/workers/library.worker.ts +2 -19
  145. package/src/workers/server-runtime.worker.ts +16 -17
  146. package/src/workers/server.worker.ts +2 -19
  147. package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
  148. package/templates/add-server/__SERVER__/package.json.hbs +2 -2
  149. package/templates/init/package.json.hbs +3 -3
  150. package/dist/utils/listr-manager.d.ts +0 -37
  151. package/dist/utils/listr-manager.d.ts.map +0 -1
  152. package/dist/utils/listr-manager.js +0 -59
  153. package/dist/utils/listr-manager.js.map +0 -6
  154. package/src/utils/listr-manager.ts +0 -89
@@ -1,5 +1,4 @@
1
1
  import path from "path";
2
- import { Listr } from "listr2";
3
2
  import { fsExists } from "@simplysm/core-node";
4
3
  import { consola } from "consola";
5
4
  import type { SdConfig, SdClientPackageConfig } from "../sd-config.types";
@@ -47,7 +46,7 @@ export async function runDevice(options: DeviceOptions): Promise<void> {
47
46
  sdConfig = await loadSdConfig({ cwd, dev: true, opt: options.options });
48
47
  logger.debug("sd.config.ts 로드 완료");
49
48
  } catch (err) {
50
- consola.error(`sd.config.ts 로드 실패: ${err instanceof Error ? err.message : err}`);
49
+ logger.error(`sd.config.ts 로드 실패: ${err instanceof Error ? err.message : err}`);
51
50
  process.exitCode = 1;
52
51
  return;
53
52
  }
@@ -55,13 +54,13 @@ export async function runDevice(options: DeviceOptions): Promise<void> {
55
54
  // 패키지 설정 확인
56
55
  const pkgConfig = sdConfig.packages[packageName];
57
56
  if (pkgConfig == null) {
58
- consola.error(`패키지를 찾을 수 없습니다: ${packageName}`);
57
+ logger.error(`패키지를 찾을 수 없습니다: ${packageName}`);
59
58
  process.exitCode = 1;
60
59
  return;
61
60
  }
62
61
 
63
62
  if (pkgConfig.target !== "client") {
64
- consola.error(`client 타겟 패키지만 지원합니다: ${packageName} (현재: ${pkgConfig.target})`);
63
+ logger.error(`client 타겟 패키지만 지원합니다: ${packageName} (현재: ${pkgConfig.target})`);
65
64
  process.exitCode = 1;
66
65
  return;
67
66
  }
@@ -76,7 +75,7 @@ export async function runDevice(options: DeviceOptions): Promise<void> {
76
75
  if (typeof clientConfig.server === "number") {
77
76
  serverUrl = `http://localhost:${clientConfig.server}/${packageName}/`;
78
77
  } else {
79
- consola.error(`--url 옵션이 필요합니다. server가 패키지명으로 설정되어 있습니다: ${clientConfig.server}`);
78
+ logger.error(`--url 옵션이 필요합니다. server가 패키지명으로 설정되어 있습니다: ${clientConfig.server}`);
80
79
  process.exitCode = 1;
81
80
  return;
82
81
  }
@@ -84,21 +83,13 @@ export async function runDevice(options: DeviceOptions): Promise<void> {
84
83
 
85
84
  logger.debug("개발 서버 URL", { serverUrl });
86
85
 
87
- const listr = new Listr([
88
- {
89
- title: `${packageName} (electron)`,
90
- task: async () => {
91
- const electron = await Electron.create(pkgDir, clientConfig.electron!);
92
- await electron.run(serverUrl);
93
- },
94
- },
95
- ]);
96
-
86
+ logger.start(`${packageName} (electron) 실행 중...`);
97
87
  try {
98
- await listr.run();
99
- logger.info("Electron 실행 완료");
88
+ const electron = await Electron.create(pkgDir, clientConfig.electron);
89
+ await electron.run(serverUrl);
90
+ logger.success("Electron 실행 완료");
100
91
  } catch (err) {
101
- consola.error(`Electron 실행 실패: ${err instanceof Error ? err.message : err}`);
92
+ logger.error(`Electron 실행 실패: ${err instanceof Error ? err.message : err}`);
102
93
  process.exitCode = 1;
103
94
  }
104
95
  } else if (clientConfig.capacitor != null) {
@@ -108,7 +99,7 @@ export async function runDevice(options: DeviceOptions): Promise<void> {
108
99
  if (typeof clientConfig.server === "number") {
109
100
  serverUrl = `http://localhost:${clientConfig.server}/${packageName}/capacitor/`;
110
101
  } else {
111
- consola.error(`--url 옵션이 필요합니다. server가 패키지명으로 설정되어 있습니다: ${clientConfig.server}`);
102
+ logger.error(`--url 옵션이 필요합니다. server가 패키지명으로 설정되어 있습니다: ${clientConfig.server}`);
112
103
  process.exitCode = 1;
113
104
  return;
114
105
  }
@@ -120,30 +111,22 @@ export async function runDevice(options: DeviceOptions): Promise<void> {
120
111
 
121
112
  const capPath = path.join(pkgDir, ".capacitor");
122
113
  if (!(await fsExists(capPath))) {
123
- consola.error(`Capacitor 프로젝트가 초기화되지 않았습니다. 먼저 'pnpm watch ${packageName}'를 실행하세요.`);
114
+ logger.error(`Capacitor 프로젝트가 초기화되지 않았습니다. 먼저 'pnpm watch ${packageName}'를 실행하세요.`);
124
115
  process.exitCode = 1;
125
116
  return;
126
117
  }
127
118
 
128
- const listr = new Listr([
129
- {
130
- title: `${packageName} (device)`,
131
- task: async () => {
132
- const cap = await Capacitor.create(pkgDir, clientConfig.capacitor!);
133
- await cap.runOnDevice(serverUrl);
134
- },
135
- },
136
- ]);
137
-
119
+ logger.start(`${packageName} (device) 실행 중...`);
138
120
  try {
139
- await listr.run();
140
- logger.info("디바이스 실행 완료");
121
+ const cap = await Capacitor.create(pkgDir, clientConfig.capacitor);
122
+ await cap.runOnDevice(serverUrl);
123
+ logger.success("디바이스 실행 완료");
141
124
  } catch (err) {
142
- consola.error(`디바이스 실행 실패: ${err instanceof Error ? err.message : err}`);
125
+ logger.error(`디바이스 실행 실패: ${err instanceof Error ? err.message : err}`);
143
126
  process.exitCode = 1;
144
127
  }
145
128
  } else {
146
- consola.error(`electron 또는 capacitor 설정이 없습니다: ${packageName}`);
129
+ logger.error(`electron 또는 capacitor 설정이 없습니다: ${packageName}`);
147
130
  process.exitCode = 1;
148
131
  }
149
132
  }
@@ -3,6 +3,7 @@ import fs from "fs";
3
3
  import { consola } from "consola";
4
4
  import { renderTemplateDir } from "../utils/template";
5
5
  import { spawn } from "../utils/spawn";
6
+ import { findPackageRoot } from "../utils/package-utils";
6
7
 
7
8
  //#region Types
8
9
 
@@ -15,19 +16,6 @@ export interface InitOptions {}
15
16
 
16
17
  //#region Utilities
17
18
 
18
- /**
19
- * import.meta.dirname에서 상위로 올라가며 package.json을 찾아 패키지 루트를 반환한다.
20
- */
21
- function findPackageRoot(startDir: string): string {
22
- let dir = startDir;
23
- while (!fs.existsSync(path.join(dir, "package.json"))) {
24
- const parent = path.dirname(dir);
25
- if (parent === dir) throw new Error("package.json을 찾을 수 없습니다.");
26
- dir = parent;
27
- }
28
- return dir;
29
- }
30
-
31
19
  /**
32
20
  * npm 스코프 이름 유효성 검증
33
21
  */
@@ -1,11 +1,10 @@
1
1
  import { ESLint } from "eslint";
2
2
  import { createJiti } from "jiti";
3
3
  import path from "path";
4
- import { Listr } from "listr2";
5
4
  import { fsExists, fsGlob, pathFilterByTargets } from "@simplysm/core-node";
6
5
  import "@simplysm/core-common";
7
6
  import { SdError } from "@simplysm/core-common";
8
- import { consola, LogLevels } from "consola";
7
+ import { consola } from "consola";
9
8
  import stylelint from "stylelint";
10
9
 
11
10
  //#region Types
@@ -22,23 +21,6 @@ export interface LintOptions {
22
21
  timing: boolean;
23
22
  }
24
23
 
25
- /**
26
- * Listr2 컨텍스트 타입
27
- */
28
- interface LintContext {
29
- ignorePatterns: string[];
30
- /** 파일 수집 태스크 완료 후 초기화됨 */
31
- files?: string[];
32
- /** 린트 대상 파일이 있을 때만 초기화됨 */
33
- eslint?: ESLint;
34
- /** 린트 대상 파일이 있을 때만 초기화됨 */
35
- results?: ESLint.LintResult[];
36
- // Stylelint
37
- hasStylelintConfig?: boolean;
38
- cssFiles?: string[];
39
- stylelintResult?: stylelint.LinterResult;
40
- }
41
-
42
24
  //#endregion
43
25
 
44
26
  //#region Utilities
@@ -124,7 +106,7 @@ async function hasStylelintConfig(cwd: string): Promise<boolean> {
124
106
  * ESLint를 실행한다.
125
107
  *
126
108
  * - `eslint.config.ts/js`에서 globalIgnores 패턴을 추출하여 glob 필터링에 적용
127
- * - listr2를 사용하여 진행 상황 표시
109
+ * - consola를 사용하여 진행 상황 표시
128
110
  * - 캐시 활성화 (`.cache/eslint.cache`에 저장, 설정 변경 시 자동 무효화)
129
111
  * - 에러 발생 시 `process.exitCode = 1` 설정
130
112
  *
@@ -143,133 +125,90 @@ export async function runLint(options: LintOptions): Promise<void> {
143
125
  process.env["TIMING"] = "1";
144
126
  }
145
127
 
146
- const listr = new Listr<LintContext, "default" | "verbose">(
147
- [
148
- {
149
- title: "ESLint 설정 로드",
150
- task: async (ctx, task) => {
151
- ctx.ignorePatterns = await loadIgnorePatterns(cwd);
152
- logger.debug("ignore 패턴 로드 완료", { ignorePatternCount: ctx.ignorePatterns.length });
153
- task.title = `ESLint 설정 로드 (${ctx.ignorePatterns.length}개 ignore 패턴)`;
154
- },
155
- },
156
- {
157
- title: "린트 대상 파일 수집",
158
- task: async (ctx, task) => {
159
- let files = await fsGlob("**/*.{ts,tsx,js,jsx}", {
160
- cwd,
161
- ignore: ctx.ignorePatterns,
162
- nodir: true,
163
- absolute: true,
164
- });
165
-
166
- // targets가 주어지면 해당 경로의 하위 파일만 필터링
167
- files = pathFilterByTargets(files, targets, cwd);
168
- ctx.files = files;
169
- logger.debug("파일 수집 완료", { fileCount: files.length });
170
- task.title = `린트 대상 파일 수집 (${files.length}개)`;
171
-
172
- if (files.length === 0) {
173
- task.skip("린트할 파일이 없습니다.");
174
- }
175
- },
176
- },
177
- {
178
- title: "린트 실행",
179
- enabled: (ctx) => (ctx.files?.length ?? 0) > 0,
180
- task: async (ctx, task) => {
181
- const files = ctx.files!;
182
- task.title = `린트 실행 중... (${files.length}개 파일)`;
183
- ctx.eslint = new ESLint({
184
- cwd,
185
- fix,
186
- cache: true,
187
- cacheLocation: path.join(cwd, ".cache", "eslint.cache"),
188
- });
189
- ctx.results = await ctx.eslint.lintFiles(files);
190
- },
191
- },
192
- {
193
- title: "자동 수정 적용",
194
- enabled: () => fix,
195
- skip: (ctx) => (ctx.files?.length ?? 0) === 0 || ctx.results == null,
196
- task: async (ctx) => {
197
- if (ctx.results == null) return;
198
- await ESLint.outputFixes(ctx.results);
199
- logger.debug("자동 수정 적용 완료");
200
- },
201
- },
202
- {
203
- title: "Stylelint 설정 확인",
204
- task: async (ctx, task) => {
205
- ctx.hasStylelintConfig = await hasStylelintConfig(cwd);
206
- if (!ctx.hasStylelintConfig) {
207
- task.skip("stylelint.config 파일 없음");
208
- }
209
- },
210
- },
211
- {
212
- title: "CSS 파일 수집",
213
- enabled: (ctx) => ctx.hasStylelintConfig === true,
214
- task: async (ctx, task) => {
215
- let cssFiles = await fsGlob("**/*.css", {
216
- cwd,
217
- ignore: ctx.ignorePatterns,
218
- nodir: true,
219
- absolute: true,
220
- });
221
- cssFiles = pathFilterByTargets(cssFiles, targets, cwd);
222
- ctx.cssFiles = cssFiles;
223
- task.title = `CSS 파일 수집 (${cssFiles.length}개)`;
224
- if (cssFiles.length === 0) {
225
- task.skip("린트할 CSS 파일이 없습니다.");
226
- }
227
- },
228
- },
229
- {
230
- title: "Stylelint 실행",
231
- enabled: (ctx) => (ctx.cssFiles?.length ?? 0) > 0,
232
- task: async (ctx, task) => {
233
- const cssFiles = ctx.cssFiles!;
234
- task.title = `Stylelint 실행 중... (${cssFiles.length}개 파일)`;
235
-
236
- // Stylelint 설정 파일 경로 찾기
237
- let configFile: string | undefined;
238
- for (const f of STYLELINT_CONFIG_FILES) {
239
- const configPath = path.join(cwd, f);
240
- if (await fsExists(configPath)) {
241
- configFile = configPath;
242
- break;
243
- }
244
- }
245
-
246
- const result = await stylelint.lint({
247
- files: cssFiles,
248
- configFile,
249
- fix,
250
- cache: true,
251
- cacheLocation: path.join(cwd, ".cache", "stylelint.cache"),
252
- });
253
- ctx.stylelintResult = result;
254
- },
255
- },
256
- ],
257
- {
258
- renderer: consola.level >= LogLevels.debug ? "verbose" : "default",
259
- },
260
- );
261
-
262
- const ctx = await listr.run();
128
+ // ESLint 설정 로드
129
+ logger.start("ESLint 설정 로드");
130
+ const ignorePatterns = await loadIgnorePatterns(cwd);
131
+ logger.debug("ignore 패턴 로드 완료", { ignorePatternCount: ignorePatterns.length });
132
+ logger.success(`ESLint 설정 로드 (${ignorePatterns.length}개 ignore 패턴)`);
133
+
134
+ // 린트 대상 파일 수집
135
+ logger.start("린트 대상 파일 수집");
136
+ let files = await fsGlob("**/*.{ts,tsx,js,jsx}", {
137
+ cwd,
138
+ ignore: ignorePatterns,
139
+ nodir: true,
140
+ absolute: true,
141
+ });
142
+ files = pathFilterByTargets(files, targets, cwd);
143
+ logger.debug("파일 수집 완료", { fileCount: files.length });
144
+ logger.success(`린트 대상 파일 수집 (${files.length}개)`);
145
+
146
+ // 린트 실행
147
+ let eslint: ESLint | undefined;
148
+ let eslintResults: ESLint.LintResult[] | undefined;
149
+ if (files.length > 0) {
150
+ logger.start(`린트 실행 중... (${files.length}개 파일)`);
151
+ eslint = new ESLint({
152
+ cwd,
153
+ fix,
154
+ cache: true,
155
+ cacheLocation: path.join(cwd, ".cache", "eslint.cache"),
156
+ });
157
+ eslintResults = await eslint.lintFiles(files);
158
+ logger.success("린트 실행 완료");
159
+
160
+ // 자동 수정 적용
161
+ if (fix) {
162
+ logger.debug("자동 수정 적용 중...");
163
+ await ESLint.outputFixes(eslintResults);
164
+ logger.debug("자동 수정 적용 완료");
165
+ }
166
+ }
167
+
168
+ // Stylelint
169
+ const hasStylelintCfg = await hasStylelintConfig(cwd);
170
+ let stylelintResult: stylelint.LinterResult | undefined;
171
+ if (hasStylelintCfg) {
172
+ logger.start("CSS 파일 수집");
173
+ let cssFiles = await fsGlob("**/*.css", {
174
+ cwd,
175
+ ignore: ignorePatterns,
176
+ nodir: true,
177
+ absolute: true,
178
+ });
179
+ cssFiles = pathFilterByTargets(cssFiles, targets, cwd);
180
+ logger.success(`CSS 파일 수집 (${cssFiles.length}개)`);
181
+
182
+ if (cssFiles.length > 0) {
183
+ logger.start(`Stylelint 실행 중... (${cssFiles.length}개 파일)`);
184
+ let configFile: string | undefined;
185
+ for (const f of STYLELINT_CONFIG_FILES) {
186
+ const configPath = path.join(cwd, f);
187
+ if (await fsExists(configPath)) {
188
+ configFile = configPath;
189
+ break;
190
+ }
191
+ }
192
+ stylelintResult = await stylelint.lint({
193
+ files: cssFiles,
194
+ configFile,
195
+ fix,
196
+ cache: true,
197
+ cacheLocation: path.join(cwd, ".cache", "stylelint.cache"),
198
+ });
199
+ logger.success("Stylelint 실행 완료");
200
+ }
201
+ }
263
202
 
264
203
  // 파일이 없거나 린트가 실행되지 않았으면 조기 종료
265
- if ((ctx.files?.length ?? 0) === 0 || ctx.results == null || ctx.eslint == null) {
204
+ if (files.length === 0 || eslintResults == null || eslint == null) {
266
205
  logger.info("린트할 파일 없음");
267
206
  return;
268
207
  }
269
208
 
270
209
  // 결과 집계
271
- const errorCount = ctx.results.sum((r) => r.errorCount);
272
- const warningCount = ctx.results.sum((r) => r.warningCount);
210
+ const errorCount = eslintResults.sum((r) => r.errorCount);
211
+ const warningCount = eslintResults.sum((r) => r.warningCount);
273
212
 
274
213
  if (errorCount > 0) {
275
214
  logger.error("린트 에러 발생", { errorCount, warningCount });
@@ -280,8 +219,8 @@ export async function runLint(options: LintOptions): Promise<void> {
280
219
  }
281
220
 
282
221
  // 포맷터 출력
283
- const formatter = await ctx.eslint.loadFormatter("stylish");
284
- const resultText = await formatter.format(ctx.results);
222
+ const formatter = await eslint.loadFormatter("stylish");
223
+ const resultText = await formatter.format(eslintResults);
285
224
  if (resultText) {
286
225
  process.stdout.write(resultText);
287
226
  }
@@ -292,11 +231,11 @@ export async function runLint(options: LintOptions): Promise<void> {
292
231
  }
293
232
 
294
233
  // Stylelint 결과 출력
295
- if (ctx.stylelintResult != null && ctx.stylelintResult.results.length > 0) {
296
- const stylelintErrorCount = ctx.stylelintResult.results.sum(
234
+ if (stylelintResult != null && stylelintResult.results.length > 0) {
235
+ const stylelintErrorCount = stylelintResult.results.sum(
297
236
  (r) => r.warnings.filter((w) => w.severity === "error").length,
298
237
  );
299
- const stylelintWarningCount = ctx.stylelintResult.results.sum(
238
+ const stylelintWarningCount = stylelintResult.results.sum(
300
239
  (r) => r.warnings.filter((w) => w.severity === "warning").length,
301
240
  );
302
241
 
@@ -313,7 +252,7 @@ export async function runLint(options: LintOptions): Promise<void> {
313
252
 
314
253
  // Stylelint formatter 출력
315
254
  const stylelintFormatter = await stylelint.formatters.string;
316
- const stylelintOutput = stylelintFormatter(ctx.stylelintResult.results, ctx.stylelintResult);
255
+ const stylelintOutput = stylelintFormatter(stylelintResult.results, stylelintResult);
317
256
  if (stylelintOutput) {
318
257
  process.stdout.write(stylelintOutput);
319
258
  }
@@ -1,7 +1,6 @@
1
1
  import path from "path";
2
2
  import semver from "semver";
3
- import { consola, LogLevels } from "consola";
4
- import { Listr, type ListrTask } from "listr2";
3
+ import { consola } from "consola";
5
4
  import { StorageFactory } from "@simplysm/storage";
6
5
  import { fsExists, fsRead, fsReadJson, fsWrite, fsGlob, fsCopy } from "@simplysm/core-node";
7
6
  import { env, jsonStringify } from "@simplysm/core-common";
@@ -10,6 +9,7 @@ import type { SdConfig, SdPublishConfig } from "../sd-config.types";
10
9
  import { loadSdConfig } from "../utils/sd-config";
11
10
  import { spawn } from "../utils/spawn";
12
11
  import { runBuild } from "./build";
12
+ import { parseWorkspaceGlobs } from "../utils/replace-deps";
13
13
  import os from "os";
14
14
  import fs from "fs";
15
15
  import ssh2 from "ssh2";
@@ -214,10 +214,11 @@ function registerSshPublicKey(
214
214
  const conn = new SshClient();
215
215
  conn.on("ready", () => {
216
216
  // authorized_keys에 공개키 추가
217
+ const escapedKey = publicKey.replace(/'/g, "'\\''");
217
218
  const cmd = [
218
219
  "mkdir -p ~/.ssh",
219
220
  "chmod 700 ~/.ssh",
220
- `echo '${publicKey}' >> ~/.ssh/authorized_keys`,
221
+ `echo '${escapedKey}' >> ~/.ssh/authorized_keys`,
221
222
  "chmod 600 ~/.ssh/authorized_keys",
222
223
  ].join(" && ");
223
224
 
@@ -481,7 +482,7 @@ export async function runPublish(options: PublishOptions): Promise<void> {
481
482
  sdConfig = await loadSdConfig({ cwd, dev: false, opt: options.options });
482
483
  logger.debug("sd.config.ts 로드 완료");
483
484
  } catch (err) {
484
- consola.error(`sd.config.ts 로드 실패: ${err instanceof Error ? err.message : err}`);
485
+ logger.error(`sd.config.ts 로드 실패: ${err instanceof Error ? err.message : err}`);
485
486
  process.exitCode = 1;
486
487
  return;
487
488
  }
@@ -495,26 +496,12 @@ export async function runPublish(options: PublishOptions): Promise<void> {
495
496
  const workspaceGlobs: string[] = [];
496
497
  if (await fsExists(workspaceYamlPath)) {
497
498
  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
- }
499
+ workspaceGlobs.push(...parseWorkspaceGlobs(yamlContent));
513
500
  }
514
501
 
515
502
  const allPkgPaths = (await Promise.all(workspaceGlobs.map((item) => fsGlob(path.resolve(cwd, item)))))
516
503
  .flat()
517
- .filter((item) => !item.includes("."));
504
+ .filter((item) => !path.basename(item).includes("."));
518
505
 
519
506
  // publish 설정이 있는 패키지 필터링
520
507
  const publishPackages: Array<{
@@ -571,7 +558,7 @@ export async function runPublish(options: PublishOptions): Promise<void> {
571
558
  }
572
559
  logger.debug(`npm 로그인 확인: ${whoami.trim()}`);
573
560
  } catch {
574
- consola.error(
561
+ logger.error(
575
562
  "npm 토큰이 유효하지 않거나 만료되었습니다.\n" +
576
563
  "https://www.npmjs.com/settings/~/tokens 에서 Granular Access Token 생성 후:\n" +
577
564
  " npm config set //registry.npmjs.org/:_authToken <토큰>",
@@ -585,7 +572,7 @@ export async function runPublish(options: PublishOptions): Promise<void> {
585
572
  try {
586
573
  await ensureSshAuth(publishPackages, logger);
587
574
  } catch (err) {
588
- consola.error(`SSH 인증 설정 실패: ${err instanceof Error ? err.message : err}`);
575
+ logger.error(`SSH 인증 설정 실패: ${err instanceof Error ? err.message : err}`);
589
576
  process.exitCode = 1;
590
577
  return;
591
578
  }
@@ -617,7 +604,7 @@ export async function runPublish(options: PublishOptions): Promise<void> {
617
604
  logger.info("자동 커밋 완료.");
618
605
  }
619
606
  } catch (err) {
620
- consola.error(err instanceof Error ? err.message : err);
607
+ logger.error(err instanceof Error ? err.message : err);
621
608
  process.exitCode = 1;
622
609
  return;
623
610
  }
@@ -666,7 +653,7 @@ export async function runPublish(options: PublishOptions): Promise<void> {
666
653
  if (dryRun) {
667
654
  logger.error("[DRY-RUN] 빌드 실패");
668
655
  } else {
669
- consola.error(
656
+ logger.error(
670
657
  "빌드 실패. 수동 복구가 필요할 수 있습니다:\n" +
671
658
  " 버전 변경을 되돌리려면:\n" +
672
659
  " git checkout -- package.json packages/*/package.json packages/sd-cli/templates/",
@@ -699,7 +686,7 @@ export async function runPublish(options: PublishOptions): Promise<void> {
699
686
  await spawn("git", ["push", "--tags"]);
700
687
  logger.debug("Git 작업 완료");
701
688
  } catch (err) {
702
- consola.error(
689
+ logger.error(
703
690
  `Git 작업 실패: ${err instanceof Error ? err.message : err}\n` +
704
691
  "수동 복구가 필요할 수 있습니다:\n" +
705
692
  ` git revert HEAD # 버전 커밋 되돌리기\n` +
@@ -716,59 +703,56 @@ export async function runPublish(options: PublishOptions): Promise<void> {
716
703
 
717
704
  //#endregion
718
705
 
719
- //#region Phase 4: 배포 (의존성 레벨별 병렬, Listr)
706
+ //#region Phase 4: 배포 (의존성 레벨별 순차, 레벨 내 병렬)
720
707
 
721
708
  const levels = await computePublishLevels(publishPackages);
722
709
  const publishedPackages: string[] = [];
723
710
  let publishFailed = false;
724
711
 
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
- );
712
+ // 레벨별 순차 실행
713
+ for (let levelIdx = 0; levelIdx < levels.length; levelIdx++) {
714
+ if (publishFailed) break;
767
715
 
768
- try {
769
- await publishListr.run();
770
- } catch {
771
- // Listr 내부 에러는 아래에서 처리
716
+ const levelPkgs = levels[levelIdx];
717
+ logger.start(`Level ${levelIdx + 1}/${levels.length}`);
718
+
719
+ // 레벨 패키지 병렬 실행 (Promise.allSettled)
720
+ const publishPromises = levelPkgs.map(async (pkg) => {
721
+ const maxRetries = 3;
722
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
723
+ try {
724
+ await publishPackage(pkg.path, pkg.config, version, cwd, logger, dryRun);
725
+ logger.debug(dryRun ? `[DRY-RUN] ${pkg.name}` : pkg.name);
726
+ publishedPackages.push(pkg.name);
727
+ return { status: "success" as const, name: pkg.name };
728
+ } catch (err) {
729
+ if (attempt < maxRetries) {
730
+ const delay = attempt * 5_000;
731
+ logger.debug(
732
+ dryRun
733
+ ? `[DRY-RUN] ${pkg.name} (재시도 ${attempt + 1}/${maxRetries})`
734
+ : `${pkg.name} (재시도 ${attempt + 1}/${maxRetries})`,
735
+ );
736
+ await new Promise((resolve) => setTimeout(resolve, delay));
737
+ } else {
738
+ throw err;
739
+ }
740
+ }
741
+ }
742
+ // TypeScript 타입 체커를 위한 fallback (실제로는 도달하지 않음)
743
+ return { status: "error" as const, name: pkg.name, error: new Error("Unknown error") };
744
+ });
745
+
746
+ const results = await Promise.allSettled(publishPromises);
747
+
748
+ // 레벨 내 실패 확인
749
+ const levelFailed = results.some((r) => r.status === "rejected");
750
+ if (levelFailed) {
751
+ publishFailed = true;
752
+ logger.fail(`Level ${levelIdx + 1}/${levels.length}`);
753
+ } else {
754
+ logger.success(`Level ${levelIdx + 1}/${levels.length}`);
755
+ }
772
756
  }
773
757
 
774
758
  // 실패한 패키지 확인
@@ -776,10 +760,8 @@ export async function runPublish(options: PublishOptions): Promise<void> {
776
760
  const failedPkgNames = allPkgNames.filter((n) => !publishedPackages.includes(n));
777
761
 
778
762
  if (failedPkgNames.length > 0) {
779
- publishFailed = true;
780
-
781
763
  if (publishedPackages.length > 0) {
782
- consola.error(
764
+ logger.error(
783
765
  "배포 중 오류가 발생했습니다.\n" +
784
766
  "이미 배포된 패키지:\n" +
785
767
  publishedPackages.map((n) => ` - ${n}`).join("\n") +
@@ -789,7 +771,7 @@ export async function runPublish(options: PublishOptions): Promise<void> {
789
771
  }
790
772
 
791
773
  for (const name of failedPkgNames) {
792
- consola.error(`[${name}] 배포 실패`);
774
+ logger.error(`[${name}] 배포 실패`);
793
775
  }
794
776
  process.exitCode = 1;
795
777
  return;