@simplysm/sd-cli 13.0.8 → 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
  }
@@ -186,27 +186,18 @@ export async function setupReplaceDeps(projectRoot: string, replaceDeps: Record<
186
186
  try {
187
187
  // targetPath가 symlink면 realpath로 해석하여 실제 .pnpm 스토어 경로 얻기
188
188
  let actualTargetPath = targetPath;
189
- let isSymlink = false;
190
189
  try {
191
190
  const stat = await fs.promises.lstat(targetPath);
192
191
  if (stat.isSymbolicLink()) {
193
192
  actualTargetPath = await fs.promises.realpath(targetPath);
194
- isSymlink = true;
195
193
  }
196
194
  } catch {
197
195
  // targetPath가 존재하지 않으면 그대로 사용
198
196
  }
199
197
 
200
- // 기존 디렉토리 제거
198
+ // actualTargetPath의 기존 내용 제거 후 소스 복사 (symlink는 유지)
201
199
  await fs.promises.rm(actualTargetPath, { recursive: true, force: true });
202
-
203
- // symlink였다면 symlink도 제거
204
- if (isSymlink) {
205
- await fs.promises.rm(targetPath, { recursive: true, force: true });
206
- }
207
-
208
- // 소스를 복사 (node_modules, package.json, .cache, tests 제외)
209
- await fsCopy(resolvedSourcePath, targetPath, replaceDepsCopyFilter);
200
+ await fsCopy(resolvedSourcePath, actualTargetPath, replaceDepsCopyFilter);
210
201
 
211
202
  logger.info(`${targetName} → ${sourcePath}`);
212
203
  } catch (err) {
@@ -4,7 +4,7 @@
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
7
- "@simplysm/solid": "~13.0.8",
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.8",
8
- "@simplysm/service-server": "~13.0.8"
7
+ "@simplysm/core-common": "~13.0.10",
8
+ "@simplysm/service-server": "~13.0.10"
9
9
  }
10
10
  }
@@ -15,9 +15,9 @@
15
15
  "vitest": "vitest"
16
16
  },
17
17
  "devDependencies": {
18
- "@simplysm/sd-cli": "~13.0.8",
19
- "@simplysm/claude": "~13.0.8",
20
- "@simplysm/lint": "~13.0.8",
18
+ "@simplysm/sd-cli": "~13.0.10",
19
+ "@simplysm/claude": "~13.0.10",
20
+ "@simplysm/lint": "~13.0.10",
21
21
  "@types/node": "^20.19.33",
22
22
  "eslint": "^9.39.2",
23
23
  "prettier": "^3.8.1",