project-auto-wizard 0.1.12 → 0.1.14
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/README.md +7 -5
- package/package.json +1 -1
- package/src/cli/args.js +6 -1
- package/src/cli/help.js +2 -2
- package/src/commands/interactive.js +2 -2
- package/src/context.js +7 -0
- package/src/core/copy/workflows.js +16 -2
- package/src/index.js +12 -11
- package/src/ui/prompts.js +1 -1
- package/src/ui/summary.js +14 -20
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npx project-auto-wizard
|
|
|
16
16
|
[](package.json)
|
|
17
17
|
|
|
18
18
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
19
|
-
## 최신 버전 : v0.1.
|
|
19
|
+
## 최신 버전 : v0.1.13 (2026-08-04)
|
|
20
20
|
|
|
21
21
|
[전체 버전 기록 보기](CHANGELOG.md)
|
|
22
22
|
|
|
@@ -46,6 +46,7 @@ npx project-auto-wizard --mode full --force --type spring,react # CI에서 비
|
|
|
46
46
|
|
|
47
47
|
- **멀티타입**: `--type spring,react,python` — 한 레포에 여러 타입 공존
|
|
48
48
|
- **모노레포**: `--paths "flutter=app,react=client"` — 타입별 서브폴더 지정 (마커 파일 자동 감지)
|
|
49
|
+
- `spring`/`flutter`/`react`/`next`/`python` 5개 타입은 아래 "타입별 워크플로우 구성"처럼 전용 CI/CD가 설치됩니다. `node`/`react-native`/`react-native-expo`/`basic`은 타입 전용 CI 없이 릴리스 자동화(버전 관리·체인지로그·AI 요약)를 담당하는 공통 워크플로우만 설치됩니다 — 빌드/배포 CI는 직접 추가해서 확장할 수 있습니다.
|
|
49
50
|
|
|
50
51
|
### 질문 문구 커스터마이징
|
|
51
52
|
|
|
@@ -65,13 +66,14 @@ flutter.APP_ARTIFACT_NAME:
|
|
|
65
66
|
`spring`/`flutter`는 아래처럼 단일 CI 이상으로 깊게 구성되어 있습니다:
|
|
66
67
|
|
|
67
68
|
- **flutter**: Android(Firebase/Playstore/Selfhosted/TestAPK 배포), iOS(TestFlight/Test-TestFlight), CI, Lab 트리거까지 8종
|
|
68
|
-
- **spring**: 무중단 배포 2종(Nginx/Traefik) +
|
|
69
|
+
- **spring**: 단일 서버 배포(SIMPLE-CICD, **기본 활성**) + 무중단 배포 2종(Nginx/Traefik, opt-in) + PR 프리뷰 + GitHub Packages publish(항상 설치) + Nexus publish(`--nexus` opt-in)
|
|
70
|
+
- 무중단 배포 워크플로우는 파일은 설치되지만 `push` 트리거가 기본적으로 비활성(`workflow_dispatch`만 활성)입니다. 기본 배포는 SIMPLE-CICD가 담당하며, 무중단 배포로 전환하려면 해당 워크플로우 YAML의 `push.branches` 주석을 해제하고 SIMPLE-CICD의 `push` 트리거는 주석 처리해야 합니다(각 YAML 상단 주석에 안내되어 있습니다).
|
|
69
71
|
- **react/next**: CI와 CI+CD 분리 구성
|
|
70
72
|
- **python**: CI / PR 프리뷰 / SimpleCICD
|
|
71
73
|
|
|
72
74
|
### 되돌리기(`--mode revert`)
|
|
73
75
|
|
|
74
|
-
`npx project-auto-wizard --mode revert`는 payload가 설치한 파일명과 **정확히 일치하는 것만** 제거합니다. 사용자가 직접 만든 워크플로우, `version.yml`, `README.md`, `.gitignore`는 건드리지 않습니다. 설치 시 충돌 처리로 생성된 `.bak`/`.template.yaml` 파생 파일도 함께 정리됩니다.
|
|
76
|
+
`npx project-auto-wizard --mode revert --force`는 payload가 설치한 파일명과 **정확히 일치하는 것만** 제거합니다. 사용자가 직접 만든 워크플로우, `version.yml`, `README.md`, `.gitignore`는 건드리지 않습니다. 설치 시 충돌 처리로 생성된 `.bak`/`.template.yaml` 파생 파일도 함께 정리됩니다.
|
|
75
77
|
|
|
76
78
|
### 완전 삭제(`--mode uninstall`)
|
|
77
79
|
|
|
@@ -137,7 +139,7 @@ npx project-auto-wizard [옵션]
|
|
|
137
139
|
--purge-readme --mode uninstall --force 시 README.md 버전 섹션도 제거
|
|
138
140
|
--purge-gitignore --mode uninstall --force 시 .gitignore 자동 추가 항목도 제거
|
|
139
141
|
--purge-version --mode uninstall --force 시 version.yml도 제거
|
|
140
|
-
--force 전 질문
|
|
142
|
+
--force full/version/workflows/revert 실행에 필수 (전 질문 생략, CI용)
|
|
141
143
|
```
|
|
142
144
|
|
|
143
145
|
## 설치 상태 확인 · 진단 · 미리보기
|
|
@@ -205,7 +207,7 @@ flowchart TB
|
|
|
205
207
|
## 개발
|
|
206
208
|
|
|
207
209
|
```bash
|
|
208
|
-
npm test # node --test + python unittest (node
|
|
210
|
+
npm test # node --test + python unittest (node 222 + py 87)
|
|
209
211
|
npm run test:node
|
|
210
212
|
npm run test:py
|
|
211
213
|
```
|
package/package.json
CHANGED
package/src/cli/args.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// CLI 인자 파싱 (.sh top-level while-case 등가) — template_integrator.sh 842~920.
|
|
2
|
-
import { VALID_TYPES } from "../context.js";
|
|
2
|
+
import { VALID_TYPES, VALID_MODES } from "../context.js";
|
|
3
3
|
|
|
4
4
|
// argv(process.argv.slice(2)) → 파싱 결과. 오류 시 throw(호출부에서 exit 1).
|
|
5
5
|
export function parseArgs(argv) {
|
|
@@ -88,6 +88,11 @@ export function parseArgs(argv) {
|
|
|
88
88
|
throw new CliError(`알 수 없는 옵션: ${a}`);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
if (!VALID_MODES.includes(result.mode)) {
|
|
92
|
+
throw new CliError(
|
|
93
|
+
`지원하지 않는 모드: '${result.mode}'\n지원 모드: interactive full version workflows revert uninstall status doctor`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
91
96
|
return result;
|
|
92
97
|
}
|
|
93
98
|
|
package/src/cli/help.js
CHANGED
|
@@ -19,7 +19,7 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
19
19
|
--nexus / --no-nexus Nexus 라이브러리 publish 워크플로우 포함/제외
|
|
20
20
|
--secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
|
|
21
21
|
--semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
|
|
22
|
-
--force 모든 확인 생략, 비대화형 기본값 사용
|
|
22
|
+
--force full/version/workflows/revert 실행에 필수 (모든 확인 생략, 비대화형 기본값 사용)
|
|
23
23
|
--dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌 (full/version/workflows/revert/uninstall 전체 지원)
|
|
24
24
|
--purge-readme --mode uninstall --force 시 README.md 버전 섹션도 제거
|
|
25
25
|
--purge-gitignore --mode uninstall --force 시 .gitignore 자동 추가 항목도 제거
|
|
@@ -29,7 +29,7 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
29
29
|
|
|
30
30
|
예시:
|
|
31
31
|
npx project-auto-wizard --mode full --force --type spring,react
|
|
32
|
-
npx project-auto-wizard --mode workflows --type flutter --paths "flutter=app"
|
|
32
|
+
npx project-auto-wizard --mode workflows --force --type flutter --paths "flutter=app"
|
|
33
33
|
npx project-auto-wizard --mode status
|
|
34
34
|
npx project-auto-wizard --mode doctor
|
|
35
35
|
npx project-auto-wizard --mode full --force --type node --dry-run
|
|
@@ -240,9 +240,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
240
240
|
// 완료 요약 (.sh print_summary L5438)
|
|
241
241
|
io.summary?.({
|
|
242
242
|
mode, types, version, branches,
|
|
243
|
-
|
|
243
|
+
copiedFiles: result?.workflows?.copiedFiles ?? [],
|
|
244
244
|
gitignoreUpdated: result?.gitignoreUpdated === true,
|
|
245
|
-
}
|
|
245
|
+
});
|
|
246
246
|
io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
|
|
247
247
|
return 0;
|
|
248
248
|
}
|
package/src/context.js
CHANGED
|
@@ -4,6 +4,13 @@ export const VALID_TYPES = [
|
|
|
4
4
|
"react-native", "react-native-expo", "node", "python", "basic",
|
|
5
5
|
];
|
|
6
6
|
|
|
7
|
+
// --mode 화이트리스트 (issue #19) — 알 수 없는 값은 부수효과(브랜치 조회 등) 이전에 즉시 거부해야 한다.
|
|
8
|
+
// purge는 --help/대화형 메뉴에 노출하지 않는 숨김 모드(issue #6)이지만 검증 대상에는 포함한다.
|
|
9
|
+
export const VALID_MODES = [
|
|
10
|
+
"interactive", "full", "version", "workflows",
|
|
11
|
+
"revert", "uninstall", "status", "doctor", "purge",
|
|
12
|
+
];
|
|
13
|
+
|
|
7
14
|
export const DEFAULT_VERSION = "0.0.0"; // 패키지 버전 읽기 실패 시 폴백 (배너용 — breaking 비교엔 안 씀)
|
|
8
15
|
|
|
9
16
|
export function createContext(overrides = {}) {
|
|
@@ -69,6 +69,7 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
69
69
|
const counters = { copied: 0, skipped: 0, templateAdded: 0, optionalCopied: 0, backupAdded: 0 };
|
|
70
70
|
const deployValues = new Map(); // Map<type, Map<key,value>> — deploy 블록용 ask 값
|
|
71
71
|
counters.deployValues = deployValues;
|
|
72
|
+
counters.copiedFiles = []; // 이번 실행에서 실제로 새로 쓰여진 파일명 (issue #19 — printSummary 정확성용)
|
|
72
73
|
const srcText = makeSrcText(context.branches || null);
|
|
73
74
|
// values/useDefaults는 치환 경로에서만 의미 (isUnchanged는 내부에서 useDefaults:true 강제 — 가상 비교 무손상)
|
|
74
75
|
const envOptsFor = (type) => ({ type, projectPath: paths.get(type) || ".", repoName, resolvers, values: envValues, useDefaults: envUseDefaults });
|
|
@@ -89,6 +90,7 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
89
90
|
}
|
|
90
91
|
writeText(dst, body);
|
|
91
92
|
counters.copied++;
|
|
93
|
+
counters.copiedFiles.push(filename);
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
|
|
@@ -108,6 +110,7 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
108
110
|
writeText(dst, srcText(join(secretDir, filename)));
|
|
109
111
|
counters.optionalCopied++;
|
|
110
112
|
counters.copied++;
|
|
113
|
+
counters.copiedFiles.push(filename);
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
116
|
|
|
@@ -125,6 +128,7 @@ function applyDecision(decision, srcDir, workflowsDir, filename, counters, srcTe
|
|
|
125
128
|
writeText(dst, srcText(src));
|
|
126
129
|
counters.copied++;
|
|
127
130
|
counters.backupAdded++;
|
|
131
|
+
counters.copiedFiles.push(filename);
|
|
128
132
|
return;
|
|
129
133
|
}
|
|
130
134
|
if (decision === "template") {
|
|
@@ -132,6 +136,7 @@ function applyDecision(decision, srcDir, workflowsDir, filename, counters, srcTe
|
|
|
132
136
|
const templateName = (filename.endsWith(".yaml") ? filename.slice(0, -".yaml".length) : filename) + ".template.yaml";
|
|
133
137
|
writeText(join(workflowsDir, templateName), srcText(src)); // 기존 .template.yaml 덮어씀(.sh rm -f + cp 등가)
|
|
134
138
|
counters.templateAdded++;
|
|
139
|
+
counters.copiedFiles.push(templateName);
|
|
135
140
|
return;
|
|
136
141
|
}
|
|
137
142
|
counters.skipped++; // 'skip'/미지정/ESC → 기존 유지 (.sh S)·force 기본)
|
|
@@ -185,7 +190,11 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
185
190
|
const { newFiles, unchanged, changed } = classify(typeDir, workflowsDir, envOpts, srcText);
|
|
186
191
|
unchangedNames = unchanged.slice();
|
|
187
192
|
for (const f of unchanged) counters.skipped++;
|
|
188
|
-
for (const f of newFiles) {
|
|
193
|
+
for (const f of newFiles) {
|
|
194
|
+
writeText(join(workflowsDir, f), srcText(join(typeDir, f)));
|
|
195
|
+
counters.copied++;
|
|
196
|
+
counters.copiedFiles.push(f);
|
|
197
|
+
}
|
|
189
198
|
// changed: 결정 Map에 따라 처리 (미지정=skip → 현행 force 동작과 동일)
|
|
190
199
|
for (const f of changed) applyDecision(decisions.get(f), typeDir, workflowsDir, f, counters, srcText);
|
|
191
200
|
}
|
|
@@ -198,7 +207,11 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
198
207
|
} else {
|
|
199
208
|
const { newFiles, unchanged, changed } = classify(serverDeployDir, workflowsDir, envOpts, srcText);
|
|
200
209
|
for (const f of unchanged) counters.skipped++;
|
|
201
|
-
for (const f of newFiles) {
|
|
210
|
+
for (const f of newFiles) {
|
|
211
|
+
writeText(join(workflowsDir, f), srcText(join(serverDeployDir, f)));
|
|
212
|
+
counters.copied++;
|
|
213
|
+
counters.copiedFiles.push(f);
|
|
214
|
+
}
|
|
202
215
|
for (const f of changed) applyDecision(decisions.get(f), serverDeployDir, workflowsDir, f, counters, srcText);
|
|
203
216
|
}
|
|
204
217
|
}
|
|
@@ -218,6 +231,7 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
218
231
|
writeText(dst, body);
|
|
219
232
|
counters.optionalCopied++;
|
|
220
233
|
counters.copied++;
|
|
234
|
+
counters.copiedFiles.push(filename);
|
|
221
235
|
}
|
|
222
236
|
}
|
|
223
237
|
|
package/src/index.js
CHANGED
|
@@ -93,9 +93,10 @@ export async function run(argv, {
|
|
|
93
93
|
|
|
94
94
|
// revert 모드 — payload 유래 파일 제거 (감지·질문 불필요, --force 게이트만)
|
|
95
95
|
if (opts.mode === "revert") {
|
|
96
|
+
// TTY 여부와 무관하게 --force가 없으면 거부한다 (issue #19 — TTY에서 확인 없이 즉시 실행되던 결함 수정).
|
|
96
97
|
// --dry-run은 파일을 쓰지 않으므로 --force 게이트를 우회한다 (status/doctor와 동일한 안전성).
|
|
97
|
-
if (!opts.force && !opts.dryRun
|
|
98
|
-
console.error("
|
|
98
|
+
if (!opts.force && !opts.dryRun) {
|
|
99
|
+
console.error("revert 모드는 --force 없이 실행할 수 없습니다 (확인 절차가 없습니다).");
|
|
99
100
|
return 1;
|
|
100
101
|
}
|
|
101
102
|
if (opts.dryRun) {
|
|
@@ -220,10 +221,11 @@ export async function run(argv, {
|
|
|
220
221
|
printDoctorReport(runDoctor(cwd));
|
|
221
222
|
return 0;
|
|
222
223
|
}
|
|
223
|
-
// 명시
|
|
224
|
+
// 명시 모드(full/version/workflows)인데 --force 없으면 TTY 여부와 무관하게 즉시 거부한다
|
|
225
|
+
// (issue #19 — TTY에서 확인 없이 즉시 설치되던 결함 수정).
|
|
224
226
|
// --dry-run은 파일을 쓰지 않으므로 --force 게이트를 우회한다 (status/doctor와 동일한 안전성).
|
|
225
|
-
if (!opts.force && !opts.dryRun
|
|
226
|
-
console.error("
|
|
227
|
+
if (!opts.force && !opts.dryRun) {
|
|
228
|
+
console.error("--force 없이는 이 모드를 실행할 수 없습니다 (확인 절차가 없습니다).");
|
|
227
229
|
return 1;
|
|
228
230
|
}
|
|
229
231
|
|
|
@@ -265,7 +267,7 @@ export async function run(argv, {
|
|
|
265
267
|
const { now, today } = clock || utcNow();
|
|
266
268
|
|
|
267
269
|
const context = createContext({
|
|
268
|
-
mode: opts.mode, force:
|
|
270
|
+
mode: opts.mode, force: opts.force, types, version, versionCode, branch,
|
|
269
271
|
branches,
|
|
270
272
|
paths,
|
|
271
273
|
// 옵션 워크플로우: CLI 플래그 최우선 → version.yml 저장 옵션(.sh read_template_options 등가) → false
|
|
@@ -295,21 +297,20 @@ export async function run(argv, {
|
|
|
295
297
|
return 0;
|
|
296
298
|
}
|
|
297
299
|
|
|
300
|
+
// opts.mode는 parseArgs()에서 화이트리스트 검증을 통과했고, interactive/revert/purge/uninstall/status/doctor는
|
|
301
|
+
// 전부 위에서 조기 반환했으므로 이 시점엔 full/version/workflows 중 하나로 보장된다(issue #19 — default 분기 제거).
|
|
298
302
|
let result = null;
|
|
299
303
|
switch (opts.mode) {
|
|
300
304
|
case "full": result = runFull(context, payload, cwd); break;
|
|
301
305
|
case "version": result = runVersion(context, payload, cwd); break;
|
|
302
306
|
case "workflows": result = runWorkflows(context, payload, cwd); break;
|
|
303
|
-
default:
|
|
304
|
-
// 알 수 없는 모드 → .sh와 동일하게 복사 0건, 에러 아님
|
|
305
|
-
break;
|
|
306
307
|
}
|
|
307
308
|
|
|
308
309
|
// 완료 요약 (.sh print_summary — CLI 모드에서도 출력)
|
|
309
310
|
printSummary({
|
|
310
311
|
mode: opts.mode, types, version, branches,
|
|
311
|
-
|
|
312
|
+
copiedFiles: result?.workflows?.copiedFiles ?? [],
|
|
312
313
|
gitignoreUpdated: result?.gitignoreUpdated === true,
|
|
313
|
-
}
|
|
314
|
+
});
|
|
314
315
|
return 0;
|
|
315
316
|
}
|
package/src/ui/prompts.js
CHANGED
|
@@ -89,7 +89,7 @@ export function banner(info) { _printBanner(info); }
|
|
|
89
89
|
export function detectionLog(info) { _detLog(info); }
|
|
90
90
|
export function analysisCard(info) { _card(info); }
|
|
91
91
|
export function installKind(info) { _installKind(info); }
|
|
92
|
-
export function summary(ctx
|
|
92
|
+
export function summary(ctx) { _summary(ctx); }
|
|
93
93
|
|
|
94
94
|
// env 계획·경로 해석·충돌 메뉴가 쓰는 저수준 엔진 io (env-plan/paths-resolve의 io 계약)
|
|
95
95
|
export const engineIo = {
|
package/src/ui/summary.js
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
// 완료 요약 출력 (.sh print_summary 등가). 전부 stderr.
|
|
2
|
-
// ctx: { mode, types:[], version,
|
|
3
|
-
import {
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { PATHS, WORKFLOW_PREFIX, WORKFLOW_COMMON_PREFIX } from "../core/paths.js";
|
|
6
|
-
import { listYamlFiles } from "../core/fsutil.js";
|
|
2
|
+
// ctx: { mode, types:[], version, copiedFiles:[], branches?, gitignoreUpdated? }
|
|
3
|
+
import { WORKFLOW_PREFIX, WORKFLOW_COMMON_PREFIX } from "../core/paths.js";
|
|
7
4
|
|
|
8
5
|
const SEPARATOR = "────────────────────────────────────────";
|
|
9
6
|
|
|
10
|
-
export function printSummary(ctx
|
|
11
|
-
const { mode, types = [], version = "",
|
|
7
|
+
export function printSummary(ctx) {
|
|
8
|
+
const { mode, types = [], version = "", copiedFiles = [], branches = null, gitignoreUpdated = false } = ctx || {};
|
|
12
9
|
const err = (s = "") => process.stderr.write(`${s}\n`);
|
|
13
10
|
// 색상은 TTY일 때만 (.sh YELLOW/CYAN/NC 등가)
|
|
14
11
|
const isTty = !!process.stderr.isTTY;
|
|
15
12
|
const YELLOW = isTty ? "\x1b[1;33m" : "";
|
|
16
13
|
const CYAN = isTty ? "\x1b[0;36m" : "";
|
|
17
14
|
const NC = isTty ? "\x1b[0m" : "";
|
|
18
|
-
const workflowsCopied = counters.workflows ?? 0;
|
|
19
15
|
|
|
20
16
|
err("");
|
|
21
17
|
err(SEPARATOR);
|
|
@@ -66,24 +62,22 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
66
62
|
err("");
|
|
67
63
|
err("추가된 워크플로우:");
|
|
68
64
|
|
|
69
|
-
//
|
|
65
|
+
// 실제로 이번 실행에서 복사된 파일만 분류한다 (copyWorkflows()가 반환한 copiedFiles —
|
|
66
|
+
// 디렉터리 재스캔은 재실행 시 skip된 파일까지 "새로 설치됨"으로 보여주는 결함이 있었다, issue #19).
|
|
70
67
|
const commonWorkflows = [];
|
|
71
68
|
const typeWorkflows = [];
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
} else if (typePrefixes.some((p) => filename.startsWith(p))) {
|
|
80
|
-
typeWorkflows.push(filename);
|
|
81
|
-
}
|
|
69
|
+
const typePrefixes = types.map((t) => `${WORKFLOW_PREFIX}-${t.toUpperCase()}-`);
|
|
70
|
+
for (const filename of copiedFiles) {
|
|
71
|
+
if (!filename.startsWith(`${WORKFLOW_PREFIX}-`)) continue; // PROJECT-*만
|
|
72
|
+
if (filename.startsWith(`${WORKFLOW_COMMON_PREFIX}-`)) {
|
|
73
|
+
commonWorkflows.push(filename);
|
|
74
|
+
} else if (typePrefixes.some((p) => filename.startsWith(p))) {
|
|
75
|
+
typeWorkflows.push(filename);
|
|
82
76
|
}
|
|
83
77
|
}
|
|
84
78
|
|
|
85
79
|
if (commonWorkflows.length > 0 || typeWorkflows.length > 0) {
|
|
86
|
-
err(` 📦 새로 설치됨 (${
|
|
80
|
+
err(` 📦 새로 설치됨 (${commonWorkflows.length + typeWorkflows.length}개):`);
|
|
87
81
|
for (const wf of commonWorkflows) err(` 📌 ${wf}`);
|
|
88
82
|
for (const wf of typeWorkflows) err(` 🎯 ${wf}`);
|
|
89
83
|
}
|