@simplysm/sd-cli 13.0.0-beta.46 → 13.0.0-beta.47

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 (102) hide show
  1. package/README.md +3 -3
  2. package/dist/builders/BaseBuilder.js.map +0 -1
  3. package/dist/builders/DtsBuilder.js.map +0 -1
  4. package/dist/builders/LibraryBuilder.js.map +0 -1
  5. package/dist/builders/index.js.map +0 -1
  6. package/dist/builders/types.js.map +0 -1
  7. package/dist/capacitor/capacitor.js.map +0 -1
  8. package/dist/commands/add-client.js.map +0 -1
  9. package/dist/commands/add-server.js.map +0 -1
  10. package/dist/commands/build.js.map +0 -1
  11. package/dist/commands/dev.js.map +0 -1
  12. package/dist/commands/device.js.map +0 -1
  13. package/dist/commands/init.js.map +0 -1
  14. package/dist/commands/lint.js.map +0 -1
  15. package/dist/commands/publish.js.map +0 -1
  16. package/dist/commands/typecheck.js.map +0 -1
  17. package/dist/commands/watch.js.map +0 -1
  18. package/dist/electron/electron.js.map +0 -1
  19. package/dist/index.js.map +0 -1
  20. package/dist/infra/ResultCollector.js.map +0 -1
  21. package/dist/infra/SignalHandler.js.map +0 -1
  22. package/dist/infra/WorkerManager.js.map +0 -1
  23. package/dist/infra/index.js.map +0 -1
  24. package/dist/orchestrators/WatchOrchestrator.js.map +0 -1
  25. package/dist/orchestrators/index.js.map +0 -1
  26. package/dist/sd-cli.js.map +0 -1
  27. package/dist/sd-config.types.js.map +0 -1
  28. package/dist/utils/build-env.js.map +0 -1
  29. package/dist/utils/config-editor.js.map +0 -1
  30. package/dist/utils/copy-src.js.map +0 -1
  31. package/dist/utils/esbuild-config.d.ts +1 -0
  32. package/dist/utils/esbuild-config.d.ts.map +1 -1
  33. package/dist/utils/esbuild-config.js +2 -1
  34. package/dist/utils/esbuild-config.js.map +1 -2
  35. package/dist/utils/listr-manager.js.map +0 -1
  36. package/dist/utils/output-utils.js.map +0 -1
  37. package/dist/utils/package-utils.js.map +0 -1
  38. package/dist/utils/replace-deps.js.map +0 -1
  39. package/dist/utils/sd-config.js.map +0 -1
  40. package/dist/utils/spawn.js.map +0 -1
  41. package/dist/utils/tailwind-config-deps.js.map +0 -1
  42. package/dist/utils/template.js.map +0 -1
  43. package/dist/utils/tsconfig.js.map +0 -1
  44. package/dist/utils/typecheck-serialization.js.map +0 -1
  45. package/dist/utils/vite-config.js.map +0 -1
  46. package/dist/utils/worker-events.js.map +0 -1
  47. package/dist/workers/client.worker.js.map +0 -1
  48. package/dist/workers/dts.worker.js.map +0 -1
  49. package/dist/workers/library.worker.js.map +0 -1
  50. package/dist/workers/server-runtime.worker.js.map +0 -1
  51. package/dist/workers/server.worker.js.map +0 -1
  52. package/package.json +5 -4
  53. package/src/builders/BaseBuilder.ts +141 -0
  54. package/src/builders/DtsBuilder.ts +138 -0
  55. package/src/builders/LibraryBuilder.ts +161 -0
  56. package/src/builders/index.ts +4 -0
  57. package/src/builders/types.ts +55 -0
  58. package/src/capacitor/capacitor.ts +827 -0
  59. package/src/commands/add-client.ts +135 -0
  60. package/src/commands/add-server.ts +150 -0
  61. package/src/commands/build.ts +475 -0
  62. package/src/commands/dev.ts +602 -0
  63. package/src/commands/device.ts +151 -0
  64. package/src/commands/init.ts +104 -0
  65. package/src/commands/lint.ts +216 -0
  66. package/src/commands/publish.ts +836 -0
  67. package/src/commands/typecheck.ts +329 -0
  68. package/src/commands/watch.ts +38 -0
  69. package/src/electron/electron.ts +329 -0
  70. package/src/index.ts +1 -0
  71. package/src/infra/ResultCollector.ts +81 -0
  72. package/src/infra/SignalHandler.ts +52 -0
  73. package/src/infra/WorkerManager.ts +65 -0
  74. package/src/infra/index.ts +3 -0
  75. package/src/orchestrators/WatchOrchestrator.ts +211 -0
  76. package/src/orchestrators/index.ts +1 -0
  77. package/src/sd-cli.ts +307 -0
  78. package/src/sd-config.types.ts +271 -0
  79. package/src/utils/build-env.ts +12 -0
  80. package/src/utils/config-editor.ts +131 -0
  81. package/src/utils/copy-src.ts +60 -0
  82. package/src/utils/esbuild-config.ts +263 -0
  83. package/src/utils/listr-manager.ts +89 -0
  84. package/src/utils/output-utils.ts +61 -0
  85. package/src/utils/package-utils.ts +63 -0
  86. package/src/utils/replace-deps.ts +163 -0
  87. package/src/utils/sd-config.ts +44 -0
  88. package/src/utils/spawn.ts +79 -0
  89. package/src/utils/tailwind-config-deps.ts +95 -0
  90. package/src/utils/template.ts +51 -0
  91. package/src/utils/tsconfig.ts +111 -0
  92. package/src/utils/typecheck-serialization.ts +82 -0
  93. package/src/utils/vite-config.ts +184 -0
  94. package/src/utils/worker-events.ts +102 -0
  95. package/src/workers/client.worker.ts +236 -0
  96. package/src/workers/dts.worker.ts +416 -0
  97. package/src/workers/library.worker.ts +245 -0
  98. package/src/workers/server-runtime.worker.ts +154 -0
  99. package/src/workers/server.worker.ts +435 -0
  100. package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
  101. package/templates/add-server/__SERVER__/package.json.hbs +2 -2
  102. package/templates/init/package.json.hbs +3 -3
@@ -0,0 +1,151 @@
1
+ import path from "path";
2
+ import { Listr } from "listr2";
3
+ import { fsExists } from "@simplysm/core-node";
4
+ import { consola } from "consola";
5
+ import type { SdConfig, SdClientPackageConfig } from "../sd-config.types";
6
+ import { loadSdConfig } from "../utils/sd-config";
7
+ import { Capacitor } from "../capacitor/capacitor";
8
+ import { Electron } from "../electron/electron";
9
+
10
+ //#region Types
11
+
12
+ /**
13
+ * Device 명령 옵션
14
+ */
15
+ export interface DeviceOptions {
16
+ /** 패키지 이름 (필수) */
17
+ package: string;
18
+ /** 개발 서버 URL (선택, 미지정 시 sd.config.ts의 server 설정 사용) */
19
+ url?: string;
20
+ /** sd.config.ts에 전달할 추가 옵션 */
21
+ options: string[];
22
+ }
23
+
24
+ //#endregion
25
+
26
+ //#region Main
27
+
28
+ /**
29
+ * Android 디바이스에서 앱을 실행한다.
30
+ *
31
+ * - 연결된 Android 디바이스에서 앱 실행
32
+ * - 개발 서버 URL을 WebView에 연결하여 Hot Reload 지원
33
+ *
34
+ * @param options - device 실행 옵션
35
+ * @returns 완료 시 resolve
36
+ */
37
+ export async function runDevice(options: DeviceOptions): Promise<void> {
38
+ const { package: packageName, url } = options;
39
+ const cwd = process.cwd();
40
+ const logger = consola.withTag("sd:cli:device");
41
+
42
+ logger.debug("device 시작", { package: packageName, url });
43
+
44
+ // sd.config.ts 로드
45
+ let sdConfig: SdConfig;
46
+ try {
47
+ sdConfig = await loadSdConfig({ cwd, dev: true, opt: options.options });
48
+ logger.debug("sd.config.ts 로드 완료");
49
+ } catch (err) {
50
+ consola.error(`sd.config.ts 로드 실패: ${err instanceof Error ? err.message : err}`);
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+
55
+ // 패키지 설정 확인
56
+ const pkgConfig = sdConfig.packages[packageName];
57
+ if (pkgConfig == null) {
58
+ consola.error(`패키지를 찾을 수 없습니다: ${packageName}`);
59
+ process.exitCode = 1;
60
+ return;
61
+ }
62
+
63
+ if (pkgConfig.target !== "client") {
64
+ consola.error(`client 타겟 패키지만 지원합니다: ${packageName} (현재: ${pkgConfig.target})`);
65
+ process.exitCode = 1;
66
+ return;
67
+ }
68
+
69
+ const clientConfig: SdClientPackageConfig = pkgConfig;
70
+ const pkgDir = path.join(cwd, "packages", packageName);
71
+
72
+ if (clientConfig.electron != null) {
73
+ // Electron 개발 실행
74
+ let serverUrl = url;
75
+ if (serverUrl == null) {
76
+ if (typeof clientConfig.server === "number") {
77
+ serverUrl = `http://localhost:${clientConfig.server}/${packageName}/`;
78
+ } else {
79
+ consola.error(`--url 옵션이 필요합니다. server가 패키지명으로 설정되어 있습니다: ${clientConfig.server}`);
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+ }
84
+
85
+ logger.debug("개발 서버 URL", { serverUrl });
86
+
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
+
97
+ try {
98
+ await listr.run();
99
+ logger.info("Electron 실행 완료");
100
+ } catch (err) {
101
+ consola.error(`Electron 실행 실패: ${err instanceof Error ? err.message : err}`);
102
+ process.exitCode = 1;
103
+ }
104
+ } else if (clientConfig.capacitor != null) {
105
+ // Capacitor 디바이스 실행 (기존 로직)
106
+ let serverUrl = url;
107
+ if (serverUrl == null) {
108
+ if (typeof clientConfig.server === "number") {
109
+ serverUrl = `http://localhost:${clientConfig.server}/${packageName}/capacitor/`;
110
+ } else {
111
+ consola.error(`--url 옵션이 필요합니다. server가 패키지명으로 설정되어 있습니다: ${clientConfig.server}`);
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+ } else if (!serverUrl.endsWith("/")) {
116
+ serverUrl = `${serverUrl}/${packageName}/capacitor/`;
117
+ }
118
+
119
+ logger.debug("개발 서버 URL", { serverUrl });
120
+
121
+ const capPath = path.join(pkgDir, ".capacitor");
122
+ if (!(await fsExists(capPath))) {
123
+ consola.error(`Capacitor 프로젝트가 초기화되지 않았습니다. 먼저 'pnpm watch ${packageName}'를 실행하세요.`);
124
+ process.exitCode = 1;
125
+ return;
126
+ }
127
+
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
+
138
+ try {
139
+ await listr.run();
140
+ logger.info("디바이스 실행 완료");
141
+ } catch (err) {
142
+ consola.error(`디바이스 실행 실패: ${err instanceof Error ? err.message : err}`);
143
+ process.exitCode = 1;
144
+ }
145
+ } else {
146
+ consola.error(`electron 또는 capacitor 설정이 없습니다: ${packageName}`);
147
+ process.exitCode = 1;
148
+ }
149
+ }
150
+
151
+ //#endregion
@@ -0,0 +1,104 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { consola } from "consola";
4
+ import { renderTemplateDir } from "../utils/template";
5
+ import { spawn } from "../utils/spawn";
6
+
7
+ //#region Types
8
+
9
+ /**
10
+ * Init 명령 옵션
11
+ */
12
+ export interface InitOptions {}
13
+
14
+ //#endregion
15
+
16
+ //#region Utilities
17
+
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
+ /**
32
+ * npm 스코프 이름 유효성 검증
33
+ */
34
+ function isValidScopeName(name: string): boolean {
35
+ return /^[a-z][a-z0-9-]*$/.test(name);
36
+ }
37
+
38
+ //#endregion
39
+
40
+ //#region Main
41
+
42
+ /**
43
+ * 새 Simplysm 프로젝트를 현재 디렉토리에 초기화한다.
44
+ *
45
+ * 1. 디렉토리 비어있는지 확인
46
+ * 2. 프로젝트명(폴더명) 검증
47
+ * 3. Handlebars 템플릿 렌더링
48
+ * 4. pnpm install 실행
49
+ */
50
+ export async function runInit(_options: InitOptions): Promise<void> {
51
+ const cwd = process.cwd();
52
+ const logger = consola.withTag("sd:cli:init");
53
+
54
+ // 1. 디렉토리 비어있는지 확인 (dotfile/dotfolder 제외)
55
+ const entries = fs.readdirSync(cwd).filter((e) => !e.startsWith("."));
56
+ if (entries.length > 0) {
57
+ consola.error("디렉토리가 비어있지 않습니다. 빈 디렉토리에서 실행해주세요.");
58
+ process.exitCode = 1;
59
+ return;
60
+ }
61
+
62
+ // 2. 프로젝트명 검증
63
+ const projectName = path.basename(cwd);
64
+ if (!isValidScopeName(projectName)) {
65
+ consola.error(`프로젝트 이름 "${projectName}"이(가) 유효하지 않습니다. 소문자, 숫자, 하이픈만 사용 가능합니다.`);
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+
70
+ // 3. 템플릿 렌더링
71
+ const pkgRoot = findPackageRoot(import.meta.dirname);
72
+ const templateDir = path.join(pkgRoot, "templates", "init");
73
+
74
+ const context = { projectName };
75
+
76
+ logger.info("프로젝트 파일 생성 중...");
77
+ await renderTemplateDir(templateDir, cwd, context);
78
+ logger.success("프로젝트 파일 생성 완료");
79
+
80
+ // 4. pnpm install
81
+ logger.info("pnpm install 실행 중...");
82
+ await spawn("pnpm", ["install"], { cwd });
83
+ logger.success("pnpm install 완료");
84
+
85
+ // 5. git 초기화
86
+ logger.info("git 저장소 초기화 중...");
87
+ await spawn("git", ["init"], { cwd });
88
+ await spawn("git", ["add", "."], { cwd });
89
+ await spawn("git", ["commit", "-m", "init"], { cwd });
90
+ logger.success("git 저장소 초기화 완료");
91
+
92
+ // 6. 완료 메시지
93
+ consola.box(
94
+ [
95
+ "프로젝트가 생성되었습니다!",
96
+ "",
97
+ "다음 단계:",
98
+ " sd-cli add client 클라이언트 패키지 추가",
99
+ " sd-cli add server 서버 패키지 추가",
100
+ ].join("\n"),
101
+ );
102
+ }
103
+
104
+ //#endregion
@@ -0,0 +1,216 @@
1
+ import { ESLint } from "eslint";
2
+ import { createJiti } from "jiti";
3
+ import path from "path";
4
+ import { Listr } from "listr2";
5
+ import { fsExists, fsGlob, pathFilterByTargets } from "@simplysm/core-node";
6
+ import "@simplysm/core-common";
7
+ import { SdError } from "@simplysm/core-common";
8
+ import { consola, LogLevels } from "consola";
9
+
10
+ //#region Types
11
+
12
+ /**
13
+ * ESLint 실행 옵션
14
+ */
15
+ export interface LintOptions {
16
+ /** 린트할 경로 필터 (예: `packages/core-common`). 빈 배열이면 전체 대상 */
17
+ targets: string[];
18
+ /** 자동 수정 활성화 */
19
+ fix: boolean;
20
+ /** ESLint 규칙별 실행 시간 측정 활성화 (TIMING 환경변수 설정) */
21
+ timing: boolean;
22
+ }
23
+
24
+ /**
25
+ * Listr2 컨텍스트 타입
26
+ */
27
+ interface LintContext {
28
+ ignorePatterns: string[];
29
+ /** 파일 수집 태스크 완료 후 초기화됨 */
30
+ files?: string[];
31
+ /** 린트 대상 파일이 있을 때만 초기화됨 */
32
+ eslint?: ESLint;
33
+ /** 린트 대상 파일이 있을 때만 초기화됨 */
34
+ results?: ESLint.LintResult[];
35
+ }
36
+
37
+ //#endregion
38
+
39
+ //#region Utilities
40
+
41
+ /** ESLint 설정 파일 탐색 순서 */
42
+ const ESLINT_CONFIG_FILES = ["eslint.config.ts", "eslint.config.mts", "eslint.config.js", "eslint.config.mjs"] as const;
43
+
44
+ /**
45
+ * ignores 속성만 가진 ESLint 설정 객체인지 검사하는 타입 가드
46
+ */
47
+ function isGlobalIgnoresConfig(item: unknown): item is { ignores: string[] } {
48
+ if (item == null || typeof item !== "object") return false;
49
+ if (!("ignores" in item)) return false;
50
+ if ("files" in item) return false; // files가 있으면 globalIgnores가 아님
51
+ const ignores = (item as { ignores: unknown }).ignores;
52
+ if (!Array.isArray(ignores)) return false;
53
+ return ignores.every((i) => typeof i === "string");
54
+ }
55
+
56
+ /**
57
+ * eslint.config.ts/js에서 globalIgnores 패턴을 추출한다.
58
+ * files 속성 없이 ignores만 있는 설정 객체가 globalIgnores이다.
59
+ * @internal 테스트용으로 export
60
+ */
61
+ export async function loadIgnorePatterns(cwd: string): Promise<string[]> {
62
+ let configPath: string | undefined;
63
+ for (const f of ESLINT_CONFIG_FILES) {
64
+ const p = path.join(cwd, f);
65
+ if (await fsExists(p)) {
66
+ configPath = p;
67
+ break;
68
+ }
69
+ }
70
+
71
+ if (configPath == null) {
72
+ throw new SdError(`ESLint 설정 파일을 찾을 수 없습니다 (cwd: ${cwd}): ${ESLINT_CONFIG_FILES.join(", ")}`);
73
+ }
74
+
75
+ const jiti = createJiti(import.meta.url);
76
+ const configModule = await jiti.import(configPath);
77
+
78
+ let configs: unknown;
79
+ if (Array.isArray(configModule)) {
80
+ configs = configModule;
81
+ } else if (configModule != null && typeof configModule === "object" && "default" in configModule) {
82
+ configs = configModule.default;
83
+ } else {
84
+ throw new SdError(`ESLint 설정 파일이 올바른 형식이 아닙니다: ${configPath}`);
85
+ }
86
+
87
+ if (!Array.isArray(configs)) {
88
+ throw new SdError(`ESLint 설정이 배열이 아닙니다: ${configPath}`);
89
+ }
90
+
91
+ return configs.filter(isGlobalIgnoresConfig).flatMap((item) => item.ignores);
92
+ }
93
+
94
+ //#endregion
95
+
96
+ //#region Main
97
+
98
+ /**
99
+ * ESLint를 실행한다.
100
+ *
101
+ * - `eslint.config.ts/js`에서 globalIgnores 패턴을 추출하여 glob 필터링에 적용
102
+ * - listr2를 사용하여 진행 상황 표시
103
+ * - 캐시 활성화 (`.cache/eslint.cache`에 저장, 설정 변경 시 자동 무효화)
104
+ * - 에러 발생 시 `process.exitCode = 1` 설정
105
+ *
106
+ * @param options - 린트 실행 옵션
107
+ * @returns 완료 시 resolve. 에러 발견 시 `process.exitCode`를 1로 설정하고 resolve (throw하지 않음)
108
+ */
109
+ export async function runLint(options: LintOptions): Promise<void> {
110
+ const { targets, fix, timing } = options;
111
+ const cwd = process.cwd();
112
+ const logger = consola.withTag("sd:cli:lint");
113
+
114
+ logger.debug("린트 시작", { targets, fix, timing });
115
+
116
+ // TIMING 환경변수 설정
117
+ if (timing) {
118
+ process.env["TIMING"] = "1";
119
+ }
120
+
121
+ const listr = new Listr<LintContext, "default" | "verbose">(
122
+ [
123
+ {
124
+ title: "ESLint 설정 로드",
125
+ task: async (ctx, task) => {
126
+ ctx.ignorePatterns = await loadIgnorePatterns(cwd);
127
+ logger.debug("ignore 패턴 로드 완료", { ignorePatternCount: ctx.ignorePatterns.length });
128
+ task.title = `ESLint 설정 로드 (${ctx.ignorePatterns.length}개 ignore 패턴)`;
129
+ },
130
+ },
131
+ {
132
+ title: "린트 대상 파일 수집",
133
+ task: async (ctx, task) => {
134
+ let files = await fsGlob("**/*.{ts,tsx,js,jsx}", {
135
+ cwd,
136
+ ignore: ctx.ignorePatterns,
137
+ nodir: true,
138
+ absolute: true,
139
+ });
140
+
141
+ // targets가 주어지면 해당 경로의 하위 파일만 필터링
142
+ files = pathFilterByTargets(files, targets, cwd);
143
+ ctx.files = files;
144
+ logger.debug("파일 수집 완료", { fileCount: files.length });
145
+ task.title = `린트 대상 파일 수집 (${files.length}개)`;
146
+
147
+ if (files.length === 0) {
148
+ task.skip("린트할 파일이 없습니다.");
149
+ }
150
+ },
151
+ },
152
+ {
153
+ title: "린트 실행",
154
+ enabled: (ctx) => (ctx.files?.length ?? 0) > 0,
155
+ task: async (ctx, task) => {
156
+ const files = ctx.files!;
157
+ task.title = `린트 실행 중... (${files.length}개 파일)`;
158
+ ctx.eslint = new ESLint({
159
+ cwd,
160
+ fix,
161
+ cache: true,
162
+ cacheLocation: path.join(cwd, ".cache", "eslint.cache"),
163
+ });
164
+ ctx.results = await ctx.eslint.lintFiles(files);
165
+ },
166
+ },
167
+ {
168
+ title: "자동 수정 적용",
169
+ enabled: () => fix,
170
+ skip: (ctx) => (ctx.files?.length ?? 0) === 0 || ctx.results == null,
171
+ task: async (ctx) => {
172
+ if (ctx.results == null) return;
173
+ await ESLint.outputFixes(ctx.results);
174
+ logger.debug("자동 수정 적용 완료");
175
+ },
176
+ },
177
+ ],
178
+ {
179
+ renderer: consola.level >= LogLevels.debug ? "verbose" : "default",
180
+ },
181
+ );
182
+
183
+ const ctx = await listr.run();
184
+
185
+ // 파일이 없거나 린트가 실행되지 않았으면 조기 종료
186
+ if ((ctx.files?.length ?? 0) === 0 || ctx.results == null || ctx.eslint == null) {
187
+ logger.info("린트할 파일 없음");
188
+ return;
189
+ }
190
+
191
+ // 결과 집계
192
+ const errorCount = ctx.results.sum((r) => r.errorCount);
193
+ const warningCount = ctx.results.sum((r) => r.warningCount);
194
+
195
+ if (errorCount > 0) {
196
+ logger.error("린트 에러 발생", { errorCount, warningCount });
197
+ } else if (warningCount > 0) {
198
+ logger.info("린트 완료 (경고 있음)", { errorCount, warningCount });
199
+ } else {
200
+ logger.info("린트 완료", { errorCount, warningCount });
201
+ }
202
+
203
+ // 포맷터 출력
204
+ const formatter = await ctx.eslint.loadFormatter("stylish");
205
+ const resultText = await formatter.format(ctx.results);
206
+ if (resultText) {
207
+ process.stdout.write(resultText);
208
+ }
209
+
210
+ // 에러 있으면 exit code 1
211
+ if (errorCount > 0) {
212
+ process.exitCode = 1;
213
+ }
214
+ }
215
+
216
+ //#endregion