@simplysm/sd-cli 13.0.7 → 13.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sd-cli.ts CHANGED
@@ -1,330 +1,99 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // side-effect: Map/Array 프로토타입 확장 (getOrCreate 등)
4
- import "@simplysm/core-common";
5
- import yargs, { type Argv } from "yargs";
6
- import { hideBin } from "yargs/helpers";
7
- import { runLint } from "./commands/lint";
8
- import { runTypecheck } from "./commands/typecheck";
9
- import { runWatch } from "./commands/watch";
10
- import { runDev } from "./commands/dev";
11
- import { runBuild } from "./commands/build";
12
- import { runPublish } from "./commands/publish";
13
- import { runReplaceDeps } from "./commands/replace-deps";
14
- import { runDevice } from "./commands/device";
3
+ /**
4
+ * CLI Launcher
5
+ *
6
+ * .ts 실행 (개발): CPU affinity 적용 후 sd-cli-entry 직접 import
7
+ * .js 실행 (배포): replaceDeps 실행 후 새 프로세스로 sd-cli-entry spawn
8
+ */
9
+
10
+ import { exec, spawn } from "child_process";
11
+ import os from "os";
15
12
  import path from "path";
16
- import fs from "fs";
17
13
  import { fileURLToPath } from "url";
18
- import { EventEmitter } from "node:events";
19
- import { consola, LogLevels } from "consola";
20
14
 
21
- Error.stackTraceLimit = Infinity;
22
- EventEmitter.defaultMaxListeners = 100;
15
+ const cliEntryUrl = import.meta.resolve("./sd-cli-entry");
16
+ const cliEntryFilePath = fileURLToPath(cliEntryUrl);
17
+
18
+ if (path.extname(cliEntryFilePath) === ".ts") {
19
+ // 개발 모드 (.ts): affinity 적용 후 직접 실행
20
+ // import만으로는 메인 모듈 감지가 실패하므로 (process.argv[1] ≠ sd-cli-entry)
21
+ // createCliParser를 명시적으로 호출한다.
22
+ configureAffinityAndPriority(process.pid);
23
+ const { createCliParser } = await import(cliEntryUrl);
24
+ await createCliParser(process.argv.slice(2)).parse();
25
+ } else {
26
+ // 배포 모드 (.js): 2단계 실행
27
+
28
+ // Phase 1: replaceDeps (inline — 설치된 버전으로 복사)
29
+ try {
30
+ const { loadSdConfig } = await import("./utils/sd-config.js");
31
+ const { setupReplaceDeps } = await import("./utils/replace-deps.js");
32
+ const sdConfig = await loadSdConfig({ cwd: process.cwd(), dev: false, opt: [] });
33
+ if (sdConfig.replaceDeps != null) {
34
+ await setupReplaceDeps(process.cwd(), sdConfig.replaceDeps);
35
+ }
36
+ } catch {
37
+ // sd.config.ts 없거나 replaceDeps 미설정 시 스킵
38
+ }
39
+
40
+ // Phase 2: 새 프로세스로 실제 CLI 실행 (모듈 캐시 초기화)
41
+ const child = spawn(
42
+ "node",
43
+ ["--max-old-space-size=8192", "--max-semi-space-size=16", cliEntryFilePath, ...process.argv.slice(2)],
44
+ { stdio: "inherit" },
45
+ );
46
+ child.on("spawn", () => {
47
+ if (child.pid != null) configureAffinityAndPriority(child.pid);
48
+ });
49
+ child.on("exit", (code) => {
50
+ process.exitCode = code ?? 0;
51
+ });
52
+ }
23
53
 
24
54
  /**
25
- * CLI 파서를 생성한다.
26
- * @internal 테스트용으로 export
55
+ * CPU affinity mask 계산 (앞쪽 코어 제외)
56
+ *
57
+ * CPU 4개당 1개를 제외하고, 나머지 코어의 비트를 ON으로 설정한다.
58
+ * 예: 8코어 → 2개 제외 → 0xFC (코어 2~7)
27
59
  */
28
- export function createCliParser(argv: string[]): Argv {
29
- return yargs(argv)
30
- .help("help", "도움말")
31
- .alias("help", "h")
32
- .option("debug", {
33
- type: "boolean",
34
- describe: "debug 로그 출력",
35
- default: false,
36
- global: true,
37
- })
38
- .middleware((args) => {
39
- if (args.debug) consola.level = LogLevels.debug;
40
- })
41
- .command(
42
- "lint [targets..]",
43
- "ESLint + Stylelint를 실행한다.",
44
- (cmd) =>
45
- cmd
46
- .version(false)
47
- .hide("help")
48
- .positional("targets", {
49
- type: "string",
50
- array: true,
51
- describe: "린트할 경로 (예: packages/core-common, tests/orm)",
52
- default: [],
53
- })
54
- .options({
55
- fix: {
56
- type: "boolean",
57
- describe: "자동 수정",
58
- default: false,
59
- },
60
- timing: {
61
- type: "boolean",
62
- describe: "규칙별 실행 시간 출력",
63
- default: false,
64
- },
65
- }),
66
- async (args) => {
67
- await runLint({
68
- targets: args.targets,
69
- fix: args.fix,
70
- timing: args.timing,
71
- });
72
- },
73
- )
74
- .command(
75
- "typecheck [targets..]",
76
- "TypeScript 타입체크를 실행한다.",
77
- (cmd) =>
78
- cmd
79
- .version(false)
80
- .hide("help")
81
- .positional("targets", {
82
- type: "string",
83
- array: true,
84
- describe: "타입체크할 경로 (예: packages/core-common, tests/orm)",
85
- default: [],
86
- })
87
- .options({
88
- options: {
89
- type: "string",
90
- array: true,
91
- alias: "o",
92
- description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
93
- default: [] as string[],
94
- },
95
- }),
96
- async (args) => {
97
- await runTypecheck({
98
- targets: args.targets,
99
- options: args.options,
100
- });
101
- },
102
- )
103
- .command(
104
- "watch [targets..]",
105
- "패키지를 watch 모드로 빌드한다.",
106
- (cmd) =>
107
- cmd
108
- .version(false)
109
- .hide("help")
110
- .positional("targets", {
111
- type: "string",
112
- array: true,
113
- describe: "watch할 패키지 (예: solid, solid-demo)",
114
- default: [],
115
- })
116
- .options({
117
- options: {
118
- type: "string",
119
- array: true,
120
- alias: "o",
121
- description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
122
- default: [] as string[],
123
- },
124
- }),
125
- async (args) => {
126
- await runWatch({
127
- targets: args.targets,
128
- options: args.options,
129
- });
130
- },
131
- )
132
- .command(
133
- "dev [targets..]",
134
- "Client와 Server 패키지를 개발 모드로 실행한다.",
135
- (cmd) =>
136
- cmd
137
- .version(false)
138
- .hide("help")
139
- .positional("targets", {
140
- type: "string",
141
- array: true,
142
- describe: "실행할 패키지 (예: solid-demo)",
143
- default: [],
144
- })
145
- .options({
146
- options: {
147
- type: "string",
148
- array: true,
149
- alias: "o",
150
- description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
151
- default: [] as string[],
152
- },
153
- }),
154
- async (args) => {
155
- await runDev({
156
- targets: args.targets,
157
- options: args.options,
158
- });
159
- },
160
- )
161
- .command(
162
- "build [targets..]",
163
- "프로덕션 빌드를 실행한다.",
164
- (cmd) =>
165
- cmd
166
- .version(false)
167
- .hide("help")
168
- .positional("targets", {
169
- type: "string",
170
- array: true,
171
- describe: "빌드할 패키지 (예: solid, core-common)",
172
- default: [],
173
- })
174
- .options({
175
- options: {
176
- type: "string",
177
- array: true,
178
- alias: "o",
179
- description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
180
- default: [] as string[],
181
- },
182
- }),
183
- async (args) => {
184
- await runBuild({
185
- targets: args.targets,
186
- options: args.options,
187
- });
188
- },
189
- )
190
- .command(
191
- "device",
192
- "Android 디바이스에서 앱을 실행한다.",
193
- (cmd) =>
194
- cmd
195
- .version(false)
196
- .hide("help")
197
- .options({
198
- package: {
199
- type: "string",
200
- alias: "p",
201
- describe: "패키지 이름",
202
- demandOption: true,
203
- },
204
- url: {
205
- type: "string",
206
- alias: "u",
207
- describe: "개발 서버 URL (미지정 시 sd.config.ts의 server 설정 사용)",
208
- },
209
- options: {
210
- type: "string",
211
- array: true,
212
- alias: "o",
213
- description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
214
- default: [] as string[],
215
- },
216
- }),
217
- async (args) => {
218
- await runDevice({
219
- package: args.package,
220
- url: args.url,
221
- options: args.options,
222
- });
223
- },
224
- )
225
- .command(
226
- "init",
227
- "새 프로젝트를 초기화한다.",
228
- (cmd) => cmd.version(false).hide("help"),
229
- async () => {
230
- const { runInit } = await import("./commands/init.js");
231
- await runInit({});
232
- },
233
- )
234
- .command("add", "프로젝트에 패키지를 추가한다.", (cmd) =>
235
- cmd
236
- .version(false)
237
- .hide("help")
238
- .command(
239
- "client",
240
- "클라이언트 패키지를 추가한다.",
241
- (subCmd) => subCmd.version(false).hide("help"),
242
- async () => {
243
- const { runAddClient } = await import("./commands/add-client.js");
244
- await runAddClient({});
245
- },
246
- )
247
- .command(
248
- "server",
249
- "서버 패키지를 추가한다.",
250
- (subCmd) => subCmd.version(false).hide("help"),
251
- async () => {
252
- const { runAddServer } = await import("./commands/add-server.js");
253
- await runAddServer({});
254
- },
255
- )
256
- .demandCommand(1, "패키지 타입을 지정해주세요. (client, server)"),
257
- )
258
- .command(
259
- "publish [targets..]",
260
- "패키지를 배포한다.",
261
- (cmd) =>
262
- cmd
263
- .version(false)
264
- .hide("help")
265
- .positional("targets", {
266
- type: "string",
267
- array: true,
268
- describe: "배포할 패키지 (예: solid, core-common)",
269
- default: [],
270
- })
271
- .options({
272
- "build": {
273
- type: "boolean",
274
- describe: "빌드 실행 (--no-build로 스킵)",
275
- default: true,
276
- },
277
- "dry-run": {
278
- type: "boolean",
279
- describe: "실제 배포 없이 시뮬레이션",
280
- default: false,
281
- },
282
- "options": {
283
- type: "string",
284
- array: true,
285
- alias: "o",
286
- description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
287
- default: [] as string[],
288
- },
289
- }),
290
- async (args) => {
291
- await runPublish({
292
- targets: args.targets,
293
- noBuild: !args.build,
294
- dryRun: args.dryRun,
295
- options: args.options,
296
- });
297
- },
298
- )
299
- .command(
300
- "replace-deps",
301
- "sd.config.ts의 replaceDeps 설정에 따라 node_modules 패키지를 로컬 소스로 symlink 교체한다.",
302
- (cmd) =>
303
- cmd
304
- .version(false)
305
- .hide("help")
306
- .options({
307
- options: {
308
- type: "string",
309
- array: true,
310
- alias: "o",
311
- description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
312
- default: [] as string[],
313
- },
314
- }),
315
- async (args) => {
316
- await runReplaceDeps({
317
- options: args.options,
318
- });
319
- },
320
- )
321
- .demandCommand(1, "명령어를 지정해주세요.")
322
- .strict();
60
+ function calculateAffinityMask(cpuCount: number): string {
61
+ const exclude = cpuCount <= 1 ? 0 : Math.ceil(cpuCount / 4);
62
+ let mask = 0n;
63
+ for (let i = exclude; i < cpuCount; i++) {
64
+ mask |= 1n << BigInt(i);
65
+ }
66
+ return "0x" + mask.toString(16).toUpperCase();
323
67
  }
324
68
 
325
- // CLI로 직접 실행될 때만 파싱 수행
326
- // ESM에서 메인 모듈 판별: import.meta.url과 process.argv[1]을 정규화하여 비교
327
- const cliEntryPath = process.argv.at(1);
328
- if (cliEntryPath != null && fileURLToPath(import.meta.url) === fs.realpathSync(path.resolve(cliEntryPath))) {
329
- await createCliParser(hideBin(process.argv)).parse();
69
+ /**
70
+ * Cross-platform CPU affinity + priority 설정
71
+ *
72
+ * - Windows: PowerShell ProcessorAffinity + PriorityClass
73
+ * - Linux/WSL: taskset + renice
74
+ *
75
+ * 실패해도 경고만 출력하고 CLI 동작에는 영향 없음.
76
+ */
77
+ function configureAffinityAndPriority(pid: number): void {
78
+ const cpuCount = os.cpus().length;
79
+ const mask = calculateAffinityMask(cpuCount);
80
+
81
+ let command: string;
82
+ if (process.platform === "win32") {
83
+ const commands = [
84
+ `$p = Get-Process -Id ${pid}`,
85
+ `$p.ProcessorAffinity = ${mask}`,
86
+ `$p.PriorityClass = 'BelowNormal'`,
87
+ ].join("; ");
88
+ command = `powershell -Command "${commands}"`;
89
+ } else {
90
+ command = `taskset -p ${mask} ${pid} && renice +10 -p ${pid}`;
91
+ }
92
+
93
+ exec(command, (err) => {
94
+ if (err) {
95
+ // eslint-disable-next-line no-console
96
+ console.warn("CPU affinity/priority 설정 실패:", err.message);
97
+ }
98
+ });
330
99
  }
@@ -2,6 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { glob } from "glob";
4
4
  import { consola } from "consola";
5
+ import { fsCopy, fsMkdir, fsRm, FsWatcher } from "@simplysm/core-node";
5
6
 
6
7
  /**
7
8
  * replaceDeps 설정의 glob 패턴과 대상 패키지 목록을 매칭하여
@@ -83,11 +84,47 @@ export function parseWorkspaceGlobs(content: string): string[] {
83
84
  }
84
85
 
85
86
  /**
86
- * replaceDeps 설정에 따라 node_modules 내 패키지를 소스 디렉토리로 symlink 교체한다.
87
+ * 복사 제외할 항목 이름들
88
+ */
89
+ const EXCLUDED_NAMES = new Set(["node_modules", "package.json", ".cache", "tests"]);
90
+
91
+ /**
92
+ * replaceDeps 복사 시 사용할 필터 함수
93
+ * node_modules, package.json, .cache, tests를 제외한다.
94
+ *
95
+ * @param itemPath - 복사할 항목의 절대 경로
96
+ * @returns 복사 대상이면 true, 제외하면 false
97
+ */
98
+ function replaceDepsCopyFilter(itemPath: string): boolean {
99
+ const basename = path.basename(itemPath);
100
+ return !EXCLUDED_NAMES.has(basename);
101
+ }
102
+
103
+ /**
104
+ * replaceDeps 복사 교체 항목
105
+ */
106
+ export interface ReplaceDepEntry {
107
+ targetName: string;
108
+ sourcePath: string;
109
+ targetPath: string;
110
+ resolvedSourcePath: string;
111
+ actualTargetPath: string;
112
+ }
113
+
114
+ /**
115
+ * watchReplaceDeps 반환 타입
116
+ */
117
+ export interface WatchReplaceDepResult {
118
+ entries: ReplaceDepEntry[];
119
+ dispose: () => void;
120
+ }
121
+
122
+ /**
123
+ * replaceDeps 설정에 따라 node_modules 내 패키지를 소스 디렉토리로 복사 교체한다.
87
124
  *
88
125
  * 1. pnpm-workspace.yaml 파싱 → workspace 패키지 경로 목록
89
126
  * 2. [루트, ...workspace 패키지]의 node_modules에서 매칭되는 패키지 찾기
90
- * 3. 기존 symlink/디렉토리 제거 → 소스 경로로 symlink 생성
127
+ * 3. 기존 symlink/디렉토리 제거 → 소스 경로를 복사 (node_modules, package.json, .cache, tests 제외)
91
128
  *
92
129
  * @param projectRoot - 프로젝트 루트 경로
93
130
  * @param replaceDeps - sd.config.ts의 replaceDeps 설정
@@ -133,7 +170,7 @@ export async function setupReplaceDeps(projectRoot: string, replaceDeps: Record<
133
170
  // 패턴 매칭 및 경로 해석
134
171
  const entries = resolveReplaceDepEntries(replaceDeps, targetNames);
135
172
 
136
- // 3. Symlink 교체
173
+ // 3. 복사 교체
137
174
  for (const { targetName, sourcePath } of entries) {
138
175
  const targetPath = path.join(nodeModulesDir, targetName);
139
176
  const resolvedSourcePath = path.resolve(projectRoot, sourcePath);
@@ -147,17 +184,184 @@ export async function setupReplaceDeps(projectRoot: string, replaceDeps: Record<
147
184
  }
148
185
 
149
186
  try {
150
- // 기존 symlink/디렉토리 제거
151
- await fs.promises.rm(targetPath, { recursive: true, force: true });
187
+ // targetPath가 symlink realpath로 해석하여 실제 .pnpm 스토어 경로 얻기
188
+ let actualTargetPath = targetPath;
189
+ try {
190
+ const stat = await fs.promises.lstat(targetPath);
191
+ if (stat.isSymbolicLink()) {
192
+ actualTargetPath = await fs.promises.realpath(targetPath);
193
+ }
194
+ } catch {
195
+ // targetPath가 존재하지 않으면 그대로 사용
196
+ }
152
197
 
153
- // 상대 경로로 symlink 생성
154
- const relativePath = path.relative(path.dirname(targetPath), resolvedSourcePath);
155
- await fs.promises.symlink(relativePath, targetPath, "dir");
198
+ // actualTargetPath의 기존 내용 제거 후 소스 복사 (symlink 유지)
199
+ await fs.promises.rm(actualTargetPath, { recursive: true, force: true });
200
+ await fsCopy(resolvedSourcePath, actualTargetPath, replaceDepsCopyFilter);
156
201
 
157
202
  logger.info(`${targetName} → ${sourcePath}`);
158
203
  } catch (err) {
159
- logger.error(`symlink 교체 실패 (${targetName}): ${err instanceof Error ? err.message : err}`);
204
+ logger.error(`복사 교체 실패 (${targetName}): ${err instanceof Error ? err.message : err}`);
160
205
  }
161
206
  }
162
207
  }
163
208
  }
209
+
210
+ /**
211
+ * replaceDeps 설정에 따라 소스 디렉토리를 watch하여 변경 시 대상 경로로 복사한다.
212
+ *
213
+ * 1. pnpm-workspace.yaml 파싱 → workspace 패키지 경로 목록
214
+ * 2. [루트, ...workspace 패키지]의 node_modules에서 매칭되는 패키지 찾기
215
+ * 3. 소스 디렉토리를 FsWatcher로 watch (300ms delay)
216
+ * 4. 변경 시 대상 경로로 복사 (node_modules, package.json, .cache, tests 제외)
217
+ *
218
+ * @param projectRoot - 프로젝트 루트 경로
219
+ * @param replaceDeps - sd.config.ts의 replaceDeps 설정
220
+ * @returns entries와 dispose 함수
221
+ */
222
+ export async function watchReplaceDeps(
223
+ projectRoot: string,
224
+ replaceDeps: Record<string, string>,
225
+ ): Promise<WatchReplaceDepResult> {
226
+ const logger = consola.withTag("sd:cli:replace-deps:watch");
227
+ const entries: ReplaceDepEntry[] = [];
228
+
229
+ // 1. Workspace 패키지 경로 목록 수집
230
+ const searchRoots = [projectRoot];
231
+
232
+ const workspaceYamlPath = path.join(projectRoot, "pnpm-workspace.yaml");
233
+ try {
234
+ const yamlContent = await fs.promises.readFile(workspaceYamlPath, "utf-8");
235
+ const workspaceGlobs = parseWorkspaceGlobs(yamlContent);
236
+
237
+ for (const pattern of workspaceGlobs) {
238
+ const dirs = await glob(pattern, { cwd: projectRoot, absolute: true });
239
+ searchRoots.push(...dirs);
240
+ }
241
+ } catch {
242
+ // pnpm-workspace.yaml가 없으면 루트만 처리
243
+ }
244
+
245
+ // 2. 각 searchRoot의 node_modules에서 매칭되는 패키지 찾기
246
+ for (const searchRoot of searchRoots) {
247
+ const nodeModulesDir = path.join(searchRoot, "node_modules");
248
+
249
+ try {
250
+ await fs.promises.access(nodeModulesDir);
251
+ } catch {
252
+ continue; // node_modules 없으면 스킵
253
+ }
254
+
255
+ // replaceDeps의 각 glob 패턴으로 node_modules 내 디렉토리 탐색
256
+ const targetNames: string[] = [];
257
+ for (const pattern of Object.keys(replaceDeps)) {
258
+ const matches = await glob(pattern, { cwd: nodeModulesDir });
259
+ targetNames.push(...matches);
260
+ }
261
+
262
+ if (targetNames.length === 0) continue;
263
+
264
+ // 패턴 매칭 및 경로 해석
265
+ const matchedEntries = resolveReplaceDepEntries(replaceDeps, targetNames);
266
+
267
+ // 3. entry 정보 수집 (symlink 해석 포함)
268
+ for (const { targetName, sourcePath } of matchedEntries) {
269
+ const targetPath = path.join(nodeModulesDir, targetName);
270
+ const resolvedSourcePath = path.resolve(projectRoot, sourcePath);
271
+
272
+ // 소스 경로 존재 확인
273
+ try {
274
+ await fs.promises.access(resolvedSourcePath);
275
+ } catch {
276
+ logger.warn(`소스 경로가 존재하지 않아 스킵합니다: ${resolvedSourcePath}`);
277
+ continue;
278
+ }
279
+
280
+ // targetPath가 symlink면 realpath로 해석하여 실제 .pnpm 스토어 경로 얻기
281
+ let actualTargetPath = targetPath;
282
+ try {
283
+ const stat = await fs.promises.lstat(targetPath);
284
+ if (stat.isSymbolicLink()) {
285
+ actualTargetPath = await fs.promises.realpath(targetPath);
286
+ }
287
+ } catch {
288
+ // targetPath가 존재하지 않으면 그대로 사용
289
+ }
290
+
291
+ entries.push({
292
+ targetName,
293
+ sourcePath,
294
+ targetPath,
295
+ resolvedSourcePath,
296
+ actualTargetPath,
297
+ });
298
+ }
299
+ }
300
+
301
+ // 4. 소스 디렉토리 watch 설정
302
+ const watchers: FsWatcher[] = [];
303
+ const watchedSources = new Set<string>();
304
+
305
+ for (const entry of entries) {
306
+ if (watchedSources.has(entry.resolvedSourcePath)) continue;
307
+ watchedSources.add(entry.resolvedSourcePath);
308
+
309
+ const watcher = await FsWatcher.watch([entry.resolvedSourcePath], { followSymlinks: false });
310
+ watcher.onChange({ delay: 300 }, async (changeInfos) => {
311
+ for (const { path: changedPath } of changeInfos) {
312
+ // 제외 항목 필터링
313
+ const basename = path.basename(changedPath);
314
+ if (EXCLUDED_NAMES.has(basename)) continue;
315
+
316
+ // 이 소스 경로를 사용하는 모든 entry에 대해 복사
317
+ for (const e of entries) {
318
+ if (e.resolvedSourcePath !== entry.resolvedSourcePath) continue;
319
+
320
+ // 소스 경로 기준 상대 경로 계산
321
+ const relativePath = path.relative(e.resolvedSourcePath, changedPath);
322
+ const destPath = path.join(e.actualTargetPath, relativePath);
323
+
324
+ try {
325
+ // 소스가 존재하는지 확인
326
+ let sourceExists = false;
327
+ try {
328
+ await fs.promises.access(changedPath);
329
+ sourceExists = true;
330
+ } catch {
331
+ // 소스가 삭제됨
332
+ }
333
+
334
+ if (sourceExists) {
335
+ // 소스가 디렉토리인지 파일인지 확인
336
+ const stat = await fs.promises.stat(changedPath);
337
+ if (stat.isDirectory()) {
338
+ await fsMkdir(destPath);
339
+ } else {
340
+ await fsMkdir(path.dirname(destPath));
341
+ await fsCopy(changedPath, destPath, replaceDepsCopyFilter);
342
+ }
343
+ logger.info(`복사: ${relativePath} → ${e.targetName}`);
344
+ } else {
345
+ // 소스가 삭제됨 → 대상도 삭제
346
+ await fsRm(destPath);
347
+ logger.info(`삭제: ${relativePath} (${e.targetName})`);
348
+ }
349
+ } catch (err) {
350
+ logger.error(`복사 실패 (${e.targetName}/${relativePath}): ${err instanceof Error ? err.message : err}`);
351
+ }
352
+ }
353
+ }
354
+ });
355
+
356
+ watchers.push(watcher);
357
+ }
358
+
359
+ return {
360
+ entries,
361
+ dispose: () => {
362
+ for (const watcher of watchers) {
363
+ void watcher.close();
364
+ }
365
+ },
366
+ };
367
+ }
@@ -4,7 +4,7 @@
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
7
- "@simplysm/solid": "~13.0.7",
7
+ "@simplysm/solid": "~13.0.10",
8
8
  {{#if router}}
9
9
  "@solidjs/router": "^0.15.4",
10
10
  {{/if}}
@@ -4,7 +4,7 @@
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
7
- "@simplysm/core-common": "~13.0.7",
8
- "@simplysm/service-server": "~13.0.7"
7
+ "@simplysm/core-common": "~13.0.10",
8
+ "@simplysm/service-server": "~13.0.10"
9
9
  }
10
10
  }